diff --git a/AGENT-LOG.md b/AGENT-LOG.md index 464efbb..22b7db3 100644 --- a/AGENT-LOG.md +++ b/AGENT-LOG.md @@ -594,3 +594,54 @@ The 17 meters band default range in `BookmarkMgr.cpp` (17.044-19.092 MHz) is inc | File | Action | |------|--------| | `docs/design/signal-flow.md` | Fixed 11 issues across pipeline diagram, stage descriptions, queue tables, ReBuffer description, visual pipeline, and push semantics | + +## Session 21: Signal Flow Design Document Verification + +**Date:** 2026-07-28 +**Model:** opencode/mimo-v2.5-free + +### Actions + +1. Reviewed `docs/design/signal-flow.md` by verifying all claims against source code using 3 parallel agent tasks +2. Verified SDRThread, SDRPostThread (polyphase filterbank, DC blocking, visual data push), DemodulatorPreThread (frequency shifting, resampling, modem dispatch, blocking push), DemodulatorThread (modem callout, signal level, squelch, try_push), AudioThread (controller/bound pattern, static audioCallback), ReBuffer (shared_ptr pool, use_count scan, GC), all 6 data types, all 8 queue wirings, visual processing pipeline, and canvas polling behavior +3. Found 1 discrepancy (~99% accuracy): canvas polling location incorrectly attributed to `OnIdle()` for all canvases +4. Applied 1 fix + +### Issue Fixed + +| File | Issue | Fix | +|------|-------|-----| +| `docs/design/signal-flow.md` | "canvases poll their input queues via `try_pop()` in their `OnIdle()` handlers" — only WaterfallCanvas does this | Corrected: WaterfallCanvas uses `OnIdle()`; SpectrumCanvas and ScopeCanvas call `try_pop()` in `OnPaint()` | + +### Files Modified + +| File | Action | +|------|--------| +| `docs/design/signal-flow.md` | Fixed canvas polling location: clarified only WaterfallCanvas uses OnIdle(); SpectrumCanvas and ScopeCanvas poll in OnPaint() | + +## Session 22: Signal Flow Design Document Review + +**Date:** 2026-07-28 +**Model:** opencode/mimo-v2.5-free + +### Actions + +1. Reviewed `docs/design/signal-flow.md` by verifying all claims against source code using a detailed agent task +2. Verified all 12 categories: SDRThread, SDRPostThread, DemodulatorPreThread, DemodulatorThread, AudioThread, global queues, per-demodulator queues, ReBuffer pool, data types, visual pipeline, queue max sizes, ThreadBlockingQueue +3. Found 4 issues (~97% accuracy): 1 imprecise constant, 1 omitted push semantics, 1 unexplained type aliasing, 1 undocumented internal sub-pipeline +4. Applied all 4 fixes + +### Issues Fixed + +| # | Issue | Fix | +|---|-------|-----| +| 1 | ReBuffer GC threshold: `REBUFFER_GC_LIMIT` define is 100 (positive); code checks `age < -REBUFFER_GC_LIMIT` — document parenthetical "(-100)" could be misread as the define value | Changed to "i.e. age < -100" for clarity | +| 2 | SDRPostThread demod distribution uses non-blocking `try_push()` but document only mentions non-blocking for visuals | Documented that all output pushes use `try_push()`, including per-demodulator distribution | +| 3 | `DemodulatorThreadOutputQueue` and `AudioThreadInputQueue` are both `ThreadBlockingQueue` but document uses both names without explanation | Added note explaining they are aliases for the same underlying type | +| 4 | `FFTVisualDataThread` internal sub-pipeline (`FFTDataDistributor` → `fftQueue` → `SpectrumVisualProcessor`) not documented | Added description of internal rate-limiting pipeline | + +### Files Modified + +| File | Action | +|------|--------| +| `docs/design/signal-flow.md` | Fixed ReBuffer GC wording, documented non-blocking demod distribution, added type aliasing note, added FFTVisualDataThread internals | diff --git a/docs/design/signal-flow.md b/docs/design/signal-flow.md index c839073..c8fedcc 100644 --- a/docs/design/signal-flow.md +++ b/docs/design/signal-flow.md @@ -56,13 +56,13 @@ Receives raw IQ from SDRThread. Behavior depends on channel count: **Multi-channel mode** (`numChannels > 1`): 1. **Polyphase filterbank channelization** (`firpfbch_crcf` or `firpfbch2_crcf`) — splits wideband IQ into N sub-channels 2. **DC blocking** on channel 0 only via IIR filter (`iirfilt_crcf`) — other channels pass through unfiltered -3. **Distribution** — pushes each channel's data to its assigned demodulator's input queue +3. **Distribution** — non-blocking `try_push()` each channel's data to its assigned demodulator's input queue **Single-channel mode** (`numChannels <= 1`): 1. **DC blocking** on the entire bandwidth via IIR filter (`iirfilt_crcf`) -2. **Distribution** — pushes the full bandwidth to every active demodulator (no channelization) +2. **Distribution** — non-blocking `try_push()` the full bandwidth to every active demodulator (no channelization) -In both modes, also pushes full-rate IQ (DC-corrected) to visual processing queues for the main spectrum and waterfall displays. All visual queue pushes use **non-blocking** `try_push()` — visual data can be silently dropped when queues are full. +In both modes, also pushes full-rate IQ (DC-corrected in single-channel, raw in multi-channel) to visual processing queues for the main spectrum and waterfall displays. **All output pushes use non-blocking `try_push()`** — both visual data and per-demodulator data can be silently dropped when queues are full. SDRPostThread never blocks on any consumer. ### Stage 3: Demodulator Pre-Processing @@ -122,6 +122,8 @@ Manages RtAudio hardware output using a **controller/bound** pattern: Note: `audioVisOutputQueue` is a per-DemodulatorThread member that is bound at runtime to the global `pipeAudioVisualData` queue via `setOutputQueue("AudioVisualOutput", ...)`. `audioSinkOutputQueue` is bound dynamically only when WAV recording starts. +Note: `DemodulatorThreadOutputQueue` and `AudioThreadInputQueue` are both aliases for `ThreadBlockingQueue` (defined in `src/audio/AudioThread.h`). They carry the same data type; the distinct names reflect the queue's role in the pipeline. + ## Buffer Management ### ReBuffer Pool @@ -130,7 +132,7 @@ To avoid heap allocation on every frame, CubicSDR uses `ReBuffer` (`src/IOThr - `getBuffer()` scans for a buffer with `use_count == 1` (not in transit), resets its age, and returns it; if none available, allocates a new one - When the last consumer releases a buffer, its `use_count` drops to 1 and it becomes available for reuse -- Idle buffers age out: each time a buffer is selected, other unused buffers have their age decremented. When the oldest buffer's age drops below `-REBUFFER_GC_LIMIT` (-100), it is garbage collected +- Idle buffers age out: each time a buffer is selected, other unused buffers have their age decremented. When the oldest buffer's age drops below `-REBUFFER_GC_LIMIT` (i.e. age < -100), it is garbage collected - A warning is emitted if the pool exceeds `REBUFFER_WARNING_THRESHOLD` (2000) buffers Used by: SDRThread, SDRPostThread, DemodulatorPreThread, DemodulatorThread, SpectrumVisualProcessor, ScopeVisualProcessor. @@ -162,4 +164,6 @@ DemodulatorThread +--[audioVisOutputQueue]--> ScopeVisualProcessor --> ScopeCanvas (UI thread) ``` -The UI thread is **pull-based on the consumer side** — canvases poll their input queues via `try_pop()` in their `OnIdle()` handlers. However, producer threads **push** data into these queues via non-blocking `try_push()`, which can silently drop data when queues are full. This is intentional for visual data where occasional dropped frames are acceptable. +`FFTVisualDataThread` contains an internal sub-pipeline: an `FFTDataDistributor` accumulates raw IQ samples into FFT-sized chunks, which are then processed by a `SpectrumVisualProcessor` to produce FFT output. The `FFTDataDistributor` rate-limits by `linesPerSecond`, so not every incoming IQ frame produces an output line. + +The UI thread is **pull-based on the consumer side** — canvases poll their input queues via `try_pop()`, though the exact trigger varies: WaterfallCanvas does this in its `OnIdle()` handler, while SpectrumCanvas and ScopeCanvas do it in `OnPaint()`. Meanwhile, producer threads **push** data into these queues via non-blocking `try_push()`, which can silently drop data when queues are full. This is intentional for visual data where occasional dropped frames are acceptable.