Docs: reflect the Design View redesign — EXPOSE replaces the park refusal, FX offlining moved to the reclaimer, undo repair is ours

This commit is contained in:
2026-08-06 23:56:50 -04:00
parent c9c957a3da
commit 2173263bb9
4 changed files with 106 additions and 138 deletions
+4 -4
View File
@@ -867,9 +867,9 @@ public accessor surface. Doing that in the same commit that changed the byte for
the golden test literals pin would roll a format change and a codec extraction the golden test literals pin would roll a format change and a codec extraction
together, which is the riskier order. together, which is the riskier order.
**`src/shell/view/view.cpp` is no longer over the bar.** It measures **594 lines** **`src/shell/view/view.cpp` is no longer over the bar.** It measures **230 lines**
(`wc -l`, re-measured after the undo collapse moved one track's whole park/restore — (`wc -l`, re-measured after the undo collapse moved one track's whole park/restore —
snapshot, flags and per-FX offline — into `view_fx_park`'s `parkTrack`/ snapshot, flags and per-FX offline — into `view_park`'s `parkTrack`/
`restoreTrack`). Recorded because the entry above used to name it as a second `restoreTrack`). Recorded because the entry above used to name it as a second
over-ceiling file: if it grows again, note that a further seam there is blocked over-ceiling file: if it grows again, note that a further seam there is blocked
not by a private-state/friend question but by a build file another team owns — not by a private-state/friend question but by a build file another team owns —
@@ -885,7 +885,7 @@ sidestepped), dropping the file under the ~600-line ceiling.
## FX-GUID stability for the park's restore is unverified in the DAW ## FX-GUID stability for the park's restore is unverified in the DAW
**Context.** The Design View park/restore FX keying (`restoreTrack`, **Context.** The Design View park/restore FX keying (`restoreTrack`,
`src/shell/view/view_fx_park.cpp`) rests on `TrackFX_GetFXGUID` returning an identity that `src/shell/view/view_park.cpp`) rests on `TrackFX_GetFXGUID` returning an identity that
survives a chain reorder while a track is parked. SWS issue #802 reports that after survives a chain reorder while a track is parked. SWS issue #802 reports that after
`SNM_MoveOrRemoveTrackFX` reorders a chain, the FXID lines do not follow the plugin `SNM_MoveOrRemoveTrackFX` reorders a chain, the FXID lines do not follow the plugin
(`SNM_PreObjectState()` → `RemoveAllIds()`) — if that still holds, an SWS-driven (`SNM_PreObjectState()` → `RemoveAllIds()`) — if that still holds, an SWS-driven
@@ -896,7 +896,7 @@ operation this keying targets.
instances of the same plugin. instances of the same plugin.
**Already flagged in code — this entry is the tracked home, not a restatement.** **Already flagged in code — this entry is the tracked home, not a restatement.**
There is a `[verify — DAW]` marker at `fxGuidString` in `src/shell/view/view_fx_park.cpp` and There is a `[verify — DAW]` marker at `fxGuidString` in `src/shell/view/view_park.cpp` and
a note in `src/shell/view/CLAUDE.md`'s Gotchas; point at them rather than restating a note in `src/shell/view/CLAUDE.md`'s Gotchas; point at them rather than restating
them in full. them in full.
+8 -5
View File
@@ -47,15 +47,18 @@ Checks for Θ, Ξ, Ψ, Ε, Ρ, Γ, and Ω work that no unit test can close. Buil
- [ ] Solo tracks in Arrange, switch to Design, solo different tracks, switch back — each mode restores its own solo set verbatim (`docs/COMPLETED.md` §"Ψ-W1-T2") - [ ] Solo tracks in Arrange, switch to Design, solo different tracks, switch back — each mode restores its own solo set verbatim (`docs/COMPLETED.md` §"Ψ-W1-T2")
- [ ] Attempt a mode switch while the transport is playing, then while recording — both refuse, visibly (`docs/COMPLETED.md` §"Ψ-W1-T2") - [ ] Attempt a mode switch while the transport is playing, then while recording — both refuse, visibly (`docs/COMPLETED.md` §"Ψ-W1-T2")
- [ ] Click the footer mode segment, save, reopen the project — the mode persisted (`docs/COMPLETED.md` §"Ψ-W1-T2") - [ ] Click the footer mode segment, save, reopen the project — the mode persisted (`docs/COMPLETED.md` §"Ψ-W1-T2")
- [ ] `[verify — DAW]` Reproduce the strand: project with FX in both modes, switch to Design, save, close, reopen, toggle to Arrange. Toggle back and forth several more times — the arrangement's FX must NOT converge on permanently offline/hidden. A track the tool refuses to park is NAMED in the console, and the same refusal is not reprinted on a later reapply unless the refused set changed (`src/shell/view/view_fx_park.h`'s `decidePark` / `reportRefusedParks`) - [ ] `[verify — DAW]` Reproduce the strand: a track with no park record whose live chain already reads park-shaped (hand-offline a plugin, or drop the tool's record via undo/redo), then switch modes — the track is driven visible, in-mix and FX-enabled (not parked), and NAMED in the console once. Switch again with nothing else changed and confirm the same message does NOT reprint (`src/shell/view/view_park.h`'s `parkTrack` / `reportExposedTracks` / `shouldReport`)
- [ ] `[verify — DAW]` **ONE switch, ONE point.** In a project with several FX across several tracks, switch modes once and count the undo points REAPER shows (Ctrl-Z tooltip / undo history): expect exactly ONE, `ReaSampler: activate <mode> view` — never two, and never one per FX. Then press Ctrl-Z once: the flags unpark and EVERY FX comes back online, in that single step. Verify the EFFECT, not merely that one entry exists — a mask that failed to pick up `UNDO_STATE_FX` would still show one correct-looking entry while leaving every plugin offline, which reads as a pass and isn't. Then Ctrl-Y: the redo re-parks all of it, also in one step (`src/shell/view/CLAUDE.md` §Invariants — "ONE mode switch is ONE undo point") - [ ] `[verify — DAW]` **ONE switch, ONE point.** In a project with several FX across several tracks, switch modes once and count the undo points REAPER shows (Ctrl-Z tooltip / undo history): expect exactly ONE, `ReaSampler: activate <mode> view` — never two, and never one per FX. Then press Ctrl-Z once: the flags and lane state unpark in that single step — FX are the reclaimer's and are not part of this undo point (see the item below on undo and FX). Then Ctrl-Y: the redo re-parks the flags and lane state, also in one step (`src/shell/view/CLAUDE.md` §Invariants — "ONE mode switch is ONE undo point")
- [x] **VERIFIED — the discard-block mint-suppression premise.** Ten unblocked `TrackFX_SetOffline` calls minted ten undo points; the same ten calls wrapped in `Undo_BeginBlock2(p)` … `Undo_EndBlock2(p, "", 0)` minted zero. This is what the reclaimer's per-write discard block (`fx_reclaim.cpp`'s `writeOffline`) and the off-switch FX design both rest on — no longer an unobserved inference (`src/shell/view/CLAUDE.md` §Invariants — "ONE mode switch is ONE undo point")
- [ ] `[verify — DAW]` Whether a `SetProjExtState` issued from inside `SaveExtensionConfig` reaches the `<EXTSTATE>` block of the file being written now, or lands one save later (`src/shell/view/fx_reclaim.h`'s `ReleaseFlush`, `fx_reclaim.cpp`'s `fxReclaimFlushRelease`)
- [ ] `[verify — DAW]` Whether an undo (Ctrl-Z) of a mode switch now restores visibility correctly, now that FX offline/online moved off `applyMode`'s block onto the reclaimer — a Ctrl-Z rolls back only the flags and lane state, not whatever the reclaimer had (or had not) converged (`src/shell/view/CLAUDE.md` §Gotchas — "An UNDO of a mode switch no longer rolls back FX at all")
- [ ] `[verify — DAW]` **Where the `view_state` lands on undo AND redo, and whether the redo strands a track.** `persistViewState` writes the ext state AFTER `applyMode`'s block closes, so the point never saw the new `view_state` — the undo direction is expected to come out right by position, the redo direction is the open one. Use a project with FX on the tracks that park. After the Ctrl-Z above, read the footer segment: it must show the mode the user came FROM, and a further switch must bank solos under that mode id. Then Ctrl-Y and read it again: if the tracks re-park while the footer still shows the pre-switch mode, the redo restored the stale ext state and model-vs-project is out of step. **Then switch modes ONCE MORE and read the console** — the hypothesised consequence is a `left unparked` refusal line naming those tracks, because the redo-restored model carries no snapshot for a chain that reads parked. A clean switch with no refusal line kills the hypothesis; the line appearing confirms a NEW refusal route and the persist position has to be reopened. Report which, and the footer state at each step. Do NOT fix by moving `persistViewState` inside the block — it can open a modal Save-As (`src/shell/view/CLAUDE.md` §Gotchas) - [ ] `[verify — DAW]` **Where the `view_state` lands on undo AND redo, and whether the redo strands a track.** `persistViewState` writes the ext state AFTER `applyMode`'s block closes, so the point never saw the new `view_state` — the undo direction is expected to come out right by position, the redo direction is the open one. Use a project with FX on the tracks that park. After the Ctrl-Z above, read the footer segment: it must show the mode the user came FROM, and a further switch must bank solos under that mode id. Then Ctrl-Y and read it again: if the tracks re-park while the footer still shows the pre-switch mode, the redo restored the stale ext state and model-vs-project is out of step. **Then switch modes ONCE MORE and read the console** — the hypothesised consequence is a `left unparked` refusal line naming those tracks, because the redo-restored model carries no snapshot for a chain that reads parked. A clean switch with no refusal line kills the hypothesis; the line appearing confirms a NEW refusal route and the persist position has to be reopened. Report which, and the footer state at each step. Do NOT fix by moving `persistViewState` inside the block — it can open a modal Save-As (`src/shell/view/CLAUDE.md` §Gotchas)
- [ ] `[verify — DAW]` **No point on open.** Open a project saved in Design mode with every inactive leaf already fully parked (flags AND FX), and check the undo history: the load-tick reapply writes nothing, so it must add NO undo point at all — the `("", 0)` discard form. This only holds when the saved state really was fully parked; a first open after tagging a new leaf, an FX added to a parked track, or a plugin hand-onlined since the last save all have real state to write and legitimately mint one correct point. An entry appearing on a project that WAS fully parked means the discard is not working — and since `mintManagedLanes`' no-op path rests on the same idiom, check that alongside it (`src/shell/view/view.h`'s `applyMintsUndoPoint`) - [ ] `[verify — DAW]` **No point on open.** Open a project saved in Design mode with every inactive leaf already fully parked (flags AND FX), and check the undo history: the load-tick reapply writes nothing, so it must add NO undo point at all — the `("", 0)` discard form. This only holds when the saved state really was fully parked; a first open after tagging a new leaf, an FX added to a parked track, or a plugin hand-onlined since the last save all have real state to write and legitimately mint one correct point. An entry appearing on a project that WAS fully parked means the discard is not working — and since `mintManagedLanes`' no-op path rests on the same idiom, check that alongside it (`src/shell/view/view.h`'s `applyMintsUndoPoint`)
- [ ] `[verify — DAW]` **The hitch now sits on the switch.** On a project with heavy plugins (convolution, a loaded sampler) across several parked tracks, time the mode toggle: by design the action does not return until every plugin has unloaded/reloaded. Report roughly how long, and whether the UI recovers cleanly afterwards — the whole write phase runs under one `PreventUIRefresh(1)` hold (`src/shell/view/CLAUDE.md` §Invariants — the documented caveat) - [ ] `[verify — DAW]` **The switch itself returns fast; the hitch moved to the reclaimer.** On a project with heavy plugins (convolution, a loaded sampler) across several parked tracks, time the mode toggle action itself — it no longer waits on any plugin load/unload, so it should return near-instantly. Then watch the background reclaim: once a track has read continuously parked for `kParkHoldMs` (3s), the reclaimer moves it in budgeted ticks (`kOnlineBudgetMs`/`kOfflineBudgetMs`) — report whether those ticks are noticeable/disruptive to the UI while they run (`src/shell/view/CLAUDE.md` §Invariants — the documented caveat, `fx_reclaim.h`'s `kOnlineBudgetMs`/`kOfflineBudgetMs`)
- [ ] `[verify — DAW]` **Nested blocks around the FX writes.** Two directions, both undocumented in the SDK header. (a) `render_in_place` calls `applyMode` inside its OWN undo block, so its single `ReaSampler: render selected track to a new track` point should now absorb the reapply's FX moves — run that verb from Design mode over a track with FX and confirm ONE point, not two, and that Ctrl-Z reverses all of it. (b) The inverse: fire a mode-switch action WHILE a switch's plugin loads are in flight (a heavy chain widens the window) — `applyMode` is latched, so the expected outcome is that the second switch is DROPPED entirely and silently; the history must show one point, and the footer must still read the first switch's target. Re-firing it afterwards must work normally. (c) The same window, but let `OnTimer` -> `bankPanelRefresh` -> `mintManagedLanes` run into it: that path is NOT latched and opens a block of its own, so check the history for a split or misordered point (`src/shell/view/CLAUDE.md` §Gotchas) - [ ] `[verify — DAW]` **Nested blocks around the flag/lane writes.** Two directions, both undocumented in the SDK header (`applyMode` no longer writes FX at all — that moved to the reclaimer, off this block entirely). (a) `render_in_place` calls `applyMode` inside its OWN undo block, so its single `ReaSampler: render selected track to a new track` point should absorb the reapply's flag/lane writes — run that verb from Design mode over a track with FX and confirm ONE point, not two, and that Ctrl-Z reverses all of it. (b) The inverse: fire a mode-switch action WHILE a switch's plugin loads are in flight (a heavy chain widens the window) — `applyMode` is latched, so the expected outcome is that the second switch is DROPPED entirely and silently; the history must show one point, and the footer must still read the first switch's target. Re-firing it afterwards must work normally. (c) The same window, but let `OnTimer` -> `bankPanelRefresh` -> `mintManagedLanes` run into it: that path is NOT latched and opens a block of its own, so check the history for a split or misordered point (`src/shell/view/CLAUDE.md` §Gotchas)
- [ ] `[verify — DAW]` **The load reapply is never lost to the latch.** Open a project saved in Design mode with heavy plugins on parked tracks (so an apply is long) while another apply can overlap — e.g. trigger a project open from an action fired during a switch. The saved mode must still be applied: the load glue tests `modeApplyInProgress` before spending its one-shot signal and retries on the next timer tick, so a delayed-by-a-tick reapply is a pass and a never-applied one is a failure (`src/app/main.cpp`'s load glue, `src/shell/view/view.h`'s `modeApplyInProgress`) - [ ] `[verify — DAW]` **The load reapply is never lost to the latch.** Open a project saved in Design mode with heavy plugins on parked tracks (so an apply is long) while another apply can overlap — e.g. trigger a project open from an action fired during a switch. The saved mode must still be applied: the load glue tests `modeApplyInProgress` before spending its one-shot signal and retries on the next timer tick, so a delayed-by-a-tick reapply is a pass and a never-applied one is a failure (`src/app/main.cpp`'s load glue, `src/shell/view/view.h`'s `modeApplyInProgress`)
- [ ] `[verify — DAW]` **A tab switch mid-apply cannot split the block.** `applyMode` pins the project once at entry and threads it through `Undo_BeginBlock2`/`Undo_EndBlock2`/`ValidatePtr2`. With a long switch in flight, switch project tabs (the pump premise makes this reachable) and confirm the undo point lands on the project the switch started in, with nothing written into the other tab (`src/shell/view/view.cpp`'s `applyMode`) - [ ] `[verify — DAW]` **A tab switch mid-apply cannot split the block.** `applyMode` pins the project once at entry and threads it through `Undo_BeginBlock2`/`Undo_EndBlock2`/`ValidatePtr2`. With a long switch in flight, switch project tabs (the pump premise makes this reachable) and confirm the undo point lands on the project the switch started in, with nothing written into the other tab (`src/shell/view/view.cpp`'s `applyMode`)
- [ ] `[verify — DAW]` **A→B→A costs two full cycles.** Toggle Arrange→Design→Arrange quickly on a project with heavy plugins: every plugin unloads and reloads twice, where the retired intent queue made a fast double-toggle free. Confirm the END STATE is still correct (every FX online, every driven flag back at its captured value) and report whether the doubled cost is tolerable in practice (`src/shell/view/CLAUDE.md` §Invariants — the A→B→A bullet) - [ ] `[verify — DAW]` **A fast A→B→A moves zero plugins; a held one costs a cycle each way.** Toggle Arrange→Design→Arrange quickly (well inside `kParkHoldMs`, 3s) on a project with heavy plugins — the debounce should mean nothing ever unloads: confirm the END STATE is correct (every FX online, every driven flag back at its captured value) and that the reclaimer reports no writes. Then repeat holding each side past 3s before flipping back and confirm the reclaimer now does converge each leg (offline while parked, online again on return), reporting whether that cost is tolerable in practice (`src/shell/view/CLAUDE.md` §Invariants — "A rapid A→B→A flip moves zero plugins", `fx_reclaim.h`'s `kParkHoldMs`)
- [ ] `[verify — DAW]` For a track the console names as left unparked, follow the message's recovery IN FULL: re-enable its TCP/mixer visibility, main send and FX enable, **and set every FX in its chain online** (FX enable is the chain bypass — it does not bring an individually offlined FX back). Then switch modes twice: it parks and restores normally, with no further console line. Do the flag half ONLY on a second such track and confirm it is still refused — the FX half of the trust test, and the reason the message spells the step out (unit-tested only as a decision, never against a live chain) - [ ] `[verify — DAW]` For a track the console names as left unparked, follow the message's recovery IN FULL: re-enable its TCP/mixer visibility, main send and FX enable, **and set every FX in its chain online** (FX enable is the chain bypass — it does not bring an individually offlined FX back). Then switch modes twice: it parks and restores normally, with no further console line. Do the flag half ONLY on a second such track and confirm it is still refused — the FX half of the trust test, and the reason the message spells the step out (unit-tested only as a decision, never against a live chain)
## Actions and drops ## Actions and drops
+7
View File
@@ -45,6 +45,11 @@ settled 2026-07-23):
`ViewModeModel::reconcile(liveGuids)`). The same rule binds one level down: a `ViewModeModel::reconcile(liveGuids)`). The same rule binds one level down: a
parked track's per-FX offline state keys on the FX's own identity parked track's per-FX offline state keys on the FX's own identity
(`TrackFX_GetFXGUID`), never its slot — see `fx_offline`. (`TrackFX_GetFXGUID`), never its slot — see `fx_offline`.
- **A park record's two halves release independently.** The flag half is spent
the moment a restore runs (`releaseSnapshotFlags`); the FX half survives until
the reclaimer has actually driven that chain back online (`releaseSnapshotFx`)
— releasing both at restore would destroy the only record of which plugins the
user had offline before the park, and a later online would blanket-enable them.
- **Relative/portable state only** in the persisted view section (GUID strings, - **Relative/portable state only** in the persisted view section (GUID strings,
mode ids — no absolute paths, no index positions). mode ids — no absolute paths, no index positions).
- **Show-both semantics.** A per-track "pin visible across modes" flag that - **Show-both semantics.** A per-track "pin visible across modes" flag that
@@ -103,6 +108,8 @@ settled 2026-07-23):
- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch. - `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch.
- `guid_diff` — the pure, REAPER-free core of the D2 Wave-2 new-content detection: `newGuids(previous, current)` computes the GUIDs present in `current` but absent from `previous` (empty GUIDs ignored); `GuidBaseline` tracks the live GUID set across polls for one project, implementing the first-poll-after-open guard (the first `observe()` after construction/`reset()` records a baseline and reports nothing new, so pre-existing content is never mass-tagged) and re-arms via `reset()` on a detected project switch so detection never diffs across two unrelated projects. - `guid_diff` — the pure, REAPER-free core of the D2 Wave-2 new-content detection: `newGuids(previous, current)` computes the GUIDs present in `current` but absent from `previous` (empty GUIDs ignored); `GuidBaseline` tracks the live GUID set across polls for one project, implementing the first-poll-after-open guard (the first `observe()` after construction/`reset()` records a baseline and reports nothing new, so pre-existing content is never mass-tagged) and re-arms via `reset()` on a detected project switch so detection never diffs across two unrelated projects.
- `lane_keys` — the pure, REAPER-free convention mapping a fixed lane's durable REAPER name (`P_LANENAME:n`) to the opaque lane-key `view_mode_model` keys by, plus the managed/manual heuristic both the toggle-apply path and the new-content/auto-tag exemption path share: `kManagedLanePrefix` ("reasampler:") stamps every lane the tool mints; `isManagedLaneName`/`managedLaneKey`/`laneNameForMode`/`modeIdFromLaneName` round-trip a lane name ↔ its owning mode id; `isOnManualLane` is the single predicate governing which lanes a toggle may drive and which items are exempt from auto-tag. - `lane_keys` — the pure, REAPER-free convention mapping a fixed lane's durable REAPER name (`P_LANENAME:n`) to the opaque lane-key `view_mode_model` keys by, plus the managed/manual heuristic both the toggle-apply path and the new-content/auto-tag exemption path share: `kManagedLanePrefix` ("reasampler:") stamps every lane the tool mints; `isManagedLaneName`/`managedLaneKey`/`laneNameForMode`/`modeIdFromLaneName` round-trip a lane name ↔ its owning mode id; `isOnManualLane` is the single predicate governing which lanes a toggle may drive and which items are exempt from auto-tag.
- `fx_state` — what the FX chains SHOULD be, derived from view state alone: `desiredFxState` (the whole project's Online/Offline work, Online sorted first), `fxRestoreTargets` (one track's Online targets from its park record), and `resolveFxWork` (one item's move resolved by identity against a live chain). Derived and never persisted; the park record's captured FX half is the only durable half.
- `applied_state` — a comparable fingerprint (`AppliedState`/`appliedState`) of what the tool's last apply actually drove, so a reconcile can ask "did our output drift from what a restore would drive it back to" rather than "did an undo happen" — membership, lane ownership and the solo cache are deliberately excluded.
## Gotchas ## Gotchas
+86 -128
View File
@@ -27,28 +27,22 @@ decide membership or mode rules.
lost, only parked with the mode it belongs to. A REAPPLY (target == active — lost, only parked with the mode it belongs to. A REAPPLY (target == active —
tag/untag/show-both, project load) touches solo not at all. tag/untag/show-both, project load) touches solo not at all.
- **Parking a track** (inactive-mode leaf) drives `B_SHOWINTCP=0`, `B_SHOWINMIXER=0` - **Parking a track** (inactive-mode leaf) drives `B_SHOWINTCP=0`, `B_SHOWINMIXER=0`
(hide both panels), `B_MAINSEND=0` (out of mix), `I_FXEN=0` (FX bypassed), and (hide both panels), `B_MAINSEND=0` (out of mix), and `I_FXEN=0` (FX bypassed) —
`TrackFX_SetOffline(track, fx, true)` for each FX (reclaim CPU) — full CPU-park, flags only. Per-FX `TrackFX_SetOffline` is the reclaimer's job (`fx_reclaim`),
not mix-removal-only. off this path entirely.
- **A snapshot is only ever taken from a chain no park has touched — BOTH halves - **EXPOSE, not refuse.** When no record exists for a track
of it.** The trust test reads the four driven flags AND the FX chain, and either (`model.snapshot(guid) == nullptr`) and the live chain already reads
reading parked alone is enough to refuse: each half is the only source of truth park-shaped (`chainReadsParked`, flags only — the FX half is excluded, see its
for its own half of the snapshot, so one of them lying poisons the whole record. own comment), parking now would adopt a park with no account behind it as the
The FX half asks "is ANY FX offline", not "every" — no read distinguishes a user's intent — an undo/redo or reload that dropped the snapshot, a hand/script
park's leftover from one the user set by hand, and only "any" errs toward edit, or simply a track the user keeps hidden. Instead the tool drives the
committing nothing false. An absent snapshot is NOT evidence of a clean chain — track visible/in-mix/FX-enabled, stores no snapshot, and reports it once per
a saved project, a hand-edited chain and an undo/redo can each present one; the changed set per project (`reportExposedTracks`/`describeExposedTracks`, which
Gotchas below enumerate the routes that still reach it. A park meeting that pair NAME the tracks — they were hidden from both the TCP and the mixer, so a bare
REFUSES: no flag write, no FX write, no snapshot, and a count points at nothing the user can go and find). The next apply sees an
console report NAMING the refused tracks (they are hidden from both the TCP and honest chain and parks/captures it normally — self-healing, no user action
the mixer, so a count points at nothing the user can go and find) plus the hand required. Daniel's ruling: expose a broken track rather than hide it forever.
recovery, printed once per changed refused set per project rather than on every Decision at `view_park`'s `parkTrack`.
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 half 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 - **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 value BEFORE parking; on toggle-back restore FROM the snapshot, never to a
hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag hardcoded "on." Round-trip (snapshot → park → restore) returns every driven flag
@@ -66,29 +60,20 @@ decide membership or mode rules.
active mode — stateful plugins (convolution, loaded samplers, tail-holding active mode — stateful plugins (convolution, loaded samplers, tail-holding
effects) re-initialize on return (load hitch, un-persisted internal state lost). effects) re-initialize on return (load hitch, un-persisted internal state lost).
Accepted cost of the CPU reclaim; surfaced at the toggle affordance (tooltip). Accepted cost of the CPU reclaim; surfaced at the toggle affordance (tooltip).
**The hitch sits on the switch's synchronous path, deliberately.** Per-FX The hitch no longer sits on the switch's synchronous path: per-FX
offline/online runs inside `applyMode`'s own undo block, between the flag offline/online runs on the background reclaimer (`fx_reclaim`), in ticks
writes and `Undo_EndBlock2`, so an action-driven switch does not return until bounded by `kOnlineBudgetMs`/`kOfflineBudgetMs`, off `applyMode`'s block
every plugin has moved. That cost is the price of the invariant below, and it entirely (measured: `TrackFX_SetOffline` was 1897ms of a 1942ms switch).
was accepted as such. - **ONE mode switch is ONE undo point.** The driven flags and the lane state
- **ONE mode switch is ONE undo point.** Everything the switch wrote into the written inside `applyMode`'s block roll back in a single Ctrl-Z. (The
project — the driven flags, every per-FX offline/online, the lane state — `view_state` ext-state write is NOT among them; it lands after the block, see
rolls back in a single Ctrl-Z. (The `view_state` ext-state write is NOT among Gotchas.) Per-FX offline/online moved off this block entirely, onto the
them; it lands after the block, see Gotchas.) What was MEASURED (before any reclaimer (`fx_reclaim`) — each write there runs inside its OWN discarded
fix): a `TrackFX_SetOffline` made outside any undo block mints ONE implicit block (`Undo_EndBlock2(proj, "", 0)`), so it mints nothing. DAW-VERIFIED: ten
undo point per call, and a small project showed 10 points for one switch — 1 unblocked `TrackFX_SetOffline` calls minted ten undo points; the same ten
for the switch itself and 9 for its FX. That measurement is the whole reason calls wrapped in `Undo_BeginBlock2`/`Undo_EndBlock2(proj, "", 0)` minted zero
the FX writes must sit inside a block. Which block is the design decision, and — confirming the discard-block premise the reclaimer's per-write block
it is settled: the SWITCH'S OWN. The undo surface (`fx_reclaim.cpp`'s `writeOffline`) rests on.
(`vendor/reaper-sdk/sdk/reaper_plugin_functions.h:7745-7822`) is
begin/end/state-change and nothing else — no append-to-point, no coalescing
flag, no undo-silent write — and a block cannot be held open across a return
to the message loop, so ANY deferral of the writes costs a second undo point.
Two points is a rejected design, not a lesser one: do not reintroduce a
deferred queue, an idle drain, or a second block. That an explicit block
suppresses the per-call implicit points is still the PREMISE the whole
arrangement rests on — INFERENCE from the measurement above, not observed;
`docs/VERIFICATION.md` §"Mode switching" is where it gets checked.
- **A reapply that wrote nothing leaves no undo point.** The load-tick reapply - **A reapply that wrote nothing leaves no undo point.** The load-tick reapply
re-plans a park for every inactive leaf, and over a project saved fully parked re-plans a park for every inactive leaf, and over a project saved fully parked
it finds every flag and every FX already where the plan wants them. The block it finds every flag and every FX already where the plan wants them. The block
@@ -97,20 +82,11 @@ decide membership or mode rules.
not cost the user a Ctrl-Z. A REAL switch always mints, written state or not: not cost the user a Ctrl-Z. A REAL switch always mints, written state or not:
it is an explicitly fired action and has to stay undoable even when the plan it is an explicitly fired action and has to stay undoable even when the plan
found nothing to write. found nothing to write.
- **Stated DEVIATION — the undo mask does not keep FX out of a real switch.** The - **A rapid A→B→A flip moves zero plugins.** The reclaimer only starts a park's
apply mask (`kApplyUndoMask`) drops `UNDO_STATE_FX` and ORs it back in when OFFLINE work once a track has read continuously parked for `kParkHoldMs`
`I_FXEN` or a per-FX offline state actually moved — which every park does. So (3000ms). A flip inside that window flips the flags twice but the debounce
any switch that parks at least one track still makes REAPER marshal the never matures, so the reclaimer finds every chain already where the record
project-wide FX chunk into its undo record. The saving is real only on a wants it and writes nothing.
reapply that wrote nothing and on a no-op switch. Narrowing it further would
mean not carrying the FX domain in the undo record at all, which would break
"one switch is one Ctrl-Z."
- **A rapid A→B→A flip pays two full park/restore cycles.** The retired intent
queue let an intent annihilate its own pending inverse, so a fast double-toggle
moved no plugins at all. Synchronous writes have no such window: the first
switch offlines the chain, the second brings it back, and both hitches are
paid. Accepted — the flip was never free (the flag writes always ran twice),
and the alternative costs the one-point invariant above.
- **Show-both semantics:** a per-track "pin visible across modes" flag re-enables - **Show-both semantics:** a per-track "pin visible across modes" flag re-enables
processing whenever shown. A show-both leaf appears in every mode's visible set processing whenever shown. A show-both leaf appears in every mode's visible set
and is never parked — its driven flags stay at snapshot/restored values, FX and is never parked — its driven flags stay at snapshot/restored values, FX
@@ -148,20 +124,19 @@ applies the resulting lane state to live tracks.
## Modules ## Modules
- `view` — Design View shell: runs the pure planner over the live folder tree and iterates its park/restore plans, applies managed-lane and parent-visibility writes, and owns the ONE undo block all of that rides (`applyMintsUndoPoint` decides whether it leaves a point). 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` — Design View shell: runs the pure planner over the live folder tree and iterates its park/restore plans, applies managed-lane and parent-visibility writes, and owns the ONE undo block all of that rides (`applyMintsUndoPoint` decides whether it leaves a point). 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`.** FX are NOT written here — see `fx_reclaim`.
- `view_fx_park` — the park surface: ONE track's whole move, both halves together. `parkTrack` is the trust 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), the pre-park snapshot when it is owed, then flags before FX; `restoreTrack` mirrors it, FX before flags, and consumes the snapshot. Each reports back what the caller's block needs (`TrackApplyResult`: did anything move, does the mask owe `UNDO_STATE_FX`, was the park refused, what could a restore not place). Also the `TrackFX_GetFXGUID` identity read all three of snapshot/park/restore address FX through, `trackFlagParm` — the ONE `Flag` → REAPER-parameter mapping they likewise share — the `writeIfChanged` read-before-write every driven parameter goes through, `trackStillLive` (a per-track `ValidatePtr2`, because a plugin unloading is assumed to pump the message loop), and the two console reports (`reportRefusedParks`, which names the tracks and holds the last-reported set so a reapply does not reprint it; `reportFxRestoreDrops`). **The snapshot is consumed where the restore RUNS** — from that line the flags are back at their captured values, and a model still describing the track as parked would let a later persist or reapply replan a restore over whatever the user changed since. - `view_park` — the park surface, FLAG DOMAIN ONLY. `parkTrack` captures the pre-park snapshot (flags AND the FX chain's identity-keyed offline state) when the model holds none, drives the four flags, and EXPOSEs a track that reads park-shaped with no record behind it rather than adopting it; `restoreTrack` writes the flags back from the snapshot and spends its flag half (`releaseSnapshotFlags`). Also `trackFlagParm` (the one `Flag` → REAPER-parameter mapping), `writeIfChanged` (the read-before-write every driven parameter goes through), `fxGuidString` (the one FX-identity read snapshot/reclaim both address FX through), `kViewUndoMask`, and `reportExposedTracks`/`describeExposedTracks` (named tracks, once per changed set per project).
- `view_lanes` — managed fixed lanes: `mintManagedLanes` splits multi-mode content onto per-mode lanes, `applyLaneOps` drives `C_LANEPLAYS` for the active mode, `reconcileManagedLanes` recovers ownership from durable `P_LANENAME` on project open. Manual lanes are never minted, driven, renamed or reassigned.
- `view_reconcile` — the repair pass for a project state REAPER reloaded under us: `noteAppliedState` records what an apply left driven; `reconcileAppliedFlags` diffs that fingerprint against the reloaded model and hands back any track the reload no longer records but still reads driven — the fix for `UNDO_STATE_TRACKCFG` not carrying `B_SHOWINTCP`/`B_SHOWINMIXER` (DAW-confirmed).
- `view_tracks` — the scaffolding every write pass sets up: `TrackHandles`/`TrackByGuid`, `readFolderEntries`/`indexHandles`, and `UiRefreshHold` (the `PreventUIRefresh` RAII pair every write phase runs under).
- `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`. - `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`.
- `fx_reclaim` — the FX convergence loop, off the switch's synchronous path (`TrackFX_SetOffline` was 1897ms of a measured 1942ms switch). `drainTick` moves one track's FX per step against `core/view/fx_state`'s derived work list (Online before Offline; an Offline item waits out `kParkHoldMs` so a fast A→B→A flip moves nothing), budgeted per tick (`kOnlineBudgetMs`/`kOfflineBudgetMs`) and gated off the transport for the Offline direction only. `FxReclaimQueue` owns the derived queue's lifetime across re-entrancy (a plugin unload is assumed to pump); each `writeOffline` runs inside its own discarded undo block, DAW-verified to mint nothing. `ReleaseFlush`/`fxReclaimFlushRelease` make a completed record's spend durable on the project's next save, never on the drain's own initiative.
## Gotchas ## Gotchas
- The pure mode model (`ViewModeModel`, membership, `reconcile(liveGuids)`, the - The pure mode model (`ViewModeModel`, membership, `reconcile(liveGuids)`, the
snapshot-based park/restore planner) lives in `core/view` — reference it, do not snapshot-based park/restore planner) lives in `core/view` — reference it, do not
duplicate its spec here. duplicate its spec here.
- **`TrackPlan::fxOffline` is read on the RESTORE side only.** `planToggle` builds
every park plan with `fxCount=0`, and this shell's park loop passes `tp.flags`
alone to `parkTrack`, which expands the real writes itself from
`TrackFX_GetCount` — so the park-side field is populated only by tests. A shell
change that starts reading it on the park path is reading an empty vector.
- The Two-canvas sub-phase (Phase D2/E)'s settled DAW-application rules - The Two-canvas sub-phase (Phase D2/E)'s settled DAW-application rules
(fixed-lane mechanics, mode-aware capture placement, hidden-AND-silenced) are (fixed-lane mechanics, mode-aware capture placement, hidden-AND-silenced) are
reflected in Invariants above; the membership/lane-ownership model concepts reflected in Invariants above; the membership/lane-ownership model concepts
@@ -172,37 +147,23 @@ applies the resulting lane state to live tracks.
under whatever mode id is active at that point, not the one the user undid back under whatever mode id is active at that point, not the one the user undid back
to. Pre-existing: `snapshots_` already carries this same model-vs-undo split; to. Pre-existing: `snapshots_` already carries this same model-vs-undo split;
the solo cache inherits it rather than introducing it. Not fixed here. the solo cache inherits it rather than introducing it. Not fixed here.
- **Which routes reach the no-snapshot-plus-parked-chain pair, re-derived for the - **Which routes reach the no-record-plus-parked-chain pair EXPOSE handles.**
one-point design.** Three of the routes that used to reach it were artifacts of A `view_state` that PARSED but carries no snapshot for the track — a snapshot
the retired deferral window, and are now NARROWED TO THE RE-ENTRANCY WINDOW
rather than gone: the flags-written-FX-not moment still exists inside
`parkTrack`, but nothing can observe it except code re-entered through the
pump premise below, and `applyMode` fails closed when re-entered
(`modeApplyInProgress`). The window that remains is what a call REACHABLE FROM
a pumped message loop but not routed through `applyMode` could see — no save,
no reload and no discard reaches it (`main.cpp`'s `OnTimer` defers
`session.poll()` itself, the one caller of `loadFromProject` a re-entered timer
could reach, under this same latch), and there is no queued intent for an undo
to drop. What survives:
- A `view_state` that PARSED but carries no snapshot for the track — a snapshot
`reconcile` pruned while its track was out of the live enumeration, then `reconcile` pruned while its track was out of the live enumeration, then
undo/redo-restored with the track but not the snapshot. (Asserted by the undo/redo-restored with the track but not the snapshot; a hand or script edit
branch that introduced the refusal; not re-observed since.) that leaves a chain park-shaped, including the likeliest real case, a track
- A hand or script edit that leaves a chain park-shaped — including the single with untouched flags and just ONE hand-offlined plugin (`chainReadsParked`
likeliest real case, a track with untouched flags and just ONE hand-offlined never reads the FX half at all, so this route is flags-only); or a track the
plugin (the FX half of the disjunction refuses alone; the flags never have to USER keeps hidden from both panels, out of the mix and FX-bypassed, which
read parked too). reads identically and is EXPOSEd too — no lost state there at all, which is
- A track the USER keeps hidden from both panels, out of the mix and why the report never asserts a cause. A REDO of a mode switch is a further,
FX-bypassed. It reads identically to both of the above and is refused too — hypothetical route — see the `view_state` gotcha below.
no lost state there at all, which is why the report asserts no cause. - **An UNDO of a mode switch no longer rolls back FX at all.** FX offline/online
- A REDO of a mode switch, hypothetically — the mechanism and its two moved off `applyMode`'s block entirely onto the reclaimer, so a Ctrl-Z restores
unverified links are in the `view_state` gotcha below, which owns that only the flags and lane state the block wrote — whatever the reclaimer had
question; do not re-derive it here. converged (or had not yet converged) survives the undo untouched. Whether this
reads as correct in the DAW is `[verify — DAW]`, `docs/VERIFICATION.md`
An UNDO of a mode switch (Ctrl-Z) is NOT a route, and no longer needs an §"Mode switching".
ordering premise to say so: flags and FX moved inside ONE block, so they roll
back together and no intermediate state exists for a park to misread. The redo
direction is not symmetric with it — see below.
- **The `view_state` write is NOT inside `applyMode`'s block.** `persistViewState` - **The `view_state` write is NOT inside `applyMode`'s block.** `persistViewState`
(`design_view_actions.cpp`) runs after `applyMode` has returned, so (`design_view_actions.cpp`) runs after `applyMode` has returned, so
`SetProjExtState` lands past `Undo_EndBlock2`. The UNDO direction still comes `SetProjExtState` lands past `Undo_EndBlock2`. The UNDO direction still comes
@@ -210,7 +171,7 @@ applies the resulting lane state to live tracks.
which is what a Ctrl-Z should restore — but by position, not by design. The which is what a Ctrl-Z should restore — but by position, not by design. The
REDO direction is what this leaves open, and the cost is worse than the REDO direction is what this leaves open, and the cost is worse than the
model-vs-undo split the solo-cache gotcha describes: **a redo can plausibly model-vs-undo split the solo-cache gotcha describes: **a redo can plausibly
produce a NEW refusal route.** As a HYPOTHESIS — the redo restores the parked produce a NEW EXPOSE route.** As a HYPOTHESIS — the redo restores the parked
flags AND the ext state as the point closed over it, i.e. the PRE-switch flags AND the ext state as the point closed over it, i.e. the PRE-switch
`view_state`, carrying no snapshot for those tracks (`UNDO_STATE_MISCCFG` `view_state`, carrying no snapshot for those tracks (`UNDO_STATE_MISCCFG`
covers extension state, `reaper_plugin.h:1544`). The `projectconfig` reload covers extension state, `reaper_plugin.h:1544`). The `projectconfig` reload
@@ -218,7 +179,8 @@ applies the resulting lane state to live tracks.
plans NEITHER a park (the track is a member of the restored active mode) nor a plans NEITHER a park (the track is a member of the restored active mode) nor a
restore (no snapshot to restore from) — verified in `planToggle`, which pushes restore (no snapshot to restore from) — verified in `planToggle`, which pushes
nothing at all for an active leaf without a snapshot. The tracks stay parked nothing at all for an active leaf without a snapshot. The tracks stay parked
with no snapshot, so the next real switch parks them and `decidePark` refuses. with no snapshot, so the next real switch parks them and `parkTrack` EXPOSEs
them instead.
TWO links are unverified and the route dies if either fails: (1) that the TWO links are unverified and the route dies if either fails: (1) that the
point's stored state is captured at `Undo_EndBlock2` and therefore excludes the point's stored state is captured at `Undo_EndBlock2` and therefore excludes the
`persistViewState` write that follows it — the SDK documents no capture-time `persistViewState` write that follows it — the SDK documents no capture-time
@@ -228,43 +190,39 @@ applies the resulting lane state to live tracks.
keeps its snapshots and the redo is consistent. `[verify — DAW]`. Do not "fix" keeps its snapshots and the redo is consistent. `[verify — DAW]`. Do not "fix"
it by moving `persistViewState` inside the block — it can open a modal Save-As; it by moving `persistViewState` inside the block — it can open a modal Save-As;
whether to split the persist is a separate decision. whether to split the persist is a separate decision.
- A DIFFERENT strand entirely, which the refusal report does NOT cover: an - A DIFFERENT strand entirely, which the EXPOSE report does NOT cover: an
absent or malformed `view_state` (`loadViewModel`, `ext_state_io.cpp`) is not absent or malformed `view_state` (`loadViewModel`, `ext_state_io.cpp`) is not
a parked-chain reading at all — it falls back to a DEFAULT model (active mode a parked-chain reading at all — it falls back to a DEFAULT model (active mode
Arrange, empty membership), so nothing is planned to park the track, Arrange, empty membership), so nothing is planned to park the track,
`decidePark` never runs, and a track saved parked stays stuck with no console `parkTrack` never runs, and a track saved parked stays stuck with no console
line whatsoever. line whatsoever.
- **`applyMode`'s block can be nested, and now carries FX writes while it is.** - **`applyMode` fails closed when re-entered — a model-consistency guard now,
`render_in_place` calls `applyMode` inside its own `Undo_BeginBlock2` pair, so not a pump-during-FX-writes one.** Nothing `applyMode` calls is known to pump
that verb's single `UNDO_STATE_ALL` point now absorbs the reapply's FX moves the message loop any more (the FX writes are gone from it), but a re-entrant
instead of a separate point landing after it — the better outcome, but it rests apply would still plan against the same model the outer call is iterating and
on REAPER coalescing nested blocks, which the SDK header does not document. consume records out from under it — `modeApplyInProgress`
The inverse is also open: a plugin unloading is ASSUMED to pump the message (`view.cpp`'s `ApplyLatch`) drops the re-entrant call rather than risk that. A
loop, so an action fired mid-apply could open its own block inside this one. mode switch or membership reapply fired while another apply is on the stack is
`applyMode` itself cannot be that action — it fails closed when re-entered DROPPED, not queued; the action callers just no-op, silently
(`modeApplyInProgress`, `view.cpp`'s `ApplyLatch`) — but nothing else is (`reportModeSwitchRefused` speaks only for the transport gate) and the user
latched, `mintManagedLanes` included, and it opens a block of its own. Both are re-fires. The one caller with nothing to fall back on is `main.cpp`'s load
glue, which tests `modeApplyInProgress` AND `fxReclaimDrainInProgress` before
spending its one-shot signal and retries on the next tick — the reclaimer's
own drain (`fx_reclaim`'s `DrainLatch`) is latched for the same reason over
the same window: `TrackFX_SetOffline` is what pumps now, and it holds the view
model the drain reads, not `applyMode`. `render_in_place`'s own `applyMode`
call is the exception to the drop's recovery path, not to the drop itself: its
refusal leaves the membership tags it already wrote in place, so the next
reapply — not a user re-fire of render-in-place — is what brings the tracks
into sync.
- `render_in_place` still calls `applyMode` inside its own `Undo_BeginBlock2`
pair for the reapply — one point rather than two for the flags and lane state
— but with no FX writes left inside `applyMode`'s block there is nothing left
to coalesce beyond that; the SDK's nested-block coalescing itself is still
`[verify — DAW]`. `[verify — DAW]`.
That same premise put a residual on track handles, now CLOSED by construction
rather than by a partial guard. The park and restore loops re-`ValidatePtr2`
per track (`trackStillLive`) because they are the ones interleaved with the FX
writes; the whole enumeration is then re-validated ONCE where those loops end,
so every pass after them — lane ops, parent visibility, the solo replay — sees
only handles REAPER still recognizes. A dead track's handle is nulled in place
rather than erased from the vector, which is what `view_solo`'s writers already
skip on.
The latch's own cost, stated: a mode switch or a membership reapply fired while
another apply is on the stack is DROPPED, not queued. The one caller with
nothing to fall back on is `main.cpp`'s load glue — it spends a one-shot signal
— so it tests `modeApplyInProgress` before consuming and retries next tick. The
action callers just no-op, silently (`reportModeSwitchRefused` speaks only for
the transport gate); the user re-fires. `render_in_place`'s own `applyMode` call
is the exception to that recovery path, not to the drop itself: its refusal
leaves the membership tags it already wrote in place, so the next reapply — not
a user re-fire of render-in-place — is what brings the tracks into sync.
- `fx_offline`'s identity keying (`TrackFX_GetFXGUID`) assumes the GUID stays - `fx_offline`'s identity keying (`TrackFX_GetFXGUID`) assumes the GUID stays
attached to its plugin across a chain mutation while parked. That is 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 `[verify — DAW]` (see `fxGuidString` in `view_park.cpp`) and SWS issue #802 is a
known reason it might not hold: `SNM_MoveOrRemoveTrackFX` reportedly leaves known reason it might not hold: `SNM_MoveOrRemoveTrackFX` reportedly leaves
the FXID lines behind on reorder rather than moving them with the plugin. If the FXID lines behind on reorder rather than moving them with the plugin. If
confirmed, an SWS-driven reorder of a parked track's chain — not a native confirmed, an SWS-driven reorder of a parked track's chain — not a native