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 preAccumulated is true, the bytes represent qint64 values 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 through FtmwConfig::addPreAccumulatedFids() instead of the normal parse pipeline.

  • flushMarker — when true, the entry is a segment-boundary sentinel (data is empty and shotCount is 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.

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; pass reserveBytes > 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.