Revert "Merge Ω-W2-T4: split the deferred FX-park drain by kind"

This reverts commit 70bd29dc89, reversing
changes made to 84afd1b545.
This commit is contained in:
2026-08-03 17:46:17 -04:00
parent 4b19556513
commit 8487833342
7 changed files with 104 additions and 401 deletions
+20 -93
View File
@@ -22,30 +22,15 @@
#define REAPERAPI_WANT_TrackFX_SetOffline
#define REAPERAPI_WANT_ValidatePtr2
#define REAPERAPI_WANT_guidToString
#define REAPERAPI_WANT_time_precise
#include "reaper_plugin_functions.h"
namespace reasampler {
namespace {
// How long a fresh park intent waits before its first FX moves. Long enough to
// swallow a rapid A→B→A flip whole — that flip's restore then cancels the park
// outright and the plugins never move at all — and short enough that the window
// in which a user could add an FX to the track the switch just hid, and so miss
// the pre-park snapshot, stays implausible. Seconds, not minutes; this is ONE
// term of the hazard's width — see tickDeferredFxParks for the other.
constexpr double kParkCoalesceSeconds = 1.0;
FxParkQueue g_queue;
ReaProject* g_owner = nullptr; // the project the pending intents were enqueued against
bool g_draining = false;
// The gate is GLOBAL, not per-track: a park enqueued for track Z re-arms this
// same stamp even though it does nothing to an already-partial park on track A.
// Benign — that track's FX are already I_FXEN=0/B_MAINSEND=0 from the
// synchronous half, so only its CPU reclaim (not correctness) is delayed — but
// this is not a per-intent debounce.
double g_parkReadyAt = 0.0; // parkReadyAt() stamp; re-armed by every enqueued park
// Makes "one drain at a time" explicit rather than implied by the call sites.
// RAII because an apply can throw and a stuck flag would silence the queue for
@@ -76,43 +61,9 @@ void setOfflineIfChanged(MediaTrack* tr, int fx, bool offline) {
TrackFX_SetOffline(tr, fx, offline);
}
// The park's next unit of work, or -1 when the track is fully parked. Derived
// from the live chain rather than a stored cursor so a chain edited between two
// ticks cannot leave the park writing past the end of it or skipping a slot.
int firstOnlineFx(MediaTrack* tr) {
void applyPark(MediaTrack* tr) {
const int fxCount = TrackFX_GetCount(tr);
for (int fx = 0; fx < fxCount; ++fx)
if (!TrackFX_GetOffline(tr, fx)) return fx;
return -1;
}
// One track by GUID, resolved afresh every tick — caching the handle across
// ticks is exactly the dangle ValidatePtr2 is here to prevent. Still O(tracks),
// still one guidString() per track per tick; a per-tick unordered_map keyed by
// GUID would cost the same enumeration to build for a single lookup, so the only
// thing avoided by NOT building one is that allocation.
MediaTrack* findTrack(ReaProject* proj, const std::string& guid) {
const int count = CountTracks(proj);
for (int i = 0; i < count; ++i) {
MediaTrack* tr = GetTrack(proj, i);
if (tr && guidString(tr) == guid) return tr;
}
return nullptr;
}
// Shared entry gate for both drains: nothing pending, someone already draining,
// or a queue belonging to a project that is no longer current — the last of
// which discards. Catches only a DIFFERENT live pointer; a REAPER-recycled
// address is caught upstream, by the caller discarding on the session's own load
// transition.
bool drainGateOpen() {
if (g_queue.empty()) return false;
if (g_draining) return false; // re-entered mid-apply: those intents are the next pass's
if (g_owner != currentProject()) {
discardDeferredFxParks();
return false;
}
return true;
for (int fx = 0; fx < fxCount; ++fx) setOfflineIfChanged(tr, fx, true);
}
// Restores per-FX offline from the plan verbatim — never a blanket "online".
@@ -173,9 +124,6 @@ PreParkFx snapshotFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& canc
std::vector<FxOfflineOp> deferFxPark(ReaProject* proj, const std::string& guid) {
adoptOwner(proj);
// Re-armed, not set once: while switches keep arriving the park keeps
// waiting, which is the whole point of coalescing them.
g_parkReadyAt = parkReadyAt(time_precise(), kParkCoalesceSeconds);
return g_queue.park(guid);
}
@@ -189,8 +137,19 @@ void discardDeferredFxParks() {
g_owner = nullptr;
}
void drainDeferredFxRestores() {
if (!drainGateOpen()) return;
void drainDeferredFxParks() {
if (g_queue.empty()) return;
if (g_draining) return; // re-entered mid-apply: those intents are the outer drain's next pass
if (g_owner != currentProject()) {
// The project the intents were planned against was closed or switched
// away from: its tracks are not ours to write and its handles may be
// gone. Discard rather than apply. This catches only a DIFFERENT live
// pointer; a REAPER-recycled address is caught upstream, by the caller
// discarding on the session's own load transition.
discardDeferredFxParks();
return;
}
ReaProject* const proj = g_owner;
const DrainScope scope;
@@ -199,8 +158,7 @@ void drainDeferredFxRestores() {
// the message loop (plugins load and unload), so a re-entrant switch can
// enqueue while this runs. Everything defensive below rests on that one
// assumption; each piece is correct regardless of whether it holds.
const std::vector<FxParkIntent> draining = g_queue.takeRestores();
if (draining.empty()) return; // parks only — not this half's work
const std::vector<FxParkIntent> draining = g_queue.take();
std::unordered_map<std::string, MediaTrack*> byGuid;
const int count = CountTracks(proj);
@@ -227,6 +185,10 @@ void drainDeferredFxRestores() {
if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return;
if (!ValidatePtr2(proj, it->second, "MediaTrack*")) continue;
if (intent.park) {
applyPark(it->second);
continue;
}
const FxRestoreDrops d = applyRestore(it->second, intent.restoreOps);
if (d.total() > 0) {
drops.add(d);
@@ -243,39 +205,4 @@ void drainDeferredFxRestores() {
if (!fxDropMsg.empty()) ShowConsoleMsg(("!SHOW:" + fxDropMsg).c_str());
}
void tickDeferredFxParks() {
if (!drainGateOpen()) return;
if (!parkIsReady(time_precise(), g_parkReadyAt)) return; // still coalescing
const std::string guid = g_queue.nextParkGuid();
if (guid.empty()) return; // restores only — not this half's work
ReaProject* const proj = g_owner;
const DrainScope scope;
// Validated before the enumeration below reads the project, and again for the
// track it resolves — same gate, and the same reason, as the restore drain's
// per-intent re-validation: a longer park window makes a deleted track
// likelier, not less.
if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return;
MediaTrack* tr = findTrack(proj, guid);
if (!tr || !ValidatePtr2(proj, tr, "MediaTrack*")) {
g_queue.finishPark(guid); // track deleted since the switch — prune
return;
}
const int fx = firstOnlineFx(tr);
if (fx < 0) {
g_queue.finishPark(guid); // fully parked
return;
}
// BEFORE the write, not after: the write may pump the message loop, and a
// restore arriving in that window has to supersede this park rather than
// cancel it outright — the FX about to go offline would otherwise be left
// there with the only record of its prior state dropped.
g_queue.markPartial(guid);
TrackFX_SetOffline(tr, fx, true);
}
} // namespace reasampler