From d1ef29aa3ed969597cc68a4cca63a7d8cb500192 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Wed, 26 Oct 2022 23:35:54 +0200 Subject: [PATCH] Implement the CLAP timer-support extension This is entirely implemented on the Wine side. I'll assume most Windows plugins will use their own timers instead, but this could be useful for plugins that try to use the same interface on all platforms. --- src/common/serialization/clap/README.md | 2 +- .../bridges/clap-impls/host-proxy.cpp | 102 ++++++++++++++++++ src/wine-host/bridges/clap-impls/host-proxy.h | 40 +++++++ src/wine-host/bridges/clap.cpp | 2 + src/wine-host/bridges/clap.h | 3 + src/wine-host/utils.cpp | 2 - 6 files changed, 148 insertions(+), 3 deletions(-) diff --git a/src/common/serialization/clap/README.md b/src/common/serialization/clap/README.md index 93cf7e88..f7bc18ad 100644 --- a/src/common/serialization/clap/README.md +++ b/src/common/serialization/clap/README.md @@ -32,7 +32,7 @@ Yabridge currently tracks CLAP 1.1.2. The implementation status for CLAP's core | `clap.tail` | :heavy_check_mark: | | `clap.thread-check` | :heavy_check_mark: No bridging involved | | `clap.thread-pool` | :x: Not supported yet | -| `clap.timer-support` | :x: Not supported yet | +| `clap.timer-support` | :heavy_check_mark: No bridging involved | | `clap.voice-info` | :heavy_check_mark: | | draft extension | status | diff --git a/src/wine-host/bridges/clap-impls/host-proxy.cpp b/src/wine-host/bridges/clap-impls/host-proxy.cpp index fbdec048..f58c699a 100644 --- a/src/wine-host/bridges/clap-impls/host-proxy.cpp +++ b/src/wine-host/bridges/clap-impls/host-proxy.cpp @@ -91,6 +91,10 @@ clap_host_proxy::clap_host_proxy(ClapBridge& bridge, .is_main_thread = ext_thread_check_is_main_thread, .is_audio_thread = ext_thread_check_is_audio_thread, }), + ext_timer_support_vtable(clap_host_timer_support_t{ + .register_timer = ext_timer_support_register_timer, + .unregister_timer = ext_timer_support_unregister_timer, + }), ext_voice_info_vtable(clap_host_voice_info_t{ .changed = ext_voice_info_changed, }) {} @@ -134,6 +138,9 @@ clap_host_proxy::host_get_extension(const struct clap_host* host, } else if (self->supported_extensions_.supports_tail && strcmp(extension_id, CLAP_EXT_TAIL) == 0) { extension_ptr = &self->ext_tail_vtable; + } else if (strcmp(extension_id, CLAP_EXT_TIMER_SUPPORT) == 0) { + // This extension doesn't require any bridging + extension_ptr = &self->ext_timer_support_vtable; } else if (strcmp(extension_id, CLAP_EXT_THREAD_CHECK) == 0) { // This extension doesn't require any bridging extension_ptr = &self->ext_thread_check_vtable; @@ -452,6 +459,77 @@ void CLAP_ABI clap_host_proxy::ext_tail_changed(const clap_host_t* host) { .owner_instance_id = self->owner_instance_id()}); } +bool CLAP_ABI +clap_host_proxy::ext_timer_support_register_timer(const clap_host_t* host, + uint32_t period_ms, + clap_id* timer_id) { + assert(host && host->host_data && timer_id); + auto self = static_cast(host->host_data); + + // There's no message for this, so we'll just format the logging inline + // since it may still be useful + const auto log_response = + self->bridge_.logger_.log_request_base(false, [&](auto& message) { + message << self->owner_instance_id() + << ": clap_host_timer_support::register_timer(period_ms = " + << period_ms << ", *timer_id)"; + }); + + // In case the plugin somehow does not implement the plugin side of the + // interface then we should just not register the timer at all + const auto& [instance, _] = + self->bridge_.get_instance(self->owner_instance_id()); + if (!instance.extensions.timer_support) { + if (log_response) { + self->bridge_.logger_.log_response_base( + false, [&](auto& message) { message << "false"; }); + } + + return false; + } + + *timer_id = self->next_timer_id_.fetch_add(1); + self->timers_.emplace( + *timer_id, ClapTimer{.interval = asio::chrono::milliseconds(period_ms), + .timer = asio::steady_timer( + self->bridge_.main_context_.context_)}); + + // This timer will keep rescheduling itself until it is removed + self->async_schedule_timer_support_timer(*timer_id); + + if (log_response) { + self->bridge_.logger_.log_response_base(false, [&](auto& message) { + message << "true, *timer_id = " << *timer_id; + }); + } + + return true; +} + +bool CLAP_ABI +clap_host_proxy::ext_timer_support_unregister_timer(const clap_host_t* host, + clap_id timer_id) { + assert(host && host->host_data); + auto self = static_cast(host->host_data); + + const auto log_response = + self->bridge_.logger_.log_request_base(false, [&](auto& message) { + message << self->owner_instance_id() + << ": clap_host_timer_support::unregister_timer(timer_id = " + << timer_id << ")"; + }); + + // This implicitly cancels the timers + const bool result = self->timers_.erase(timer_id) > 0; + if (log_response) { + self->bridge_.logger_.log_response_base(false, [&](auto& message) { + message << (result ? "true" : "false"); + }); + } + + return result; +} + bool CLAP_ABI clap_host_proxy::ext_thread_check_is_main_thread(const clap_host_t* host) { assert(host && host->host_data); @@ -477,3 +555,27 @@ void CLAP_ABI clap_host_proxy::ext_voice_info_changed(const clap_host_t* host) { self->bridge_.send_main_thread_message(clap::ext::voice_info::host::Changed{ .owner_instance_id = self->owner_instance_id()}); } + +void clap_host_proxy::async_schedule_timer_support_timer(clap_id timer_id) { + auto& clap_timer = timers_.at(timer_id); + + // Try to keep a steady framerate, but add in tiny delays so this timer + // can't starve our other main thread tasks for resources + clap_timer.timer.expires_at( + std::max(clap_timer.timer.expiry() + clap_timer.interval, + std::chrono::steady_clock::now() + clap_timer.interval / 8)); + clap_timer.timer.async_wait([this, timer_id](const std::error_code& error) { + // If the timer has been removed (either as a result of unregistering it + // or the entire instance being removed), then this callback will still + // be called but with an error set + if (error) { + return; + } + + const auto& [instance, _] = bridge_.get_instance(owner_instance_id()); + instance.extensions.timer_support->on_timer(instance.plugin.get(), + timer_id); + + async_schedule_timer_support_timer(timer_id); + }); +} diff --git a/src/wine-host/bridges/clap-impls/host-proxy.h b/src/wine-host/bridges/clap-impls/host-proxy.h index 58290929..ad2c6869 100644 --- a/src/wine-host/bridges/clap-impls/host-proxy.h +++ b/src/wine-host/bridges/clap-impls/host-proxy.h @@ -29,14 +29,29 @@ #include #include #include +#include #include #include +#include "../../use-linux-asio.h" + +#include + #include "../../common/serialization/clap/plugin-factory.h" // Forward declaration to avoid circular includes class ClapBridge; +/** + * A timer registered by the plugin. + * + * @see clap_host_proxy::timers_ + */ +struct ClapTimer { + asio::chrono::steady_clock::duration interval; + asio::steady_timer timer; +}; + /** * A proxy for a plugin's `clap_host`. * @@ -123,6 +138,14 @@ class clap_host_proxy { static void CLAP_ABI ext_tail_changed(const clap_host_t* host); + static bool CLAP_ABI + ext_timer_support_register_timer(const clap_host_t* host, + uint32_t period_ms, + clap_id* timer_id); + static bool CLAP_ABI + ext_timer_support_unregister_timer(const clap_host_t* host, + clap_id timer_id); + static bool CLAP_ABI ext_thread_check_is_main_thread(const clap_host_t* host); static bool CLAP_ABI @@ -131,6 +154,12 @@ class clap_host_proxy { static void CLAP_ABI ext_voice_info_changed(const clap_host_t* host); private: + /** + * Activate and schedule a timer-support timer. When the timer procs, this + * function is called again and again until the timer is removed. + */ + void async_schedule_timer_support_timer(clap_id timer_id); + ClapBridge& bridge_; size_t owner_instance_id_; clap::host::Host host_args_; @@ -160,6 +189,8 @@ class clap_host_proxy { const clap_host_tail_t ext_tail_vtable; // This is always available regardless of the proxied host const clap_host_thread_check_t ext_thread_check_vtable; + // This is always available regardless of the proxied host + const clap_host_timer_support_t ext_timer_support_vtable; const clap_host_voice_info_t ext_voice_info_vtable; /** @@ -169,4 +200,13 @@ class clap_host_proxy { * `clap_plugin::on_main_thread()` is called. */ std::atomic_bool has_pending_host_callbacks_ = false; + + /** + * Any timers the plugin has registered through the `timer-support` + * extension. The timers are registered on the `bridge_`'s IO context. + * Likely not used on Windows, but who knows. This should not need an + * accompanying mutex since it's exclusively accessed from the main thread. + */ + std::unordered_map timers_; + std::atomic_uint32_t next_timer_id_ = 0; }; diff --git a/src/wine-host/bridges/clap.cpp b/src/wine-host/bridges/clap.cpp index d5f9033e..ffc7c7f5 100644 --- a/src/wine-host/bridges/clap.cpp +++ b/src/wine-host/bridges/clap.cpp @@ -47,6 +47,8 @@ ClapPluginExtensions::ClapPluginExtensions(const clap_plugin& plugin) noexcept plugin.get_extension(&plugin, CLAP_EXT_STATE))), tail(static_cast( plugin.get_extension(&plugin, CLAP_EXT_TAIL))), + timer_support(static_cast( + plugin.get_extension(&plugin, CLAP_EXT_TIMER_SUPPORT))), voice_info(static_cast( plugin.get_extension(&plugin, CLAP_EXT_VOICE_INFO))) {} diff --git a/src/wine-host/bridges/clap.h b/src/wine-host/bridges/clap.h index 36382e2f..b2ef1619 100644 --- a/src/wine-host/bridges/clap.h +++ b/src/wine-host/bridges/clap.h @@ -76,6 +76,9 @@ struct ClapPluginExtensions { const clap_plugin_render_t* render = nullptr; const clap_plugin_state_t* state = nullptr; const clap_plugin_tail_t* tail = nullptr; + // Used for the timer-support extension implementation purely on the Wine + // side + const clap_plugin_timer_support_t* timer_support = nullptr; const clap_plugin_voice_info_t* voice_info = nullptr; }; diff --git a/src/wine-host/utils.cpp b/src/wine-host/utils.cpp index f1038174..67c356f0 100644 --- a/src/wine-host/utils.cpp +++ b/src/wine-host/utils.cpp @@ -172,8 +172,6 @@ MainContext::WatchdogGuard MainContext::register_watchdog(HostBridge& bridge) { void MainContext::async_handle_watchdog_timer( std::chrono::steady_clock::duration interval) { - // 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. watchdog_timer_.expires_at(std::chrono::steady_clock::now() + interval); watchdog_timer_.async_wait([&](const std::error_code& error) { if (error) {