Batch VST3 parameter info querying #236

To hopefully work mitigate the Kontakt bug that causes the host to
rescan thousands of parameters hundreds of times when using certain VST3
Kontakt patches in REAPER.
This commit is contained in:
Robbert van der Helm
2023-04-28 19:22:54 +02:00
parent 9005474ded
commit 8289d76818
9 changed files with 148 additions and 136 deletions
+4
View File
@@ -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
+7 -21
View File
@@ -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 << ", <ParameterInfo for '" << param_title << "'>";
if (from_cache) {
message << " (from cache)";
}
message << "<ParameterInfo> for " << response.infos.size()
<< " parameters";
if (from_cache) {
message << " (from cache)";
}
});
}
+2 -4
View File
@@ -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&);
+1 -2
View File
@@ -75,8 +75,7 @@ using Vst3ControlRequest =
YaConnectionPoint::Notify,
YaContextMenuTarget::ExecuteMenuItem,
YaEditController::SetComponentState,
YaEditController::GetParameterCount,
YaEditController::GetParameterInfo,
YaEditController::GetParameterInfos,
YaEditController::GetParamStringByValue,
YaEditController::GetParamValueByString,
YaEditController::NormalizedParamToPlain,
@@ -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<int32>;
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<std::optional<Steinberg::Vst::ParameterInfo>> infos;
template <typename S>
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 <typename S>
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 <typename S>
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;
+57 -46
View File
@@ -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<int32>(
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<int32>(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<int32>(
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<int32>(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_) {
+20 -6
View File
@@ -398,6 +398,14 @@ class Vst3PluginProxyImpl : public Vst3PluginProxy {
Steinberg::FUnknownPtr<Steinberg::Vst::IUnitHandler2> 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<int32, tresult> 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<int32> parameter_count;
/**
* Memoizes `IEditController::getParameterInfo()`.
*/
std::unordered_map<int32, Steinberg::Vst::ParameterInfo> parameter_info;
std::vector<std::optional<Steinberg::Vst::ParameterInfo>>
parameter_info;
};
/**
+26 -16
View File
@@ -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<std::optional<Steinberg::Vst::ParameterInfo>> 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) {
+1 -2
View File
@@ -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