f7505d0e53
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.
77 lines
4.0 KiB
C++
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
|