Make sure effProcessEvents() also never allocates

This basically changes the default small vectors during VST2 event
processing from 256 bytes to the size of a `DynamicVstEvents`
object (which also includes a small_vector to hold MIDI events without
allocating) and makes them thread local. We already have a similar
optimization for VST3. There it's a bit neater since we already had to
separate audio processing functions from non-time critical functions.
Here we don't have that separation, so we just made these buffers thread
local, large enough to hold our predefined number of events, and we then
just shrink them to fit if these buffers grow even more (which can only
happen after reading or writing chunk data).

The change doesn't specifically target `effProcessEvents()`, but that's
where you would see the differences. This is also relevant for
`audioMasterProcessEvents()`.
This commit is contained in:
Robbert van der Helm
2021-05-23 17:37:09 +02:00
parent 77d43e4f08
commit 0b9b1330ad
4 changed files with 48 additions and 14 deletions
+2 -2
View File
@@ -49,8 +49,8 @@ Versioning](https://semver.org/spec/v2.0.0.html).
- VST2 audio processing also received the same small vector optimization to get - VST2 audio processing also received the same small vector optimization to get
rid of any last potential allocations during audio processing. rid of any last potential allocations during audio processing.
- The same small vector optimization has been applied across yabridge's entire - The same small vector optimization has been applied across yabridge's entire
communication architecture, meaning that most function calls should no longer communication and event handling architecture, meaning that most function
produce any allocations for both VST2 and VST3 plugins. calls should no longer produce any allocations for both VST2 and VST3 plugins.
- Changed the way mutual recursion in VST3 plugins on the plugin side works to - Changed the way mutual recursion in VST3 plugins on the plugin side works to
counter any potential GUI related timing issues with VST3 plugins when using counter any potential GUI related timing issues with VST3 plugins when using
multiple instances of a plugin. multiple instances of a plugin.
+4 -3
View File
@@ -74,7 +74,8 @@ intptr_t DefaultDataConverter::return_value(const int /*opcode*/,
Vst2EventResult DefaultDataConverter::send_event( Vst2EventResult DefaultDataConverter::send_event(
boost::asio::local::stream_protocol::socket& socket, boost::asio::local::stream_protocol::socket& socket,
const Vst2Event& event) const { const Vst2Event& event,
write_object(socket, event); SerializationBufferBase& buffer) const {
return read_object<Vst2EventResult>(socket); write_object(socket, event, buffer);
return read_object<Vst2EventResult>(socket, buffer);
} }
+38 -6
View File
@@ -87,7 +87,8 @@ class DefaultDataConverter {
*/ */
virtual Vst2EventResult send_event( virtual Vst2EventResult send_event(
boost::asio::local::stream_protocol::socket& socket, boost::asio::local::stream_protocol::socket& socket,
const Vst2Event& event) const; const Vst2Event& event,
SerializationBufferBase& buffer) const;
}; };
/** /**
@@ -193,8 +194,8 @@ class Vst2EventHandler : public AdHocSocketHandler<Thread> {
.index = index, .index = index,
.value = value, .value = value,
.option = option, .option = option,
.payload = payload, .payload = std::move(payload),
.value_payload = value_payload}; .value_payload = std::move(value_payload)};
// A socket only handles a single request at a time as to prevent // A socket only handles a single request at a time as to prevent
// messages from arriving out of order. `AdHocSocketHandler::send()` // messages from arriving out of order. `AdHocSocketHandler::send()`
@@ -206,7 +207,8 @@ class Vst2EventHandler : public AdHocSocketHandler<Thread> {
// calling thread (i.e. mutual recursion). // calling thread (i.e. mutual recursion).
const Vst2EventResult response = this->send( const Vst2EventResult response = this->send(
[&](boost::asio::local::stream_protocol::socket& socket) { [&](boost::asio::local::stream_protocol::socket& socket) {
return data_converter.send_event(socket, event); return data_converter.send_event(socket, event,
serialization_buffer());
}); });
if (logging) { if (logging) {
@@ -249,7 +251,9 @@ class Vst2EventHandler : public AdHocSocketHandler<Thread> {
const auto process_event = const auto process_event =
[&](boost::asio::local::stream_protocol::socket& socket, [&](boost::asio::local::stream_protocol::socket& socket,
bool on_main_thread) { bool on_main_thread) {
auto event = read_object<Vst2Event>(socket); SerializationBufferBase& buffer = serialization_buffer();
auto event = read_object<Vst2Event>(socket, buffer);
if (logging) { if (logging) {
auto [logger, is_dispatch] = *logging; auto [logger, is_dispatch] = *logging;
logger.log_event(is_dispatch, event.opcode, event.index, logger.log_event(is_dispatch, event.opcode, event.index,
@@ -265,7 +269,7 @@ class Vst2EventHandler : public AdHocSocketHandler<Thread> {
response.payload, response.value_payload); response.payload, response.value_payload);
} }
write_object(socket, response); write_object(socket, response, buffer);
}; };
this->receive_multi( this->receive_multi(
@@ -278,6 +282,34 @@ class Vst2EventHandler : public AdHocSocketHandler<Thread> {
process_event(socket, false); process_event(socket, false);
}); });
} }
private:
/**
* Unlike our VST3 implementation, in the VST2 implementation there's no
* separation between potentially real time critical events that will be
* called on the audio thread an all other events. This is absolutely fine,
* except for sending and receiving MIDI events. Those objects can get
* rather large, and because we want to avoid allocations on the audio
* thread at all cost we'll just predefine a large buffer for every thread.
*/
SerializationBufferBase& serialization_buffer() {
// This object also contains a `boost::container::small_vector` that has
// capacity for a large-ish number of events so we don't have to
// allocate under normal circumstances.
constexpr size_t initial_events_size = sizeof(DynamicVstEvents);
thread_local SerializationBuffer<initial_events_size> buffer{};
// This buffer is already pretty large, but it can still grow immensely
// when sending and receiving preset data. In such cases we do want to
// reallocate the buffer on the next event to free up memory again. This
// won't happen during audio processing.
if (buffer.size() > initial_events_size) {
buffer.resize(initial_events_size);
buffer.shrink_to_fit();
}
return buffer;
}
}; };
/** /**
+4 -3
View File
@@ -621,13 +621,14 @@ class HostCallbackDataConverter : public DefaultDataConverter {
Vst2EventResult send_event( Vst2EventResult send_event(
boost::asio::local::stream_protocol::socket& socket, boost::asio::local::stream_protocol::socket& socket,
const Vst2Event& event) const override { const Vst2Event& event,
SerializationBufferBase& buffer) const override {
if (mutually_recursive_callbacks.contains(event.opcode)) { if (mutually_recursive_callbacks.contains(event.opcode)) {
return mutual_recursion.fork([&]() { return mutual_recursion.fork([&]() {
return DefaultDataConverter::send_event(socket, event); return DefaultDataConverter::send_event(socket, event, buffer);
}); });
} else { } else {
return DefaultDataConverter::send_event(socket, event); return DefaultDataConverter::send_event(socket, event, buffer);
} }
} }