view: split the deferred FX-park drain by kind — restores stay forced, parks go one FX per idle tick behind a 1s coalescing delay
A rapid A→B→A flip now costs no plugin work: the restore cancels the still-pending park outright. A park that has already written one FX carries a `partial` flag and is superseded by its inverse rather than cancelled, so a half-parked chain is never stranded.
This commit is contained in:
+107
-40
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
// Design View's per-FX offline surface: the FX-identity read that snapshot,
|
||||
// park and restore all address FX through, the deferred intent queue that keeps
|
||||
// TrackFX_SetOffline off the mode switch's synchronous path, and the idle-tick
|
||||
// drain that applies it. See src/shell/view/CLAUDE.md's FX-parking caveat.
|
||||
// TrackFX_SetOffline off the mode switch's synchronous path, and the two drains
|
||||
// that apply it — restores whole, parks one FX per idle tick. See
|
||||
// src/shell/view/CLAUDE.md's FX-parking caveat.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -24,17 +25,27 @@ std::vector<std::string> liveFxGuids(MediaTrack* tr);
|
||||
// One deferred per-FX intent for one track. A park carries no ops (every live
|
||||
// slot goes offline); a restore carries the planned ops verbatim.
|
||||
struct FxParkIntent {
|
||||
std::string guid;
|
||||
bool park = false;
|
||||
std::string guid;
|
||||
bool park = false;
|
||||
// A park write is landing or has landed on this track, so the live chain
|
||||
// matches NEITHER endpoint. Parks apply one FX per idle tick, so this is a
|
||||
// state a park genuinely sits in — and while it holds, the cancel rule
|
||||
// below is unsafe in both directions: only the restore's own ops can put
|
||||
// back what a half-finished park offlined, and a park arriving on that
|
||||
// restore must RESUME rather than annihilate or it strands every FX the
|
||||
// first pass had not reached. Restores carry no such state: they are
|
||||
// detached whole and applied in one drain.
|
||||
bool partial = false;
|
||||
std::vector<FxOfflineOp> restoreOps;
|
||||
};
|
||||
|
||||
// The queue's re-entrancy rule, pure so it can be asserted without a DAW: at
|
||||
// most ONE intent per track GUID, and the latest one wins. Park and restore are
|
||||
// inverses, so an intent landing on its own pending inverse CANCELS it rather
|
||||
// than stacking — the queued work never ran, so the track already holds the
|
||||
// state the newcomer asks for, and replaying both would be both slower and
|
||||
// observably wrong.
|
||||
// inverses, so an UNSTARTED intent landing on its own pending inverse CANCELS it
|
||||
// rather than stacking — the queued work never ran, so the track already holds
|
||||
// the state the newcomer asks for, and replaying both would be both slower and
|
||||
// observably wrong. That cancel is what makes a rapid A→B→A mode flip cost zero
|
||||
// plugin work: B→A's restore annihilates A's park before a single FX moved.
|
||||
class FxParkQueue {
|
||||
public:
|
||||
// Returns the ops of a pending restore this park CANCELLED, empty otherwise.
|
||||
@@ -49,47 +60,83 @@ public:
|
||||
std::vector<FxOfflineOp> park(const std::string& guid) {
|
||||
FxParkIntent* held = find(guid);
|
||||
if (!held) {
|
||||
pending_.push_back(FxParkIntent{guid, true, {}});
|
||||
pending_.push_back(FxParkIntent{guid, true, false, {}});
|
||||
return {};
|
||||
}
|
||||
if (held->park) return {};
|
||||
std::vector<FxOfflineOp> cancelled = std::move(held->restoreOps);
|
||||
erase(held);
|
||||
if (held->partial) *held = FxParkIntent{guid, true, true, {}};
|
||||
else erase(held);
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
// A restore cancelling a pending park is COMPLETE at that point — the park
|
||||
// never ran, so no drain will ever come for this GUID.
|
||||
// A restore cancelling an UNSTARTED park is COMPLETE at that point — the
|
||||
// park never ran, so no drain will ever come for this GUID. Against a park
|
||||
// that has already written, the restore supersedes instead: its ops are the
|
||||
// only thing that can put the offlined FX back.
|
||||
void restore(const std::string& guid, std::vector<FxOfflineOp> ops) {
|
||||
FxParkIntent* held = find(guid);
|
||||
if (!held) {
|
||||
pending_.push_back(FxParkIntent{guid, false, std::move(ops)});
|
||||
} else if (held->park) {
|
||||
pending_.push_back(FxParkIntent{guid, false, false, std::move(ops)});
|
||||
} else if (held->park && !held->partial) {
|
||||
erase(held);
|
||||
} else if (held->park) {
|
||||
*held = FxParkIntent{guid, false, true, std::move(ops)};
|
||||
} else {
|
||||
held->restoreOps = std::move(ops);
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue order, which is apply order: park before restore within one
|
||||
// switch, as the synchronous body already orders them.
|
||||
// Enqueue order, which is apply order WITHIN a kind — the two kinds drain
|
||||
// separately and on different schedules (see the two drains below).
|
||||
const std::vector<FxParkIntent>& pending() const { return pending_; }
|
||||
bool empty() const { return pending_.empty(); }
|
||||
void clear() { pending_.clear(); }
|
||||
|
||||
// Detaches everything pending, leaving the queue able to accept intents
|
||||
// enqueued WHILE the caller applies what it took. [verify — DAW] applying
|
||||
// loads/unloads plugins, which is ASSUMED to pump the message loop, so a
|
||||
// re-entrant switch can enqueue mid-apply: iterating the live queue would
|
||||
// dangle on the push_back, and clearing it afterwards would discard
|
||||
// whatever arrived during the apply. The detach is correct either way; only
|
||||
// the need for it is unconfirmed.
|
||||
std::vector<FxParkIntent> take() {
|
||||
// Detaches every pending RESTORE and leaves the parks behind, in order. The
|
||||
// detach — not a live iteration, not a clear afterwards — is what lets the
|
||||
// queue accept intents enqueued WHILE the caller applies what it took:
|
||||
// [verify — DAW] applying loads/unloads plugins, which is ASSUMED to pump
|
||||
// the message loop, so a re-entrant switch can enqueue mid-apply. Iterating
|
||||
// the live queue would dangle on the push_back and clearing it afterwards
|
||||
// would discard whatever arrived. Correct either way; only the need for it
|
||||
// is unconfirmed.
|
||||
std::vector<FxParkIntent> takeRestores() {
|
||||
std::vector<FxParkIntent> taken;
|
||||
taken.swap(pending_);
|
||||
std::vector<FxParkIntent> kept;
|
||||
for (FxParkIntent& i : pending_) {
|
||||
if (i.park) kept.push_back(std::move(i));
|
||||
else taken.push_back(std::move(i));
|
||||
}
|
||||
pending_.swap(kept);
|
||||
return taken;
|
||||
}
|
||||
|
||||
// The park the drain should work next, in enqueue order; empty when none is
|
||||
// pending. A PEEK, by value — an apply can reshape the queue under a pointer
|
||||
// — and it marks nothing: a park still cancellable for free stays that way
|
||||
// until markPartial says a write is imminent.
|
||||
std::string nextParkGuid() const {
|
||||
for (const FxParkIntent& i : pending_)
|
||||
if (i.park) return i.guid;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Records that a park write is ABOUT TO land on `guid` — set before the
|
||||
// write, because the write may pump the message loop and an intent arriving
|
||||
// in that window must already see a chain that matches neither endpoint.
|
||||
void markPartial(const std::string& guid) {
|
||||
if (FxParkIntent* held = find(guid)) held->partial = true;
|
||||
}
|
||||
|
||||
// Retires a park with nothing left to offline. A no-op when a restore
|
||||
// replaced it mid-apply — that restore is the newer intent and owns the
|
||||
// chain from here.
|
||||
void finishPark(const std::string& guid) {
|
||||
FxParkIntent* held = find(guid);
|
||||
if (held && held->park) erase(held);
|
||||
}
|
||||
|
||||
private:
|
||||
FxParkIntent* find(const std::string& guid) {
|
||||
for (FxParkIntent& i : pending_)
|
||||
@@ -147,22 +194,42 @@ PreParkFx snapshotFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& canc
|
||||
std::vector<FxOfflineOp> deferFxPark(ReaProject* proj, const std::string& guid);
|
||||
void deferFxRestore(ReaProject* proj, const std::string& guid, std::vector<FxOfflineOp> ops);
|
||||
|
||||
// Applies every pending intent. Touches NO model state — a restore's snapshot is
|
||||
// dropped where the restore is planned (applyMode). Discards the queue unapplied
|
||||
// if the project it was enqueued against is no longer current (close / switch);
|
||||
// an intent whose track is gone is pruned. Idle cost is one empty-queue test.
|
||||
// Re-entrant calls early-out: one drain at a time, the outer one owns the queue.
|
||||
// The two halves of the drain, split because only ONE of them is unsafe to
|
||||
// persist over. Both touch NO model state — a restore's snapshot is dropped
|
||||
// where the restore is planned (applyMode) — both discard the queue unapplied if
|
||||
// the project it was enqueued against is no longer current (close / switch),
|
||||
// both prune an intent whose track is gone, both cost one empty-queue test when
|
||||
// idle, and both early-out re-entrantly: one drain at a time, the outer one owns
|
||||
// the queue.
|
||||
//
|
||||
// Called on the idle tick AND synchronously before the view model is serialized
|
||||
// (persistViewState, render_in_place). The second call is not an optimization:
|
||||
// between a restore's synchronous flag writes and its drain the FX are still
|
||||
// offline while the model has already dropped the snapshot that would replan
|
||||
// them, so a save inside that window — deterministic under a custom action chain
|
||||
// like "toggle mode; save project" — records offline FX beside a snapshot-free
|
||||
// model, and nothing on reopen brings them back online. The cost is that an
|
||||
// action-driven switch pays the FX hitch before it returns; its repaint and undo
|
||||
// block have both closed by then, which is what the deferral was for.
|
||||
void drainDeferredFxParks();
|
||||
// RESTORES, applied whole, synchronously. Called on the idle tick AND before the
|
||||
// view model is serialized (persistViewState, render_in_place). The second call
|
||||
// is not an optimization: between a restore's synchronous flag writes and its
|
||||
// drain the FX are still offline while the model has already dropped the
|
||||
// snapshot that would replan them, so a save inside that window — deterministic
|
||||
// under a custom action chain like "toggle mode; save project" — records offline
|
||||
// FX beside a snapshot-free model, and nothing on reopen brings them back
|
||||
// online. The cost is that an action-driven switch pays the RELOAD hitch before
|
||||
// it returns; its repaint and undo block have both closed by then, which is what
|
||||
// the deferral was for.
|
||||
void drainDeferredFxRestores();
|
||||
|
||||
// PARKS, one FX per idle tick behind a short coalescing delay. Nothing waits on
|
||||
// a park, and unlike a restore it is safe to persist over: the flags are already
|
||||
// parked and the snapshot already stored, so a .rpp saved mid-window reopens with
|
||||
// the FX online, applyMode replans a park for that inactive leaf, and the drain
|
||||
// offlines it. It converges on its own, which is why this half does not force.
|
||||
//
|
||||
// One FX, not one track: a single convolution reverb or loaded sampler is the
|
||||
// unit of cost, so per-track chunking would not bound the hitch.
|
||||
//
|
||||
// The delay's cost is the width of a documented hazard, not a new one. applyPark
|
||||
// enumerates the chain LIVE at drain time while the pre-park snapshot was taken
|
||||
// at switch time, so an FX added inside the window is offlined carrying no
|
||||
// snapshot entry — resolveFxRestore then has no op for it and it stays offline.
|
||||
// Same failure as the cancelled-restore case below, over a longer window; keep
|
||||
// the delay short.
|
||||
void tickDeferredFxParks();
|
||||
|
||||
// Drops every pending intent without applying it. Called when the model the
|
||||
// intents were planned against has been replaced (project load/switch, undo/redo
|
||||
|
||||
Reference in New Issue
Block a user