pS-usage: captures held by live ReaSampler 9000 instances are un-prunable — instances publish usage_<guid> ext-state records (ComponentState v11), prune unions live holds into referenced
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -23,6 +25,7 @@
|
||||
#include "reasampler_editor.h"
|
||||
#include "reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there)
|
||||
#include "sample_map.h" // refs resolve, buildZonedKeymap, state (de)ser (pS self-contained)
|
||||
#include "sample_usage.h" // pS-usage publish plan + wire (prune-protection seam)
|
||||
#include "wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
||||
|
||||
using namespace Steinberg;
|
||||
@@ -47,6 +50,21 @@ constexpr std::size_t kPreserveVoiceCap = 8;
|
||||
constexpr float kGainRampRate = 1.0f / 960.0f; // 960 samples @ 48 kHz ≈ 20 ms
|
||||
constexpr float kGainRampSnap = kGainRampRate * 0.5f;
|
||||
|
||||
// pS-usage: mint a fresh per-instance publish identity — 32 lowercase hex chars from the
|
||||
// OS entropy source. Uniqueness (not cryptographic strength) is the requirement: two
|
||||
// instances sharing a key is the copy-collision planUsagePublish resolves fail-safe
|
||||
// anyway; the mint just makes accidental collision vanishingly unlikely. Off-thread only.
|
||||
std::string mintUsageInstanceGuid() {
|
||||
std::random_device rd;
|
||||
std::mt19937_64 gen((static_cast<std::uint64_t>(rd()) << 32) ^ rd());
|
||||
std::uniform_int_distribution<std::uint64_t> dist;
|
||||
char buf[33] = {0};
|
||||
std::snprintf(buf, sizeof(buf), "%016llx%016llx",
|
||||
static_cast<unsigned long long>(dist(gen)),
|
||||
static_cast<unsigned long long>(dist(gen)));
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on
|
||||
// any failure — the caller treats an unreadable WAV as "nothing to play".
|
||||
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
|
||||
@@ -258,6 +276,15 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
||||
std::lock_guard<std::mutex> lock(refsMutex_);
|
||||
sampleRefs_ = cs.sampleRefs;
|
||||
}
|
||||
// pS-usage: restore the persisted publish identity (v11; pre-v11 lifts to empty —
|
||||
// minted on first publish). lastPublishedUsageWire_ resets: a restored blob is a NEW
|
||||
// LIFETIME for the copy-collision analysis (planUsagePublish must compare the key's
|
||||
// current value against what THIS incarnation wrote, not a previous one's writes).
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(usageMutex_);
|
||||
instanceGuid_ = cs.instanceGuid;
|
||||
lastPublishedUsageWire_.clear();
|
||||
}
|
||||
// A new blob is new facts: a staleness proof latched against the PREVIOUS state does
|
||||
// not carry over (#A — the legacy lift gets one fresh run per restored state).
|
||||
legacyLiftConcluded_.store(false, std::memory_order_relaxed);
|
||||
@@ -302,6 +329,12 @@ tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
|
||||
state_out.sampleRefs = sampleRefs();
|
||||
retainRefs(state_out.sampleRefs,
|
||||
referencedSampleIds(state_out.selectionId, state_out.map));
|
||||
// pS-usage: persist the publish identity (v11) so the instance's usage key is
|
||||
// stable across sessions (records do not proliferate per reopen).
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(usageMutex_);
|
||||
state_out.instanceGuid = instanceGuid_;
|
||||
}
|
||||
const std::vector<std::uint8_t> bytes = serializeComponentState(state_out);
|
||||
if (!bytes.empty()) {
|
||||
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
|
||||
@@ -601,9 +634,55 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// `built` is heap-owned; release() hands ownership to the atomic; the drain-evicted
|
||||
// pointer is re-owned by the graveyard.
|
||||
publishBuiltLocked(std::move(built));
|
||||
|
||||
// 5. pS-usage: publish this instance's held captures so the extension's prune can
|
||||
// never reclaim them (see publishUsage). AFTER the instrument swap, still off the
|
||||
// audio thread and under reloadMutex_. Publishes regardless of decode success:
|
||||
// the holds are the refs the instance RETAINS (its play-set), not what decoded —
|
||||
// a transiently unreadable WAV must stay protected.
|
||||
publishUsage(refs, ids);
|
||||
return resolvedId;
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishUsage(const SampleRefs& refs,
|
||||
const std::vector<std::string>& ids) {
|
||||
if (!bridge_.isConnected()) return; // non-REAPER host / no ext-state — nothing to do
|
||||
|
||||
UsageRecord mine;
|
||||
mine.trackGuid = bridge_.currentTrackGuid();
|
||||
for (const std::string& id : ids) {
|
||||
if (const SelectedSample* ref = findRef(refs, id)) {
|
||||
if (!ref->relativePath.empty()) {
|
||||
mine.holds.push_back(UsageHold{id, ref->relativePath});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(usageMutex_);
|
||||
// A never-published instance with nothing held writes nothing — no key litter for
|
||||
// fresh/empty instances. Once an identity exists, empties DO publish (they release
|
||||
// holds the prune would otherwise keep protecting).
|
||||
if (instanceGuid_.empty() && mine.holds.empty()) return;
|
||||
if (instanceGuid_.empty()) instanceGuid_ = mintUsageInstanceGuid();
|
||||
|
||||
const std::optional<std::string> existing =
|
||||
bridge_.readReasamplerExtState(usageKeyFor(instanceGuid_));
|
||||
const UsagePublishPlan plan =
|
||||
planUsagePublish(existing, lastPublishedUsageWire_, mine);
|
||||
if (plan.remint) {
|
||||
// This state was cloned onto another track (FX copy / track duplication): take a
|
||||
// fresh identity and leave the original's record untouched. The abandoned old
|
||||
// identity's record dies by the extension's liveness rule when its track no
|
||||
// longer hosts an instance. getState persists the new guid on the next save.
|
||||
instanceGuid_ = mintUsageInstanceGuid();
|
||||
} else if (plan.skipWrite) {
|
||||
return; // byte-identical to what this lifetime already wrote — idle tick
|
||||
}
|
||||
if (bridge_.writeUsageExtState(usageKeyFor(instanceGuid_), plan.wire)) {
|
||||
lastPublishedUsageWire_ = plan.wire;
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> built) {
|
||||
// REQUIRES reloadMutex_ held (single-writer over both slots + the graveyard). Shared by
|
||||
// reloadInstrument and rebuildVoiceEngine — the one safety-critical swap dance.
|
||||
|
||||
Reference in New Issue
Block a user