memlnaut-nisps/nisps/core/event_queue.hpp
monkey-w1n5t0n 96737a3d42 refactor(engines): extract the shared sequencer machinery
Phase 3 (L8). Pure statement-for-statement relocation into
dsp/ratio_seq.hpp, dsp/seq_clock.hpp and core/event_queue.hpp.

AUDIT CORRECTION: L8 says "breakor and elysiamorf duplicate ratio_seq". That is
false — ElysiamorfEngine has no ratio_seq at all; it triggers continuously via
FM operators. The real duplicate pair is BreakOrEngine and MEMLCeliumEngine,
whose copies are byte-for-byte identical. Elysiamorf did share the clock and
event-queue machinery, so it uses those. memlcelium now includes the shared
ratio_seq too, which is what actually closes this finding.

Deliberately NOT folded into core/ring_buffer.hpp: RingBuffer is an
atomics-based cross-core SPSC channel (its header says so), whereas the engines'
event queue is produced and drained on one thread. Reusing it would have meant
paying for atomics to serve a single-threaded FIFO. The distinction is now
recorded in MAP.md so the next audit does not read them as duplicates.

Bit-exactness: verified the MIDI-clock tick and bar-phasor tick preserve the
original operation order with no floating-point re-association, and that
EventQueue keeps the original `% N` indexing rather than adopting RingBuffer's
bitmask. The golden suite (nisps_golden_tests) and the native<->WASM parity blob
both pass unchanged — they are the check, and they were not re-baselined.
2026-07-21 14:02:23 +02:00

73 lines
2.6 KiB
C++

// nisps/core/event_queue.hpp — same-thread, batch-drain FIFO for engine
// output events (NoteOn/NoteOff/Clock/CC, ...).
//
// This is NOT a replacement for RingBuffer (nisps/core/ring_buffer.hpp).
// RingBuffer is an atomics-based SPSC channel for genuine cross-thread /
// cross-core hand-off (its own header: "the inter-core hand-off can wrap
// this OR use queue_t directly"). Sequencer engines push events from inside
// `process()` and the mode layer drains them via `pop_events()` right after
// — same call chain, same thread, never concurrent — so there is nothing to
// synchronize. Reusing RingBuffer here would add atomic load/store traffic
// to the audio-hot `process()` path for no correctness benefit, and would
// still need a wrapping loop to get the "drain up to N in one call" batch
// shape `pop_events()` callers rely on (RingBuffer::try_pop is one element
// at a time). EventQueue is deliberately the plain, non-atomic version of
// that shape.
//
// Extracted from the byte-for-byte-identical event-queue member blocks
// previously duplicated in nisps/engines/breakor.hpp and
// nisps/engines/elysiamorf.hpp (2026-07 simplification audit, finding L8).
#pragma once
#include <array>
#include <cstddef>
#include <span>
#include <type_traits>
#include "perf.hpp"
namespace nisps {
template <typename T, std::size_t N>
class EventQueue {
static_assert(N > 0u, "EventQueue capacity must be > 0");
static_assert(std::is_trivially_copyable_v<T>,
"EventQueue element type must be trivially copyable");
public:
static constexpr std::size_t capacity() noexcept { return N; }
// Enqueues one event. Drops silently on overflow (matches the engines'
// original push_event behaviour — a full event queue on a stalled
// consumer should not stall or branch the audio-hot producer).
NISPS_FORCE_INLINE void push(const T& e) noexcept {
if (count_ >= N) return;
buf_[write_] = e;
write_ = (write_ + 1u) % N;
++count_;
}
// Drains up to `out.size()` queued events into `out`. Returns the number
// actually copied.
std::size_t pop(std::span<T> out) noexcept {
std::size_t n = 0u;
while (n < out.size() && count_ > 0u) {
out[n++] = buf_[read_];
read_ = (read_ + 1u) % N;
--count_;
}
return n;
}
std::size_t size() const noexcept { return count_; }
bool empty() const noexcept { return count_ == 0u; }
private:
std::array<T, N> buf_{};
std::size_t read_ = 0u;
std::size_t write_ = 0u;
std::size_t count_ = 0u;
};
} // namespace nisps