Reduce DAX RX latency on Linux by shrinking pipe buffers (#1008)#1901
Closed
aethersdr-agent[bot] wants to merge 1 commit into
Closed
Reduce DAX RX latency on Linux by shrinking pipe buffers (#1008)#1901aethersdr-agent[bot] wants to merge 1 commit into
aethersdr-agent[bot] wants to merge 1 commit into
Conversation
The RX module-pipe-source modules were loaded without a pipe_size parameter, defaulting to 65536 bytes (~1365ms at 24kHz mono s16le). The TX sink already used pipe_size=2048. Add the same pipe_size=2048 to RX sources and shrink the kernel pipe buffer with F_SETPIPE_SZ to 4096 bytes, reducing the RX buffering latency by ~100-150ms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 tasks
Collaborator
|
Closing as part of a maintenance cleanup of stale AetherClaude PRs. The underlying issue this PR addressed remains tracked separately, and we plan to revisit it later. |
RiskAndReward1337
added a commit
to RiskAndReward1337/AetherSDR
that referenced
this pull request
May 3, 2026
) Replaces module-pipe-source with a native libpipewire-0.3 pw_stream per DAX channel. This eliminates both the kernel FIFO and the pulseaudio-pipe translation that the previous path relied on, getting WSJT-X DT from a ~400 ms baseline down to a steady ~200 ms. See issue aethersdr#1008 comment thread for measurement traces. Architecture ------------ - PipeWireNativeContext (singleton) owns one pw_thread_loop + pw_context + pw_core, refcounted across the four RX channels. Locked across pw_stream creation/destruction; the loop runs on its own dedicated thread. - PipeWireNativeRxSource is one pw_stream per channel, registered as Audio/Source with PW_DIRECTION_OUTPUT. Producer (Qt audio thread) and consumer (PipeWire RT thread) communicate via a fixed-size SPSC float ring with std::atomic head/tail indices — no locks, no allocations on the RT path. Drops oldest samples on overflow so backlog cannot grow past ring size (~42 ms). Stream is created with: PW_KEY_NODE_LATENCY = 256/48000 (5.3 ms quantum request) PW_KEY_NODE_RATE = 1/48000 PW_KEY_NODE_FORCE_QUANTUM = 256 PW_KEY_NODE_FORCE_RATE = 48000 PW_KEY_NODE_ALWAYS_PROCESS = true Verified live via pw-top: DAX nodes lock at QUANT=256, B/Q ~ 0, no xruns once the ring is primed. pw-cat (native PipeWire client) captures cleanly from the source with ~5.3 ms latency end-to-end. Bridge changes -------------- - PipeWireAudioBridge::feedDaxAudio is now lock-free / atomic-only and runs on PanadapterStream's network thread via Qt::DirectConnection (wired in MainWindow::startDax). Removes a 10–50 ms variable hop through the GUI thread that showed up under heavy waterfall paint load. - m_open / m_channelGain / m_transmitting / m_lastAudioMs converted to std::atomic so the audio fast path never touches Qt-thread-affine state. - Silence timer self-stops by observing m_lastAudioMs from the main thread rather than calling QTimer::stop() from the audio thread. - Per-channel native source falls back to legacy module-pipe-source if the native open() fails, so non-PipeWire systems still work. - Audio is float32 mono 48 kHz end-to-end. Linear-interp upsample of the radio's 24 kHz stereo to 48 kHz mono runs in feedDaxAudio so the PipeWire graph does not have to insert a resampler. Build ----- - pkg_check_modules(PIPEWIRE_NATIVE libpipewire-0.3) gates the new path behind a HAVE_PIPEWIRE_NATIVE define. When the dev headers are missing, the build silently falls back to the legacy module-pipe-source path (issue aethersdr#1008's PR aethersdr#1901 work, still present and used). - libpipewire-0.3-dev added to .github/docker/Dockerfile for CI. Remaining gap (~200 ms, consumer-side) -------------------------------------- The remaining 200 ms is the libpulse fragment-pool buffer that pulse-compat holds for clients using the PulseAudio API (WSJT-X via Qt PulseAudio backend connects with pulse.attr.fragsize=4800 = 50 ms × ~4 fragments queued). Confirmed via pw-cat bypass test — native PipeWire clients get sub-10 ms latency from the same source. PipeWire 1.0.5 does not expose a documented server-side mechanism to clamp pulse.attr.fragsize from the source side; pulse.min.* keys are minimums (xrun protection), not maximums. See issue aethersdr#1008 for full measurement trace and the failed pulse.rules attempts. Real fix paths are consumer-side (PULSE_LATENCY_MSEC env, Qt native PipeWire backend) and tracked separately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
RiskAndReward1337
added a commit
to RiskAndReward1337/AetherSDR
that referenced
this pull request
May 3, 2026
) Replaces module-pipe-source with a native libpipewire-0.3 pw_stream per DAX channel. This eliminates both the kernel FIFO and the pulseaudio-pipe translation that the previous path relied on, getting WSJT-X DT from a ~400 ms baseline down to a steady ~200 ms. See issue aethersdr#1008 comment thread for measurement traces. Architecture ------------ - PipeWireNativeContext (singleton) owns one pw_thread_loop + pw_context + pw_core, refcounted across the four RX channels. Locked across pw_stream creation/destruction; the loop runs on its own dedicated thread. - PipeWireNativeRxSource is one pw_stream per channel, registered as Audio/Source with PW_DIRECTION_OUTPUT. Producer (Qt audio thread) and consumer (PipeWire RT thread) communicate via a fixed-size SPSC float ring with std::atomic head/tail indices — no locks, no allocations on the RT path. Drops oldest samples on overflow so backlog cannot grow past ring size (~42 ms). Stream is created with: PW_KEY_NODE_LATENCY = 256/48000 (5.3 ms quantum request) PW_KEY_NODE_RATE = 1/48000 PW_KEY_NODE_FORCE_QUANTUM = 256 PW_KEY_NODE_FORCE_RATE = 48000 PW_KEY_NODE_ALWAYS_PROCESS = true Verified live via pw-top: DAX nodes lock at QUANT=256, B/Q ~ 0, no xruns once the ring is primed. pw-cat (native PipeWire client) captures cleanly from the source with ~5.3 ms latency end-to-end. Bridge changes -------------- - PipeWireAudioBridge::feedDaxAudio is now lock-free / atomic-only. The audio fast path no longer touches Qt-thread-affine state, which is a prerequisite for an upcoming MainWindow connect-type change (split into a separate PR per maintainer request, so the connection-type change can be tested in isolation against non-Linux DAX/TCI use cases). - m_open / m_channelGain / m_transmitting / m_lastAudioMs converted to std::atomic. - Silence timer self-stops by observing m_lastAudioMs from the main thread rather than calling QTimer::stop() from the audio thread. - Per-channel native source falls back to legacy module-pipe-source if the native open() fails, so non-PipeWire systems still work. - Audio is float32 mono 48 kHz end-to-end. Linear-interp upsample of the radio's 24 kHz stereo to 48 kHz mono runs in feedDaxAudio so the PipeWire graph does not have to insert a resampler. Build ----- - pkg_check_modules(PIPEWIRE_NATIVE libpipewire-0.3) gates the new path behind a HAVE_PIPEWIRE_NATIVE define. When the dev headers are missing, the build silently falls back to the legacy module-pipe-source path (issue aethersdr#1008's PR aethersdr#1901 work, still present and used). - libpipewire-0.3-dev added to .github/docker/Dockerfile for CI. Remaining gap (~200 ms, consumer-side) -------------------------------------- The remaining 200 ms is the libpulse fragment-pool buffer that pulse-compat holds for clients using the PulseAudio API (WSJT-X via Qt PulseAudio backend connects with pulse.attr.fragsize=4800 = 50 ms × ~4 fragments queued). Confirmed via pw-cat bypass test — native PipeWire clients get sub-10 ms latency from the same source. PipeWire 1.0.5 does not expose a documented server-side mechanism to clamp pulse.attr.fragsize from the source side; pulse.min.* keys are minimums (xrun protection), not maximums. See issue aethersdr#1008 for full measurement trace and the failed pulse.rules attempts. Real fix paths are consumer-side (PULSE_LATENCY_MSEC env, Qt native PipeWire backend) and tracked separately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ten9876
pushed a commit
that referenced
this pull request
May 4, 2026
* DAX RX: native pw_stream source on Linux — 400ms → 200ms (#1008) Replaces module-pipe-source with a native libpipewire-0.3 pw_stream per DAX channel. This eliminates both the kernel FIFO and the pulseaudio-pipe translation that the previous path relied on, getting WSJT-X DT from a ~400 ms baseline down to a steady ~200 ms. See issue #1008 comment thread for measurement traces. Architecture ------------ - PipeWireNativeContext (singleton) owns one pw_thread_loop + pw_context + pw_core, refcounted across the four RX channels. Locked across pw_stream creation/destruction; the loop runs on its own dedicated thread. - PipeWireNativeRxSource is one pw_stream per channel, registered as Audio/Source with PW_DIRECTION_OUTPUT. Producer (Qt audio thread) and consumer (PipeWire RT thread) communicate via a fixed-size SPSC float ring with std::atomic head/tail indices — no locks, no allocations on the RT path. Drops oldest samples on overflow so backlog cannot grow past ring size (~42 ms). Stream is created with: PW_KEY_NODE_LATENCY = 256/48000 (5.3 ms quantum request) PW_KEY_NODE_RATE = 1/48000 PW_KEY_NODE_FORCE_QUANTUM = 256 PW_KEY_NODE_FORCE_RATE = 48000 PW_KEY_NODE_ALWAYS_PROCESS = true Verified live via pw-top: DAX nodes lock at QUANT=256, B/Q ~ 0, no xruns once the ring is primed. pw-cat (native PipeWire client) captures cleanly from the source with ~5.3 ms latency end-to-end. Bridge changes -------------- - PipeWireAudioBridge::feedDaxAudio is now lock-free / atomic-only. The audio fast path no longer touches Qt-thread-affine state, which is a prerequisite for an upcoming MainWindow connect-type change (split into a separate PR per maintainer request, so the connection-type change can be tested in isolation against non-Linux DAX/TCI use cases). - m_open / m_channelGain / m_transmitting / m_lastAudioMs converted to std::atomic. - Silence timer self-stops by observing m_lastAudioMs from the main thread rather than calling QTimer::stop() from the audio thread. - Per-channel native source falls back to legacy module-pipe-source if the native open() fails, so non-PipeWire systems still work. - Audio is float32 mono 48 kHz end-to-end. Linear-interp upsample of the radio's 24 kHz stereo to 48 kHz mono runs in feedDaxAudio so the PipeWire graph does not have to insert a resampler. Build ----- - pkg_check_modules(PIPEWIRE_NATIVE libpipewire-0.3) gates the new path behind a HAVE_PIPEWIRE_NATIVE define. When the dev headers are missing, the build silently falls back to the legacy module-pipe-source path (issue #1008's PR #1901 work, still present and used). - libpipewire-0.3-dev added to .github/docker/Dockerfile for CI. Remaining gap (~200 ms, consumer-side) -------------------------------------- The remaining 200 ms is the libpulse fragment-pool buffer that pulse-compat holds for clients using the PulseAudio API (WSJT-X via Qt PulseAudio backend connects with pulse.attr.fragsize=4800 = 50 ms × ~4 fragments queued). Confirmed via pw-cat bypass test — native PipeWire clients get sub-10 ms latency from the same source. PipeWire 1.0.5 does not expose a documented server-side mechanism to clamp pulse.attr.fragsize from the source side; pulse.min.* keys are minimums (xrun protection), not maximums. See issue #1008 for full measurement trace and the failed pulse.rules attempts. Real fix paths are consumer-side (PULSE_LATENCY_MSEC env, Qt native PipeWire backend) and tracked separately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * DAX RX (#2312 review): SPSC safety + listener hook + ctx mutex (#1008) Addresses the review on #2312: 1. SPSC ring invariant. feedAudio() previously advanced m_readIdx from the producer side on overflow ("drop oldest"), which is a real (rare) race against the consumer's in-flight memcpy when a wraparound write crosses the slot the consumer has already claimed. Switch to drop-newest: clamp toCopy to the available space and bail if zero. m_readIdx is now consumer-only as documented. The 42 ms latency cap is preserved; in steady state the ring is near-empty so the choice between drop-oldest and drop-newest only matters during consumer stalls (which in practice only occur under extreme system load). 2. Raw new for the listener hook. pw_stream_add_listener was being handed a heap-allocated spa_hook that leaked on every open(). Make the hook a by-value member of PipeWireNativeRxSource so its lifetime is tied to the source itself — no raw new/delete, and zero leaks. 3. PipeWireNativeContext init/teardown race. acquire() did m_refCount.fetch_add(1) and then read m_loop without serialisation, so a second thread arriving while the first was mid-init would see m_loop == nullptr and bail incorrectly. Add a std::mutex around the init/teardown body in acquire()/release() so the documented "safe from any thread" contract is real instead of relying on incidental main-thread serialisation. m_refCount no longer needs to be atomic under the lock; it is now a plain int. 4. reinterpret_cast on the state-changed callback. Replace the cast of our int-arg static onStateChanged into pipewire's enum-arg slot with a small anonymous-namespace trampoline that has the correct C signature and forwards via static_cast<int>. Header stays free of <pipewire/stream.h>. 5. Stale doc. The feedAudio() comment still claimed "~170 ms" backlog from the earlier 8192 ring sizing. RING_SIZE has been 2048 (~42 ms) for some time — fix the comment. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1008
What was changed
Reduce DAX RX latency on Linux by shrinking pipe buffers (#1008)
Files modified
src/core/PipeWireAudioBridge.cppGenerated by AetherClaude (automated agent for AetherSDR)