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) {