Design View: drop the undo blocks around writes that mint nothing; the membership actions own theirs

Flag writes are silent from a timer, so a block around a reapply or the reconcile
repair could only manufacture a point. The real switch's span is unchanged.
This commit is contained in:
2026-08-07 14:41:17 -04:00
parent 67d9ec3da9
commit 39529a5ac4
8 changed files with 141 additions and 144 deletions
+40 -33
View File
@@ -64,22 +64,30 @@ decide membership or mode rules.
offline/online runs on the background reclaimer (`fx_reclaim`), in ticks
bounded by `kOnlineBudgetMs`/`kOfflineBudgetMs`, off `applyMode`'s block
entirely (measured: `TrackFX_SetOffline` was 1897ms of a 1942ms switch).
- **`TrackFX_SetOffline` is the ONLY write in this feature that mints an undo point,
and a block around anything else can only manufacture one.** DAW-MEASURED, and the
premise every decision below rests on: ten writes of the four driven flags
(`B_SHOWINTCP`, `B_SHOWINMIXER`, `B_MAINSEND`, `I_FXEN`) from a timer callback, with
no block and no undo call, via `SetMediaTrackInfo_Value` and again via
`GetSetMediaTrackInfo`, added ZERO entries to the Undo History. Ten
`TrackFX_SetOffline` calls from a timer minted TEN points unblocked, TEN under a
block per write, and ONE under a single block spanning all ten — block WIDTH is the
entire effect there. The `("", 0)` discard form does NOT discard outside an action,
it coalesces; it discards only from a hand-fired action. A block opened on one timer
callback and closed on a LATER one holds. So: a path that writes only flags opens NO
block (a repair or a reapply must not cost the user a Ctrl-Z), and a path whose act
IS the user's opens one to make that act undoable at all — which is why the three
membership-edit actions own their own (`design_view_actions`' `ActionUndoBlock`)
rather than leaning on a reapply that now mints nothing.
- **ONE mode switch is ONE undo point — the background FX parking included.** The
driven flags, the lane state, AND the per-FX offline/online the reclaimer writes
minutes-of-plugin-loads later all roll back in a single Ctrl-Z, because they share
ONE undo block: `applyMode` opens it (`fx_reclaim`'s `UndoSpan`) and a REAL switch
RETURNS WITH IT OPEN, for the reclaimer to close once the drain has converged. A
reapply closes its own before returning. This does not move an FX write back onto
the switch — the writes still run on the timer, off the 1897ms path.
DAW-MEASURED, and the reason the design is shaped this way: block WIDTH is the
entire effect. For ten `TrackFX_SetOffline` calls from a TIMER — unblocked, or
under a block per write, by every dispatch route — REAPER minted TEN points; under
ONE block spanning all ten it minted ONE. The `("", 0)` discard form does NOT
discard outside an action, it coalesces; it discards only from a hand-fired action.
A block opened on one timer callback and closed on a LATER one holds. Whether one
held from an ACTION into a later timer callback holds is the same property but is
`[verify — DAW]`; if it does not, the switch and its parking land as two points,
which is the intended degradation.
ONE undo block: `applyMode` opens it on a REAL SWITCH ONLY (`fx_reclaim`'s
`UndoSpan`) and RETURNS WITH IT OPEN, for the reclaimer to close once the drain has
converged. This does not move an FX write back onto the switch — the writes still
run on the timer, off the 1897ms path. Whether a block held from an ACTION into a
later timer callback holds is `[verify — DAW]`; if it does not, the switch and its
parking land as two points, which is the intended degradation.
- **No path may return past a terminating condition with the span open.** A leaked
block swallows every edit the user makes afterwards, indefinitely, so every
terminating path routes through ONE idempotent close (`fxReclaimEndUndoSpan`): the
@@ -129,16 +137,13 @@ decide membership or mode rules.
rather than resuming on the next tick. (That a revert leaves the view model stale at
all is the identity poll's defect, not the reclaimer's — this invariant only keeps the
reclaimer from acting on it.)
- **A reapply that wrote nothing leaves no undo point** — and a reapply never holds
the span open, so a project open, a membership edit and `render_in_place`'s inner
reapply all keep the block shape they had. The load-tick reapply
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
then closes on the `("", 0)` discard form (`view.h`'s `applyMintsUndoPoint`;
`mintManagedLanes`' no-op path uses the same idiom) — opening a project must
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
found nothing to write.
- **A reapply opens no undo block at all, so it can leave no point** — whether it
wrote or not, and whoever drove it: a project open, a membership edit,
`render_in_place`'s inner reapply. Nothing a reapply writes mints unaided (see the
measurement above), so a block there could only manufacture the Ctrl-Z opening a
project must not cost. A caller whose own act needs to stay undoable spans it
itself. A REAL switch still always mints, written state or not: it is an explicitly
fired action and has to stay undoable even when the plan found nothing to write.
- **A rapid A→B→A flip moves zero plugins.** The reclaimer only starts a park's
OFFLINE work once a track has read continuously parked for `kParkHoldMs`
(3000ms). A flip inside that window flips the flags twice but the debounce
@@ -181,10 +186,10 @@ applies the resulting lane state to live tracks.
## 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 OPENS the ONE undo span all of that rides (`applyMintsUndoPoint` decides whether it leaves a point; on a real switch `fx_reclaim` is what closes it). 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` — 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 on a REAL SWITCH opens the ONE undo span all of that rides (`fx_reclaim` is what closes it; a reapply opens none). 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_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_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). It opens no undo block: it runs in the wake of a Ctrl-Z, and its writes are flags only.
- `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`.
- `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). `UndoSpan` owns the ONE undo block the whole drain writes under — opened by `applyMode` on a real switch, else lazily by the first `writeOffline`, and closed through the single idempotent `fxReclaimEndUndoSpan` that every terminating path routes through (`spanEndsAfter` decides which ticks terminate; `fxReclaimAbandonForStateLoad` is the state-load terminator, which also stops the drain). `ReleaseFlush`/`fxReclaimFlushRelease` make a completed record's spend durable on the project's next save, never on the drain's own initiative. `fxReclaimActivity` reports WHICH DIRECTION the drain is moving for an indicator to paint — Idle whenever nothing may move, so a debounced or transport-held offline item never names a direction nothing is travelling in; undirected `fxReclaimPending` stays the timer's one idle test and must not drive a UI (it is true throughout the debounce, when only the OPPOSITE direction is eligible).
@@ -231,9 +236,10 @@ applies the resulting lane state to live tracks.
unsaved project — land inside the block after all. That is a consequence of the
span, not a decision made here; it does not resolve the redo hypothesis below,
which turns on capture-time semantics the SDK does not document. `[verify — DAW]`
that a Save-As dialog opening under an open undo block is harmless. The rest of
this gotcha describes the reapply path, where nothing changed. The UNDO direction
still comes
that a Save-As dialog opening under an open undo block is harmless. On the reapply
path the write stays OUTSIDE every block, as it always did: the reapply opens none,
and a membership action's own `ActionUndoBlock` closes before `persistViewState` for
exactly this modal-Save-As reason. The UNDO direction still comes
out right — the point closed over the ext state as it stood BEFORE the switch,
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
@@ -282,11 +288,12 @@ applies the resulting lane state to live tracks.
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]`.
- `render_in_place` calls `applyMode` inside its own `Undo_BeginBlock2` pair, and
that block is now the only one in play on that path — the reapply opens none of
its own, so nothing nests and the SDK's undocumented nested-block coalescing
stopped being load-bearing here. The same holds for the membership actions'
`ActionUndoBlock`; `doMoveItems` is the one that still nests, over
`mintManagedLanes`.
- `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_park.cpp`) and SWS issue #802 is a