Fix the reclaimer stranding a tab-switched-into project: lift the pin and wakeup into FxReclaimQueue, retarget instead of going idle
Also validate the cached track in its project, and stop a drop-free tick resetting the report memo.
This commit is contained in:
+145
-1
@@ -4,10 +4,12 @@
|
||||
// yield, the queue being a cache of intent that a model change discards, the user's own
|
||||
// offline plugins surviving an online, the release reaching the persisted model, and —
|
||||
// via a host that invalidates from inside a write — that a re-entered invalidation
|
||||
// reaches nothing the drain is using.
|
||||
// reaches nothing the drain is using. The queue owns the project pin and the timer's
|
||||
// wakeup for the same reason: a compare-only pointer makes the tab switch assertable.
|
||||
|
||||
#include "../src/shell/view/fx_reclaim.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
@@ -176,6 +178,11 @@ FxReclaimQueue queueOf(const ViewModeModel& vm, const std::set<std::string>& liv
|
||||
return q;
|
||||
}
|
||||
|
||||
// Two project handles. The queue treats a project pointer as COMPARE-ONLY and never
|
||||
// dereferences it, so a fabricated address is a faithful stand-in for REAPER's.
|
||||
ReaProject* const kProjA = reinterpret_cast<ReaProject*>(static_cast<std::uintptr_t>(0x1));
|
||||
ReaProject* const kProjB = reinterpret_cast<ReaProject*>(static_cast<std::uintptr_t>(0x2));
|
||||
|
||||
} // namespace
|
||||
|
||||
// -- 1. debounce ---------------------------------------------------------------
|
||||
@@ -633,6 +640,138 @@ static void testAnOfflineTickReportsNoDrops() {
|
||||
CHECK(r.drops.empty());
|
||||
}
|
||||
|
||||
// -- 8. the pin and the wakeup ------------------------------------------------------
|
||||
|
||||
static void testATabSwitchRetargetsTheQueueAndLeavesTheWakeupStanding() {
|
||||
// Project A, one parked leaf owed offline work, derived against A and pinned to it.
|
||||
ViewModeModel a = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost hostA;
|
||||
hostA.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q;
|
||||
q.invalidate(); // what applyMode does on its way out of the switch that parked {T}
|
||||
CHECK(q.pending());
|
||||
CHECK(q.beginTick(kProjA));
|
||||
CHECK(q.project() == kProjA);
|
||||
q.adopt(derive(a, {"{T}"}));
|
||||
CHECK(!q.run(hostA).settled); // observes the hold at t=0; still owed
|
||||
CHECK(!q.stale());
|
||||
|
||||
// The tab switch. poll()'s Load reapply invalidates on the way in, and NOTHING there
|
||||
// moves the pin, so the tick arrives holding A's work with B active.
|
||||
q.invalidate();
|
||||
CHECK(q.beginTick(kProjB)); // the tick proceeds — it does not bail on the mismatch
|
||||
CHECK(q.pending()); // and the timer will still call it: the strand's one bit
|
||||
CHECK(q.project() == kProjB);
|
||||
CHECK(q.stale()); // A's work is gone, B's is owed
|
||||
CHECK(q.items().empty());
|
||||
|
||||
// B's own parked leaf converges on this very tick and every one after it.
|
||||
ViewModeModel b = parkedDesignLeaves({{"{T2}", {"{FX-B}"}}});
|
||||
FakeHost hostB;
|
||||
hostB.chains["{T2}"] = onlineChain({"{FX-B}"});
|
||||
q.adopt(derive(b, {"{T2}"}));
|
||||
CHECK(!q.run(hostB).settled); // B's own hold starts here
|
||||
hostB.clock += kParkHoldMs;
|
||||
const ReclaimRunResult done = q.run(hostB);
|
||||
CHECK(done.settled);
|
||||
CHECK(hostB.writeLog == std::vector<std::string>{"{T2}:0=off"});
|
||||
CHECK(hostB.offlineAt("{T2}", 0));
|
||||
|
||||
// ...and A's chain was never touched from B: the queue was dropped, not run.
|
||||
CHECK(hostA.writeLog.empty());
|
||||
|
||||
// Only now, settled, does the shell stop the ticks.
|
||||
q.goIdle();
|
||||
CHECK(!q.pending());
|
||||
CHECK(q.project() == nullptr);
|
||||
}
|
||||
|
||||
static void testGoingIdleKeepsTheParkHoldWhileARetargetDropsIt() {
|
||||
// A hold is evidence about a track in ONE project, so a tab switch drops it. An
|
||||
// ordinary settle does not: restarting every 3s hold on the next unrelated membership
|
||||
// edit would defer an unchanged track's reclaim indefinitely.
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost host;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q;
|
||||
CHECK(q.beginTick(kProjA));
|
||||
q.adopt(derive(vm, {"{T}"}));
|
||||
q.run(host); // hold starts at t=0
|
||||
host.clock = kParkHoldMs;
|
||||
CHECK(q.run(host).settled);
|
||||
CHECK(host.writeLog.size() == 1);
|
||||
|
||||
// Settled: the queue and the pin go, the hold stays. A membership edit elsewhere
|
||||
// re-derives the SAME offline item, which is matured on arrival — one tick, no
|
||||
// second 3s wait, and nothing to write because the chain is already there.
|
||||
q.goIdle();
|
||||
q.invalidate();
|
||||
CHECK(q.beginTick(kProjA));
|
||||
q.adopt(derive(vm, {"{T}"}));
|
||||
const ReclaimRunResult resumed = q.run(host);
|
||||
CHECK(resumed.settled);
|
||||
CHECK(resumed.writes == 0);
|
||||
CHECK(host.writeLog.size() == 1);
|
||||
|
||||
// The mirror: away to B and back to A, and the same item must wait a fresh hold.
|
||||
q.invalidate();
|
||||
CHECK(q.beginTick(kProjB));
|
||||
CHECK(q.beginTick(kProjA));
|
||||
q.adopt(derive(vm, {"{T}"}));
|
||||
const double back = host.clock;
|
||||
CHECK(!q.run(host).settled); // the hold restarts here, at `back`
|
||||
host.clock = back + kParkHoldMs - 1.0;
|
||||
CHECK(!q.run(host).settled);
|
||||
host.clock = back + kParkHoldMs;
|
||||
CHECK(q.run(host).settled);
|
||||
}
|
||||
|
||||
static void testOnlyANoProjectTickClearsTheWakeup() {
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost host;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q;
|
||||
q.invalidate();
|
||||
CHECK(q.beginTick(kProjA));
|
||||
q.adopt(derive(vm, {"{T}"}));
|
||||
q.run(host);
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
// An abort leaves the wakeup up: the work is re-owed, and the shell's settle test
|
||||
// never runs on this path.
|
||||
host.reentrant = &q;
|
||||
host.invalidateAfterWrite = 1;
|
||||
const ReclaimRunResult r = q.run(host);
|
||||
CHECK(r.aborted);
|
||||
CHECK(q.pending());
|
||||
|
||||
// No active project at all — the one arm with nothing to converge and nothing to
|
||||
// come back to. Everything goes, wakeup included.
|
||||
CHECK(!q.beginTick(nullptr));
|
||||
CHECK(!q.pending());
|
||||
CHECK(q.project() == nullptr);
|
||||
CHECK(q.stale());
|
||||
CHECK(q.items().empty());
|
||||
}
|
||||
|
||||
static void testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo() {
|
||||
const std::vector<TrackFxDrops> none;
|
||||
const std::vector<TrackFxDrops> lost{TrackFxDrops{"{T}", FxRestoreDrops{1, 0, 0}}};
|
||||
|
||||
CHECK(shouldReportDrops(/*sameOwnerAsLast=*/false, none, lost)); // first sighting
|
||||
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, lost)); // the re-derivation
|
||||
|
||||
// The regression: every ordinary tick carries no drops at all. Treating that as a
|
||||
// change would overwrite the memo and re-arm the identical report for the next
|
||||
// re-derivation — the shell holds the memo back on exactly this answer.
|
||||
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, none));
|
||||
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/false, lost, none));
|
||||
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, lost));
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
||||
|
||||
@@ -657,6 +796,11 @@ int main() {
|
||||
testADropIsReportedOnceAcrossRepeatedRederivations();
|
||||
testAnOfflineTickReportsNoDrops();
|
||||
|
||||
testATabSwitchRetargetsTheQueueAndLeavesTheWakeupStanding();
|
||||
testGoingIdleKeepsTheParkHoldWhileARetargetDropsIt();
|
||||
testOnlyANoProjectTickClearsTheWakeup();
|
||||
testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user