fix(persist): use project-object identity to stop forked-bank cross-contamination
M4's GUID-only classifier read a tab-switch between two Save-As forks (shared copied GUID, different paths) as a Save-As and clobbered a bank. Thread sameProjectObject into classifyProjectTransition: a different object always Loads, never relocates; a forked sibling gets re-GUID'd to diverge.
This commit is contained in:
+63
-22
@@ -14,23 +14,33 @@
|
||||
// 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 that GUID — CONTENT-BASED, not the ReaProject* pointer:
|
||||
// * GUID changed -> a different project became active (open a project, switch
|
||||
// tab, new project, OR a recycled pointer) -> LOAD its index from ext state.
|
||||
// * same GUID, .rpp path changed -> genuine Save-As to a new location ->
|
||||
// relocate the bank folder from the old dir to the new one.
|
||||
// Why not the pointer: REAPER recycles a closed project's ReaProject* address
|
||||
// for a newly-active project. Keying Save-As off "same pointer + new path" let
|
||||
// that recycling read as a Save-As and copy the wrong bank over a real one
|
||||
// (the M4 defect). classifyProjectTransition (pure, capture_paths) encodes the
|
||||
// decision; poll() only supplies the observed identity and executes the verdict.
|
||||
// state. Identity rests on the live ReaProject* POINTER first, with the GUID as
|
||||
// a secondary signal:
|
||||
// * different project object (proj != lastProject_) -> a switch/open/new
|
||||
// project -> LOAD its index; NEVER relocate. If its stored GUID still equals
|
||||
// the one we just left (a forked sibling that copied our GUID via Save-As),
|
||||
// re-GUID it so the siblings diverge going forward.
|
||||
// * 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 the pointer is back (W10 fix): the M4 GUID-only scheme could not tell a
|
||||
// Save-As from a tab-switch between two FORKED projects. Save-As copies the whole
|
||||
// .rpp incl. our stored GUID, so a fork and its parent share a GUID on disk;
|
||||
// switching between them (same GUID, different paths) read as a Save-As and
|
||||
// clobbered a bank. The pointer is the only signal that separates "same object
|
||||
// saved elsewhere" (Save-As) from "different object sharing a copied GUID"
|
||||
// (a switch). classifyProjectTransition (pure, capture_paths) takes a
|
||||
// `sameProjectObject` bool (poll() computes `proj == lastProject_`) so the
|
||||
// decision stays REAPER-free and testable; poll() executes the verdict.
|
||||
// Pointer *reuse* (a recycled address for a genuinely different project) stays
|
||||
// correct: it is a different object at that moment, so it Loads, and its GUID
|
||||
// differs or gets re-diverged.
|
||||
//
|
||||
// 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. 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 so the two projects' identities diverge going forward.
|
||||
// (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), and the timer composes cleanly with ext-state
|
||||
@@ -267,33 +277,63 @@ void ReaSamplerSession::poll() {
|
||||
// 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;
|
||||
return;
|
||||
}
|
||||
|
||||
const ProjectTransition transition =
|
||||
classifyProjectTransition(lastGuid_, lastRppPath_, currentGuid, rppPath);
|
||||
// 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 ProjectTransition transition = classifyProjectTransition(
|
||||
sameProjectObject, lastGuid_, lastRppPath_, currentGuid, rppPath);
|
||||
|
||||
switch (transition) {
|
||||
case ProjectTransition::NoOp:
|
||||
return;
|
||||
|
||||
case ProjectTransition::Load:
|
||||
// A different project is active (open / tab switch / new project /
|
||||
// recycled pointer). Load ITS index; never relocate. Establish its
|
||||
// identity the same way prime does.
|
||||
case ProjectTransition::Load: {
|
||||
// A DIFFERENT project object is active (open / tab switch / new project
|
||||
// / recycled pointer). Load ITS index; never relocate.
|
||||
//
|
||||
// Forked-sibling divergence: if the now-active project's stored GUID
|
||||
// still equals the one we just left, it is a Save-As fork that copied
|
||||
// our GUID and never re-saved (its fresh GUID was runtime-only on the
|
||||
// sibling we came from). Left alone, the two keep colliding on identity.
|
||||
// Mint a fresh GUID for the now-active project so the siblings diverge
|
||||
// going forward. Do this BEFORE loadFromProject reads the index (order
|
||||
// is irrelevant to the index — GUID and bank_index are distinct keys —
|
||||
// but keeping the write self-contained is clearest).
|
||||
if (proj && !currentGuid.empty() && currentGuid == lastGuid_ &&
|
||||
!rppPath.empty()) {
|
||||
const std::string fresh = genProjectGuidString();
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
||||
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 ProjectTransition::SaveAsRelocate: {
|
||||
// Same GUID + new .rpp path: a genuine Save-As. 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.
|
||||
// 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 BankRelocation plan = deriveRelocationPlan(oldDir, newDir);
|
||||
@@ -312,6 +352,7 @@ void ReaSamplerSession::poll() {
|
||||
kProjExtGuidKey, fresh.c_str());
|
||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||
}
|
||||
lastProject_ = proj; // unchanged (same object) — set for symmetry
|
||||
lastGuid_ = fresh;
|
||||
lastRppPath_ = rppPath;
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user