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,8 +247,16 @@ static void OnTimer()
|
|||||||
static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/)
|
static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/)
|
||||||
{
|
{
|
||||||
if (isUndo)
|
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();
|
g_session.requestReload();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Intentional no-op: ReaSampler stores state via project EXT STATE, not this
|
// Intentional no-op: ReaSampler stores state via project EXT STATE, not this
|
||||||
// extension's own project lines.
|
// extension's own project lines.
|
||||||
|
|||||||
@@ -84,12 +84,25 @@ decide membership or mode rules.
|
|||||||
block swallows every edit the user makes afterwards, indefinitely, so every
|
block swallows every edit the user makes afterwards, indefinitely, so every
|
||||||
terminating path routes through ONE idempotent close (`fxReclaimEndUndoSpan`): the
|
terminating path routes through ONE idempotent close (`fxReclaimEndUndoSpan`): the
|
||||||
drain settling, aborting on a generation bump, or deferring on the transport; 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;
|
project retargeting or closing; the next apply superseding it; a USER-DRIVEN STATE
|
||||||
extension unload. The span may stay open only across a wait it can BOUND — the 3s
|
LOAD; a project save; extension unload. The span may stay open only across a wait it
|
||||||
debounce, which is the dead time between the switch and its first FX write, and
|
can BOUND — the 3s debounce, which is the dead time between the switch and its first
|
||||||
the only reason the span exists. A TRANSPORT deferral is not bounded (it lasts as
|
FX write, and the only reason the span exists. A TRANSPORT deferral is not bounded
|
||||||
long as the playback pass), so it ends the span and the parking that follows mints
|
(it lasts as long as the playback pass), so it ends the span and the parking that
|
||||||
its own point (`spanEndsAfter`).
|
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
|
- **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
|
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
|
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_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_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`.
|
- `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
|
## 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
|
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
|
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
|
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]`,
|
point — and, since the terminator above, whatever the drain had NOT yet written when
|
||||||
`docs/VERIFICATION.md` §"Mode switching".
|
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
|
- **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
|
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
|
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);
|
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
|
// 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
|
// 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.
|
// second queue for the same project and race the outer one's writes track for track.
|
||||||
@@ -122,21 +131,18 @@ struct ReclaimHost {
|
|||||||
return cachedTrack_;
|
return cachedTrack_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monotonic and sub-millisecond: the budget below discriminates at 30ms, which
|
double nowMs() const { return monotonicMs(); }
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The SAME predicate applyMode refuses a real mode switch on, deliberately shared
|
// 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
|
// 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.
|
// (view.h). drainTick narrows what it gates with this answer to offline work only.
|
||||||
bool transportRolling() const { return transportBlocksModeSwitch(project); }
|
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) {
|
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||||
|
if (g_queue.abandoned()) return false;
|
||||||
MediaTrack* tr = resolveForStep(guid);
|
MediaTrack* tr = resolveForStep(guid);
|
||||||
if (!tr) return false;
|
if (!tr) return false;
|
||||||
const int count = TrackFX_GetCount(tr);
|
const int count = TrackFX_GetCount(tr);
|
||||||
@@ -148,7 +154,11 @@ struct ReclaimHost {
|
|||||||
return true;
|
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) {
|
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
|
||||||
|
if (g_queue.abandoned()) return false;
|
||||||
MediaTrack* tr = resolveForStep(guid);
|
MediaTrack* tr = resolveForStep(guid);
|
||||||
if (!tr) return false;
|
if (!tr) return false;
|
||||||
if (fxIndex < 0 || fxIndex >= TrackFX_GetCount(tr)) return false; // chain shrank
|
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(); }
|
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() {
|
void invalidateFxReclaim() {
|
||||||
// A generation bump and a flag, and deliberately nothing more. Discarding the queue
|
// 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
|
// 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 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) {
|
void fxReclaimFlushRelease(ReaSamplerSession& session) {
|
||||||
// GetCurrentProjectInLoadSave() names the project THIS hook is being asked to save
|
// GetCurrentProjectInLoadSave() names the project THIS hook is being asked to save
|
||||||
// (reaper_plugin_functions.h:1582-1587) -- not merely the active tab, which a
|
// (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;
|
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 ----------------------------------------------------------------
|
// --- the drain ----------------------------------------------------------------
|
||||||
|
|
||||||
// One ONLINE item's captured state the chain had no home for. An OFFLINE item can
|
// 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.
|
// its track genuinely stops owing offline work, matured or not.
|
||||||
clock.observe(items, host.nowMs());
|
clock.observe(items, host.nowMs());
|
||||||
|
|
||||||
// Mirrors applyMode's refusal (view.h, transportBlocksModeSwitch), narrowed to the
|
// Read once for the whole tick and applied through nextMovable, which owns the gate
|
||||||
// OFFLINE direction only: onlining is the mode switch's own unfinished promise — the
|
// (and the reasoning for narrowing it to Offline). Onlining while rolling still costs
|
||||||
// user is waiting on it — while offlining is opportunistic reclaim that can always
|
// a plugin load mid-playback; that is an accepted trade for this gate's shape, not a
|
||||||
// wait for the transport to stop. Onlining while rolling still costs a plugin load
|
// hazard it introduces.
|
||||||
// mid-playback; that is an accepted trade for this gate's shape, not a hazard it
|
const bool rolling = host.transportRolling();
|
||||||
// introduces.
|
|
||||||
const bool offlineAllowed = !host.transportRolling();
|
|
||||||
auto nextAllowed = [&](double nowMs) {
|
auto nextAllowed = [&](double nowMs) {
|
||||||
const int i = nextEligible(items, clock, nowMs);
|
return nextMovable(items, clock, nowMs, rolling, result.transportDeferred);
|
||||||
if (i >= 0 && !offlineAllowed &&
|
|
||||||
items[static_cast<std::size_t>(i)].work.kind == view::FxWorkKind::Offline) {
|
|
||||||
result.transportDeferred = true;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const int first = nextAllowed(host.nowMs());
|
const int first = nextAllowed(host.nowMs());
|
||||||
@@ -318,6 +360,21 @@ public:
|
|||||||
pending_ = true;
|
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
|
// The ONE test the timer makes per tick. False means settled: nothing to converge
|
||||||
// until the next invalidation.
|
// until the next invalidation.
|
||||||
bool pending() const { return pending_; }
|
bool pending() const { return pending_; }
|
||||||
@@ -374,16 +431,29 @@ public:
|
|||||||
|
|
||||||
const std::vector<ReclaimItem>& items() const { return items_; }
|
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>
|
template <class Host>
|
||||||
ReclaimRunResult run(Host& host) {
|
ReclaimRunResult run(Host& host) {
|
||||||
const unsigned long long entered = generation_;
|
const unsigned long long entered = generation_;
|
||||||
|
abandoned_ = false;
|
||||||
|
|
||||||
std::vector<ReclaimItem> work;
|
std::vector<ReclaimItem> work;
|
||||||
ParkClock clock;
|
ParkClock clock;
|
||||||
work.swap(items_);
|
work.swap(items_);
|
||||||
clock.swap(clock_);
|
clock.swap(clock_);
|
||||||
|
|
||||||
|
draining_ = nextActivity(work, clock, host.nowMs(), host.transportRolling());
|
||||||
const ReclaimTickResult tick = drainTick(work, clock, host);
|
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
|
// 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.
|
// back whether or not the drain's own work survives.
|
||||||
@@ -420,6 +490,8 @@ private:
|
|||||||
unsigned long long derivedAt_ = 0;
|
unsigned long long derivedAt_ = 0;
|
||||||
bool derived_ = false;
|
bool derived_ = false;
|
||||||
bool pending_ = false;
|
bool pending_ = false;
|
||||||
|
bool abandoned_ = false;
|
||||||
|
FxReclaimActivity draining_ = FxReclaimActivity::Idle;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- the release's durable half --------------------------------------------------
|
// --- 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.
|
// pointer test. False means the queue is settled and there is nothing to converge.
|
||||||
bool fxReclaimPending();
|
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;
|
// 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
|
// discards nothing here, because this is reachable from inside a drain. Called from
|
||||||
// applyMode and nothing else
|
// 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
|
// The ONE close, idempotent: every terminating path routes through it — the drain
|
||||||
// settling, aborting, or deferring on the transport; the project retargeting or closing;
|
// 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
|
// the next apply superseding it; a user-driven state load; a project save; extension
|
||||||
// would swallow everything the user does afterwards, so nothing may return past a
|
// unload. A leaked open block would swallow everything the user does afterwards, so
|
||||||
// terminating condition without calling this.
|
// nothing may return past a terminating condition without calling this.
|
||||||
void fxReclaimEndUndoSpan();
|
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
|
// Writes an in-memory release into the SAVING project's view state (identified via
|
||||||
// GetCurrentProjectInLoadSave(), not the active tab). Called from the project-save
|
// 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
|
// 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 invalidateAfterWrite = -1;
|
||||||
int itemsSeenMidDrain = -1; // queue size as the re-entrant caller finds it
|
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
|
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
|
// 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; }
|
double nowMs() const { return clock; }
|
||||||
bool transportRolling() const { return rolling; }
|
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) {
|
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||||
|
if (reentrant && reentrant->abandoned()) return false;
|
||||||
auto it = chains.find(guid);
|
auto it = chains.find(guid);
|
||||||
if (it == chains.end()) return false;
|
if (it == chains.end()) return false;
|
||||||
out.clear();
|
out.clear();
|
||||||
@@ -79,6 +90,7 @@ struct FakeHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
|
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
|
||||||
|
if (reentrant && reentrant->abandoned()) return false;
|
||||||
auto it = chains.find(guid);
|
auto it = chains.find(guid);
|
||||||
if (it == chains.end()) return false;
|
if (it == chains.end()) return false;
|
||||||
if (fxIndex < 0 || fxIndex >= static_cast<int>(it->second.size())) return false;
|
if (fxIndex < 0 || fxIndex >= static_cast<int>(it->second.size())) return false;
|
||||||
@@ -97,6 +109,12 @@ struct FakeHost {
|
|||||||
reentrant->discard();
|
reentrant->discard();
|
||||||
reentrant->invalidate();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1197,6 +1215,160 @@ static void testADrainNoSwitchOpenedMintsOnePointOfItsOwn() {
|
|||||||
CHECK(!span.open());
|
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() {
|
int main() {
|
||||||
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
||||||
|
|
||||||
@@ -1240,6 +1412,13 @@ int main() {
|
|||||||
testOneSwitchAndItsMultiTickParkingAreOneSpan();
|
testOneSwitchAndItsMultiTickParkingAreOneSpan();
|
||||||
testADrainNoSwitchOpenedMintsOnePointOfItsOwn();
|
testADrainNoSwitchOpenedMintsOnePointOfItsOwn();
|
||||||
|
|
||||||
|
testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack();
|
||||||
|
|
||||||
|
testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold();
|
||||||
|
testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures();
|
||||||
|
testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline();
|
||||||
|
testTheDirectionHoldsThroughTheDrainsOwnMessagePump();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user