192 lines
9.8 KiB
C++
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
|