Reclaim drain owns its work: invalidation only bumps a generation, and a generation that moved aborts the tick's commit and its release.
This commit is contained in:
+10
-6
@@ -189,7 +189,10 @@ static void OnTimer()
|
|||||||
// this timer mid-apply), same retry-next-tick shape as the consume guard below.
|
// this timer mid-apply), same retry-next-tick shape as the consume guard below.
|
||||||
// Deferred WHOLE, not just the reload branches: poll()'s identity tracking is
|
// Deferred WHOLE, not just the reload branches: poll()'s identity tracking is
|
||||||
// cheap to skip for one tick and resumes correctly, unlatched, on the next.
|
// cheap to skip for one tick and resumes correctly, unlatched, on the next.
|
||||||
if (!reasampler::modeApplyInProgress()) g_session.poll();
|
// The reclaim drain is latched for the same reason and is the same window: it
|
||||||
|
// holds that view model for the length of a plugin unload, which pumps.
|
||||||
|
if (!reasampler::modeApplyInProgress() && !reasampler::fxReclaimDrainInProgress())
|
||||||
|
g_session.poll();
|
||||||
|
|
||||||
// persist stays MODEL-ONLY (loads the saved view model but does not apply
|
// persist stays MODEL-ONLY (loads the saved view model but does not apply
|
||||||
// visibility, to avoid coupling persist to the view shell); poll() raises a
|
// visibility, to avoid coupling persist to the view shell); poll() raises a
|
||||||
@@ -198,11 +201,12 @@ static void OnTimer()
|
|||||||
// signal re-arms the bank panel's new-content detector — notified BEFORE the
|
// signal re-arms the bank panel's new-content detector — notified BEFORE the
|
||||||
// reapply so re-arm and model restore ride the one load event (otherwise
|
// reapply so re-arm and model restore ride the one load event (otherwise
|
||||||
// pre-existing tracks can be mis-detected as "new" and mass-tagged).
|
// pre-existing tracks can be mis-detected as "new" and mass-tagged).
|
||||||
// modeApplyInProgress is tested BEFORE the consume, not after the applyMode call:
|
// Both latches are tested BEFORE the consume, not after the applyMode call: the load
|
||||||
// the load signal is one-shot, a re-entered applyMode fails closed, and a reapply
|
// signal is one-shot, a re-entered applyMode fails closed, and a reapply dropped here
|
||||||
// dropped here is the saved mode never applied at all. Holding the whole block
|
// is the saved mode never applied at all. Holding the whole block back one tick also
|
||||||
// back one tick also keeps the panel re-arm ahead of the reapply.
|
// keeps the panel re-arm ahead of the reapply.
|
||||||
if (!reasampler::modeApplyInProgress() && g_session.consumeLoadSignal()) {
|
if (!reasampler::modeApplyInProgress() && !reasampler::fxReclaimDrainInProgress() &&
|
||||||
|
g_session.consumeLoadSignal()) {
|
||||||
reasampler::bankPanelNotifyProjectLoaded();
|
reasampler::bankPanelNotifyProjectLoaded();
|
||||||
// Reconcile lane ownership against the live project's lanes (P_LANENAME,
|
// Reconcile lane ownership against the live project's lanes (P_LANENAME,
|
||||||
// the cross-session source of truth) BEFORE reapplying visibility. Never
|
// the cross-session source of truth) BEFORE reapplying visibility. Never
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ std::vector<std::string> selectedTrackGuids() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reapplies the CURRENT active mode so a membership mutation takes visible effect
|
// Reapplies the CURRENT active mode so a membership mutation takes visible effect
|
||||||
// immediately (park/unpark/re-derive parents).
|
// immediately (park/unpark/re-derive parents). The return is deliberately discarded: a
|
||||||
|
// refused reapply leaves the membership edit standing and unapplied, and the user's next
|
||||||
|
// edit or mode switch reapplies it. What must NOT be dropped with it is the reclaimer's
|
||||||
|
// queue, which is why applyMode invalidates on its refusal path too (view.cpp).
|
||||||
void reapplyActiveMode() {
|
void reapplyActiveMode() {
|
||||||
applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
|
applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,14 +32,12 @@ namespace {
|
|||||||
// COMPARE-ONLY and never dereferenced beyond the ValidatePtr2 the tick already makes —
|
// COMPARE-ONLY and never dereferenced beyond the ValidatePtr2 the tick already makes —
|
||||||
// a tab switch has to be detectable without following a pointer that may name a closed
|
// a tab switch has to be detectable without following a pointer that may name a closed
|
||||||
// project.
|
// project.
|
||||||
std::vector<ReclaimItem> g_items;
|
FxReclaimQueue g_queue;
|
||||||
ReaProject* g_project = nullptr;
|
ReaProject* g_project = nullptr;
|
||||||
ParkClock g_clock;
|
|
||||||
|
|
||||||
// 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 walk the
|
// the timer and land back here mid-drain. Fail closed: a re-entered drain would derive a
|
||||||
// same queue the outer one holds a reference into, re-running the very item that outer
|
// second queue for the same project and race the outer one's writes track for track.
|
||||||
// step is still writing.
|
|
||||||
bool g_draining = false;
|
bool g_draining = false;
|
||||||
struct DrainLatch {
|
struct DrainLatch {
|
||||||
DrainLatch() { g_draining = true; }
|
DrainLatch() { g_draining = true; }
|
||||||
@@ -112,13 +110,15 @@ struct ReclaimHost {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reached only after the drain has unwound and the queue has proved its generation
|
||||||
|
// did not move, so `model` is still the object the queue was derived against.
|
||||||
void releaseFx(const std::string& guid) { model->releaseSnapshotFx(guid); }
|
void releaseFx(const std::string& guid) { model->releaseSnapshotFx(guid); }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Every path that stops scheduling ticks leaves the same state behind: no queue, no pin,
|
// Every path that stops scheduling ticks leaves the same state behind: no queue, no pin,
|
||||||
// nothing pending. The debounce clock is NOT part of it — see fx_reclaim.h.
|
// nothing pending. The debounce clock is NOT part of it — see fx_reclaim.h.
|
||||||
void goIdle() {
|
void goIdle() {
|
||||||
g_items.clear();
|
g_queue.discard();
|
||||||
g_project = nullptr;
|
g_project = nullptr;
|
||||||
g_fxReclaimPending = false;
|
g_fxReclaimPending = false;
|
||||||
}
|
}
|
||||||
@@ -128,10 +128,15 @@ void goIdle() {
|
|||||||
bool g_fxReclaimPending = false;
|
bool g_fxReclaimPending = false;
|
||||||
|
|
||||||
void invalidateFxReclaim() {
|
void invalidateFxReclaim() {
|
||||||
goIdle();
|
// 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
|
||||||
|
// through, and clear the very wakeup this call had just raised.
|
||||||
|
g_queue.invalidate();
|
||||||
g_fxReclaimPending = true;
|
g_fxReclaimPending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool fxReclaimDrainInProgress() { return g_draining; }
|
||||||
|
|
||||||
void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
|
void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
|
||||||
if (g_draining) return;
|
if (g_draining) return;
|
||||||
// The model is mid-rewrite and its records are being consumed out from under any
|
// The model is mid-rewrite and its records are being consumed out from under any
|
||||||
@@ -148,20 +153,21 @@ void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
|
|||||||
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||||
if (!ValidatePtr2(nullptr, project, "ReaProject*") || (g_project && g_project != project)) {
|
if (!ValidatePtr2(nullptr, project, "ReaProject*") || (g_project && g_project != project)) {
|
||||||
goIdle();
|
goIdle();
|
||||||
g_clock.forget();
|
g_queue.forgetHolds();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DERIVED, never persisted: recomputed from view state every time the queue was
|
// DERIVED, never persisted: recomputed from view state every time the queue was
|
||||||
// discarded, so a REAPER quit mid-drain simply re-derives and finishes on reopen.
|
// invalidated, so a REAPER quit mid-drain simply re-derives and finishes on reopen.
|
||||||
if (!g_project) {
|
if (g_queue.stale()) {
|
||||||
g_project = project;
|
g_project = project;
|
||||||
g_items = makeQueue(
|
g_queue.adopt(makeQueue(
|
||||||
view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project)));
|
view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project))));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReclaimHost host{project, &model};
|
ReclaimHost host{project, &model};
|
||||||
if (drainTick(g_items, g_clock, host).settled) goIdle();
|
const ReclaimRunResult run = g_queue.run(host);
|
||||||
|
if (!run.aborted && run.settled) goIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
+111
-13
@@ -92,6 +92,9 @@ public:
|
|||||||
|
|
||||||
void forget() { since_.clear(); }
|
void forget() { since_.clear(); }
|
||||||
|
|
||||||
|
// The owner swaps the clock out for the duration of a drain — see FxReclaimQueue.
|
||||||
|
void swap(ParkClock& other) { since_.swap(other.since_); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::map<std::string, double> since_;
|
std::map<std::string, double> since_;
|
||||||
};
|
};
|
||||||
@@ -119,6 +122,12 @@ inline int nextEligible(const std::vector<ReclaimItem>& items, const ParkClock&
|
|||||||
struct ReclaimTickResult {
|
struct ReclaimTickResult {
|
||||||
int writes = 0;
|
int writes = 0;
|
||||||
bool settled = false;
|
bool settled = false;
|
||||||
|
|
||||||
|
// Tracks whose park record the tick drove fully online, so the record's FX half is
|
||||||
|
// spent. REPORTED, not applied: releasing is a model write and the model is exactly
|
||||||
|
// what a re-entered load can replace mid-drain, so only the owner may commit it —
|
||||||
|
// see FxReclaimQueue::run.
|
||||||
|
std::vector<std::string> released;
|
||||||
};
|
};
|
||||||
|
|
||||||
// One tick of the convergence loop. Templated on `Host` so the ordering, budget and
|
// One tick of the convergence loop. Templated on `Host` so the ordering, budget and
|
||||||
@@ -131,7 +140,14 @@ struct ReclaimTickResult {
|
|||||||
// bool writeOffline(guid, fxIndex, offline) compare-before-write inside its
|
// bool writeOffline(guid, fxIndex, offline) compare-before-write inside its
|
||||||
// own discarded undo block;
|
// own discarded undo block;
|
||||||
// true iff it actually wrote
|
// true iff it actually wrote
|
||||||
// void releaseFx(guid) release the record's FX half
|
// void releaseFx(guid) release the record's FX half;
|
||||||
|
// called by FxReclaimQueue::run
|
||||||
|
// after the drain has unwound,
|
||||||
|
// never from inside the drain
|
||||||
|
//
|
||||||
|
// `items` and `clock` must be the caller's OWN storage, not shared state a re-entered
|
||||||
|
// call could reach: a write pumps the message loop. FxReclaimQueue::run is what makes
|
||||||
|
// that true for the live loop.
|
||||||
template <class Host>
|
template <class Host>
|
||||||
ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, Host& host) {
|
ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, Host& host) {
|
||||||
ReclaimTickResult result;
|
ReclaimTickResult result;
|
||||||
@@ -166,11 +182,12 @@ ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, H
|
|||||||
for (const FxOfflineWrite& w : resolved.writes)
|
for (const FxOfflineWrite& w : resolved.writes)
|
||||||
if (host.writeOffline(item.work.trackGuid, w.fxIndex, w.offline)) ++result.writes;
|
if (host.writeOffline(item.work.trackGuid, w.fxIndex, w.offline)) ++result.writes;
|
||||||
|
|
||||||
// The record's FX half is released HERE and only here: the chain has now
|
// The record is spent only once the chain has been driven to every state it
|
||||||
// been driven to every state the record captured, so the record is spent.
|
// captured — releasing any earlier destroys the only account of which plugins
|
||||||
// Releasing it any earlier destroys the only account of which plugins the
|
// the USER had offline before the park, and the next online blanket-enables
|
||||||
// USER had offline before the park, and the next online blanket-enables them.
|
// them. Reported rather than applied; see ReclaimTickResult::released.
|
||||||
if (item.work.kind == view::FxWorkKind::Online) host.releaseFx(item.work.trackGuid);
|
if (item.work.kind == view::FxWorkKind::Online)
|
||||||
|
result.released.push_back(item.work.trackGuid);
|
||||||
}
|
}
|
||||||
item.done = true; // both arms: a step is atomic, so no item is half-moved
|
item.done = true; // both arms: a step is atomic, so no item is half-moved
|
||||||
|
|
||||||
@@ -181,23 +198,104 @@ ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, H
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the queue's owner ----------------------------------------------------------
|
||||||
|
|
||||||
|
struct ReclaimRunResult {
|
||||||
|
int writes = 0;
|
||||||
|
bool settled = false;
|
||||||
|
|
||||||
|
// Something invalidated the queue while the drain was on the stack. The work and
|
||||||
|
// the releases are DISCARDED, `settled` is never true, and the caller must not go
|
||||||
|
// idle — the next tick re-derives against whatever changed.
|
||||||
|
bool aborted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Owns the derived queue and the debounce clock across a tick that can be re-entered:
|
||||||
|
// TrackFX_SetOffline is ASSUMED to pump the message loop, so an invalidation can arrive
|
||||||
|
// with a drain half-way through a track.
|
||||||
|
//
|
||||||
|
// The two halves of that safety are structural, not guarded. `invalidate()` bumps a
|
||||||
|
// counter and touches NOTHING else, so there is nothing for it to clear under a drain.
|
||||||
|
// `run()` moves the queue and the clock into locals for the drain's whole duration, so
|
||||||
|
// the object holds nothing the drain is reading — and re-checks the counter before it
|
||||||
|
// commits either the surviving queue or a single release.
|
||||||
|
class FxReclaimQueue {
|
||||||
|
public:
|
||||||
|
void invalidate() { ++generation_; }
|
||||||
|
|
||||||
|
// No queue at all, or one derived before the last invalidation.
|
||||||
|
bool stale() const { return !derived_ || derivedAt_ != generation_; }
|
||||||
|
|
||||||
|
void adopt(std::vector<ReclaimItem> items) {
|
||||||
|
items_ = std::move(items);
|
||||||
|
derived_ = true;
|
||||||
|
derivedAt_ = generation_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void discard() {
|
||||||
|
items_.clear();
|
||||||
|
derived_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void forgetHolds() { clock_.forget(); }
|
||||||
|
|
||||||
|
const std::vector<ReclaimItem>& items() const { return items_; }
|
||||||
|
|
||||||
|
template <class Host>
|
||||||
|
ReclaimRunResult run(Host& host) {
|
||||||
|
const unsigned long long entered = generation_;
|
||||||
|
|
||||||
|
std::vector<ReclaimItem> work;
|
||||||
|
ParkClock clock;
|
||||||
|
work.swap(items_);
|
||||||
|
clock.swap(clock_);
|
||||||
|
|
||||||
|
const ReclaimTickResult tick = drainTick(work, clock, host);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
clock_.swap(clock);
|
||||||
|
|
||||||
|
if (generation_ != entered) return ReclaimRunResult{tick.writes, false, true};
|
||||||
|
|
||||||
|
items_.swap(work);
|
||||||
|
// A release dropped by an abort is re-owed, not lost: the next derivation plans
|
||||||
|
// the same Online item, its writes all find the chain already there, and the
|
||||||
|
// release fires then.
|
||||||
|
for (const std::string& guid : tick.released) host.releaseFx(guid);
|
||||||
|
return ReclaimRunResult{tick.writes, tick.settled, false};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<ReclaimItem> items_;
|
||||||
|
ParkClock clock_;
|
||||||
|
unsigned long long generation_ = 0;
|
||||||
|
unsigned long long derivedAt_ = 0;
|
||||||
|
bool derived_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
// --- shell entry points --------------------------------------------------------
|
// --- shell entry points --------------------------------------------------------
|
||||||
|
|
||||||
// The ONE test the timer makes per tick, mirroring the realtime capture's single
|
// The ONE test the timer makes per tick, mirroring the realtime capture's single
|
||||||
// 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.
|
||||||
extern bool g_fxReclaimPending;
|
extern bool g_fxReclaimPending;
|
||||||
|
|
||||||
// Discards the derived queue. Called from every path that can change what the FX chains
|
// Marks the derived queue for re-derivation by the NEXT tick; discards nothing here,
|
||||||
// SHOULD be — which is applyMode's success path and nothing else, because every
|
// because this is reachable from inside a drain. Called from applyMode and nothing else
|
||||||
// membership, snapshot and active-mode mutation reapplies through it. The debounce clock
|
// — every membership, snapshot and active-mode mutation reapplies through it — on the
|
||||||
// deliberately survives: it is keyed on the track, not on the queue.
|
// refusal path as well as the success one (see view.cpp for why the refusal is the
|
||||||
|
// load-bearing half). The debounce clock deliberately survives: it is keyed on the
|
||||||
|
// track, not on the queue.
|
||||||
void invalidateFxReclaim();
|
void invalidateFxReclaim();
|
||||||
|
|
||||||
|
// True while a drain is on the stack. Exposed so the timer's other work can stand off
|
||||||
|
// the model the drain is holding, the same way it stands off an applyMode.
|
||||||
|
bool fxReclaimDrainInProgress();
|
||||||
|
|
||||||
// Drains one tick against `proj` (nullptr = current project), re-deriving the queue from
|
// Drains one tick against `proj` (nullptr = current project), re-deriving the queue from
|
||||||
// `model` first if it was invalidated. Returns without touching the project while an
|
// `model` first if it was invalidated. Returns without touching the project while an
|
||||||
// applyMode is on the stack, while already draining (TrackFX_SetOffline is assumed to
|
// applyMode is on the stack, while already draining, or when the active project is not
|
||||||
// pump the message loop), or when the active project is not the one the queue was
|
// the one the queue was derived against.
|
||||||
// derived against.
|
|
||||||
void fxReclaimTick(ViewModeModel& model, ReaProject* proj);
|
void fxReclaimTick(ViewModeModel& model, ReaProject* proj);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
+22
-6
@@ -62,7 +62,9 @@ bool transportBlocksModeSwitch(ReaProject* proj) {
|
|||||||
|
|
||||||
bool modeApplyInProgress() { return g_applying; }
|
bool modeApplyInProgress() { return g_applying; }
|
||||||
|
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
namespace {
|
||||||
|
|
||||||
|
bool applyModeImpl(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||||
if (g_applying) return false; // see ApplyLatch; same fail-closed shape as the guards below
|
if (g_applying) return false; // see ApplyLatch; same fail-closed shape as the guards below
|
||||||
ApplyLatch latch;
|
ApplyLatch latch;
|
||||||
|
|
||||||
@@ -203,12 +205,26 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
// an undo rolling the model back underneath it.
|
// an undo rolling the model back underneath it.
|
||||||
noteAppliedState(project, model);
|
noteAppliedState(project, model);
|
||||||
|
|
||||||
// Every membership, snapshot and active-mode mutation in the system reaches the
|
|
||||||
// project through this call, so this is the whole invalidation surface: what the FX
|
|
||||||
// chains SHOULD be has just changed, and the reclaimer's queue is a cache of that
|
|
||||||
// intent, never state.
|
|
||||||
invalidateFxReclaim();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||||
|
const bool applied = applyModeImpl(model, targetModeId, proj);
|
||||||
|
|
||||||
|
// Every membership, snapshot and active-mode mutation in the system reaches the
|
||||||
|
// project through this call, so this is the whole invalidation surface: the
|
||||||
|
// reclaimer's queue is a cache of what the FX chains SHOULD be, never state.
|
||||||
|
//
|
||||||
|
// Dropped on a REFUSAL too, and that is the load-bearing half. Callers mutate the
|
||||||
|
// model FIRST and reapply to make it visible (design_view_actions' tag/untag/
|
||||||
|
// show-both, render_in_place, the load tick), and every one of them discards this
|
||||||
|
// return — so a refusal is exactly the case where the mutation stands and nothing
|
||||||
|
// else would drop a queue that now converges toward the mode the user just left.
|
||||||
|
// The cost when the refusal followed no mutation is one re-derivation on the timer.
|
||||||
|
invalidateFxReclaim();
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ bool modeApplyInProgress();
|
|||||||
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
|
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
|
||||||
// active) is never gated and never touches solo. `proj` == nullptr means the current
|
// active) is never gated and never touches solo. `proj` == nullptr means the current
|
||||||
// project, resolved ONCE and used for every call this apply makes.
|
// project, resolved ONCE and used for every call this apply makes.
|
||||||
|
//
|
||||||
|
// Either way — applied or refused — this drops the FX reclaimer's derived queue, which
|
||||||
|
// is why callers may safely discard the return after mutating the model.
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
+151
-28
@@ -1,8 +1,9 @@
|
|||||||
// Standalone tests for the FX reclaimer's drain — no REAPER, no test framework. The
|
// Standalone tests for the FX reclaimer's drain — no REAPER, no test framework. The
|
||||||
// loop is templated on its host precisely so the four properties that make it safe to
|
// loop is templated on its host precisely so the properties that make it safe to run in
|
||||||
// run in the background are assertable here: the debounce, the per-tick budget's
|
// the background are assertable here: the debounce, the per-tick budget's between-track
|
||||||
// between-track yield, the queue being a cache of intent that a model change discards,
|
// yield, the queue being a cache of intent that a model change discards, the user's own
|
||||||
// and the user's own offline plugins surviving an online.
|
// offline plugins surviving an online, and — via a host that invalidates from inside a
|
||||||
|
// write — that a re-entered invalidation reaches nothing the drain is using.
|
||||||
|
|
||||||
#include "../src/shell/view/fx_reclaim.h"
|
#include "../src/shell/view/fx_reclaim.h"
|
||||||
|
|
||||||
@@ -39,6 +40,13 @@ struct FakeHost {
|
|||||||
double clock = 0.0;
|
double clock = 0.0;
|
||||||
double stepCostMs = 50.0;
|
double stepCostMs = 50.0;
|
||||||
|
|
||||||
|
// Models a re-entered timer landing inside TrackFX_SetOffline's message pump: on the
|
||||||
|
// Nth successful write, the write itself does to the queue what a nested applyMode
|
||||||
|
// used to do — clear it and mark it stale.
|
||||||
|
FxReclaimQueue* reentrant = nullptr;
|
||||||
|
int invalidateAfterWrite = -1;
|
||||||
|
int itemsSeenMidDrain = -1; // queue size as the re-entrant caller finds it
|
||||||
|
|
||||||
double nowMs() const { return clock; }
|
double nowMs() const { return clock; }
|
||||||
|
|
||||||
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||||
@@ -58,6 +66,12 @@ struct FakeHost {
|
|||||||
it->second[static_cast<std::size_t>(fxIndex)].offline = offline;
|
it->second[static_cast<std::size_t>(fxIndex)].offline = offline;
|
||||||
clock += stepCostMs;
|
clock += stepCostMs;
|
||||||
writeLog.push_back(guid + ":" + std::to_string(fxIndex) + (offline ? "=off" : "=on"));
|
writeLog.push_back(guid + ":" + std::to_string(fxIndex) + (offline ? "=off" : "=on"));
|
||||||
|
|
||||||
|
if (reentrant && static_cast<int>(writeLog.size()) == invalidateAfterWrite) {
|
||||||
|
itemsSeenMidDrain = static_cast<int>(reentrant->items().size());
|
||||||
|
reentrant->discard();
|
||||||
|
reentrant->invalidate();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,15 +135,29 @@ void matureHold(std::vector<ReclaimItem>& items, ParkClock& clock, FakeHost& hos
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Runs ticks until the queue settles, bounded so a non-terminating drain fails the test
|
// Runs ticks until the queue settles, bounded so a non-terminating drain fails the test
|
||||||
// rather than hanging CTest.
|
// rather than hanging CTest. The two overloads are the two entry points: the pure drain
|
||||||
|
// over the caller's own storage, and the owned queue the shell actually runs.
|
||||||
int drainToSettled(std::vector<ReclaimItem>& items, ParkClock& clock, FakeHost& host,
|
int drainToSettled(std::vector<ReclaimItem>& items, ParkClock& clock, FakeHost& host,
|
||||||
int maxTicks = 64) {
|
int maxTicks = 64) {
|
||||||
for (int t = 1; t <= maxTicks; ++t)
|
for (int t = 1; t <= maxTicks; ++t)
|
||||||
if (drainTick(items, clock, host).settled) return t;
|
if (drainTick(items, clock, host).settled) return t;
|
||||||
|
CHECK(false);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int drainToSettled(FxReclaimQueue& queue, FakeHost& host, int maxTicks = 64) {
|
||||||
|
for (int t = 1; t <= maxTicks; ++t)
|
||||||
|
if (queue.run(host).settled) return t;
|
||||||
CHECK(false); // did not settle — the drain is not making forward progress
|
CHECK(false); // did not settle — the drain is not making forward progress
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FxReclaimQueue queueOf(const ViewModeModel& vm, const std::set<std::string>& live) {
|
||||||
|
FxReclaimQueue q;
|
||||||
|
q.adopt(derive(vm, live));
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// -- 1. debounce ---------------------------------------------------------------
|
// -- 1. debounce ---------------------------------------------------------------
|
||||||
@@ -139,21 +167,20 @@ static void testFastFlipMovesZeroPluginsButAHeldParkStillReclaims() {
|
|||||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"});
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"});
|
||||||
FakeHost host;
|
FakeHost host;
|
||||||
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}"});
|
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}"});
|
||||||
ParkClock clock;
|
|
||||||
|
|
||||||
std::vector<ReclaimItem> items = derive(vm, {"{T}"});
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
CHECK(items.size() == 1 && items[0].work.kind == FxWorkKind::Offline);
|
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Offline);
|
||||||
CHECK(drainTick(items, clock, host).writes == 0);
|
CHECK(q.run(host).writes == 0);
|
||||||
CHECK(!drainTick(items, clock, host).settled); // waiting out its hold, not finished
|
CHECK(!q.run(host).settled); // waiting out its hold, not finished
|
||||||
|
|
||||||
// B→A half a second later: the flags come back and the FX half is still held, so the
|
// B→A half a second later: the flags come back and the FX half is still held, so the
|
||||||
// reclaimer is now owed an ONLINE — over a chain it never got round to offlining.
|
// reclaimer is now owed an ONLINE — over a chain it never got round to offlining.
|
||||||
host.clock = 500.0;
|
host.clock = 500.0;
|
||||||
vm.setActiveMode(kDesignModeId);
|
vm.setActiveMode(kDesignModeId);
|
||||||
vm.releaseSnapshotFlags("{T}");
|
vm.releaseSnapshotFlags("{T}");
|
||||||
items = derive(vm, {"{T}"});
|
q.adopt(derive(vm, {"{T}"}));
|
||||||
CHECK(items.size() == 1 && items[0].work.kind == FxWorkKind::Online);
|
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Online);
|
||||||
drainToSettled(items, clock, host);
|
drainToSettled(q, host);
|
||||||
|
|
||||||
// The whole point of the 3s hold: a fast A→B→A flip moved NO plugins.
|
// The whole point of the 3s hold: a fast A→B→A flip moved NO plugins.
|
||||||
CHECK(host.writeLog.empty());
|
CHECK(host.writeLog.empty());
|
||||||
@@ -165,11 +192,10 @@ static void testFastFlipMovesZeroPluginsButAHeldParkStillReclaims() {
|
|||||||
ViewModeModel held = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"});
|
ViewModeModel held = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"});
|
||||||
FakeHost host2;
|
FakeHost host2;
|
||||||
host2.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}"});
|
host2.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}"});
|
||||||
ParkClock clock2;
|
FxReclaimQueue q2 = queueOf(held, {"{T}"});
|
||||||
std::vector<ReclaimItem> items2 = derive(held, {"{T}"});
|
CHECK(q2.run(host2).writes == 0); // observed parked at t=0
|
||||||
CHECK(drainTick(items2, clock2, host2).writes == 0); // observed parked at t=0
|
|
||||||
host2.clock = kParkHoldMs;
|
host2.clock = kParkHoldMs;
|
||||||
drainToSettled(items2, clock2, host2);
|
drainToSettled(q2, host2);
|
||||||
CHECK(host2.writeLog.size() == 3);
|
CHECK(host2.writeLog.size() == 3);
|
||||||
CHECK(host2.offlineAt("{T}", 0) && host2.offlineAt("{T}", 1) && host2.offlineAt("{T}", 2));
|
CHECK(host2.offlineAt("{T}", 0) && host2.offlineAt("{T}", 1) && host2.offlineAt("{T}", 2));
|
||||||
CHECK(host2.released.empty()); // an OFFLINE never releases — the record is still owed
|
CHECK(host2.released.empty()); // an OFFLINE never releases — the record is still owed
|
||||||
@@ -329,10 +355,9 @@ static void testAPluginTheUserOfflinedIsNeverOnlinedByTheDrain() {
|
|||||||
|
|
||||||
FakeHost host;
|
FakeHost host;
|
||||||
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}", "{FX-C}"}); // fully parked
|
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}", "{FX-C}"}); // fully parked
|
||||||
ParkClock clock;
|
|
||||||
|
|
||||||
std::vector<ReclaimItem> items = derive(vm, {"{T}"});
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
drainToSettled(items, clock, host);
|
drainToSettled(q, host);
|
||||||
|
|
||||||
CHECK(!host.offlineAt("{T}", 0));
|
CHECK(!host.offlineAt("{T}", 0));
|
||||||
CHECK(host.offlineAt("{T}", 1)); // still offline — never blanket-onlined
|
CHECK(host.offlineAt("{T}", 1)); // still offline — never blanket-onlined
|
||||||
@@ -360,11 +385,10 @@ static void testSlotKeyedOnlineRecordRestoresByPosition() {
|
|||||||
// Dearer per plugin than the whole online budget: the step still runs the chain to
|
// Dearer per plugin than the whole online budget: the step still runs the chain to
|
||||||
// the end, because the budget cannot split one track's move.
|
// the end, because the budget cannot split one track's move.
|
||||||
host.stepCostMs = kOnlineBudgetMs + 10.0;
|
host.stepCostMs = kOnlineBudgetMs + 10.0;
|
||||||
ParkClock clock;
|
|
||||||
|
|
||||||
std::vector<ReclaimItem> items = derive(vm, {"{T}"});
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
CHECK(items.size() == 1);
|
CHECK(q.items().size() == 1);
|
||||||
CHECK(drainToSettled(items, clock, host) == 1);
|
CHECK(drainToSettled(q, host) == 1);
|
||||||
const std::vector<std::string> expected{"{T}:0=on", "{T}:2=on"};
|
const std::vector<std::string> expected{"{T}:0=on", "{T}:2=on"};
|
||||||
CHECK(host.writeLog == expected); // slot 1 stays offline, as the record captured
|
CHECK(host.writeLog == expected); // slot 1 stays offline, as the record captured
|
||||||
CHECK(host.offlineAt("{T}", 1));
|
CHECK(host.offlineAt("{T}", 1));
|
||||||
@@ -373,15 +397,110 @@ static void testSlotKeyedOnlineRecordRestoresByPosition() {
|
|||||||
static void testATrackThatLeftTheProjectCompletesRatherThanStalls() {
|
static void testATrackThatLeftTheProjectCompletesRatherThanStalls() {
|
||||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||||
FakeHost host; // no chain registered ⇒ liveChain reports the track gone
|
FakeHost host; // no chain registered ⇒ liveChain reports the track gone
|
||||||
ParkClock clock;
|
|
||||||
|
|
||||||
std::vector<ReclaimItem> items = derive(vm, {"{T}"});
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
matureHold(items, clock, host);
|
CHECK(!q.run(host).settled); // observes the hold at t=0; not matured yet
|
||||||
CHECK(drainTick(items, clock, host).settled);
|
host.clock += kParkHoldMs;
|
||||||
|
CHECK(q.run(host).settled);
|
||||||
CHECK(host.writeLog.empty());
|
CHECK(host.writeLog.empty());
|
||||||
CHECK(host.released.empty()); // the next apply's reconcile drops the whole record
|
CHECK(host.released.empty()); // the next apply's reconcile drops the whole record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- 5. re-entrancy: an invalidation arriving with a drain on the stack ------------
|
||||||
|
|
||||||
|
static void testAReentrantInvalidationReachesNothingTheDrainIsUsing() {
|
||||||
|
// The second of {T1}'s four writes re-enters and does what a nested applyMode used to
|
||||||
|
// do to the shared queue: clear it. The drain is mid-item at that moment.
|
||||||
|
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"}},
|
||||||
|
{"{T2}", {"{FX-E}"}}});
|
||||||
|
FakeHost host;
|
||||||
|
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"});
|
||||||
|
host.chains["{T2}"] = onlineChain({"{FX-E}"});
|
||||||
|
host.stepCostMs = 50.0;
|
||||||
|
|
||||||
|
FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}"});
|
||||||
|
q.run(host); // observes both parked at t=0
|
||||||
|
host.clock += kParkHoldMs;
|
||||||
|
|
||||||
|
host.reentrant = &q;
|
||||||
|
host.invalidateAfterWrite = 2;
|
||||||
|
const ReclaimRunResult r = q.run(host);
|
||||||
|
|
||||||
|
// The ownership claim: for the whole length of the drain the queue object holds
|
||||||
|
// nothing, so there is no element for a re-entrant caller to destroy.
|
||||||
|
CHECK(host.itemsSeenMidDrain == 0);
|
||||||
|
|
||||||
|
// The drain finished the track it was on out of its own copy — a step is atomic.
|
||||||
|
const std::vector<std::string> whole{"{T1}:0=off", "{T1}:1=off", "{T1}:2=off",
|
||||||
|
"{T1}:3=off"};
|
||||||
|
CHECK(host.writeLog == whole);
|
||||||
|
CHECK(r.writes == 4);
|
||||||
|
|
||||||
|
// ...and then refused to commit work derived from a model that has since moved.
|
||||||
|
CHECK(r.aborted);
|
||||||
|
CHECK(!r.settled);
|
||||||
|
CHECK(q.stale());
|
||||||
|
CHECK(q.items().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled() {
|
||||||
|
// One item, so the drain would otherwise report settled and the shell would go idle —
|
||||||
|
// clearing the pending flag the re-entrant invalidation had just raised and stranding
|
||||||
|
// its work until the next mode switch.
|
||||||
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||||
|
FakeHost host;
|
||||||
|
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||||
|
|
||||||
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
|
q.run(host);
|
||||||
|
host.clock += kParkHoldMs;
|
||||||
|
|
||||||
|
host.reentrant = &q;
|
||||||
|
host.invalidateAfterWrite = 1;
|
||||||
|
const ReclaimRunResult r = q.run(host);
|
||||||
|
CHECK(r.writes == 1);
|
||||||
|
CHECK(!r.settled); // the one bit that decides whether the shell clears its wakeup
|
||||||
|
CHECK(r.aborted);
|
||||||
|
CHECK(q.stale()); // and the next tick re-derives rather than finding an empty queue
|
||||||
|
|
||||||
|
// The contrast, so the assertion above is about the invalidation and not about this
|
||||||
|
// queue being unable to settle: the same drain settles once nothing re-enters it.
|
||||||
|
host.reentrant = nullptr;
|
||||||
|
q.adopt(derive(vm, {"{T}"}));
|
||||||
|
CHECK(q.run(host).settled);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAnInvalidationDuringAnOnlineDropsThatTicksRelease() {
|
||||||
|
// Releasing spends the record's FX half — the only account of which plugins the USER
|
||||||
|
// had offline before the park. An invalidation mid-drain is the signal that the model
|
||||||
|
// underneath may have been replaced (a project load replaces it in place), so the
|
||||||
|
// release must not land on it.
|
||||||
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
||||||
|
vm.setActiveMode(kDesignModeId);
|
||||||
|
vm.releaseSnapshotFlags("{T}");
|
||||||
|
|
||||||
|
FakeHost host;
|
||||||
|
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
||||||
|
|
||||||
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
|
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Online);
|
||||||
|
host.reentrant = &q;
|
||||||
|
host.invalidateAfterWrite = 1;
|
||||||
|
|
||||||
|
const ReclaimRunResult r = q.run(host);
|
||||||
|
CHECK(r.aborted);
|
||||||
|
CHECK(r.writes == 2); // the item still completed — a step is atomic
|
||||||
|
CHECK(host.released.empty()); // but its record was NOT spent
|
||||||
|
|
||||||
|
// Re-owed, not lost: the re-derived item finds the chain already where the record
|
||||||
|
// wants it, writes nothing more, and releases then.
|
||||||
|
host.reentrant = nullptr;
|
||||||
|
q.adopt(derive(vm, {"{T}"}));
|
||||||
|
CHECK(q.run(host).settled);
|
||||||
|
CHECK(host.writeLog.size() == 2);
|
||||||
|
CHECK(host.released.count("{T}") == 1);
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
||||||
|
|
||||||
@@ -396,6 +515,10 @@ int main() {
|
|||||||
testSlotKeyedOnlineRecordRestoresByPosition();
|
testSlotKeyedOnlineRecordRestoresByPosition();
|
||||||
testATrackThatLeftTheProjectCompletesRatherThanStalls();
|
testATrackThatLeftTheProjectCompletesRatherThanStalls();
|
||||||
|
|
||||||
|
testAReentrantInvalidationReachesNothingTheDrainIsUsing();
|
||||||
|
testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled();
|
||||||
|
testAnInvalidationDuringAnOnlineDropsThatTicksRelease();
|
||||||
|
|
||||||
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