152 lines
8.6 KiB
C++
152 lines
8.6 KiB
C++
#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 ("usage_<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.
|
|
//
|
|
// 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.
|
|
//
|
|
// -- The data-ownership boundary (load-bearing) -------------------------------
|
|
//
|
|
// 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 "usage_"-prefixed keys.
|
|
//
|
|
// -- 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. The residual: a deleted instance whose track still
|
|
// hosts a sibling 9000 keeps its record alive (false-PROTECT only — prune skips a file
|
|
// it could have reclaimed; never the delete direction). 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. The publish
|
|
// plan resolves every collision in the fail-safe direction:
|
|
// * existing value == what THIS instance wrote this lifetime -> clean replace (the
|
|
// normal single-owner path; holds the instance released genuinely drop).
|
|
// * existing value is foreign but from the SAME track -> UNION of holds (a same-track
|
|
// copy; neither sibling's holds may be dropped — over-protects until the next clean
|
|
// replace, never under-protects).
|
|
// * existing value is foreign from a DIFFERENT track -> 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).
|
|
|
|
#include <optional>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
namespace reasampler {
|
|
|
|
// 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 REAPER track GUID it was hosted on at publish
|
|
// time ("{...}" canonical form; empty when the host context could not resolve one) 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.
|
|
struct UsageRecord {
|
|
std::string trackGuid;
|
|
std::vector<UsageHold> holds;
|
|
|
|
bool operator==(const UsageRecord& o) const {
|
|
return trackGuid == o.trackGuid && holds == o.holds;
|
|
}
|
|
};
|
|
|
|
// 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>':'<holdCount-decimal>
|
|
// 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 extension
|
|
// treats an undecodable record as absent — it can protect nothing 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 be byte-identical to what this instance
|
|
// already wrote this lifetime (idle reload tick) — skip the ext-state
|
|
// churn entirely.
|
|
// * 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 and what this instance
|
|
// last wrote THIS LIFETIME (empty string = nothing yet this lifetime — a fresh load;
|
|
// the existing value is then this instance's own persisted record from the last
|
|
// session, OR a copy-source's record: same-track -> union, other-track -> remint).
|
|
// * existing absent or undecodable -> write mine.
|
|
// * existing == lastPublishedThisLifetime -> write mine (clean replace).
|
|
// * existing.trackGuid == mine.trackGuid -> write union(existing.holds, mine.holds)
|
|
// (existing-first order, de-duped).
|
|
// * else -> remint = true, write mine (new key).
|
|
UsagePublishPlan planUsagePublish(const std::optional<std::string>& existing,
|
|
const std::string& lastPublishedThisLifetime,
|
|
const UsageRecord& mine);
|
|
|
|
// The prune-side 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).
|
|
// Holds with an empty relativePath are skipped (nothing to protect).
|
|
std::vector<std::string> usageHeldPaths(
|
|
const std::vector<UsageRecord>& records,
|
|
const std::unordered_set<std::string>& liveTrackGuids,
|
|
bool anyInstanceLive);
|
|
|
|
} // namespace reasampler
|