From 44863b1401651545769e054bca0f9ac87d7f0428 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:35:51 +0200 Subject: [PATCH 1/3] AUv2: track CLAP activation explicitly so a failed reactivation cannot be deactivated twice activateCLAP() now propagates clap_plugin.activate()'s failure, and deactivateCLAP() leaves an inactive plugin alone. --- src/detail/auv2/auv2_base_classes.h | 5 +++++ src/wrapasauv2.cpp | 29 ++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/detail/auv2/auv2_base_classes.h b/src/detail/auv2/auv2_base_classes.h index 27ac9ea2..8b748ed3 100644 --- a/src/detail/auv2/auv2_base_classes.h +++ b/src/detail/auv2/auv2_base_classes.h @@ -877,6 +877,11 @@ class WrapAsAUV2 : public ausdk::AUBase, // flushes so gestures pair up; see ensureFlushAdapter(). std::unique_ptr _flushAdapter; std::atomic _initialized = false; + // Whether clap_plugin.activate() succeeded and has not been matched by a + // deactivate() yet -- not the same fact as _initialized, which also goes + // false while the AU keeps running but nothing may enter the plugin. Main + // thread only (activateCLAP()/deactivateCLAP()). + bool _clapActive = false; // some info about the wrapped clap // audio-port layout captured at PostConstructor. Scanning the CLAP diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index f095d389..6e68d873 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -246,9 +246,9 @@ OSStatus WrapAsAUV2::Initialize() auto guarantee_mainthread = _plugin->AlwaysMainThread(); if (!activateCLAP()) { - // The host settled on a main-bus format pair the plugin does not accept - // (see activateCLAP). Refuse the initialization rather than render with - // buffers sized differently from the plugin's ports. + // The plugin refused the host-chosen main-bus format pair, or refused to + // activate at all (see activateCLAP). Refuse the initialization rather + // than render with buffers sized differently from the plugin's ports. return kAudioUnitErr_FormatNotSupported; } @@ -1481,7 +1481,7 @@ bool WrapAsAUV2::activateCLAP() { if (_plugin) { - assert(!_initialized); + assert(!_clapActive); // Reconcile the host-chosen bus formats with the plugin's port layout // before anything reads the ports: main thread, plugin deactivated. A // failure here must fail the activation: ValidFormat can only vet each @@ -1556,7 +1556,15 @@ bool WrapAsAUV2::activateCLAP() _flushAdapter.reset(); } - _plugin->activate(); + if (!_plugin->activate()) + { + // Take the process adapter built above back down: with no active plugin + // behind it, nothing would ever drain what SetParameter queues on it. + deactivateCLAP(); + return false; + } + _clapActive = true; + _plugin->start_processing(); _initialized = true; } @@ -1607,8 +1615,15 @@ void WrapAsAUV2::deactivateCLAP() _processAdapter.reset(); } - _plugin->stop_processing(); - _plugin->deactivate(); + + // CLAP forbids either call on a plugin that is not active, and a + // reactivation that failed leaves it exactly that way. + if (_clapActive) + { + _clapActive = false; + _plugin->stop_processing(); + _plugin->deactivate(); + } } } From 0b01380865eefaa2e1e5c1b35bbfa622b66f40ea Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:39:37 +0200 Subject: [PATCH 2/3] AUv2: render under the host's flags, own the silence flag, and never wait on the process lock Only the offline non-render phases refuse a render now; the input pull gets a copy of the flags, and Render() try_locks instead of spinning behind an idle flush. --- src/detail/shared/spinlock.h | 7 ++++++ src/wrapasauv2.cpp | 43 ++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/detail/shared/spinlock.h b/src/detail/shared/spinlock.h index d624bd5e..3e718091 100644 --- a/src/detail/shared/spinlock.h +++ b/src/detail/shared/spinlock.h @@ -20,6 +20,13 @@ struct SpinLock } } + // for a caller that must not wait -- with lock()/unlock() this also makes + // std::unique_lock(lock, std::try_to_lock) work + bool try_lock() + { + return !locked_.exchange(true, std::memory_order_acquire); + } + void unlock() { locked_.store(false, std::memory_order_release); diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index 6e68d873..aca70996 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -1647,12 +1647,26 @@ void WrapAsAUV2::releaseHostMIDIOutput() OSStatus WrapAsAUV2::Render(AudioUnitRenderActionFlags &inFlags, const AudioTimeStamp &inTimeStamp, UInt32 inFrames) { - assert(inFlags == 0); - ClapWrapper::detail::shared::SpinLockGuard processGuard(_processLock); - if (_initialized && (inFlags == 0)) + // None of the flags a host may set on the way in mean "do not process": + // DoNotCheckRenderArgs only tells AUBase to skip its argument checks, and an + // input the upstream unit marked silent still has to reach a plugin that may + // have a tail. Only the offline phases that are not a render pass are refused, + // and this is not an offline unit, so they should never arrive at all. + constexpr AudioUnitRenderActionFlags cannotProcess = + kAudioOfflineUnitRenderAction_Preflight | kAudioOfflineUnitRenderAction_Complete; + + // try_lock, not lock: onIdle() can hold this across clap_plugin_params.flush(), + // which is plugin code of unbounded duration. A block the render thread would + // have had to wait for is a block it renders silent instead. + std::unique_lock processGuard(_processLock, std::try_to_lock); + if (processGuard.owns_lock() && _initialized && !(inFlags & cannotProcess)) { // do the render dance - Clap::AUv2::ProcessData data{inFlags, inTimeStamp, inFrames, this}; + // The adapter hands these to PullInput, which ORs in whatever the upstream + // unit reports -- including its own OutputIsSilence. Ours is set below, on + // purpose; inheriting it would tell the host this plugin rendered silence. + AudioUnitRenderActionFlags pullFlags = inFlags; + Clap::AUv2::ProcessData data{pullFlags, inTimeStamp, inFrames, this}; // retrieve musical information for this render block @@ -1727,17 +1741,22 @@ OSStatus WrapAsAUV2::Render(AudioUnitRenderActionFlags &inFlags, const AudioTime // () // {} // ); + + // The plugin rendered: whatever silence the host or an upstream unit + // claimed on the way in does not describe this output. + inFlags &= static_cast(~kAudioUnitRenderAction_OutputIsSilence); } else { - // Nothing rendered this cycle (the CLAP is down while onIdle() cycles it - // for a restart), and AUBase will not silence anything for us: the AU stays - // initialized throughout, so DoRenderBus copies the output element's cache - // into the host's buffer after every noErr Render and returning without - // writing replays the last block. Zero every output element, not just the - // bus being rendered - RenderBus answers the others from their caches - and - // the silence flag on top is only a hint. _renderedSinceIdle is left alone: - // the idle tick's flush still has to make up for this block. + // Nothing rendered this cycle (the CLAP is down while onIdle() cycles it for + // a restart, or that tick still holds the process lock), and AUBase will not + // silence anything for us: the AU stays initialized throughout, so + // DoRenderBus copies the output element's cache into the host's buffer after + // every noErr Render and returning without writing replays the last block. + // Zero every output element, not just the bus being rendered - RenderBus + // answers the others from their caches - and the silence flag on top is only + // a hint. _renderedSinceIdle is left alone: the idle tick's flush still has + // to make up for this block. const auto numOutputs = Outputs().GetNumberOfElements(); for (UInt32 i = 0; i < numOutputs; ++i) { From 1525ceb5ce02ac50e29d345da9de23e8366b88c0 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:40:24 +0200 Subject: [PATCH 3/3] AUv2: publish factory presets only once the crawl is complete rebuildPresetCache() no longer numbers a partial, unsorted list and leaves the cache empty until the index finishes. --- src/wrapasauv2.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index aca70996..7963b8d4 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -362,10 +362,13 @@ void WrapAsAUV2::rebuildPresetCache() const return; } - // Only a completed crawl is worth caching. A host asking during one - AU - // asks early - would otherwise pin whatever partial list existed at that - // moment, and an empty one would stay empty until the completion tick. - _presetCacheBuilt = _presetIndex->isComplete(); + // Only a completed crawl is publishable, so stay empty until there is one: + // the index sorts itself when the crawl finishes, so a host that read a + // partial list - AU asks early - would show names against numbers that + // resolve to other presets once the sort lands. The completion tick rebuilds + // and notifies the host. + if (!_presetIndex->isComplete()) return; + _presetCacheBuilt = true; const auto presets = _presetIndex->presets(); _presetCache.reserve(presets.size());