Files
reasampler/src/core/wire/sample_usage.h
T

192 lines
9.8 KiB
C++

#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_<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.
//
// 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 <optional>
#include <string>
#include <unordered_set>
#include <vector>
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<UsageHold> 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" <len>':'<trackGuid> <len>':'<ownerNonce> <len>':'<unioned "0"|"1">
// <len>':'<holdCount> then per hold: <len>':'<sampleId> <len>':'<relativePath>
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<UsageRecord> 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<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) 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);
// One enumerated rsusage_* key and what came back from it: nullopt = present but
// unreadable/undecodable.
struct DecodedUsage {
std::string key; // "rsusage_<guid>"
std::optional<UsageRecord> 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<std::string> offendingKeys; // non-empty iff abortPrune
std::vector<std::string> heldPaths;
std::vector<CountedUsage> counted; // empty on abort
};
UsageFoldResult foldUsageRecords(
const std::vector<DecodedUsage>& decoded,
const std::unordered_set<std::string>& 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