Keep the extension's tick out of REAPER's own project load, and re-earn the park holds across one load

This commit is contained in:
2026-08-07 18:03:58 -04:00
parent 3d95eb715c
commit 20fef2a3bf
4 changed files with 124 additions and 9 deletions
+63 -1
View File
@@ -1272,14 +1272,75 @@ static void testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack(
CHECK(q.stale());
CHECK(!q.pending());
// ...and the stop belonged to that tick alone. The reloaded project's own drain writes.
// ...and the stop belonged to that tick alone. The reloaded project's own drain writes —
// but not before its holds are earned again: the abandon dropped them (it reached the
// clock through the drain's own local, see FxReclaimQueue::run), so the re-park starts
// its 3s from the first post-load tick rather than inheriting the pre-load observation.
host.abandonAfterWrite = -1;
q.adopt(derive(vm, {"{T1}", "{T2}"}));
CHECK(q.run(host).writes == 0); // first post-load tick: the holds start here
CHECK(!host.offlineAt("{T1}", 1));
host.clock += kParkHoldMs;
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));
}
static void testTheWholePostLoadReconvergenceIsOneSpanHoweverFarTheParkingGot() {
// The acceptance property behind a Ctrl-Z landing anywhere in the drain: whatever
// fraction of the parking had completed, everything the reloaded project owes rides
// ONE span and costs ONE point. Two things carry it — the abandon dropping the holds,
// so nothing writes on the tick that re-derives (the load's own wake), and the lazy
// open being the drain's, so the many ticks that follow share one block.
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}},
{"{T2}", {"{FX-C}"}},
{"{T3}", {"{FX-D}"}}});
FakeHost host;
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"});
host.chains["{T2}"] = onlineChain({"{FX-C}"});
host.chains["{T3}"] = onlineChain({"{FX-D}"});
host.stepCostMs = 40.0; // over the offline budget: one track per tick, many ticks
UndoSpan span; // the switch's, still open when the undo arrives
CHECK(span.begin(kProjA, "ReaSampler: activate Design view", 9));
host.span = &span;
FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}", "{T3}"});
q.run(host);
host.clock += kParkHoldMs;
host.reentrant = &q;
host.abandonAfterWrite = 1; // Ctrl-Z one write in — the parking is part-done
q.run(host);
CHECK(!span.open()); // the terminator closed the switch's point over pre-load state
host.reentrant = nullptr;
host.abandonAfterWrite = -1;
// The reloaded model's reapply raises the wakeup and the queue re-derives.
q.invalidate();
q.adopt(derive(vm, {"{T1}", "{T2}", "{T3}"}));
int closes = 0;
auto tick = [&]() {
const ReclaimRunResult r = q.run(host);
if (spanEndsAfter(r) && span.end()) ++closes;
return r;
};
CHECK(!tick().settled); // the holds start here, so this tick writes nothing at all
CHECK(host.spanOpens == 0);
CHECK(!span.open());
CHECK(closes == 0);
host.clock += kParkHoldMs;
int ticks = 0;
while (!tick().settled && ++ticks < 20) {}
CHECK(ticks >= 2); // it really did take several ticks to converge...
CHECK(host.spanOpens == 1); // ...under ONE span, opened by the drain's first write
CHECK(closes == 1); // ...closed exactly once, on the tick that settled
CHECK(!span.open());
CHECK(host.offlineAt("{T1}", 1) && host.offlineAt("{T2}", 0) && host.offlineAt("{T3}", 0));
}
static void testARevertBetweenTicksLeavesTheLoopAsleepOnAStaleModel() {
// File > Revert is the load that re-reads NOTHING: it keeps the same ReaProject, so the
// session's identity poll answers NoOp, no reload follows and therefore no reapply
@@ -1576,6 +1637,7 @@ int main() {
testADrainNoSwitchOpenedMintsOnePointOfItsOwn();
testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack();
testTheWholePostLoadReconvergenceIsOneSpanHoweverFarTheParkingGot();
testARevertBetweenTicksLeavesTheLoopAsleepOnAStaleModel();
testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold();