diff --git a/src/common/communication/clap.h b/src/common/communication/clap.h index 17e09830..eb0679eb 100644 --- a/src/common/communication/clap.h +++ b/src/common/communication/clap.h @@ -23,6 +23,73 @@ #include "../serialization/clap.h" #include "common.h" +/** + * Every CLAP plugin instance gets its own audio thread along with host->plugin + * control and a plugin->host callback sockets. This feels like a bit much, but + * some CLAP extensions require plugins to make audio thread callbacks and those + * should not have to wait for other callbacks (or spin up a new thread). + */ +template +class ClapAudioThreadSockets { + public: + /** + * Sets up the audio thread sockets for a specific plugin instance. The + * sockets won't be active until `connect()` gets called. This cannot be + * initialized inline in the `ClapSockets::add_audio_thread_and_listen_*()` + * functions as that would require the sockets to be moved, which is not + * possible they contain atomics. + * + * @param io_context The IO context the sockets should be bound to. Relevant + * when doing asynchronous operations. + * @param endpoint_base_dir The base directory that will be used for the + * Unix domain sockets. + * @param instance_id The CLAP plugin instance ID these sockets belong to. + * @param listen If `true`, start listening on the sockets. Incoming + * connections will be accepted when `connect()` gets called. This should + * be set to `true` on the plugin side, and `false` on the Wine host side. + * + * @see ClapSockets::connect + */ + ClapAudioThreadSockets(asio::io_context& io_context, + const ghc::filesystem::path& endpoint_base_dir, + size_t instance_id, + bool listen) + : control_(io_context, + (endpoint_base_dir / ("host_plugin_audio_thread_control_" + + std::to_string(instance_id) + ".sock")) + .string(), + // The Wine side will end up listening for control messages + !listen), + callback_( + io_context, + (endpoint_base_dir / ("plugin_host_audio_thread_callback_" + + std::to_string(instance_id) + ".sock")) + .string(), + // And the plugin side for callbacks + listen) {} + + void connect() { + control_.connect(); + callback_.connect(); + } + + void close() { + control_.close(); + callback_.close(); + } + + /** + * Used for host->plugin audio thread function calls. + */ + TypedMessageHandler + control_; + /** + * Used for plugin->host audio thread callbacks. + */ + TypedMessageHandler + callback_; +}; + /** * Manages all the sockets used for communicating between the plugin and the * Wine host when hosting a CLAP plugin. @@ -91,35 +158,17 @@ class ClapSockets final : public Sockets { // This map should be empty at this point, but who knows std::lock_guard lock(audio_thread_sockets_mutex_); - for (auto& [instance_id, socket] : audio_thread_sockets_) { - socket.close(); + for (auto& [instance_id, sockets] : audio_thread_sockets_) { + sockets.close(); } } /** - * Connect to the dedicated audio thread socket socket for a plugin - * instance. This should be called on the plugin side after instantiating - * the plugin. - * - * @param instance_id The object instance identifier of the socket. - */ - void add_audio_thread_and_connect(size_t instance_id) { - std::lock_guard lock(audio_thread_sockets_mutex_); - audio_thread_sockets_.try_emplace( - instance_id, io_context_, - (base_dir_ / ("host_plugin_audio_thread_" + - std::to_string(instance_id) + ".sock")) - .string(), - false); - - audio_thread_sockets_.at(instance_id).connect(); - } - - /** - * Create and listen on a dedicated audio thread socket for a plugin object - * instance. The calling thread will block until the socket has been closed. - * This should be called from the Wine plugin host side after instantiating - * the plugin. + * Create and listen on a dedicated audio thread socket for host->plugin + * audio thread messages, and connect to the corresponding socket for + * plugin->host audio thread callbacks. The thread will blocked until the + * socket has been closed. This should be called from the Wine plugin host + * side after instantiating the plugin. * * @param instance_id The object instance identifier of the socket. * @param socket_listening_latch A promise we'll set a value for once the @@ -133,19 +182,23 @@ class ClapSockets final : public Sockets { * in `ClapAudioThreadControlRequest::Payload`. */ template - void add_audio_thread_and_listen(size_t instance_id, - std::promise& socket_listening_latch, - F&& callback) { + void add_audio_thread_and_listen_control( + size_t instance_id, + std::promise& socket_listening_latch, + F&& callback) { { std::lock_guard lock(audio_thread_sockets_mutex_); - audio_thread_sockets_.try_emplace( - instance_id, io_context_, - (base_dir_ / ("host_plugin_audio_thread_" + - std::to_string(instance_id) + ".sock")) - .string(), - true); + // This is called on the Wine side when creating the plugin + // instance. Once the sockets have been created we'll unlock the + // latch and send the result to the native plugin. At that point the + // native plugin will connect to the sockets and everything will + // continue. + audio_thread_sockets_.try_emplace(instance_id, io_context_, + base_dir_, instance_id, false); } + // We're blocking for a connection here, so the latch must be unlocked + // before doing so socket_listening_latch.set_value(); audio_thread_sockets_.at(instance_id).connect(); @@ -153,8 +206,56 @@ class ClapSockets final : public Sockets { // receiving buffers for all calls. This slightly reduces the amount of // allocations in the audio processing loop. audio_thread_sockets_.at(instance_id) - .template receive_messages(std::nullopt, - std::forward(callback)); + .control_.template receive_messages( + std::nullopt, std::forward(callback)); + } + + /** + * Create and listen on a dedicated audio thread socket for plugin->host + * audio thread callbacks, and connect to the corresponding socket for + * host->plugin audio thread messages. The thread will blocked until the + * socket has been closed. This should be called from the native plugin side + * after instantiating the plugin. + * + * @param instance_id The object instance identifier of the socket. + * @param logger The native plugin's logger instance. + * @param socket_listening_latch A promise we'll set a value for once the + * socket is being listened on so we can wait for it. Otherwise it can be + * that the native plugin already tries to connect to the socket before + * Wine plugin host is even listening on it. + * @param cb An overloaded function that can take every type `T` in the + * `ClapAudioThreadCallbackRequest` variant and then returns + * `T::Response`. + * + * @tparam F A function type in the form of `T::Response(T)` for every `T` + * in `ClapAudioThreadCallbackRequest::Payload`. + */ + template + void add_audio_thread_and_listen_callback( + size_t instance_id, + ClapLogger& logger, + std::promise& socket_listening_latch, + F&& callback) { + { + std::lock_guard lock(audio_thread_sockets_mutex_); + audio_thread_sockets_.try_emplace(instance_id, io_context_, + base_dir_, instance_id, true); + } + + // This is called on the native plugin side after the Wine side is + // already listening on the sockets. We'll connect here, and once the + // connection has been made we unlock the latch to finalize the plugin + // instance creation. + audio_thread_sockets_.at(instance_id).connect(); + socket_listening_latch.set_value(); + + // This `true` indicates that we want to reuse our serialization and + // receiving buffers for all calls. This slightly reduces the amount of + // allocations in the audio processing loop. + audio_thread_sockets_.at(instance_id) + .callback_.template receive_messages( + std::pair(logger, false), + std::forward(callback)); } /** @@ -261,25 +362,19 @@ class ClapSockets final : public Sockets { thread_local SerializationBuffer<256> audio_thread_buffer{}; return audio_thread_sockets_.at(instance_id) - .receive_into(object, response_object, logging, - audio_thread_buffer); + .control_.receive_into(object, response_object, logging, + audio_thread_buffer); } asio::io_context& io_context_; /** - * Every plugin instance gets a dedicated audio thread socket. These - * functions are always called in a hot loop, so there should not be any - * waiting or additional thread or socket creation happening there. - * - * TODO: All plugin instances have a socket for re-entrant audio thread - * function calls. This is probably not needed. It's probably useful - * to investgate why this socket was added back to the VST3 audio - * threads. + * Every plugin instance gets dedicated audio thread sockets for plugin + * function calls and callbacks. These functions are always called in a hot + * loop, so there should not be any waiting or additional thread or socket + * creation happening there. */ - std::unordered_map< - size_t, - TypedMessageHandler> + std::unordered_map> audio_thread_sockets_; std::mutex audio_thread_sockets_mutex_; }; diff --git a/src/common/serialization/clap.h b/src/common/serialization/clap.h index b0414caa..c5d0c194 100644 --- a/src/common/serialization/clap.h +++ b/src/common/serialization/clap.h @@ -160,7 +160,29 @@ using ClapMainThreadCallbackRequest = template void serialize(S& s, ClapMainThreadCallbackRequest& payload) { // All of the objects in `ClapMainThreadCallbackRequest` should have their - // own serialization function. + // own serialization function + s.ext(payload, bitsery::ext::InPlaceVariant{}); +} + +/** + * The same as `ClapMainThreadCallbackRequest`, but for callbacks that can be + * made from the audio therad. This uses a separate per-instance socket to avoid + * blocking or spawning up a new thread when multiple plugin instances make + * callbacks at the same time, or when they made simultaneous GUI and audio + * thread callbacks. A request of type `ClapAudioThreadCallbackRequest(T)` + * should send back a `T::Response`. + * + * TODO: I still have absolutely no idea why you enter template deduction hell + * if you remove the `WantsConfiguration` entry. This is not actually + * used. + */ +using ClapAudioThreadCallbackRequest = + std::variant; + +template +void serialize(S& s, ClapAudioThreadCallbackRequest& payload) { + // All of the objects in `ClapAudioThreadCallbackRequest` should have their + // own serialization function s.ext(payload, bitsery::ext::InPlaceVariant{}); } diff --git a/src/plugin/bridges/clap-impls/plugin-proxy.h b/src/plugin/bridges/clap-impls/plugin-proxy.h index 25c4fb75..1e634de6 100644 --- a/src/plugin/bridges/clap-impls/plugin-proxy.h +++ b/src/plugin/bridges/clap-impls/plugin-proxy.h @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include @@ -201,6 +202,13 @@ class clap_plugin_proxy { */ ClapHostExtensions host_extensions_; + /** + * A handler for receiving audio thread callbacks. Set when initializing the + * plugin. This is needed to minimize blocking during those callbacks, as + * certain CLAP extensions allow callbacks on the audio thread. + */ + std::jthread audio_thread_handler_; + private: ClapPluginBridge& bridge_; size_t instance_id_; diff --git a/src/plugin/bridges/clap.cpp b/src/plugin/bridges/clap.cpp index a2e1294e..f07fc814 100644 --- a/src/plugin/bridges/clap.cpp +++ b/src/plugin/bridges/clap.cpp @@ -251,8 +251,48 @@ void ClapPluginBridge::register_plugin_proxy( plugin_proxies_.emplace(instance_id, std::move(plugin_proxy)); // For optimization reaons we use dedicated sockets for functions that will - // be run in the audio processing loop - sockets_.add_audio_thread_and_connect(instance_id); + // be run in the audio processing loop. + // Every plugin instance gets its own audio thread along with sockets for + // host->plugin control messages and plugin->host callbacks + std::promise socket_listening_latch; + plugin_proxies_.at(instance_id) + ->audio_thread_handler_ = std::jthread([&, instance_id]() { + set_realtime_priority(true); + + // XXX: Like with VST2 worker threads, when using plugin groups the + // thread names from different plugins will clash. Not a huge + // deal probably, since duplicate thread names are still more + // useful than no thread names. + const std::string thread_name = "audio-" + std::to_string(instance_id); + pthread_setname_np(pthread_self(), thread_name.c_str()); + + // Certain CLAP extensions allow audio thread callbacks, so we need + // a dedicated per-instance socket for that + sockets_.add_audio_thread_and_listen_callback( + instance_id, logger_, socket_listening_latch, + overload{ + [&](const WantsConfiguration&) -> WantsConfiguration::Response { + // FIXME: Without starting the variant with + // `WantsConfiguration` you enter template deduction + // hell. I haven't been able to figure out why. + return {}; + }, + [&](const clap::ext::tail::host::Changed& request) + -> clap::ext::tail::host::Changed::Response { + // FIXME: + // const auto& [instance, _] = + // get_instance(request.instance_id); + + // return instance.plugin->start_processing( + // instance.plugin.get()); + }, + }); + }); + + // Wait for the new socket to be listening on before continuing. Otherwise + // the native plugin may try to connect to it before our thread is up and + // running. + socket_listening_latch.get_future().wait(); } void ClapPluginBridge::unregister_plugin_proxy(size_t instance_id) { diff --git a/src/wine-host/bridges/clap.cpp b/src/wine-host/bridges/clap.cpp index d8c914c2..8bf6b631 100644 --- a/src/wine-host/bridges/clap.cpp +++ b/src/wine-host/bridges/clap.cpp @@ -605,7 +605,8 @@ void ClapBridge::register_plugin_instance( object_instances_.emplace( instance_id, ClapPluginInstance(plugin, std::move(host_proxy))); - // Every plugin instance gets its own audio thread + // Every plugin instance gets its own audio thread along with sockets for + // host->plugin control messages and plugin->host callbacks std::promise socket_listening_latch; object_instances_.at(instance_id).audio_thread_handler = Win32Thread([&, instance_id]() { @@ -619,7 +620,7 @@ void ClapBridge::register_plugin_instance( "audio-" + std::to_string(instance_id); pthread_setname_np(pthread_self(), thread_name.c_str()); - sockets_.add_audio_thread_and_listen( + sockets_.add_audio_thread_and_listen_control( instance_id, socket_listening_latch, overload{ [&](const clap::plugin::StartProcessing& request)