diff --git a/src/common/communication.h b/src/common/communication.h index 658a8fab..10ea422c 100644 --- a/src/common/communication.h +++ b/src/common/communication.h @@ -364,7 +364,7 @@ class EventHandler { // `next_plugin_id` in `GroupBridge` std::map active_secondary_requests{}; std::atomic_size_t next_request_id{}; - std::mutex active_secondary_requests_mutex; + std::mutex active_secondary_requests_mutex{}; accept_requests( *acceptor, logging, [&](boost::asio::local::stream_protocol::socket secondary_socket) { diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index ebb24028..e8620ed0 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -29,11 +29,6 @@ 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: @@ -94,10 +89,9 @@ GroupBridge::GroupBridge(boost::filesystem::path group_socket_path) 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) { + group_socket_acceptor(create_acceptor_if_inactive(plugin_context.context, + group_socket_endpoint)), + shutdown_timer(plugin_context.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? @@ -131,7 +125,7 @@ void GroupBridge::handle_plugin_dispatch(size_t plugin_id) { // potentially corrupt our heap. This way we can also properly join the // thread again. If no active plugins remain, then we'll terminate the // process. - boost::asio::post(plugin_context, [this, plugin_id]() { + boost::asio::post(plugin_context.context, [this, plugin_id]() { std::lock_guard lock(active_plugins_mutex); // The join is implicit because we're using std::jthread @@ -240,16 +234,7 @@ void GroupBridge::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; - } - + plugin_context.async_handle_events([&]() { { // Always handle X11 events std::lock_guard lock(active_plugins_mutex); @@ -279,8 +264,6 @@ void GroupBridge::async_handle_events() { DispatchMessage(&msg); } } - - async_handle_events(); }); } diff --git a/src/wine-host/bridges/group.h b/src/wine-host/bridges/group.h index cf7690e8..52b43138 100644 --- a/src/wine-host/bridges/group.h +++ b/src/wine-host/bridges/group.h @@ -217,7 +217,7 @@ class GroupBridge { * 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; + PluginContext plugin_context; /** * A seperate IO context that handles the STDIO redirect through * `StdIoCapture`. This is seperated the `plugin_context` above so that @@ -279,14 +279,6 @@ class GroupBridge { */ 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 diff --git a/src/wine-host/bridges/vst2.cpp b/src/wine-host/bridges/vst2.cpp index 6d639f98..f205fa26 100644 --- a/src/wine-host/bridges/vst2.cpp +++ b/src/wine-host/bridges/vst2.cpp @@ -64,13 +64,13 @@ Vst2Bridge& get_bridge_instance(const AEffect* plugin) { return *static_cast(plugin->ptr1); } -Vst2Bridge::Vst2Bridge(boost::asio::io_context& main_context, +Vst2Bridge::Vst2Bridge(PluginContext& main_context, std::string plugin_dll_path, std::string endpoint_base_dir) : vst_plugin_path(plugin_dll_path), - io_context(main_context), + plugin_context(main_context), plugin_handle(LoadLibrary(plugin_dll_path.c_str()), FreeLibrary), - sockets(io_context, endpoint_base_dir, false) { + sockets(plugin_context.context, endpoint_base_dir, false) { // Got to love these C APIs if (!plugin_handle) { throw std::runtime_error("Could not load the Windows .dll file at '" + @@ -158,13 +158,14 @@ void Vst2Bridge::handle_dispatch() { // `plugin->dispatcher()` (or `dispatch_wrapper()`) // directly, we'll run the function within the IO context so // all events will be executed on the same thread as the one - // that runs the Win32 message loop. We'll assume every - // event that comes in while the main thread is already - // handling an event in the IO context can safely be handled - // on an off thread. - if (on_main_thread) { + // that runs the Win32 message loop. In some scenarios we'll + // receive incoming calls from multiple threads or we'll + // receive calls while we're currently stuck in the Win32 + // message loop. In those cases we'll assume that these + // events can be safely handled directlyfrom another thread. + if (on_main_thread && !plugin_context.event_loop_active) { std::promise dispatch_result; - boost::asio::dispatch(io_context, [&]() { + boost::asio::dispatch(plugin_context.context, [&]() { const intptr_t result = dispatch_wrapper( plugin, opcode, index, value, data, option); diff --git a/src/wine-host/bridges/vst2.h b/src/wine-host/bridges/vst2.h index 0a0ada14..7f12527f 100644 --- a/src/wine-host/bridges/vst2.h +++ b/src/wine-host/bridges/vst2.h @@ -74,7 +74,7 @@ class Vst2Bridge { * @throw std::runtime_error Thrown when the VST plugin could not be loaded, * or if communication could not be set up. */ - Vst2Bridge(boost::asio::io_context& main_context, + Vst2Bridge(PluginContext& main_context, std::string plugin_dll_path, std::string endpoint_base_dir); @@ -172,7 +172,7 @@ class Vst2Bridge { * message handling can be performed from a single thread, even when hosting * multiple plugins. */ - boost::asio::io_context& io_context; + PluginContext& plugin_context; /** * The configuration for this instance of yabridge based on the `.so` file diff --git a/src/wine-host/individual-host.cpp b/src/wine-host/individual-host.cpp index 3f93e46b..087372f0 100644 --- a/src/wine-host/individual-host.cpp +++ b/src/wine-host/individual-host.cpp @@ -24,19 +24,6 @@ #include "../common/utils.h" #include "bridges/vst2.h" -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; - -/** - * Handle both Win32 and X11 events on a timer. This is more or less a - * simplified version of `GroupBridge::async_handle_events`. - */ -void async_handle_events(boost::asio::steady_timer& timer, Vst2Bridge& bridge); - /** * This is the default VST host application. It will load the specified VST2 * plugin, and then connect back to the `libyabridge.so` instance that spawned @@ -80,10 +67,10 @@ int __cdecl main(int argc, char* argv[]) { // setup is slightly more convoluted than it has to be, but doing it this // way we don't need to differentiate between individually hosted plugins // and plugin groups when it comes to event handling. - boost::asio::io_context io_context{}; + PluginContext plugin_context{}; std::unique_ptr bridge; try { - bridge = std::make_unique(io_context, plugin_dll_path, + bridge = std::make_unique(plugin_context, plugin_dll_path, socket_endpoint_path); } catch (const std::runtime_error& error) { std::cerr << "Error while initializing Wine VST host:" << std::endl; @@ -101,25 +88,9 @@ int __cdecl main(int argc, char* argv[]) { // Handle Win32 messages and X11 events on a timer, just like in // `GroupBridge::async_handle_events()`` - boost::asio::steady_timer events_timer(io_context); - async_handle_events(events_timer, *bridge); - - io_context.run(); -} - -void async_handle_events(boost::asio::steady_timer& timer, Vst2Bridge& bridge) { - // 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. - timer.expires_at(std::max(timer.expiry() + event_loop_interval, - std::chrono::steady_clock::now() + 5ms)); - timer.async_wait([&](const boost::system::error_code& error) { - if (error.failed()) { - return; - } - - bridge.handle_x11_events(); - bridge.handle_win32_events(); - - async_handle_events(timer, bridge); + plugin_context.async_handle_events([&]() { + bridge->handle_x11_events(); + bridge->handle_win32_events(); }); + plugin_context.run(); } diff --git a/src/wine-host/utils.cpp b/src/wine-host/utils.cpp index cc26ffb4..3ef90392 100644 --- a/src/wine-host/utils.cpp +++ b/src/wine-host/utils.cpp @@ -16,6 +16,16 @@ #include "utils.h" +PluginContext::PluginContext() : context(), events_timer(context) {} + +void PluginContext::run() { + context.run(); +} + +void PluginContext::stop() { + context.stop(); +} + Win32Thread::Win32Thread() : handle(nullptr, nullptr) {} Win32Timer::Win32Timer(HWND window_handle, diff --git a/src/wine-host/utils.h b/src/wine-host/utils.h index 14826e85..20d52bf3 100644 --- a/src/wine-host/utils.h +++ b/src/wine-host/utils.h @@ -16,6 +16,8 @@ #pragma once +#include "boost-fix.h" + #include #include @@ -26,6 +28,91 @@ #define WIN32_LEAN_AND_MEAN #include +#include + +/** + * The delay between calls to the event loop at an even more than cinematic 30 + * fps. + */ +constexpr std::chrono::duration event_loop_interval = + std::chrono::milliseconds(1000) / 30; + +/** + * A wrapper around `boost::asio::io_context()`. A single instance is shared for + * all plugins in a plugin group so that most events can be handled on the main + * thread, which can be required because all GUI related operations have to be + * handled from the same thread. If during the Win32 message loop the plugin + * performs a host callback and the host then calls a function on the plugin in + * response, then this IO context will still be busy with the message loop + * which. To prevent a deadlock in this situation, we'll allow different threads + * to handle `dispatch()` calls while the message loop is running. + */ +class PluginContext { + public: + PluginContext(); + + /** + * Run the IO context. This rest of this class assumes that this is only + * done from a single thread. + */ + void run(); + + /** + * Drop all future work from the IO context. This does not necessarily mean + * that the thread that called `plugin_context.run()` immediatly returns. + */ + void stop(); + + /** + * Start a timer to handle events every `event_loop_interval` milliseconds. + * `message_loop_active()` will return `true` while `handler` is being + * executed. + * + * @param handler The function that should be executed in the IO context + * when the timer ticks. This should be a function that handles both the + * X11 events and the Win32 message loop. + */ + template + void async_handle_events(F handler) { + // 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() + std::chrono::milliseconds(5))); + events_timer.async_wait( + [&, handler](const boost::system::error_code& error) { + if (error.failed()) { + return; + } + + event_loop_active = true; + handler(); + event_loop_active = false; + + async_handle_events(handler); + }); + } + + /** + * Is `true` if the context is currently handling the Win32 message loop and + * incoming `dispatch()` events should be handled on their own thread (as + * posting them to the IO context will thus block). + */ + std::atomic_bool event_loop_active; + + /** + * The raw IO context. Can and should be used directly for everything that's + * not the event handling loop. + */ + boost::asio::io_context context; + + private: + /** + * The timer used to periodically handle X11 events and Win32 messages. + */ + boost::asio::steady_timer events_timer; +}; + /** * A simple RAII wrapper around the Win32 thread API. *