mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-09 20:29:10 +02:00
Update the architecture document
This commit is contained in:
+65
-81
@@ -1,7 +1,5 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
<!-- TODO: Mention the new special socket approach for `dispatch()` and `audioMaster()-->
|
|
||||||
|
|
||||||
The project consists of two components: a Linux native VST plugin
|
The project consists of two components: a Linux native VST plugin
|
||||||
(`libyabridge.so`) and a VST host that runs under Wine
|
(`libyabridge.so`) and a VST host that runs under Wine
|
||||||
(`yabridge-host.exe`/`yabridge-host.exe.so`, and
|
(`yabridge-host.exe`/`yabridge-host.exe.so`, and
|
||||||
@@ -42,41 +40,31 @@ as the _Windows VST plugin_. The whole process works as follows:
|
|||||||
|
|
||||||
3. The plugin then sets up several Unix domain socket endpoints to communicate
|
3. The plugin then sets up several Unix domain socket endpoints to communicate
|
||||||
with the Wine VST host somewhere in a temporary directory and starts
|
with the Wine VST host somewhere in a temporary directory and starts
|
||||||
listening on them. We'll use multiple sockets so we can easily handle
|
listening on them. We use multiple sockets so we can easily concurrently
|
||||||
multiple data streams from different threads using blocking synchronous
|
handle multiple data streams from different threads using blocking
|
||||||
operations. This greatly simplifies the way communication works without
|
synchronous operations. This greatly simplifies the way communication works
|
||||||
compromising on latency. The different sockets will be described below. We
|
without compromising on latency. The different sockets are described below.
|
||||||
communicate over Unix domain sockets rather than using shared memory directly
|
|
||||||
because this way we get low latency communication without any manual
|
|
||||||
synchronisation for free, while being able to send messages of arbitrary
|
|
||||||
length without having to split them up first. This is useful for transmitting
|
|
||||||
audio and preset data which can be any arbitrary size.
|
|
||||||
4. The plugin launches the Wine VST host in the detected wine prefix, passing
|
4. The plugin launches the Wine VST host in the detected wine prefix, passing
|
||||||
the name of the `.dll` file it should be loading and the base directory for
|
the name of the `.dll` file it should be loading and the base directory for
|
||||||
the Unix domain sockets that are going to be communciated over as its
|
the Unix domain sockets that are going to be communciated over as its
|
||||||
arguments.
|
arguments. See the [Wine hosts](#wine-hosts) below for more information on
|
||||||
|
the different Wine VST host binaries.
|
||||||
5. The Wine VST host connects to the sockets and communication between the
|
5. The Wine VST host connects to the sockets and communication between the
|
||||||
plugin and the Wine VST host gets set up. The following types of events each
|
plugin and the Wine VST host gets set up. The following types of events are
|
||||||
get their own socket:
|
handled seperately:
|
||||||
|
|
||||||
- Calls from the native VST host to the plugin's `dispatcher()` function.
|
- Calls from the native VST host to the plugin's `dispatcher()` function.
|
||||||
These get forwarded to the Windows VST plugin through the Wine VST host.
|
These get forwarded to the Windows VST plugin through the Wine VST host.
|
||||||
|
|
||||||
- Calls from the native VST host to the plugin's `dispatcher()` function with
|
|
||||||
the `effProcessEvents` opcode. These also get forwarded to the Windows VST
|
|
||||||
plugin through the Wine VST host. This has to be handled separately from
|
|
||||||
all other events because of limitations of the Win32 API. Without doing
|
|
||||||
this the plugin would not be able to receive any MIDI events while the GUI
|
|
||||||
is being resized or a dropdown menu or message box is shown.
|
|
||||||
|
|
||||||
- Host callback calls from the Windows VST plugin through the
|
- Host callback calls from the Windows VST plugin through the
|
||||||
`audioMasterCallback` function. These get forwarded to the native VST host
|
`audioMasterCallback` function. These get forwarded to the native VST host
|
||||||
through the plugin.
|
through the plugin.
|
||||||
|
|
||||||
Both the `dispatcher()` and `audioMasterCallback()` functions are handled
|
Both the `dispatcher()` and `audioMasterCallback()` functions are handled
|
||||||
in the same way, with some minor variations on how payload data gets
|
in the same way with some minor variations on how payload data gets
|
||||||
serialized depending on the opcode of the event being sent. See the section
|
serialized depending on the opcode of the event being sent. See the [event
|
||||||
below this for more details on this procedure.
|
handling section](#event-handling) below this for more details on this
|
||||||
|
procedure.
|
||||||
|
|
||||||
- Calls from the native VST host to the plugin's `getParameter()` and
|
- Calls from the native VST host to the plugin's `getParameter()` and
|
||||||
`setParameter()` functions. Both functions get forwarded to the Windows VST
|
`setParameter()` functions. Both functions get forwarded to the Windows VST
|
||||||
@@ -91,44 +79,16 @@ as the _Windows VST plugin_. The whole process works as follows:
|
|||||||
emulate the behavior of `processReplacing()` instead. Single and double
|
emulate the behavior of `processReplacing()` instead. Single and double
|
||||||
precision audio go over the same socket since the host will only call one
|
precision audio go over the same socket since the host will only call one
|
||||||
or the other, and we just use a variant to determine which one should be
|
or the other, and we just use a variant to determine which one should be
|
||||||
called on the Wine host side.
|
called on the Wine host side. If the host somehow does end up calling the
|
||||||
|
deprecated accumulative `process()` function instead of
|
||||||
|
`processReplacing()`, then we'll emulate `process()` using
|
||||||
|
`processReplacing()`.
|
||||||
|
|
||||||
- And finally there's a separate socket for control messages. At the moment
|
- And finally there's a separate socket for control messages. At the moment
|
||||||
this is only used to transfer the Windows VST plugin's `AEffect` object to
|
this is only used to transfer the Windows VST plugin's `AEffect` object to
|
||||||
the plugin and the current configuration from the plugin to the Wine VST
|
the plugin and the current configuration from the plugin to the Wine VST
|
||||||
host on startup.
|
host on startup.
|
||||||
|
|
||||||
The operations described above involving the host -> plugin `dispatcher()`and
|
|
||||||
plugin -> host `audioMaster()` functions are all handled by first serializing
|
|
||||||
the function parameters and any payload data into a binary format so they can
|
|
||||||
be sent over a socket. The objects used for encoding both the requests and
|
|
||||||
the responses for theses events can be found in `src/common/serialization.h`,
|
|
||||||
and the functions that actually read and write these objects over the sockets
|
|
||||||
are located in `src/common/communication.h`. The actual binary serialization
|
|
||||||
is handled using [bitsery](https://github.com/fraillt/bitsery).
|
|
||||||
|
|
||||||
TODO: Rewrite this after the socket changes are done
|
|
||||||
|
|
||||||
Actually sending and receiving the events happens in the
|
|
||||||
`EventHandler::send_event()` and `EventHandler::receive_events()` functions.
|
|
||||||
When calling either `dispatch()` or `audioMaster()`, the caller will
|
|
||||||
oftentimes either pass along some kind of data structure through the void
|
|
||||||
pointer function argument, or they expect the function's return value to be a
|
|
||||||
pointer to some kind of struct provided by the plugin or host. The behaviour
|
|
||||||
for reading from and writing into these void pointers and returning pointers
|
|
||||||
to objects when needed is encapsulated in the `DispatchDataConverter` and
|
|
||||||
`HostCallbackDataCovnerter` classes for the `dispatcher()` and
|
|
||||||
`audioMaster()` functions respectively. For operations involving the plugin
|
|
||||||
editor there is also some extra glue in `Vst2Bridge::dispatch_wrapper`. On
|
|
||||||
the receiving end of the function calls, the `passthrough_event()` function
|
|
||||||
which calls the callback functions and handles the marshalling between our
|
|
||||||
data types created by the `*DataConverter` classes and the VST API's
|
|
||||||
different pointer types. This behaviour is separated from `receive_event()`
|
|
||||||
so we can handle MIDI events separately. This is needed because a select few
|
|
||||||
plugins only store pointers to the received events rather than copies of the
|
|
||||||
objects. Because of this, the received event data must live at least until
|
|
||||||
the next audio buffer gets processed so it needs to be stored temporarily.
|
|
||||||
|
|
||||||
6. The Wine VST host loads the Windows VST plugin and starts forwarding messages
|
6. The Wine VST host loads the Windows VST plugin and starts forwarding messages
|
||||||
over the sockets described above.
|
over the sockets described above.
|
||||||
7. After the Windows VST plugin has started loading we will forward all values
|
7. After the Windows VST plugin has started loading we will forward all values
|
||||||
@@ -137,32 +97,56 @@ as the _Windows VST plugin_. The whole process works as follows:
|
|||||||
this point the plugin will stop blocking and the initialization process is
|
this point the plugin will stop blocking and the initialization process is
|
||||||
finished.
|
finished.
|
||||||
|
|
||||||
## Plugin groups
|
## Event handling
|
||||||
|
|
||||||
When using plugin groups, the startup and event handling behavior is slightly
|
Event handling for the host -> plugin `dispatcher()`and plugin -> host
|
||||||
different.
|
`audioMaster()` functions work in the same way. The function parameters and any
|
||||||
|
payload data are serialized into a binary format using
|
||||||
|
[bitsery](https://github.com/fraillt/bitsery). The receiving side then
|
||||||
|
unmarshalls the payload data into the representation used by VST2, calls the
|
||||||
|
actual function, and then serializes the results again and sends them back to
|
||||||
|
the caller. The conversions on the sending side are handled by the
|
||||||
|
`*DataConverter` classes, and on the receiving side the `passthrough_event()`
|
||||||
|
function knows how to convert between yabridge's representation types and the
|
||||||
|
types used by VST2.
|
||||||
|
|
||||||
- First of all, instead of directly spawning a Wine process to host the plugin,
|
One special implementation detail about yabridge's event handling is its use of
|
||||||
yabridge will either:
|
sockets. Whenever possible yabridge uses a single long living socket for each of
|
||||||
|
the operations described in the section above. For event handling however it can
|
||||||
|
happen that the host is calling `dispatch()` a second time from another thread
|
||||||
|
while the first call is still pending. Or `audioMaster()` and `dispatch()` can
|
||||||
|
be called in a mutually recursive fashion. In order to be able to handle those
|
||||||
|
situations, yabridge will create additional socket connections as needed. The
|
||||||
|
receiving side listens for incoming connections, and when it accepts a new
|
||||||
|
connection an additional thread will be spawned to handle the incoming request.
|
||||||
|
This allows for fully concurrent event handling without any blocking.
|
||||||
|
|
||||||
- Connect to an existing group host process that matches the plugin's
|
Lastly there are some `dispatch()` calls that will have to be handled on the
|
||||||
combination of group name, Wine prefix, and Windows VST plugin architecture,
|
Wine VST host's main thread. This is because in the Win32 programming model all
|
||||||
and ask it to host the Windows VST plugin.
|
GUI operations have to be done from a single thread, so any `dispatch()` calls
|
||||||
- Spawn a new group process and detach it from the process, then proceed as
|
that potentially use any of those APIs will have to be handled from the same
|
||||||
normal by connecting to that process as described above. When two yabridge
|
thread that's running the Win32 message loop. In
|
||||||
instances are initialized simultaneously and both try to launch a new group
|
`src/wine-host/bridges/vst2.cpp` there are several opcodes marked as unsafe.
|
||||||
process, then the process that manages to listen on the group's socket first
|
When we encounter one of those events, we'll use Boost.Asio's strands to call
|
||||||
will handle both instances.
|
the plugin's `dispatch()` function from within the main IO context which also
|
||||||
|
handles the Win32 message loop. That way we can easily execute all potential GUI
|
||||||
|
code from the same thread.
|
||||||
|
|
||||||
- Events, both Win32 messages and `dispatcher()` events, are handled slightly
|
## Wine hosts
|
||||||
differently when using plugin groups. Because most of the Win32 API cannot be
|
|
||||||
used from multiple threads, all plugin initialization and all event handling
|
Yabridge has four different VST host binaries. There are binaries for hosting a
|
||||||
has to be done from the same thread. To achieve this, yabridge will use a
|
single plugin and binaries for hosting multiple plugins within a plugin group,
|
||||||
slightly modified version of the `dispatcher()` handler that executes the
|
with 32-bit and 64-bit versions of both.
|
||||||
actual events for all plugins within a single Boost.Asio IO context.
|
|
||||||
- Win32 messages are now also handled on a timer within the same IO context so
|
The group host binaries for plugin groups host plugins in the exact same way as
|
||||||
mentioned above. This behavior is different from individually hosted plugins,
|
the regular host binaries, but instead of directly hosting a plugin they instead
|
||||||
where the message loop can simply be run after every event. If any of the
|
start listening on a socket for incoming requests to host a particular plugin.
|
||||||
plugins within the plugin group is in a state that would cause the message
|
When a group host receives a request to host a plugin, it will initialize the
|
||||||
loop to fail, such as when a plugin is in the process of opening its editor
|
plugin from within the main Boost.Asio IO context, and it will then spawn a new
|
||||||
GUI, then the message loop will be skipped temporarily.
|
thread to start handling events. After that everything works the exact same way
|
||||||
|
as individually hosted plugins, and when the plugin exits the thread and all the
|
||||||
|
plugin's resources are cleaned up. Initializing the plugin within the main IO
|
||||||
|
context is important because all operations potentially using GUI or other Win32
|
||||||
|
message loop related operations should be performed from the same thread. When
|
||||||
|
all plugins have exited, the group host process will wait for a few seconds
|
||||||
|
before it also shuts down.
|
||||||
|
|||||||
Reference in New Issue
Block a user