memlnaut-nisps/nisps/core/ring_buffer.hpp
w1n5t0n 4f60fc8405 feat: nisps/core foundation — perf, types, concepts, buffers, rng, math
Greenfield C++20 core for the unified firmware+WASM rewrite (architecture.md
streams, meml-dn7). Header-only, platform-agnostic, no heap, no virtual
dispatch.

Components:
- perf.hpp        memory section + inlining macros, RP2040/RP2350-aware,
                  inert on host/Emscripten
- types.hpp       stereosample_t (mirrors firmware AudioDriver API),
                  sample_t/param_t aliases, DriverConfig negotiation struct
- concepts.hpp    MLEngine, AudioEngine, Mode (architecture §4.1-4.3)
- fixed_buffer.hpp  std::array-backed cursor; replaces std::vector in hot paths
- ring_buffer.hpp   SPSC lock-free FIFO, power-of-two capacity, atomic
                    head/tail; replaces pico/util/queue in core
- rng.hpp         xoshiro256+ with splitmix64 seeding, uniform/signed/
                  gaussian-via-3-uniforms (matches legacy MoveWeights shape)
- math.hpp        clamp01, fast_sigmoid (tanh-Padé, ~1.2% max err on [-6,6]),
                  exact_sigmoid, fast_exp, named Curve catalog (linear/exp/
                  log/square/sqrt/sigmoid/cubic) — TypeScript twin lives in
                  playground/src/output/curves.ts (stream 5)

Performance discipline (Chris's rules):
- No heap, no std::vector, no malloc/new in core
- All float literals carry .f suffix
- Memory section attrs syntactically present, inert on non-firmware builds
2026-04-29 15:21:52 +03:00

80 lines
3.1 KiB
C++

// nisps/core/ring_buffer.hpp — single-producer single-consumer lock-free FIFO.
//
// Replaces pico/util/queue across the platform-agnostic core. On firmware,
// the inter-core hand-off can wrap this OR use queue_t directly — that
// decision lives in stream 6 (firmware glue). Within `nisps/`, this is the
// canonical channel.
//
// Design notes
// - Capacity N must be a power of two. We mask the head/tail indices instead
// of taking modulus; this lets the indices wrap naturally at size_t and we
// compare them with subtraction (i.e. `head - tail == N` ⇒ full).
// - T must be trivially copyable. We do not run T's destructor on pop —
// callers want POD-shaped messages here, not RAII handles.
// - Memory orders follow Vyukov's classic SPSC pattern:
// producer: relaxed load(tail), [write slot], release store(head)
// consumer: relaxed load(head), acquire load(head), [read slot], release store(tail)
#pragma once
#include <atomic>
#include <cstddef>
#include <type_traits>
namespace nisps {
template <typename T, std::size_t N>
class RingBuffer {
static_assert(N > 0u, "RingBuffer capacity must be > 0");
static_assert((N & (N - 1u)) == 0u, "RingBuffer capacity must be power of two");
static_assert(std::is_trivially_copyable_v<T>,
"RingBuffer element type must be trivially copyable");
public:
static constexpr std::size_t capacity() noexcept { return N; }
RingBuffer() noexcept : head_(0u), tail_(0u) {}
// No copy / no move — atomics aren't trivially movable and there's no
// good story for "transfer half-full ring under contention".
RingBuffer(const RingBuffer&) = delete;
RingBuffer& operator=(const RingBuffer&) = delete;
bool try_push(const T& v) noexcept {
const auto head = head_.load(std::memory_order_relaxed);
const auto tail = tail_.load(std::memory_order_acquire);
if (head - tail >= N) return false; // full
buf_[head & kMask] = v;
head_.store(head + 1u, std::memory_order_release);
return true;
}
bool try_pop(T& out) noexcept {
const auto tail = tail_.load(std::memory_order_relaxed);
const auto head = head_.load(std::memory_order_acquire);
if (head == tail) return false; // empty
out = buf_[tail & kMask];
tail_.store(tail + 1u, std::memory_order_release);
return true;
}
// Approximate; relies on head/tail being read in arbitrary order. Use
// for diagnostics, not for synchronization.
std::size_t size_approx() const noexcept {
const auto h = head_.load(std::memory_order_relaxed);
const auto t = tail_.load(std::memory_order_relaxed);
return h - t;
}
bool empty_approx() const noexcept { return size_approx() == 0u; }
bool full_approx() const noexcept { return size_approx() >= N; }
private:
static constexpr std::size_t kMask = N - 1u;
// Head/tail use std::size_t and rely on natural unsigned wrap.
std::atomic<std::size_t> head_;
std::atomic<std::size_t> tail_;
T buf_[N]{};
};
} // namespace nisps