Add CLAP audio thread callback sockets

The initialization here is a bit funky and happens in lockstep to ensure
both sides are synchronized.
This commit is contained in:
Robbert van der Helm
2022-09-26 16:28:30 +02:00
parent da97885062
commit 0d472dcd58
5 changed files with 220 additions and 54 deletions
+144 -49
View File
@@ -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 <typename Thread>
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<Thread, ClapLogger, ClapAudioThreadControlRequest>
control_;
/**
* Used for plugin->host audio thread callbacks.
*/
TypedMessageHandler<Thread, ClapLogger, ClapAudioThreadCallbackRequest>
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 <typename F>
void add_audio_thread_and_listen(size_t instance_id,
std::promise<void>& socket_listening_latch,
F&& callback) {
void add_audio_thread_and_listen_control(
size_t instance_id,
std::promise<void>& 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<true>(std::nullopt,
std::forward<F>(callback));
.control_.template receive_messages<true>(
std::nullopt, std::forward<F>(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 <typename F>
void add_audio_thread_and_listen_callback(
size_t instance_id,
ClapLogger& logger,
std::promise<void>& 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<true>(
std::pair<ClapLogger&, bool>(logger, false),
std::forward<F>(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<Thread, ClapLogger, ClapAudioThreadControlRequest>>
std::unordered_map<size_t, ClapAudioThreadSockets<Thread>>
audio_thread_sockets_;
std::mutex audio_thread_sockets_mutex_;
};