LiveEventQueue
A fixed-capacity ring for LiveEvent, sized at compile time.
template <int CapacityV> class LiveEventQueue
Declared in src/libs/libbahn/bahn/tracker/engine/LiveEventQueue.hpp.
Detailed description
specs/bahn-live-feedback §BLT-3a.
Why a ring and not a QVector
The drain runs inside `BahnPlayer::driveOneBlock`, which is the block drive. A `QVector` that grows allocates, and an allocation there makes the block time unpredictable (§BLT-2). The capacity is fixed at construction and never changes, so neither `push` nor `pop` can allocate.
Overflow drops the OLDEST
A full queue means events arrived faster than blocks drained it, which is a person holding a key or dragging a slider. The newest value is the one the person means, so the oldest is discarded and counted (§BLT-3d). A silent drop is indistinguishable from correct behaviour, which is why `droppedCount()` exists and why a test asserts it.
Threading: one producer, one consumer, no lock
Bahn renders on the GUI thread today (§BLT-1), so both ends are the same thread and any correct queue would do. This one is written for one producer and one consumer anyway, because `AudioSchedule` is written to be driven from a real-time thread and `AudioEngine` already carries an unused `QThreadPool`. The day either is wired up is the day a one-thread assumption becomes a defect nobody is looking for.
**The producer never touches the consumer's index.** An earlier version dropped the oldest event by advancing the read index from the producer side, which is a write to the consumer's state. `testBahnEventQueue` caught it under two threads within twenty thousand events: a value appeared out of order because both ends moved the same counter.
The indices are monotonic 64-bit counters rather than wrapped positions, so the consumer can see that it has been lapped by comparing them and can account for the gap itself. The producer only ever increments `mHead`. The consumer only ever increments `mTail`. Overflow is therefore detected and counted where it is observed, which is also where it is true.
**Each slot carries a sequence, so a torn read cannot escape.** Monotonic counters alone were not enough: a producer can lap the consumer between its bounds check and its copy, and the copy then mixes two events. The test caught that too, four runs in five.
Every slot stamps the write index before and after the payload. The consumer copies, then re-reads the stamp; if it moved, the slot was overwritten mid-copy, so the copy is discarded and counted as a drop rather than returned. That is a seqlock per slot, it allocates nothing, and it is why `pop` has a retry loop bounded by the capacity.