view: assert the FX-park coalescing delay's debounce; correct four overclaiming doc/comment claims

Extracts parkReadyAt/parkIsReady as a tested pure fold per PLAN.md's phase
criterion; the rest is wording fixes — hitch bound, hazard width, forced-drain
scope, progressive CPU reclaim.
This commit is contained in:
2026-08-03 15:50:07 -04:00
parent 6e2128e937
commit 761125d0fe
4 changed files with 114 additions and 69 deletions
+26 -11
View File
@@ -57,17 +57,32 @@ decide membership or mode rules.
`[verify — DAW]`. **The drain is SPLIT BY INTENT KIND, because only one kind is
unsafe to persist over.** A pending RESTORE is: flags restored, snapshot already
dropped, FX still offline — a save inside that window records offline FX beside a
model that can no longer replan them, unrecoverable on reopen. So any path that
serializes the view model drains every restore synchronously first
(`persistViewState`, `render_in_place`), and an action-driven switch still pays
the RELOAD hitch before it returns — its repaint and undo block have both closed
by then, which is what the deferral was for. Any new caller that reapplies a mode
and then persists inherits this obligation. A pending PARK is safe to save over:
flags parked, snapshot stored, FX still online, so a reopen replans the park and
the drain offlines it — it converges. Parks therefore go lazy, ONE FX per idle
tick behind a one-second coalescing delay, so the unload half never blocks the
UI at all. The delay is also the width of the live-enumeration hazard at
`tickDeferredFxParks` — do not lengthen it casually.
model that can no longer replan them, unrecoverable on reopen. `persistViewState`
and `render_in_place` are the only two paths that force the restore drain before
they serialize; roughly ten other `saveToActiveProject` callers (`ingest.cpp`,
`bank_ops.cpp`, `bake_land.cpp`, `capture_batch.cpp`, `capture_orchestrator.cpp`,
`realtime_lifecycle.cpp`, `panel_input.cpp`) persist without forcing it first — a
pre-existing, roughly one-tick-wide gap (the idle tick drains restores too),
not known to be exploitable in practice. A new caller that reapplies a mode and
then persists inherits the forced-drain obligation; one that merely persists does
not. `drainDeferredFxRestores` is also a silent no-op while a drain is already in
flight (`g_draining`), so a restore forced from inside another drain's
message-pump window can itself be skipped — same order of window, same
pre-existing status. An action-driven switch still pays the RELOAD hitch before
it returns — its repaint and undo block have both closed by then, which is what
the deferral was for. A pending PARK is safe to save over: flags parked, snapshot
stored, FX still online, so a reopen replans the park and the drain offlines it —
it converges. Parks therefore go lazy, ONE FX per idle tick behind a one-second
coalescing delay, which bounds each tick's hitch to one FX's unload rather than
eliminating it — see `view_fx_park.h` for why per-track chunking would not help.
CPU reclaim is therefore PROGRESSIVE, completing over FX-count ticks rather than
in one step: a user watching the CPU meter after a switch sees a ramp, not a
drop — in tension with `design-view.md`'s full-CPU-park rationale ("a heavy FX
bench you don't want taxing the CPU while you arrange"). The delay is only ONE term
of the live-enumeration hazard's width: `firstOnlineFx` re-enumerates the chain
every tick, not just the first, so the actual exposure is delay + (FX count ×
tick interval), and for a large chain the second term dominates — see
`tickDeferredFxParks`. Do not lengthen the delay casually.
`[verify — DAW]` whether the switch's `UpdateArrange` paints before the first
drain tick: nothing pumps the message loop between them, so "the switch paints
first" is an assumption, not an SDK guarantee.