Files
reasampler/src/usage_scan.cpp
T

276 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// usage_scan.cpp — see usage_scan.h. The REAPER reads behind the pS-usage prune
// protection; every decision is in the pure sample_usage module, this TU only reads.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API pointers
// (CLAUDE.md §contract). Every REAPER symbol used here is verified against
// vendor/reaper-sdk/sdk/reaper_plugin_functions.h:
// * EnumProjExtState(proj, extname, idx, keyOut, sz, valOut, sz) -> bool (~1272)
// * GetProjExtState(proj, extname, key, valOut, sz) -> int (~2591)
// * CountTracks / GetTrack / GetMasterTrack (track scan)
// * TrackFX_GetCount(MediaTrack*) / TrackFX_GetRecCount(MediaTrack*) (~7283/7570)
// * TrackFX_GetNamedConfigParm(MediaTrack*, int, parm, buf, sz) -> bool (~7377)
// * CountMediaItems / GetMediaItem (~423/1964)
// * CountTakes(MediaItem*) / GetMediaItemTake(MediaItem*, int) (~471/2029)
// * GetMediaItemTrack(MediaItem*) (~2133)
// * TakeFX_GetCount / TakeFX_GetNamedConfigParm (~6710/6774)
// * guidToString (via track_guid::guidString)
#include "usage_scan.h"
#include <cstdlib>
#include <functional>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
#include "app_version.h" // vstPluginName / vstOutputName (channel name needles)
#include "ext_keys.h" // kProjExtNamespace / kProjExtUsageKeyPrefix
#include "instrument_drop.h" // vstClassIdHex — the frozen channel class-UID hex
#include "sample_usage.h" // identityMatches, foldUsageRecords (the pure decisions)
#include "track_guid.h" // guidString — the ONE canonical GUID key formatter
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjExtState
#define REAPERAPI_WANT_GetProjExtState
#define REAPERAPI_WANT_CountTracks
#define REAPERAPI_WANT_GetTrack
#define REAPERAPI_WANT_GetMasterTrack
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetRecCount
#define REAPERAPI_WANT_TrackFX_GetNamedConfigParm
#define REAPERAPI_WANT_CountMediaItems
#define REAPERAPI_WANT_GetMediaItem
#define REAPERAPI_WANT_CountTakes
#define REAPERAPI_WANT_GetMediaItemTake
#define REAPERAPI_WANT_GetMediaItemTrack
#define REAPERAPI_WANT_TakeFX_GetCount
#define REAPERAPI_WANT_TakeFX_GetNamedConfigParm
#include "reaper_plugin_functions.h"
namespace reasampler {
namespace {
// The three UPPERCASED channel needles identityMatches (pure, sample_usage) checks
// every FX identity string against. One instance drives the whole scan.
struct FxIdentityNeedles {
std::string uidHexUpper; // 32-hex VST3 class UID (may not appear on all builds)
std::string outputNameUpper; // "REASAMPLER_9000" — the .vst3 filename base fx_ident embeds
std::string nameUpper; // "REASAMPLER 9000" — factory display name
};
// A named-config-parm getter abstracted over the FX-chain kind: track FX and take FX
// share the identical identity walk (fx_ident + original_name + container recursion),
// differing only in which REAPER getter reads the parm.
using FxParmGetter =
std::function<std::string(int fxId, const char* parm)>;
// True if any FX in the (possibly container-nested) sub-chain rooted at `fxId` is a
// ReaSampler 9000. BOTH fx_ident and original_name are checked on BOTH chain kinds (a
// renamed instance may keep its original_name; fx_ident carries the module path — the
// primary identification net is the module filename base via fx_ident, which holds even
// after a user renames the FX instance). Containers are walked via
// the documented container_count / container_item.X addressing (v7.06+); on a chain
// kind or REAPER version without containers the parm read returns empty and recursion
// is a no-op. `depth` bounds pathological nesting. fx_ident is queried per FX — chain
// enumeration is chunk-level, so OFFLINE instances match too (load-bearing: a
// Design-View-parked instance must keep protecting its holds).
bool fxSubtreeHasInstance(const FxParmGetter& parm, int fxId,
const FxIdentityNeedles& id, int depth) {
if (identityMatches(parm(fxId, "fx_ident"), id.uidHexUpper, id.nameUpper,
id.outputNameUpper) ||
identityMatches(parm(fxId, "original_name"), id.uidHexUpper, id.nameUpper,
id.outputNameUpper))
return true;
const std::string countStr = parm(fxId, "container_count");
if (countStr.empty()) return false; // not a container; no children to miss
if (depth <= 0) {
// This node IS a container but we have exhausted our descent budget. We cannot
// prove that none of its children is a ReaSampler 9000 instance — treat the
// incomplete walk as a positive identification (the protect direction). This is
// defense-in-depth: kMaxContainerDepth = 32 should prevent reaching this branch
// in any real project, but if it IS reached the fail-safe fires rather than
// silently missing a live nested instance.
return true;
}
const int n = std::atoi(countStr.c_str());
for (int k = 0; k < n; ++k) {
const std::string item =
parm(fxId, ("container_item." + std::to_string(k)).c_str());
if (item.empty()) continue;
const int childId = std::atoi(item.c_str());
if (childId <= 0) continue;
if (fxSubtreeHasInstance(parm, childId, id, depth - 1)) return true;
}
return false;
}
// Raised from 8 to 32 (defense in depth against truncation). Real-world FX containers
// are typically 24 levels deep; 32 is unreachable in practice while remaining finite.
// Even at 32, the truncation→protect-all guard below is the primary protection.
constexpr int kMaxContainerDepth = 32;
std::string trackFxParm(MediaTrack* tr, int fxId, const char* parm) {
char buf[2048] = {0};
if (!TrackFX_GetNamedConfigParm(tr, fxId, parm, buf, static_cast<int>(sizeof(buf))))
return {};
return std::string(buf);
}
std::string takeFxParm(MediaItem_Take* take, int fxId, const char* parm) {
char buf[2048] = {0};
if (!TakeFX_GetNamedConfigParm(take, fxId, parm, buf, static_cast<int>(sizeof(buf))))
return {};
return std::string(buf);
}
// True if `tr` hosts >= 1 ReaSampler 9000 anywhere: normal chain, record/input chain
// (index | 0x1000000), containers recursively.
bool trackHasInstance(MediaTrack* tr, const FxIdentityNeedles& id) {
const FxParmGetter parm = [tr](int fxId, const char* p) {
return trackFxParm(tr, fxId, p);
};
const int n = TrackFX_GetCount(tr);
for (int i = 0; i < n; ++i) {
if (fxSubtreeHasInstance(parm, i, id, kMaxContainerDepth)) return true;
}
const int rec = TrackFX_GetRecCount(tr);
for (int i = 0; i < rec; ++i) {
if (fxSubtreeHasInstance(parm, 0x1000000 + i, id, kMaxContainerDepth))
return true;
}
return false;
}
// True if any take FX on `item` is a ReaSampler 9000 (all takes, not just active — a
// non-active take's instance still exists in the project and reactivates with the
// take). The SAME identity walk as the track path: fx_ident + original_name + container
// recursion (an unrecognized exotic still lands in the pure protect-all net — records
// with zero identified instances protect everything rather than nothing).
bool itemHasInstance(MediaItem* item, const FxIdentityNeedles& id) {
const int takes = CountTakes(item);
for (int t = 0; t < takes; ++t) {
MediaItem_Take* take = GetMediaItemTake(item, t);
if (!take) continue;
const FxParmGetter parm = [take](int fxId, const char* p) {
return takeFxParm(take, fxId, p);
};
const int n = TakeFX_GetCount(take);
for (int i = 0; i < n; ++i) {
if (fxSubtreeHasInstance(parm, i, id, kMaxContainerDepth)) return true;
}
}
return false;
}
// Growing GetProjExtState read (the persist.cpp idiom): the usage record scales with
// the hold count, so a fixed buffer risks a truncated decode. Returns nullopt when the
// key cannot be read WHOLE — absent-after-enumeration (rv <= 0) or pathologically large
// (> 16 MB give-up). The caller only queries keys the enumeration just listed, so a
// nullopt here is a PRESENT-BUT-UNREADABLE record: it folds to abortPrune (fail-safe —
// silently reduced protection is the delete direction).
std::optional<std::string> readExtStateValue(ReaProject* proj, const char* key) {
for (int cap = 1 << 12; cap <= (1 << 24); cap <<= 2) {
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
const int rv = GetProjExtState(proj, kProjExtNamespace(), key, buf.data(), cap);
if (rv <= 0) return std::nullopt;
std::string s(buf.data());
if (static_cast<int>(s.size()) + 1 < cap) return s;
// else possibly truncated -> grow and retry
}
return std::nullopt; // > 16 MB — unreadable whole, never "absent"
}
} // namespace
UsageScanResult liveInstanceHeldPaths(void* projOpaque) {
ReaProject* proj = static_cast<ReaProject*>(projOpaque);
UsageScanResult result;
// 1. Enumerate the rsusage_* keys and read+decode each record. Key names first
// (values via the growing reader — EnumProjExtState's fixed val buffer could
// truncate a large record). A nullopt element = present-but-unreadable/
// undecodable -> the pure fold ABORTS the prune.
std::vector<std::string> usageKeys;
{
const std::string prefix = kProjExtUsageKeyPrefix; // hoisted: one alloc, not N
char keyBuf[256];
for (int idx = 0;; ++idx) {
keyBuf[0] = '\0';
if (!EnumProjExtState(proj, kProjExtNamespace(), idx, keyBuf,
static_cast<int>(sizeof(keyBuf)), nullptr, 0))
break;
const std::string key(keyBuf);
if (key.compare(0, prefix.size(), prefix) == 0) usageKeys.push_back(key);
}
}
if (usageKeys.empty()) return result; // no instance ever published — skip the scan
std::vector<std::optional<UsageRecord>> decoded;
decoded.reserve(usageKeys.size());
for (std::size_t ki = 0; ki < usageKeys.size(); ++ki) {
const std::string& key = usageKeys[ki];
const std::optional<std::string> value = readExtStateValue(proj, key.c_str());
if (!value) {
decoded.push_back(std::nullopt); // unreadable -> abort (pure fold)
result.offendingKeys.push_back(key);
continue;
}
const std::optional<UsageRecord> rec = decodeUsageRecord(*value);
if (!rec) result.offendingKeys.push_back(key);
decoded.push_back(rec); // undecodable nullopt -> abort
}
// 2. Enumerate live ReaSampler 9000 hosts. One channel-frozen needle set drives
// every match; a track needs only ONE instance to keep all its records live.
FxIdentityNeedles id;
id.uidHexUpper = toUpperAscii(vstClassIdHex());
id.outputNameUpper = toUpperAscii(vstOutputName());
id.nameUpper = toUpperAscii(vstPluginName());
std::unordered_set<std::string> liveTrackGuids;
bool anyLive = false;
if (MediaTrack* master = GetMasterTrack(proj)) {
if (trackHasInstance(master, id)) {
liveTrackGuids.insert(guidString(master));
anyLive = true;
}
}
const int trackCount = CountTracks(proj);
for (int i = 0; i < trackCount; ++i) {
MediaTrack* tr = GetTrack(proj, i);
if (!tr) continue;
if (trackHasInstance(tr, id)) {
liveTrackGuids.insert(guidString(tr));
anyLive = true;
}
}
// Take-FX instances: attributed to the owning track (the VST-side getReaperParent(1)
// resolves the same track), and they set anyLive for the empty-guid fallback.
const int itemCount = CountMediaItems(proj);
for (int i = 0; i < itemCount; ++i) {
MediaItem* item = GetMediaItem(proj, i);
if (!item) continue;
if (itemHasInstance(item, id)) {
if (MediaTrack* tr = GetMediaItemTrack(item)) {
liveTrackGuids.insert(guidString(tr));
}
anyLive = true;
}
}
// 3. The pure fold decides: abort on any unreadable record; protect-all when zero
// instances were identified; otherwise the per-record liveness rule.
const UsageFoldResult fold = foldUsageRecords(decoded, liveTrackGuids, anyLive);
result.abortPrune = fold.abortPrune;
result.heldPaths = fold.heldPaths;
// offendingKeys already populated above (unreadable + undecodable entries);
// clear it on success so callers see it only when abortPrune is set.
if (!result.abortPrune) result.offendingKeys.clear();
return result;
}
} // namespace reasampler