Collapse a Design View mode switch and its background FX parking to ONE undo point
applyMode now returns with its undo block open; the reclaimer closes it when the drain settles. A transport deferral or a tab switch ends it early -- two points, never a leak.
This commit is contained in:
+49
-22
@@ -64,17 +64,35 @@ 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).
|
||||
- **ONE mode switch is ONE undo point.** The driven flags and the lane state
|
||||
written inside `applyMode`'s block roll back in a single Ctrl-Z. (The
|
||||
`view_state` ext-state write is NOT among them; it lands after the block, see
|
||||
Gotchas.) Per-FX offline/online moved off this block entirely, onto the
|
||||
reclaimer (`fx_reclaim`) — each write there runs inside its OWN discarded
|
||||
block (`Undo_EndBlock2(proj, "", 0)`), so it mints nothing. DAW-VERIFIED: ten
|
||||
unblocked `TrackFX_SetOffline` calls minted ten undo points; the same ten
|
||||
calls wrapped in `Undo_BeginBlock2`/`Undo_EndBlock2(proj, "", 0)` minted zero
|
||||
— confirming the discard-block premise the reclaimer's per-write block
|
||||
(`fx_reclaim.cpp`'s `writeOffline`) rests on.
|
||||
- **A reapply that wrote nothing leaves no undo point.** The load-tick reapply
|
||||
- **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.
|
||||
- **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
|
||||
drain settling, aborting on a generation bump, or deferring on the transport; the
|
||||
project retargeting or closing; the next apply superseding it; a project save;
|
||||
extension unload. The span may stay open only across a wait it can BOUND — the 3s
|
||||
debounce, which is the dead time between the switch and its first FX write, and
|
||||
the only reason the span exists. A TRANSPORT deferral is not bounded (it lasts as
|
||||
long as the playback pass), so it ends the span and the parking that follows mints
|
||||
its own point (`spanEndsAfter`).
|
||||
- **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`;
|
||||
@@ -124,13 +142,13 @@ 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 owns the ONE undo block all of that rides (`applyMintsUndoPoint` decides whether it leaves a point). 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 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_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_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); each `writeOffline` runs inside its own discarded undo block, DAW-verified to mint nothing. `ReleaseFlush`/`fxReclaimFlushRelease` make a completed record's spend durable on the project's next save, never on the drain's own initiative.
|
||||
- `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). `ReleaseFlush`/`fxReclaimFlushRelease` make a completed record's spend durable on the project's next save, never on the drain's own initiative.
|
||||
|
||||
## Gotchas
|
||||
|
||||
@@ -158,15 +176,24 @@ applies the resulting lane state to live tracks.
|
||||
reads identically and is EXPOSEd too — no lost state there at all, which is
|
||||
why the report never asserts a cause. A REDO of a mode switch is a further,
|
||||
hypothetical route — see the `view_state` gotcha below.
|
||||
- **An UNDO of a mode switch no longer rolls back FX at all.** FX offline/online
|
||||
moved off `applyMode`'s block entirely onto the reclaimer, so a Ctrl-Z restores
|
||||
only the flags and lane state the block wrote — whatever the reclaimer had
|
||||
converged (or had not yet converged) survives the undo untouched. Whether this
|
||||
reads as correct in the DAW is `[verify — DAW]`, `docs/VERIFICATION.md`
|
||||
§"Mode switching".
|
||||
- **The `view_state` write is NOT inside `applyMode`'s block.** `persistViewState`
|
||||
(`design_view_actions.cpp`) runs after `applyMode` has returned, so
|
||||
`SetProjExtState` lands past `Undo_EndBlock2`. The UNDO direction still comes
|
||||
- **An UNDO of a mode switch rolls back the FX the span covered, and only those.**
|
||||
The span carries `UNDO_STATE_FX` only once the drain has actually written FX under
|
||||
it (`UndoSpan::widen`), so a switch whose parking never matured — a fast A→B→A
|
||||
flip — still closes on `kViewUndoMask` alone and costs no FX serialization. What a
|
||||
Ctrl-Z cannot restore is anything the reclaimer converges AFTER the span closed: a
|
||||
transport deferral, and a drain no switch opened a span for, each mint their own
|
||||
point. Whether that reads as correct in the DAW is `[verify — DAW]`,
|
||||
`docs/VERIFICATION.md` §"Mode switching".
|
||||
- **The `view_state` write is NOT inside `applyMode`'s block — but on a REAL switch
|
||||
it is now inside the SPAN.** `persistViewState` (`design_view_actions.cpp`) runs
|
||||
after `applyMode` has returned, and a real switch returns with the span open, so
|
||||
the `SetProjExtState` — and the modal Save-As `persistViewState` can pop on an
|
||||
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
|
||||
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
|
||||
|
||||
@@ -38,6 +38,28 @@ FxReclaimQueue g_queue;
|
||||
// A release applied to the in-memory model and not yet written — see ReleaseFlush.
|
||||
ReleaseFlush g_releaseFlush;
|
||||
|
||||
// The one open undo block a mode switch and its parking share — see UndoSpan.
|
||||
UndoSpan g_span;
|
||||
|
||||
// The ONE open and the ONE close. Both idempotent, so every caller may say either
|
||||
// unconditionally; the close reads the span's values before clearing it, because the
|
||||
// clear has to precede the SDK call — nothing re-entered through Undo_EndBlock2 may
|
||||
// find a span it could close a second time.
|
||||
void openSpan(ReaProject* proj, const char* label, int mask) {
|
||||
if (g_span.begin(proj, label, mask)) Undo_BeginBlock2(proj);
|
||||
}
|
||||
|
||||
void closeSpan() {
|
||||
if (!g_span.open()) return;
|
||||
ReaProject* const proj = g_span.project();
|
||||
const std::string label = g_span.label();
|
||||
const int mask = g_span.mask();
|
||||
g_span.end();
|
||||
// The span outlives its opener, so by here the project may be the one that just
|
||||
// closed — its undo history went with it and there is no block left to end.
|
||||
if (ValidatePtr2(nullptr, proj, "ReaProject*")) Undo_EndBlock2(proj, label.c_str(), mask);
|
||||
}
|
||||
|
||||
// TrackFX_SetOffline is ASSUMED to pump the message loop, so a plugin load can re-enter
|
||||
// the timer and land back here mid-drain. Fail closed: a re-entered drain would derive a
|
||||
// second queue for the same project and race the outer one's writes track for track.
|
||||
@@ -132,16 +154,15 @@ struct ReclaimHost {
|
||||
if (fxIndex < 0 || fxIndex >= TrackFX_GetCount(tr)) return false; // chain shrank
|
||||
if (TrackFX_GetOffline(tr, fxIndex) == offline) return false; // already there
|
||||
|
||||
// DAW-VERIFIED: the same ten TrackFX_SetOffline calls mint ten undo points
|
||||
// unblocked and ZERO inside this pair. One block per WRITE even though a step is
|
||||
// now one whole track: the write itself is what pumps, so the block cannot avoid
|
||||
// spanning a pump — it can only span as little of one as possible. Widening it to
|
||||
// the track would multiply that exposure by the chain length while suppressing no
|
||||
// extra undo point (the discard form mints none either way), and a user action
|
||||
// landing inside a discarded block can lose its own Ctrl-Z.
|
||||
Undo_BeginBlock2(project);
|
||||
// Every write in the drain rides ONE span. Do not reintroduce a block per write:
|
||||
// it minted one undo point per FX toggle, because the ("", 0) discard form
|
||||
// coalesces rather than discards outside an action (see UndoSpan). The open is a
|
||||
// no-op under a switch's span, which is why the FX bit is added separately —
|
||||
// here, at the first real write, so a switch whose parking never matured closes
|
||||
// on the flags-and-items mask it opened with and serializes no FX state.
|
||||
openSpan(project, kReclaimUndoLabel, UNDO_STATE_FX);
|
||||
g_span.widen(UNDO_STATE_FX);
|
||||
TrackFX_SetOffline(tr, fxIndex, offline);
|
||||
Undo_EndBlock2(project, "", 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -188,6 +209,8 @@ void invalidateFxReclaim() {
|
||||
bool fxReclaimDrainInProgress() { return g_draining; }
|
||||
|
||||
void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
|
||||
// Neither guard may close the span: both name a caller that is still on the stack and
|
||||
// still owns it — the outer drain, or the applyMode that opened it. Both re-tick.
|
||||
if (g_draining) return;
|
||||
// The model is mid-rewrite and its records are being consumed out from under any
|
||||
// plan derived from it. The apply invalidates on its way out, so nothing is lost.
|
||||
@@ -200,6 +223,12 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
|
||||
// that gate that poll.
|
||||
ReaProject* project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||
if (!ValidatePtr2(nullptr, project, "ReaProject*")) project = nullptr;
|
||||
|
||||
// A span belongs to the project it was opened on. Keyed on the SPAN's own project
|
||||
// rather than the queue's pin, which moves independently: a tab switch, a closed
|
||||
// project, or no active project at all ends it where it stands rather than carrying
|
||||
// it into another project's history — and the no-project arm below returns.
|
||||
if (g_span.open() && g_span.project() != project) closeSpan();
|
||||
if (!g_queue.beginTick(project)) return;
|
||||
|
||||
// DERIVED, never persisted: recomputed from view state every time the queue was
|
||||
@@ -213,9 +242,20 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
|
||||
ReclaimHost host{project, &session};
|
||||
const ReclaimRunResult run = g_queue.run(host);
|
||||
reportDrops(project, run.drops);
|
||||
if (spanEndsAfter(run)) closeSpan();
|
||||
if (!run.aborted && run.settled) g_queue.goIdle();
|
||||
}
|
||||
|
||||
void fxReclaimBeginUndoSpan(ReaProject* proj) {
|
||||
openSpan(proj, "", 0);
|
||||
}
|
||||
|
||||
void fxReclaimLabelUndoSpan(const char* label, int mask) {
|
||||
g_span.relabel(label ? label : "", mask);
|
||||
}
|
||||
|
||||
void fxReclaimEndUndoSpan() { closeSpan(); }
|
||||
|
||||
void fxReclaimFlushRelease(ReaSamplerSession& session) {
|
||||
// GetCurrentProjectInLoadSave() names the project THIS hook is being asked to save
|
||||
// (reaper_plugin_functions.h:1582-1587) -- not merely the active tab, which a
|
||||
@@ -224,6 +264,9 @@ void fxReclaimFlushRelease(ReaSamplerSession& session) {
|
||||
// the project actually being written, or match the arm against the active tab while a
|
||||
// different project saved and write into that wrong project's ext state.
|
||||
ReaProject* saving = GetCurrentProjectInLoadSave();
|
||||
// BEFORE the arm test, and unconditionally: a save is a user act the span must not
|
||||
// absorb, and the arm is not what decides whether one is open.
|
||||
closeSpan();
|
||||
if (!g_releaseFlush.pendingFor(saving)) return;
|
||||
// Promptless, unlike design_view_actions' persistViewState, which can open a modal
|
||||
// Save-As. [verify — DAW] whether this write reaches the <EXTSTATE> block of the
|
||||
|
||||
+129
-6
@@ -7,6 +7,7 @@
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/fx_state.h"
|
||||
@@ -155,6 +156,12 @@ struct ReclaimTickResult {
|
||||
int writes = 0;
|
||||
bool settled = false;
|
||||
|
||||
// The transport gate — and only it — held an otherwise-eligible item back this
|
||||
// tick. Distinguished from the debounce because the two waits differ in KIND: a
|
||||
// hold expires at kParkHoldMs, a deferral lasts as long as the playback pass does,
|
||||
// and the undo span may only stay open across a wait it can bound (spanEndsAfter).
|
||||
bool transportDeferred = false;
|
||||
|
||||
// Tracks whose park record the tick drove fully online, so the record's FX half is
|
||||
// spent. REPORTED, not applied: releasing is a model write and the model is exactly
|
||||
// what a re-entered load can replace mid-drain, so only the owner may commit it —
|
||||
@@ -178,8 +185,9 @@ struct ReclaimTickResult {
|
||||
// may still online while it rolls
|
||||
// bool liveChain(guid, std::vector<std::string>&) FX identity per live slot;
|
||||
// false when the track is gone
|
||||
// bool writeOffline(guid, fxIndex, offline) compare-before-write inside its
|
||||
// own discarded undo block;
|
||||
// bool writeOffline(guid, fxIndex, offline) compare-before-write, opening
|
||||
// the drain's undo span if no
|
||||
// switch already did (UndoSpan);
|
||||
// true iff it actually wrote
|
||||
// void releaseFx(guid) release the record's FX half and
|
||||
// arm the save-time flush that
|
||||
@@ -213,8 +221,10 @@ ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, H
|
||||
auto nextAllowed = [&](double nowMs) {
|
||||
const int i = nextEligible(items, clock, nowMs);
|
||||
if (i >= 0 && !offlineAllowed &&
|
||||
items[static_cast<std::size_t>(i)].work.kind == view::FxWorkKind::Offline)
|
||||
items[static_cast<std::size_t>(i)].work.kind == view::FxWorkKind::Offline) {
|
||||
result.transportDeferred = true;
|
||||
return -1;
|
||||
}
|
||||
return i;
|
||||
};
|
||||
|
||||
@@ -274,6 +284,9 @@ struct ReclaimRunResult {
|
||||
int writes = 0;
|
||||
bool settled = false;
|
||||
|
||||
// Carried straight out of the tick — see ReclaimTickResult::transportDeferred.
|
||||
bool transportDeferred = false;
|
||||
|
||||
// Something invalidated the queue while the drain was on the stack. The work and
|
||||
// the releases are DISCARDED, `settled` is never true, and the caller must not go
|
||||
// idle — the next tick re-derives against whatever changed.
|
||||
@@ -376,8 +389,18 @@ public:
|
||||
// back whether or not the drain's own work survives.
|
||||
clock_.swap(clock);
|
||||
|
||||
if (generation_ != entered)
|
||||
return ReclaimRunResult{tick.writes, false, true, tick.drops};
|
||||
// Assembled field by field rather than positionally: the aggregate is read by
|
||||
// the undo span's close decision now, and a silently mis-slotted bool there
|
||||
// leaks an open block.
|
||||
ReclaimRunResult run;
|
||||
run.writes = tick.writes;
|
||||
run.transportDeferred = tick.transportDeferred;
|
||||
run.drops = tick.drops;
|
||||
|
||||
if (generation_ != entered) {
|
||||
run.aborted = true;
|
||||
return run;
|
||||
}
|
||||
|
||||
items_.swap(work);
|
||||
// A release dropped by an abort is re-owed, not lost: the next derivation plans
|
||||
@@ -385,7 +408,8 @@ public:
|
||||
// release fires then. Applied in MEMORY only — the spend reaches disk on the
|
||||
// project's next save (ReleaseFlush), never on the drain's own initiative.
|
||||
for (const std::string& guid : tick.released) host.releaseFx(guid);
|
||||
return ReclaimRunResult{tick.writes, tick.settled, false, tick.drops};
|
||||
run.settled = tick.settled;
|
||||
return run;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -433,6 +457,87 @@ private:
|
||||
ReaProject* project_ = nullptr; // COMPARE-ONLY, never dereferenced
|
||||
};
|
||||
|
||||
// --- the undo span ---------------------------------------------------------------
|
||||
|
||||
// The label a drain that no mode switch opened a span for closes under. Names the
|
||||
// feature the user knows — Design View, and its CPU reclaim (the toggle's own tooltip
|
||||
// wording) — rather than the loop behind it, and covers both directions: one act,
|
||||
// converging the parked/returning chains to what the view state says.
|
||||
inline constexpr const char* kReclaimUndoLabel = "ReaSampler: reclaim Design View FX";
|
||||
|
||||
// One open REAPER undo block, held across the timer callbacks the drain runs on.
|
||||
//
|
||||
// DAW-measured this phase, and the whole reason this type exists: block WIDTH is the
|
||||
// entire effect. From a timer, ten TrackFX_SetOffline calls mint ten undo points
|
||||
// unblocked, ten with a block PER WRITE, and ONE under a single block spanning all
|
||||
// ten — the ("", 0) discard form does not discard outside an action, it coalesces. A
|
||||
// block opened on one timer callback and closed on a LATER one holds.
|
||||
//
|
||||
// Pure: the Undo_BeginBlock2/EndBlock2 calls are fx_reclaim.cpp's, which owns the ONE
|
||||
// instance and routes every path through one open and one close. Both moves report
|
||||
// whether they actually moved, so a caller may say either unconditionally and still
|
||||
// make no second BeginBlock2 and no unbalanced EndBlock2.
|
||||
class UndoSpan {
|
||||
public:
|
||||
bool open() const { return open_; }
|
||||
ReaProject* project() const { return project_; }
|
||||
const std::string& label() const { return label_; }
|
||||
int mask() const { return mask_; }
|
||||
|
||||
// True iff this call opened it. `label`/`mask` are adopted only then, so a drain
|
||||
// writing under a switch's span cannot displace the switch's own label.
|
||||
bool begin(ReaProject* proj, std::string label, int mask) {
|
||||
if (open_) return false;
|
||||
project_ = proj;
|
||||
label_ = std::move(label);
|
||||
mask_ = mask;
|
||||
open_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// A mode switch decides its label only after its writes (applyMintsUndoPoint).
|
||||
void relabel(std::string label, int mask) {
|
||||
if (!open_) return;
|
||||
label_ = std::move(label);
|
||||
mask_ = mask;
|
||||
}
|
||||
|
||||
// Add state the span did not originally promise to serialize — the FX bit, once
|
||||
// this drain has actually written FX under a switch's flags-and-items mask.
|
||||
void widen(int extraMask) {
|
||||
if (open_) mask_ |= extraMask;
|
||||
}
|
||||
|
||||
// True iff this call closed it; the caller owes Undo_EndBlock2 with the values it
|
||||
// read BEFORE calling, since this clears them.
|
||||
bool end() {
|
||||
if (!open_) return false;
|
||||
project_ = nullptr;
|
||||
label_.clear();
|
||||
mask_ = 0;
|
||||
open_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
ReaProject* project_ = nullptr; // passed back to Undo_EndBlock2, never dereferenced here
|
||||
std::string label_;
|
||||
int mask_ = 0;
|
||||
bool open_ = false;
|
||||
};
|
||||
|
||||
// Must the tick that produced `run` end the span?
|
||||
//
|
||||
// The span may stay open only across a wait whose length it can bound. Waiting out the
|
||||
// 3s debounce qualifies and is the wait the whole design turns on — that dead time sits
|
||||
// between the switch and its first FX write. A TRANSPORT deferral does not: it lasts as
|
||||
// long as the user plays, and a block held open for a playback pass swallows every edit
|
||||
// made during it. Ending there costs a second undo point for the parking that follows,
|
||||
// which is the documented degradation, not a failure.
|
||||
inline bool spanEndsAfter(const ReclaimRunResult& run) {
|
||||
return run.settled || run.aborted || run.transportDeferred;
|
||||
}
|
||||
|
||||
// --- shell entry points --------------------------------------------------------
|
||||
|
||||
// The ONE test the timer makes per tick, mirroring the realtime capture's single
|
||||
@@ -460,6 +565,24 @@ bool fxReclaimDrainInProgress();
|
||||
// park record's FX half — see FxReclaimQueue::run.
|
||||
void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj);
|
||||
|
||||
// Opens the undo span a mode switch and its background FX parking share, so the two are
|
||||
// ONE Ctrl-Z. Called by applyMode in place of its own Undo_BeginBlock2; a REAL switch
|
||||
// then returns with the span still open and the reclaimer closes it once the parking has
|
||||
// drained. No-op if a span is already open, so callers END first (see applyMode) rather
|
||||
// than nesting.
|
||||
void fxReclaimBeginUndoSpan(ReaProject* proj);
|
||||
|
||||
// Sets what the eventual close names the point. Separate from the begin because the
|
||||
// switch's own label is only decided after its writes (applyMintsUndoPoint).
|
||||
void fxReclaimLabelUndoSpan(const char* label, int mask);
|
||||
|
||||
// The ONE close, idempotent: every terminating path routes through it — the drain
|
||||
// settling, aborting, or deferring on the transport; the project retargeting or closing;
|
||||
// the next apply superseding it; a project save; extension unload. A leaked open block
|
||||
// would swallow everything the user does afterwards, so nothing may return past a
|
||||
// terminating condition without calling this.
|
||||
void fxReclaimEndUndoSpan();
|
||||
|
||||
// Writes an in-memory release into the SAVING project's view state (identified via
|
||||
// GetCurrentProjectInLoadSave(), not the active tab). Called from the project-save
|
||||
// hook and nowhere else: this is the ONE path that makes a spent record durable, and
|
||||
|
||||
+18
-9
@@ -2,7 +2,8 @@
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
// Tree arithmetic lives in view_tree (pure), one track's park/restore in view_park
|
||||
// and the lane writes in view_lanes; this file owns the plan iteration, parent
|
||||
// visibility, and the one undo block they all ride.
|
||||
// visibility, and opening the one undo span they all ride (fx_reclaim's UndoSpan,
|
||||
// which a real switch hands to the reclaimer still open).
|
||||
|
||||
#include "shell/view/view.h"
|
||||
|
||||
@@ -23,8 +24,6 @@
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetPlayStateEx
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||
#define REAPERAPI_WANT_UpdateArrange
|
||||
#define REAPERAPI_WANT_UpdateTimeline
|
||||
@@ -117,9 +116,13 @@ bool applyModeImpl(ViewModeModel& model, const std::string& targetModeId, ReaPro
|
||||
|
||||
bool laneModeChanged = false;
|
||||
bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint
|
||||
std::vector<std::string> exposedNames; // named while handles are fresh, reported after the block
|
||||
std::vector<std::string> exposedNames; // named while handles are fresh, reported after the writes
|
||||
|
||||
Undo_BeginBlock2(project);
|
||||
// A prior switch's span, if the reclaimer has not closed it yet: this apply
|
||||
// supersedes it, so that parking lands as its own point rather than nesting under
|
||||
// ours. Both calls are idempotent — the end reaches nothing when none is open.
|
||||
fxReclaimEndUndoSpan();
|
||||
fxReclaimBeginUndoSpan(project);
|
||||
{
|
||||
UiRefreshHold uiHold; // every write below lands with the TCP/MCP frozen
|
||||
|
||||
@@ -195,14 +198,20 @@ bool applyModeImpl(ViewModeModel& model, const std::string& targetModeId, ReaPro
|
||||
if (laneModeChanged) UpdateTimeline();
|
||||
|
||||
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
||||
Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? kViewUndoMask : 0);
|
||||
fxReclaimLabelUndoSpan(mint ? undoLabel.c_str() : "", mint ? kViewUndoMask : 0);
|
||||
|
||||
// After the undo block: an EXPOSE report is a console message, not a project
|
||||
// write, and must not join what a Ctrl-Z rolls back.
|
||||
// A REAL switch returns with the span STILL OPEN: the reclaimer closes it once the
|
||||
// parking this apply is about to invalidate has drained, so the switch and its FX
|
||||
// writes are ONE Ctrl-Z. Everything else closes here as it always did — a reapply is
|
||||
// not a switch, and the load tick must not leave a block open across a project open.
|
||||
if (!realSwitch) fxReclaimEndUndoSpan();
|
||||
|
||||
// Neither of the two below is a project write — a console message and an in-memory
|
||||
// fingerprint — so whether the span is still open over them is immaterial.
|
||||
reportExposedTracks(project, exposedNames);
|
||||
|
||||
// The tool's own account of what it just left driven — the only one that survives
|
||||
// an undo rolling the model back underneath it.
|
||||
// an undo rolling the model back underneath it. In memory, never on disk.
|
||||
noteAppliedState(project, model);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -32,9 +32,6 @@ bool transportBlocksModeSwitch(ReaProject* proj);
|
||||
//
|
||||
// A real switch mints regardless, fail-safe: it is an explicitly fired action and has
|
||||
// to stay undoable even in the degenerate case where the plan found nothing to write.
|
||||
// Note what applyMode CANNOT see when it decides — the `view_state` ext-state write
|
||||
// lands in persistViewState, after this block has closed (src/shell/view/CLAUDE.md's
|
||||
// Gotchas).
|
||||
inline bool applyMintsUndoPoint(bool realSwitch, bool wroteAnything) {
|
||||
return realSwitch || wroteAnything;
|
||||
}
|
||||
@@ -54,6 +51,10 @@ bool modeApplyInProgress();
|
||||
// path: TrackFX_SetOffline was 1897ms of a measured 1942ms switch, against 0.02ms for
|
||||
// all the flag work below. Do not reintroduce a synchronous FX write.
|
||||
//
|
||||
// A REAL switch therefore RETURNS WITH ITS UNDO BLOCK STILL OPEN (fx_reclaim's
|
||||
// UndoSpan): the reclaimer closes it once the parking has drained, which is what keeps
|
||||
// the switch and its FX writes ONE point rather than two. A reapply closes its own.
|
||||
//
|
||||
// Returns false (no mutation) if `targetModeId` isn't registered, if this is a real
|
||||
// switch (target != active) while the transport is running, if it is re-entered
|
||||
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
|
||||
|
||||
Reference in New Issue
Block a user