Extend the park trust test to the FX chain, name the refused tracks, and stop reprinting an unchanged refusal

This commit is contained in:
2026-08-05 17:10:19 -04:00
parent 203961f41c
commit 5376ab085c
8 changed files with 318 additions and 86 deletions
+45 -15
View File
@@ -30,15 +30,25 @@ decide membership or mode rules.
(hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and
`TrackFX_SetOffline(track, fx, true)` for each FX (reclaim CPU) — full CPU-park,
not mix-removal-only.
- **A snapshot is only ever taken from a chain no park has touched.** An absent
snapshot is NOT evidence of a clean chain — `discardDeferredFxParks` drops
intents whose flag writes already landed, so a reloaded model and an
already-parked project routinely coexist. A park meeting that pair REFUSES:
no flag write, no FX-offline enqueue, no snapshot, and one console line naming
the hand recovery. Leaving the track as found is the only non-destructive
answer once the pre-park truth is gone — snapshotting there would record park
state as the user's state and every later restore would faithfully write it
back. Decision at `view_fx_park`'s `decidePark`.
- **A snapshot is only ever taken from a chain no park has touched — BOTH halves
of it.** The trust test reads the four driven flags AND the FX chain, and either
reading parked alone is enough to refuse: each half is the only source of truth
for its own half of the snapshot, so one of them lying poisons the whole record.
The FX half asks "is ANY FX offline", not "every" — no read distinguishes a
park's leftover from one the user set by hand, and only "any" errs toward
committing nothing false. An absent snapshot is NOT evidence of a clean chain —
`discardDeferredFxParks` drops intents whose flag writes already landed, so a
reloaded model and an already-parked project routinely coexist. A park meeting
that pair REFUSES: no flag write, no FX-offline enqueue, no snapshot, and a
console report NAMING the refused tracks (they are hidden from both the TCP and
the mixer, so a count points at nothing the user can go and find) plus the hand
recovery, printed once per changed refused set per project rather than on every
reapply. Leaving the track as found is the only non-destructive answer once the
pre-park truth is gone. **The refusal's cost is deliberate and it is not
symmetric with "hidden forever":** because it skips the FX enqueue too, a track
whose flags read parked but whose FX are still online keeps them online for
good. Refusing guarantees nothing false is written; it does not promise to
finish the park. Decision at `view_fx_park`'s `decidePark`.
- **Non-destructive restore.** For every flag the tool drives, snapshot the prior
value BEFORE parking; on toggle-back restore FROM the snapshot, never to a
hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag
@@ -60,10 +70,18 @@ decide membership or mode rules.
offline/online is enqueued and applied on a later idle tick (`view_fx_park`),
so the new mode paints first. The deferral changes only WHEN the plugins move —
they still unload and re-instantiate, and un-persisted internal state is still
lost. What it does change is the undo record: the offline writes land outside
the switch's undo block, so the tool no longer re-drives them on an undo or a
redo — what a Ctrl-Z then leaves the chain at is REAPER's own FX-state record,
`[verify — DAW]`. **The idle tick is not the only drain point.** Any path that
lost. What it does change is the undo record, and REAPER's behaviour there is
now OBSERVED, not assumed: the drain's `TrackFX_SetOffline` calls (the only
`TrackFX_SetOffline` in the tree) run outside any undo block, and REAPER mints
ONE implicit undo point per call — a small project measured 10 points for one
switch, 1 for the switch itself and 9 for its FX. Ctrl-Z walks them one at a
time, each step re-onlining a single FX, so one Ctrl-Z after a switch does not
bring the plugins back. It also puts the switch point BELOW every FX point: an
undo deep enough to restore the driven flags and roll the `"reasampler"` ext
state back has already re-onlined every FX, so flags, FX and snapshot move
together. The per-FX fragmentation is itself a defect to be closed by collapsing
the drain's writes into one undo point; nothing here depends on them staying
separate. **The idle tick is not the only drain point.** Any path that
serializes the view model drains synchronously first (`persistViewState`,
`render_in_place`), because a save landing between a restore's synchronous flag
writes and its drain would record offline FX beside a model that no longer
@@ -116,7 +134,7 @@ applies the resulting lane state to live tracks.
## Modules
- `view` — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN`, with per-FX offline deferred to `view_fx_park`), restores from snapshot. Owns the one discriminator (`target != active`) that separates a real switch from a reapply, and with it both the playback gate (`transportBlocksModeSwitch`) and the solo cache/clear/restore seams. **Never touches master or `B_MUTE`.**
- `view_fx_park` — the park surface: the `TrackFX_GetFXGUID` identity read snapshot/park/restore share, the deferred intent queue that keeps `TrackFX_SetOffline` off the switch's synchronous path (at most one intent per track GUID, latest wins, an intent landing on its own pending inverse cancels it), the idle-tick drain `main.cpp`'s `OnTimer` calls, and the pure per-track park decision (`decidePark` over "does the model hold a snapshot" × "does the chain already read parked", plus `trackFlagParm` the ONE `Flag` → REAPER-parameter mapping park's live read and restore's write both address). **The drain owns no model state.** A snapshot is dropped where the restore is PLANNED — the flags are back at their captured values from that moment, and a model that still described the track as parked would let a persist or a reapply inside the drain window replan a restore over whatever the user changed since. What the deferral costs instead is that the live FX chain stops being a trustworthy snapshot source while an intent is pending: a park that CANCELS a pending restore takes the pre-park FX states from that restore's ops (`preParkFxFromCancelledRestore`), because the chain still reads the parked values and the cancel means no drain will ever fix them.
- `view_fx_park` — the park surface: the `TrackFX_GetFXGUID` identity read snapshot/park/restore share, the deferred intent queue that keeps `TrackFX_SetOffline` off the switch's synchronous path (at most one intent per track GUID, latest wins, an intent landing on its own pending inverse cancels it), the idle-tick drain `main.cpp`'s `OnTimer` calls, the per-track park decision (`decidePark` over "does the model hold a snapshot" × "does the chain already read parked", with `decideParkForTrack` owning the live reads BOTH halves of that second term needs, and `trackFlagParm` the ONE `Flag` → REAPER-parameter mapping snapshot's read, park's read and restore's write all address), and the refusal report (`reportRefusedParks`, which names the tracks and holds the last-reported set so a reapply does not reprint it). **The drain owns no model state.** A snapshot is dropped where the restore is PLANNED — the flags are back at their captured values from that moment, and a model that still described the track as parked would let a persist or a reapply inside the drain window replan a restore over whatever the user changed since. What the deferral costs instead is that the live FX chain stops being a trustworthy snapshot source while an intent is pending: a park that CANCELS a pending restore takes the pre-park FX states from that restore's ops (`preParkFxFromCancelledRestore`), because the chain still reads the parked values and the cancel means no drain will ever fix them.
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface, plus `clearTrackSolos`/`restoreTrackSolos`, the outgoing-clear and incoming-replay entry points `view` drives them through. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
## Gotchas
@@ -141,7 +159,19 @@ applies the resulting lane state to live tracks.
track whose reloaded model carries no snapshot — nothing plans its restore, so
FX left offline stay offline, and its next park REFUSES rather than
re-snapshotting the parked chain (snapshot-source invariant above). Stuck, but
never falsely committed. Full contract at `discardDeferredFxParks`.
never falsely committed — and never finished either, since the refusal skips the
FX enqueue too. Full contract at `discardDeferredFxParks`.
- **Which routes actually reach that no-snapshot-plus-parked-chain pair.** An undo
of a mode switch is NOT one of them: per the observed undo semantics above, the
flags, the FX and the ext-state snapshots all come back at the same step, and
stopping short of it leaves the snapshot in place (so the next park is a
`ParkOnly`, not a refusal). What remains: a model replaced without the project
rolling back with it (an unreadable or absent `view_state` against an `.rpp`
saved parked), a snapshot `reconcile` pruned while its track was out of the live
enumeration and then undo-restored with the track, and a hand or script edit
that leaves a chain park-shaped. A track the USER keeps hidden from both panels,
out of the mix and FX-bypassed reads identically to all of them and is refused
too — no lost state there at all, which is why the report asserts no cause.
- `fx_offline`'s identity keying (`TrackFX_GetFXGUID`) assumes the GUID stays
attached to its plugin across a chain mutation while parked. That is
`[verify — DAW]` (see `fxGuidString` in `view_fx_park.cpp`) and SWS issue #802 is a