Persist the reclaimer's FX release so a reload cannot re-drive a spent record, and report the FX-restore drops it was swallowing

This commit is contained in:
2026-08-06 21:26:55 -04:00
parent 5dcfd3a53f
commit 7687ea76fc
4 changed files with 247 additions and 19 deletions
+64 -9
View File
@@ -16,6 +16,8 @@ class ReaProject;
namespace reasampler {
class ReaSamplerSession;
// --- tuning ------------------------------------------------------------------
// A track's OFFLINE work fires only after it has been continuously parked this long.
@@ -116,6 +118,25 @@ inline int nextEligible(const std::vector<ReclaimItem>& items, const ParkClock&
// --- the drain ----------------------------------------------------------------
// One ONLINE item's captured state the chain had no home for. An OFFLINE item can
// never produce one: its targets are the live chain's own slots, so every op resolves.
struct TrackFxDrops {
std::string trackGuid;
FxRestoreDrops drops;
bool operator==(const TrackFxDrops& o) const {
return trackGuid == o.trackGuid && drops == o.drops;
}
};
// True when a drop set is worth printing: any change of owner or of the set itself.
// The one-per-changed-set memo this gates is view_park's `shouldReport`/
// `reportExposedTracks`, which states the reason; the shell holds the memo.
inline bool shouldReportDrops(bool sameOwnerAsLast, const std::vector<TrackFxDrops>& last,
const std::vector<TrackFxDrops>& now) {
return !(sameOwnerAsLast && last == now);
}
// What one tick did. `settled` means every item is complete and the caller may go idle
// until the next invalidation. An item merely waiting out its debounce is NOT settled —
// going idle there would strand its reclaim for the rest of the session.
@@ -128,6 +149,12 @@ struct ReclaimTickResult {
// what a re-entered load can replace mid-drain, so only the owner may commit it —
// see FxReclaimQueue::run.
std::vector<std::string> released;
// Per track that lost any, what the tick could not place. Carried out for the shell
// to print through core/view/fx_offline's describeFxRestoreDrops — a plugin deleted
// while its track was parked is a degrade the user has to repair by hand, and
// swallowing it leaves no trace at all.
std::vector<TrackFxDrops> drops;
};
// One tick of the convergence loop. Templated on `Host` so the ordering, budget and
@@ -144,6 +171,9 @@ struct ReclaimTickResult {
// called by FxReclaimQueue::run
// after the drain has unwound,
// never from inside the drain
// void persistModel() make the releases just applied
// durable; run calls it ONCE per
// tick and only after a release
//
// `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
@@ -181,9 +211,14 @@ ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, H
const FxRestoreResolution resolved = view::resolveFxWork(item.work, liveFxGuids);
for (const FxOfflineWrite& w : resolved.writes)
if (host.writeOffline(item.work.trackGuid, w.fxIndex, w.offline)) ++result.writes;
if (resolved.drops.total() > 0)
result.drops.push_back(TrackFxDrops{item.work.trackGuid, resolved.drops});
// The record is spent only once the chain has been driven to every state it
// captured — releasing any earlier destroys the only account of which plugins
// The record is spent once the chain has been driven to every captured state
// that is still PLACEABLE — which is why the release fires with drops
// outstanding: a dropped entry names an FX the chain no longer has, so
// holding the record for it would replan work that can never resolve.
// Releasing any EARLIER is what destroys the only account of which plugins
// the USER had offline before the park, and the next online blanket-enables
// them. Reported rather than applied; see ReclaimTickResult::released.
if (item.work.kind == view::FxWorkKind::Online)
@@ -208,6 +243,12 @@ struct ReclaimRunResult {
// 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;
// Carried out on the abort path TOO, unlike the releases: a drop is a fact about the
// chain the tick just wrote, not a claim about the model a re-entered load may have
// replaced. The re-derivation that follows an abort resolves the very same set,
// which is what shouldReportDrops is there to swallow.
std::vector<TrackFxDrops> drops;
};
// Owns the derived queue and the debounce clock across a tick that can be re-entered:
@@ -256,14 +297,26 @@ public:
// back whether or not the drain's own work survives.
clock_.swap(clock);
if (generation_ != entered) return ReclaimRunResult{tick.writes, false, true};
if (generation_ != entered)
return ReclaimRunResult{tick.writes, false, true, tick.drops};
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};
if (!tick.released.empty()) {
for (const std::string& guid : tick.released) host.releaseFx(guid);
// The spend has to be as durable as the record it spends. A reload — a
// reopen, or the undo/redo loadFromProject — rebuilds the model from the
// PERSISTED view state, and a record still held there re-derives this same
// Online work and re-drives the chain, silently reverting whatever per-FX
// offline the user set once the reclaim had finished. One persist per tick,
// after every release the tick earned, and still killable in between: die
// there and the record simply survives on disk, whereupon the re-derived
// item finds the chain already online, writes nothing, and releases again.
host.persistModel();
}
return ReclaimRunResult{tick.writes, tick.settled, false, tick.drops};
}
private:
@@ -293,9 +346,11 @@ void invalidateFxReclaim();
bool fxReclaimDrainInProgress();
// 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
// applyMode is on the stack, while already draining, or when the active project is not
// the one the queue was derived against.
void fxReclaimTick(ViewModeModel& model, ReaProject* proj);
// the session's view model first if it was invalidated. Returns without touching the
// project while an applyMode is on the stack, while already draining, or when the active
// project is not the one the queue was derived against. Takes the SESSION rather than the
// model alone because completing an online spends the park record's FX half, and that
// spend has to reach the persisted view state — see FxReclaimQueue::run.
void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj);
} // namespace reasampler