Files
reasampler/src/core/instrument/engine/seqlock.h
T
daniel f7505d0e53 Make spline EG drags audible live: held and new notes follow a dragged knot
Contours ride their own seqlock block into a double-buffered engine copy, and the swap step is smoothed. Each drag move publishes contours without a model write; release still commits and reloads.
2026-09-25 13:42:25 -04:00

77 lines
4.0 KiB
C++

#pragma once
// seqlock.h — the lock-free publication every live block rides from its off-thread writer to
// the audio thread: a single-writer seqlock over one plain value. Generic over that value so the
// live-parameter block and the drawn-contour block share one implementation of the protocol.
#include <atomic>
#include <cstdint>
#include <type_traits>
namespace reasampler::instrument::engine {
// Single-writer seqlock. The writer publishes a whole block between an odd and an even
// generation; a reader copies the block and re-checks the generation, retrying a bounded number
// of times, so it can never act on a half-applied edit. Wait-free for the reader: after the
// retry budget it reports "nothing new" and the caller keeps its last good snapshot rather than
// spinning on the audio thread. Reading does not mutate, so any number of readers may share one
// block (the live and the draining engine both do).
//
// SINGLE-WRITER IS THE CALLER'S JOB and is load-bearing: two concurrent writers can leave the
// generation EVEN mid-write (A stores gen+1, B reads odd and stores gen+2) while both copy the
// block, and a reader then accepts a torn block as coherent. Every publisher must serialize.
//
// The plain (non-atomic) block copied across the fences is the standard pragmatic seqlock:
// the fences give correct ordering, but the concurrent read of a non-atomic object is a data
// race under the C++ object model, so TSan/UBSan will report it. That report is expected, not
// a defect — there is no clean lock-free standard-C++ alternative that keeps the block a plain
// value the audio thread can copy in one shot.
//
// The writer interface deliberately assumes NO particular thread beyond single-writer, so a
// host's own parameter-change queue (delivered on the audio thread with sample offsets) can
// drive it later without a redesign.
template <class T>
class Seqlock {
static_assert(std::is_trivially_copyable_v<T>,
"the block is copied under a seqlock — it must stay a plain value");
public:
// A generation of 0 means "never published"; the first publish lands on 2.
void publish(const T& values) {
const std::uint32_t gen = seq_.load(std::memory_order_relaxed);
seq_.store(gen + 1, std::memory_order_relaxed); // odd: a write is in progress
std::atomic_thread_fence(std::memory_order_release);
values_ = values;
std::atomic_thread_fence(std::memory_order_release);
// Skip 0 on wrap (~2^31 publishes): landing there would read as "never published" and
// stall every reader until the NEXT publish — a silent mode, unlike a loud one.
const std::uint32_t next = (gen + 2 == 0u) ? 2u : gen + 2;
seq_.store(next, std::memory_order_release); // even: complete and coherent
}
// The last generation published, without copying the block — one relaxed load, so a reader
// that only needs "has anything moved" pays nothing for asking on a block where nothing has.
std::uint32_t generation() const { return seq_.load(std::memory_order_relaxed); }
// Copies the block into `out` and returns the generation actually observed, or 0 when
// nothing has been published yet or the retry budget ran out (in which case `out` may hold
// a torn copy and MUST be discarded — compare the return against 0 before using it).
std::uint32_t read(T& out, int maxAttempts = 4) const {
for (int attempt = 0; attempt < maxAttempts; ++attempt) {
const std::uint32_t before = seq_.load(std::memory_order_acquire);
if (before == 0) return 0; // never published
if ((before & 1u) != 0u) continue; // writer mid-update
std::atomic_thread_fence(std::memory_order_acquire);
out = values_;
std::atomic_thread_fence(std::memory_order_acquire);
if (seq_.load(std::memory_order_relaxed) == before) return before;
}
return 0;
}
private:
std::atomic<std::uint32_t> seq_{0};
T values_{};
};
} // namespace reasampler::instrument::engine