Keep the extension's tick out of REAPER's own project load, and re-earn the park holds across one load

This commit is contained in:
2026-08-07 18:03:58 -04:00
parent 3d95eb715c
commit 20fef2a3bf
4 changed files with 124 additions and 9 deletions
+30 -4
View File
@@ -108,9 +108,12 @@ decide membership or mode rules.
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.
run on the timer, off the 1897ms path. **DAW-CONFIRMED, no longer inferred:** a block
opened in the `applyMode` ACTION and closed several timer callbacks later by the drain
holds — switch, then an undisturbed parking drained to completion, then one Ctrl-Z gave
exactly ONE undo entry and returned to the previous mode. (The two-point degradation
this once hedged against never materialised; the degradations that DO occur are the
transport deferral and `mintManagedLanes`' early close, both named below.)
- **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
@@ -225,7 +228,30 @@ decide membership or mode rules.
OFFLINE work once a track has read continuously parked for `kParkHoldMs`
(3000ms). A flip inside that window flips the flags twice but the debounce
never matures, so the reclaimer finds every chain already where the record
wants it and writes nothing.
wants it and writes nothing. **Undoing a switch mid-park is that same flip**, so a
state load DROPS the holds where an invalidation keeps them (`FxReclaimQueue::abandon`,
and again at the end of `run` for an abandon that arrived through a write's own pump,
which found the clock moved into the drain's local). Keeping them fired the post-load
re-park on the FIRST tick after the load, with zero debounce, in the wake of REAPER's
own restore; re-earning them is what makes the undo cost nothing and keeps the writes
clear of the load.
- **NOTHING THE EXTENSION DOES PER TICK RUNS WHILE REAPER IS IN A PROJECT LOAD OR SAVE.**
`BeginLoadProjectState` fires before REAPER restores anything, and the restore itself
brings plugins online — a plugin load pumps the message loop, which is how a timer tick
lands INSIDE the load. `main.cpp`'s `OnTimer` therefore returns immediately while
`GetCurrentProjectInLoadSave()` names a project
(`reaper_plugin_functions.h:1584-1587`), self-lowering, with no latch to leak. Without
it, that tick's `poll()` reads the half-restored `<EXTSTATE>` — the very pre-load value
the hook defers past because it is wrong — the reapply plans against it, and the reclaim
tick writes FX into a project REAPER is still rewriting; each such round is its own
settle-and-close, so a re-convergence that owes ONE point costs one per round. That last
step is reachable by inspection, not measured: the DAW report it answers is one undo
entry per FX after a Ctrl-Z mid-drain, and whether this is the whole of that cause is
`[verify — DAW]`. The extension's other re-entrancy guards (`modeApplyInProgress`,
`fxReclaimDrainInProgress`) cover OUR pumps only; this is the one that covers REAPER's.
It consumes nothing — reload request, load signal, wakeup, derived queue and pin all
stand. The realtime-capture drive deliberately sits AHEAD of it, keeping its
single-pointer-test idle path and its behaviour across a load unchanged.
- **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
and is never parked — its driven flags stay at snapshot/restored values, FX