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.
This commit is contained in:
Robbert van der Helm
2022-10-26 23:35:54 +02:00
parent eeadf36195
commit d1ef29aa3e
6 changed files with 148 additions and 3 deletions
+1 -1
View File
@@ -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 |
@@ -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<clap_host_proxy*>(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<clap_host_proxy*>(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);
});
}
@@ -29,14 +29,29 @@
#include <clap/ext/state.h>
#include <clap/ext/tail.h>
#include <clap/ext/thread-check.h>
#include <clap/ext/timer-support.h>
#include <clap/ext/voice-info.h>
#include <clap/host.h>
#include "../../use-linux-asio.h"
#include <asio/steady_timer.hpp>
#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<clap_id, ClapTimer> timers_;
std::atomic_uint32_t next_timer_id_ = 0;
};
+2
View File
@@ -47,6 +47,8 @@ ClapPluginExtensions::ClapPluginExtensions(const clap_plugin& plugin) noexcept
plugin.get_extension(&plugin, CLAP_EXT_STATE))),
tail(static_cast<const clap_plugin_tail_t*>(
plugin.get_extension(&plugin, CLAP_EXT_TAIL))),
timer_support(static_cast<const clap_plugin_timer_support_t*>(
plugin.get_extension(&plugin, CLAP_EXT_TIMER_SUPPORT))),
voice_info(static_cast<const clap_plugin_voice_info_t*>(
plugin.get_extension(&plugin, CLAP_EXT_VOICE_INFO))) {}
+3
View File
@@ -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;
};
-2
View File
@@ -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) {