WaveformBuffer and WaveformEntry
WaveformBuffer is a bounded, single-producer/single-consumer (SPSC) ring
buffer that transfers waveform data from the digitizer hardware thread to the
acquisition manager thread. It provides bounded memory usage, drop-newest
overflow, and low-latency consumer notification via QSemaphore.
WaveformBuffer does not inherit QObject. It is created and owned by the
FtmwDigitizer hardware object; FtmwConfig holds a non-owning
pointer to it (set via setWaveformBuffer() during acquisition setup and
retrieved by AcquisitionManager via waveformBuffer()). The digitizer
data-flow design, including the rationale for the ring-buffer approach, is
described in FTMW Acquisition and Visualization.
Thread-safety discipline
Exactly one producer thread (the digitizer hardware thread) and one
consumer thread (the acquisition manager thread) may access the buffer at any
time. Multi-producer or multi-consumer usage is undefined behavior. The producer
calls write(), writeFlushMarker(), and reset(); the consumer calls
read(), drainAll(), available(), and waitForData().
Overflow policy
The producer never blocks. When the buffer is full, the incoming entry is
silently discarded (drop-newest policy) and an internal counter is incremented.
Call droppedCount() to retrieve the total number of dropped entries since
the last reset(). The drop-newest approach is race-free because the
producer never modifies the read index.
When the producer detects a full buffer via isFull(), it may optionally
begin accumulating incoming waveforms locally (pre-accumulation). Once a slot
becomes available, the accumulated result is written as a single entry with
preAccumulated = true, allowing the consumer to bypass the normal parse
pipeline for that entry.
Consumer notification
The producer releases a QSemaphore after each successful write. The
consumer calls waitForData(timeoutMs) to block until at least one entry is
available, then drains the buffer with drainAll() or reads entries one at a
time with read(). The timeout allows the consumer thread to perform periodic
housekeeping (autosave checks, abort polling) even when no waveform data
arrives.
WaveformEntry
Each ring slot holds a WaveformEntry struct with four fields:
data — raw waveform bytes (empty for flush markers). When
preAccumulatedistrue, the bytes representqint64values rather than raw digitizer samples.shotCount — number of shots or FIDs represented by this entry. For a single raw waveform this equals the digitizer’s shot-increment value; for a pre-accumulated entry it equals the sum of all accumulated shot increments.
preAccumulated — when
true, the consumer routes the entry throughFtmwConfig::addPreAccumulatedFids()instead of the normal parse pipeline.flushMarker — when
true, the entry is a segment-boundary sentinel (datais empty andshotCountis zero). The consumer drains all preceding entries for the current segment before advancing to the next one.
API Reference
-
struct WaveformEntry
A single entry in the waveform ring buffer.
For normal waveform data, data contains the raw bytes and shotCount holds the number of shots/FIDs represented. For segment boundary sentinels, flushMarker is true and data is empty.
Public Members
-
QByteArray data
Raw waveform bytes (empty for flush markers)
-
quint64 shotCount = {0}
Number of shots/FIDs represented by this entry.
-
bool preAccumulated = {false}
If true, data contains qint64 values (pre-summed)
-
bool flushMarker = {false}
If true, this is a segment boundary sentinel.
-
QByteArray data
-
class WaveformBuffer
Bounded SPSC ring buffer for transferring waveform data between threads.
Single-producer/single-consumer: the digitizer hardware thread is the producer (
write(),writeFlushMarker(),reset()) and the acquisition manager thread is the consumer (read(),drainAll(),available(),waitForData()). Multi-producer or multi-consumer usage is undefined behavior. The producer never blocks; when the buffer is full the incoming entry is silently dropped (droppedCount()exposes the count). Slot storage is pre-allocated at construction; passreserveBytes> 0 to also pre-reserve each slot’s QByteArray and avoid per-shot heap allocation. Does not inherit from QObject.Public Functions
-
explicit WaveformBuffer(int capacity = 10, qint64 reserveBytes = 0)
Construct a WaveformBuffer.
- Parameters:
capacity – Number of slots in the ring buffer (fixed at creation).
reserveBytes – If > 0, pre-reserve each slot’s QByteArray to this size to avoid per-shot heap allocation during steady-state operation.
-
~WaveformBuffer() = default
-
WaveformBuffer(const WaveformBuffer&) = delete
-
WaveformBuffer &operator=(const WaveformBuffer&) = delete
-
WaveformBuffer(WaveformBuffer&&) = delete
-
WaveformBuffer &operator=(WaveformBuffer&&) = delete
-
void write(const QByteArray &data, quint64 shotCount, bool preAccumulated = false)
Write a waveform entry into the buffer.
If the buffer is full, the incoming entry is discarded (drop-newest policy) and the dropped counter is incremented. This call never blocks.
- Parameters:
data – Raw waveform bytes.
shotCount – Number of shots/FIDs represented by this entry.
preAccumulated – If true, data contains qint64 values (pre-summed).
-
void writeFlushMarker()
Write a segment boundary sentinel into the buffer.
The entry has empty data and flushMarker == true. Same drop-newest overflow behavior as write().
-
bool isFull() const
Check if the buffer is full (producer-side query).
Uses the same logic as writeEntry’s overflow check. This allows the producer to decide whether to begin pre-accumulation before attempting a write.
- Returns:
true if (writeIndex - readIndex) >= capacity.
-
void reset()
Clear all entries and reset indices.
Call at the start of acquisition. This is NOT thread-safe with concurrent producer/consumer activity — call only when both threads are quiescent.
-
bool read(WaveformEntry &out)
Non-blocking read of a single entry.
Moves data out of the ring slot to avoid a copy. Returns false immediately if no entries are available.
- Parameters:
out – Receives the entry on success.
- Returns:
true if an entry was available and written to out.
-
int available() const
Number of entries currently available for reading.
- Returns:
Count of unread entries.
-
bool waitForData(int timeoutMs = 100)
Block until data is available or the timeout expires.
Uses QSemaphore::tryAcquire internally. After this returns true, use drainAll() or read() to consume the entries.
- Parameters:
timeoutMs – Maximum time to wait in milliseconds (default 100 ms).
- Returns:
true if at least one entry is available.
-
int drainAll(std::vector<WaveformEntry> &out)
Drain all available entries into out.
More efficient than calling read() in a loop. Appends to out rather than clearing it first, so the caller may pre-size the vector if desired. This is the primary consumption API.
- Parameters:
out – Vector to append drained entries to.
- Returns:
Number of entries drained.
-
quint64 droppedCount() const
Number of entries dropped due to overflow since last reset().
- Returns:
Dropped entry count.
-
int capacity() const
Ring buffer capacity (number of slots).
- Returns:
Capacity set at construction.
Private Functions
-
void writeEntry(WaveformEntry &&entry)
Internal write implementation shared by write() and writeFlushMarker().
Handles slot reuse, overflow detection, index advancement, and semaphore release.
- Parameters:
entry – Entry to write (moved into the slot).
Private Members
-
const int d_capacity
Fixed ring buffer capacity.
-
QVector<WaveformEntry> d_slots
Pre-allocated ring slot storage.
-
std::atomic<int> d_writeIndex = {0}
Next slot to write (producer-owned)
-
std::atomic<int> d_readIndex = {0}
Next slot to read (consumer-owned)
-
std::atomic<quint64> d_droppedCount = {0}
Overflow drop counter.
-
QSemaphore d_semaphore
Consumer notification semaphore.
-
explicit WaveformBuffer(int capacity = 10, qint64 reserveBytes = 0)