From 7da5ec113c6d669c15fc0c0fd47aa8cacd5a2030 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 25 Dec 2020 16:22:53 +0100 Subject: [PATCH] Reuse buffers in VST3 audio processing --- src/common/communication/common.h | 3 ++ src/common/communication/vst3.h | 90 ++++++++++++++++++++++++++----- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/common/communication/common.h b/src/common/communication/common.h index f3f97107..d1a8d873 100644 --- a/src/common/communication/common.h +++ b/src/common/communication/common.h @@ -101,6 +101,9 @@ inline void write_object(Socket& socket, const T& object) { * @throw boost::system::system_error If the socket is closed or gets closed * while reading. * + * TODO: Swap these arguments around so they match `write_object`'s argument + * order + * * @relates write_object */ template diff --git a/src/common/communication/vst3.h b/src/common/communication/vst3.h index 9fb4b546..233da066 100644 --- a/src/common/communication/vst3.h +++ b/src/common/communication/vst3.h @@ -84,10 +84,29 @@ class Vst3MessageHandler : public AdHocSocketHandler { * then this indicates that this `Vst3MessageHandler` is handling plugin * -> host callbacks isntead. Optional since it only has to be set on the * plugin's side. + * @param buffer The serialization and receiving buffer to reuse. This is + * optional, but it's useful for minimizing allocations in the audio + * processing loop. * * @relates Vst3MessageHandler::receive_messages */ template + typename T::Response send_message( + const T& object, + std::optional> logging, + std::vector& buffer) { + typename T::Response response_object; + receive_into(object, response_object, logging, buffer); + + return response_object; + } + + /** + * The same as the above, but with a small default buffer. + * + * @overload + */ + template typename T::Response send_message( const T& object, std::optional> logging) { @@ -101,8 +120,6 @@ class Vst3MessageHandler : public AdHocSocketHandler { * `Vst3MessageHandler::send_message()`, but deserializing the response into * an existing object. * - * TODO: We might also need overloads that reuse buffers - * * @param response_object The object to deserialize into. * * @overload Vst3MessageHandler::send_message @@ -111,7 +128,8 @@ class Vst3MessageHandler : public AdHocSocketHandler { typename T::Response& receive_into( const T& object, typename T::Response& response_object, - std::optional> logging) { + std::optional> logging, + std::vector& buffer) { using TResponse = typename T::Response; // Since a lot of messages just return a `tresult`, we can't filter out @@ -129,8 +147,8 @@ class Vst3MessageHandler : public AdHocSocketHandler { // in use it will spawn a new socket for us. this->template send( [&](boost::asio::local::stream_protocol::socket& socket) { - write_object(socket, Request(object)); - read_object(socket, response_object); + write_object(socket, Request(object), buffer); + read_object(socket, buffer, response_object); // FIXME: We have to return something here, and ML was not yet // invented when they came up with C++ so void is not // valid here @@ -145,6 +163,21 @@ class Vst3MessageHandler : public AdHocSocketHandler { return response_object; } + /** + * The same function as above, but with a small default buffer. + * + * @overload + */ + template + typename T::Response& receive_into( + const T& object, + typename T::Response& response_object, + std::optional> logging) { + std::vector buffer(64); + return receive_into(object, response_object, std::move(logging), + buffer); + } + /** * Spawn a new thread to listen for extra connections to `endpoint`, and * then start a blocking loop that handles messages from the primary @@ -165,18 +198,32 @@ class Vst3MessageHandler : public AdHocSocketHandler { * @tparam F A function type in the form of `T::Response(T)` for every `T` * in `Request`. This way we can directly deserialize into a `T::Response` * on the side that called `receive_into(T, T::Response&)`. + * @tparam keep_buffers Whether processing buffers should be kept around and + * reused. This is used to minimize allocations in the audio processing + * loop. This can only be used when `ad_hoc_sockets` is set to false, + * since we can of course only reuse buffers within a single thread. These + * buffers will also never shrink, but that should not be an issue here. * * @relates Vst3MessageHandler::send_event */ - template + template void receive_messages(std::optional> logging, F callback) { + std::vector persistent_buffer{}; + if constexpr (keep_buffers) { + static_assert(!ad_hoc_sockets, + "Buffers can only be reused when ad-hoc socket " + "spawning has been disabled."); + } + // Reading, processing, and writing back the response for the requests // we receive works in the same way regardless of which socket we're // using const auto process_message = [&](boost::asio::local::stream_protocol::socket& socket) { - auto request = read_object(socket); + auto request = keep_buffers ? read_object( + socket, persistent_buffer) + : read_object(socket); // See the comment in `receive_into()` for more information bool should_log_response = false; @@ -201,7 +248,11 @@ class Vst3MessageHandler : public AdHocSocketHandler { logger.log_response(!is_host_vst, response); } - write_object(socket, response); + if constexpr (keep_buffers) { + write_object(socket, response, persistent_buffer); + } else { + write_object(socket, response); + } }, request); }; @@ -299,6 +350,8 @@ class Vst3Sockets : public Sockets { std::to_string(instance_id) + ".sock")) .string(), false); + audio_processor_buffers.try_emplace(instance_id); + audio_processor_sockets.at(instance_id).connect(); } @@ -329,12 +382,17 @@ class Vst3Sockets : public Sockets { std::to_string(instance_id) + ".sock")) .string(), true); + audio_processor_buffers.try_emplace(instance_id); } socket_listening_latch.set_value(); audio_processor_sockets.at(instance_id).connect(); + + // This `true` indicates that we want to reuse our serialization and + // receiving buffers for all calls. This slightly reduces the amount of + // allocations in the audio processing loop. audio_processor_sockets.at(instance_id) - .receive_messages(std::nullopt, cb); + .template receive_messages(std::nullopt, cb); } /** @@ -353,6 +411,7 @@ class Vst3Sockets : public Sockets { if (audio_processor_sockets.contains(instance_id)) { audio_processor_sockets.at(instance_id).close(); audio_processor_sockets.erase(instance_id); + audio_processor_buffers.erase(instance_id); return true; } else { @@ -364,7 +423,8 @@ class Vst3Sockets : public Sockets { * Send a message from the native plugin to the Wine plugin host to handle * an `IAudioProcessor` or `IComponent` call. Since those functions are * called from a hot loop we want every instance to have a dedicated socket - * and thread for handling those. + * and thread for handling those. These calls also always reuse buffers to + * minimize allocations. * * @tparam T Some object in the `AudioProcessorRequest` variant. */ @@ -372,9 +432,9 @@ class Vst3Sockets : public Sockets { typename T::Response send_audio_processor_message( const T& object, std::optional> logging) { - // TODO: These calls should reuse buffers return audio_processor_sockets.at(object.instance_id) - .send_message(object, logging); + .send_message(object, logging, + audio_processor_buffers.at(object.instance_id)); } /** @@ -412,5 +472,11 @@ class Vst3Sockets : public Sockets { */ std::map> audio_processor_sockets; + /** + * Binary buffers used for serializing objects and receiving messages into + * during `send_audio_processor_message()`. This is used to minimize the + * amount of allocations in the audio processing loop. + */ + std::map> audio_processor_buffers; std::mutex audio_processor_sockets_mutex; };