fix: layer GUID-primary project identity so reopened/new projects reload the bank

classifyProjectTransition now checks the stored GUID first, then the pointer,
fixing the w10 regression where a recycled ReaProject* address stopped the bank
reloading. poll()'s fork re-GUID gate is bound to !sameProjectObject. Full
transition matrix pinned in tests.
This commit is contained in:
2026-07-23 04:46:36 -04:00
parent 39baf28c93
commit affde0ef53
5 changed files with 205 additions and 148 deletions
+33 -16
View File
@@ -110,26 +110,43 @@ ProjectTransition classifyProjectTransition(bool sameProjectObject,
const std::string& lastPath,
const std::string& currentGuid,
const std::string& currentPath) {
// A DIFFERENT project object is a tab-switch / open / recycled pointer — load
// ITS index; NEVER relocate a bank. This is the load-bearing safety fix: it
// holds even when currentGuid == lastGuid, which is exactly the forked-sibling
// case (Save-As copied our GUID, so two distinct projects share it on disk).
// The GUID is deliberately NOT consulted here — the object identity alone
// decides, and it cannot be fooled by a copied GUID.
(void)lastGuid;
(void)currentGuid;
// 1. The GUID is the identity of record and is checked FIRST. A different
// stored GUID means a genuinely different project is active — Load ITS index.
// This catches the regression that pointer-primary classification missed:
// REAPER RECYCLES ReaProject* addresses across close/open, so a reopened /
// new project can reuse the previous project's address (sameProjectObject ==
// true) while carrying a different stored GUID. Deciding on the pointer alone
// then returned NoOp/SaveAsRelocate and the bank never reloaded. The GUID is
// immune to address recycling, so it leads. Also covers new/unsaved<->saved
// transitions (one GUID empty, the other not) and switching between two
// distinct saved projects.
if (currentGuid != lastGuid) {
return ProjectTransition::Load;
}
// From here currentGuid == lastGuid (they are equal; both may be empty for
// unsaved projects). The pointer now disambiguates the same-GUID case.
// 2. Same GUID but a DIFFERENT object is a forked sibling: Save-As copied our
// GUID onto a distinct project object. Load its (own) index; never relocate.
// Two unsaved projects (both GUIDs empty, distinct objects) also land here —
// Load, so switching between them installs the right in-memory state.
if (!sameProjectObject) {
return ProjectTransition::Load;
}
// Same object from here on: identity is PROVEN by the pointer. A path change is
// a Save-As (or a first save, when the old path was empty); an unchanged path
// is Save-in-place / idle. Note SaveAsRelocate is safe even for a first save:
// the old project dir is empty, so deriveRelocationPlan makes `needed` false
// and nothing is physically relocated (empty-GUID safety preserved), while
// poll() still mints a GUID on that branch.
return (currentPath == lastPath) ? ProjectTransition::NoOp
: ProjectTransition::SaveAsRelocate;
// 3. Same object AND same GUID with a NEW path is a genuine Save-As (the object
// identity is proven and the record identity is unchanged — only the .rpp
// moved). Also the first save of an unsaved project (both GUIDs empty, old
// path empty): SaveAsRelocate is safe there because deriveRelocationPlan
// no-ops on the empty old dir (empty-GUID safety preserved) while poll()
// mints a GUID.
if (currentPath != lastPath) {
return ProjectTransition::SaveAsRelocate;
}
// 4. Same object, same GUID, same path — Save in place / idle tick.
return ProjectTransition::NoOp;
}
} // namespace reasampler