From 72a6a0c31a5f95ae00b44a5474f0872d8777ffdb Mon Sep 17 00:00:00 2001 From: Johannes Lorenz Date: Wed, 10 Aug 2022 21:32:32 +0200 Subject: [PATCH] Implement Lv2 Worker --- include/AudioEngine.h | 1 + include/LocklessRingBuffer.h | 3 +- include/Lv2Proc.h | 8 ++ include/Lv2Worker.h | 91 +++++++++++++++++ include/Semaphore.h | 87 ++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/Semaphore.cpp | 136 +++++++++++++++++++++++++ src/core/lv2/Lv2Manager.cpp | 2 + src/core/lv2/Lv2Proc.cpp | 28 ++++- src/core/lv2/Lv2Worker.cpp | 192 +++++++++++++++++++++++++++++++++++ 10 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 include/Lv2Worker.h create mode 100644 include/Semaphore.h create mode 100644 src/core/Semaphore.cpp create mode 100644 src/core/lv2/Lv2Worker.cpp diff --git a/include/AudioEngine.h b/include/AudioEngine.h index 8cadcc0e212..3434ed8a5e4 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -197,6 +197,7 @@ class LMMS_EXPORT AudioEngine : public QObject // audio-device-stuff + bool renderOnly() const { return m_renderOnly; } // Returns the current audio device's name. This is not necessarily // the user's preferred audio device, in case you were thinking that. inline const QString & audioDevName() const diff --git a/include/LocklessRingBuffer.h b/include/LocklessRingBuffer.h index 60f8ab98996..00861a01586 100644 --- a/include/LocklessRingBuffer.h +++ b/include/LocklessRingBuffer.h @@ -51,13 +51,14 @@ class LocklessRingBuffer std::size_t capacity() const {return m_buffer.maximum_eventual_write_space();} std::size_t free() const {return m_buffer.write_space();} void wakeAll() {m_notifier.wakeAll();} - std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false) + std::size_t write(const T *src, std::size_t cnt, bool notify = false) { std::size_t written = LocklessRingBuffer::m_buffer.write(src, cnt); // Let all waiting readers know new data are available. if (notify) {LocklessRingBuffer::m_notifier.wakeAll();} return written; } + void mlock() { m_buffer.mlock(); } protected: ringbuffer_t m_buffer; diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 31a69404437..2fb46eae1d7 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -31,10 +31,12 @@ #include #include +#include #include "Lv2Basics.h" #include "Lv2Features.h" #include "Lv2Options.h" +#include "Lv2Worker.h" #include "LinkedModelGroups.h" #include "Plugin.h" #include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" @@ -172,8 +174,14 @@ class Lv2Proc : public LinkedModelGroup const LilvPlugin* m_plugin; LilvInstance* m_instance; Lv2Features m_features; + + // options Lv2Options m_options; + // worker + std::optional m_worker; + Semaphore m_work_lock; // this must be shared by different workers + // full list of ports std::vector> m_ports; // quick reference to specific, unique ports diff --git a/include/Lv2Worker.h b/include/Lv2Worker.h new file mode 100644 index 00000000000..902b61c6288 --- /dev/null +++ b/include/Lv2Worker.h @@ -0,0 +1,91 @@ +/* + * Lv2Worker.h - Lv2Worker class + * + * Copyright (c) 2022-2022 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2WORKER_H +#define LV2WORKER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include + +#include "LocklessRingBuffer.h" +#include "Semaphore.h" + +namespace lmms +{ + +/** + Worker container +*/ +class Lv2Worker +{ +public: + // CTOR/DTOR/feature access + Lv2Worker(LV2_Handle handle, const LV2_Worker_Interface* iface, Semaphore& common_work_lock, bool threaded); + ~Lv2Worker(); + const LV2_Worker_Schedule* feature() const { return &m_scheduleFeature; } + + // public API + void emitResponses(); + void notifyPluginThatRunFinished() + { + if(iface->end_run) { iface->end_run(m_scheduleFeature.handle); } + } + + // to be called only by static functions + LV2_Worker_Status scheduleWork(uint32_t size, const void* data); + LV2_Worker_Status respond(uint32_t size, const void* data); + +private: + // functions + void* workerFunc(); + static std::size_t bufferSize(); //!< size of internal buffers + + // parameters + const LV2_Worker_Interface* iface; + bool threaded; + LV2_Handle handle; + LV2_Worker_Schedule m_scheduleFeature; + + // threading/synchronization + std::thread thread; + std::vector response; + LocklessRingBuffer requests, responses; + LocklessRingBufferReader requestsReader, responsesReader; + std::atomic exit = false; + Semaphore sem; + Semaphore& work_lock; +}; + + +} // namespace lmms + +#endif // LMMS_HAVE_LV2 + +#endif // LV2WORKER_H + diff --git a/include/Semaphore.h b/include/Semaphore.h new file mode 100644 index 00000000000..11ce4eb10c6 --- /dev/null +++ b/include/Semaphore.h @@ -0,0 +1,87 @@ +/* + * Semaphore.h - Semaphore declaration + * + * Copyright (c) 2022-2022 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +/* + * This code has been copied and adapted from https://github.com/drobilla/jalv + * File src/zix/sem.h + */ + +#ifndef LMMS_SEMAPHORE_H +#define LMMS_SEMAPHORE_H + +#ifdef LMMS_BUILD_APPLE +# include +#elif defined(LMMS_BUILD_WIN32) +# include +#else +# include +#endif + +#include + +/** + A counting semaphore. + + This is an integer that is always positive, and has two main operations: + increment (post) and decrement (wait). If a decrement can not be performed + (i.e. the value is 0) the caller will be blocked until another thread posts + and the operation can succeed. + + Semaphores can be created with any starting value, but typically this will + be 0 so the semaphore can be used as a simple signal where each post + corresponds to one wait. + + Semaphores are very efficient (much moreso than a mutex/cond pair). In + particular, at least on Linux, post is async-signal-safe, which means it + does not block and will not be interrupted. If you need to signal from + a realtime thread, this is the most appropriate primitive to use. + + @note Likely outdated with C++20's std::counting_semaphore + (though we have to check that this will be RT conforming on all platforms) +*/ +class Semaphore +{ +public: + Semaphore(unsigned initial); + Semaphore(const Semaphore&) = delete; + Semaphore& operator=(const Semaphore&) = delete; + Semaphore(Semaphore&&) = delete; + Semaphore& operator=(Semaphore&&) = delete; + ~Semaphore(); + + void post(); + void wait(); + bool try_wait(); + +private: +#ifdef LMMS_BUILD_APPLE + semaphore_t sem; +#elif defined(LMMS_BUILD_WIN32) + HANDLE sem; +#else + sem_t sem; +#endif +}; + +#endif // LMMS_SEMAPHORE_H diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b8809ed78f3..d70420adb98 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -70,6 +70,7 @@ set(LMMS_SRCS core/SamplePlayHandle.cpp core/SampleRecordHandle.cpp core/Scale.cpp + core/Semaphore.cpp core/SerializingObject.cpp core/Song.cpp core/TempoSyncKnobModel.cpp @@ -110,6 +111,7 @@ set(LMMS_SRCS core/lv2/Lv2SubPluginFeatures.cpp core/lv2/Lv2UridCache.cpp core/lv2/Lv2UridMap.cpp + core/lv2/Lv2Worker.cpp core/midi/MidiAlsaRaw.cpp core/midi/MidiAlsaSeq.cpp diff --git a/src/core/Semaphore.cpp b/src/core/Semaphore.cpp new file mode 100644 index 00000000000..9d865acb69f --- /dev/null +++ b/src/core/Semaphore.cpp @@ -0,0 +1,136 @@ +/* + * Semaphore.cpp - Semaphore implementation + * + * Copyright (c) 2022-2022 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +/* + * This code has been copied and adapted from https://github.com/drobilla/jalv + * File src/zix/sem.h + */ + +#include "Semaphore.h" + +#if defined(LMMS_BUILD_WIN32) +# include +#else +# include +#endif + +#include + +#ifdef LMMS_BUILD_APPLE +Semaphore::Semaphore(unsigned val) +{ + kern_return_t rval = semaphore_create(mach_task_self(), &sem, SYNC_POLICY_FIFO, val); + if(rval != 0) + throw std::system_error(rval, std::system_category(), "Could not create semaphore"); +} + +Semaphore::~Semaphore() +{ + semaphore_destroy(mach_task_self(), sem); +} + +void Semaphore::post() +{ + semaphore_signal(sem); +} + +void Semaphore::wait() +{ + kern_return_t rval = semaphore_wait(sem); + if (rval != KERN_SUCCESS) { + throw std::system_error(rval, std::system_category(), "Waiting for semaphore failed"); + } +} + +bool Semaphore::try_wait() +{ + const mach_timespec_t zero = { 0, 0 }; + return semaphore_timedwait(sem, zero) == KERN_SUCCESS; +} + +#elif defined(LMMS_BUILD_WIN32) + +Semaphore::Semaphore(unsigned initial) +{ + if(CreateSemaphore(NULL, initial, LONG_MAX, NULL) == nullptr) + throw std::system_error(GetLastError(), std::syssystem_category(), "Could not create semaphore"); +} + +Semaphore::~Semaphore() +{ + CloseHandle(sem); +} + +void Semaphore::post() +{ + ReleaseSemaphore(sem, 1, NULL); +} + +void Semaphore::wait() +{ + if (WaitForSingleObject(sem, INFINITE) != WAIT_OBJECT_0) { + throw std::system_error(GetLastError(), std::syssystem_category(), "Waiting for semaphore failed"); + } +} + +bool Semaphore::try_wait() +{ + return WaitForSingleObject(sem, 0) == WAIT_OBJECT_0; +} + +#else /* !defined(LMMS_BUILD_APPLE) && !defined(LMMS_BUILD_WIN32) */ + +Semaphore::Semaphore(unsigned initial) +{ + if(sem_init(&sem, 0, initial) != 0) + throw std::system_error(errno, std::generic_category(), "Could not create semaphore"); +} + +Semaphore::~Semaphore() +{ + sem_destroy(&sem); +} + +void Semaphore::post() +{ + sem_post(&sem); +} + +void Semaphore::wait() +{ + while (sem_wait(&sem) != 0) { + if (errno != EINTR) { + throw std::system_error(errno, std::generic_category(), "Waiting for semaphore failed"); + } + /* Otherwise, interrupted, so try again. */ + } +} + +bool Semaphore::try_wait() +{ + return (sem_trywait(&sem) == 0); +} + +#endif + diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp index 1622d4c1b2d..027b74bf15c 100644 --- a/src/core/lv2/Lv2Manager.cpp +++ b/src/core/lv2/Lv2Manager.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -76,6 +77,7 @@ Lv2Manager::Lv2Manager() : m_supportedFeatureURIs.insert(LV2_URID__map); m_supportedFeatureURIs.insert(LV2_URID__unmap); m_supportedFeatureURIs.insert(LV2_OPTIONS__options); + m_supportedFeatureURIs.insert(LV2_WORKER__schedule); // min/max is always passed in the options m_supportedFeatureURIs.insert(LV2_BUF_SIZE__boundedBlockLength); // block length is only changed initially in AudioEngine CTOR diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 20987b32fe4..088cb7077dd 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -161,6 +162,7 @@ Plugin::PluginTypes Lv2Proc::check(const LilvPlugin *plugin, Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : LinkedModelGroup(parent), m_plugin(plugin), + m_work_lock(1), m_midiInputBuf(m_maxMidiInputEvents), m_midiInputReader(m_midiInputBuf) { @@ -332,6 +334,14 @@ void Lv2Proc::copyBuffersToCore(sampleFrame* buf, void Lv2Proc::run(fpp_t frames) { lilv_instance_run(m_instance, static_cast(frames)); + + if (m_worker) + { + // Process any worker replies + m_worker.value().emitResponses(m_instance); + // Notify the plugin the run() cycle is finished + if (m_worker) { m_worker->notifyPluginThatRunFinished(); } + } } @@ -365,7 +375,7 @@ void Lv2Proc::handleMidiInputEvent(const MidiEvent &event, const TimePos &time, else { qWarning() << "Warning: Caught MIDI event for an Lv2 instrument" - << "that can not hande MIDI... Ignoring"; + << "that can not handle MIDI... Ignoring"; } } @@ -399,6 +409,13 @@ void Lv2Proc::initPlugin() if (m_instance) { + const LV2_Worker_Interface* iface = (const LV2_Worker_Interface*) + lilv_instance_get_extension_data(m_instance, LV2_WORKER__interface); + if(iface) { + bool threaded = !Engine::audioEngine()->renderOnly(); + m_worker.emplace(m_instance->lv2_handle, iface, m_work_lock, threaded); + } + for (std::size_t portNum = 0; portNum < m_ports.size(); ++portNum) connectPort(portNum); lilv_instance_activate(m_instance); @@ -472,8 +489,17 @@ void Lv2Proc::initMOptions() void Lv2Proc::initPluginSpecificFeatures() { + // options initMOptions(); m_features[LV2_OPTIONS__options] = const_cast(m_options.feature()); + + // worker (if plugin has worker extension) + Lv2Manager* mgr = Engine::getLv2Manager(); + if (lilv_plugin_has_extension_data(m_plugin, mgr->uri(LV2_WORKER__interface).get())) { + m_features[LV2_WORKER__schedule] = const_cast(m_worker->feature()); + // note: the worker interface can not be instantiated yet - it requires m_instance + // see initPlugin() + } } diff --git a/src/core/lv2/Lv2Worker.cpp b/src/core/lv2/Lv2Worker.cpp new file mode 100644 index 00000000000..c9206823bb4 --- /dev/null +++ b/src/core/lv2/Lv2Worker.cpp @@ -0,0 +1,192 @@ +/* + * Lv2Worker.cpp - Lv2Worker implementation + * + * Copyright (c) 2022-2022 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include +#include + +#include "Lv2Worker.h" + +#ifdef LMMS_HAVE_LV2 + +#include "Engine.h" + + +namespace lmms +{ + + +// static wrappers (TODO: avoid those somehow?) + +static LV2_Worker_Status staticScheduleWork(LV2_Worker_Schedule_Handle handle, + uint32_t size, const void* data) +{ + Lv2Worker* worker = static_cast(handle); + return worker->scheduleWork(size, data); +} + + + + +static LV2_Worker_Status +staticWorkerRespond(LV2_Worker_Respond_Handle handle, + uint32_t size, const void* data) +{ + Lv2Worker* worker = static_cast(handle); + return worker->respond(size, data); +} + + + + +std::size_t Lv2Worker::bufferSize() +{ + // ardour uses this fixed size for ALSA: + return 8192 * 4; + // for jack, they use 4 * jack_port_type_get_buffer_size (..., JACK_DEFAULT_MIDI_TYPE) + // (possible extension for AudioDevice) +} + + + + +Lv2Worker::Lv2Worker(LV2_Handle handle, + const LV2_Worker_Interface* iface, + Semaphore& common_work_lock, + bool threaded) : + iface(iface), + threaded(threaded), + handle(handle), + response(bufferSize()), + requests(bufferSize()), + responses(bufferSize()), + requestsReader(requests), + responsesReader(responses), + sem(0), + work_lock(common_work_lock) +{ + assert(iface); + m_scheduleFeature.handle = static_cast(this); + m_scheduleFeature.schedule_work = staticScheduleWork; + + if (threaded) { thread = std::thread(&Lv2Worker::workerFunc, this); } + + requests.mlock(); + responses.mlock(); +} + + + + +Lv2Worker::~Lv2Worker() +{ + exit = true; + if(threaded) { + sem.post(); + thread.join(); + } +} + + + + +LV2_Worker_Status Lv2Worker::respond(uint32_t size, const void* data) +{ + if(!size) { return LV2_WORKER_ERR_UNKNOWN; } + + responses.write((const char*)&size, sizeof(size)); + responses.write((const char*)data, size); + return LV2_WORKER_SUCCESS; +} + + + + +void *Lv2Worker::workerFunc() +{ + std::vector buf; + uint32_t size; + while (true) { + sem.wait(); + if (exit) break; + if (requestsReader.read_space() <= sizeof(size)) continue; // (should not happen) + + requestsReader.read(sizeof(size)).copy((char*)&size, sizeof(size)); + try { + buf.resize(size); + } catch(...) { + qWarning() << "Error: reallocating buffer failed"; + return nullptr; + } + requestsReader.read(sizeof(size)).copy(buf.data(), size); + + work_lock.wait(); + iface->work(handle, staticWorkerRespond, this, size, buf.data()); + work_lock.post(); + } + return nullptr; +} + + + + +LV2_Worker_Status Lv2Worker::scheduleWork(uint32_t size, const void *data) +{ + if (!size) { return LV2_WORKER_ERR_UNKNOWN; } + + if (threaded) { + // Schedule a request to be executed by the worker thread + requests.write((const char*)&size, sizeof(size)); + requests.write((const char*)data, size); + sem.post(); + } else { + // Execute work immediately in this thread + work_lock.wait(); + iface->work(handle, staticWorkerRespond, this, size, data); + work_lock.post(); + } + + return LV2_WORKER_SUCCESS; +} + + + + +void Lv2Worker::emitResponses() +{ + if (!exit) { + std::size_t read_space = responsesReader.read_space(); + uint32_t size; + while (read_space > sizeof(size)) { + responsesReader.read(sizeof(size)).copy((char*)&size, sizeof(size)); + responsesReader.read(size).copy(response.data(), size); + iface->work_response(handle, size, response.data()); + read_space -= sizeof(size) + size; + } + } +} + + +} // namespace lmms + +#endif // LMMS_HAVE_LV2