From 198807a15a9a91d4457a9fd37371c908e3a14662 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 26 May 2020 12:09:27 +0200 Subject: [PATCH] Run events async and centralized for group hosts At a 30 fps rate with a limit on the number of window messages per frame. This is somehow needed for Melda plugins, as they otherwise dispatch tiemr messages indefinitely after opening a second editor with seemingly no way around it. With this and some refactoring #15 should be almost done. --- src/wine-host/bridges/group.cpp | 64 ++++++++++++++++++++++++++++----- src/wine-host/bridges/group.h | 15 ++++++++ src/wine-host/bridges/vst2.cpp | 49 +++++++++++++++++++++---- src/wine-host/bridges/vst2.h | 58 +++++------------------------- 4 files changed, 122 insertions(+), 64 deletions(-) diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index 607201fe..1251c3ba 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -27,6 +27,13 @@ // 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: @@ -98,6 +105,7 @@ GroupBridge::GroupBridge(boost::filesystem::path group_socket_path) 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 @@ -114,19 +122,13 @@ GroupBridge::~GroupBridge() { } void GroupBridge::handle_plugin_dispatch(const GroupRequest request) { - using namespace std::literals::chrono_literals; - // 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 the main thread until the plugin shuts down, handling all events - // on the main IO context - // TODO: Maybe add a function to each vstbridge that returns whether the - // message loop should be postponed, and pass a function here that - // checks if this is the case for any of the plugins? - bridge->handle_dispatch_multi(plugin_context, - [&]() { return should_skip_message_loop(); }); + // 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 @@ -156,6 +158,7 @@ void GroupBridge::handle_plugin_dispatch(const GroupRequest request) { void GroupBridge::handle_incoming_connections() { accept_requests(); + async_handle_events(); logger.log( "Group host is up and running, now accepting incoming connections"); @@ -233,6 +236,49 @@ 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; + } + + { + // 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, diff --git a/src/wine-host/bridges/group.h b/src/wine-host/bridges/group.h index 87fd9d07..ae610964 100644 --- a/src/wine-host/bridges/group.h +++ b/src/wine-host/bridges/group.h @@ -183,6 +183,13 @@ class GroupBridge { */ 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`. @@ -268,6 +275,14 @@ 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 7ebd852b..b80688f3 100644 --- a/src/wine-host/bridges/vst2.cpp +++ b/src/wine-host/bridges/vst2.cpp @@ -16,9 +16,13 @@ #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. */ @@ -156,12 +160,7 @@ void Vst2Bridge::handle_dispatch_single() { plugin, std::bind(&Vst2Bridge::dispatch_wrapper, this, _1, _2, _3, _4, _5, _6))); - // Don't run them message loop during the two step process of - // opening the plugin editor since some plugins don't expect this - if (!should_skip_message_loop()) { - handle_win32_events(); - } - + handle_win32_events(); handle_x11_events(); } } catch (const boost::system::system_error&) { @@ -170,6 +169,38 @@ void Vst2Bridge::handle_dispatch_single() { } } +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) { @@ -381,6 +412,12 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, } void Vst2Bridge::handle_win32_events() { + // Don't run them message loop during the two step process of opening the + // plugin editor since some plugins don't expect this + if (should_skip_message_loop()) { + return; + } + if (editor.has_value()) { editor->handle_win32_events(); } else { diff --git a/src/wine-host/bridges/vst2.h b/src/wine-host/bridges/vst2.h index 4a35db09..5abfd252 100644 --- a/src/wine-host/bridges/vst2.h +++ b/src/wine-host/bridges/vst2.h @@ -26,13 +26,9 @@ #include #include -#include #include -#include #include -#include "../../common/communication.h" -#include "../../common/events.h" #include "../../common/logging.h" #include "../editor.h" #include "../utils.h" @@ -101,50 +97,20 @@ class Vst2Bridge { * * @param main_context The main IO context that's handling the event * handling for all plugins. - * @param message_loop_blocked A function that returns true if the message - * loop is blocked. This is used to temporarily postpone running the - * message loop while a plugin is opening its GUI. * * @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. */ - template - void handle_dispatch_multi(boost::asio::io_context& main_context, - const F& message_loop_blocked) { - // 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); + void handle_dispatch_multi(boost::asio::io_context& main_context); - dispatch_result.set_value(result); - if (!message_loop_blocked()) { - handle_win32_events(); - } - - handle_x11_events(); - }); - - 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 - } - } + /** + * 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 @@ -197,12 +163,6 @@ class Vst2Bridge { */ void handle_win32_events(); - /** - * Handle X11 events for the editor window if it is open. This can be run - * safely from any thread. - */ - void handle_x11_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 plugisn by hand.