Guard poll() against a mode-apply mid-flight; two doc corrections

Extends the OnTimer guard to skip session.poll() itself while modeApplyInProgress(), preventing an undo/redo or project-switch reload from replacing the view model under an in-flight applyMode.
This commit is contained in:
2026-08-06 05:23:58 -04:00
parent 2269fd80b6
commit ab14ae769f
4 changed files with 16 additions and 4 deletions
+7 -1
View File
@@ -180,7 +180,13 @@ static void OnTimer()
// the idle fast-path is a SINGLE POINTER TEST — drive only when a capture is live.
if (capture::g_rtCapture) capture::DriveRealtimeCapture(g_session);
g_session.poll();
// poll()'s undo/redo-reload and project-switch Load branches call
// loadFromProject, replacing g_session.view() wholesale — deferred here while
// an applyMode is on the stack (reachable if a pumped message loop re-enters
// this timer mid-apply), same retry-next-tick shape as the consume guard below.
// Deferred WHOLE, not just the reload branches: poll()'s identity tracking is
// cheap to skip for one tick and resumes correctly, unlatched, on the next.
if (!reasampler::modeApplyInProgress()) g_session.poll();
// persist stays MODEL-ONLY (loads the saved view model but does not apply
// visibility, to avoid coupling persist to the view shell); poll() raises a
+7 -2
View File
@@ -180,7 +180,9 @@ applies the resulting lane state to live tracks.
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
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
@@ -256,7 +258,10 @@ applies the resulting lane state to live tracks.
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.
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
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
+1
View File
@@ -271,6 +271,7 @@ std::vector<LaneTrack> readLaneTracks(
std::vector<LaneTrack> tracks;
tracks.reserve(handleByGuid.size());
for (const auto& [guid, tr] : handleByGuid) {
if (!tr) continue; // TrackHandles legally carries nulls post-validation (view.cpp)
LaneTrack lt;
lt.trackGuid = guid;