fix(S4): close graveyard epoch ordering race; add canvas-clip + stride integration tests
Tag LoadedInstrument with installedAt; process() publishes that field (not a re-read of reloadGeneration_) so the pruner's displacedAt <= seen bound is airtight. Also fixes sampleRowHitTest canvas.bottom over-read and adds interleave-stride + canvas-clip tests.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "reasampler_processor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
@@ -121,7 +122,6 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
||||
int32 got = 0;
|
||||
while (state->read(chunk, sizeof(chunk), &got) == kResultOk && got > 0) {
|
||||
bytes.insert(bytes.end(), chunk, chunk + got);
|
||||
if (got < static_cast<int32>(sizeof(chunk))) break;
|
||||
}
|
||||
setSelectedSampleId(deserializeSelection(bytes));
|
||||
// Rebuild from the restored selection (off-thread — setState is a load-time call).
|
||||
@@ -133,8 +133,9 @@ tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
|
||||
if (!state) return kResultFalse;
|
||||
const std::vector<std::uint8_t> bytes = serializeSelection(selectedSampleId());
|
||||
if (!bytes.empty()) {
|
||||
state->write(const_cast<std::uint8_t*>(bytes.data()),
|
||||
static_cast<int32>(bytes.size()), nullptr);
|
||||
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
|
||||
static_cast<int32>(bytes.size()), nullptr);
|
||||
if (wr != kResultOk) return wr;
|
||||
}
|
||||
return kResultOk;
|
||||
}
|
||||
@@ -155,6 +156,10 @@ std::string ReaSamplerProcessor::reloadFromBank() {
|
||||
// thread — process() only touches the atomic.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
|
||||
// Mint this reload's generation number first so we can stamp the built instrument
|
||||
// with it before publishing. Under reloadMutex_ no other reload races here.
|
||||
const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
|
||||
// 1. Read the live bank + resolve the project dir over the bridge (allocates,
|
||||
// calls REAPER — fine here, off-thread).
|
||||
std::optional<std::string> banksJson =
|
||||
@@ -187,7 +192,7 @@ std::string ReaSamplerProcessor::reloadFromBank() {
|
||||
static_cast<int>(layout.sampleRate), sel->rootNote,
|
||||
sel->loop);
|
||||
built = std::make_unique<LoadedInstrument>(
|
||||
std::move(km), kMaxVoices, tier0Adsr(sampleRate_));
|
||||
std::move(km), kMaxVoices, tier0Adsr(sampleRate_), gen);
|
||||
// Record which id actually resolved so a first-sample fallback
|
||||
// (empty stored id) becomes the concrete selection.
|
||||
resolvedId = selectedSampleId();
|
||||
@@ -198,19 +203,43 @@ std::string ReaSamplerProcessor::reloadFromBank() {
|
||||
}
|
||||
|
||||
// 4. Publish. Atomically install the new instrument; the DISPLACED one goes to the
|
||||
// graveyard (process may still be reading it this block — it is reclaimed only when
|
||||
// process is stopped, in setActive(false)/terminate). A null `built` (no bank /
|
||||
// unreadable WAV) installs silence. `built` is heap-owned; release() hands
|
||||
// ownership to the atomic, and the exchanged pointer is re-owned by the graveyard.
|
||||
// graveyard tagged with this generation (process may still be mid-block reading
|
||||
// it). A null `built` (no bank / unreadable WAV) installs silence.
|
||||
// `built` is heap-owned; release() hands ownership to the atomic, and the
|
||||
// exchanged pointer is re-owned by the graveyard.
|
||||
//
|
||||
// Bounded reclaim: prune graveyard entries where displacedAt <= seen, where seen
|
||||
// is the last generation process() published. process() publishes inst->installedAt
|
||||
// (not a re-read of reloadGeneration_), so seen == D means process holds the
|
||||
// instrument installed at gen D. An entry with displacedAt == D was displaced by
|
||||
// reload D, which installed that very successor — process cannot be holding the
|
||||
// displaced entry. The pruning condition is therefore <= (see header for the full
|
||||
// proof). Remaining entries drain at setActive(false) / terminate() when process
|
||||
// is guaranteed stopped.
|
||||
const std::uint64_t seen = processGeneration_.load(std::memory_order_acquire);
|
||||
graveyard_.erase(
|
||||
std::remove_if(graveyard_.begin(), graveyard_.end(),
|
||||
[seen](const GraveyardEntry& e) { return e.displacedAt <= seen; }),
|
||||
graveyard_.end());
|
||||
LoadedInstrument* prev = live_.exchange(built.release());
|
||||
if (prev) graveyard_.emplace_back(prev);
|
||||
if (prev) graveyard_.push_back({gen, std::unique_ptr<LoadedInstrument>(prev)});
|
||||
return resolvedId;
|
||||
}
|
||||
|
||||
tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
// REAL-TIME: no allocation, no IO, no locks. Load the live instrument once for the
|
||||
// whole block (a single atomic acquire).
|
||||
// whole block (a single atomic acquire), then publish inst->installedAt so the off-
|
||||
// thread graveyard pruner knows exactly which generation this block is holding.
|
||||
//
|
||||
// We publish installedAt — not a fresh re-read of reloadGeneration_ — to close an
|
||||
// ordering race: reading reloadGeneration_ after live_ could observe a generation
|
||||
// newer than the pointer we actually hold, causing the pruner to free an instrument
|
||||
// process is still reading. installedAt was set on the reload path before the atomic
|
||||
// exchange that made the instrument visible, so it is always <= the generation of any
|
||||
// instrument that could have been loaded after our acquire above.
|
||||
LoadedInstrument* inst = live_.load(std::memory_order_acquire);
|
||||
const std::uint64_t heldGen = inst ? inst->installedAt : 0;
|
||||
processGeneration_.store(heldGen, std::memory_order_release);
|
||||
|
||||
// Marshal MIDI note-on/off from the event input into the voice engine. Tier 0 maps
|
||||
// events at block granularity (no per-event sample-offset split) — audible timing is
|
||||
|
||||
Reference in New Issue
Block a user