Make realtime scheduling less aggressive

On the Wine side. Instead of always having it enabled and disabling it
when it could potentially hurt (i.e. when handling GUI related things),
we'll now only enable it when it's potentially beneficial. This way we
don't have to constantly switch scheduling policies on the GUI thread.
This commit is contained in:
Robbert van der Helm
2021-06-01 16:21:38 +02:00
parent 5f4ffed90b
commit 712ef41a7f
8 changed files with 92 additions and 78 deletions
+2
View File
@@ -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
+1 -7
View File
@@ -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 {
+43 -18
View File
@@ -96,6 +96,15 @@ static const std::set<int> 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<int> 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<audioMasterCallback>(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>(
[&](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<size_t>(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;
+45 -34
View File
@@ -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<Steinberg::FUnknown> 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::FUnknown> {
Steinberg::IPtr<Steinberg::FUnknown> 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<YaHostApplication*>(
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{
-2
View File
@@ -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.
-2
View File
@@ -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
+1 -4
View File
@@ -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() {
-11
View File
@@ -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);