Q-W5: persist → session/ext_state_io/prune_fs (deletion authority concentrated); one GetProjExtState grow-loop in bridge_marshal (T2-04, ×3 rewired); bank_book JSON codec → bank_book_json via private static nameKey; persist.h stays umbrella. 61/61 green.
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
// session.cpp — the ReaSamplerSession lifecycle half of the persist seam (Q-W5
|
||||
// split of the former persist.cpp; see session.h for the TU map): the poll-driven
|
||||
// identity-transition detection and the deferred undo/redo reload drain.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
||||
// pointers; here they are extern (CLAUDE.md §contract).
|
||||
//
|
||||
// PROJECT-LOAD / SAVE-AS DETECTION (chosen mechanism):
|
||||
// Driven by REAPER's "timer" register (main.cpp). Each poll() reads the active
|
||||
// project (EnumProjects(-1)), its .rpp path, and the GUID we store in its ext
|
||||
// state. Identity is layered GUID-PRIMARY, with the ReaProject* pointer as the
|
||||
// secondary disambiguator (classifyProjectTransition owns the exact order):
|
||||
// * different stored GUID -> a different project of record -> LOAD its index;
|
||||
// NEVER relocate. Catches pointer RECYCLING (REAPER reuses a closed project's
|
||||
// address, so a reopened/new project can present the previous pointer with a
|
||||
// different GUID), new/unsaved<->saved, and switching between distinct saved
|
||||
// projects.
|
||||
// * SAME GUID, DIFFERENT object -> a forked sibling that copied our GUID via
|
||||
// Save-As -> LOAD its index; NEVER relocate; re-GUID it so the siblings
|
||||
// diverge going forward.
|
||||
// * SAME GUID, SAME object, .rpp path changed -> genuine Save-As to a new
|
||||
// location -> relocate the bank folder from the old dir to the new one, then
|
||||
// re-GUID.
|
||||
// Why GUID-primary (W12 fix): this layers the two prior designs. M4 (GUID-only)
|
||||
// broke Save-As forks — Save-As copies the whole .rpp incl. our stored GUID, so a
|
||||
// fork and its parent share a GUID on disk; switching between them read as a
|
||||
// Save-As and clobbered a bank. W10 (pointer-primary, GUID voided) broke pointer
|
||||
// RECYCLING — a reopened/new project reusing the previous project's address read
|
||||
// as NoOp/SaveAsRelocate and the bank never reloaded. Checking the GUID first
|
||||
// catches recycling; the pointer then separates a fork (same GUID, different
|
||||
// object -> Load) from a Save-As (same GUID, same object, new path -> relocate).
|
||||
// classifyProjectTransition (pure, capture_paths) takes a `sameProjectObject`
|
||||
// bool (poll() computes `proj == lastProject_`) so the decision stays REAPER-free
|
||||
// and testable; poll() executes the verdict.
|
||||
//
|
||||
// REAPER exposes no stable per-project GUID (GetSetProjectInfo_String has no
|
||||
// PROJECT_GUID desc; GetProjectStateChangeCount is a session-local counter, not
|
||||
// a cross-open identity), so we MINT one with genGuid/guidToString and store it
|
||||
// under kProjExtGuidKey (ext_state_io.cpp owns the minting helpers). On Save-As
|
||||
// REAPER copies the whole .rpp incl. our ext state, so the new project initially
|
||||
// shares the old GUID; poll() re-GUIDs it (after relocating, or on the forked-
|
||||
// sibling Load branch) so identities diverge.
|
||||
//
|
||||
// Rationale for the timer: the brief mandates ext-state storage (rules out the
|
||||
// projectconfig .rpp-line hook for STORAGE), and the timer composes cleanly with
|
||||
// ext-state while covering identity-transition load + Save-As detection in one
|
||||
// place.
|
||||
//
|
||||
// DIVISION OF LABOUR (R-B undo):
|
||||
// * Identity-transition poll (this file, classifyProjectTransition) owns
|
||||
// open / tab-switch / new / forked-sibling / Save-As-relocation — every case
|
||||
// where the project OF RECORD changes.
|
||||
// * The `projectconfig` hook (main.cpp registers project_config_extension_t;
|
||||
// BeginLoadProjectState with isUndo) owns UNDO/REDO — where the project
|
||||
// identity is unchanged but its ext state rolled back/forward on disk. The
|
||||
// identity poll sees NoOp there and would never re-read ext state, so the hook
|
||||
// requests a reload (requestReload) that poll() drains on the next tick, once
|
||||
// REAPER has restored the <EXTSTATE> block. See requestReload / the poll drain.
|
||||
// The hook fires on undo AND redo (isUndo true for both), and on normal open
|
||||
// (isUndo false) — but we set the reload flag ONLY for isUndo, so a normal open
|
||||
// flows solely through the identity-transition Load path and never double-loads.
|
||||
|
||||
#include "shell/persist/session.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "shell/persist/ext_state_io.h"
|
||||
#include "shell/persist/persist_internal.h"
|
||||
|
||||
#include "core/capture/capture_paths.h" // classifyProjectTransition / deriveRelocationPlan (pure)
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_MarkProjectDirty
|
||||
#define REAPERAPI_WANT_SetProjExtState
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
using persist_detail::ensureProjectGuid;
|
||||
using persist_detail::genProjectGuidString;
|
||||
using persist_detail::getProjExtStateString;
|
||||
using persist_detail::projectDirOf;
|
||||
using persist_detail::readActiveProject;
|
||||
using persist_detail::relocateBankFolder;
|
||||
|
||||
bool ReaSamplerSession::consumeLoadSignal() {
|
||||
const bool pending = loadPending_;
|
||||
loadPending_ = false;
|
||||
return pending;
|
||||
}
|
||||
|
||||
void ReaSamplerSession::requestReload() {
|
||||
// Set-only; poll() drains it on the next tick (see the poll() drain block for why
|
||||
// the read is deferred past the projectconfig callback). Cheap and idempotent —
|
||||
// multiple undo/redo callbacks before the next tick collapse to one reload.
|
||||
reloadRequested_ = true;
|
||||
}
|
||||
|
||||
void ReaSamplerSession::poll() {
|
||||
std::string rppPath;
|
||||
void* proj = readActiveProject(rppPath);
|
||||
const std::string currentGuid =
|
||||
proj ? getProjExtStateString(proj, projExtNamespace(), kProjExtGuidKey)
|
||||
: std::string{};
|
||||
|
||||
if (!primed_) {
|
||||
// First observation: adopt current identity and load its index, without
|
||||
// treating it as a "change" (avoids a spurious relocation on startup).
|
||||
primed_ = true;
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
reloadRequested_ = false; // priming already loaded — a co-tick request is moot
|
||||
return;
|
||||
}
|
||||
|
||||
// Undo/redo reload (owner: the projectconfig hook, NOT the identity classifier
|
||||
// below). An undo/redo keeps the SAME project identity — same ReaProject*, GUID,
|
||||
// and .rpp path — so classifyProjectTransition would return NoOp and never re-read
|
||||
// ext state, leaving book_/view_ stale after the on-disk ext state rolled back.
|
||||
// The projectconfig BeginLoadProjectState callback (isUndo) raised reloadRequested_
|
||||
// one or more ticks ago; by NOW REAPER has finished restoring the project's
|
||||
// <EXTSTATE> block, so GetProjExtState returns the POST-undo value. Reload from the
|
||||
// current active project and identity-adopt it (no relocation — the path is
|
||||
// unchanged), then return. loadFromProject raises loadPending_, so the existing
|
||||
// consumeLoadSignal() glue re-baselines the panel detector and reapplies the active
|
||||
// mode; bankPanelRefresh's fingerprint pass then repaints the restored book. This is
|
||||
// the ONLY undo/redo reload path — the timer never polls ext-state CONTENT to detect
|
||||
// an undo (Daniel's directive: the hook drives it, not a poll heuristic).
|
||||
if (reloadRequested_) {
|
||||
reloadRequested_ = false;
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
return;
|
||||
}
|
||||
|
||||
// Pointer identity is the primary signal: a genuine Save-As keeps the SAME
|
||||
// ReaProject* (one object saved elsewhere); a tab-switch/open is a different
|
||||
// object. Passing the bool (not the pointer) keeps the classifier pure.
|
||||
const bool sameProjectObject = (proj == lastProject_);
|
||||
const capture::ProjectTransition transition = capture::classifyProjectTransition(
|
||||
sameProjectObject, lastGuid_, lastRppPath_, currentGuid, rppPath);
|
||||
|
||||
switch (transition) {
|
||||
case capture::ProjectTransition::NoOp:
|
||||
return;
|
||||
|
||||
case capture::ProjectTransition::Load: {
|
||||
// A different project of record is active (open / tab switch / new /
|
||||
// reopened / recycled pointer / forked sibling). Load ITS index; never
|
||||
// relocate.
|
||||
//
|
||||
// Forked-sibling divergence: gate on `!sameProjectObject` so this fires
|
||||
// ONLY for a step-2 Load (same GUID, different object) — a Save-As fork
|
||||
// that copied our GUID and never re-saved (its fresh GUID was runtime-
|
||||
// only on the sibling we came from). A recycled-pointer Load (step 1:
|
||||
// currentGuid != lastGuid_) must NOT re-GUID — it is already a distinct
|
||||
// identity. currentGuid == lastGuid_ can only hold here when step 1 did
|
||||
// NOT fire, i.e. this is the fork case; the explicit !sameProjectObject
|
||||
// makes that intent load-bearing rather than incidental. Do this BEFORE
|
||||
// loadFromProject reads the index (order is irrelevant — GUID and
|
||||
// bank_index are distinct keys — but self-contained is clearest).
|
||||
if (proj && !sameProjectObject && !currentGuid.empty() &&
|
||||
currentGuid == lastGuid_ && !rppPath.empty()) {
|
||||
const std::string fresh = genProjectGuidString();
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtGuidKey, fresh.c_str());
|
||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = fresh;
|
||||
lastRppPath_ = rppPath;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal load: establish identity the same way prime does.
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
return;
|
||||
}
|
||||
|
||||
case capture::ProjectTransition::SaveAsRelocate: {
|
||||
// SAME project object + new .rpp path: a genuine Save-As (the pointer
|
||||
// proves it — a fork tab-switch is a DIFFERENT object and took the Load
|
||||
// branch above). Relocate the bank folder from the old dir to the new
|
||||
// one so the wavs sit under the new .rpp and the index's relative paths
|
||||
// still resolve. Keep the in-memory bank as-is (Save-As copied our ext
|
||||
// state, the relative paths are unchanged) — do NOT reload.
|
||||
const std::string oldDir = projectDirOf(lastRppPath_);
|
||||
const std::string newDir = projectDirOf(rppPath);
|
||||
const capture::BankRelocation plan =
|
||||
capture::deriveRelocationPlan(oldDir, newDir);
|
||||
if (plan.needed) {
|
||||
relocateBankFolder(plan.oldBankDir, plan.newBankDir);
|
||||
}
|
||||
|
||||
// Save-As duplicated our ext state, so the new project B currently
|
||||
// shares A's GUID. Mint a FRESH GUID for B and write it, so A and B
|
||||
// no longer collide on identity when reopened later. Adopt the fresh
|
||||
// GUID as our last-seen identity. Mark dirty so the fresh GUID flushes
|
||||
// to the new .rpp on the next normal save / close-prompt.
|
||||
const std::string fresh = genProjectGuidString();
|
||||
if (proj) {
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtGuidKey, fresh.c_str());
|
||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||
}
|
||||
lastProject_ = proj; // unchanged (same object) — set for symmetry
|
||||
lastGuid_ = fresh;
|
||||
lastRppPath_ = rppPath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user