Merge branch 'feature/plugin-groups'

This commit is contained in:
Robbert van der Helm
2020-05-27 15:47:57 +02:00
28 changed files with 2540 additions and 856 deletions
+7 -8
View File
@@ -14,6 +14,11 @@ on:
release:
types: [created]
defaults:
run:
# This otherwise gets run under dash which does not support brace expansion
shell: bash
jobs:
build-bionic:
name: Build on Ubuntu 18.04
@@ -48,11 +53,8 @@ jobs:
ninja -C build
- name: Create an archive for the binaries
run: |
set -e
mkdir yabridge
# This is ran under dash which does not support brace expansion
cp build/libyabridge.so build/yabridge-host.exe build/yabridge-host.exe.so build/yabridge-host-32.exe build/yabridge-host-32.exe.so yabridge
cp build/libyabridge.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge
cp CHANGELOG.md README.md yabridge
tar -caf "$ARCHIVE_NAME" yabridge
@@ -91,11 +93,8 @@ jobs:
ninja -C build
- name: Create an archive for the binaries
run: |
set -e
mkdir yabridge
# This is ran under dash which does not support brace expansion
cp build/libyabridge.so build/yabridge-host.exe build/yabridge-host.exe.so build/yabridge-host-32.exe build/yabridge-host-32.exe.so yabridge
cp build/libyabridge.so build/yabridge-{host,group}{,-32}.exe{,.so} yabridge
cp CHANGELOG.md README.md yabridge
tar -caf "$ARCHIVE_NAME" yabridge
+17 -1
View File
@@ -8,10 +8,26 @@ Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Added the ability to host multiple plugins in the same Wine process through
_plugin groups_. A plugin group is a user-defined set of plugins that will be
hosted together in the same Wine process. This allows multiple instances of
plugins to share data and communicate with eachother. Examples of plugins that
can benefit from this are Fabfilter Pro-Q 3, MMultiAnalyzer and the iZotope
mixing plugins. See the readme for instructions on how to set this up.
### Changed
- Changed architecture to use one less socket.
- Removed dependency on 32-bit Boost.Filesystem.
### Fixed
- Steal keyboard focus when clicking on the plugin editor window to account for
the new keyboard focus behavior in Bitwig Studio 3.2.
- Fixed large amount of empty lines in the log file when the Wine process closes
unexpectedly.
- Made the plugin and host detection slightly more robust.
## [1.1.4] - 2020-05-12
+58 -129
View File
@@ -13,7 +13,7 @@ easy to debug and maintain.
## Tested with
Yabridge has been verified to work correctly in the following VST hosts using
Wine Staging 5.5 and 5.6:
Wine Staging 5.8:
- Bitwig Studio 3.1 and the beta releases of 3.2
- Carla 2.1
@@ -125,6 +125,61 @@ yabridge is also able to load 32-bit VST plugins. The installation procedure for
automatically detect whether a plugin is 32-bit or 64-bit on startup and it will
handle it accordingly.
### Plugin groups
Some plugins have the ability to communicate with other instances of that same
plugin or with other plugins made by the same manufacturer. This is often used
in mixing plugins to allow different tracks to reference each other without
having to route audio between them. Examples of plugins that do this are
Fabfilter Pro-Q 3, MMultiAnalyzer and the iZotope mixing plugins. For this to
work, all instances of a particular plugin have to be hosted in the same
process.
Yabridge has the concept of _plugin groups_, which are user defined groups of
plugins that will all be hosted in the same process. These plugins groups can be
configured using a `yabridge.toml` file located either in the same directory as
the symlink of or copy to `libyabridge.so`, or in any directories above it. This
file contains case sensitive
[glob](https://www.man7.org/linux/man-pages/man7/glob.7.html) patterns that are
used to match the names of `*.so` files relative to that `yabridge.toml` file.
These patterns can also match an entire directory. For simplicity's sake only
the first `yabridge.toml` file found and only the first glob pattern matched
within that file are considered. An example `yabridge.toml` file looks like
this:
```toml
# ~/.wine/drive_c/Program Files/Steinberg/VstPlugins/yabridge.toml
["FabFilter Pro-Q 3.so"]
group = "fabfilter"
["MeldaProduction/Tools/MMultiAnalyzer.so"]
group = "melda"
# Matches an entire directory and all files inside it. Make sure to not include
# a trailing slash.
["ToneBoosters"]
group = "toneboosters"
["iZotope*/Neutron *"]
group = "izotope"
["iZotope7/Insight 2.so"]
group = "izotope"
# This won't do anything, since the pattern above has already matched this file
["iZotope7/Neutron 2 Mix Tap.so"]
group = "This will be ignored!"
# Don't do this! This matches all plugins in this directory and all of its
# subdirectories, causing all of them to be hosted in a single process. While
# this would increase startup performance considerably, it will also break any
# form of individual plugin sandboxing provided by the host and could
# potentially introduce all kinds of weird issues.
# ["*"]
# group = "all"
```
### Wine prefixes
It is also possible to use yabridge with multiple Wine prefixes. Yabridge will
@@ -188,12 +243,6 @@ Aside from that, these are some known caveats:
- Most recent **iZotope** plugins don't have a functional GUI in a typical out
of the box Wine setup because of missing dependencies. Please let me know if
you know which dependencies are needed for these plugins to render correctly.
- Some plugins, such as **Fabfilter Pro-Q 3**, are able to communicate between
different instances of the same plugin by relying on the fact that they're all
loaded into the same process. Right now this is something that yabridge does
not do as it would break any form of sandboxing, meaning that if one plugin
were to crash, all other plugins would go down with it. If this is something
you need for your workflow, please let me know.
There are also some VST2.X extension features that have not been implemented yet
because I haven't needed them myself. Let me know if you need any of these
@@ -222,6 +271,7 @@ the following dependencies:
The following dependencies are included in the repository as a Meson wrap:
- bitsery
- tomlplusplus
The project can then be compiled as follows:
@@ -239,7 +289,7 @@ It is also possible to compile a host application for yabridge that's compatible
with 32-bit plugins such as old SynthEdit plugins. This will allow yabridge to
act as a bitbirdge, allowing you to run old 32-bit only Windows VST2 plugins in
a modern 64-bit Linux VST host. For this you'll need to have installed the 32
bit version of the XCB library. This can then be set up as follows:
bit versions of the Boost and XCB libraries. This can then be set up as follows:
```shell
# Enable the bitbridge on an existing build
@@ -309,124 +359,3 @@ modifications in `src/plugin/plugin-bridge.cpp`. To enable this, simply run:
```shell
meson configure build --buildtype=debug -Duse-winedbg=true
```
## Architecture
The project consists of two components: a Linux native VST plugin
(`libyabridge.so`) and a VST host that runs under Wine
(`yabridge-host.exe`/`yabridge-host.exe.so`, and
`yabridge-host-32.exe`/`yabridge-host-32.exe.so` if the bitbirdge is enabled).
I'll refer to the copy of or the symlink to `libyabridge.so` as _the plugin_,
the native Linux VST host that's hosting the plugin as _the native VST host_,
the Wine VST host application that's hosting a Windows `.dll` file as _the Wine
VST host_, and the Windows VST plugin that's being loaded in the Wine VST host
as the _Windows VST plugin_. The whole process works as follows:
1. Some copy of or a symlink to `libyabridge.so` gets loaded as a VST plugin in
a Linux VST host. This file should have been renamed to match a Windows VST
plugin `.dll` file in the same directory. For instance, if there's a
`Serum_x64.dll` file you'd like to bridge, then there should be a symlink to
`libyabridge.so` named `Serum_x64.so`.
2. The plugin first attempts to locate and determine:
- The Windows VST plugin `.dll` file that should be loaded.
- The architecture of that VST plugin file. This is done by inspecting the
headers if the `.dll` file.
- The location of the Wine VST host. This will depend on the architecture
detected for the plugin. If the plugin was compiled for the `x86_64`
architecture or the 'Any CPU' target, then we will look for
`yabridge-host.exe`. If the plugin was compiled for the `x86` architecture,
when we'll search for `yabridge-host-32.exe`.
We will first search for this file alongside the actual location of
`libyabridge.so`. This is useful for development, as it allows you to use a
symlink to `libyabridge.so` directly from the build directory causing
yabridge to automatically pick up the right version of the Wine VST host.
If this file cannot be found, then it will fall back to searching through
the search path.
- The Wine prefix the plugin is located in. If the `WINEPREFIX` environment
variable is specified, then that will be used instead.
3. The plugin then sets up a Unix domain socket endpoint to communicate with the
Wine VST host somewhere in a temporary directory and starts listening on it.
I chose to communicate over Unix domain sockets rather than using shared
memory directly because this way you get low latency communication with
without any busy waits or manual synchronisation for free. The added benefit
is that it also makes it possible to send arbitrarily large chunks of data
without having to split it up first. This is useful for transmitting audio
and preset data which may have any arbitrary size.
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 path to the Unix
domain socket that was just created as its arguments.
5. Communication gets set up using multiple sockets over the end point created
previously. This allows us to easily handle multiple data streams from
different threads using blocking read operations for synchronization. Doing
this greatly simplifies the way communication works without compromising on
latency. The following types of events each get their own socket:
- 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.
- 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
`audioMasterCallback` function. These get forwarded to the native VST host
through the plugin.
Both the `dispatcher()` and `audioMasterCallback()` functions are handled
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
below this for more details on this procedure.
- Calls from the native VST host to the plugin's `getParameter()` and
`setParameter()` functions. Both functions get forwarded to the Windows VST
plugin through the Wine VST host using a single socket because they're very
similar and don't need any complicated behaviour.
- Calls from the native VST host to the plugin's `processReplacing()`
function. This function gets forwarded to the Windows VST plugin through
the Wine VST. In the rare event that the plugin does not support
`processReplacing()` and only supports The deprecated commutative
`process()` function, then the Wine VST host will emulate the behavior of
`processReplacing()` instead.
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).
Actually sending and receiving the events happens in the `send_event()` and
`receive_event()` 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
`WineBridge::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
over the sockets described above.
7. After the Windows VST plugin has started loading we will forward all values
from the plugin's `AEffect` struct to the Linux native VST plugin over the
`dispatcher()` socket. This is only done once at startup. After this point
the plugin will stop blocking and has finished loading.
+150
View File
@@ -0,0 +1,150 @@
# Architecture
The project consists of two components: a Linux native VST plugin
(`libyabridge.so`) and a VST host that runs under Wine
(`yabridge-host.exe`/`yabridge-host.exe.so`, and
`yabridge-host-32.exe`/`yabridge-host-32.exe.so` if the bitbirdge is enabled).
I'll refer to the copy of or the symlink to `libyabridge.so` as _the plugin_,
the native Linux VST host that's hosting the plugin as _the native VST host_,
the Wine VST host application that's hosting a Windows `.dll` file as _the Wine
VST host_, and the Windows VST plugin that's being loaded in the Wine VST host
as the _Windows VST plugin_. The whole process works as follows:
1. Some copy of or a symlink to `libyabridge.so` gets loaded as a VST plugin in
a Linux VST host. This file should have been renamed to match a Windows VST
plugin `.dll` file in the same directory. For instance, if there's a
`Serum_x64.dll` file you'd like to bridge, then there should be a symlink to
`libyabridge.so` named `Serum_x64.so`.
2. The plugin first attempts to locate and determine:
- The Windows VST plugin `.dll` file that should be loaded.
- The architecture of that VST plugin file. This is done by inspecting the
headers if the `.dll` file.
- The location of the Wine VST host. This will depend on the architecture
detected for the plugin. If the plugin was compiled for the `x86_64`
architecture or the 'Any CPU' target, then we will look for
`yabridge-host.exe`. If the plugin was compiled for the `x86` architecture,
when we'll search for `yabridge-host-32.exe`.
We will first search for this file alongside the actual location of
`libyabridge.so`. This is useful for development, as it allows you to use a
symlink to `libyabridge.so` directly from the build directory causing
yabridge to automatically pick up the right version of the Wine VST host.
If this file cannot be found, then it will fall back to searching through
the search path.
- The Wine prefix the plugin is located in. If the `WINEPREFIX` environment
variable is specified, then that will be used instead.
3. The plugin then sets up a Unix domain socket endpoint to communicate with the
Wine VST host somewhere in a temporary directory and starts listening on it.
I chose to communicate over Unix domain sockets rather than using shared
memory directly because this way you get low latency communication with
without any busy waits or manual synchronisation for free. The added benefit
is that it also makes it possible to send arbitrarily large chunks of data
without having to split it up first. This is useful for transmitting audio
and preset data which may have any arbitrary size.
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 path to the Unix
domain socket that was just created as its arguments.
5. Communication gets set up using multiple sockets over the end point created
previously. This allows us to easily handle multiple data streams from
different threads using blocking read operations for synchronization. Doing
this greatly simplifies the way communication works without compromising on
latency. The following types of events each get their own socket:
- 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.
- 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
`audioMasterCallback` function. These get forwarded to the native VST host
through the plugin.
Both the `dispatcher()` and `audioMasterCallback()` functions are handled
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
below this for more details on this procedure.
- Calls from the native VST host to the plugin's `getParameter()` and
`setParameter()` functions. Both functions get forwarded to the Windows VST
plugin through the Wine VST host using a single socket because they're very
similar and don't need any complicated behaviour.
- Calls from the native VST host to the plugin's `processReplacing()`
function. This function gets forwarded to the Windows VST plugin through
the Wine VST. In the rare event that the plugin does not support
`processReplacing()` and only supports The deprecated commutative
`process()` function, then the Wine VST host will emulate the behavior of
`processReplacing()` instead.
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).
Actually sending and receiving the events happens in the `send_event()` and
`receive_event()` 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
over the sockets described above.
7. After the Windows VST plugin has started loading we will forward all values
from the plugin's `AEffect` struct to the Linux native VST plugin over the
`dispatcher()` socket. This is only done once at startup. After this point
the plugin will stop blocking and has finished loading.
## Plugin groups
When using plugin groups, the startup and event handling behavior is slightly
different.
- First of all, instead of directly spawning a Wine process to host the plugin,
yabridge will either:
- Connect to an existing group host process that matches the plugin's
combination of group name, Wine prefix, and Windows VST plugin architecture,
and ask it to host the Windows VST plugin.
- Spawn a new group process and detach it from the process, then proceed as
normal by connecting to that process as described above. When two yabridge
instances are initialized simultaneously and both try to launch a new group
process, then the process that manages to listen on the group's socket first
will handle both instances.
- Events, both Win32 messages and `dispatcher()` events, are handled slightly
differently when using plugin groups. Because most of the Win32 API cannot be
used from multiple threads, all plugin initialization and all event handling
has to be done from the same thread. To achieve this, yabridge will use a
slightly modified version of the `dispatcher()` handler that executes the
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
mentioned above. This behavior is different from individually hosted plugins,
where the message loop can simply be run after every event. If any of the
plugins within the plugin group is in a state that would cause the message
loop to fail, such as when a plugin is in the process of opening its editor
GUI, then the message loop will be skipped temporarily.
+100 -18
View File
@@ -15,15 +15,17 @@ if not meson.get_compiler('cpp').compiles(winelib_check)
error('You need to set up a cross compiler, check the README for compilation instructions.')
endif
# Depending on the `use-bitbridge` flag we'll enable building a second 32-bit
# host application that can act as a bit bridge for using 32-bit Windows plugins
# Depending on the `use-bitbridge` flag we'll enable building secondary 32-bit
# host applications that can act as a bit bridge for using 32-bit Windows plugins
# in 64-bit Linux VST hosts. The plugin will determine which host application to
# use based on the `.dll` file it's trying to load.
# This setup is necessary until Meson provides a way to have multiple
# cross-builds for a single build directory:
# https://github.com/mesonbuild/meson/issues/5125
host_name_64bit = 'yabridge-host'
host_name_32bit = 'yabridge-host-32'
individual_host_name_64bit = 'yabridge-host'
individual_host_name_32bit = 'yabridge-host-32'
group_host_name_64bit = 'yabridge-group'
group_host_name_32bit = 'yabridge-group-32'
# This provides an easy way to start the Wine VST host using winedbg since it
# can be quite a pain to set up
@@ -39,13 +41,13 @@ endif
# and the name of the host binary
subdir('src/common/config')
# Statically link against Boost.Filesystem, otherwise it becomes impossible to
# distribute a prebuilt version of yabridge. For the Wine host applications we
# only use the headers only libraries.
# Statically link against Boost.Filesystem, otherwise it would become impossible
# to distribute a prebuilt version of yabridge
boost_dep = dependency('boost', version : '>=1.66', static : true)
boost_filesystem_dep = dependency('boost', version : '>=1.66', modules : ['filesystem'], static : true)
bitsery_dep = subproject('bitsery').get_variable('bitsery_dep')
threads_dep = dependency('threads')
tomlplusplus_dep = subproject('tomlplusplus').get_variable('tomlplusplus_dep')
# The built in threads dependency does not know how to handle winegcc
wine_threads_dep = declare_dependency(link_args : '-lpthread')
xcb_dep = dependency('xcb')
@@ -62,6 +64,7 @@ shared_library(
[
'src/common/logging.cpp',
'src/common/serialization.cpp',
'src/plugin/configuration.cpp',
'src/plugin/plugin.cpp',
'src/plugin/plugin-bridge.cpp',
'src/plugin/utils.cpp',
@@ -69,7 +72,13 @@ shared_library(
],
native : true,
include_directories : include_dir,
dependencies : [boost_dep, boost_filesystem_dep, bitsery_dep, threads_dep],
dependencies : [
boost_dep,
boost_filesystem_dep,
bitsery_dep,
threads_dep,
tomlplusplus_dep
],
cpp_args : compiler_options,
link_args : ['-ldl']
)
@@ -77,20 +86,49 @@ shared_library(
host_sources = [
'src/common/logging.cpp',
'src/common/serialization.cpp',
'src/wine-host/bridges/vst2.cpp',
'src/wine-host/editor.cpp',
'src/wine-host/editor.cpp',
'src/wine-host/vst-host.cpp',
'src/wine-host/wine-bridge.cpp',
'src/wine-host/utils.cpp',
version_header,
]
individual_host_sources = host_sources + ['src/wine-host/individual-host.cpp']
group_host_sources = host_sources + [
'src/wine-host/bridges/group.cpp',
'src/wine-host/group-host.cpp',
]
executable(
host_name_64bit,
host_sources,
individual_host_name_64bit,
individual_host_sources,
native : false,
include_directories : include_dir,
dependencies : [boost_dep, bitsery_dep, wine_threads_dep, xcb_dep],
dependencies : [
boost_dep,
boost_filesystem_dep,
bitsery_dep,
tomlplusplus_dep,
wine_threads_dep,
xcb_dep
],
cpp_args : compiler_options + ['-m64'],
link_args : ['-m64']
)
executable(
group_host_name_64bit,
group_host_sources,
native : false,
include_directories : include_dir,
dependencies : [
boost_dep,
boost_filesystem_dep,
bitsery_dep,
tomlplusplus_dep,
wine_threads_dep,
xcb_dep
],
cpp_args : compiler_options + ['-m64'],
link_args : ['-m64']
)
@@ -98,17 +136,44 @@ executable(
if get_option('use-bitbridge')
message('Bitbridge enabled, configuring a 32-bit host application')
# I honestly have no idea what the correct way to have `find_dependency()` use
# `/usr/lib32` instead of `/usr/lib` is. If anyone does know, please tell me!
# I honestly have no idea what the correct way is to have `dependency()` or
# `compiler.find_dependency()` search for 32-bit versions of libraries when
# cross-compiling. Meson also doesn't seem to respect the default linker
# search path set by the system in `find_library()`. If anyone does know how
# to properly do this, please let me know!
winegcc = meson.get_compiler('cpp', native : false)
boost_filesystem_dep = winegcc.find_library(
'boost_filesystem',
static : true,
dirs : [
# Used by Arch based distros
'/usr/local/lib32',
'/usr/lib32',
# Used by Debian based distros
'/usr/local/lib/i386-linux-gnu',
'/usr/lib/i386-linux-gnu',
# Used by Red Hat based distros, could cause issues though since Meson
# cannot differentiate between the 32-bit version and the regular 64-bit
# version that would normally be in /lib
'/usr/local/lib',
'/usr/lib',
]
)
xcb_dep = winegcc.find_library('xcb')
executable(
host_name_32bit,
host_sources,
individual_host_name_32bit,
individual_host_sources,
native : false,
include_directories : include_dir,
dependencies : [boost_dep, bitsery_dep, wine_threads_dep, xcb_dep],
dependencies : [
boost_dep,
boost_filesystem_dep,
bitsery_dep,
tomlplusplus_dep,
wine_threads_dep,
xcb_dep
],
# FIXME: 32-bit winegcc defines `__stdcall` differently than the 64-bit
# version, and one of the changes is the inclusion of
# `__atribute__((__force_align_arg_pointer__))`. For whetever reason
@@ -119,4 +184,21 @@ if get_option('use-bitbridge')
cpp_args : compiler_options + ['-m32', '-Wno-ignored-attributes'],
link_args : ['-m32']
)
executable(
group_host_name_32bit,
group_host_sources,
native : false,
include_directories : include_dir,
dependencies : [
boost_dep,
boost_filesystem_dep,
bitsery_dep,
tomlplusplus_dep,
wine_threads_dep,
xcb_dep
],
cpp_args : compiler_options + ['-m32', '-Wno-ignored-attributes'],
link_args : ['-m32']
)
endif
+4
View File
@@ -41,6 +41,10 @@ using InputAdapter = bitsery::InputBufferAdapter<B>;
* @param buffer The buffer to write to. This is useful for sending audio and
* chunk data since that can vary in size by a lot.
*
* @warning This operation is not atomic, and calling this function with the
* same socket from multiple threads at once will cause issues with the
* packets arriving out of order.
*
* @relates read_object
*/
template <typename T, typename Socket>
+22 -7
View File
@@ -17,14 +17,29 @@
#pragma once
/**
* The name of the wine host name, e.g. `yabridge-host.exe` for the regular 64
* bit build.
* The name of the Wine VST host application, e.g. `yabridge-host.exe` for the
* regular 64-bit build.
*/
constexpr char yabridge_wine_host_name[] = "@host_binary_64bit@";
constexpr char yabridge_individual_host_name[] =
"@individual_host_binary_64bit@";
/**
* The name of the 32-bit wine host name, e.g. `yabridge-host-32.exe`.` This is
* used as a bitbridge to be able to load legacy 32-bit only Windows plugins
* from a 64-bit Linux host.
* The name of the group host application, e.g. `yabridge-group.exe` for the
* regular 64-bit build.
*/
constexpr char yabridge_wine_host_name_32bit[] = "@host_binary_32bit@";
constexpr char yabridge_group_host_name[] = "@group_host_binary_64bit@";
/**
* The name of the 32-bit Wine VST host application, e.g.
* `yabridge-host-32.exe`.` This is used as a bitbridge to be able to load
* legacy 32-bit only Windows plugins from a 64-bit Linux host.
*/
constexpr char yabridge_individual_host_name_32bit[] =
"@individual_host_binary_32bit@";
/**
* The name of the 32-bit group host application, e.g. `yabridge-group-32.exe`.`
* This is used as a bitbridge to be able to load legacy 32-bit only Windows
* plugins from a 64-bit Linux host.
*/
constexpr char yabridge_group_host_name_32bit[] = "@group_host_binary_32bit@";
+4 -2
View File
@@ -5,8 +5,10 @@ config_header = configure_file(
output : 'config.h',
configuration : configuration_data(
{
'host_binary_32bit': host_name_32bit + '.exe',
'host_binary_64bit': host_name_64bit + '.exe',
'individual_host_binary_32bit': individual_host_name_32bit + '.exe',
'individual_host_binary_64bit': individual_host_name_64bit + '.exe',
'group_host_binary_32bit': group_host_name_32bit + '.exe',
'group_host_binary_64bit': group_host_name_64bit + '.exe',
}
)
)
+8 -2
View File
@@ -72,7 +72,12 @@ Logger Logger::create_from_environment(std::string prefix) {
if (log_file->is_open()) {
return Logger(log_file, verbosity_level, prefix);
} else {
return Logger(std::shared_ptr<std::ostream>(&std::cerr, [](auto) {}),
// For STDERR we sadly can't just use `std::cerr`. In the group process
// we need to capture all output generated by the process itself, and
// the only way to do this is by reopening the STDERR and STDOUT streams
// to a pipe. Luckily `/dev/stderr` stays unaffected, so we can still
// write there without causing infinite loops.
return Logger(std::make_shared<std::ofstream>("/dev/stderr"),
verbosity_level, prefix);
}
}
@@ -301,7 +306,8 @@ bool Logger::should_filter_event(bool is_dispatch, int opcode) {
// called tens of times per second
// TODO: Figure out what opcode 52 is
if ((is_dispatch && (opcode == effEditIdle || opcode == 52)) ||
(!is_dispatch && opcode == audioMasterGetTime)) {
(!is_dispatch && (opcode == audioMasterGetTime ||
opcode == audioMasterGetCurrentProcessLevel))) {
return true;
}
+4
View File
@@ -108,3 +108,7 @@ AEffect& update_aeffect(AEffect& plugin, const AEffect& updated_plugin) {
return plugin;
}
bool GroupRequest::operator==(const GroupRequest& rhs) const {
return plugin_path == rhs.plugin_path && socket_path == rhs.socket_path;
}
+42
View File
@@ -571,3 +571,45 @@ struct AudioBuffers {
s.value4b(sample_frames);
}
};
/**
* An object containing the startup options for hosting a plugin in a plugin
* group process. These are the exact same options that would have been passed
* to `yabridge-host.exe` were the plugin to be hosted individually.
*/
struct GroupRequest {
std::string plugin_path;
std::string socket_path;
bool operator==(const GroupRequest& rhs) const;
template <typename S>
void serialize(S& s) {
s.text1b(plugin_path, 4096);
s.text1b(socket_path, 4096);
}
};
template <>
struct std::hash<GroupRequest> {
std::size_t operator()(GroupRequest const& params) const noexcept {
std::hash<string> hasher{};
return hasher(params.plugin_path) ^ (hasher(params.socket_path) << 1);
}
};
/**
* The response sent back after the group host process receives a `GroupRequest`
* object. This only holds the group process's PID because we need to know if
* the group process crashes while it is initializing the plugin to prevent us
* from waiting indefinitely for the socket to be connected to.
*/
struct GroupResponse {
pid_t pid;
template <typename S>
void serialize(S& s) {
s.value4b(pid);
}
};
+71
View File
@@ -0,0 +1,71 @@
// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "configuration.h"
#include <fnmatch.h>
#include <toml++/toml.h>
#include <fstream>
#include "utils.h"
namespace fs = boost::filesystem;
Configuration::Configuration() {}
Configuration::Configuration(const fs::path& config_path,
const fs::path& yabridge_path)
: Configuration() {
// Will throw a `toml::parsing_error` if the file cannot be parsed. Better
// to throw here rather than failing silently since syntax errors would
// otherwise be impossible to spot.
toml::table table = toml::parse_file(config_path.string());
const fs::path relative_path =
yabridge_path.lexically_relative(config_path.parent_path());
for (const auto& [pattern, value] : table) {
// First try to match the glob pattern, allow matching an entire
// directory for ease of use. If none of the patterns in the file match
// the plugin path then everything will be left at the defaults.
if (fnmatch(pattern.c_str(), relative_path.c_str(),
FNM_PATHNAME | FNM_LEADING_DIR) != 0) {
continue;
}
matched_file = config_path;
matched_pattern = pattern;
// If the table is missing some fields then they will simply be left at
// their defaults
if (toml::table* config = value.as_table(); config != nullptr) {
group = (*config)["group"].value<std::string>();
}
break;
}
}
Configuration Configuration::load_for(const fs::path& yabridge_path) {
// First find the closest `yabridge.tmol` file for the plugin, falling back
// to default configuration settings if it doesn't exist
const std::optional<fs::path> config_file =
find_dominating_file("yabridge.toml", yabridge_path);
if (!config_file.has_value()) {
return Configuration();
}
return Configuration(config_file.value(), yabridge_path);
}
+104
View File
@@ -0,0 +1,104 @@
// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <boost/filesystem.hpp>
#include <optional>
/**
* An object that's used to provide plugin-specific configuration. Right now
* this is only used to declare plugin groups. A plugin group is a set of
* plugins that will be hosted in the same process rather than individually so
* they can share resources. Configuration file loading works as follows:
*
* 1. `Configuration::load_for(path)` gets called with a path to the copy of or
* symlink to `libyabridge.so` that the plugin host has tried to load.
* 2. We start looking for a file named `yabridge.toml` in the same directory as
* that `.so` file, iteratively continuing to search one directory higher
* until we either find the file or we reach the filesystem root.
* 3. If the file is found, then parse it as a TOML file and look for the first
* table whose key is a glob pattern that (partially) matches the relative
* path between the found `yabridge.toml` and the `.so` file. As a rule of
* thumb, if the `find <pattern> -type f` command executed in Bash would list
* the `.so` file, then the following table in `yabridge.tmol` would also
* match the same `.so` file:
*
* ```toml
* ["<patern>"]
* group = "..."
* ```
* 4. If one of these glob patterns could be matched with the relative path of
* the `.so` file then we'll use the settings specified in that section.
* Otherwise the default settings will be used.
*/
class Configuration {
public:
/**
* Create an empty configuration object with default settings.
*/
Configuration();
/**
* Load the configuration for an instance of yabridge from a configuration
* file by matching the plugin's relative path to the glob patterns in that
* configuration file. Will leave the object empty if the plugin cannot be
* matched to any of the patterns. Not meant to be used directly.
*
* @throw toml::parsing_error If the file could not be parsed.
*
* @see Configuration::load_for
*/
Configuration(const boost::filesystem::path& config_path,
const boost::filesystem::path& yabridge_path);
/**
* Load the configuration that belongs to a copy of or symlink to
* `libyabridge.so`. If no configuration file could be found then this will
* return an empty configuration object with default settings.
*
* This function will take any optional compile-time features that have not
* been enabled into account.
*
* @param yabridge_path The path to the .so file that's being loaded.by the
* VST host. This will be used both for the starting location of the
* search and to determine which section in the config file to use.
*
* @return Either a configuration object populated with values from matched
* glob pattern within the found configuration file, or an empty
* configuration object if no configuration file could be found or if the
* plugin could not be matched to any of the glob patterns in the
* configuration file.
*/
static Configuration load_for(const boost::filesystem::path& yabridge_path);
/**
* The name of the plugin group that should be used for the plugin this
* configuration object was created for. If not set, then the plugin should
* be hosted individually instead.
*/
std::optional<std::string> group;
/**
* The path to the configuration file that was parsed.
*/
std::optional<boost::filesystem::path> matched_file;
/**
* The matched glob pattern in the above configuration file.
*/
std::optional<std::string> matched_pattern;
};
+231 -70
View File
@@ -19,11 +19,8 @@
#include <boost/asio/read_until.hpp>
#include <boost/process/env.hpp>
#include <boost/process/io.hpp>
#include <iostream>
#ifdef USE_WINEDBG
#include <boost/process/start_dir.hpp>
#endif
#include <iostream>
// Generated inside of build directory
#include <src/common/config/config.h>
@@ -52,16 +49,21 @@ PluginBridge& get_bridge_instance(const AEffect& plugin) {
return *static_cast<PluginBridge*>(plugin.ptr3);
}
// TODO: It would be nice to have a better way to encapsulate the small
// differences in behavior when using plugin groups, i.e. everywhere where
// we check for `config.group.has_value()`
PluginBridge::PluginBridge(audioMasterCallback host_callback)
: vst_plugin_path(find_vst_plugin()),
: config(Configuration::load_for(get_this_file_location())),
vst_plugin_path(find_vst_plugin()),
vst_plugin_arch(find_vst_architecture(vst_plugin_path)),
vst_host_path(find_vst_host(vst_plugin_arch)),
vst_host_path(find_vst_host(vst_plugin_arch, config.group.has_value())),
// All the fields should be zero initialized because
// `Vst2PluginInstance::vstAudioMasterCallback` from Bitwig's plugin
// bridge will crash otherwise
plugin(),
io_context(),
socket_endpoint(generate_endpoint_name().string()),
socket_endpoint(generate_plugin_endpoint().string()),
socket_acceptor(io_context, socket_endpoint),
host_vst_dispatch(io_context),
host_vst_dispatch_midi_events(io_context),
@@ -73,58 +75,9 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
create_logger_prefix(socket_endpoint.path()))),
wine_version(get_wine_version()),
wine_stdout(io_context),
wine_stderr(io_context),
#ifndef USE_WINEDBG
vst_host(vst_host_path,
// The Wine VST host needs to know which plugin to load
// and which Unix domain socket to connect to
vst_plugin_path,
socket_endpoint.path(),
bp::env = set_wineprefix(),
bp::std_out = wine_stdout,
bp::std_err = wine_stderr)
#else
// This is set up for KDE Plasma. Other desktop environments and window
// managers require some slight modifications to spawn a detached terminal
// emulator.
vst_host("/usr/bin/kstart5",
"konsole",
"--",
"-e",
"winedbg",
"--gdb",
vst_host_path.string() + ".so",
vst_plugin_path.filename(),
socket_endpoint.path(),
bp::env = set_wineprefix(),
// winedbg has no reliable way to escape spaces, so we'll start
// the process in the plugin's directory
bp::start_dir = vst_plugin_path.parent_path())
#endif
{
logger.log("Initializing yabridge version " +
std::string(yabridge_git_version));
logger.log("host: '" + vst_host_path.string() + "'");
logger.log("plugin: '" + vst_plugin_path.string() + "'");
logger.log("socket: '" + socket_endpoint.path() + "'");
logger.log("wine prefix: '" +
find_wineprefix().value_or("<default>").string() + "'");
logger.log("wine version: '" + wine_version + "'");
// Include a list of enabled compile-tiem features, mostly to make debug
// logs more useful
logger.log("");
logger.log("Enabled features:");
#ifdef USE_BITBRIDGE
logger.log("- bitbridge support");
#endif
#ifdef USE_WINEDBG
logger.log("- winedbg");
#endif
#if !(defined(USE_BITBRIDGE) || defined(USE_WINEDBG))
logger.log(" <none>");
#endif
logger.log("");
wine_stderr(io_context) {
log_init_message();
launch_vst_host();
// Print the Wine host's STDOUT and STDERR streams to the log file. This
// should be done before trying to accept the sockets as otherwise we will
@@ -146,10 +99,27 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
if (finished_accepting_sockets) {
return;
}
if (!vst_host.running()) {
throw std::runtime_error(
"The Wine process failed to start. Check the output above "
"for more information.");
// When using regular individually hosted plugins we can simply
// check whether the process is still running, but Boost.Process
// does not allow you to do the same thing for a process that's not
// a child if this process. When using plugin groups we'll have to
// manually check whether the PID returned by the group host process
// is still active.
if (config.group.has_value()) {
if (kill(vst_host_pid, 0) != 0) {
logger.log(
"The group host process has exited unexpectedly. Check "
"the output above for more information.");
std::terminate();
}
} else {
if (!vst_host.running()) {
logger.log(
"The Wine process failed to start. Check the output "
"above for more information.");
std::terminate();
}
}
std::this_thread::sleep_for(1s);
@@ -186,7 +156,7 @@ PluginBridge::PluginBridge(audioMasterCallback host_callback)
try {
while (true) {
// TODO: Think of a nicer way to structure this and the similar
// handler in `WineBridge::handle_dispatch_midi_events`
// handler in `Vst2Bridge::handle_dispatch_midi_events`
receive_event(
vst_host_callback, std::pair<Logger&, bool>(logger, false),
[&](Event& event) {
@@ -494,7 +464,16 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
// loaded into the Wine process crashed during shutdown
logger.log("The plugin crashed during shutdown, ignoring");
}
vst_host.terminate();
// Don't terminate group host processes. They will shut down
// automatically after all plugins have exited.
if (!config.group.has_value()) {
vst_host.terminate();
} else {
// Manually the dispatch socket will cause the host process to
// terminate
host_vst_dispatch.close();
}
// The `stop()` method will cause the IO context to just drop all of
// its work immediately and not throw any exceptions that would have
@@ -503,6 +482,10 @@ intptr_t PluginBridge::dispatch(AEffect* /*plugin*/,
// These threads should now be finished because we've forcefully
// terminated the Wine process, interupting their socket operations
if (group_host_connect_handler.joinable()) {
// This thread is only used when using plugin groups
group_host_connect_handler.join();
}
host_callback_handler.join();
wine_io_handler.join();
@@ -633,17 +616,195 @@ void PluginBridge::async_log_pipe_lines(patched_async_pipe& pipe,
boost::asio::streambuf& buffer,
std::string prefix) {
boost::asio::async_read_until(
pipe, buffer, '\n', [&, prefix](const auto&, size_t) {
pipe, buffer, '\n',
[&, prefix](const boost::system::error_code& error, size_t) {
// When we get an error code then that likely means that the pipe
// has been clsoed and we have reached the end of the file
if (error.failed()) {
return;
}
std::string line;
std::getline(std::istream(&buffer), line);
logger.log(prefix + line);
// Not sure why, but this async read will keep reading a ton of
// empty lines after the Wine process crashes
if (vst_host.running()) {
async_log_pipe_lines(pipe, buffer, prefix);
async_log_pipe_lines(pipe, buffer, prefix);
});
}
void PluginBridge::launch_vst_host() {
const bp::environment host_env = set_wineprefix();
#ifndef USE_WINEDBG
const std::vector<std::string> host_command{vst_host_path.string()};
#else
// This is set up for KDE Plasma. Other desktop environments and window
// managers require some slight modifications to spawn a detached terminal
// emulator.
const std::vector<std::string> host_command{"/usr/bin/kstart5",
"konsole",
"--",
"-e",
"winedbg",
"--gdb",
vst_host_path.string() + ".so"};
#endif
#ifndef USE_WINEDBG
const fs::path plugin_path = vst_plugin_path;
const fs::path starting_dir = fs::current_path();
#else
// winedbg has no reliable way to escape spaces, so we'll start the process
// in the plugin's directory
const fs::path plugin_path = vst_plugin_path.filename();
const fs::path starting_dir = vst_plugin_path.parent_path();
if (plugin_path.string().find(' ') != std::string::npos) {
logger.log("Warning: winedbg does not support paths containing spaces");
}
#endif
const fs::path socket_path = socket_endpoint.path();
if (!config.group.has_value()) {
vst_host =
bp::child(host_command, plugin_path, socket_path,
bp::env = host_env, bp::std_out = wine_stdout,
bp::std_err = wine_stderr, bp::start_dir = starting_dir);
return;
}
// When using plugin groups, we'll first try to connect to an existing group
// host process and ask it to host our plugin. If no such process exists,
// then we'll start a new process. In the event that two yabridge instances
// simultaneously try to start a new group process for the same group, then
// the last process to connect to the socket will terminate gracefully and
// the first process will handle the connections for both yabridge
// instances.
fs::path wine_prefix = host_env.at("WINEPREFIX").to_string();
if (host_env.at("WINEPREFIX").empty()) {
// Fall back to `~/.wine` if this has not been set or detected. This
// would happen if the plugin's .dll file is not inside of a Wine
// prefix. If this happens, then the Wine instance will be launched in
// the default Wine prefix, so we should reflect that here.
wine_prefix = fs::path(host_env.at("HOME").to_string()) / ".wine";
}
const fs::path group_socket_path = generate_group_endpoint(
config.group.value(), wine_prefix, vst_plugin_arch);
try {
// Request the existing group host process to host our plugin, and store
// the PID of that process so we'll know if it has crashed
boost::asio::local::stream_protocol::socket group_socket(io_context);
group_socket.connect(group_socket_path.string());
write_object(group_socket,
GroupRequest{plugin_path.string(), socket_path.string()});
const auto response = read_object<GroupResponse>(group_socket);
vst_host_pid = response.pid;
} catch (const boost::system::system_error&) {
// In case we could not connect to the socket, then we'll start a
// new group host process. This process is detached immediately
// because it should run independently of this yabridge instance as
// it will likely outlive it.
vst_host =
bp::child(host_command, group_socket_path, bp::env = host_env,
bp::std_out = wine_stdout, bp::std_err = wine_stderr,
bp::start_dir = starting_dir);
vst_host_pid = vst_host.id();
vst_host.detach();
// We now want to connect to the socket the in the exact same way as
// above. The only problem is that it may take some time for the
// process to start depending on Wine's current state. We'll defer
// this to a thread so we can finish the rest of the startup in the
// meantime.
group_host_connect_handler = std::thread([&, group_socket_path,
plugin_path, socket_path]() {
using namespace std::literals::chrono_literals;
// TODO: Replace this polling with inotify when encapsulating
// the different host launch behaviors
while (vst_host.running()) {
std::this_thread::sleep_for(20ms);
try {
// This is the exact same connection sequence as above
boost::asio::local::stream_protocol::socket group_socket(
io_context);
group_socket.connect(group_socket_path.string());
write_object(group_socket,
GroupRequest{plugin_path.string(),
socket_path.string()});
const auto response =
read_object<GroupResponse>(group_socket);
// If two group processes started at the same time, than the
// first one will be the one to respond to the host request
vst_host_pid = response.pid;
return;
} catch (const boost::system::system_error&) {
}
}
});
}
}
void PluginBridge::log_init_message() {
std::stringstream init_msg;
init_msg << "Initializing yabridge version " << yabridge_git_version
<< std::endl;
init_msg << "host: '" << vst_host_path.string() << "'" << std::endl;
init_msg << "plugin: '" << vst_plugin_path.string() << "'"
<< std::endl;
init_msg << "socket: '" << socket_endpoint.path() << "'" << std::endl;
init_msg << "wine prefix: '"
<< find_wineprefix().value_or("<default>").string() << "'"
<< std::endl;
init_msg << "wine version: '" << wine_version << "'" << std::endl;
init_msg << std::endl;
// Print the path to the currently loaded configuration file and all
// settings in use. Printing the matched glob pattern could also be useful
// but it'll be very noisy and it's likely going to be clear from the shown
// values anyways.
init_msg << "config from: '"
<< config.matched_file.value_or("<defaults>").string() << "'"
<< std::endl;
init_msg << "hosting mode: '";
if (config.group.has_value()) {
init_msg << "plugin group \"" << config.group.value() << "\"";
} else {
init_msg << "individually";
}
if (vst_plugin_arch == PluginArchitecture::vst_32) {
init_msg << ", 32-bit";
} else {
init_msg << ", 64-bit";
}
init_msg << "'" << std::endl;
init_msg << std::endl;
// Include a list of enabled compile-tiem features, mostly to make debug
// logs more useful
init_msg << "Enabled features:" << std::endl;
#ifdef USE_BITBRIDGE
init_msg << "- bitbridge support" << std::endl;
#endif
#ifdef USE_WINEDBG
init_msg << "- winedbg" << std::endl;
#endif
#if !(defined(USE_BITBRIDGE) || defined(USE_WINEDBG))
init_msg << " <none>" << std::endl;
#endif
init_msg << std::endl;
for (std::string line = ""; std::getline(init_msg, line);) {
logger.log(line);
}
}
// The below functions are proxy functions for the methods defined in
+70 -14
View File
@@ -26,6 +26,7 @@
#include <thread>
#include "../common/logging.h"
#include "configuration.h"
#include "utils.h"
/**
@@ -64,7 +65,7 @@ class PluginBridge {
* Ask the VST plugin to process audio for us. If the plugin somehow does
* not support `processReplacing()` and only supports the old `process()`
* function, then this will be handled implicitely in
* `WineBridge::handle_process_replacing()`.
* `Vst2Bridge::handle_process_replacing()`.
*/
void process_replacing(AEffect* plugin,
float** inputs,
@@ -73,6 +74,14 @@ class PluginBridge {
float get_parameter(AEffect* plugin, int index);
void set_parameter(AEffect* plugin, int index, float value);
/**
* The configuration for this instance of yabridge. Set based on the values
* from a `yabridge.toml`, if it exists.
*
* @see Configuration::load_for
*/
Configuration config;
/**
* The path to the .dll being loaded in the Wine VST host.
*/
@@ -95,19 +104,6 @@ class PluginBridge {
*/
AEffect plugin;
/**
* The VST host can query a plugin for arbitrary binary data such as
* presets. It will expect the plugin to write back a pointer that points to
* that data. This vector is where we store the chunk data for the last
* `effGetChunk` event.
*/
std::vector<uint8_t> chunk_data;
/**
* The VST host will expect to be returned a pointer to a struct that stores
* the dimensions of the editor window.
*/
VstRect editor_rectangle;
private:
/**
* Write output from an async pipe to the log on a line by line basis.
@@ -121,6 +117,24 @@ class PluginBridge {
boost::asio::streambuf& buffer,
std::string prefix = "");
/**
* Launch the Wine VST host to host the plugin. When using plugin groups,
* this will first try to connect to the plugin group's socket (determined
* based on group name, Wine prefix and architecture). If that fails, it
* will launch a new, detached group host process. This will likely outlive
* this plugin instance if multiple instances of yabridge using the same
* plugin group are in use. In the event that two yabridge instances are
* initialized at the same time and both instances spawn their own group
* host process, then the later one will simply terminate gracefully after
* it fails to listen on the socket.
*/
void launch_vst_host();
/**
* Format and log all relevant debug information during initialization.
*/
void log_init_message();
boost::asio::io_context io_context;
boost::asio::local::stream_protocol::endpoint socket_endpoint;
boost::asio::local::stream_protocol::acceptor socket_acceptor;
@@ -184,6 +198,12 @@ class PluginBridge {
*/
audioMasterCallback host_callback_function;
/**
* The logging facility used for this instance of yabridge. See
* `Logger::create_from_env()` for how this is configured.
*
* @see Logger::create_from_env
*/
Logger logger;
/**
@@ -210,8 +230,31 @@ class PluginBridge {
/**
* The Wine process hosting the Windows VST plugin.
*
* @see launch_vst_host
*/
boost::process::child vst_host;
/**
* The PID of the vst host process. Needed for checking whether the group
* host is still active if we are connecting to an already running group
* host instance.
*
* TODO: Remove this after encapsulating the minor differences in individual
* and group host handling
*/
pid_t vst_host_pid;
/**
* A thread that waits for the group host to have started and then ask it to
* host our plugin. This is used to defer the request since it may take a
* little while until the group host process is up and running. This way we
* don't have to delay the rest of the initialization process.
*
* TODO: Remove this after encapsulating the minor differences in individual
* and group host handling
* TODO: Replace this with inotify to prevent delays and to reduce wasting
* resources
*/
std::thread group_host_connect_handler;
/**
* A scratch buffer for sending and receiving data during `process` and
@@ -219,6 +262,19 @@ class PluginBridge {
*/
std::vector<uint8_t> process_buffer;
/**
* The VST host can query a plugin for arbitrary binary data such as
* presets. It will expect the plugin to write back a pointer that points to
* that data. This vector is where we store the chunk data for the last
* `effGetChunk` event.
*/
std::vector<uint8_t> chunk_data;
/**
* The VST host will expect to be returned a pointer to a struct that stores
* the dimensions of the editor window.
*/
VstRect editor_rectangle;
/**
* Sending MIDI events sent to the host by the plugin using
* `audioMasterProcessEvents` function has to be done during the processing
+43 -18
View File
@@ -28,6 +28,8 @@
// Generated inside of build directory
#include <src/common/config/config.h>
#include "configuration.h"
namespace bp = boost::process;
namespace fs = boost::filesystem;
@@ -53,19 +55,13 @@ std::string create_logger_prefix(const fs::path& socket_path) {
}
std::optional<fs::path> find_wineprefix() {
// Try to locate the Wine prefix the plugin's .dll file is located in by
// finding the first parent directory that contains a directory named
// `dosdevices`
fs::path wineprefix_path = find_vst_plugin();
while (wineprefix_path != "") {
if (fs::is_directory(wineprefix_path / "dosdevices")) {
return wineprefix_path;
}
wineprefix_path = wineprefix_path.parent_path();
std::optional<fs::path> dosdevices_dir =
find_dominating_file("dosdevices", find_vst_plugin(), fs::is_directory);
if (!dosdevices_dir.has_value()) {
return std::nullopt;
}
return std::nullopt;
return dosdevices_dir->parent_path();
}
PluginArchitecture find_vst_architecture(fs::path plugin_path) {
@@ -115,10 +111,12 @@ PluginArchitecture find_vst_architecture(fs::path plugin_path) {
throw std::runtime_error(error_msg.str());
}
fs::path find_vst_host(PluginArchitecture plugin_arch) {
auto host_name = yabridge_wine_host_name;
fs::path find_vst_host(PluginArchitecture plugin_arch, bool use_plugin_groups) {
auto host_name = use_plugin_groups ? yabridge_group_host_name
: yabridge_individual_host_name;
if (plugin_arch == PluginArchitecture::vst_32) {
host_name = yabridge_wine_host_name_32bit;
host_name = use_plugin_groups ? yabridge_group_host_name_32bit
: yabridge_individual_host_name_32bit;
}
fs::path host_path =
@@ -166,7 +164,27 @@ fs::path find_vst_plugin() {
"VST plugin .dll file.");
}
fs::path generate_endpoint_name() {
boost::filesystem::path generate_group_endpoint(
std::string group_name,
boost::filesystem::path wine_prefix,
PluginArchitecture architecture) {
std::ostringstream socket_name;
socket_name << "yabridge-group-" << group_name << "-"
<< std::hash<std::string>{}(wine_prefix.string()) << "-";
switch (architecture) {
case PluginArchitecture::vst_32:
socket_name << "x32";
break;
case PluginArchitecture::vst_64:
socket_name << "x64";
break;
}
socket_name << ".sock";
return fs::temp_directory_path() / socket_name.str();
}
fs::path generate_plugin_endpoint() {
const auto plugin_name =
find_vst_plugin().filename().replace_extension("").string();
@@ -204,9 +222,16 @@ fs::path get_this_file_location() {
// on both Ubuntu 18.04 and 20.04, but not on Arch based distros.
// Under Linux a path starting with two slashes is treated the same as
// a path starting with only a single slash, but Wine will refuse to
// load any files when the path starts with two slashes. Prepending
// `/` to a pad coerces theses two slashes into a single slash.
return "/" / boost::dll::this_line_location();
// load any files when the path starts with two slashes. The easiest
// way to work around this if this happens is to just add another
// leading slash and then normalize the path, since three or more
// slashes will be coerced into a single slash.
fs::path this_file = boost::dll::this_line_location();
if (this_file.string().find("//") == 0) {
this_file = ("/" / this_file).lexically_normal();
}
return this_file;
}
std::string get_wine_version() {
+65 -3
View File
@@ -80,11 +80,14 @@ PluginArchitecture find_vst_architecture(boost::filesystem::path);
*
* @param plugin_arch The architecture of the plugin, either 64-bit or 32-bit.
* Used to determine which host application to use, if available.
* @param use_plugin_groups Whether the plugin is using plugin groups and we
* should be looking for the group host instead of the individual plugin host.
*
* @return The a path to the VST host, if found.
* @throw std::runtime_error If the Wine VST host could not be found.
*/
boost::filesystem::path find_vst_host(PluginArchitecture plugin_arch);
boost::filesystem::path find_vst_host(PluginArchitecture plugin_arch,
bool use_plugin_groups);
/**
* Find the VST plugin .dll file that corresponds to this copy of
@@ -101,13 +104,40 @@ boost::filesystem::path find_vst_plugin();
/**
* Locate the Wine prefix this file is located in, if it is inside of a wine
* prefix.
* prefix. This is done by locating the first parent directory that contains a
* directory named `dosdevices`.
*
* @return Either the path to the Wine prefix (containing the `drive_c?`
* directory), or `std::nullopt` if it is not inside of a wine prefix.
*/
std::optional<boost::filesystem::path> find_wineprefix();
/**
* Generate the group socket endpoint name used based on the name of the group,
* the Wine prefix in use and the plugin architecture. The resulting format is
* `/tmp/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock`. In
* this socket name the `wine_prefix_id` is a numerical hash based on the Wine
* prefix in use. This way the same group name can be used for multiple Wine
* prefixes and for both 32 and 64 bit plugins without clashes.
*
* @param group_name The name of the plugin group.
* @param wine_prefix The name of the Wine prefix in use. This should be
* obtained by first calling `set_wineprefix()` to allow the user to override
* this, and then falling back to `$HOME/.wine` if the environment variable is
* unset. Otherwise plugins run from outwide of a Wine prefix will not be
* groupable with those run from within `~/.wine` even though they both run
* under the same prefix.
* @param architecture The architecture the plugin is using, since 64-bit
* processes can't host 32-bit plugins and the other way around.
*
* @return A socket endpoint path that corresponds to the format described
* above.
*/
boost::filesystem::path generate_group_endpoint(
std::string group_name,
boost::filesystem::path wine_prefix,
PluginArchitecture architecture);
/**
* Generate a unique name for the Unix domain socket endpoint based on the VST
* plugin's name. This will also generate the parent directory if it does not
@@ -116,7 +146,7 @@ std::optional<boost::filesystem::path> find_wineprefix();
* @return A path to a not yet existing Unix domain socket endpoint.
* @throw std::runtime_error If no matching .dll file could be found.
*/
boost::filesystem::path generate_endpoint_name();
boost::filesystem::path generate_plugin_endpoint();
/**
* Return a path to this `.so` file. This can be used to find out from where
@@ -140,3 +170,35 @@ std::string get_wine_version();
* using the user's default prefix.
*/
boost::process::environment set_wineprefix();
/**
* Starting from the starting file or directory, go up in the directory
* hierarchy until we find a file named `filename`.
*
* @param filename The name of the file we're looking for. This can also be a
* directory name since directories are also files.
* @param starting_from The directory to start searching in. If this is a file,
* then start searching in the directory the file is located in.
* @param predicate The predicate to use to check if the path matches a file.
* Needed as an easy way to limit the search to directories only since C++17
* does not have any built in coroutines or generators.
*
* @return The path to the *file* found, or `std::nullopt` if the file could not
* be found.
*/
template <typename F = bool(const boost::filesystem::path&)>
std::optional<boost::filesystem::path> find_dominating_file(
const std::string& filename,
boost::filesystem::path starting_dir,
F predicate = boost::filesystem::exists) {
while (starting_dir != "") {
const boost::filesystem::path candidate = starting_dir / filename;
if (predicate(candidate)) {
return candidate;
}
starting_dir = starting_dir.parent_path();
}
return std::nullopt;
}
+373
View File
@@ -0,0 +1,373 @@
// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "group.h"
#include <unistd.h>
#include <boost/asio/read_until.hpp>
#include <boost/process/environment.hpp>
#include <regex>
#include "../../common/communication.h"
// FIXME: `std::filesystem` is broken in wineg++, at least under Wine 5.8. Any
// path operation will thrown an encoding related error
namespace fs = boost::filesystem;
using namespace std::literals::chrono_literals;
/**
* The delay between calls to the event loop at a more than cinematic 30 fps.
*/
constexpr std::chrono::duration event_loop_interval = 1000ms / 30;
/**
* Listen on the specified endpoint if no process is already listening there,
* otherwise throw. This is needed to handle these three situations:
*
* 1. The endpoint does not already exist, and we can simply create an endpoint.
* 2. The endpoint already exists but it is stale and no process is currently
* listening. In this case we can remove the file and start listening.
* 3. The endpoint already exists and another process is currently listening on
* it. In this situation we will throw immediately and we'll terminate this
* process.
*
* If anyone knows a better way to handle this, please let me know!
*
* @throw std::runtime_error If another process is already listening on the
* endpoint.
*/
boost::asio::local::stream_protocol::acceptor create_acceptor_if_inactive(
boost::asio::io_context& io_context,
boost::asio::local::stream_protocol::endpoint& endpoint);
/**
* Create a logger prefix containing the group name based on the socket path.
*/
std::string create_logger_prefix(const fs::path& socket_path);
uint32_t WINAPI handle_plugin_dispatch_proxy(void* param);
/**
* CreateThread() is great and allows you to pass a single value to the
* function, so we'll use this to pass both `this` and the parameters to the
* below thread function so it can do its thing.
*
* @relates handle_plugin_dispatch_proxy
*/
using handle_plugin_dispatch_parameters = std::pair<GroupBridge*, GroupRequest>;
StdIoCapture::StdIoCapture(boost::asio::io_context& io_context,
int file_descriptor)
: pipe(io_context),
target_fd(file_descriptor),
original_fd_copy(dup(file_descriptor)) {
// We'll use the second element of these two file descriptors to reopen
// `file_descriptor`, and the first one to read the captured contents from
::pipe(pipe_fd);
// We've already created a copy of the original file descriptor, so we can
// reopen it using the newly created pipe
dup2(pipe_fd[1], target_fd);
close(pipe_fd[1]);
pipe.assign(pipe_fd[0]);
}
StdIoCapture::~StdIoCapture() {
// Restore the original file descriptor and close the pipe. The other wend
// was already closed in the constructor.
dup2(original_fd_copy, target_fd);
close(original_fd_copy);
close(pipe_fd[0]);
}
GroupBridge::GroupBridge(boost::filesystem::path group_socket_path)
: logger(Logger::create_from_environment(
create_logger_prefix(group_socket_path))),
plugin_context(),
stdio_context(),
stdout_redirect(stdio_context, STDOUT_FILENO),
stderr_redirect(stdio_context, STDERR_FILENO),
group_socket_endpoint(group_socket_path.string()),
group_socket_acceptor(
create_acceptor_if_inactive(plugin_context, group_socket_endpoint)),
events_timer(plugin_context),
shutdown_timer(plugin_context) {
// Write this process's original STDOUT and STDERR streams to the logger
// TODO: This works for output generated by plugins, but not for debug
// messages generated by wineserver. Is it possible to catch those?
async_log_pipe_lines(stdout_redirect.pipe, stdout_buffer, "[STDOUT] ");
async_log_pipe_lines(stderr_redirect.pipe, stderr_buffer, "[STDERR] ");
stdio_handler = std::thread([&]() { stdio_context.run(); });
}
GroupBridge::~GroupBridge() {
stdio_context.stop();
stdio_handler.join();
}
void GroupBridge::handle_plugin_dispatch(const GroupRequest request) {
// At this point the `active_plugins` map will already contain the
// intialized plugin's `Vst2Bridge` instance and this thread's handle
auto& [thread, bridge] = active_plugins.at(request);
// Blocks this thread until the plugin shuts down, handling all events on
// the main IO context
bridge->handle_dispatch_multi(plugin_context);
logger.log("'" + request.plugin_path + "' has exited");
// After the plugin has exited, we'll remove this thread's plugin from the
// active plugins. If no active plugins remain, then we'll terminate the
// process.
std::lock_guard lock(active_plugins_mutex);
active_plugins.erase(request);
// Defer actually shutting down the process to allow for fast plugin
// scanning by allowing plugins to reuse the same group host process
shutdown_timer.expires_after(2s);
shutdown_timer.async_wait([&](const boost::system::error_code& error) {
// A previous timer gets canceled automatically when another plugin
// exits
if (error.failed()) {
return;
}
std::lock_guard lock(active_plugins_mutex);
if (active_plugins.size() == 0) {
logger.log(
"All plugins have exited, shutting down the group process");
plugin_context.stop();
}
});
}
void GroupBridge::handle_incoming_connections() {
accept_requests();
async_handle_events();
logger.log(
"Group host is up and running, now accepting incoming connections");
plugin_context.run();
}
bool GroupBridge::should_skip_message_loop() {
// We do not need additional locking since the call to `AEffect::dispatcher`
// and the actual event handling and message loop handling are performed
// within the IO context and these values thus can't change while another
// the message loop is being running
std::lock_guard lock(active_plugins_mutex);
for (auto& [parameters, value] : active_plugins) {
auto& [thread, bridge] = value;
if (bridge->should_skip_message_loop()) {
return true;
}
}
return false;
}
void GroupBridge::accept_requests() {
group_socket_acceptor.async_accept(
[&](const boost::system::error_code& error,
boost::asio::local::stream_protocol::socket socket) {
std::lock_guard lock(active_plugins_mutex);
// Stop the whole process when the socket gets closed unexpectedly
if (error.failed()) {
logger.log("Error while listening for incoming connections:");
logger.log(error.message());
plugin_context.stop();
}
// Read the parameters, and then host the plugin in this process
// just like if we would be hosting the plugin individually through
// `yabridge-hsot.exe`. We will reply with this process's PID so the
// yabridge plugin will be able to tell if the plugin has caused
// this process to crash during its initialization to prevent
// waiting indefinitely on the sockets to be connected to.
const auto request = read_object<GroupRequest>(socket);
write_object(socket, GroupResponse{boost::this_process::get_id()});
// Collisions in the generated socket names should be very rare, but
// it could in theory happen
assert(active_plugins.find(request) == active_plugins.end());
// The plugin has to be initiated on the IO context's thread because
// this has to be done on the same thread that's handling messages,
// and all window messages have to be handled from the same thread.
logger.log("Received request to host '" + request.plugin_path +
"' using socket '" + request.socket_path + "'");
try {
auto bridge = std::make_unique<Vst2Bridge>(request.plugin_path,
request.socket_path);
logger.log("Finished initializing '" + request.plugin_path +
"'");
// CreateThread() doesn't support multiple arguments and
// requires manualy memory management.
handle_plugin_dispatch_parameters* thread_params =
new std::pair<GroupBridge*, GroupRequest>(this, request);
active_plugins[request] = std::pair(
Win32Thread(handle_plugin_dispatch_proxy, thread_params),
std::move(bridge));
} catch (const std::runtime_error& error) {
logger.log("Error while initializing '" + request.plugin_path +
"':");
logger.log(error.what());
}
accept_requests();
});
}
void GroupBridge::async_handle_events() {
// Try to keep a steady framerate, but add in delays to let other events get
// handled if the GUI message handling somehow takes very long.
events_timer.expires_at(
std::max(events_timer.expiry() + event_loop_interval,
std::chrono::steady_clock::now() + 5ms));
events_timer.async_wait([&](const boost::system::error_code& error) {
if (error.failed()) {
return;
}
{
// Always handle X11 events
std::lock_guard lock(active_plugins_mutex);
for (auto& [parameters, value] : active_plugins) {
auto& [thread, bridge] = value;
bridge->handle_x11_events();
}
}
// Handle Win32 messages unless plugins are in the middle of opening
// their editor
// TODO: Check if those same weird crashes with Serum are happening
// again with these normal threads
if (!should_skip_message_loop()) {
MSG msg;
// Keep the loop responsive by not handling too many events at once
// TODO: For some reason the Melda plugins run into a seemingly
// infinite timer loop for a little while after opening a
// second editor. Without this limit everything will get
// blocked indefinitely. How could this be fixed?
for (int i = 0;
i < 20 && PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE); i++) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
async_handle_events();
});
}
void GroupBridge::async_log_pipe_lines(
boost::asio::posix::stream_descriptor& pipe,
boost::asio::streambuf& buffer,
std::string prefix) {
boost::asio::async_read_until(
pipe, buffer, '\n',
[&, prefix](const boost::system::error_code& error, size_t) {
// When we get an error code then that likely means that the pipe
// has been clsoed and we have reached the end of the file
if (error.failed()) {
return;
}
std::string line;
std::getline(std::istream(&buffer), line);
logger.log(prefix + line);
async_log_pipe_lines(pipe, buffer, prefix);
});
}
boost::asio::local::stream_protocol::acceptor create_acceptor_if_inactive(
boost::asio::io_context& io_context,
boost::asio::local::stream_protocol::endpoint& endpoint) {
// First try to listen on the endpoint normally
try {
return boost::asio::local::stream_protocol::acceptor(io_context,
endpoint);
} catch (const boost::system::system_error& error) {
// If this failed, then either there is a stale socket file or another
// process is already is already listening. In the last case we will
// simply throw so the other process can handle the request.
std::ifstream open_sockets("/proc/net/unix");
std::string endpoint_path = endpoint.path();
for (std::string line; std::getline(open_sockets, line);) {
if (line.size() < endpoint_path.size()) {
continue;
}
std::string file = line.substr(line.size() - endpoint_path.size());
if (file == endpoint_path) {
// Another process is already listening, so we don't have to do
// anything
throw error;
}
}
// At this point we can remove the stale socket and start listening
fs::remove(endpoint_path);
return boost::asio::local::stream_protocol::acceptor(io_context,
endpoint);
}
}
std::string create_logger_prefix(const fs::path& socket_path) {
// The group socket filename will be in the format
// '/tmp/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock',
// where Wine prefix ID is just Wine prefix ran through `std::hash` to
// prevent collisions without needing complicated filenames. We want to
// extract the group name.
std::string socket_name =
socket_path.filename().replace_extension().string();
std::smatch group_match;
std::regex group_regexp("^yabridge-group-(.*)-[^-]+-[^-]+$",
std::regex::ECMAScript);
if (std::regex_match(socket_name, group_match, group_regexp)) {
socket_name = group_match[1].str();
#ifdef __i386__
// Mark 32-bit versions to avoid potential confusion caused by 32-bit
// and regular 64-bit group processes with the same name running
// alongside eachother
socket_name += "-x32";
#endif
}
return "[" + socket_name + "] ";
}
uint32_t WINAPI handle_plugin_dispatch_proxy(void* param) {
// The Win32 API only allows you to pass a void pointer to threads, so we
// need to use manual memory management.
auto thread_params = static_cast<handle_plugin_dispatch_parameters*>(param);
GroupBridge* instance = thread_params->first;
GroupRequest parameters = thread_params->second;
delete thread_params;
instance->handle_plugin_dispatch(parameters);
return 0;
}
+294
View File
@@ -0,0 +1,294 @@
// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include "../boost-fix.h"
#include <boost/asio/posix/stream_descriptor.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/filesystem.hpp>
#include <thread>
#include "vst2.h"
/**
* Encapsulate capturing the STDOUT or STDERR stream by opening a pipe and
* reopening the passed file descriptor as one of the ends of the newly opened
* pipe. This allows all output sent to be read from that pipe. This is needed
* to capture all (debug) output from Wine and the hosted plugins so we can
* prefix it with a timestamp and a group identifier and potentially write it to
* a log file. Since the host application is run independently of the yabridge
* instance that spawned it, this can't simply be done by the caller like we're
* doing for Wine output in individually hosted plugins.
*/
class StdIoCapture {
public:
/**
* Redirect all output sent to a file descriptor (e.g. `STDOUT_FILENO` or
* `STDERR_FILENO`) to a pipe. `StdIoCapture::pipe` can be used to read from
* this pipe.
*
* @param io_context The IO context to create the captured pipe stream on.
* @param file_descriptor The file descriptor to remap.
*/
StdIoCapture(boost::asio::io_context& io_context, int file_descriptor);
StdIoCapture(const StdIoCapture&) = delete;
StdIoCapture& operator=(const StdIoCapture&) = delete;
/**
* On cleanup, close the outgoing file descriptor from the pipe and restore
* the original file descriptor for the captured stream.
*/
~StdIoCapture();
/**
* The pipe endpoint where all output from the original file descriptor gets
* redirected to. This can be read from like any other `Boost.Asio` stream.
*/
boost::asio::posix::stream_descriptor pipe;
private:
/**
* The file descriptor of the stream we're capturing.
*/
const int target_fd;
/**
* A copy of the original file descriptor. Will be used to undo
* the capture when this object gets destroyed.
*/
const int original_fd_copy;
/**
* The two file descriptors generated by the `pipe()` function call.
* `pipe_fd[1]` is used to reopen/capture the passed file descriptor, and
* `pipe_fd[0]` can be used to read the captured output from.
*/
int pipe_fd[2];
};
/**
* A 'plugin group' that listens on a _group socket_ for plugins to host in this
* process. Once the plugin gets loaded into a new thread the actual bridging
* process is identical to individually hosted plugins.
*
* An important detail worth mentioning here is that while this plugin group can
* throw in the constructor when another process is already listening on the
* socket, this should not be treated as an error. When using plugins groups,
* yabridge will try to connect to the group socket on initialization and it
* will launch a new group host process if it can't. If this is done for
* multiple yabridge instances at the same time, then multiple group host
* processes will be launched. Instead of using complicated inter-process
* synchronization, we'll simply allow the processes to fail when another
* process is already listening on the socket.
*/
class GroupBridge {
public:
/**
* Create a plugin group by listening on the provided socket for incoming
* plugin host requests.
*
* @param gruop_socket_path The path to the group socket endpoint. This path
* should be in the form of
* `/tmp/yabridge-group-<group_name>-<wine_prefix_id>-<architecture>.sock`
* where `<wine_prefix_id>` is a numerical hash as explained in the
* `create_logger_prefix()` function in `./group.cpp`.
*
* @throw boost::system::system_error If we can't listen on the socket.
*
* @note Creating an `GroupBridge` instance has the side effect that the
* STDOUT and STDERR streams of the current process will be redirected to
* a pipe so they can be properly written to a log file.
*/
GroupBridge(boost::filesystem::path group_socket_path);
~GroupBridge();
GroupBridge(const GroupBridge&) = delete;
GroupBridge& operator=(const GroupBridge&) = delete;
/**
* Run a plugin's dispatcher and message loop, processing all events on the
* main IO context. The plugin will have already been created in
* `accept_requests` since it has to be initiated inside of the IO context's
* thread. Called by proxy using `handle_plugin_dispatch_proxy()` in
* `./group.cpp` because the Win32 `CreateThread` API only allows passing a
* single pointer to the function and does not allow lambdas.
*
* Once the plugin has exited, this thread will then remove itself from the
* `active_plugins` map. If this causes the vector to become empty, we will
* terminate this process. This check will be delayed by a few seconds to
* prevent having to constantly restart the group process during plugin
* scanning.
*
* @param request Information about the plugin to launch, i.e. the path to
* the plugin and the path of the socket endpoint that will be used for
* communication.
*
* @note In the case that the process starts but no plugin gets initiated,
* then the process will never exit on its own. This should not happen
* though.
*/
void handle_plugin_dispatch(const GroupRequest request);
/**
* Listen for new requests to spawn plugins within this process and handle
* them accordingly. Will terminate once all plugins have exited.
*/
void handle_incoming_connections();
/**
* Returns true if the message loop should not be run at this time. This is
* necessary because hosts will always call either `effEditOpen()` and then
* `effEditGetRect()` or the other way around. If the message loop is
* handled in between these two actions, then some plugins will either
* freeze or sometimes outright crash. Because every plugin has to be run
* from the same thread, this is a simple way to synchronize blocking the
* mesage loop between the different plugin instances.
*/
bool should_skip_message_loop();
private:
/**
* Listen on the group socket for incoming requests to host a new plugin
* within this group process. This will read a `GoupRequest` object
* containing information about the plugin, reply with this process's PID so
* the yabridge instance can tell if the plugin crashed during
* initialization, and it will then try to initialize the plugin. After
* intialization the plugin handling will be handed over to a new thread
* running `handle_plugin_dispatch()`. Because of the way the Win32 API
* works, all plugins have to be initialized from the same thread, and all
* event handling and message loop interaction also has to be done from that
* thread, which is why we initialize the plugin here and use the
* `handle_dispatch_multi()` function to run events within the same
* `plugin_context`.
*
* @see handle_plugin_dispatch
*/
void accept_requests();
/**
* Handle both Win32 messages and X11 events on a timer within the IO
* context. This is a centralized replacement for the event handling in
* `Vst2Bridge::handle_dispatch_single` for plugin groups.
*/
void async_handle_events();
/**
* Continuously read from a pipe and write the output to the log file. Used
* with the IO streams captured by `stdout_redirect` and `stderr_redirect`.
*
* TODO: Merge this with `PluginBridge::async_log_pipe_lines`
*
* @param pipe The pipe to read from.
* @param buffer The stream buffer to write to.
* @param prefix Text to prepend to the line before writing to the log.
*/
void async_log_pipe_lines(boost::asio::posix::stream_descriptor& pipe,
boost::asio::streambuf& buffer,
std::string prefix);
/**
* The logging facility used for this group host process. Since we can't
* identify which plugin is generating (debug) output, every line will only
* be prefixed with the name of the group.
*/
Logger logger;
/**
* The IO context that connections will be accepted on, and that any plugin
* operations that may involve the Win32 mesasge loop (e.g. initialization
* and most `AEffect::dispatcher()` calls) should be run on.
*/
boost::asio::io_context plugin_context;
/**
* A seperate IO context that handles the STDIO redirect through
* `StdIoCapture`. This is seperated the `plugin_context` above so that
* STDIO capture does not get blocked by blocking GUI operations. Since
* every GUI related operation should be run from the same thread, we can't
* just add another thread to the main IO context.
*/
boost::asio::io_context stdio_context;
boost::asio::streambuf stdout_buffer;
boost::asio::streambuf stderr_buffer;
/**
* Contains a pipe used for capturing this process's STDOUT stream. Needed
* to be able to process the output generated by Wine and plugins and to be
* able write it write it to an external log file.
*/
StdIoCapture stdout_redirect;
/**
* Contains a pipe used for capturing this process's STDERR stream. Needed
* to be able to process the output generated by Wine and plugins and to be
* able write it write it to an external log file.
*/
StdIoCapture stderr_redirect;
/**
* A thread that runs the `stdio_context` loop.
*/
std::thread stdio_handler;
boost::asio::local::stream_protocol::endpoint group_socket_endpoint;
/**
* The UNIX domain socket acceptor that will be used to listen for incoming
* connections to spawn new plugins within this process.
*/
boost::asio::local::stream_protocol::acceptor group_socket_acceptor;
/**
* A map of threads that are currently hosting a plugin within this process
* along with their plugin instance. After a plugin has exited or its
* initialization has failed, the thread handling it will remove itself from
* this map. This is to keep track of the amount of plugins currently
* running with their associated thread handles.
*
* TODO: Check again if we can just use std::thread here instead, that would
* make everything much simpler. `std::thread` was a problem with
* gdiplus in the past as Serum would randomly crash because calling
* conventions were nto being respected.
*/
std::unordered_map<GroupRequest,
std::pair<Win32Thread, std::unique_ptr<Vst2Bridge>>>
active_plugins;
/**
* A mutex to prevent two threads from simultaneously accessing the plugins
* map, and also to prevent `handle_plugin_dispatch()` from terminating the
* process because it thinks there are no active plugins left just as a new
* plugin is being spawned.
*/
std::mutex active_plugins_mutex;
/**
* A timer used to repeatedly handle the Win32 message loop and the X11
* events.
*
8 @see async_handle_events
*/
boost::asio::steady_timer events_timer;
/**
* A timer to defer shutting down the process, allowing for fast plugin
* scanning without having to start a new group host process for each
* plugin.
*
* @see handle_plugin_dispatch
*/
boost::asio::steady_timer shutdown_timer;
};
+559
View File
@@ -0,0 +1,559 @@
// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "vst2.h"
#include <boost/asio/dispatch.hpp>
#include <future>
#include <iostream>
#include "../../common/communication.h"
#include "../../common/events.h"
/**
* A function pointer to what should be the entry point of a VST plugin.
*/
using VstEntryPoint = AEffect*(VST_CALL_CONV*)(audioMasterCallback);
/**
* This ugly global is needed so we can get the instance of a `Brdige` class
* from an `AEffect` when it performs a host callback during its initialization.
*/
Vst2Bridge* current_bridge_instance = nullptr;
/**
* Needed for the rare event that two plugins are getting initialized at the
* same time.
*/
std::mutex current_bridge_instance_mutex;
intptr_t VST_CALL_CONV
host_callback_proxy(AEffect*, int, int, intptr_t, void*, float);
// We need to use the `CreateThread` WinAPI functions instead of `std::thread`
// to use the correct calling conventions within threads. Otherwise we'll get
// some rare and impossible to debug data races in some particular plugins.
uint32_t WINAPI handle_dispatch_midi_events_proxy(void*);
uint32_t WINAPI handle_parameters_proxy(void*);
uint32_t WINAPI handle_process_replacing_proxy(void*);
/**
* Fetch the Vst2Bridge instance stored in one of the two pointers reserved
* for the host of the hosted VST plugin. This is sadly needed as a workaround
* to avoid using globals since we need free function pointers to interface with
* the VST C API.
*/
Vst2Bridge& get_bridge_instance(const AEffect* plugin) {
// This is needed during the initialization of the plugin since we can only
// add our own pointer after it's done initializing
if (current_bridge_instance != nullptr) {
return *current_bridge_instance;
}
return *static_cast<Vst2Bridge*>(plugin->ptr1);
}
Vst2Bridge::Vst2Bridge(std::string plugin_dll_path,
std::string socket_endpoint_path)
// See `plugin_handle`s docstring for information on why we're leaking
// memory here
// : plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary),
: plugin_handle(LoadLibrary(plugin_dll_path.c_str())),
io_context(),
socket_endpoint(socket_endpoint_path),
host_vst_dispatch(io_context),
host_vst_dispatch_midi_events(io_context),
vst_host_callback(io_context),
host_vst_parameters(io_context),
host_vst_process_replacing(io_context) {
// Got to love these C APIs
if (plugin_handle == nullptr) {
throw std::runtime_error("Could not load the Windows .dll file at '" +
plugin_dll_path + "'");
}
// VST plugin entry point functions should be called `VSTPluginMain`, but
// there are some older deprecated names that legacy plugins may still use
VstEntryPoint vst_entry_point = nullptr;
for (auto name : {"VSTPluginMain", "main_plugin", "main"}) {
vst_entry_point = reinterpret_cast<VstEntryPoint>(
reinterpret_cast<size_t>(GetProcAddress(plugin_handle, name)));
if (vst_entry_point != nullptr) {
break;
}
}
if (vst_entry_point == nullptr) {
throw std::runtime_error(
"Could not find a valid VST entry point for '" + plugin_dll_path +
"'.");
}
// It's very important that these sockets are accepted to in the same order
// in the Linux plugin
host_vst_dispatch.connect(socket_endpoint);
host_vst_dispatch_midi_events.connect(socket_endpoint);
vst_host_callback.connect(socket_endpoint);
host_vst_parameters.connect(socket_endpoint);
host_vst_process_replacing.connect(socket_endpoint);
// Initialize after communication has been set up
// We'll try to do the same `get_bridge_isntance` trick as in
// `plugin/plugin.cpp`, but since the plugin will probably call the host
// callback while it's initializing we sadly have to use a global here.
{
std::lock_guard lock(current_bridge_instance_mutex);
current_bridge_instance = this;
plugin = vst_entry_point(host_callback_proxy);
if (plugin == nullptr) {
throw std::runtime_error("VST plugin at '" + plugin_dll_path +
"' failed to initialize.");
}
// We only needed this little hack during initialization
current_bridge_instance = nullptr;
plugin->ptr1 = this;
}
// Send the plugin's information to the Linux VST plugin. This is done over
// the `dispatch()` socket since this has to be done only once during
// initialization. Any updates during runtime are handled using the
// `audioMasterIOChanged` host callback.
write_object(host_vst_dispatch, EventResult{0, *plugin, std::nullopt});
// This works functionally identically to the `handle_dispatch_single()`
// function below, but this socket will only handle MIDI events. This is
// needed because of Win32 API limitations.
dispatch_midi_events_handler =
Win32Thread(handle_dispatch_midi_events_proxy, this);
parameters_handler = Win32Thread(handle_parameters_proxy, this);
process_replacing_handler =
Win32Thread(handle_process_replacing_proxy, this);
}
bool Vst2Bridge::should_skip_message_loop() {
return std::holds_alternative<EditorOpening>(editor);
}
void Vst2Bridge::handle_dispatch_single() {
using namespace std::placeholders;
// For our communication we use simple threads and blocking operations
// instead of asynchronous IO since communication has to be handled in
// lockstep anyway
try {
while (true) {
receive_event(host_vst_dispatch, std::nullopt,
passthrough_event(
plugin, std::bind(&Vst2Bridge::dispatch_wrapper,
this, _1, _2, _3, _4, _5, _6)));
handle_x11_events();
handle_win32_events();
}
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this host
// application
}
}
void Vst2Bridge::handle_dispatch_multi(boost::asio::io_context& main_context) {
// This works exactly the same as the function above, but execute the
// actual event and run the message loop from the main thread that's
// also instantiating these plugins. This is required for a few plugins
// to run multiple instances in the same process
try {
while (true) {
receive_event(
host_vst_dispatch, std::nullopt,
passthrough_event(
plugin,
[&](AEffect* plugin, int opcode, int index, intptr_t value,
void* data, float option) -> intptr_t {
std::promise<intptr_t> dispatch_result;
boost::asio::dispatch(main_context, [&]() {
const intptr_t result = dispatch_wrapper(
plugin, opcode, index, value, data, option);
dispatch_result.set_value(result);
});
// The message loop and X11 event handling will be run
// separately on a timer
return dispatch_result.get_future().get();
}));
}
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this
// host application
}
}
void Vst2Bridge::handle_dispatch_midi_events() {
try {
while (true) {
receive_event(
host_vst_dispatch_midi_events, std::nullopt, [&](Event& event) {
if (BOOST_LIKELY(event.opcode == effProcessEvents)) {
// For 99% of the plugins we can just call
// `effProcessReplacing()` and be done with it, but a
// select few plugins (I could only find Kontakt that
// does this) don't actually make copies of the events
// they receive and only store pointers, meaning that
// they have to live at least until the next audio
// buffer gets processed. We're not using
// `passhtourhg_events()` here directly because we need
// to store a copy of the `DynamicVstEvents` struct
// before passing the generated `VstEvents` object to
// the plugin.
std::lock_guard lock(next_buffer_midi_events_mutex);
next_audio_buffer_midi_events.push_back(
std::get<DynamicVstEvents>(event.payload));
DynamicVstEvents& events =
next_audio_buffer_midi_events.back();
// Exact same handling as in `passthrough_event`, apart
// from making a copy of the events first
const intptr_t return_value = plugin->dispatcher(
plugin, event.opcode, event.index, event.value,
&events.as_c_events(), event.option);
EventResult response{return_value, nullptr,
std::nullopt};
return response;
} else {
using namespace std::placeholders;
std::cerr << "[Warning] Received non-MIDI "
"event on MIDI processing thread"
<< std::endl;
// Maybe this should just be a hard error instead, since
// it should never happen
return passthrough_event(
plugin,
std::bind(&Vst2Bridge::dispatch_wrapper, this, _1,
_2, _3, _4, _5, _6))(event);
}
});
}
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this host
// application
}
}
void Vst2Bridge::handle_parameters() {
try {
while (true) {
// Both `getParameter` and `setParameter` functions are passed
// through on this socket since they have a lot of overlap. The
// presence of the `value` field tells us which one we're dealing
// with.
auto request = read_object<Parameter>(host_vst_parameters);
if (request.value.has_value()) {
// `setParameter`
plugin->setParameter(plugin, request.index,
request.value.value());
ParameterResult response{std::nullopt};
write_object(host_vst_parameters, response);
} else {
// `getParameter`
float value = plugin->getParameter(plugin, request.index);
ParameterResult response{value};
write_object(host_vst_parameters, response);
}
}
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this host
// application
}
}
void Vst2Bridge::handle_process_replacing() {
std::vector<std::vector<float>> output_buffers(plugin->numOutputs);
try {
while (true) {
auto request = read_object<AudioBuffers>(host_vst_process_replacing,
process_buffer);
// The process functions expect a `float**` for their inputs and
// their outputs
std::vector<float*> inputs;
for (auto& buffer : request.buffers) {
inputs.push_back(buffer.data());
}
// We reuse the buffers to avoid some unnecessary heap allocations,
// so we need to make sure the buffers are large enough since
// plugins can change their output configuration
std::vector<float*> outputs;
output_buffers.resize(plugin->numOutputs);
for (auto& buffer : output_buffers) {
buffer.resize(request.sample_frames);
outputs.push_back(buffer.data());
}
// Let the plugin process the MIDI events that were received since
// the last buffer, and then clean up those events. This approach
// should not be needed but Kontakt only stores pointers to rather
// than copies of the events.
{
std::lock_guard lock(next_buffer_midi_events_mutex);
// Any plugin made in the last fifteen years or so should
// support `processReplacing`. In the off chance it does not we
// can just emulate this behavior ourselves.
if (plugin->processReplacing != nullptr) {
plugin->processReplacing(plugin, inputs.data(),
outputs.data(),
request.sample_frames);
} else {
// If we zero out this buffer then the behavior is the same
// as `processReplacing``
for (std::vector<float>& buffer : output_buffers) {
std::fill(buffer.begin(), buffer.end(), 0.0);
}
plugin->process(plugin, inputs.data(), outputs.data(),
request.sample_frames);
}
next_audio_buffer_midi_events.clear();
}
AudioBuffers response{output_buffers, request.sample_frames};
write_object(host_vst_process_replacing, response, process_buffer);
}
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this host
// application
}
}
intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
// We have to intercept GUI open calls since we can't use
// the X11 window handle passed by the host
switch (opcode) {
case effEditGetRect: {
// Some plugins will have a race condition if the message loops gets
// handled between the call to `effEditGetRect()` and
// `effEditOpen()`, although this behavior never appears on Windows
// as hosts will always either call these functions in sequence or
// in reverse. We need to temporarily stop handling messages when
// this happens.
if (!std::holds_alternative<Editor>(editor)) {
editor = EditorOpening{};
}
return plugin->dispatcher(plugin, opcode, index, value, data,
option);
} break;
case effEditOpen: {
// Create a Win32 window through Wine, embed it into the window
// provided by the host, and let the plugin embed itself into
// the Wine window
const auto x11_handle = reinterpret_cast<size_t>(data);
// Win32 window classes have to be unique for the whole application.
// When hosting multiple plugins in a group process, all plugins
// should get a unique window class
const std::string window_class =
"yabridge plugin " + socket_endpoint.path();
Editor& editor_instance =
editor.emplace<Editor>(window_class, plugin, x11_handle);
return plugin->dispatcher(plugin, opcode, index, value,
editor_instance.win32_handle.get(),
option);
} break;
case effEditClose: {
const intptr_t return_value =
plugin->dispatcher(plugin, opcode, index, value, data, option);
// Cleanup is handled through RAII
editor = std::monostate();
return return_value;
} break;
default:
return plugin->dispatcher(plugin, opcode, index, value, data,
option);
break;
}
}
void Vst2Bridge::handle_win32_events() {
std::visit(
overload{[](Editor& editor) { editor.handle_win32_events(); },
[](std::monostate&) {
MSG msg;
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
},
[](EditorOpening&) {
// Don't handle any events in this
// particular case as explained in
// `Vst2Bridge::editor`
}},
editor);
}
void Vst2Bridge::handle_x11_events() {
std::visit(overload{[](Editor& editor) { editor.handle_x11_events(); },
[](auto&) {}},
editor);
}
class HostCallbackDataConverter : DefaultDataConverter {
public:
HostCallbackDataConverter(AEffect* plugin,
std::optional<VstTimeInfo>& time_info)
: plugin(plugin), time_info(time_info) {}
EventPayload read(const int opcode,
const int index,
const intptr_t value,
const void* data) {
switch (opcode) {
case audioMasterGetTime:
return WantsVstTimeInfo{};
break;
case audioMasterIOChanged:
// This is a helpful event that indicates that the VST
// plugin's `AEffect` struct has changed. Writing these
// results back is done inside of `passthrough_event`.
return AEffect(*plugin);
break;
case audioMasterProcessEvents:
return DynamicVstEvents(*static_cast<const VstEvents*>(data));
break;
// We detect whether an opcode should return a string by
// checking whether there's a zeroed out buffer behind the void
// pointer. This works for any host, but not all plugins zero
// out their buffers.
case audioMasterGetVendorString:
case audioMasterGetProductString:
return WantsString{};
break;
default:
return DefaultDataConverter::read(opcode, index, value, data);
break;
}
}
std::optional<EventPayload> read_value(const int opcode,
const intptr_t value) {
return DefaultDataConverter::read_value(opcode, value);
}
void write(const int opcode, void* data, const EventResult& response) {
switch (opcode) {
case audioMasterGetTime:
// Write the returned `VstTimeInfo` struct into a field and
// make the function return a poitner to it in the function
// below. Depending on whether the host supported the
// requested time information this operations returns either
// a null pointer or a pointer to a `VstTimeInfo` object.
if (std::holds_alternative<std::nullptr_t>(response.payload)) {
time_info = std::nullopt;
} else {
time_info = std::get<VstTimeInfo>(response.payload);
}
break;
default:
DefaultDataConverter::write(opcode, data, response);
break;
}
}
intptr_t return_value(const int opcode, const intptr_t original) {
switch (opcode) {
case audioMasterGetTime: {
// Return a pointer to the `VstTimeInfo` object written in
// the function above
VstTimeInfo* time_info_pointer = nullptr;
if (time_info.has_value()) {
time_info_pointer = &time_info.value();
}
return reinterpret_cast<intptr_t>(time_info_pointer);
} break;
default:
return DefaultDataConverter::return_value(opcode, original);
break;
}
}
void write_value(const int opcode,
intptr_t value,
const EventResult& response) {
return DefaultDataConverter::write_value(opcode, value, response);
}
private:
AEffect* plugin;
std::optional<VstTimeInfo>& time_info;
};
intptr_t Vst2Bridge::host_callback(AEffect* effect,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
HostCallbackDataConverter converter(effect, time_info);
return send_event(vst_host_callback, host_callback_mutex, converter,
std::nullopt, opcode, index, value, data, option);
}
intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
return get_bridge_instance(effect).host_callback(effect, opcode, index,
value, data, option);
}
uint32_t WINAPI handle_dispatch_midi_events_proxy(void* instance) {
static_cast<Vst2Bridge*>(instance)->handle_dispatch_midi_events();
return 0;
}
uint32_t WINAPI handle_parameters_proxy(void* instance) {
static_cast<Vst2Bridge*>(instance)->handle_parameters();
return 0;
}
uint32_t WINAPI handle_process_replacing_proxy(void* instance) {
static_cast<Vst2Bridge*>(instance)->handle_process_replacing();
return 0;
}
@@ -16,7 +16,7 @@
#pragma once
#include "boost-fix.h"
#include "../boost-fix.h"
#define NOMINMAX
#define NOSERVICE
@@ -29,23 +29,34 @@
#include <boost/asio/local/stream_protocol.hpp>
#include <mutex>
#include "../common/logging.h"
#include "editor.h"
#include "utils.h"
#include "../../common/logging.h"
#include "../editor.h"
#include "../utils.h"
/**
* A marker struct to indicate that the editor is about to be opened.
*
* @see WineBridge::editor
* @see Vst2Bridge::editor
*/
struct EditorOpening {};
/**
* This handles the communication between the Linux native VST plugin and the
* Wine VST host. The functions below should be used as callback functions in an
* `AEffect` object.
* This hosts a Windows VST2 plugin, forwards messages sent by the Linux VST
* plugin and provides host callback function for the plugin to talk back.
*
* @remark Because of Win32 API limitations, all window handling has to be done
* from the same thread. For individually hosted plugins this only means that
* this class has to be initialized from the same thread as the one that calls
* `handle_dispatch_single()`, and thus also runs the message loop. When using
* plugin groups, however, all instantiation, editor event handling and
* message loop pumping has to be done from a single thread. Most plugins
* won't have any issues when using multiple message loops, but the Melda
* plugins for instance will only update their GUIs from the message loop of
* the thread that created the first instance. When running multiple plugins
* `handle_dispatch_multi()` should be used to make sure all plugins
* handle their events on the same thread.
*/
class WineBridge {
class Vst2Bridge {
public:
/**
* Initializes the Windows VST plugin and set up communication with the
@@ -56,27 +67,67 @@ class WineBridge {
* @param socket_endpoint_path A (Unix style) path to the Unix socket
* endpoint the native VST plugin created to communicate over.
*
* @note When using plugin groups and `handle_dispatch_multi()`, this
* object has to be constructed from within the IO context.
*
* @throw std::runtime_error Thrown when the VST plugin could not be loaded,
* or if communication could not be set up.
*/
WineBridge(std::string plugin_dll_path, std::string socket_endpoint_path);
Vst2Bridge(std::string plugin_dll_path, std::string socket_endpoint_path);
/**
* Returns true if the message loop should be skipped. This happens when the
* editor is in the process of being opened. In VST hosts on Windows
* `effEditOpen()` and `effEditGetRect()` will always be called in sequence,
* but in our approach there will be an opportunity to handle events in
* between these two calls. Most plugins will handle this just fine, but
* some plugins end up blocking indefinitely while waiting for the other
* function to be called, hence why this function is needed. For
* individually hosted plugins this check is done implicitely in
* `Vst2Bridge::handle_win32_events()`.
*/
bool should_skip_message_loop();
/**
* Handle events on the main thread until the plugin quits. This can't be
* done on another thread since some plugins (e.g. Melda) expect certain
* (but for some reason not all) events to be passed from the same thread it
* was initiated from. This is then also the same thread that should handle
* Win32 GUI events.
* events to be passed from the same thread it was initiated from. This is
* then also the same thread that should handle Win32 GUI events.
*/
void handle_dispatch();
void handle_dispatch_single();
// These functions are the entry points for the `*_handler` threads defined
// below. They're defined here because we can't use lambdas with WinAPI's
// `CreateThread` which is needed to support the proper call conventions the
// VST plugins expect.
[[noreturn]] void handle_dispatch_midi_events();
[[noreturn]] void handle_parameters();
[[noreturn]] void handle_process_replacing();
/**
* Handle events just like in the function above, but do the actual
* execution on the IO context. As explained in this class' docstring, this
* is needed because some plugins make the assumption that all of their
* instances are handled from the same thread, and that the thread that the
* first instance was initiated on will be kept alive until the VST host
* terminates.
*
* @param main_context The main IO context that's handling the event
* handling for all plugins.
*
* @note With this approach you'll have to make sure that the object was
* instantiated from the same thread as the one that runs the IO context.
* @note This appraoch does _not_ handle any events. This has to be done on
* a timer within the IO context since otherwise things would become very
* messy very quick.
*/
void handle_dispatch_multi(boost::asio::io_context& main_context);
/**
* Handle X11 events for the editor window if it is open. This can be run
* safely from any thread.
*/
void handle_x11_events();
// These functions are the entry points for the `*_handler` threads
// defined below. They're defined here because we can't use lambdas with
// WinAPI's `CreateThread` which is needed to support the proper call
// conventions the VST plugins expect.
void handle_dispatch_midi_events();
void handle_parameters();
void handle_process_replacing();
/**
* Forward the host callback made by the plugin to the host and return the
@@ -105,11 +156,37 @@ class WineBridge {
float option);
/**
* The shared library handle of the VST plugin. I sadly could not get
* Boost.DLL to work here, so we'll just load the VST plugisn by hand.
* Run the message loop for this plugin and potentially also for other
* plugins. This is only used in `handle_dispatch_single()`, as this will be
* run on a timer when using plugin groups. The caller should first check
* whether the event loop can be run through `should_skip_message_loop()`.
*
* Because of the way the Win32 API works we have to process events on the
* same thread as the one the window was created on, and that thread is the
* thread that's handling dispatcher calls. Some plugins will also rely on
* the Win32 message loop to run tasks on a timer and to defer loading, so
* we have to make sure to always run this loop. The only exception is a in
* specific situation that can cause a race condition in some plugins
* because of incorrect assumptions made by the plugin. See the dostring for
* `Vst2Bridge::editor` for more information.
*/
std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)>
plugin_handle;
void handle_win32_events();
/**
* The shared library handle of the VST plugin. I sadly could not get
* Boost.DLL to work here, so we'll just load the VST plugins by hand.
*
* FIXME: I don't know why, but `FreeLibrary()` seems to corrupt memory.
* This leads to a lot of weird behavior, such as plugins crashing as
* soon as other plugins get loaded or calls to `LoadLibrary()`
* returning null pointers while they would otherwise load fine
* without the prior call to `FreeLibrary`. We are leaking memory
* here until this is fixed, but it should not be a huge issue since
* this leak only exists for plugin groups.
*/
// std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)>
// plugin_handle;
HMODULE plugin_handle;
/**
* The loaded plugin's `AEffect` struct, obtained using the above library
@@ -210,6 +287,8 @@ class WineBridge {
* To work around this we'll use this third state to temporarily stop
* processing Windows events in the one or two ticks between these two
* events.
*
* @see should_postpone_message_loop
*/
std::variant<std::monostate, Editor, EditorOpening> editor;
};
+89 -61
View File
@@ -87,19 +87,28 @@ Editor::Editor(const std::string& window_class_name,
// The Win32 API will block the `DispatchMessage` call when opening e.g. a
// dropdown, but it will still allow timers to be run so the GUI can still
// update in the background. Because of this we send `effEditIdle` to the
// plugin on a timer. The refresh rate is purposely fairly low since we
// we'll also trigger this manually in `Editor::handle_events()` whenever
// the plugin is not busy.
// plugin on a timer. The refresh rate is purposely fairly low since the
// host will call `effEditIdle()` explicitely when the plugin is not busy.
// TODO: Add a `KillTimer()` now that we are hosting multiple plugins
SetTimer(win32_handle.get(), idle_timer_id, 100, nullptr);
// We need to tell the Wine window it has been moved whenever the window
// it's been embedded in gets moved around. In most cases this is
// `parent_window`, but for instance REAPER reparents `parent_window` in
// another window so we'll have to find the correct window first.
// Because we're not using XEmbed Wine will interpret any local coordinates
// as global coordinates. To work around this we'll tell the Wine window
// it's located at its actual coordinates on screen rather than somewhere
// within. For robustness's sake this should be done both when the actual
// window the Wine window is embedded in (which may not be the parent
// window) is moved or resized, and when the user moves his mouse over the
// window. We also want to set keyboard focus when the user clicks on the
// Windows since Bitwig 3.2 now explicitely requires this.
const uint32_t topmost_event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY;
xcb_change_window_attributes(x11_connection.get(), topmost_window,
XCB_CW_EVENT_MASK, &topmost_event_mask);
xcb_flush(x11_connection.get());
const uint32_t parent_event_mask =
XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_ENTER_WINDOW;
xcb_change_window_attributes(x11_connection.get(), parent_window,
XCB_CW_EVENT_MASK, &parent_event_mask);
xcb_flush(x11_connection.get());
// Embed the Win32 window into the window provided by the host. Instead of
// using the XEmbed protocol, we'll register a few events and manage the
@@ -137,7 +146,7 @@ void Editor::send_idle_event() {
plugin->dispatcher(plugin, effEditIdle, 0, 0, nullptr, 0);
}
void Editor::handle_events() {
void Editor::handle_win32_events() {
MSG msg;
// The null value for the second argument is needed to handle interaction
@@ -158,8 +167,9 @@ void Editor::handle_events() {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// Handle X11 events
void Editor::handle_x11_events() {
// TODO: Initiating drag-and-drop in Serum _sometimes_ causes the GUI to
// update while dragging while other times it does not. From all the
// plugins I've tested this only happens in Serum though.
@@ -167,66 +177,84 @@ void Editor::handle_events() {
while ((generic_event = xcb_poll_for_event(x11_connection.get())) !=
nullptr) {
switch (generic_event->response_type & event_type_mask) {
// We're listening for ConfigureNotify events on the topmost window
// before the root window, i.e. the window that's actually going to
// get dragged around the by the user. In most cases this is the
// same as `parent_window`.
case XCB_CONFIGURE_NOTIFY: {
// We're purposely not using XEmbed. This has the consequence
// that wine still thinks that any X and Y coordinates are
// relative to the x11 window root instead of the parent window
// provided by the DAW, causing all sorts of GUI interactions to
// break. To alleviate this we'll just lie to Wine and tell it
// that it's located at the parent window's location on the root
// window. We also will keep the child window at its largest
// possible size to allow for smooth resizing. This works
// because the embedding hierarchy is DAW window -> Win32 window
// (created in this class) -> VST plugin window created by the
// plugin itself. In this case it doesn't matter that the Win32
// window is larger than the part of the client area the plugin
// draws to since any excess will be clipped off by the parent
// window.
const auto query_cookie =
xcb_query_tree(x11_connection.get(), parent_window);
xcb_window_t root = xcb_query_tree_reply(x11_connection.get(),
query_cookie, nullptr)
->root;
// We're listening for `ConfigureNotify` events on the topmost
// window before the root window, i.e. the window that's actually
// going to get dragged around the by the user. In most cases this
// is the same as `parent_window`. When either this window gets
// moved, or when the user moves his mouse over our window, the
// local coordinates should be updated. The additional `EnterWindow`
// check is sometimes necessary for using multiple editor windows
// within a single plugin group.
case XCB_CONFIGURE_NOTIFY:
case XCB_ENTER_NOTIFY:
fix_local_coordinates();
break;
case XCB_FOCUS_IN:
fix_local_coordinates();
// We can't directly use the `event.x` and `event.y` coordinates
// because the parent window may also be embedded inside another
// window.
const auto translate_cookie = xcb_translate_coordinates(
x11_connection.get(), parent_window, root, 0, 0);
const xcb_translate_coordinates_reply_t*
translated_coordiantes = xcb_translate_coordinates_reply(
x11_connection.get(), translate_cookie, nullptr);
xcb_configure_notify_event_t translated_event{};
translated_event.response_type = XCB_CONFIGURE_NOTIFY;
translated_event.event = child_window;
translated_event.window = child_window;
// This should be set to the same sizes the window was created
// on. Since we're not using `SetWindowPos` to resize the
// Window, Wine can get a bit confused when we suddenly report a
// different client area size. Without this certain plugins
// (such as those by Valhalla DSP) would break.
translated_event.width = client_area.width;
translated_event.height = client_area.height;
translated_event.x = translated_coordiantes->dst_x;
translated_event.y = translated_coordiantes->dst_y;
xcb_send_event(x11_connection.get(), false, child_window,
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<char*>(&translated_event));
// Explicitely request input focus when the user clicks on the
// window. This is needed for Bitwig Studio 3.2, as the parent
// window now captures all keyboard events and forwards them to
// the main Bitwig Studio window instead of allowing the child
// window to handle those events.
xcb_set_input_focus(x11_connection.get(),
XCB_INPUT_FOCUS_PARENT, child_window,
XCB_CURRENT_TIME);
xcb_flush(x11_connection.get());
} break;
break;
}
free(generic_event);
}
}
void Editor::fix_local_coordinates() {
// We're purposely not using XEmbed. This has the consequence that wine
// still thinks that any X and Y coordinates are relative to the x11 window
// root instead of the parent window provided by the DAW, causing all sorts
// of GUI interactions to break. To alleviate this we'll just lie to Wine
// and tell it that it's located at the parent window's location on the root
// window. We also will keep the child window at its largest possible size
// to allow for smooth resizing. This works because the embedding hierarchy
// is DAW window -> Win32 window (created in this class) -> VST plugin
// window created by the plugin itself. In this case it doesn't matter that
// the Win32 window is larger than the part of the client area the plugin
// draws to since any excess will be clipped off by the parent window.
const auto query_cookie =
xcb_query_tree(x11_connection.get(), parent_window);
xcb_window_t root =
xcb_query_tree_reply(x11_connection.get(), query_cookie, nullptr)->root;
// We can't directly use the `event.x` and `event.y` coordinates because the
// parent window may also be embedded inside another window.
// TODO: This seems to get clamped at (0, 0), causing large windows dragged
// off screen on the top or the left borders to act up
const auto translate_cookie = xcb_translate_coordinates(
x11_connection.get(), parent_window, root, 0, 0);
const xcb_translate_coordinates_reply_t* translated_coordiantes =
xcb_translate_coordinates_reply(x11_connection.get(), translate_cookie,
nullptr);
xcb_configure_notify_event_t translated_event{};
translated_event.response_type = XCB_CONFIGURE_NOTIFY;
translated_event.event = child_window;
translated_event.window = child_window;
// This should be set to the same sizes the window was created on. Since
// we're not using `SetWindowPos` to resize the Window, Wine can get a bit
// confused when we suddenly report a different client area size. Without
// this certain plugins (such as those by Valhalla DSP) would break.
translated_event.width = client_area.width;
translated_event.height = client_area.height;
translated_event.x = translated_coordiantes->dst_x;
translated_event.y = translated_coordiantes->dst_y;
xcb_send_event(
x11_connection.get(), false, child_window,
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<char*>(&translated_event));
xcb_flush(x11_connection.get());
}
LRESULT CALLBACK window_proc(HWND handle,
UINT message,
WPARAM wParam,
+16 -4
View File
@@ -101,13 +101,25 @@ class Editor {
void send_idle_event();
/**
* Pump messages from the editor GUI's event loop until all events are
* process. Must be run from the same thread the GUI was created in because
* of Win32 limitations.
* Pump messages from the editor loop loop until all events are process.
* Must be run from the same thread the GUI was created in because of Win32
* limitations.
*/
void handle_events();
void handle_win32_events();
/**
* Handle X11 events sent to the window our editor is embedded in.
*/
void handle_x11_events();
private:
/**
* Lie to the Wine window about its coordinates on the screen for
* reparenting without using XEmbed. See the comment at the top of the
* implementation on why this is needed.
*/
void fix_local_coordinates();
/**
* A pointer to the currently active window. Will be a null pointer if no
* window is active.
+84
View File
@@ -0,0 +1,84 @@
// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "boost-fix.h"
#include <iostream>
// Generated inside of build directory
#include <src/common/config/config.h>
#include <src/common/config/version.h>
#include "bridges/group.h"
#include "bridges/vst2.h"
/**
* This works very similar to the host application defined in
* `individual-host.cpp`, but instead of just loading a single plugin this will
* act as a daemon that can host multiple 'grouped' plugins. This works by
* allowing the `libyabridge.so` instance to connect this this process over a
* socket to ask this process to host a VST `.dll` file using a provided socket.
* After that initialization step both the regular individual plugin host and
* this group plugin host will function identically on both the plugin and the
* Wine VST host side.
*
* The explicit calling convention is needed to work around a bug introduced in
* Wine 5.7: https://bugs.winehq.org/show_bug.cgi?id=49138
*/
int __cdecl main(int argc, char* argv[]) {
// Instead of directly hosting a plugin, this process will receive a UNIX
// domain socket endpoint path that it should listen on to allow yabridge
// instances to spawn plugins in this process.
if (argc < 2) {
std::cerr << "Usage: "
#ifdef __i386__
<< yabridge_group_host_name_32bit
#else
<< yabridge_group_host_name
#endif
<< " <unix_domain_socket>" << std::endl;
return 1;
}
const std::string group_socket_endpoint_path(argv[1]);
std::cerr << "Initializing yabridge group host version "
<< yabridge_git_version
#ifdef __i386__
<< " (32-bit compatibility mode)"
#endif
<< std::endl;
try {
GroupBridge bridge(group_socket_endpoint_path);
// Blocks the main thread until all plugins have exited
bridge.handle_incoming_connections();
} catch (const boost::system::system_error& error) {
// If another process is already listening on the socket, we'll just
// print a message and exit quietly. This could happen if the host
// starts multiple yabridge instances that all use the same plugin group
// at the same time.
// TODO: Check if this is printed on the right stream
std::cerr << "Another process is already listening on this group's "
"socket, connecting to the existing process:"
<< std::endl;
std::cerr << error.what() << std::endl;
return 0;
}
}
@@ -20,11 +20,16 @@
#include <src/common/config/config.h>
#include <src/common/config/version.h>
#include "wine-bridge.h"
#include "bridges/vst2.h"
// This explicit calling convention is needed to work around a bug introduced in
// Wine 5.7
// https://bugs.winehq.org/show_bug.cgi?id=49138
/**
* This is the default VST host application. It will load the specified VST2
* plugin, and then connect back to the `libyabridge.so` instace that spawned
* this over the socket.
*
* The explicit calling convention is needed to work around a bug introduced in
* Wine 5.7: https://bugs.winehq.org/show_bug.cgi?id=49138
*/
int __cdecl main(int argc, char* argv[]) {
// We pass the name of the VST plugin .dll file to load and the Unix domain
// socket to connect to in plugin/bridge.cpp as the first two arguments of
@@ -32,9 +37,9 @@ int __cdecl main(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: "
#ifdef __i386__
<< yabridge_wine_host_name_32bit
<< yabridge_individual_host_name_32bit
#else
<< yabridge_wine_host_name
<< yabridge_individual_host_name
#endif
<< " <vst_plugin_dll> <unix_domain_socket>" << std::endl;
@@ -49,13 +54,14 @@ int __cdecl main(int argc, char* argv[]) {
<< " (32-bit compatibility mode)"
#endif
<< std::endl;
try {
WineBridge bridge(plugin_dll_path, socket_endpoint_path);
Vst2Bridge bridge(plugin_dll_path, socket_endpoint_path);
std::cerr << "Finished initializing '" << plugin_dll_path << "'"
<< std::endl;
// Blocks the main thread until the plugin shuts down
bridge.handle_dispatch();
bridge.handle_dispatch_single();
} catch (const std::runtime_error& error) {
std::cerr << "Error while initializing Wine VST host:" << std::endl;
std::cerr << error.what() << std::endl;
-486
View File
@@ -1,486 +0,0 @@
// yabridge: a Wine VST bridge
// Copyright (C) 2020 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include "wine-bridge.h"
#include <iostream>
#include <boost/filesystem.hpp>
#include "../common/communication.h"
#include "../common/events.h"
namespace fs = boost::filesystem;
/**
* A function pointer to what should be the entry point of a VST plugin.
*/
using VstEntryPoint = AEffect*(VST_CALL_CONV*)(audioMasterCallback);
/**
* This ugly global is needed so we can get the instance of a `Brdige` class
* from an `AEffect` when it performs a host callback during its initialization.
*/
WineBridge* current_bridge_instance = nullptr;
intptr_t VST_CALL_CONV
host_callback_proxy(AEffect*, int, int, intptr_t, void*, float);
// We need to use the `CreateThread` WinAPI functions instead of `std::thread`
// to use the correct calling conventions within threads. Otherwise we'll get
// some rare and impossible to debug data races in some particular plugins.
uint32_t WINAPI handle_dispatch_midi_events_proxy(void*);
uint32_t WINAPI handle_parameters_proxy(void*);
uint32_t WINAPI handle_process_replacing_proxy(void*);
/**
* Fetch the WineBridge instance stored in one of the two pointers reserved
* for the host of the hosted VST plugin. This is sadly needed as a workaround
* to avoid using globals since we need free function pointers to interface with
* the VST C API.
*/
WineBridge& get_bridge_instance(const AEffect* plugin) {
// This is needed during the initialization of the plugin since we can only
// add our own pointer after it's done initializing
if (current_bridge_instance != nullptr) {
// This should only be used during initialization
assert(plugin == nullptr || plugin->ptr1 == nullptr);
return *current_bridge_instance;
}
return *static_cast<WineBridge*>(plugin->ptr1);
}
WineBridge::WineBridge(std::string plugin_dll_path,
std::string socket_endpoint_path)
: plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary),
io_context(),
socket_endpoint(socket_endpoint_path),
host_vst_dispatch(io_context),
host_vst_dispatch_midi_events(io_context),
vst_host_callback(io_context),
host_vst_parameters(io_context),
host_vst_process_replacing(io_context) {
// Got to love these C APIs
if (plugin_handle == nullptr) {
throw std::runtime_error("Could not load the Windows .dll file at '" +
plugin_dll_path + "'");
}
// VST plugin entry point functions should be called `VSTPluginMain`, but
// there are some older deprecated names that legacy plugins may still use
VstEntryPoint vst_entry_point = nullptr;
for (auto name : {"VSTPluginMain", "main_plugin", "main"}) {
vst_entry_point =
reinterpret_cast<VstEntryPoint>(reinterpret_cast<size_t>(
GetProcAddress(plugin_handle.get(), name)));
if (vst_entry_point != nullptr) {
break;
}
}
if (vst_entry_point == nullptr) {
throw std::runtime_error(
"Could not find a valid VST entry point for '" + plugin_dll_path +
"'.");
}
// It's very important that these sockets are accepted to in the same order
// in the Linux plugin
host_vst_dispatch.connect(socket_endpoint);
host_vst_dispatch_midi_events.connect(socket_endpoint);
vst_host_callback.connect(socket_endpoint);
host_vst_parameters.connect(socket_endpoint);
host_vst_process_replacing.connect(socket_endpoint);
// Initialize after communication has been set up
// We'll try to do the same `get_bridge_isntance` trick as in
// `plugin/plugin.cpp`, but since the plugin will probably call the host
// callback while it's initializing we sadly have to use a global here.
current_bridge_instance = this;
plugin = vst_entry_point(host_callback_proxy);
if (plugin == nullptr) {
throw std::runtime_error("VST plugin at '" + plugin_dll_path +
"' failed to initialize.");
}
// We only needed this little hack during initialization
current_bridge_instance = nullptr;
plugin->ptr1 = this;
// Send the plugin's information to the Linux VST plugin. This is done over
// the `dispatch()` socket since this has to be done only once during
// initialization. Any updates during runtime are handled using the
// `audioMasterIOChanged` host callback.
write_object(host_vst_dispatch, EventResult{0, *plugin, std::nullopt});
// This works functionally identically to the `handle_dispatch()` function
// below, but this socket will only handle MIDI events. This is needed
// because of Win32 API limitations.
dispatch_midi_events_handler =
Win32Thread(handle_dispatch_midi_events_proxy, this);
parameters_handler = Win32Thread(handle_parameters_proxy, this);
process_replacing_handler =
Win32Thread(handle_process_replacing_proxy, this);
}
void WineBridge::handle_dispatch() {
using namespace std::placeholders;
// For our communication we use simple threads and blocking operations
// instead of asynchronous IO since communication has to be handled in
// lockstep anyway
try {
while (true) {
receive_event(host_vst_dispatch, std::nullopt,
passthrough_event(
plugin, std::bind(&WineBridge::dispatch_wrapper,
this, _1, _2, _3, _4, _5, _6)));
// Because of the way the Win32 API works we have to process events
// on the same thread as the one the window was created on, and that
// thread is the thread that's handling dispatcher calls. Some
// plugins will also rely on the Win32 message loop to run tasks on
// a timer and to defer loading, so we have to make sure to always
// run this loop. The only exception is a in specific situation that
// can cause a race condition in some plugins because of incorrect
// assumptions made by the plugin. See the dostring for
// `WineBridge::editor` for more information.
std::visit(overload{[](Editor& editor) { editor.handle_events(); },
[](std::monostate&) {
MSG msg;
while (PeekMessage(&msg, nullptr, 0, 0,
PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
},
[](EditorOpening&) {
// Don't handle any events in this
// particular case as explained in
// `WineBridge::editor`
}},
editor);
}
} catch (const boost::system::system_error&) {
// The plugin has cut off communications, so we can shut down this host
// application
}
}
[[noreturn]] void WineBridge::handle_dispatch_midi_events() {
while (true) {
receive_event(
host_vst_dispatch_midi_events, std::nullopt, [&](Event& event) {
if (BOOST_LIKELY(event.opcode == effProcessEvents)) {
// For 99% of the plugins we can just call
// `effProcessReplacing()` and be done with it, but a select
// few plugins (I could only find Kontakt that does this)
// don't actually make copies of the events they receive and
// only store pointers, meaning that they have to live at
// least until the next audio buffer gets processed. We're
// not using `passhtourhg_events()` here directly because we
// need to store a copy of the `DynamicVstEvents` struct
// before passing the generated `VstEvents` object to the
// plugin.
std::lock_guard lock(next_buffer_midi_events_mutex);
next_audio_buffer_midi_events.push_back(
std::get<DynamicVstEvents>(event.payload));
DynamicVstEvents& events =
next_audio_buffer_midi_events.back();
// Exact same handling as in `passthrough_event`, apart from
// making a copy of the events first
const intptr_t return_value = plugin->dispatcher(
plugin, event.opcode, event.index, event.value,
&events.as_c_events(), event.option);
EventResult response{return_value, nullptr, std::nullopt};
return response;
} else {
using namespace std::placeholders;
std::cerr << "[Warning] Received non-MIDI "
"event on MIDI processing thread"
<< std::endl;
// Maybe this should just be a hard error instead, since it
// should never happen
return passthrough_event(
plugin, std::bind(&WineBridge::dispatch_wrapper, this,
_1, _2, _3, _4, _5, _6))(event);
}
});
}
}
[[noreturn]] void WineBridge::handle_parameters() {
while (true) {
// Both `getParameter` and `setParameter` functions are passed
// through on this socket since they have a lot of overlap. The
// presence of the `value` field tells us which one we're dealing
// with.
auto request = read_object<Parameter>(host_vst_parameters);
if (request.value.has_value()) {
// `setParameter`
plugin->setParameter(plugin, request.index, request.value.value());
ParameterResult response{std::nullopt};
write_object(host_vst_parameters, response);
} else {
// `getParameter`
float value = plugin->getParameter(plugin, request.index);
ParameterResult response{value};
write_object(host_vst_parameters, response);
}
}
}
[[noreturn]] void WineBridge::handle_process_replacing() {
std::vector<std::vector<float>> output_buffers(plugin->numOutputs);
while (true) {
auto request = read_object<AudioBuffers>(host_vst_process_replacing,
process_buffer);
// The process functions expect a `float**` for their inputs and
// their outputs
std::vector<float*> inputs;
for (auto& buffer : request.buffers) {
inputs.push_back(buffer.data());
}
// We reuse the buffers to avoid some unnecessary heap allocations,
// so we need to make sure the buffers are large enough since
// plugins can change their output configuration
std::vector<float*> outputs;
output_buffers.resize(plugin->numOutputs);
for (auto& buffer : output_buffers) {
buffer.resize(request.sample_frames);
outputs.push_back(buffer.data());
}
// Let the plugin process the MIDI events that were received since the
// last buffer, and then clean up those events. This approach should not
// be needed but Kontakt only stores pointers to rather than copies of
// the events.
{
std::lock_guard lock(next_buffer_midi_events_mutex);
// Any plugin made in the last fifteen years or so should support
// `processReplacing`. In the off chance it does not we can just
// emulate this behavior ourselves.
if (plugin->processReplacing != nullptr) {
plugin->processReplacing(plugin, inputs.data(), outputs.data(),
request.sample_frames);
} else {
// If we zero out this buffer then the behavior is the same as
// `processReplacing``
for (std::vector<float>& buffer : output_buffers) {
std::fill(buffer.begin(), buffer.end(), 0.0);
}
plugin->process(plugin, inputs.data(), outputs.data(),
request.sample_frames);
}
next_audio_buffer_midi_events.clear();
}
AudioBuffers response{output_buffers, request.sample_frames};
write_object(host_vst_process_replacing, response, process_buffer);
}
}
intptr_t WineBridge::dispatch_wrapper(AEffect* plugin,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
// We have to intercept GUI open calls since we can't use
// the X11 window handle passed by the host
switch (opcode) {
case effEditGetRect: {
// Some plugins will have a race condition if the message loops gets
// handled between the call to `effEditGetRect()` and
// `effEditOpen()`, although this behavior never appears on Windows
// as hosts will always either call these functions in sequence or
// in reverse. We need to temporarily stop handling messages when
// this happens.
if (!std::holds_alternative<Editor>(editor)) {
editor = EditorOpening{};
}
return plugin->dispatcher(plugin, opcode, index, value, data,
option);
} break;
case effEditOpen: {
// Create a Win32 window through Wine, embed it into the window
// provided by the host, and let the plugin embed itself into
// the Wine window
const auto x11_handle = reinterpret_cast<size_t>(data);
Editor& editor_instance =
editor.emplace<Editor>("yabridge plugin", plugin, x11_handle);
return plugin->dispatcher(plugin, opcode, index, value,
editor_instance.win32_handle.get(),
option);
} break;
case effEditClose: {
const intptr_t return_value =
plugin->dispatcher(plugin, opcode, index, value, data, option);
// Cleanup is handled through RAII
editor = std::monostate();
return return_value;
} break;
default:
return plugin->dispatcher(plugin, opcode, index, value, data,
option);
break;
}
}
class HostCallbackDataConverter : DefaultDataConverter {
public:
HostCallbackDataConverter(AEffect* plugin,
std::optional<VstTimeInfo>& time_info)
: plugin(plugin), time_info(time_info) {}
EventPayload read(const int opcode,
const int index,
const intptr_t value,
const void* data) {
switch (opcode) {
case audioMasterGetTime:
return WantsVstTimeInfo{};
break;
case audioMasterIOChanged:
// This is a helpful event that indicates that the VST plugin's
// `AEffect` struct has changed. Writing these results back is
// done inside of `passthrough_event`.
return AEffect(*plugin);
break;
case audioMasterProcessEvents:
return DynamicVstEvents(*static_cast<const VstEvents*>(data));
break;
// We detect whether an opcode should return a string by checking
// whether there's a zeroed out buffer behind the void pointer. This
// works for any host, but not all plugins zero out their buffers.
case audioMasterGetVendorString:
case audioMasterGetProductString:
return WantsString{};
break;
default:
return DefaultDataConverter::read(opcode, index, value, data);
break;
}
}
std::optional<EventPayload> read_value(const int opcode,
const intptr_t value) {
return DefaultDataConverter::read_value(opcode, value);
}
void write(const int opcode, void* data, const EventResult& response) {
switch (opcode) {
case audioMasterGetTime:
// Write the returned `VstTimeInfo` struct into a field and make
// the function return a poitner to it in the function below.
// Depending on whether the host supported the requested time
// information this operations returns either a null pointer or
// a pointer to a `VstTimeInfo` object.
if (std::holds_alternative<std::nullptr_t>(response.payload)) {
time_info = std::nullopt;
} else {
time_info = std::get<VstTimeInfo>(response.payload);
}
break;
default:
DefaultDataConverter::write(opcode, data, response);
break;
}
}
intptr_t return_value(const int opcode, const intptr_t original) {
switch (opcode) {
case audioMasterGetTime: {
// Return a pointer to the `VstTimeInfo` object written in the
// function above
VstTimeInfo* time_info_pointer = nullptr;
if (time_info.has_value()) {
time_info_pointer = &time_info.value();
}
return reinterpret_cast<intptr_t>(time_info_pointer);
} break;
default:
return DefaultDataConverter::return_value(opcode, original);
break;
}
}
void write_value(const int opcode,
intptr_t value,
const EventResult& response) {
return DefaultDataConverter::write_value(opcode, value, response);
}
private:
AEffect* plugin;
std::optional<VstTimeInfo>& time_info;
};
intptr_t WineBridge::host_callback(AEffect* effect,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
HostCallbackDataConverter converter(effect, time_info);
return send_event(vst_host_callback, host_callback_mutex, converter,
std::nullopt, opcode, index, value, data, option);
}
intptr_t VST_CALL_CONV host_callback_proxy(AEffect* effect,
int opcode,
int index,
intptr_t value,
void* data,
float option) {
return get_bridge_instance(effect).host_callback(effect, opcode, index,
value, data, option);
}
uint32_t WINAPI handle_dispatch_midi_events_proxy(void* instance) {
static_cast<WineBridge*>(instance)->handle_dispatch_midi_events();
}
uint32_t WINAPI handle_parameters_proxy(void* instance) {
static_cast<WineBridge*>(instance)->handle_parameters();
}
uint32_t WINAPI handle_process_replacing_proxy(void* instance) {
static_cast<WineBridge*>(instance)->handle_process_replacing();
}
+3
View File
@@ -1 +1,4 @@
/*/*
# The above pattern doesn't match submodules
/tomlplusplus
+4
View File
@@ -0,0 +1,4 @@
[wrap-git]
url = https://github.com/marzer/tomlplusplus.git
revision = v1.2.5
depth = 1