// 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 #include #include #include #include #include #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; // 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 2–4 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(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(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 readExtStateValue(ReaProject* proj, const char* key) { for (int cap = 1 << 12; cap <= (1 << 24); cap <<= 2) { std::vector buf(static_cast(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(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(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 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(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> decoded; decoded.reserve(usageKeys.size()); for (std::size_t ki = 0; ki < usageKeys.size(); ++ki) { const std::string& key = usageKeys[ki]; const std::optional 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 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 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