From 1df2279d30ccb5141b01f118cb0c628959bc9a63 Mon Sep 17 00:00:00 2001 From: defiantnerd <97224712+defiantnerd@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:29:34 +0200 Subject: [PATCH] AUv2: carry the parameter backlog across a deactivate SetParameter queues onto the process adapter, and deactivateCLAP() destroys it. Anything the host set since the last render or flush is still in that queue and goes with it -- silently, and after the host has been told the value took. The window is small but it is not rare: a host sets a parameter and then deactivates the unit on a restart the plugin asked for, on a sample-rate or block-size change, or on the way through a project close. In every one of those the value is simply gone, and on the restart cases the plugin comes back up in a state the host UI disagrees with. This is the same loss the deactivated-state adapter already covers at the other end of the window -- the span when there is no process adapter -- so the backlog goes to the same place. The events move to the flush adapter as the process adapter is dropped, and the deactivated path delivers them: the next idle tick, or activateCLAP() ahead of clap_plugin.activate(), whichever comes first. Both run on the main thread with the plugin inactive, which is where clap_plugin_params.flush() is legal, so nothing is handed to the plugin during teardown while it is still active. Only parameter events move. Whatever else is queued belongs to a render that is not going to happen, and a flush may not carry it in any case. Their offsets go with the block they were offsets into, and dropping those is not tidiness. The event sort keys on time first and only breaks ties on the insertion index, so an offset left over from an earlier block would sort behind a value the deactivated state queues later at time 0 -- the stale value applied last, inverting exactly the last-write-wins this is meant to preserve. Zeroed, they tie, and the tiebreak orders them as they arrived. --- src/detail/auv2/process.cpp | 51 +++++++++++++++++++++++++++++++++++++ src/detail/auv2/process.h | 4 +++ src/wrapasauv2.cpp | 27 ++++++++++++++++++-- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/detail/auv2/process.cpp b/src/detail/auv2/process.cpp index 50ed81c8..ebe3c4a3 100644 --- a/src/detail/auv2/process.cpp +++ b/src/detail/auv2/process.cpp @@ -852,4 +852,55 @@ void ProcessAdapter::addParameterEvent(const clap_param_info_t &info, double val this->_eventindices.emplace_back(this->_events.size()); this->_events.emplace_back(n); } + +// Moves the queued parameter events onto another adapter. This exists for one +// moment: deactivateCLAP() drops the process adapter, and anything the host set +// since the last render or flush is still sitting in it. Those values are as +// real as any other -- a host that sets a parameter and then deactivates the +// unit (a restart, a format change, a project close-and-reopen) has been told +// nothing went wrong -- so they move to the adapter the deactivated state +// flushes from rather than dying with this one. +// +// Only parameter events move. Everything else queued here belongs to a render +// that is not going to happen now, and a flush may not carry it anyway. +// +// The offsets go with the block they were offsets into, and dropping them is +// not tidiness: sortEventIndices() sorts on time first, and only breaks ties on +// the insertion index. Left as they were, an event stamped into some earlier +// block would sort *after* a value the deactivated state queues later at time +// 0 -- the stale value applied last, which is the inversion this transfer +// exists to avoid. At 0 they all tie, and the tiebreak then orders them the way +// they arrived: these first, anything queued afterwards over the top. +// +// Order follows position in _events, because flush() rebuilds _eventindices +// over it before sorting. Keeping the index list in step here is for a +// process() that never comes on the adapter these are going to. +size_t ProcessAdapter::transferPendingParametersTo(ProcessAdapter &other) +{ + if (&other == this) return 0; + + size_t moved = 0; + size_t kept = 0; + for (size_t i = 0; i < _events.size(); ++i) + { + if (isParameterEvent(_events[i].header.type)) + { + clap_multi_event_t n = _events[i]; + n.header.time = 0; + other._eventindices.emplace_back(other._events.size()); + other._events.emplace_back(n); + ++moved; + } + else + { + _events[kept++] = _events[i]; + } + } + + _events.resize(kept); + _eventindices.clear(); + for (size_t i = 0; i < kept; ++i) _eventindices.emplace_back(i); + + return moved; +} } // namespace Clap::AUv2 diff --git a/src/detail/auv2/process.h b/src/detail/auv2/process.h index 30186b1e..004a7e11 100644 --- a/src/detail/auv2/process.h +++ b/src/detail/auv2/process.h @@ -113,6 +113,10 @@ class ProcessAdapter UInt32 inOffsetSampleFrame); void stopNote(int32_t note_id, int16_t channel, UInt32 inOffsetSampleFrame); void addParameterEvent(const clap_param_info_t &info, double value, uint32_t inOffsetSampleFrame); + // Hands the parameter events still queued here to another adapter, so they + // survive this one being destroyed. Returns how many moved. See + // WrapAsAUV2::deactivateCLAP(). + size_t transferPendingParametersTo(ProcessAdapter &other); // void startNote() ~ProcessAdapter(); diff --git a/src/wrapasauv2.cpp b/src/wrapasauv2.cpp index f94388e4..d0dac7a1 100644 --- a/src/wrapasauv2.cpp +++ b/src/wrapasauv2.cpp @@ -1570,7 +1570,6 @@ void WrapAsAUV2::deactivateCLAP() // pointer and then use it, and the idle tick may be inside a flush on it. ClapWrapper::detail::shared::SpinLockGuard processGuard(_processLock); _initialized = false; - _processAdapter.reset(); // Stand the deactivated-state adapter up here, on the main thread, rather // than leaving SetParameter to build it on demand. AUBase calls @@ -1580,7 +1579,31 @@ void WrapAsAUV2::deactivateCLAP() // rebuild its DSP. A render thread that had to allocate the replacement // would be allocating inside a real-time callback; finding one already // here makes it a queue push and nothing more. - ensureFlushAdapter(); + // + // It is also where the process adapter's own backlog goes. Whatever the + // host set since the last render or flush is still queued there, and + // dropping the adapter would drop it: the host was told the value took, + // and the plugin would never hear it. This is the same loss SetParameter + // avoids while the CLAP is deactivated, at the other end of the window -- + // the moment the adapter goes away rather than the span when there is + // none. Delivery is the deactivated path's job from here: the next idle + // tick, or activateCLAP() ahead of clap_plugin.activate(), whichever + // comes first, both on the main thread with the plugin inactive, which is + // where clap_plugin_params.flush() is legal. Nothing is handed to the + // plugin here -- it is still active until the deactivate() below, and a + // flush at this point would have to claim the audio thread during + // teardown. + if (auto *flushAdapter = ensureFlushAdapter()) + { + if (_processAdapter && _processAdapter->transferPendingParametersTo(*flushAdapter) > 0) + { + // Nothing else would ask: the transfer is not a SetParameter, and the + // idle tick only flushes when something has asked it to. + _requestedFlush = true; + } + } + + _processAdapter.reset(); } _plugin->stop_processing(); _plugin->deactivate();