Refuse to park a track whose pre-park snapshot is gone, rather than re-snapshotting the already-parked chain as the user's state

This commit is contained in:
2026-08-03 18:30:40 -04:00
parent 2f96dd3da5
commit 203961f41c
6 changed files with 189 additions and 22 deletions
+13 -2
View File
@@ -30,6 +30,15 @@ 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`.
- **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
@@ -107,7 +116,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 per-FX offline 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), and the idle-tick drain `main.cpp`'s `OnTimer` calls. **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, 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_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
@@ -130,7 +139,9 @@ applies the resulting lane state to live tracks.
over the reloaded model, re-planning a park for every inactive leaf, so parked
FX converge on the following drain. The one case that does not self-heal is a
track whose reloaded model carries no snapshot — nothing plans its restore, so
FX left offline stay offline. Full contract at `discardDeferredFxParks`.
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`.
- `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