diff --git a/dpf.mk b/dpf.mk index 3cef759a..f4442beb 100644 --- a/dpf.mk +++ b/dpf.mk @@ -106,6 +106,7 @@ SFIZZ_SOURCES = \ src/sfizz/simd/HelpersAVX.cpp \ src/sfizz/Synth.cpp \ src/sfizz/Tuning.cpp \ + src/sfizz/utility/SpinMutex.cpp \ src/sfizz/Voice.cpp \ src/sfizz/Wavetables.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7f1c9dcc..6c694239 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,8 @@ set (SFIZZ_HEADERS sfizz/Config.h sfizz/Curve.h sfizz/Debug.h + sfizz/utility/SpinMutex.h + sfizz/utility/SpinMutex.cpp sfizz/effects/impl/ResonantArray.h sfizz/effects/impl/ResonantArrayAVX.h sfizz/effects/impl/ResonantArraySSE.h diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index 6d3d75b2..cc608a12 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -108,7 +108,7 @@ sfz::EQPool::EQPool(const MidiState& state, int numEQs) sfz::EQHolderPtr sfz::EQPool::getEQ(const EQDescription& description, unsigned numChannels, float velocity) { - const std::unique_lock lock { eqGuard, std::try_to_lock }; + const std::unique_lock lock { eqGuard, std::try_to_lock }; if (!lock.owns_lock()) return {}; @@ -132,7 +132,7 @@ size_t sfz::EQPool::getActiveEQs() const size_t sfz::EQPool::setnumEQs(size_t numEQs) { - const std::lock_guard eqLock { eqGuard }; + const std::lock_guard eqLock { eqGuard }; swapAndPopAll(eqs, [](sfz::EQHolderPtr& eq) { return eq.use_count() == 1; }); diff --git a/src/sfizz/EQPool.h b/src/sfizz/EQPool.h index 7c70af22..d1f7cebb 100644 --- a/src/sfizz/EQPool.h +++ b/src/sfizz/EQPool.h @@ -2,6 +2,7 @@ #include "SfzFilter.h" #include "EQDescription.h" #include "MidiState.h" +#include "utility/SpinMutex.h" #include #include #include @@ -115,7 +116,7 @@ public: */ void setSampleRate(float sampleRate); private: - std::mutex eqGuard; + SpinMutex eqGuard; float sampleRate { config::defaultSampleRate }; const MidiState& midiState; std::vector eqs; diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 57e0e691..d33689c0 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -345,7 +345,7 @@ void sfz::FilePool::setPreloadSize(uint32_t preloadSize) noexcept void sfz::FilePool::tryToClearPromises() { - const std::lock_guard promiseLock { promiseGuard }; + const std::lock_guard promiseLock { promiseGuard }; for (auto& promise: promisesToClear) { if (promise->dataStatus != FilePromise::DataStatus::Wait) @@ -427,7 +427,7 @@ void sfz::FilePool::clear() void sfz::FilePool::cleanupPromises() noexcept { - const std::unique_lock lock { promiseGuard, std::try_to_lock }; + const std::unique_lock lock { promiseGuard, std::try_to_lock }; if (!lock.owns_lock()) return; diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 2547591f..cbec5f8f 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -32,6 +32,7 @@ #include "AudioSpan.h" #include "FileId.h" #include "SIMDHelpers.h" +#include "utility/SpinMutex.h" #include "ghc/fs_std.hpp" #include #include @@ -288,7 +289,7 @@ private: std::vector emptyPromises; std::vector temporaryFilePromises; std::vector promisesToClear; - std::mutex promiseGuard; + SpinMutex promiseGuard; // Preloaded data absl::flat_hash_map preloadedFiles; diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 164b3ea8..70811320 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -109,7 +109,7 @@ sfz::FilterPool::FilterPool(const MidiState& state, int numFilters) sfz::FilterHolderPtr sfz::FilterPool::getFilter(const FilterDescription& description, unsigned numChannels, int noteNumber, float velocity) { - const std::unique_lock lock { filterGuard, std::try_to_lock }; + const std::unique_lock lock { filterGuard, std::try_to_lock }; if (!lock.owns_lock()) return {}; @@ -133,7 +133,7 @@ size_t sfz::FilterPool::getActiveFilters() const size_t sfz::FilterPool::setNumFilters(size_t numFilters) { - const std::lock_guard filterLock { filterGuard }; + const std::lock_guard filterLock { filterGuard }; swapAndPopAll(filters, [](sfz::FilterHolderPtr& filter) { return filter.use_count() == 1; }); diff --git a/src/sfizz/FilterPool.h b/src/sfizz/FilterPool.h index 36db8f51..197eb8ad 100644 --- a/src/sfizz/FilterPool.h +++ b/src/sfizz/FilterPool.h @@ -3,6 +3,7 @@ #include "FilterDescription.h" #include "MidiState.h" #include "Defaults.h" +#include "utility/SpinMutex.h" #include #include #include @@ -120,7 +121,7 @@ public: */ void setSampleRate(float sampleRate); private: - std::mutex filterGuard; + SpinMutex filterGuard; float sampleRate { config::defaultSampleRate }; const MidiState& midiState; std::vector filters; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 58669f1d..e5f091a8 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -30,7 +30,7 @@ sfz::Synth::Synth() sfz::Synth::Synth(int numVoices) { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; parser.setListener(this); effectFactory.registerStandardEffectTypes(); effectBuses.reserve(5); // sufficient room for main and fx1-4 @@ -39,7 +39,7 @@ sfz::Synth::Synth(int numVoices) sfz::Synth::~Synth() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -166,7 +166,7 @@ void sfz::Synth::buildRegion(const std::vector& regionOpcodes) void sfz::Synth::clear() { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); @@ -396,7 +396,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) { clear(); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; std::error_code ec; fs::path realFile = fs::canonical(file, ec); @@ -417,7 +417,7 @@ bool sfz::Synth::loadSfzString(const fs::path& path, absl::string_view text) { clear(); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; parser.parseString(path, text); if (parser.getErrorCount() > 0) return false; @@ -647,7 +647,7 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept { ASSERT(samplesPerBlock < config::maxBlockSize); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; this->samplesPerBlock = samplesPerBlock; for (auto& voice : voices) @@ -663,7 +663,7 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept void sfz::Synth::setSampleRate(float sampleRate) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; this->sampleRate = sampleRate; for (auto& voice : voices) @@ -702,7 +702,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept if (resources.synthConfig.freeWheeling) resources.filePool.waitForBackgroundLoading(); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -801,7 +801,7 @@ void sfz::Synth::noteOn(int delay, int noteNumber, uint8_t velocity) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.noteOnEvent(delay, noteNumber, normalizedVelocity); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -817,7 +817,7 @@ void sfz::Synth::noteOff(int delay, int noteNumber, uint8_t velocity) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.noteOffEvent(delay, noteNumber, normalizedVelocity); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -970,7 +970,7 @@ void sfz::Synth::hdcc(int delay, int ccNumber, float normValue) noexcept ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; resources.midiState.ccEvent(delay, ccNumber, normValue); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -1281,7 +1281,7 @@ int sfz::Synth::getNumVoices() const noexcept void sfz::Synth::setNumVoices(int numVoices) noexcept { ASSERT(numVoices > 0); - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (numVoices == this->numVoices) @@ -1329,7 +1329,7 @@ void sfz::Synth::applySettingsPerVoice() void sfz::Synth::setOversamplingFactor(sfz::Oversampling factor) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (factor == oversamplingFactor) @@ -1352,7 +1352,7 @@ sfz::Oversampling sfz::Synth::getOversamplingFactor() const noexcept void sfz::Synth::setPreloadSize(uint32_t preloadSize) noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; // fast path if (preloadSize == resources.filePool.getPreloadSize()) @@ -1385,7 +1385,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept { resources.midiState.resetAllControllers(delay); - const std::unique_lock lock { callbackGuard, std::try_to_lock }; + const std::unique_lock lock { callbackGuard, std::try_to_lock }; if (!lock.owns_lock()) return; @@ -1440,7 +1440,7 @@ void sfz::Synth::disableLogging() noexcept void sfz::Synth::allSoundOff() noexcept { - const std::lock_guard disableCallback { callbackGuard }; + const std::lock_guard disableCallback { callbackGuard }; for (auto& voice : voices) voice->reset(); diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index c04b13d0..13b1f9ba 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -17,10 +17,10 @@ #include "AudioSpan.h" #include "parser/Parser.h" #include "VoiceStealing.h" +#include "utility/SpinMutex.h" #include "absl/types/span.h" #include #include -#include #include #include #include @@ -748,7 +748,7 @@ private: // Distribution used to generate random value for the *rand opcodes std::uniform_real_distribution randNoteDistribution { 0, 1 }; - std::mutex callbackGuard; + SpinMutex callbackGuard; // Singletons passed as references to the voices Resources resources; diff --git a/src/sfizz/utility/SpinMutex.cpp b/src/sfizz/utility/SpinMutex.cpp new file mode 100644 index 00000000..cb416081 --- /dev/null +++ b/src/sfizz/utility/SpinMutex.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "SpinMutex.h" +#include +#include + +// based on Timur Doumler's implementation advice for spinlocks + +void SpinMutex::lock() noexcept +{ + for (int i = 0; i < 5; ++i) { + if (try_lock()) + return; + } + + for (int i = 0; i < 10; ++i) { + if (try_lock()) + return; + atomic_queue::spin_loop_pause(); + } + + for (;;) { + for (int i = 0; i < 3000; ++i) { + if (try_lock()) + return; + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + atomic_queue::spin_loop_pause(); + } + std::this_thread::yield(); + } +} diff --git a/src/sfizz/utility/SpinMutex.h b/src/sfizz/utility/SpinMutex.h new file mode 100644 index 00000000..5e1aa347 --- /dev/null +++ b/src/sfizz/utility/SpinMutex.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include + +class SpinMutex { +public: + void lock() noexcept; + bool try_lock() noexcept; + void unlock() noexcept; + +private: + std::atomic_flag flag_ = ATOMIC_FLAG_INIT; +}; + +inline bool SpinMutex::try_lock() noexcept +{ + return !flag_.test_and_set(std::memory_order_acquire); +} + +inline void SpinMutex::unlock() noexcept +{ + flag_.clear(std::memory_order_release); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 31e134c8..21bc72d1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,7 @@ set(SFIZZ_TEST_SOURCES SemaphoreT.cpp SwapAndPopT.cpp TuningT.cpp + ConcurrencyT.cpp ) add_executable(sfizz_tests ${SFIZZ_TEST_SOURCES}) diff --git a/tests/ConcurrencyT.cpp b/tests/ConcurrencyT.cpp new file mode 100644 index 00000000..9ec72cd5 --- /dev/null +++ b/tests/ConcurrencyT.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "sfizz/utility/SpinMutex.h" +#include "catch2/catch.hpp" +#include +#include +#include + +TEST_CASE("[SpinMutex] Basic synchronization") +{ + constexpr size_t num_threads = 8; + constexpr size_t num_iterations = 1000000; + + volatile size_t counter = 0; + SpinMutex counter_mutex; + + std::thread threads[num_threads]; + + volatile bool ready = false; + std::condition_variable ready_cv; + std::mutex ready_mtx; + + auto thread_run = [&]() + { + std::unique_lock lock(ready_mtx); + ready_cv.wait(lock, [&]() -> bool { return ready; }); + lock.unlock(); + + for (size_t i = 0; i < num_iterations; ++i) { + std::unique_lock lock(counter_mutex); + ++counter; + } + }; + + for (unsigned i = 0; i < num_threads; ++i) + threads[i] = std::thread(thread_run); + + std::unique_lock lock(ready_mtx); + ready = true; + ready_cv.notify_all(); + lock.unlock(); + + for (unsigned i = 0; i < num_threads; ++i) + threads[i].join(); + + REQUIRE(counter == num_threads * num_iterations); +}