diff --git a/CHANGELOG.md b/CHANGELOG.md index 1717ec3a..7839be98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). - Changed the way mutual recursion in VST3 plugins on the plugin side works to counter any potential GUI related timing issues with VST3 plugins when using multiple instances of a plugin. +- Changed the way realtime scheduling is used on the Wine side to be less + aggressive, potentially reducing CPU usage when plugins are idle. - The deserialization part of yabridge's communication is now slightly faster by skipping some unnecessary checks. - Log messages about VST3 query interfaces are now only printed when diff --git a/src/wine-host/bridges/group.cpp b/src/wine-host/bridges/group.cpp index 89cb268e..7c317bc1 100644 --- a/src/wine-host/bridges/group.cpp +++ b/src/wine-host/bridges/group.cpp @@ -103,13 +103,7 @@ GroupBridge::GroupBridge(boost::filesystem::path group_socket_path) logger.async_log_pipe_lines(stderr_redirect.pipe, stderr_buffer, "[STDERR] "); - stdio_handler = Win32Thread([&]() { - // In case a plugin generates a lot of FIXMEs relaying this IO with - // realtime scheduling could in theory cause latency issues - set_realtime_priority(false); - - stdio_context.run(); - }); + stdio_handler = Win32Thread([&]() { stdio_context.run(); }); } GroupBridge::~GroupBridge() noexcept { diff --git a/src/wine-host/bridges/vst2.cpp b/src/wine-host/bridges/vst2.cpp index 513d6dfe..c6cb708a 100644 --- a/src/wine-host/bridges/vst2.cpp +++ b/src/wine-host/bridges/vst2.cpp @@ -96,6 +96,15 @@ static const std::set unsafe_requests{ effOpen, effClose, effEditGetRect, effEditOpen, effEditClose, effEditIdle, effEditTop, effMainsChanged, effGetChunk, effSetChunk}; +/** + * These opcodes from `unsafe_requests` should be run under realtime scheduling + * so that if they spawn audio worker threads, those threads will also be run + * with `SCHED_FIFO`. This is needed because unpatched Wine still does not + * implement thread priorities. Normally these unsafe requests are run on the + * main thread, which doesn't use realtime scheduling. + */ +static const std::set unsafe_requests_realtime{effOpen, effMainsChanged}; + intptr_t VST_CALL_CONV host_callback_proxy(AEffect*, int, int, intptr_t, void*, float); @@ -174,9 +183,17 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context, // Note that this reinterpret cast is not needed at all since the function // pointer types are exactly the same, but clangd will complain otherwise current_bridge_instance = this; + + // We'll also need to make sure that any audio worker threads created by the + // plugin are running using realtime scheduling, since Wine doesn't fully + // implement the Win32 process priority API yet. + set_realtime_priority(true); plugin = vst_entry_point( reinterpret_cast(host_callback_proxy)); + set_realtime_priority(false); + if (!plugin) { + set_realtime_priority(false); throw std::runtime_error("VST plugin at '" + plugin_dll_path + "' failed to initialize."); } @@ -203,6 +220,8 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context, main_context.update_timer_interval(config.event_loop_interval()); parameters_handler = Win32Thread([&]() { + set_realtime_priority(true); + sockets.host_vst_parameters.receive_multi( [&](Parameter& request, SerializationBufferBase& buffer) { // Both `getParameter` and `setParameter` functions are passed @@ -226,6 +245,8 @@ Vst2Bridge::Vst2Bridge(MainContext& main_context, }); process_replacing_handler = Win32Thread([&]() { + set_realtime_priority(true); + // Most plugins will already enable FTZ, but there are a handful of // plugins that don't that suffer from extreme DSP load increases when // they start producing denormals @@ -368,6 +389,8 @@ bool Vst2Bridge::inhibits_event_loop() noexcept { } void Vst2Bridge::run() { + set_realtime_priority(true); + sockets.host_vst_dispatch.receive_events( std::nullopt, [&](Vst2Event& event, bool /*on_main_thread*/) { if (event.opcode == effProcessEvents) { @@ -418,12 +441,26 @@ void Vst2Bridge::run() { // where the plugins were instantiated and where the // Win32 message loop is handled. if (unsafe_requests.contains(opcode)) { + // Requests that potentially spawn an audio worker + // thread should be run with `SCHED_FIFO` until Wine + // implements the corresponding Windows API + const bool is_realtime_request = + unsafe_requests_realtime.contains(opcode); + return main_context .run_in_context([&]() -> intptr_t { + if (is_realtime_request) { + set_realtime_priority(true); + } + const intptr_t result = dispatch_wrapper(plugin, opcode, index, value, data, option); + if (is_realtime_request) { + set_realtime_priority(false); + } + // The Win32 message loop will not be run up // to this point to prevent plugins with // partially initialized states from @@ -475,8 +512,12 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, 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 + // We have to intercept GUI open calls since we can't use the X11 window + // handle passed by the host. Keep in mind that in our `run()` function + // above some of these events will be called on some arbitrary thread (where + // we're running with realtime scheduling) and some might be called on the + // main thread using `main_context.run_in_context()` (where we don't use + // realtime scheduling). switch (opcode) { case effEditOpen: { // Create a Win32 window through Wine, embed it into the window @@ -484,11 +525,6 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, // the Wine window const auto x11_handle = reinterpret_cast(data); - // NOTE: Just like in the event loop, we want to run this with lower - // priority to prevent whatever operation the plugin does - // while it's loading its editor from preempting the audio - // thread. - set_realtime_priority(false); Editor& editor_instance = editor.emplace( main_context, config, x11_handle, [plugin = this->plugin]() { plugin->dispatcher(plugin, effEditIdle, 0, 0, nullptr, 0.0); @@ -496,25 +532,14 @@ intptr_t Vst2Bridge::dispatch_wrapper(AEffect* plugin, const intptr_t result = plugin->dispatcher(plugin, opcode, index, value, editor_instance.get_win32_handle(), option); - set_realtime_priority(true); return result; } break; case effEditClose: { // Cleanup is handled through RAII - set_realtime_priority(false); const intptr_t return_value = plugin->dispatcher(plugin, opcode, index, value, data, option); editor.reset(); - set_realtime_priority(true); - - return return_value; - } break; - case effEditGetRect: { - set_realtime_priority(false); - const intptr_t return_value = - plugin->dispatcher(plugin, opcode, index, value, data, option); - set_realtime_priority(true); return return_value; } break; diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index 277cd725..d5ace1c6 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -132,6 +132,8 @@ bool Vst3Bridge::inhibits_event_loop() noexcept { } void Vst3Bridge::run() { + set_realtime_priority(true); + // XXX: In theory all of thise should be safe assuming the host doesn't do // anything weird. We're using mutexes when inserting and removing // things, but for correctness we should have a multiple-readers @@ -153,12 +155,10 @@ void Vst3Bridge::run() { // drop it here as well, along with the `IPlugFrame` // proxy object it may have received in // `IPlugView::setFrame()`. - set_realtime_priority(false); object_instances[request.owner_instance_id] .plug_view_instance.reset(); object_instances[request.owner_instance_id] .plug_frame_proxy.reset(); - set_realtime_priority(true); }) .wait(); @@ -179,28 +179,43 @@ void Vst3Bridge::run() { // shouldn't) Steinberg::IPtr object = main_context - .run_in_context([&]() -> Steinberg::IPtr< - Steinberg::FUnknown> { - switch (request.requested_interface) { - case Vst3PluginProxy::Construct::Interface:: - IComponent: - return module->getFactory() - .createInstance< - Steinberg::Vst::IComponent>(cid); - break; - case Vst3PluginProxy::Construct::Interface:: - IEditController: - return module->getFactory() - .createInstance< - Steinberg::Vst::IEditController>( - cid); - break; - default: - // Unreachable - return nullptr; - break; - } - }) + .run_in_context( + [&]() -> Steinberg::IPtr { + Steinberg::IPtr result; + + // The plugin may spawn audio worker threads + // when constructing an object. Since Wine + // doesn't implement Window's realtime process + // priority yet we'll just have to make sure the + // any spawned threads are running with + // `SCHED_FIFO` ourselves. + set_realtime_priority(true); + switch (request.requested_interface) { + case Vst3PluginProxy::Construct::Interface:: + IComponent: + result = + module->getFactory() + .createInstance< + Steinberg::Vst::IComponent>( + cid); + break; + case Vst3PluginProxy::Construct::Interface:: + IEditController: + result = + module->getFactory() + .createInstance< + Steinberg::Vst:: + IEditController>(cid); + break; + default: + // Unreachable + result = nullptr; + break; + } + set_realtime_priority(false); + + return result; + }) .get(); if (!object) { @@ -465,17 +480,11 @@ void Vst3Bridge::run() { // Instantiate the object from the GUI thread main_context .run_in_context([&]() -> void { - // NOTE: Just like in the event loop, we want to run - // this with lower priority to prevent whatever - // operation the plugin does while it's loading - // its editor from preempting the audio thread. - set_realtime_priority(false); object_instances[request.instance_id] .plug_view_instance.emplace(Steinberg::owned( object_instances[request.instance_id] .edit_controller->createView( request.name.c_str()))); - set_realtime_priority(true); }) .wait(); @@ -702,7 +711,6 @@ void Vst3Bridge::run() { // be done in the main UI thread return main_context .run_in_context([&]() -> tresult { - set_realtime_priority(false); Editor& editor_instance = object_instances[request.owner_instance_id] .editor.emplace(main_context, config, @@ -712,7 +720,6 @@ void Vst3Bridge::run() { .plug_view_instance->plug_view->attached( editor_instance.get_win32_handle(), type.c_str()); - set_realtime_priority(true); // Get rid of the editor again if the plugin didn't // embed itself in it @@ -730,13 +737,11 @@ void Vst3Bridge::run() { return main_context .run_in_context([&]() -> tresult { // Cleanup is handled through RAII - set_realtime_priority(false); const tresult result = object_instances[request.owner_instance_id] .plug_view_instance->plug_view->removed(); object_instances[request.owner_instance_id] .editor.reset(); - set_realtime_priority(true); return result; }) @@ -902,6 +907,9 @@ void Vst3Bridge::run() { // functions from the main GUI thread return main_context .run_in_context([&]() -> tresult { + // The plugin may try to spawn audio worker threads + // during its initialization + set_realtime_priority(true); // This static cast is required to upcast to `FUnknown*` const tresult result = object_instances[request.instance_id] @@ -909,6 +917,7 @@ void Vst3Bridge::run() { static_cast( object_instances[request.instance_id] .host_context_proxy)); + set_realtime_priority(false); // The Win32 message loop will not be run up to this // point to prevent plugins with partially initialized @@ -1172,6 +1181,8 @@ size_t Vst3Bridge::register_object_instance( object_instances[instance_id] .audio_processor_handler = Win32Thread([&, instance_id]() { + set_realtime_priority(true); + sockets.add_audio_processor_and_listen( instance_id, socket_listening_latch, overload{ diff --git a/src/wine-host/group-host.cpp b/src/wine-host/group-host.cpp index 1bac341c..a38a56e4 100644 --- a/src/wine-host/group-host.cpp +++ b/src/wine-host/group-host.cpp @@ -41,8 +41,6 @@ int __attribute__((visibility("default"))) __cdecl #endif main(int argc, char* argv[]) { - set_realtime_priority(true); - // 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. diff --git a/src/wine-host/individual-host.cpp b/src/wine-host/individual-host.cpp index 9a37c169..84dc1d22 100644 --- a/src/wine-host/individual-host.cpp +++ b/src/wine-host/individual-host.cpp @@ -37,8 +37,6 @@ int __attribute__((visibility("default"))) __cdecl #endif main(int argc, char* argv[]) { - set_realtime_priority(true); - // We pass the plugin format, the name of the VST2 plugin .dll file or VST3 // bundle to load, the base directory for the Unix domain socket endpoints // to connect to and the process ID of the process the native plugin is diff --git a/src/wine-host/utils.cpp b/src/wine-host/utils.cpp index da01cb7d..e3528d89 100644 --- a/src/wine-host/utils.cpp +++ b/src/wine-host/utils.cpp @@ -86,10 +86,7 @@ MainContext::MainContext() // we'll run the timer on a 30 second interval. async_handle_watchdog_timer(5s); - watchdog_handler = Win32Thread([&]() { - set_realtime_priority(false); - watchdog_context.run(); - }); + watchdog_handler = Win32Thread([&]() { watchdog_context.run(); }); } void MainContext::run() { diff --git a/src/wine-host/utils.h b/src/wine-host/utils.h index 510a57ea..81254dd7 100644 --- a/src/wine-host/utils.h +++ b/src/wine-host/utils.h @@ -291,18 +291,7 @@ class MainContext { } if (predicate()) { - // NOTE: These periodic callbacks should not be able to - // interrupt other threads that are actively - // processing audio. For me personally having the GUI - // open makes absolutely zero difference on DSP usage - // (as it should), but for some others it does have an - // impact. - // TODO: Benchmark this further on a properly configured - // system, see if it does not increase average load - // because of the rapid scheduling switching. - set_realtime_priority(false); handler(); - set_realtime_priority(true); } async_handle_events(handler, predicate);