Avoid allocations when reading VST3 process data

On the plugin side. We still need to do a lot of optimizations
elsewhere.
This commit is contained in:
Robbert van der Helm
2021-05-06 17:50:41 +02:00
parent 0b173ecba8
commit 9f5066a293
9 changed files with 168 additions and 85 deletions
+12 -7
View File
@@ -174,19 +174,24 @@ Steinberg::Vst::Event YaEvent::get() const {
return event; return event;
} }
YaEventList::YaEventList(){FUNKNOWN_CTOR} YaEventList::YaEventList() {
YaEventList::YaEventList(Steinberg::Vst::IEventList& event_list) {
FUNKNOWN_CTOR FUNKNOWN_CTOR
}
void YaEventList::clear() {
events.clear();
}
void YaEventList::repopulate(Steinberg::Vst::IEventList& event_list) {
// Copy over all events. Everything gets converted to `YaEvent`s. We sadly
// can't construct these in place because we don't know the event type yet.
events.clear();
events.reserve(event_list.getEventCount()); events.reserve(event_list.getEventCount());
// Copy over all events. Everything gets converted to `YaEvent`s.
Steinberg::Vst::Event event;
for (int i = 0; i < event_list.getEventCount(); i++) { for (int i = 0; i < event_list.getEventCount(); i++) {
// We're skipping the `kResultOk` assertions here // We're skipping the `kResultOk` assertions here
Steinberg::Vst::Event event;
event_list.getEvent(i, event); event_list.getEvent(i, event);
events.push_back(event); events.emplace_back(event);
} }
} }
+12 -4
View File
@@ -213,15 +213,23 @@ struct YaEvent {
class YaEventList : public Steinberg::Vst::IEventList { class YaEventList : public Steinberg::Vst::IEventList {
public: public:
/** /**
* Default constructor with an empty event list. The plugin can use this to * We only provide a default constructor here, because we need to fill the
* output data. * existing object with new events every processing cycle to avoid
* reallocating a new object every time.
*/ */
YaEventList(); YaEventList();
/** /**
* Read data from an existing `IEventList` object. * Remove all events. Used when a null pointer gets passed to the input
* events field, and so the plugin can output its own events if the host
* supports this.
*/ */
YaEventList(Steinberg::Vst::IEventList& event_list); void clear();
/**
* Read data from an `IEventList` object into this existing object.
*/
void repopulate(Steinberg::Vst::IEventList& event_list);
~YaEventList(); ~YaEventList();
@@ -16,19 +16,22 @@
#include "param-value-queue.h" #include "param-value-queue.h"
YaParamValueQueue::YaParamValueQueue(){FUNKNOWN_CTOR} YaParamValueQueue::YaParamValueQueue() {
YaParamValueQueue::YaParamValueQueue(Steinberg::Vst::ParamID parameter_id)
: parameter_id(parameter_id){FUNKNOWN_CTOR}
// clang-format /really/ doesn't like these macros
YaParamValueQueue::YaParamValueQueue(Steinberg::Vst::IParamValueQueue &
original_queue)
: parameter_id(original_queue.getParameterId()),
queue(original_queue.getPointCount()) {
FUNKNOWN_CTOR FUNKNOWN_CTOR
}
void YaParamValueQueue::clear_for_parameter(
Steinberg::Vst::ParamID parameter_id) {
this->parameter_id = parameter_id;
queue.clear();
}
void YaParamValueQueue::repopulate(
Steinberg::Vst::IParamValueQueue& original_queue) {
parameter_id = original_queue.getParameterId();
// Copy over all points to our vector // Copy over all points to our vector
queue.resize(original_queue.getPointCount());
for (int i = 0; i < original_queue.getPointCount(); i++) { for (int i = 0; i < original_queue.getPointCount(); i++) {
// We're skipping the assertions here and just assume that the function // We're skipping the assertions here and just assume that the function
// returns `kResultOk` // returns `kResultOk`
@@ -32,20 +32,22 @@
class YaParamValueQueue : public Steinberg::Vst::IParamValueQueue { class YaParamValueQueue : public Steinberg::Vst::IParamValueQueue {
public: public:
/** /**
* Default constructor with an empty queue. * We only provide a default constructor here, because we need to fill the
* existing object with new data every processing cycle to avoid
* reallocating a new object every time.
*/ */
YaParamValueQueue(); YaParamValueQueue();
/** /**
* Create an empty queue for a specific parameter. Used in * Clear this queue in place so that it can be used to write parameter data
* `YaParameterChanges::addParameterData`. * to. Used in `YaParameterChanges::addParameterData`.
*/ */
YaParamValueQueue(Steinberg::Vst::ParamID parameter_id); void clear_for_parameter(Steinberg::Vst::ParamID parameter_id);
/** /**
* Read data from an existing `IParamValueQueue` object. * Read data from an `IParamValueQueue` object into this existing object.
*/ */
YaParamValueQueue(Steinberg::Vst::IParamValueQueue& original_queue); void repopulate(Steinberg::Vst::IParamValueQueue& original_queue);
~YaParamValueQueue(); ~YaParamValueQueue();
@@ -16,17 +16,20 @@
#include "parameter-changes.h" #include "parameter-changes.h"
YaParameterChanges::YaParameterChanges(){FUNKNOWN_CTOR} YaParameterChanges::YaParameterChanges() {
YaParameterChanges::YaParameterChanges(
Steinberg::Vst::IParameterChanges& original_queues) {
FUNKNOWN_CTOR FUNKNOWN_CTOR
}
// Copy over all parameter changne queues. Everything gets converted to void YaParameterChanges::clear() {
// `YaParamValueQueue`s. queues.clear();
queues.reserve(original_queues.getParameterCount()); }
void YaParameterChanges::repopulate(
Steinberg::Vst::IParameterChanges& original_queues) {
// Copy over all parameter changne queues
queues.resize(original_queues.getParameterCount());
for (int i = 0; i < original_queues.getParameterCount(); i++) { for (int i = 0; i < original_queues.getParameterCount(); i++) {
queues.push_back(*original_queues.getParameterData(i)); queues[i].repopulate(*original_queues.getParameterData(i));
} }
} }
@@ -75,7 +78,11 @@ Steinberg::Vst::IParamValueQueue* PLUGIN_API
YaParameterChanges::addParameterData(const Steinberg::Vst::ParamID& id, YaParameterChanges::addParameterData(const Steinberg::Vst::ParamID& id,
int32& index /*out*/) { int32& index /*out*/) {
index = static_cast<int32>(queues.size()); index = static_cast<int32>(queues.size());
queues.push_back(YaParamValueQueue(id));
// Tiny hack, resizing avoids calling the constructor the second time we
// resize the vector to the same size
queues.resize(queues.size() + 1);
queues[index].clear_for_parameter(id);
return &queues[index]; return &queues[index];
} }
@@ -31,15 +31,23 @@
class YaParameterChanges : public Steinberg::Vst::IParameterChanges { class YaParameterChanges : public Steinberg::Vst::IParameterChanges {
public: public:
/** /**
* Default constructor with an empty parameter changes list. The plugin can * We only provide a default constructor here, because we need to fill the
* use this to output data. * existing object with new data every processing cycle to avoid
* reallocating a new object every time.
*/ */
YaParameterChanges(); YaParameterChanges();
/** /**
* Read data from an existing `IParameterChanges` object. * Remove all parameter changes. Used when a null pointer gets passed to the
* input parameters field, and so the plugin can output its own parameter
* changes.
*/ */
YaParameterChanges(Steinberg::Vst::IParameterChanges& original_queues); void clear();
/**
* Read data from an `IParameterChanges` object into this existing object.
*/
void repopulate(Steinberg::Vst::IParameterChanges& original_queues);
~YaParameterChanges(); ~YaParameterChanges();
+74 -33
View File
@@ -31,33 +31,44 @@ YaAudioBusBuffers::YaAudioBusBuffers(int32 sample_size,
num_channels, num_channels,
std::vector<float>(num_samples, 0.0)))) {} std::vector<float>(num_samples, 0.0)))) {}
YaAudioBusBuffers::YaAudioBusBuffers( void YaAudioBusBuffers::repopulate(
int32 sample_size, int32 sample_size,
int32 num_samples, int32 num_samples,
const Steinberg::Vst::AudioBusBuffers& data) const Steinberg::Vst::AudioBusBuffers& data) {
: silence_flags(data.silenceFlags) { silence_flags = data.silenceFlags;
switch (sample_size) { switch (sample_size) {
case Steinberg::Vst::kSample64: { case Steinberg::Vst::kSample64: {
std::vector<std::vector<double>> vector_buffers(data.numChannels); if (!std::holds_alternative<std::vector<std::vector<double>>>(
buffers)) {
buffers.emplace<std::vector<std::vector<double>>>();
}
std::vector<std::vector<double>>& vector_buffers =
std::get<std::vector<std::vector<double>>>(buffers);
vector_buffers.resize(data.numChannels);
for (int channel = 0; channel < data.numChannels; channel++) { for (int channel = 0; channel < data.numChannels; channel++) {
vector_buffers[channel].assign( vector_buffers[channel].assign(
&data.channelBuffers64[channel][0], &data.channelBuffers64[channel][0],
&data.channelBuffers64[channel][num_samples]); &data.channelBuffers64[channel][num_samples]);
} }
buffers = std::move(vector_buffers);
} break; } break;
case Steinberg::Vst::kSample32: case Steinberg::Vst::kSample32:
// I don't think they'll add any other sample sizes any time soon // I don't think they'll add any other sample sizes any time soon
default: { default: {
std::vector<std::vector<float>> vector_buffers(data.numChannels); if (!std::holds_alternative<std::vector<std::vector<float>>>(
buffers)) {
buffers.emplace<std::vector<std::vector<float>>>();
}
std::vector<std::vector<float>>& vector_buffers =
std::get<std::vector<std::vector<float>>>(buffers);
vector_buffers.resize(data.numChannels);
for (int channel = 0; channel < data.numChannels; channel++) { for (int channel = 0; channel < data.numChannels; channel++) {
vector_buffers[channel].assign( vector_buffers[channel].assign(
&data.channelBuffers32[channel][0], &data.channelBuffers32[channel][0],
&data.channelBuffers32[channel][num_samples]); &data.channelBuffers32[channel][num_samples]);
} }
buffers = std::move(vector_buffers);
} break; } break;
} }
} }
@@ -140,39 +151,62 @@ void YaProcessDataResponse::write_back_outputs(
YaProcessData::YaProcessData() {} YaProcessData::YaProcessData() {}
YaProcessData::YaProcessData(const Steinberg::Vst::ProcessData& process_data) void YaProcessData::repopulate(
: process_mode(process_data.processMode), const Steinberg::Vst::ProcessData& process_data) {
symbolic_sample_size(process_data.symbolicSampleSize), // In this function and in every function we call, we should be careful to
num_samples(process_data.numSamples), // not use `push_back`/`emplace_back` anywhere. Resizing vectors and
outputs_num_channels(process_data.numOutputs), // modifying them in place performs much better because that avoids
// Even though `ProcessData::inputParamterChanges` is mandatory, the VST3 // destroying and creating objects most of the time.
// validator will pass a null pointer here process_mode = process_data.processMode;
input_parameter_changes( symbolic_sample_size = process_data.symbolicSampleSize;
process_data.inputParameterChanges num_samples = process_data.numSamples;
? YaParameterChanges(*process_data.inputParameterChanges)
: YaParameterChanges()), // We'll make sure to not do any allocations here after the first processing
output_parameter_changes_supported(process_data.outputParameterChanges), // cycle
input_events(process_data.inputEvents ? std::make_optional<YaEventList>( inputs.resize(process_data.numInputs);
*process_data.inputEvents)
: std::nullopt),
output_events_supported(process_data.outputEvents),
process_context(process_data.processContext
? std::make_optional<Steinberg::Vst::ProcessContext>(
*process_data.processContext)
: std::nullopt) {
for (int i = 0; i < process_data.numInputs; i++) { for (int i = 0; i < process_data.numInputs; i++) {
inputs.emplace_back(symbolic_sample_size, num_samples, inputs[i].repopulate(symbolic_sample_size, num_samples,
process_data.inputs[i]); process_data.inputs[i]);
} }
// Fetch the number of channels for each output so we can recreate these // We only store how many channels ouch output has so we can recreate the
// buffers in the Wine plugin host // objects on the Wine side
outputs_num_channels.resize(process_data.numOutputs);
for (int i = 0; i < process_data.numOutputs; i++) { for (int i = 0; i < process_data.numOutputs; i++) {
outputs_num_channels[i] = process_data.outputs[i].numChannels; outputs_num_channels[i] = process_data.outputs[i].numChannels;
} }
// Even though `ProcessData::inputParamterChanges` is mandatory, the VST3
// validator will pass a null pointer here
if (process_data.inputParameterChanges) {
input_parameter_changes.repopulate(*process_data.inputParameterChanges);
} else {
input_parameter_changes.clear();
}
output_parameter_changes_supported = process_data.outputParameterChanges;
if (process_data.inputEvents) {
if (!input_events) {
input_events.emplace();
}
input_events->repopulate(*process_data.inputEvents);
} else {
input_events.reset();
}
output_events_supported = process_data.outputEvents;
if (process_data.processContext) {
process_context.emplace(*process_data.processContext);
} else {
process_context.reset();
}
} }
Steinberg::Vst::ProcessData& YaProcessData::get() { Steinberg::Vst::ProcessData& YaProcessData::get() {
// TODO: Also optimize this to make use of the reused objects
// We'll have to transform out `YaAudioBusBuffers` objects into an array of // We'll have to transform out `YaAudioBusBuffers` objects into an array of
// `AudioBusBuffers` object so the plugin can deal with them. These objects // `AudioBusBuffers` object so the plugin can deal with them. These objects
// contain pointers to those original objects and thus don't store any // contain pointers to those original objects and thus don't store any
@@ -193,6 +227,13 @@ Steinberg::Vst::ProcessData& YaProcessData::get() {
outputs_audio_bus_buffers.push_back(buffers.get()); outputs_audio_bus_buffers.push_back(buffers.get());
} }
if (output_parameter_changes) {
output_parameter_changes->clear();
}
if (output_events) {
output_events->clear();
}
reconstructed_process_data.processMode = process_mode; reconstructed_process_data.processMode = process_mode;
reconstructed_process_data.symbolicSampleSize = symbolic_sample_size; reconstructed_process_data.symbolicSampleSize = symbolic_sample_size;
reconstructed_process_data.numSamples = num_samples; reconstructed_process_data.numSamples = num_samples;
+19 -8
View File
@@ -39,14 +39,18 @@
class YaAudioBusBuffers { class YaAudioBusBuffers {
public: public:
/** /**
* A default constructor does not make any sense here since the actual data * We only provide a default constructor here, because we need to fill the
* is a union, but we need a default constructor for bitsery. * existing object with new audio data every processing cycle to avoid
* reallocating a new object every time.
*/ */
YaAudioBusBuffers(); YaAudioBusBuffers();
/** /**
* Create a new, zero initialize audio bus buffers object. Used to * Create a new, zero initialize audio bus buffers object. Used to
* reconstruct the output buffers during `YaProcessData::get()`. * reconstruct the output buffers during `YaProcessData::get()`.
*
* TODO: Replace with a function similar to `repopulate` that just reassigns
* the existing buffers for the outputs created on the Wine side.
*/ */
YaAudioBusBuffers(int32 sample_size, YaAudioBusBuffers(int32 sample_size,
size_t num_samples, size_t num_samples,
@@ -54,15 +58,15 @@ class YaAudioBusBuffers {
/** /**
* Copy data from a host provided `AudioBusBuffers` object during a process * Copy data from a host provided `AudioBusBuffers` object during a process
* call. Constructed as part of `YaProcessData`. Since `AudioBusBuffers` * call. Used in `YaProcessData::repopulate()`. Since `AudioBusBuffers`
* contains an untagged union for storing single and double precision * contains an untagged union for storing single and double precision
* floating point values, the original `ProcessData`'s `symbolicSampleSize` * floating point values, the original `ProcessData`'s `symbolicSampleSize`
* field determines which variant of that union to use. Similarly the * field determines which variant of that union to use. Similarly the
* `ProcessData`' `numSamples` field determines the extent of these arrays. * `ProcessData`' `numSamples` field determines the extent of these arrays.
*/ */
YaAudioBusBuffers(int32 sample_size, void repopulate(int32 sample_size,
int32 num_samples, int32 num_samples,
const Steinberg::Vst::AudioBusBuffers& data); const Steinberg::Vst::AudioBusBuffers& data);
/** /**
* Reconstruct the original `AudioBusBuffers` object passed to the * Reconstruct the original `AudioBusBuffers` object passed to the
@@ -170,14 +174,21 @@ struct YaProcessDataResponse {
*/ */
class YaProcessData { class YaProcessData {
public: public:
/**
* Initialize the process data. We only provide a default constructor here,
* because we need to fill the existing object with new data every
* processing cycle to avoid reallocating a new object every time.
*/
YaProcessData(); YaProcessData();
/** /**
* Copy data from a host provided `ProcessData` object during a process * Copy data from a host provided `ProcessData` object during a process
* call. This struct can then be serialized, and `YaProcessData::get()` can * call. This struct can then be serialized, and `YaProcessData::get()` can
* then be used again to recreate the original `ProcessData` object. * then be used again to recreate the original `ProcessData` object. This
* will avoid allocating unless it's absolutely necessary (e.g. when we
* receive more parameter changes than we've received in previous calls).
*/ */
YaProcessData(const Steinberg::Vst::ProcessData& process_data); void repopulate(const Steinberg::Vst::ProcessData& process_data);
/** /**
* Reconstruct the original `ProcessData` object passed to the constructor * Reconstruct the original `ProcessData` object passed to the constructor
@@ -186,10 +186,8 @@ Vst3PluginProxyImpl::process(Steinberg::Vst::ProcessData& data) {
last_audio_thread_priority_synchronization = now; last_audio_thread_priority_synchronization = now;
} }
// TODO: Document // We reuse this existing object to avoid allocations
// TODO: Actually repopulate `process_data` with new data, right now this process_data.repopulate(data);
// assignment just destroys the old object and creates a new object.
process_data = data;
ProcessResponse response = ProcessResponse response =
bridge.send_audio_processor_message(YaAudioProcessor::Process{ bridge.send_audio_processor_message(YaAudioProcessor::Process{