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
+13 -11
View File
@@ -53,13 +53,14 @@ inline constexpr const char* kProjExtGuidKey = "project_guid";
// * project load -> load the index from ext state, resolve bank paths
// * Save-As (new dir) -> relocate the bank folder under the new .rpp
//
// Identity rests on the live ReaProject* pointer FIRST (a genuine Save-As is one
// object saved to a new path — same pointer; a tab-switch/open is a different
// object), with a minted GUID as a SECONDARY signal to survive pointer *reuse*
// (REAPER recycles a closed project's address). The pointer is what distinguishes
// a Save-As from a switch between two FORKED siblings that share a copied GUID on
// disk (the W10 data-integrity defect: dropping the pointer let a fork tab-switch
// masquerade as a Save-As and clobber a bank).
// Identity is layered GUID-PRIMARY: the minted GUID (content-based identity of
// record, immune to REAPER recycling a closed project's ReaProject* address) is
// checked FIRST, and the live pointer disambiguates only the same-GUID case — a
// forked sibling (same GUID, different object -> Load) vs a genuine Save-As (same
// GUID, same object, new path -> relocate). GUID-first catches pointer recycling
// (a reopened/new project reusing the previous address with a different GUID — the
// W12 defect that stopped the bank reloading); the pointer catches forks (Save-As
// copies our GUID onto a distinct object — the W10 defect that clobbered a bank).
//
// The bank itself is exposed for the capture/action layer to mutate; persist
// only reads it on save and replaces it on load.
@@ -110,10 +111,11 @@ private:
ViewModeModel view_;
// The project identity last observed by poll(), used to detect load/Save-As.
// The pointer is the PRIMARY signal (same object across ticks = a candidate
// Save-As; different object = a switch/open, never a relocate). The GUID and
// path travel alongside: the GUID distinguishes pointer *reuse* and drives
// forked-sibling re-divergence; the path tells a Save-As from an idle tick.
// The GUID is the PRIMARY signal (a different stored GUID = a different project
// of record = Load, immune to pointer recycling). The pointer disambiguates the
// same-GUID case (different object = forked sibling -> Load; same object + new
// path -> Save-As) and drives forked-sibling re-divergence; the path tells a
// Save-As from an idle tick.
// Held as void* so the header stays REAPER-free; it is a compared-only opaque
// handle (never dereferenced), so a stale/recycled address is harmless.
void* lastProject_ = nullptr; // last active ReaProject* (opaque; compare only)