diff --git a/CHANGELOG.md b/CHANGELOG.md index 16f1beed..b64ba55a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Changed +- Parameter information for VST3 plugins is now queried all at once. This should + work around a bug in _Kontakt_ that would cause loading patches with lots of + exposed parameters to become very slow in **REAPER** + ([#236](https://github.com/robbert-vdh/yabridge/issues/236)). - When dragging plugin windows around, yabridge now waits for the mouse buttons to be released before informing Wine about the window's new screen coordinates. This prevents constant flickering when dragging plugin windows diff --git a/src/common/logging/vst3.cpp b/src/common/logging/vst3.cpp index d0c921df..be904f57 100644 --- a/src/common/logging/vst3.cpp +++ b/src/common/logging/vst3.cpp @@ -233,20 +233,10 @@ bool Vst3Logger::log_request( bool Vst3Logger::log_request( bool is_host_plugin, - const YaEditController::GetParameterCount& request) { + const YaEditController::GetParameterInfos& request) { return log_request_base(is_host_plugin, [&](auto& message) { message << request.instance_id - << ": IEditController::getParameterCount()"; - }); -} - -bool Vst3Logger::log_request( - bool is_host_plugin, - const YaEditController::GetParameterInfo& request) { - return log_request_base(is_host_plugin, [&](auto& message) { - message << request.instance_id - << ": IEditController::getParameterInfo(paramIndex = " - << request.param_index << ", &info)"; + << ": IEditController::getParameterInfo(..., &info) (batched)"; }); } @@ -1478,17 +1468,13 @@ void Vst3Logger::log_response( void Vst3Logger::log_response( bool is_host_plugin, - const YaEditController::GetParameterInfoResponse& response, + const YaEditController::GetParameterInfosResponse& response, bool from_cache) { log_response_base(is_host_plugin, [&](auto& message) { - message << response.result.string(); - if (response.result == Steinberg::kResultOk) { - std::string param_title = - VST3::StringConvert::convert(response.info.title); - message << ", "; - if (from_cache) { - message << " (from cache)"; - } + message << " for " << response.infos.size() + << " parameters"; + if (from_cache) { + message << " (from cache)"; } }); } diff --git a/src/common/logging/vst3.h b/src/common/logging/vst3.h index 3f1c98cd..d8fe987e 100644 --- a/src/common/logging/vst3.h +++ b/src/common/logging/vst3.h @@ -82,9 +82,7 @@ class Vst3Logger { bool log_request(bool is_host_plugin, const YaEditController::SetComponentState&); bool log_request(bool is_host_plugin, - const YaEditController::GetParameterCount&); - bool log_request(bool is_host_plugin, - const YaEditController::GetParameterInfo&); + const YaEditController::GetParameterInfos&); bool log_request(bool is_host_plugin, const YaEditController::GetParamStringByValue&); bool log_request(bool is_host_plugin, @@ -272,7 +270,7 @@ class Vst3Logger { void log_response(bool is_host_plugin, const Vst3PluginProxy::GetStateResponse&); void log_response(bool is_host_plugin, - const YaEditController::GetParameterInfoResponse&, + const YaEditController::GetParameterInfosResponse&, bool from_cache = false); void log_response(bool is_host_plugin, const YaEditController::GetParamStringByValueResponse&); diff --git a/src/common/serialization/vst3.h b/src/common/serialization/vst3.h index 3886b1b9..bf4a9416 100644 --- a/src/common/serialization/vst3.h +++ b/src/common/serialization/vst3.h @@ -75,8 +75,7 @@ using Vst3ControlRequest = YaConnectionPoint::Notify, YaContextMenuTarget::ExecuteMenuItem, YaEditController::SetComponentState, - YaEditController::GetParameterCount, - YaEditController::GetParameterInfo, + YaEditController::GetParameterInfos, YaEditController::GetParamStringByValue, YaEditController::GetParamValueByString, YaEditController::NormalizedParamToPlain, diff --git a/src/common/serialization/vst3/plugin/edit-controller.h b/src/common/serialization/vst3/plugin/edit-controller.h index 93879388..fa9986ea 100644 --- a/src/common/serialization/vst3/plugin/edit-controller.h +++ b/src/common/serialization/vst3/plugin/edit-controller.h @@ -98,11 +98,37 @@ class YaEditController : public Steinberg::Vst::IEditController { getState(Steinberg::IBStream* state) override = 0; /** - * Message to pass through a call to `IEditController::getParameterCount()` - * to the Wine plugin host. + * All of a plugin's parameter infos. + * + * @see GetParameterInfos */ - struct GetParameterCount { - using Response = PrimitiveResponse; + struct GetParameterInfosResponse { + /** + * All of the plugin's parameter infos. If the plugin somehow returned + * an error for a parameter that should be in range, then this contains + * a nullopt value. + */ + std::vector> infos; + + template + void serialize(S& s) { + s.container(infos, 1 << 16, [](S& s, auto& v) { + s.ext(v, bitsery::ext::InPlaceOptional{}); + }); + } + }; + + /** + * Get all of the plugin's parameter information using both + * `IEditController::getParameterCount()` and + * `IEditController::getParameterInfo()`. This is queried all at once and + * then cached until the plugin asks for a rescan to speed up loading for + * plugins with huge amounts of parameters, and plugins like Kontakt that + * may tell the host to rescan for parameters hundreds of times in a row + * (https://github.com/robbert-vdh/yabridge/issues/236). + */ + struct GetParameterInfos { + using Response = GetParameterInfosResponse; native_size_t instance_id; @@ -113,41 +139,6 @@ class YaEditController : public Steinberg::Vst::IEditController { }; virtual int32 PLUGIN_API getParameterCount() override = 0; - - /** - * The response code and returned parameter information for a call to - * `IEditController::getParameterInfo(param_index, &info)`. - */ - struct GetParameterInfoResponse { - UniversalTResult result; - Steinberg::Vst::ParameterInfo info; - - template - void serialize(S& s) { - s.object(result); - s.object(info); - } - }; - - /** - * Message to pass through a call to - * `IEditController::getParameterInfo(param_index, &info)` to the Wine - * plugin host. - */ - struct GetParameterInfo { - using Response = GetParameterInfoResponse; - - native_size_t instance_id; - - int32 param_index; - - template - void serialize(S& s) { - s.value8b(instance_id); - s.value4b(param_index); - } - }; - virtual tresult PLUGIN_API getParameterInfo(int32 paramIndex, Steinberg::Vst::ParameterInfo& info /*out*/) override = 0; diff --git a/src/plugin/bridges/vst3-impls/plugin-proxy.cpp b/src/plugin/bridges/vst3-impls/plugin-proxy.cpp index 052577b1..71438076 100644 --- a/src/plugin/bridges/vst3-impls/plugin-proxy.cpp +++ b/src/plugin/bridges/vst3-impls/plugin-proxy.cpp @@ -623,72 +623,75 @@ Vst3PluginProxyImpl::setComponentState(Steinberg::IBStream* state) { } int32 PLUGIN_API Vst3PluginProxyImpl::getParameterCount() { - const auto request = - YaEditController::GetParameterCount{.instance_id = instance_id()}; - + // Parameter information is queried all at once to work around a Kontakt + // bug, see https://github.com/robbert-vdh/yabridge/issues/236 { + // We'll assume that the plugin has at least one parameter. If it does + // not have any parameters then everything will work as expected, except + // that the parameter count is not cached. std::lock_guard lock(function_result_cache_mutex_); - if (function_result_cache_.parameter_count) { - const bool log_response = - bridge_.logger_.log_request(true, request); - if (log_response) { - bridge_.logger_.log_response( - true, - YaEditController::GetParameterCount::Response( - *function_result_cache_.parameter_count), - true); - } - - return *function_result_cache_.parameter_count; + if (!function_result_cache_.parameter_info.empty()) { + // We can't cleanly log here, but it also doesn't really matter + return static_cast( + function_result_cache_.parameter_info.size()); } } - const int32 result = bridge_.send_message(request); + // The first time either of these two functions is called we'll fetch the + // infos for all parameters. These are cleared when the plugin triggers a + // component restart. + query_parameter_info(); - { - std::lock_guard lock(function_result_cache_mutex_); - function_result_cache_.parameter_count = result; - } - - return result; + std::lock_guard lock(function_result_cache_mutex_); + return static_cast(function_result_cache_.parameter_info.size()); } tresult PLUGIN_API Vst3PluginProxyImpl::getParameterInfo( int32 paramIndex, Steinberg::Vst::ParameterInfo& info /*out*/) { - const auto request = YaEditController::GetParameterInfo{ - .instance_id = instance_id(), .param_index = paramIndex}; + // The integer parameter indices are _fun_ + if (paramIndex < 0) { + return Steinberg::kInvalidArgument; + } + // See above { std::lock_guard lock(function_result_cache_mutex_); - if (auto it = function_result_cache_.parameter_info.find(paramIndex); - it != function_result_cache_.parameter_info.end()) { - const bool log_response = - bridge_.logger_.log_request(true, request); - if (log_response) { - bridge_.logger_.log_response( - true, - YaEditController::GetParameterInfo::Response{ - .result = Steinberg::kResultOk, .info = it->second}, - true); + if (!function_result_cache_.parameter_info.empty()) { + if (paramIndex < + static_cast( + function_result_cache_.parameter_info.size())) { + if (const auto& result = + function_result_cache_.parameter_info[paramIndex]) { + info = *result; + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } + } else { + return Steinberg::kInvalidArgument; } - - info = it->second; - - return Steinberg::kResultOk; } } - const GetParameterInfoResponse response = bridge_.send_message(request); + // The first time either of these two functions is called we'll fetch the + // infos for all parameters. These are cleared when the plugin triggers a + // component restart. + query_parameter_info(); - info = response.info; - - { - std::lock_guard lock(function_result_cache_mutex_); - function_result_cache_.parameter_info[paramIndex] = response.info; + std::lock_guard lock(function_result_cache_mutex_); + if (paramIndex < + static_cast(function_result_cache_.parameter_info.size())) { + if (const auto& result = + function_result_cache_.parameter_info[paramIndex]) { + info = *result; + return Steinberg::kResultOk; + } else { + return Steinberg::kResultFalse; + } + } else { + return Steinberg::kInvalidArgument; } - - return response.result; } tresult PLUGIN_API Vst3PluginProxyImpl::getParamStringByValue( @@ -1376,6 +1379,14 @@ tresult PLUGIN_API Vst3PluginProxyImpl::getXmlRepresentationStream( } } +void Vst3PluginProxyImpl::query_parameter_info() { + std::lock_guard lock(function_result_cache_mutex_); + + const GetParameterInfosResponse response = bridge_.send_message( + YaEditController::GetParameterInfos{.instance_id = instance_id()}); + function_result_cache_.parameter_info = std::move(response.infos); +} + void Vst3PluginProxyImpl::clear_bus_cache() noexcept { std::lock_guard lock(processing_bus_cache_mutex_); if (processing_bus_cache_) { diff --git a/src/plugin/bridges/vst3-impls/plugin-proxy.h b/src/plugin/bridges/vst3-impls/plugin-proxy.h index ed8b935e..02c53a1f 100644 --- a/src/plugin/bridges/vst3-impls/plugin-proxy.h +++ b/src/plugin/bridges/vst3-impls/plugin-proxy.h @@ -398,6 +398,14 @@ class Vst3PluginProxyImpl : public Vst3PluginProxy { Steinberg::FUnknownPtr unit_handler_2_; private: + /** + * Query information for all of the plugin's parameters and writes the + * results to `function_result_cache_`. Acquires a lock on the struct in the + * process, so it must not be locked before calling this function (thanks + * STL). + */ + void query_parameter_info(); + /** * Clear the bus count and information cache. We need this cache for REAPER * as it makes `num_inputs + num_outputs + 2` function calls to retrieve @@ -529,13 +537,19 @@ class Vst3PluginProxyImpl : public Vst3PluginProxy { */ std::map can_process_sample_size; /** - * Memoizes `IEditController::getParameterCount()`. + * Memoizes `IEditController::getParameterCount()` and + * `IEditController::getParameterInfo()`. This information is queried + * all at to work around a Kontakt bug where they tell the host to + * rescan the parameters hundreds of times in a row when loading a patch + * that has hundreds of custom parameters instead of doing it only once + * at the end. + * + * Because the plugin _can_ return an error when fetching the info for a + * parameter that should be in range, this array stores `std::optional`s + * so we can do the same thing here. */ - std::optional parameter_count; - /** - * Memoizes `IEditController::getParameterInfo()`. - */ - std::unordered_map parameter_info; + std::vector> + parameter_info; }; /** diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index d84d522e..211407af 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -400,23 +400,34 @@ void Vst3Bridge::run() { return instance.interfaces.edit_controller->setComponentState( &request.state); }, - [&](const YaEditController::GetParameterCount& request) - -> YaEditController::GetParameterCount::Response { + [&](const YaEditController::GetParameterInfos& request) + -> YaEditController::GetParameterInfos::Response { const auto& [instance, _] = get_instance(request.instance_id); - return instance.interfaces.edit_controller->getParameterCount(); - }, - [&](YaEditController::GetParameterInfo& request) - -> YaEditController::GetParameterInfo::Response { - Steinberg::Vst::ParameterInfo info{}; - const auto& [instance, _] = get_instance(request.instance_id); + // This is an optimization mostly for Kontakt, which may tell + // tell the host to rescan its 3000 parameters hundreds of times + // in rapid succession. Querying all parameters at once can save + // minutes of waiting around on slower machines. + const int num_parameters = + instance.interfaces.edit_controller->getParameterCount(); - const tresult result = - instance.interfaces.edit_controller->getParameterInfo( - request.param_index, info); + std::vector> infos; + infos.reserve(num_parameters); + for (int i = 0; i < num_parameters; i++) { + // This should never fail, but we can't make things up and + // we don't want to change parameter orders around so we'll + // store a nullopt if the plugin returns an error here + Steinberg::Vst::ParameterInfo info{}; + if (instance.interfaces.edit_controller->getParameterInfo( + i, info) == Steinberg::kResultOk) { + infos.push_back(std::move(info)); + } else { + infos.push_back(std::nullopt); + } + } - return YaEditController::GetParameterInfoResponse{ - .result = result, .info = std::move(info)}; + return YaEditController::GetParameterInfosResponse{ + .infos = std::move(infos)}; }, [&](const YaEditController::GetParamStringByValue& request) -> YaEditController::GetParamStringByValue::Response { @@ -793,8 +804,7 @@ void Vst3Bridge::run() { x11_handle); const tresult result = instance.plug_view_instance->plug_view->attached( - editor_instance.win32_handle(), - type.c_str()); + editor_instance.win32_handle(), type.c_str()); // Set the window's initial size according to what the // plugin reports. Otherwise get rid of the editor again @@ -1315,7 +1325,7 @@ void Vst3Bridge::run() { } bool Vst3Bridge::resize_editor(size_t instance_id, - const Steinberg::ViewRect& new_size) { + const Steinberg::ViewRect& new_size) { const auto& [instance, _] = get_instance(instance_id); if (instance.editor) { diff --git a/src/wine-host/bridges/vst3.h b/src/wine-host/bridges/vst3.h index e3e09452..12d6d788 100644 --- a/src/wine-host/bridges/vst3.h +++ b/src/wine-host/bridges/vst3.h @@ -308,8 +308,7 @@ class Vst3Bridge : public HostBridge { * the new size. This is called from `IPlugFrame::resizeView()` to make sure * we do the resize before the request gets sent to the host. */ - bool resize_editor(size_t instance_id, - const Steinberg::ViewRect& new_size); + bool resize_editor(size_t instance_id, const Steinberg::ViewRect& new_size); /** * Register a context with with `context_menu`'s ID and owner in