Cut core/wire and shell/persist comment bloat ~46% (comments only, zero code change)
This commit is contained in:
+99
-178
@@ -1,104 +1,66 @@
|
||||
#pragma once
|
||||
// sample_usage — the pure core of the pS-usage seam: ReaSampler 9000 instances count
|
||||
// as USAGE for the prune. Each live instance PUBLISHES the captures it holds (its v10
|
||||
// SampleRefs — sample ids + project-relative paths) to a per-instance project ext-state
|
||||
// key ("rsusage_<instanceGuid>", see ext_keys.h); the EXTENSION reads every usage record
|
||||
// at prune-scan time, keeps only the records backed by a live ReaSampler 9000 FX
|
||||
// instance, and folds the surviving paths into the prune's `referenced` set — so a file
|
||||
// any live instance holds can never be an orphan and BANK_PRUNE_FOLDER can never
|
||||
// delete it.
|
||||
// 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_<instanceGuid>"); 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.
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO VST3, NO SWELL,
|
||||
// NO vendor/ includes. Standard library only. The mirror of assignment_request (the
|
||||
// other VST<->extension ext-state wire): the wire format AND the two safety-critical
|
||||
// decisions (what to write on publish, which records count at prune time) live here so
|
||||
// they are provable without a DAW. The shells only move strings.
|
||||
// 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 data-ownership boundary (load-bearing) -------------------------------
|
||||
// The INSTRUMENT writes usage keys, the EXTENSION only reads them — the one
|
||||
// sanctioned instrument->ext-state write. 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.
|
||||
//
|
||||
// The INSTRUMENT writes usage keys; the EXTENSION reads them. This is the ONE sanctioned
|
||||
// instrument->ext-state write (Daniel's ruling: "if that means the VST writes to the
|
||||
// bridge when it grabs a capture, so be it") and it does NOT weaken the read-only-BANK
|
||||
// invariant: the instrument publishes its OWN usage under its OWN per-instance key,
|
||||
// and never touches banks/view/tail/assign or any other extension-owned key. The bridge
|
||||
// enforces this structurally — its write entry point accepts only "rsusage_"-prefixed keys.
|
||||
// THE SAFETY PROPERTY (overrides every other consideration): every failure,
|
||||
// ambiguity, or uncertainty here must fail-safe toward PROTECT. Over-protection
|
||||
// (prune skips a reclaimable file, or refuses to run) is an accepted residual;
|
||||
// under-protection (deleting a file an instance may still be playing) is a
|
||||
// data-loss bug. Three folds enforce this:
|
||||
// * 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).
|
||||
//
|
||||
// -- THE SAFETY PROPERTY (overrides every other consideration) -----------------
|
||||
// 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 un-prunable guarantee is a SAFETY property: every failure, ambiguity, or
|
||||
// uncertainty in this seam must FAIL-SAFE toward PROTECT. Over-protection (prune skips a
|
||||
// reclaimable file, or refuses to run at all) is an acceptable residual; under-protection
|
||||
// (deleting a file an instance may still be playing) is a data-loss bug. Three fail-safe
|
||||
// folds live in this pure module so they are provable without a DAW:
|
||||
// * sibling-collision -> UNION, never clean-replace over a foreign writer (ownerNonce);
|
||||
// * zero-identified -> records exist but NO instance was identified live -> protect
|
||||
// ALL records' paths (an identity-matcher failure must never
|
||||
// degrade toward delete);
|
||||
// * unreadable record -> ABORT the prune entirely (foldUsageRecords.abortPrune — 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 — that narrow case takes the fresh-write branch
|
||||
// (not remint), noted here for completeness.
|
||||
// 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.
|
||||
//
|
||||
// -- Liveness (no stale-key false-protect, no false-delete) --------------------
|
||||
//
|
||||
// A usage record must protect exactly the captures of instances that still EXIST. Two
|
||||
// rejected designs shape the rules below:
|
||||
// * NO teardown clearing. The obvious "clear my key in terminate()" is WRONG here:
|
||||
// REAPER destroys the plugin instance when an FX is set OFFLINE — including the
|
||||
// extension's own Design View CPU-park (per-FX offline on inactive-mode tracks). A
|
||||
// terminate-time clear would strip the record of an instance that still exists in
|
||||
// the project, opening a prune-deletes-a-used-file window. Records are therefore
|
||||
// never cleared by the instrument; staleness is resolved by the EXTENSION at read
|
||||
// time against the live FX enumeration.
|
||||
// * NO challenge/response. Instances only poll ext-state on the EDITOR's UI timer
|
||||
// (pollBankSync); a closed-editor instance could never answer a prune-time
|
||||
// challenge, and its holds would be false-deleted. Publishing is therefore EAGER
|
||||
// (on load + on every play-set change via reloadInstrument), and liveness is
|
||||
// decided extension-side.
|
||||
//
|
||||
// The liveness rule (usageHeldPaths): a record counts iff the track it was published
|
||||
// from still exists AND that track still hosts at least one ReaSampler 9000 FX
|
||||
// instance (offline FX included — chain enumeration is chunk-level, so a parked
|
||||
// instance still protects its holds). A record whose track GUID could not be resolved
|
||||
// at publish time (empty) counts while ANY ReaSampler 9000 instance exists in the
|
||||
// project — the fail-safe fallback. And the identity-failure net: when records exist
|
||||
// but ZERO instances were identified live anywhere, EVERY record's paths are protected
|
||||
// (see the safety property above — indistinguishable from a matcher failure, so it may
|
||||
// never resolve toward delete). Residuals: a deleted instance whose track still hosts a
|
||||
// sibling 9000 keeps its record alive, and a project whose instances were all deleted
|
||||
// keeps its leftover records protecting until an instance is identified again — both
|
||||
// false-PROTECT only, bounded, documented, accepted.
|
||||
//
|
||||
// -- Identity & the copy problem (planUsagePublish) -----------------------------
|
||||
//
|
||||
// The publishing key is a minted per-instance GUID persisted in ComponentState (v11).
|
||||
// A persisted id is inherently COPYABLE (FX copy / track duplication clones component
|
||||
// state byte-for-byte), so two live instances can wake up sharing one key. Worse, two
|
||||
// same-track copies converge on byte-identical wires, so "existing == what I last
|
||||
// wrote" is NOT a sound ownership test — a sibling's byte-identical write would pass
|
||||
// it, and a later clean replace would silently drop the sibling's holds (the delete
|
||||
// direction). TWO in-wire facts close this:
|
||||
// * ownerNonce — a per-LIFETIME nonce minted fresh in memory each instance lifetime,
|
||||
// NEVER persisted (a persisted nonce would clone with the state, recreating the
|
||||
// ambiguity). Proves "exactly this incarnation wrote the key last".
|
||||
// * unioned — a STICKY multi-writer poison flag. "I wrote the key last" does NOT
|
||||
// imply "the key contains only my holds": after I union a sibling's holds under my
|
||||
// own nonce, a later nonce-matching clean replace would drop them. So the first
|
||||
// union sets unioned=true in the wire, and a unioned record REFUSES clean replace
|
||||
// forever — every subsequent write is a union (holds only accumulate). Over-protect
|
||||
// residual, accepted; a solo never-restarted instance keeps clean-replace
|
||||
// semantics, and a remint starts a fresh un-poisoned key.
|
||||
// The publish plan resolves every collision in the fail-safe direction:
|
||||
// * existing ownerNonce == mine AND not unioned -> clean replace (sole writer,
|
||||
// provably my content; holds the instance released genuinely drop).
|
||||
// * same track with a foreign nonce, OR unioned -> UNION of holds, written with
|
||||
// unioned=true (a same-track sibling, my own last-session record, or a
|
||||
// multi-writer key; nothing may be dropped — over-protects, never under-protects).
|
||||
// * foreign nonce, DIFFERENT track, not unioned-by-me -> RE-MINT (a cross-track copy
|
||||
// or move; the newcomer takes a fresh identity and leaves the original's record
|
||||
// untouched; a moved-away original's old record dies by the liveness rule).
|
||||
// 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 <optional>
|
||||
#include <string>
|
||||
@@ -119,14 +81,10 @@ struct UsageHold {
|
||||
}
|
||||
};
|
||||
|
||||
// One instance's published usage: the REAPER track GUID it was hosted on at publish
|
||||
// time ("{...}" canonical form; empty when the host context could not resolve one), the
|
||||
// writing incarnation's per-LIFETIME ownerNonce (the exact "did I write this?" ownership
|
||||
// discriminator — see the copy-problem note above; never persisted in ComponentState),
|
||||
// the sticky multi-writer `unioned` poison flag (once true, clean replace is refused
|
||||
// forever — see the note above), plus every capture it holds. The record is
|
||||
// self-contained — the extension needs nothing from the instance beyond this value and
|
||||
// the live FX enumeration.
|
||||
// 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;
|
||||
@@ -139,29 +97,22 @@ struct UsageRecord {
|
||||
}
|
||||
};
|
||||
|
||||
// Encode a usage record to the wire string. Length-prefixed fields behind a magic tag
|
||||
// ("rsusage1"), the same idiom as assignment_request / provenance, so arbitrary bytes
|
||||
// in a GUID or path round-trip whole. Deterministic.
|
||||
//
|
||||
// FORMAT: "rsusage1" <len>':'<trackGuid> <len>':'<ownerNonce> <len>':'<unioned "0"|"1">
|
||||
// <len>':'<holdCount-decimal> then per hold: <len>':'<sampleId> <len>':'<relativePath>
|
||||
// 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" <len>':'<trackGuid> <len>':'<ownerNonce> <len>':'<unioned "0"|"1">
|
||||
// <len>':'<holdCount> then per hold: <len>':'<sampleId> <len>':'<relativePath>
|
||||
std::string encodeUsageRecord(const UsageRecord& rec);
|
||||
|
||||
// Parse a wire string produced by encodeUsageRecord. 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) — it must
|
||||
// never proceed with protection it cannot read.
|
||||
// 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<UsageRecord> decodeUsageRecord(const std::string& wire);
|
||||
|
||||
// The publish decision computed BEFORE a write (see the identity note above).
|
||||
// * remint — true when the existing key value belongs to a live foreign instance
|
||||
// on another track: the caller must mint a fresh instance GUID and
|
||||
// write under the NEW key, leaving the existing record untouched.
|
||||
// * skipWrite — true when the write would change nothing that matters: byte-identical
|
||||
// to the existing value (idle reload tick), or a union over an
|
||||
// ALREADY-unioned record that adds no holds (the write would flip only
|
||||
// the ownerNonce — redundant ext-state churn, skipped; a false->true
|
||||
// unioned flip is never skipped, it is the multi-writer poison).
|
||||
// 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;
|
||||
@@ -169,57 +120,32 @@ struct UsagePublishPlan {
|
||||
std::string wire;
|
||||
};
|
||||
|
||||
// Decide what to write for `mine` given the key's current value. `mine.ownerNonce` is
|
||||
// THIS lifetime's nonce; `mine.unioned` is ignored (the plan computes the written
|
||||
// flag). Branches, in order:
|
||||
// * existing absent/empty -> write mine (unioned=false — sole known writer).
|
||||
// * existing undecodable -> REMINT (mine, unioned=false, under a fresh key)
|
||||
// rather than overwriting the corrupt key: overwriting would clear the prune-side
|
||||
// abort, leaving a same-key sibling's holds unprotected until it republishes.
|
||||
// Leaving the corrupt key in place keeps the prune-side abort (foldUsageRecords)
|
||||
// firing so no delete-ward window opens. The sibling writes its own decodable
|
||||
// record on the next publish tick; the corrupt key is eventually evicted once no
|
||||
// live instance references it. Narrow gap: a >16 MB value reads back as nullopt
|
||||
// (indistinguishable from absent), so it takes the fresh-write branch rather than
|
||||
// remint — both outcomes are safe; the gap is noted in the header's fail-safe list.
|
||||
// * nonce match AND !unioned -> clean replace (sole writer, provably my content;
|
||||
// released holds drop); skipWrite when
|
||||
// byte-identical (idle reload tick).
|
||||
// * same track OR unioned -> union(existing.holds, mine.holds), existing-first,
|
||||
// de-duped, written with unioned=TRUE under my
|
||||
// nonce — a sibling's holds are NEVER dropped. The
|
||||
// false->true unioned flip is ALWAYS written (it is
|
||||
// the poison that blocks the last writer's future
|
||||
// clean replace); skipWrite only when the existing
|
||||
// record is already unioned AND the union adds no
|
||||
// holds (the write would change nonce only).
|
||||
// * else (foreign, other track) -> remint = true, write mine (fresh un-poisoned key).
|
||||
// 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<std::string>& existing,
|
||||
const UsageRecord& mine);
|
||||
|
||||
// The prune-side liveness fold: every project-relative path held by a LIVE instance,
|
||||
// de-duped, in (record, hold) input order. A record counts iff
|
||||
// * its trackGuid is non-empty and present in `liveTrackGuids` (a track that still
|
||||
// exists AND still hosts >= 1 ReaSampler 9000 FX — the caller's enumeration), OR
|
||||
// * its trackGuid is empty and `anyInstanceLive` is true (the fail-safe fallback for
|
||||
// a record published without a resolvable track context).
|
||||
// FAIL-SAFE NET (the safety property): when `records` is non-empty and
|
||||
// `anyInstanceLive` is false — records exist but NOT ONE instance was identified
|
||||
// anywhere — EVERY record's paths are returned (protect-all). Zero identified with
|
||||
// records present is indistinguishable from an identity-matcher failure, and a matcher
|
||||
// failure must never resolve toward delete. (Residual: leftover records in a project
|
||||
// whose instances were all genuinely deleted keep protecting — false-PROTECT only.)
|
||||
// Holds with an empty relativePath are skipped (nothing to protect).
|
||||
// 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<std::string> usageHeldPaths(
|
||||
const std::vector<UsageRecord>& records,
|
||||
const std::unordered_set<std::string>& liveTrackGuids,
|
||||
bool anyInstanceLive);
|
||||
|
||||
// The prune-side entry fold over RAW read/decode results, one element per enumerated
|
||||
// rsusage_* key: nullopt = the key was present but could not be read or decoded
|
||||
// (oversized ext-state read, truncation, corruption). ANY nullopt sets abortPrune —
|
||||
// the prune must HALT and delete nothing (an unreadable record may protect anything;
|
||||
// proceeding with degraded protection is the delete direction). Otherwise delegates to
|
||||
// The prune-side entry fold over raw read/decode results, one element per
|
||||
// enumerated rsusage_* key: nullopt = present but unreadable/undecodable. ANY
|
||||
// nullopt sets abortPrune (halt, delete nothing); otherwise delegates to
|
||||
// usageHeldPaths (including its protect-all net).
|
||||
struct UsageFoldResult {
|
||||
bool abortPrune = false;
|
||||
@@ -230,20 +156,15 @@ UsageFoldResult foldUsageRecords(
|
||||
const std::unordered_set<std::string>& liveTrackGuids,
|
||||
bool anyInstanceLive);
|
||||
|
||||
// FX-identity match for the live-instance enumeration (pure so the matcher itself is
|
||||
// testable; the shell only supplies REAPER's identity strings). `identity` is the value
|
||||
// of an FX's "fx_ident" or "original_name" named-config parm; the three needles are the
|
||||
// UPPERCASED channel constants:
|
||||
// * uidHexUpper — the 32-hex VST3 class UID (instrument_drop::vstClassIdHex),
|
||||
// * nameUpper — the factory display name ("REASAMPLER 9000"),
|
||||
// * outputNameUpper— the .vst3 module filename base ("REASAMPLER_9000") — the form
|
||||
// fx_ident is guaranteed to embed (it carries the module path),
|
||||
// which the space-separated display name can never match.
|
||||
// Substring, case-insensitive. NOTE the deliberate beta-substring over-protect: the
|
||||
// stable needles are substrings of the beta ones ("REASAMPLER 9000" ⊂ "REASAMPLER 9000
|
||||
// BETA", "REASAMPLER_9000" ⊂ "REASAMPLER_9000_BETA"), so a stable extension scanning a
|
||||
// project with beta instances matches them too — a WIDER protected set only (fail-safe;
|
||||
// it can never cause a delete).
|
||||
// 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user