#pragma once // sample_usage — pure core of the instance-usage wire: ReaSampler 9000 instances // count as usage for the prune. Each live instance publishes the captures it // holds to a per-instance ext-state key ("rsusage_"); the // extension reads every record at prune-scan time, keeps only the ones backed // by a live FX instance, and folds the surviving paths into the prune's // `referenced` set — a file any live instance holds can never be an orphan. // // No REAPER/VST3/SWELL/vendor includes. Mirror of assignment_request on the // instrument->extension direction: the wire format and the two safety-critical // decisions (what to write on publish, which records count at prune time) are // pure and provable without a DAW; shells only move strings. // // The INSTRUMENT writes usage keys, the EXTENSION only reads them — one of the // two sanctioned instrument->ext-state writes (bake_wire's request key is the // other). It does not weaken the read-only-bank invariant: the instrument // publishes only its own per-instance key, never banks/view/tail/assign; the // bridge's write entry point structurally accepts only "rsusage_"-prefixed keys. // // Every failure, ambiguity, or uncertainty here fails safe toward PROTECT (the // territory-wide asymmetry, stated in core/tracking/CLAUDE.md). Three folds enforce // it here: // * sibling-collision -> UNION, never clean-replace over a foreign writer; // * zero-identified -> records exist but no instance was identified live -> // protect ALL records' paths (a matcher failure must // never degrade toward delete); // * unreadable record -> ABORT the prune entirely (a record we cannot read // may protect anything; halting deletes nothing). // Residual: readReasamplerExtState returning nullopt // for a >16 MB value is indistinguishable from // "absent" at the publish site, so that narrow case // takes the fresh-write branch, not remint. // // Liveness is decided extension-side at read time, not by teardown clearing // (REAPER destroys the plugin instance when an FX goes offline, including // Design View's CPU-park, so a terminate-time clear would strip a still-live // instance's record) or challenge/response (a closed-editor instance could // never answer a prune-time challenge). Publishing is eager instead (on load // + every play-set change). // // The liveness rule (usageHeldPaths): a record counts iff its track still // hosts >= 1 instance (offline included — a parked instance still protects // its holds). A record with no resolvable track GUID counts while ANY // instance exists (fail-safe fallback). Zero instances identified anywhere -> // EVERY record's paths protected. // // Identity & the copy problem (planUsagePublish): the publishing key is a // per-instance GUID persisted in ComponentState — inherently copyable (FX // copy / track duplication clones it byte-for-byte), so two live instances can // share one key, and same-track copies converge on byte-identical wires, so // "existing == what I last wrote" is not a sound ownership test. Two in-wire // facts close this: // * ownerNonce — a per-lifetime nonce, minted fresh in memory, NEVER // persisted (a persisted nonce would clone with the state). Proves // "exactly this incarnation wrote the key last." // * unioned — a sticky poison flag: once a sibling's holds are unioned in, // the record refuses clean replace forever (every subsequent write unions, // holds only accumulate) — over-protect residual, accepted. // Publish resolution, always leaning over-protect: // * ownerNonce matches mine AND not unioned -> clean replace (sole writer; // released holds drop). // * same track with a foreign nonce, OR unioned -> UNION, written unioned=true. // * foreign nonce, different track -> RE-MINT under a fresh key; the // original's record is untouched and dies later by the liveness rule if // abandoned. #include #include #include #include namespace reasampler::wire { // One held capture: the bank sample id (attribution/debugging) + the project-relative // WAV path (the prune-protection payload — compared by EXACT string against the prune // core's `present` spelling, which both sides source from the same bank-blob spelling). struct UsageHold { std::string sampleId; std::string relativePath; bool operator==(const UsageHold& o) const { return sampleId == o.sampleId && relativePath == o.relativePath; } }; // One instance's published usage: the track GUID it was hosted on at publish // time (empty if unresolvable), the writing incarnation's ownerNonce, the // sticky `unioned` poison flag, plus every capture it holds — self-contained, // the extension needs nothing beyond this value and the live FX enumeration. struct UsageRecord { std::string trackGuid; std::string ownerNonce; bool unioned = false; std::vector holds; bool operator==(const UsageRecord& o) const { return trackGuid == o.trackGuid && ownerNonce == o.ownerNonce && unioned == o.unioned && holds == o.holds; } }; // Length-prefixed fields behind a magic tag ("rsusage1"), same idiom as // assignment_request, so arbitrary bytes in a GUID or path round-trip whole. // "rsusage1" ':' ':' ':' // ':' then per hold: ':' ':' std::string encodeUsageRecord(const UsageRecord& rec); // std::nullopt on malformed/truncated/trailing-garbage input (never UB, never // a partial value). The prune scan treats an undecodable record as unreadable // and aborts (foldUsageRecords) rather than proceed with protection it cannot read. std::optional decodeUsageRecord(const std::string& wire); // The publish decision computed before a write. // * remint — the existing key belongs to a live foreign instance on // another track: mint a fresh instance GUID, write under it. // * skipWrite — the write would change nothing that matters (byte-identical, // or a union over an already-unioned record adding no holds). // * wire — the encoded value to write (mine, or the same-track union). struct UsagePublishPlan { bool remint = false; bool skipWrite = false; std::string wire; }; // Decide what to write for `mine` given the key's current value, in order: // * absent/empty -> write mine (unioned=false). // * undecodable -> REMINT under a fresh key rather than overwrite // the corrupt value — overwriting would silently clear the prune-side abort // that is currently protecting a same-key sibling's unreadable holds. // * nonce match, !unioned -> clean replace (released holds drop). // * same track, or unioned -> union(existing, mine), written unioned=true — // a sibling's holds are never dropped. // * foreign nonce, other track -> remint (fresh un-poisoned key). UsagePublishPlan planUsagePublish(const std::optional& existing, const UsageRecord& mine); // The prune-side liveness fold: every project-relative path held by a live // instance, de-duped, in (record, hold) order. A record counts iff its // trackGuid is present in `liveTrackGuids`, or its trackGuid is empty and // `anyInstanceLive` is true. FAIL-SAFE NET: when `records` is non-empty and // `anyInstanceLive` is false, EVERY record's paths are returned (protect-all; // see the safety property above). Holds with an empty relativePath are skipped. std::vector usageHeldPaths( const std::vector& records, const std::unordered_set& liveTrackGuids, bool anyInstanceLive); // One enumerated rsusage_* key and what came back from it: nullopt = present but // unreadable/undecodable. struct DecodedUsage { std::string key; // "rsusage_" std::optional record; }; // A record that counted toward heldPaths, still attributed to its key. Lets a // consumer ask "who holds this path" — the flattened path list cannot. struct CountedUsage { std::string key; UsageRecord record; }; // The prune-side entry fold. ANY unreadable record sets abortPrune (halt, delete // nothing) and names its key; otherwise delegates to usageHeldPaths (including its // protect-all net) and reports which records counted. struct UsageFoldResult { bool abortPrune = false; std::vector offendingKeys; // non-empty iff abortPrune std::vector heldPaths; std::vector counted; // empty on abort }; UsageFoldResult foldUsageRecords( const std::vector& decoded, const std::unordered_set& liveTrackGuids, bool anyInstanceLive); // FX-identity match for the live-instance enumeration (pure so the matcher is // testable; the shell supplies REAPER's identity strings). `identity` is an // FX's "fx_ident" or "original_name" parm; the needles are the UPPERCASED // channel constants — uidHexUpper (32-hex class UID), nameUpper (factory // display name), outputNameUpper (.vst3 filename base, the form fx_ident is // guaranteed to embed). Substring, case-insensitive. Deliberate beta-substring // over-protect: stable needles are substrings of the beta ones, so a stable // extension matches beta instances too — a wider protected set only, never a // delete risk. bool identityMatches(const std::string& identity, const std::string& uidHexUpper, const std::string& nameUpper, const std::string& outputNameUpper); // ASCII-only uppercase (shared by the matcher and the shell's needle preparation). std::string toUpperAscii(const std::string& s); } // namespace reasampler::wire