Close the collapse's two dropped guarantees: latch applyMode against re-entrancy, pin its project once; correct three overclaiming doc lines
This commit is contained in:
@@ -392,6 +392,8 @@ cross-reference.
|
|||||||
| **Ω-F5** | **The mode-switch symptom is both entry surfaces**, and it is *"nothing for 2 seconds, then everything switches"* — nothing renders until the whole synchronous body completes | Ω-W1-T1, Ω-W1-T2 |
|
| **Ω-F5** | **The mode-switch symptom is both entry surfaces**, and it is *"nothing for 2 seconds, then everything switches"* — nothing renders until the whole synchronous body completes | Ω-W1-T1, Ω-W1-T2 |
|
||||||
| **Ω-F6** | **ACCEPTED, and it is a spec amendment: separate the FX parking's UI half from its processing half.** Visibility / routing / FX-enable apply **synchronously**; per-FX offline/online **defers to a later idle tick** — *"the switch doesn't happen during playback anyway… the UI is the priority when toggling modes"* | Ω-W1-T1 |
|
| **Ω-F6** | **ACCEPTED, and it is a spec amendment: separate the FX parking's UI half from its processing half.** Visibility / routing / FX-enable apply **synchronously**; per-FX offline/online **defers to a later idle tick** — *"the switch doesn't happen during playback anyway… the UI is the priority when toggling modes"* | Ω-W1-T1 |
|
||||||
|
|
||||||
|
**SUPERSEDED — Ω-F6 and every deferred-park instruction under Ω-W1-T1 below (the new park-queue TU, its `OnTimer` drain, its `target_sources` line).** The deferral cost a SECOND undo point and could not be made to cost one: REAPER's undo surface has no append-to-point and no block held across a return to the message loop, so per-FX offline/online now runs inline in `applyMode`'s own block and the queue is deleted. The hitch is back on the switch's synchronous path, deliberately. The rest of Ω-W1-T1 (the honest undo mask, compare-before-write, `PreventUIRefresh`, the `resolve` hash map) stands as written.
|
||||||
|
|
||||||
**Verified by Daniel, not a fork:** the deck knob-spacing defect is in the **Trigger** face.
|
**Verified by Daniel, not a fork:** the deck knob-spacing defect is in the **Trigger** face.
|
||||||
**Gate is correct and must stay pixel-identical** — an acceptance criterion of Ω-W1-T4, not a
|
**Gate is correct and must stay pixel-identical** — an acceptance criterion of Ω-W1-T4, not a
|
||||||
hope.
|
hope.
|
||||||
|
|||||||
@@ -49,10 +49,12 @@ Checks for Θ, Ξ, Ψ, Ε, Ρ, Γ, and Ω work that no unit test can close. Buil
|
|||||||
- [ ] 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: 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]` **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 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]` **Where the `view_state` lands on undo AND redo.** `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. 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. 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 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]` **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, or let `OnTimer` -> `bankPanelRefresh` -> `mintManagedLanes` run, WHILE a switch's plugin loads are in flight (a heavy chain widens the window), and check the history for a split or misordered point. Nothing guards (b) (`src/shell/view/CLAUDE.md` §Gotchas)
|
- [ ] `[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]` **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→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→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]` 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)
|
||||||
|
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model
|
|||||||
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
||||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
|
|
||||||
# The deferred FX-park queue's re-entrancy rule is pure (header-inline, no REAPER
|
# The park surface's decisions are pure (header-inline, no REAPER types), so they are
|
||||||
# types), so it is CTest-covered like a core/ module. Declared here rather than in a
|
# CTest-covered like a core/ module: the snapshot trust test, the refusal message and
|
||||||
# src/shell/view/CMakeLists.txt because that directory deliberately has none — its
|
# its once-per-changed-set gate, and the apply's undo-point fold. Declared here rather
|
||||||
# TUs are compiled into this target directly. view_mode_model is linked for the plan
|
# than in a src/shell/view/CMakeLists.txt because that directory deliberately has none
|
||||||
# builders the pure half is a contract WITH — makeRestorePlan's round trip through
|
# — its TUs are compiled into this target directly. view_mode_model is linked for
|
||||||
# the cancel path, and makeParkPlan's flag set behind the trust test; pinning either
|
# makeParkPlan, the flag set the trust test folds over; pinning that against hand-built
|
||||||
# against hand-built ops would not catch a change to the other half.
|
# ops would not catch a change to the planner.
|
||||||
reasampler_test(view_fx_park LINK fx_offline view_mode_model)
|
reasampler_test(view_fx_park LINK fx_offline view_mode_model)
|
||||||
|
|
||||||
# Bank-package import: the promptless verb plus its action skin. Kept as its own
|
# Bank-package import: the promptless verb plus its action skin. Kept as its own
|
||||||
|
|||||||
+5
-1
@@ -189,7 +189,11 @@ static void OnTimer()
|
|||||||
// signal re-arms the bank panel's new-content detector — notified BEFORE the
|
// signal re-arms the bank panel's new-content detector — notified BEFORE the
|
||||||
// reapply so re-arm and model restore ride the one load event (otherwise
|
// reapply so re-arm and model restore ride the one load event (otherwise
|
||||||
// pre-existing tracks can be mis-detected as "new" and mass-tagged).
|
// pre-existing tracks can be mis-detected as "new" and mass-tagged).
|
||||||
if (g_session.consumeLoadSignal()) {
|
// modeApplyInProgress is tested BEFORE the consume, not after the applyMode call:
|
||||||
|
// the load signal is one-shot, a re-entered applyMode fails closed, and a reapply
|
||||||
|
// dropped here is the saved mode never applied at all. Holding the whole block
|
||||||
|
// back one tick also keeps the panel re-arm ahead of the reapply.
|
||||||
|
if (!reasampler::modeApplyInProgress() && g_session.consumeLoadSignal()) {
|
||||||
reasampler::bankPanelNotifyProjectLoaded();
|
reasampler::bankPanelNotifyProjectLoaded();
|
||||||
// Reconcile lane ownership against the live project's lanes (P_LANENAME,
|
// Reconcile lane ownership against the live project's lanes (P_LANENAME,
|
||||||
// the cross-session source of truth) BEFORE reapplying visibility. Never
|
// the cross-session source of truth) BEFORE reapplying visibility. Never
|
||||||
|
|||||||
+57
-21
@@ -157,6 +157,11 @@ applies the resulting lane state to live tracks.
|
|||||||
- 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
|
||||||
@@ -169,10 +174,14 @@ applies the resulting lane state to live tracks.
|
|||||||
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-snapshot-plus-parked-chain pair, re-derived for the
|
||||||
one-point design.** Three of the routes that used to reach it were artifacts of
|
one-point design.** Three of the routes that used to reach it were artifacts of
|
||||||
the retired deferral window and are GONE BY CONSTRUCTION: there is no longer a
|
the retired deferral window, and are now NARROWED TO THE RE-ENTRANCY WINDOW
|
||||||
moment in which a track's flags have been written but its FX have not, so no
|
rather than gone: the flags-written-FX-not moment still exists inside
|
||||||
save, no reload and no discard can land between the two halves, and there is no
|
`parkTrack`, but nothing can observe it except code re-entered through the
|
||||||
queued intent for an undo to drop. What survives:
|
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, 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
|
- 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. (Asserted by the
|
||||||
@@ -184,21 +193,39 @@ applies the resulting lane state to live tracks.
|
|||||||
- A track the USER keeps hidden from both panels, out of the mix and
|
- A track the USER keeps hidden from both panels, out of the mix and
|
||||||
FX-bypassed. It reads identically to both of the above and is refused too —
|
FX-bypassed. It reads identically to both of the above and is refused too —
|
||||||
no lost state there at all, which is why the report asserts no cause.
|
no lost state there at all, which is why the report asserts no cause.
|
||||||
|
- A REDO of a mode switch, hypothetically — the mechanism and its two
|
||||||
|
unverified links are in the `view_state` gotcha below, which owns that
|
||||||
|
question; do not re-derive it here.
|
||||||
|
|
||||||
An undo of a mode switch (Ctrl-Z) is NOT a route, and no longer needs an
|
An UNDO of a mode switch (Ctrl-Z) is NOT a route, and no longer needs an
|
||||||
ordering premise to say so: flags and FX moved inside ONE block, so they roll
|
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.
|
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
|
||||||
out right — the point closed over the ext state as it stood BEFORE the switch,
|
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
|
which is what a Ctrl-Z should restore — but by position, not by design. The
|
||||||
REDO direction is what this leaves open: the point never saw the new
|
REDO direction is what this leaves open, and the cost is worse than the
|
||||||
`view_state`, so a redo may re-park the tracks while restoring the model that
|
model-vs-undo split the solo-cache gotcha describes: **a redo can plausibly
|
||||||
describes the mode the user undid back to. `[verify — DAW]`; if it does bite,
|
produce a NEW refusal route.** As a HYPOTHESIS — the redo restores the parked
|
||||||
the result is the pre-existing model-vs-undo split the solo-cache gotcha above
|
flags AND the ext state as the point closed over it, i.e. the PRE-switch
|
||||||
already describes, not a new refusal route. Do not "fix" it by moving
|
`view_state`, carrying no snapshot for those tracks (`UNDO_STATE_MISCCFG`
|
||||||
`persistViewState` inside the block — it can open a modal Save-As.
|
covers extension state, `reaper_plugin.h:1544`). The `projectconfig` reload
|
||||||
|
then rebuilds the model from that stale value, and the reapply it triggers
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
`persistViewState` write that follows it — the SDK documents no capture-time
|
||||||
|
semantics for the state mask; (2) that a redo fires `BeginLoadProjectState`
|
||||||
|
with `isUndo == true` so the session really does reload the stale ext state
|
||||||
|
(asserted in `main.cpp`, never observed). If (2) is false the in-memory model
|
||||||
|
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;
|
||||||
|
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 refusal 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
|
||||||
@@ -212,15 +239,24 @@ applies the resulting lane state to live tracks.
|
|||||||
on REAPER coalescing nested blocks, which the SDK header does not document.
|
on REAPER coalescing nested blocks, which the SDK header does not document.
|
||||||
The inverse is also open: a plugin unloading is ASSUMED to pump the message
|
The inverse is also open: a plugin unloading is ASSUMED to pump the message
|
||||||
loop, so an action fired mid-apply could open its own block inside this one.
|
loop, so an action fired mid-apply could open its own block inside this one.
|
||||||
Nothing guards against that (the retired queue's `g_draining` never did either
|
`applyMode` itself cannot be that action — it fails closed when re-entered
|
||||||
— it only stopped a second drain). Both are `[verify — DAW]`.
|
(`modeApplyInProgress`, `view.cpp`'s `ApplyLatch`) — but nothing else is
|
||||||
That same premise leaves a STATED RESIDUAL on track handles. The park and
|
latched, `mintManagedLanes` included, and it opens a block of its own. Both are
|
||||||
restore loops re-`ValidatePtr2` per track (`trackStillLive`), because they are
|
`[verify — DAW]`.
|
||||||
the ones interleaved with the FX writes. The passes that run AFTER them — lane
|
That same premise put a residual on track handles, now CLOSED by construction
|
||||||
ops, parent visibility, the solo replay — still use handles resolved before the
|
rather than by a partial guard. The park and restore loops re-`ValidatePtr2`
|
||||||
first FX write, so a track deleted mid-apply would reach them stale. Not closed:
|
per track (`trackStillLive`) because they are the ones interleaved with the FX
|
||||||
the premise it depends on is itself unobserved, and a partial guard covering
|
writes; the whole enumeration is then re-validated ONCE where those loops end,
|
||||||
three of those four passes would read as complete.
|
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.
|
||||||
- `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_fx_park.cpp`) and SWS issue #802 is a
|
||||||
|
|||||||
+46
-16
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
#define REAPERAPI_WANT_CountTracks
|
#define REAPERAPI_WANT_CountTracks
|
||||||
|
#define REAPERAPI_WANT_EnumProjects
|
||||||
#define REAPERAPI_WANT_GetPlayStateEx
|
#define REAPERAPI_WANT_GetPlayStateEx
|
||||||
#define REAPERAPI_WANT_GetTrack
|
#define REAPERAPI_WANT_GetTrack
|
||||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||||
#define REAPERAPI_WANT_UpdateArrange
|
#define REAPERAPI_WANT_UpdateArrange
|
||||||
#define REAPERAPI_WANT_UpdateTimeline
|
#define REAPERAPI_WANT_UpdateTimeline
|
||||||
|
#define REAPERAPI_WANT_ValidatePtr2
|
||||||
// Lane minting: item-side lane reads/writes assigning each item to its mode's lane.
|
// Lane minting: item-side lane reads/writes assigning each item to its mode's lane.
|
||||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||||
@@ -85,6 +87,19 @@ struct UiRefreshHold {
|
|||||||
UiRefreshHold& operator=(const UiRefreshHold&) = delete;
|
UiRefreshHold& operator=(const UiRefreshHold&) = delete;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// applyMode is reachable from the load tick, the actions and render_in_place, and its
|
||||||
|
// TrackFX_SetOffline calls are assumed to pump the message loop — so any of them can
|
||||||
|
// re-enter it mid-apply, planning against the same model the outer loop is iterating.
|
||||||
|
// An inner restoreTrack consuming a snapshot the outer sits mid-park over leaves that
|
||||||
|
// track flags-restored, chain-offline and snapshotless: refused forever. Fail closed.
|
||||||
|
bool g_applying = false;
|
||||||
|
struct ApplyLatch {
|
||||||
|
ApplyLatch() { g_applying = true; }
|
||||||
|
~ApplyLatch() { g_applying = false; }
|
||||||
|
ApplyLatch(const ApplyLatch&) = delete;
|
||||||
|
ApplyLatch& operator=(const ApplyLatch&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
||||||
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
||||||
// but displays as non-fixed-lane).
|
// but displays as non-fixed-lane).
|
||||||
@@ -147,11 +162,6 @@ MediaTrack* resolve(const TrackByGuid& byGuid, const std::string& guid) {
|
|||||||
return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped
|
return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped
|
||||||
}
|
}
|
||||||
|
|
||||||
// Managed-lane application: LanePlayOps are keyed by the lane's DURABLE name but
|
|
||||||
// C_LANEPLAYS:N by current ordinal, which renumbers on reorder — so every write
|
|
||||||
// re-resolves durable key -> ordinal first, and a lane whose name lacks the
|
|
||||||
// managed prefix never enters the map and so can never be driven.
|
|
||||||
|
|
||||||
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
||||||
// unavailable (non-fixed-lane track).
|
// unavailable (non-fixed-lane track).
|
||||||
std::string laneName(MediaTrack* tr, int laneIdx) {
|
std::string laneName(MediaTrack* tr, int laneIdx) {
|
||||||
@@ -231,9 +241,6 @@ LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Managed-lane minting: the DECISION (which tracks split, which lanes, which item
|
|
||||||
// goes where) is planLaneMinting's; this shell only reads, calls and applies.
|
|
||||||
|
|
||||||
// One pass, so the assign loop needs no per-item re-scan.
|
// One pass, so the assign loop needs no per-item re-scan.
|
||||||
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||||
std::map<std::string, MediaItem*> byGuid;
|
std::map<std::string, MediaItem*> byGuid;
|
||||||
@@ -367,11 +374,22 @@ bool transportBlocksModeSwitch(ReaProject* proj) {
|
|||||||
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
|
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool modeApplyInProgress() { return g_applying; }
|
||||||
|
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||||
|
if (g_applying) return false; // see ApplyLatch; same fail-closed shape as the guards below
|
||||||
|
ApplyLatch latch;
|
||||||
|
|
||||||
if (!model.modes().contains(targetModeId)) {
|
if (!model.modes().contains(targetModeId)) {
|
||||||
return false; // reject before touching the project — no partial apply
|
return false; // reject before touching the project — no partial apply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pinned ONCE: begin and end sit a whole apply apart with the pump premise between
|
||||||
|
// them, so resolving "current project" independently at each would let a tab switch
|
||||||
|
// mid-apply open the block on one project and close it on another.
|
||||||
|
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||||
|
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return false;
|
||||||
|
|
||||||
// The discriminator behind both halves of the contract in view.h. A gate placed
|
// The discriminator behind both halves of the contract in view.h. A gate placed
|
||||||
// unconditionally here would break tagging and the project-load reapply during
|
// unconditionally here would break tagging and the project-load reapply during
|
||||||
// playback; a solo round on every reapply would flicker the user's solo on every
|
// playback; a solo round on every reapply would flicker the user's solo on every
|
||||||
@@ -379,13 +397,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
const std::string outgoingModeId = model.activeModeId();
|
const std::string outgoingModeId = model.activeModeId();
|
||||||
const bool realSwitch = targetModeId != outgoingModeId;
|
const bool realSwitch = targetModeId != outgoingModeId;
|
||||||
|
|
||||||
if (realSwitch && transportBlocksModeSwitch(proj)) {
|
if (realSwitch && transportBlocksModeSwitch(project)) {
|
||||||
return false; // same fail-closed shape as the mode-exists guard above
|
return false; // same fail-closed shape as the mode-exists guard above
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackHandles handleByGuid;
|
TrackHandles handleByGuid;
|
||||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
std::vector<TrackFolderEntry> entries = readFolderEntries(project, handleByGuid);
|
||||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||||
FolderTree tree = buildFolderTree(entries);
|
FolderTree tree = buildFolderTree(entries);
|
||||||
|
|
||||||
// Prune snapshots for tracks no longer in the live enumeration before
|
// Prune snapshots for tracks no longer in the live enumeration before
|
||||||
@@ -417,7 +435,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
FxRestoreDrops fxDrops;
|
FxRestoreDrops fxDrops;
|
||||||
int fxDropTracks = 0;
|
int fxDropTracks = 0;
|
||||||
|
|
||||||
Undo_BeginBlock2(proj);
|
Undo_BeginBlock2(project);
|
||||||
{
|
{
|
||||||
UiRefreshHold uiHold; // every write below lands with the TCP/MCP frozen
|
UiRefreshHold uiHold; // every write below lands with the TCP/MCP frozen
|
||||||
|
|
||||||
@@ -438,7 +456,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
// Re-validated per track, not once at resolve: offlining a plugin is
|
// Re-validated per track, not once at resolve: offlining a plugin is
|
||||||
// ASSUMED to pump the message loop, so a track deleted during an
|
// ASSUMED to pump the message loop, so a track deleted during an
|
||||||
// earlier iteration's FX writes would leave this handle dangling.
|
// earlier iteration's FX writes would leave this handle dangling.
|
||||||
if (!tr || !trackStillLive(proj, tr)) continue; // stale/deleted GUID — prune
|
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
||||||
|
|
||||||
const TrackApplyResult r = parkTrack(model, guid, tr, tp.flags);
|
const TrackApplyResult r = parkTrack(model, guid, tr, tp.flags);
|
||||||
if (r.refused) { refusedParkNames.push_back(trackDisplayName(tr)); continue; }
|
if (r.refused) { refusedParkNames.push_back(trackDisplayName(tr)); continue; }
|
||||||
@@ -450,7 +468,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
if (tp.flags.empty()) continue;
|
if (tp.flags.empty()) continue;
|
||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||||
if (!tr || !trackStillLive(proj, tr)) continue; // stale/deleted GUID — prune
|
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
||||||
|
|
||||||
const TrackApplyResult r = restoreTrack(model, guid, tr, tp.fxOffline, tp.flags);
|
const TrackApplyResult r = restoreTrack(model, guid, tr, tp.fxOffline, tp.flags);
|
||||||
if (r.wrote) wrote = true;
|
if (r.wrote) wrote = true;
|
||||||
@@ -458,6 +476,16 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The FX writes above are the only thing here assumed to pump the message
|
||||||
|
// loop, so this is the one point a handle taken before them can have died.
|
||||||
|
// Re-validated for the whole enumeration rather than per consumer: lane ops,
|
||||||
|
// parent visibility and the solo replay all write through pre-FX handles.
|
||||||
|
for (auto& kv : handleByGuid) {
|
||||||
|
if (trackStillLive(project, kv.second)) continue;
|
||||||
|
trackByGuid.erase(kv.first);
|
||||||
|
kv.second = nullptr; // view_solo's writers already skip a null handle
|
||||||
|
}
|
||||||
|
|
||||||
// MANAGED LANES: the active mode's lane plays+shows, every other managed
|
// MANAGED LANES: the active mode's lane plays+shows, every other managed
|
||||||
// lane is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
// lane is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
||||||
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
||||||
@@ -502,11 +530,11 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
if (laneModeChanged) UpdateTimeline();
|
if (laneModeChanged) UpdateTimeline();
|
||||||
|
|
||||||
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
||||||
Undo_EndBlock2(proj, mint ? undoLabel.c_str() : "", mint ? undoMask : 0);
|
Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? undoMask : 0);
|
||||||
|
|
||||||
// After the undo block and the UI hold: a report is not a project write, and
|
// After the undo block and the UI hold: a report is not a project write, and
|
||||||
// it must not join what a Ctrl-Z rolls back.
|
// it must not join what a Ctrl-Z rolls back.
|
||||||
reportRefusedParks(proj, refusedParkNames);
|
reportRefusedParks(project, refusedParkNames);
|
||||||
reportFxRestoreDrops(fxDrops, fxDropTracks);
|
reportFxRestoreDrops(fxDrops, fxDropTracks);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -524,6 +552,8 @@ bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
|||||||
|
|
||||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||||
|
|
||||||
|
// `proj` deliberately NOT pinned the way applyMode pins it: nothing between this
|
||||||
|
// begin and its end pumps the message loop, so no tab switch can land between them.
|
||||||
Undo_BeginBlock2(proj);
|
Undo_BeginBlock2(proj);
|
||||||
const bool changed = applyMintPlan(model, plan, trackByGuid);
|
const bool changed = applyMintPlan(model, plan, trackByGuid);
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -40,15 +40,22 @@ inline bool applyMintsUndoPoint(bool realSwitch, bool wroteAnything) {
|
|||||||
return realSwitch || wroteAnything;
|
return realSwitch || wroteAnything;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True while an applyMode call is on the stack. applyMode fails closed when re-entered
|
||||||
|
// (its FX writes are assumed to pump the message loop, so a timer tick or an action can
|
||||||
|
// land inside one), and this lets a caller with no fallback test BEFORE it spends a
|
||||||
|
// one-shot signal on a call that would be refused — see main.cpp's load glue.
|
||||||
|
bool modeApplyInProgress();
|
||||||
|
|
||||||
// Snapshots each about-to-park track's flags into `model`, caches/clears the
|
// Snapshots each about-to-park track's flags into `model`, caches/clears the
|
||||||
// outgoing mode's solo state and replays the incoming mode's, runs planToggle,
|
// outgoing mode's solo state and replays the incoming mode's, runs planToggle,
|
||||||
// applies park/restore writes — flags AND per-FX offline, synchronously — plus
|
// applies park/restore writes — flags AND per-FX offline, synchronously — plus
|
||||||
// parent visibility flags, then sets the active mode. ONE switch is ONE undo
|
// parent visibility flags, then sets the active mode. ONE switch is ONE undo
|
||||||
// point; everything it moved rolls back in a single Ctrl-Z. Returns false (no
|
// point; everything it moved rolls back in a single Ctrl-Z. Returns false (no
|
||||||
// mutation) if `targetModeId` isn't registered, or if this is a real switch
|
// mutation) if `targetModeId` isn't registered, if this is a real switch
|
||||||
// (target != active) while the transport is running. A reapply (target ==
|
// (target != active) while the transport is running, if it is re-entered
|
||||||
|
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
|
||||||
// active) is never gated and never touches solo. `proj` == nullptr means the
|
// active) is never gated and never touches solo. `proj` == nullptr means the
|
||||||
// current project.
|
// current project, resolved ONCE and used for every call this apply makes.
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
||||||
|
|
||||||
// Splits any track visible in more than one mode while carrying its own media
|
// Splits any track visible in more than one mode while carrying its own media
|
||||||
|
|||||||
@@ -206,10 +206,11 @@ TrackApplyResult restoreTrack(ViewModeModel& model, const std::string& guid, Med
|
|||||||
|
|
||||||
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
||||||
|
|
||||||
// Unconditional, even when every write above was elided: the snapshot is
|
// Unconditional, even when every write above was elided: the snapshot is consumed
|
||||||
// gone, so this apply is not repeatable and must not close on the discard
|
// here, so this apply changed state and is not repeatable — and `wrote`, whose one
|
||||||
// form. Model state, not project state — whether an undo actually puts the
|
// reader asks exactly that, would be a lie at false. NOT a claim about what the
|
||||||
// snapshot back is the ext-state question in src/shell/view/CLAUDE.md.
|
// undo record holds: the block never carries the snapshot either way, that being
|
||||||
|
// model state persisted after Undo_EndBlock2 (src/shell/view/CLAUDE.md).
|
||||||
model.clearSnapshot(guid);
|
model.clearSnapshot(guid);
|
||||||
out.wrote = true;
|
out.wrote = true;
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Reference in New Issue
Block a user