Add temporary undo-span tracing for the mid-drain Ctrl-Z bug

One switch (REASAMPLER_UNDO_TRACE) gates every line. Traces span open/close
reasons, FX writes, UndoBlock depth, timer standoffs, and load-state hooks.
This commit is contained in:
2026-08-09 12:28:50 -04:00
parent d5ff476a19
commit b782fb2731
8 changed files with 167 additions and 21 deletions
+29 -5
View File
@@ -38,6 +38,7 @@
#include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded
#include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown)
#include "shell/persist/session.h" // ReaSamplerSession
#include "shell/undo/undo_trace.h" // TEMPORARY — see undo_trace.h; revert together
#include "shell/view/fx_reclaim.h" // the FX reclaimer's tick
#include "shell/view/view.h" // applyMode
#include "shell/view/view_lanes.h" // reconcileManagedLanes
@@ -183,6 +184,12 @@ 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);
// TEMPORARY: captured in a local (rather than calling GetCurrentProjectInLoadSave()
// twice) so the trace can report the value on every tick, standoff or not, with no
// change to the call below. See undo_trace.h.
ReaProject* const loadSaveProj = GetCurrentProjectInLoadSave();
REASAMPLER_TRACE("[Timer] loadSaveProj=%p", (void*)loadSaveProj);
// A project load or save is IN FLIGHT, so this tick is running inside its own message
// pump — a restore brings plugins online and a plugin load pumps. Everything below
// would then act on a half-restored project: poll() reads the pre-load <EXTSTATE> that
@@ -192,7 +199,10 @@ static void OnTimer()
// the first tick after the load runs as if this one had never been due. Self-lowering
// (reaper_plugin_functions.h:1584-1587), and placed after the realtime drive so that
// fast path keeps its single-pointer-test shape. See src/shell/view/CLAUDE.md.
if (GetCurrentProjectInLoadSave()) return;
if (loadSaveProj) {
REASAMPLER_TRACE("[Timer] STANDOFF guard=loadSaveProj");
return;
}
// poll()'s undo/redo-reload and project-switch Load branches call
// loadFromProject, replacing g_session.view() wholesale — deferred here while
@@ -202,8 +212,13 @@ static void OnTimer()
// cheap to skip for one tick and resumes correctly, unlatched, on the next.
// The reclaim drain is latched for the same reason and is the same window: it
// holds that view model for the length of a plugin unload, which pumps.
if (!reasampler::modeApplyInProgress() && !reasampler::fxReclaimDrainInProgress())
if (!reasampler::modeApplyInProgress() && !reasampler::fxReclaimDrainInProgress()) {
g_session.poll();
} else {
REASAMPLER_TRACE("[Timer] SKIP-POLL modeApply=%d drain=%d",
reasampler::modeApplyInProgress() ? 1 : 0,
reasampler::fxReclaimDrainInProgress() ? 1 : 0);
}
// 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
@@ -216,8 +231,15 @@ static void OnTimer()
// 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() && !reasampler::fxReclaimDrainInProgress() &&
g_session.consumeLoadSignal()) {
// TEMPORARY: split into locals + if/else-if so the skip can be traced without
// calling consumeLoadSignal() (which has a side effect) a second time — same
// short-circuit semantics as the original single `if`. See undo_trace.h.
const bool loadGateModeApply = reasampler::modeApplyInProgress();
const bool loadGateDraining = reasampler::fxReclaimDrainInProgress();
if (loadGateModeApply || loadGateDraining) {
REASAMPLER_TRACE("[Timer] SKIP-LOAD modeApply=%d drain=%d", loadGateModeApply ? 1 : 0,
loadGateDraining ? 1 : 0);
} else if (g_session.consumeLoadSignal()) {
reasampler::bankPanelNotifyProjectLoaded();
// Reconcile lane ownership against the live project's lanes (P_LANENAME,
// the cross-session source of truth) BEFORE reapplying visibility. Never
@@ -257,6 +279,8 @@ static void OnTimer()
// forked-sibling re-GUID branch requesting a reload here would bypass.
static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/)
{
REASAMPLER_TRACE("[Load] BeginLoadProjectState isUndo=%d", isUndo ? 1 : 0);
// UNBRANCHED, and the reason the terminator is a hook at all: this is REAPER's one
// entry for a state load — "project load/undo", plus a new project
// (reaper_plugin.h:1203-1205) — and the last moment before the restore. Why that
@@ -343,7 +367,7 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
// Same reason, for the reclaimer's undo span: a block left open here has no
// owner left to close it and would swallow every edit the user makes after.
reasampler::fxReclaimEndUndoSpan();
reasampler::fxReclaimEndUndoSpan("extension unload");
g_rec->Register("-timer", (void*)&OnTimer);
g_rec->Register("-projectconfig", (void*)&g_projectConfig);