diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec90a0c4..018359f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ed748f..5f15c613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index e1bbe9cf..f42f4437 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..75cbcd98 --- /dev/null +++ b/docs/architecture.md @@ -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. diff --git a/meson.build b/meson.build index 1a6c8c5b..83da7c3a 100644 --- a/meson.build +++ b/meson.build @@ -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 diff --git a/src/common/communication.h b/src/common/communication.h index 8cea096e..22ac3516 100644 --- a/src/common/communication.h +++ b/src/common/communication.h @@ -41,6 +41,10 @@ using InputAdapter = bitsery::InputBufferAdapter; * @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 diff --git a/src/common/config/config.h.in b/src/common/config/config.h.in index 8f225016..358e9073 100644 --- a/src/common/config/config.h.in +++ b/src/common/config/config.h.in @@ -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@"; diff --git a/src/common/config/meson.build b/src/common/config/meson.build index eb2cd4f6..6c88ee17 100644 --- a/src/common/config/meson.build +++ b/src/common/config/meson.build @@ -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', } ) ) diff --git a/src/common/logging.cpp b/src/common/logging.cpp index 6950dd26..5d373c31 100644 --- a/src/common/logging.cpp +++ b/src/common/logging.cpp @@ -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::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("/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; } diff --git a/src/common/serialization.cpp b/src/common/serialization.cpp index cb9662d3..4a5e14d4 100644 --- a/src/common/serialization.cpp +++ b/src/common/serialization.cpp @@ -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; +} diff --git a/src/common/serialization.h b/src/common/serialization.h index 2c1fe7f7..c82c0def 100644 --- a/src/common/serialization.h +++ b/src/common/serialization.h @@ -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 + void serialize(S& s) { + s.text1b(plugin_path, 4096); + s.text1b(socket_path, 4096); + } +}; + +template <> +struct std::hash { + std::size_t operator()(GroupRequest const& params) const noexcept { + std::hash 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 + void serialize(S& s) { + s.value4b(pid); + } +}; diff --git a/src/plugin/configuration.cpp b/src/plugin/configuration.cpp new file mode 100644 index 00000000..a7fd2984 --- /dev/null +++ b/src/plugin/configuration.cpp @@ -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 . + +#include "configuration.h" + +#include +#include +#include + +#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(); + } + + 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 config_file = + find_dominating_file("yabridge.toml", yabridge_path); + if (!config_file.has_value()) { + return Configuration(); + } + + return Configuration(config_file.value(), yabridge_path); +} diff --git a/src/plugin/configuration.h b/src/plugin/configuration.h new file mode 100644 index 00000000..f3e51bb1 --- /dev/null +++ b/src/plugin/configuration.h @@ -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 . + +#pragma once + +#include +#include + +/** + * 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 -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 + * [""] + * 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 group; + + /** + * The path to the configuration file that was parsed. + */ + std::optional matched_file; + + /** + * The matched glob pattern in the above configuration file. + */ + std::optional matched_pattern; +}; diff --git a/src/plugin/plugin-bridge.cpp b/src/plugin/plugin-bridge.cpp index ea90240b..e6816f3e 100644 --- a/src/plugin/plugin-bridge.cpp +++ b/src/plugin/plugin-bridge.cpp @@ -19,11 +19,8 @@ #include #include #include -#include - -#ifdef USE_WINEDBG #include -#endif +#include // Generated inside of build directory #include @@ -52,16 +49,21 @@ PluginBridge& get_bridge_instance(const AEffect& plugin) { return *static_cast(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("").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(" "); -#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, 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 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 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(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(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("").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("").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 << " " << 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 diff --git a/src/plugin/plugin-bridge.h b/src/plugin/plugin-bridge.h index b180c695..a4709b35 100644 --- a/src/plugin/plugin-bridge.h +++ b/src/plugin/plugin-bridge.h @@ -26,6 +26,7 @@ #include #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 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 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 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 diff --git a/src/plugin/utils.cpp b/src/plugin/utils.cpp index e464ebfb..af28b3ff 100644 --- a/src/plugin/utils.cpp +++ b/src/plugin/utils.cpp @@ -28,6 +28,8 @@ // Generated inside of build directory #include +#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 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 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{}(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() { diff --git a/src/plugin/utils.h b/src/plugin/utils.h index c2991080..2cbad0a0 100644 --- a/src/plugin/utils.h +++ b/src/plugin/utils.h @@ -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 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---.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 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 +std::optional 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; +} diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp new file mode 100644 index 00000000..1251c3ba --- /dev/null +++ b/src/wine-host/bridges/group.cpp @@ -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 . + +#include "group.h" + +#include +#include +#include +#include + +#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; + +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(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(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(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---.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(param); + GroupBridge* instance = thread_params->first; + GroupRequest parameters = thread_params->second; + delete thread_params; + + instance->handle_plugin_dispatch(parameters); + + return 0; +} diff --git a/src/wine-host/bridges/group.h b/src/wine-host/bridges/group.h new file mode 100644 index 00000000..ae610964 --- /dev/null +++ b/src/wine-host/bridges/group.h @@ -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 . + +#pragma once + +#include "../boost-fix.h" + +#include +#include +#include + +#include + +#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---.sock` + * where `` 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>> + 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; +}; diff --git a/src/wine-host/bridges/vst2.cpp b/src/wine-host/bridges/vst2.cpp new file mode 100644 index 00000000..92e98785 --- /dev/null +++ b/src/wine-host/bridges/vst2.cpp @@ -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 . + +#include "vst2.h" + +#include +#include +#include + +#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(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( + reinterpret_cast(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(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 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(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(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> output_buffers(plugin->numOutputs); + + try { + while (true) { + auto request = read_object(host_vst_process_replacing, + process_buffer); + + // The process functions expect a `float**` for their inputs and + // their outputs + std::vector 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 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& 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 = 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(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(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& 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(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 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(response.payload)) { + time_info = std::nullopt; + } else { + time_info = std::get(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(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& 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(instance)->handle_dispatch_midi_events(); + return 0; +} + +uint32_t WINAPI handle_parameters_proxy(void* instance) { + static_cast(instance)->handle_parameters(); + return 0; +} + +uint32_t WINAPI handle_process_replacing_proxy(void* instance) { + static_cast(instance)->handle_process_replacing(); + return 0; +} diff --git a/src/wine-host/wine-bridge.h b/src/wine-host/bridges/vst2.h similarity index 57% rename from src/wine-host/wine-bridge.h rename to src/wine-host/bridges/vst2.h index b48c2546..5bd1fc93 100644 --- a/src/wine-host/wine-bridge.h +++ b/src/wine-host/bridges/vst2.h @@ -16,7 +16,7 @@ #pragma once -#include "boost-fix.h" +#include "../boost-fix.h" #define NOMINMAX #define NOSERVICE @@ -29,23 +29,34 @@ #include #include -#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, 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, 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 editor; }; diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 7279e2f3..1e329355 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -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(&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(&translated_event)); + xcb_flush(x11_connection.get()); +} + LRESULT CALLBACK window_proc(HWND handle, UINT message, WPARAM wParam, diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 1d127665..99c083e1 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -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. diff --git a/src/wine-host/group-host.cpp b/src/wine-host/group-host.cpp new file mode 100644 index 00000000..4f1b9950 --- /dev/null +++ b/src/wine-host/group-host.cpp @@ -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 . + +#include "boost-fix.h" + +#include + +// Generated inside of build directory +#include +#include + +#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 + << " " << 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; + } +} diff --git a/src/wine-host/vst-host.cpp b/src/wine-host/individual-host.cpp similarity index 77% rename from src/wine-host/vst-host.cpp rename to src/wine-host/individual-host.cpp index 2d6e059c..697cedcb 100644 --- a/src/wine-host/vst-host.cpp +++ b/src/wine-host/individual-host.cpp @@ -20,11 +20,16 @@ #include #include -#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 << " " << 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; diff --git a/src/wine-host/wine-bridge.cpp b/src/wine-host/wine-bridge.cpp deleted file mode 100644 index 800339b4..00000000 --- a/src/wine-host/wine-bridge.cpp +++ /dev/null @@ -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 . - -#include "wine-bridge.h" - -#include - -#include - -#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(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(reinterpret_cast( - 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(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(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> output_buffers(plugin->numOutputs); - - while (true) { - auto request = read_object(host_vst_process_replacing, - process_buffer); - - // The process functions expect a `float**` for their inputs and - // their outputs - std::vector 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 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& 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 = 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(data); - Editor& editor_instance = - editor.emplace("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& 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(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 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(response.payload)) { - time_info = std::nullopt; - } else { - time_info = std::get(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(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& 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(instance)->handle_dispatch_midi_events(); -} - -uint32_t WINAPI handle_parameters_proxy(void* instance) { - static_cast(instance)->handle_parameters(); -} - -uint32_t WINAPI handle_process_replacing_proxy(void* instance) { - static_cast(instance)->handle_process_replacing(); -} diff --git a/subprojects/.gitignore b/subprojects/.gitignore index ea053e16..21ced76b 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -1 +1,4 @@ /*/* + +# The above pattern doesn't match submodules +/tomlplusplus diff --git a/subprojects/tomlplusplus.wrap b/subprojects/tomlplusplus.wrap new file mode 100644 index 00000000..15bb40d5 --- /dev/null +++ b/subprojects/tomlplusplus.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/marzer/tomlplusplus.git +revision = v1.2.5 +depth = 1