Close the FX reclaimer's undo span at the state-load hook, and expose the drain's direction
A Ctrl-Z mid-span used to close the pre-undo block over post-undo state; it now terminates at BeginLoadProjectState and stops the drain where it stands. fxReclaimActivity reports Online/Offline/Idle; fxReclaimPending is unchanged.
This commit is contained in:
@@ -247,7 +247,15 @@ static void OnTimer()
|
||||
static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/)
|
||||
{
|
||||
if (isUndo)
|
||||
{
|
||||
// BEFORE the reload request, and the whole reason this is a hook rather than
|
||||
// something the reload's own reapply does on arrival: the reclaimer's undo block
|
||||
// is open for at least kParkHoldMs after every real mode switch, and a switch is
|
||||
// exactly what this undo is rolling back. Closing it here is the last moment it
|
||||
// still closes over the state it was opened on.
|
||||
reasampler::fxReclaimAbandonForStateLoad();
|
||||
g_session.requestReload();
|
||||
}
|
||||
}
|
||||
|
||||
// Intentional no-op: ReaSampler stores state via project EXT STATE, not this
|
||||
|
||||
@@ -84,12 +84,25 @@ decide membership or mode rules.
|
||||
block swallows every edit the user makes afterwards, indefinitely, so every
|
||||
terminating path routes through ONE idempotent close (`fxReclaimEndUndoSpan`): the
|
||||
drain settling, aborting on a generation bump, or deferring on the transport; the
|
||||
project retargeting or closing; the next apply superseding it; a project save;
|
||||
extension unload. The span may stay open only across a wait it can BOUND — the 3s
|
||||
debounce, which is the dead time between the switch and its first FX write, and
|
||||
the only reason the span exists. A TRANSPORT deferral is not bounded (it lasts as
|
||||
long as the playback pass), so it ends the span and the parking that follows mints
|
||||
its own point (`spanEndsAfter`).
|
||||
project retargeting or closing; the next apply superseding it; a USER-DRIVEN STATE
|
||||
LOAD; a project save; extension unload. The span may stay open only across a wait it
|
||||
can BOUND — the 3s debounce, which is the dead time between the switch and its first
|
||||
FX write, and the only reason the span exists. A TRANSPORT deferral is not bounded
|
||||
(it lasts as long as the playback pass), so it ends the span and the parking that
|
||||
follows mints its own point (`spanEndsAfter`).
|
||||
- **A Ctrl-Z terminates the span itself, at the hook, before REAPER restores
|
||||
anything.** `fxReclaimAbandonForStateLoad` runs from `BeginLoadProjectState`
|
||||
(`main.cpp`) — the only point at which the block still closes over the state it was
|
||||
OPENED on. The span is open for at least `kParkHoldMs` after every real switch and a
|
||||
switch is exactly what a user undoes, so letting the close happen where it used to —
|
||||
as a side effect of the reapply the reloaded model triggers, a full timer tick later
|
||||
— closed the pre-undo block over POST-undo state, minting a spurious point and
|
||||
destroying the redo branch. The same call stops any drain on the stack where it
|
||||
stands (`FxReclaimQueue::abandon`, refused at both host entry points), which is
|
||||
stronger than an invalidation: an invalidation still finishes the track it is on,
|
||||
because a step is atomic. Daniel's ruling, and the accepted cost: undoing a switch
|
||||
mid-drain leaves a PARTIAL park — some FX offlined, some not — covered by the one
|
||||
point the terminator closed.
|
||||
- **A reapply that wrote nothing leaves no undo point** — and a reapply never holds
|
||||
the span open, so a project open, a membership edit and `render_in_place`'s inner
|
||||
reapply all keep the block shape they had. The load-tick reapply
|
||||
@@ -148,7 +161,7 @@ applies the resulting lane state to live tracks.
|
||||
- `view_reconcile` — the repair pass for a project state REAPER reloaded under us: `noteAppliedState` records what an apply left driven; `reconcileAppliedFlags` diffs that fingerprint against the reloaded model and hands back any track the reload no longer records but still reads driven — the fix for `UNDO_STATE_TRACKCFG` not carrying `B_SHOWINTCP`/`B_SHOWINMIXER` (DAW-confirmed).
|
||||
- `view_tracks` — the scaffolding every write pass sets up: `TrackHandles`/`TrackByGuid`, `readFolderEntries`/`indexHandles`, and `UiRefreshHold` (the `PreventUIRefresh` RAII pair every write phase runs under).
|
||||
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface, plus `clearTrackSolos`/`restoreTrackSolos`, the outgoing-clear and incoming-replay entry points `view` drives them through. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
|
||||
- `fx_reclaim` — the FX convergence loop, off the switch's synchronous path (`TrackFX_SetOffline` was 1897ms of a measured 1942ms switch). `drainTick` moves one track's FX per step against `core/view/fx_state`'s derived work list (Online before Offline; an Offline item waits out `kParkHoldMs` so a fast A→B→A flip moves nothing), budgeted per tick (`kOnlineBudgetMs`/`kOfflineBudgetMs`) and gated off the transport for the Offline direction only. `FxReclaimQueue` owns the derived queue's lifetime across re-entrancy (a plugin unload is assumed to pump). `UndoSpan` owns the ONE undo block the whole drain writes under — opened by `applyMode` on a real switch, else lazily by the first `writeOffline`, and closed through the single idempotent `fxReclaimEndUndoSpan` that every terminating path routes through (`spanEndsAfter` decides which ticks terminate). `ReleaseFlush`/`fxReclaimFlushRelease` make a completed record's spend durable on the project's next save, never on the drain's own initiative.
|
||||
- `fx_reclaim` — the FX convergence loop, off the switch's synchronous path (`TrackFX_SetOffline` was 1897ms of a measured 1942ms switch). `drainTick` moves one track's FX per step against `core/view/fx_state`'s derived work list (Online before Offline; an Offline item waits out `kParkHoldMs` so a fast A→B→A flip moves nothing), budgeted per tick (`kOnlineBudgetMs`/`kOfflineBudgetMs`) and gated off the transport for the Offline direction only. `FxReclaimQueue` owns the derived queue's lifetime across re-entrancy (a plugin unload is assumed to pump). `UndoSpan` owns the ONE undo block the whole drain writes under — opened by `applyMode` on a real switch, else lazily by the first `writeOffline`, and closed through the single idempotent `fxReclaimEndUndoSpan` that every terminating path routes through (`spanEndsAfter` decides which ticks terminate; `fxReclaimAbandonForStateLoad` is the undo/redo terminator, which also stops the drain). `ReleaseFlush`/`fxReclaimFlushRelease` make a completed record's spend durable on the project's next save, never on the drain's own initiative. `fxReclaimActivity` reports WHICH DIRECTION the drain is moving for an indicator to paint — Idle whenever nothing may move, so a debounced or transport-held offline item never names a direction nothing is travelling in; undirected `fxReclaimPending` stays the timer's one idle test and must not drive a UI (it is true throughout the debounce, when only the OPPOSITE direction is eligible).
|
||||
|
||||
## Gotchas
|
||||
|
||||
@@ -182,8 +195,9 @@ applies the resulting lane state to live tracks.
|
||||
flip — still closes on `kViewUndoMask` alone and costs no FX serialization. What a
|
||||
Ctrl-Z cannot restore is anything the reclaimer converges AFTER the span closed: a
|
||||
transport deferral, and a drain no switch opened a span for, each mint their own
|
||||
point. Whether that reads as correct in the DAW is `[verify — DAW]`,
|
||||
`docs/VERIFICATION.md` §"Mode switching".
|
||||
point — and, since the terminator above, whatever the drain had NOT yet written when
|
||||
the undo arrived, because the span closes at that instant. Whether that reads as
|
||||
correct in the DAW is `[verify — DAW]`, `docs/VERIFICATION.md` §"Mode switching".
|
||||
- **The `view_state` write is NOT inside `applyMode`'s block — but on a REAL switch
|
||||
it is now inside the SPAN.** `persistViewState` (`design_view_actions.cpp`) runs
|
||||
after `applyMode` has returned, and a real switch returns with the span open, so
|
||||
|
||||
@@ -60,6 +60,15 @@ void closeSpan() {
|
||||
if (ValidatePtr2(nullptr, proj, "ReaProject*")) Undo_EndBlock2(proj, label.c_str(), mask);
|
||||
}
|
||||
|
||||
// Monotonic and sub-millisecond: the drain's budget discriminates at 30ms, which
|
||||
// GetTickCount's ~16ms granularity cannot do. No wraparound to guard against, so
|
||||
// util::elapsedAtLeast (whose whole reason for existing is that wrap) does not apply.
|
||||
double monotonicMs() {
|
||||
return std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
// TrackFX_SetOffline is ASSUMED to pump the message loop, so a plugin load can re-enter
|
||||
// the timer and land back here mid-drain. Fail closed: a re-entered drain would derive a
|
||||
// second queue for the same project and race the outer one's writes track for track.
|
||||
@@ -122,21 +131,18 @@ struct ReclaimHost {
|
||||
return cachedTrack_;
|
||||
}
|
||||
|
||||
// Monotonic and sub-millisecond: the budget below discriminates at 30ms, which
|
||||
// GetTickCount's ~16ms granularity cannot do. No wraparound to guard against, so
|
||||
// util::elapsedAtLeast (whose whole reason for existing is that wrap) does not apply.
|
||||
double nowMs() const {
|
||||
return std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
double nowMs() const { return monotonicMs(); }
|
||||
|
||||
// The SAME predicate applyMode refuses a real mode switch on, deliberately shared
|
||||
// rather than re-read here: "the transport is rolling" is one fact with one home
|
||||
// (view.h). drainTick narrows what it gates with this answer to offline work only.
|
||||
bool transportRolling() const { return transportBlocksModeSwitch(project); }
|
||||
|
||||
// Refusing here ends the drain's REMAINING tracks after an abandon: drainTick treats a
|
||||
// false as "this track is gone", so the rest of the queue costs one bool apiece and
|
||||
// reaches no REAPER call at all.
|
||||
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||
if (g_queue.abandoned()) return false;
|
||||
MediaTrack* tr = resolveForStep(guid);
|
||||
if (!tr) return false;
|
||||
const int count = TrackFX_GetCount(tr);
|
||||
@@ -148,7 +154,11 @@ struct ReclaimHost {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ...and here for the track being written RIGHT NOW, whose own pump is how the state
|
||||
// load arrived. FIRST, before the openSpan below: a write past the terminator would
|
||||
// both land in the project the load is replacing and reopen the span it just closed.
|
||||
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
|
||||
if (g_queue.abandoned()) return false;
|
||||
MediaTrack* tr = resolveForStep(guid);
|
||||
if (!tr) return false;
|
||||
if (fxIndex < 0 || fxIndex >= TrackFX_GetCount(tr)) return false; // chain shrank
|
||||
@@ -199,6 +209,20 @@ void reportDrops(ReaProject* proj, const std::vector<TrackFxDrops>& drops) {
|
||||
|
||||
bool fxReclaimPending() { return g_queue.pending(); }
|
||||
|
||||
FxReclaimActivity fxReclaimActivity() {
|
||||
// The QUEUE's own pin, never the active tab: the direction is a fact about the project
|
||||
// the drain is converging, and reading another tab's transport into it would gate the
|
||||
// answer on a playhead that has nothing to do with this work. No pin means idle — and
|
||||
// costs the caller no REAPER call at all, which is what makes this safe to poll per
|
||||
// frame.
|
||||
ReaProject* const proj = g_queue.project();
|
||||
if (!proj) return FxReclaimActivity::Idle;
|
||||
// The pin is only re-tested at beginTick; a poll landing between two ticks can find the
|
||||
// project already gone, and GetPlayStateEx is about to be handed this pointer.
|
||||
if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return FxReclaimActivity::Idle;
|
||||
return g_queue.activity(monotonicMs(), transportBlocksModeSwitch(proj));
|
||||
}
|
||||
|
||||
void invalidateFxReclaim() {
|
||||
// A generation bump and a flag, and deliberately nothing more. Discarding the queue
|
||||
// here is what let a re-entered apply free the element the outer drain was writing
|
||||
@@ -256,6 +280,14 @@ void fxReclaimLabelUndoSpan(const char* label, int mask) {
|
||||
|
||||
void fxReclaimEndUndoSpan() { closeSpan(); }
|
||||
|
||||
void fxReclaimAbandonForStateLoad() {
|
||||
// Close FIRST and unconditionally. The abandon below only stops future writes; the
|
||||
// block is what a Ctrl-Z would otherwise close over the wrong state, and it is open
|
||||
// whether or not a drain is on the stack at all.
|
||||
closeSpan();
|
||||
g_queue.abandon();
|
||||
}
|
||||
|
||||
void fxReclaimFlushRelease(ReaSamplerSession& session) {
|
||||
// GetCurrentProjectInLoadSave() names the project THIS hook is being asked to save
|
||||
// (reaper_plugin_functions.h:1582-1587) -- not merely the active tab, which a
|
||||
|
||||
+107
-17
@@ -122,6 +122,56 @@ inline int nextEligible(const std::vector<ReclaimItem>& items, const ParkClock&
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The item a step may move RIGHT NOW, under BOTH gates the loop applies: the debounce
|
||||
// above, and the transport, which mirrors applyMode's refusal of a real mode switch
|
||||
// (view.h, transportBlocksModeSwitch) narrowed to the OFFLINE direction only —
|
||||
// onlining is the switch's own unfinished promise and the user is waiting on it, while
|
||||
// offlining is opportunistic reclaim that can always wait for the transport to stop.
|
||||
// -1 when nothing may move; `transportDeferred` is raised only when the transport, and
|
||||
// not the debounce, is what held an otherwise-eligible item back.
|
||||
//
|
||||
// The ONE home of that pair: drainTick steps through it and nextActivity reports
|
||||
// through it, so the direction an indicator paints can never disagree with the
|
||||
// direction the next step actually takes.
|
||||
inline int nextMovable(const std::vector<ReclaimItem>& items, const ParkClock& clock,
|
||||
double nowMs, bool transportRolling, bool& transportDeferred) {
|
||||
const int i = nextEligible(items, clock, nowMs);
|
||||
if (i < 0) return -1;
|
||||
if (transportRolling &&
|
||||
items[static_cast<std::size_t>(i)].work.kind == view::FxWorkKind::Offline) {
|
||||
transportDeferred = true;
|
||||
return -1;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
// Which way the drain is MOVING — the panel's indicator, and undirected `fxReclaimPending`
|
||||
// is what it may not be driven off: for the whole ~3s after a switch the queue holds the
|
||||
// park's offline work while the only thing eligible is the RETURNING chains' online work,
|
||||
// so "pending" there names the opposite of what is happening.
|
||||
//
|
||||
// Idle means nothing may move on this tick, and deliberately does not distinguish why —
|
||||
// an empty queue, an offline item inside its hold, and an offline item held by a rolling
|
||||
// transport all read Idle, because none of them is work the user is currently waiting
|
||||
// through. Online while the transport rolls is a real state and reads Online: that
|
||||
// direction is not gated.
|
||||
enum class FxReclaimActivity {
|
||||
Idle,
|
||||
Online,
|
||||
Offline,
|
||||
};
|
||||
|
||||
inline FxReclaimActivity nextActivity(const std::vector<ReclaimItem>& items,
|
||||
const ParkClock& clock, double nowMs,
|
||||
bool transportRolling) {
|
||||
bool deferred = false;
|
||||
const int i = nextMovable(items, clock, nowMs, transportRolling, deferred);
|
||||
if (i < 0) return FxReclaimActivity::Idle;
|
||||
return items[static_cast<std::size_t>(i)].work.kind == view::FxWorkKind::Online
|
||||
? FxReclaimActivity::Online
|
||||
: FxReclaimActivity::Offline;
|
||||
}
|
||||
|
||||
// --- the drain ----------------------------------------------------------------
|
||||
|
||||
// One ONLINE item's captured state the chain had no home for. An OFFLINE item can
|
||||
@@ -211,21 +261,13 @@ ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, H
|
||||
// its track genuinely stops owing offline work, matured or not.
|
||||
clock.observe(items, host.nowMs());
|
||||
|
||||
// Mirrors applyMode's refusal (view.h, transportBlocksModeSwitch), narrowed to the
|
||||
// OFFLINE direction only: onlining is the mode switch's own unfinished promise — the
|
||||
// user is waiting on it — while offlining is opportunistic reclaim that can always
|
||||
// wait for the transport to stop. Onlining while rolling still costs a plugin load
|
||||
// mid-playback; that is an accepted trade for this gate's shape, not a hazard it
|
||||
// introduces.
|
||||
const bool offlineAllowed = !host.transportRolling();
|
||||
// Read once for the whole tick and applied through nextMovable, which owns the gate
|
||||
// (and the reasoning for narrowing it to Offline). Onlining while rolling still costs
|
||||
// a plugin load mid-playback; that is an accepted trade for this gate's shape, not a
|
||||
// hazard it introduces.
|
||||
const bool rolling = host.transportRolling();
|
||||
auto nextAllowed = [&](double nowMs) {
|
||||
const int i = nextEligible(items, clock, nowMs);
|
||||
if (i >= 0 && !offlineAllowed &&
|
||||
items[static_cast<std::size_t>(i)].work.kind == view::FxWorkKind::Offline) {
|
||||
result.transportDeferred = true;
|
||||
return -1;
|
||||
}
|
||||
return i;
|
||||
return nextMovable(items, clock, nowMs, rolling, result.transportDeferred);
|
||||
};
|
||||
|
||||
const int first = nextAllowed(host.nowMs());
|
||||
@@ -318,6 +360,21 @@ public:
|
||||
pending_ = true;
|
||||
}
|
||||
|
||||
// A project-state load the USER drove (undo/redo). The whole project is about to be
|
||||
// replaced, so this is STRONGER than an invalidation: the queue's work is discarded as
|
||||
// usual, AND any drain on the stack — reachable only through a write's own message
|
||||
// pump — stops where it stands instead of finishing the track it is on. Accepted, and
|
||||
// the reason a step's atomicity yields here: a park half-landed this way stays
|
||||
// half-landed, covered by the one point the terminator closed.
|
||||
void abandon() {
|
||||
invalidate();
|
||||
abandoned_ = true;
|
||||
}
|
||||
|
||||
// Read by the shell's host on every step; false again from the next run(), because the
|
||||
// stop belongs to the tick that was interrupted, not to the project that replaced it.
|
||||
bool abandoned() const { return abandoned_; }
|
||||
|
||||
// The ONE test the timer makes per tick. False means settled: nothing to converge
|
||||
// until the next invalidation.
|
||||
bool pending() const { return pending_; }
|
||||
@@ -374,16 +431,29 @@ public:
|
||||
|
||||
const std::vector<ReclaimItem>& items() const { return items_; }
|
||||
|
||||
// Which way the drain is moving, for an indicator to paint — see nextActivity, which
|
||||
// owns the rule. Const and cheap: the panel polls it per frame.
|
||||
FxReclaimActivity activity(double nowMs, bool transportRolling) const {
|
||||
// The queue's own storage is EMPTY for a drain's whole duration (run() moves it
|
||||
// into locals), and a write pumps the message loop — so a repaint landing inside
|
||||
// one would otherwise read the drain as idle at exactly the moment it is working.
|
||||
if (draining_ != FxReclaimActivity::Idle) return draining_;
|
||||
return nextActivity(items_, clock_, nowMs, transportRolling);
|
||||
}
|
||||
|
||||
template <class Host>
|
||||
ReclaimRunResult run(Host& host) {
|
||||
const unsigned long long entered = generation_;
|
||||
abandoned_ = false;
|
||||
|
||||
std::vector<ReclaimItem> work;
|
||||
ParkClock clock;
|
||||
work.swap(items_);
|
||||
clock.swap(clock_);
|
||||
|
||||
draining_ = nextActivity(work, clock, host.nowMs(), host.transportRolling());
|
||||
const ReclaimTickResult tick = drainTick(work, clock, host);
|
||||
draining_ = FxReclaimActivity::Idle;
|
||||
|
||||
// The hold is keyed on the track and outlives any re-derivation, so it comes
|
||||
// back whether or not the drain's own work survives.
|
||||
@@ -420,6 +490,8 @@ private:
|
||||
unsigned long long derivedAt_ = 0;
|
||||
bool derived_ = false;
|
||||
bool pending_ = false;
|
||||
bool abandoned_ = false;
|
||||
FxReclaimActivity draining_ = FxReclaimActivity::Idle;
|
||||
};
|
||||
|
||||
// --- the release's durable half --------------------------------------------------
|
||||
@@ -544,6 +616,13 @@ inline bool spanEndsAfter(const ReclaimRunResult& run) {
|
||||
// pointer test. False means the queue is settled and there is nothing to converge.
|
||||
bool fxReclaimPending();
|
||||
|
||||
// Which direction the drain is moving, answered against the project the queue was derived
|
||||
// against rather than the active tab. `fxReclaimPending` above is deliberately left
|
||||
// undirected and unchanged; this is the surface an indicator must be driven off instead,
|
||||
// because "pending" is true throughout the debounce, when the only thing moving is the
|
||||
// OPPOSITE direction. Idle whenever nothing may move — see FxReclaimActivity.
|
||||
FxReclaimActivity fxReclaimActivity();
|
||||
|
||||
// Raises the wakeup and marks the derived queue for re-derivation by the NEXT tick;
|
||||
// discards nothing here, because this is reachable from inside a drain. Called from
|
||||
// applyMode and nothing else
|
||||
@@ -578,11 +657,22 @@ void fxReclaimLabelUndoSpan(const char* label, int mask);
|
||||
|
||||
// The ONE close, idempotent: every terminating path routes through it — the drain
|
||||
// settling, aborting, or deferring on the transport; the project retargeting or closing;
|
||||
// the next apply superseding it; a project save; extension unload. A leaked open block
|
||||
// would swallow everything the user does afterwards, so nothing may return past a
|
||||
// terminating condition without calling this.
|
||||
// the next apply superseding it; a user-driven state load; a project save; extension
|
||||
// unload. A leaked open block would swallow everything the user does afterwards, so
|
||||
// nothing may return past a terminating condition without calling this.
|
||||
void fxReclaimEndUndoSpan();
|
||||
|
||||
// A project-state load the USER drove — Ctrl-Z or Ctrl-Shift-Z. Ends the span and stops
|
||||
// any drain on the stack (FxReclaimQueue::abandon).
|
||||
//
|
||||
// Called from the projectconfig BeginLoadProjectState hook, which fires BEFORE REAPER
|
||||
// restores anything: that is the ONLY point at which the block can still close over the
|
||||
// state it was opened on. The span is open for at least kParkHoldMs after every real
|
||||
// switch and a switch is exactly what a user undoes, so closing it as a side effect of
|
||||
// the reapply the load triggers — several calls later, over the restored project — would
|
||||
// mint the point over post-undo state and destroy the redo branch.
|
||||
void fxReclaimAbandonForStateLoad();
|
||||
|
||||
// Writes an in-memory release into the SAVING project's view state (identified via
|
||||
// GetCurrentProjectInLoadSave(), not the active tab). Called from the project-save
|
||||
// hook and nowhere else: this is the ONE path that makes a spent record durable, and
|
||||
|
||||
@@ -59,6 +59,14 @@ struct FakeHost {
|
||||
int invalidateAfterWrite = -1;
|
||||
int itemsSeenMidDrain = -1; // queue size as the re-entrant caller finds it
|
||||
|
||||
// A Ctrl-Z arriving through the same pump: what fxReclaimAbandonForStateLoad does,
|
||||
// fired on the Nth successful write. The direction probe rides the same hook because
|
||||
// the property it checks is the same one — what a caller re-entering mid-drain sees.
|
||||
int abandonAfterWrite = -1;
|
||||
int probeActivityAfterWrite = -1;
|
||||
int spanClosesMidDrain = 0;
|
||||
FxReclaimActivity activityMidDrain = FxReclaimActivity::Idle;
|
||||
|
||||
bool rolling = false; // the transport gate's input
|
||||
|
||||
// Stands in for the shell's one span instance: fx_reclaim.cpp opens it on the first
|
||||
@@ -70,7 +78,10 @@ struct FakeHost {
|
||||
double nowMs() const { return clock; }
|
||||
bool transportRolling() const { return rolling; }
|
||||
|
||||
// Both host entry points refuse once abandoned, exactly as fx_reclaim.cpp's do: this
|
||||
// one ends every REMAINING track, writeOffline below ends the one being written.
|
||||
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||
if (reentrant && reentrant->abandoned()) return false;
|
||||
auto it = chains.find(guid);
|
||||
if (it == chains.end()) return false;
|
||||
out.clear();
|
||||
@@ -79,6 +90,7 @@ struct FakeHost {
|
||||
}
|
||||
|
||||
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
|
||||
if (reentrant && reentrant->abandoned()) return false;
|
||||
auto it = chains.find(guid);
|
||||
if (it == chains.end()) return false;
|
||||
if (fxIndex < 0 || fxIndex >= static_cast<int>(it->second.size())) return false;
|
||||
@@ -97,6 +109,12 @@ struct FakeHost {
|
||||
reentrant->discard();
|
||||
reentrant->invalidate();
|
||||
}
|
||||
if (reentrant && static_cast<int>(writeLog.size()) == probeActivityAfterWrite)
|
||||
activityMidDrain = reentrant->activity(clock, rolling);
|
||||
if (reentrant && static_cast<int>(writeLog.size()) == abandonAfterWrite) {
|
||||
if (span && span->end()) ++spanClosesMidDrain; // closeSpan, then the stop
|
||||
reentrant->abandon();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1197,6 +1215,160 @@ static void testADrainNoSwitchOpenedMintsOnePointOfItsOwn() {
|
||||
CHECK(!span.open());
|
||||
}
|
||||
|
||||
// -- 11. the state-load terminator ------------------------------------------------------
|
||||
|
||||
static void testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack() {
|
||||
// The contrast with testAReentrantInvalidationReachesNothingTheDrainIsUsing above,
|
||||
// which finishes its track's remaining three writes: an ordinary invalidation only
|
||||
// says the model moved, while a state load says the PROJECT is being replaced under
|
||||
// us and the block covering these writes has already closed. So a step is atomic only
|
||||
// up to the abandon — the half-parked chain that leaves is the accepted cost.
|
||||
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}},
|
||||
{"{T2}", {"{FX-C}"}}});
|
||||
FakeHost host;
|
||||
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"});
|
||||
host.chains["{T2}"] = onlineChain({"{FX-C}"});
|
||||
host.stepCostMs = 1.0; // cheap enough that only the abandon can stop the tick
|
||||
|
||||
UndoSpan span; // what a real switch left open
|
||||
CHECK(span.begin(kProjA, "ReaSampler: activate Design view", 9));
|
||||
host.span = &span;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}"});
|
||||
q.run(host); // observes both parked at t=0
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
host.reentrant = &q;
|
||||
host.abandonAfterWrite = 1;
|
||||
const ReclaimRunResult r = q.run(host);
|
||||
|
||||
CHECK(host.writeLog == std::vector<std::string>{"{T1}:0=off"});
|
||||
CHECK(host.offlineAt("{T1}", 0));
|
||||
CHECK(!host.offlineAt("{T1}", 1)); // the same track, mid-chain — the partial park
|
||||
CHECK(!host.offlineAt("{T2}", 0)); // and every track after it, untouched
|
||||
|
||||
CHECK(host.spanClosesMidDrain == 1);
|
||||
CHECK(!span.open());
|
||||
CHECK(r.aborted);
|
||||
CHECK(!r.settled);
|
||||
CHECK(spanEndsAfter(r)); // the tick's own close still fires...
|
||||
CHECK(!span.end()); // ...and finds nothing: the terminator got there first
|
||||
|
||||
// The wakeup stands and the work is owed again: going idle here would strand the
|
||||
// half-parked chain for the rest of the session.
|
||||
CHECK(q.stale());
|
||||
CHECK(q.pending());
|
||||
|
||||
// ...and the stop belonged to that tick alone. The reloaded project's own drain writes.
|
||||
host.abandonAfterWrite = -1;
|
||||
q.adopt(derive(vm, {"{T1}", "{T2}"}));
|
||||
CHECK(q.run(host).writes == 2); // {T1}'s remaining slot, and {T2}'s whole chain
|
||||
CHECK(host.offlineAt("{T1}", 1));
|
||||
CHECK(host.offlineAt("{T2}", 0));
|
||||
}
|
||||
|
||||
// -- 12. the direction the drain is moving ----------------------------------------------
|
||||
|
||||
static void testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold() {
|
||||
// The panel bug this surface exists for: `pending` is true from the instant of the
|
||||
// switch, but for the whole kParkHoldMs the only thing eligible is the RETURNING
|
||||
// chains' online work — an indicator driven off the queue's mere contents would name
|
||||
// parking while the drain is doing the opposite.
|
||||
ViewModeModel vm;
|
||||
vm.membership().tag("{Z-back}", kArrangeModeId); // active; owed an online
|
||||
vm.membership().tag("{A-park}", kDesignModeId); // parked; owed an offline
|
||||
vm.storeSnapshot("{Z-back}", recordWithFx({{"{FX-1}", 0}}));
|
||||
vm.storeSnapshot("{A-park}", recordWithFx({{"{FX-2}", 0}}));
|
||||
|
||||
FakeHost host;
|
||||
host.chains["{Z-back}"] = offlineChain({"{FX-1}"});
|
||||
host.chains["{A-park}"] = onlineChain({"{FX-2}"});
|
||||
host.stepCostMs = 1.0;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{Z-back}", "{A-park}"});
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Online);
|
||||
|
||||
CHECK(q.run(host).writes == 1); // the online item only — the offline is still held
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
|
||||
host.clock += kParkHoldMs;
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline);
|
||||
CHECK(q.run(host).settled);
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
}
|
||||
|
||||
static void testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures() {
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost host;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
// Idle, not Offline: nothing may move, and the wait is the reason the span is allowed
|
||||
// to stay open across it. Reporting Offline here is what would light the indicator for
|
||||
// 3s before a single plugin moves.
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
CHECK(!q.run(host).settled); // the hold's origin is set by this tick's observe
|
||||
CHECK(host.writeLog.empty());
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
|
||||
host.clock += kParkHoldMs;
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline);
|
||||
CHECK(q.run(host).writes == 1);
|
||||
}
|
||||
|
||||
static void testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline() {
|
||||
ViewModeModel vm;
|
||||
vm.membership().tag("{Z-back}", kArrangeModeId);
|
||||
vm.membership().tag("{A-park}", kDesignModeId);
|
||||
vm.storeSnapshot("{Z-back}", recordWithFx({{"{FX-1}", 0}}));
|
||||
vm.storeSnapshot("{A-park}", recordWithFx({{"{FX-2}", 0}}));
|
||||
|
||||
FakeHost host;
|
||||
host.chains["{Z-back}"] = offlineChain({"{FX-1}"});
|
||||
host.chains["{A-park}"] = onlineChain({"{FX-2}"});
|
||||
host.stepCostMs = 1.0;
|
||||
host.rolling = true;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{Z-back}", "{A-park}"});
|
||||
// Online is not transport-gated, so it both drains and reads through a rolling
|
||||
// transport — the second half of the panel bug: this work had no indicator at all.
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Online);
|
||||
CHECK(q.run(host).writes == 1);
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
// The offline half is eligible now and still may not move, so it reads Idle rather
|
||||
// than naming a direction nothing is travelling in.
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle);
|
||||
CHECK(q.run(host).transportDeferred);
|
||||
CHECK(host.writeLog.size() == 1);
|
||||
|
||||
host.rolling = false;
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline);
|
||||
CHECK(q.run(host).writes == 1);
|
||||
}
|
||||
|
||||
static void testTheDirectionHoldsThroughTheDrainsOwnMessagePump() {
|
||||
// A write pumps, and a repaint landing in there polls this. The queue object holds
|
||||
// NOTHING for the drain's whole duration (itemsSeenMidDrain == 0, asserted above), so
|
||||
// without the latch the indicator would read Idle at exactly the moment the plugin
|
||||
// loads the user is waiting through are happening.
|
||||
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}}});
|
||||
FakeHost host;
|
||||
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"});
|
||||
host.stepCostMs = 1.0;
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T1}"});
|
||||
q.run(host);
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
host.reentrant = &q;
|
||||
host.probeActivityAfterWrite = 1;
|
||||
CHECK(q.run(host).settled);
|
||||
|
||||
CHECK(host.activityMidDrain == FxReclaimActivity::Offline);
|
||||
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); // latch released
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
||||
|
||||
@@ -1240,6 +1412,13 @@ int main() {
|
||||
testOneSwitchAndItsMultiTickParkingAreOneSpan();
|
||||
testADrainNoSwitchOpenedMintsOnePointOfItsOwn();
|
||||
|
||||
testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack();
|
||||
|
||||
testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold();
|
||||
testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures();
|
||||
testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline();
|
||||
testTheDirectionHoldsThroughTheDrainsOwnMessagePump();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user