Repository navigation
fix: flush denormals to zero in the DSP threads - #304
Merged
Merged
Conversation
The DSP state decays into denormal floats once the input becomes digital silence, e.g. right after the last client of jamesdsp_sink stops playing. On x86 denormal arithmetic is about an order of magnitude slower, so the filter no longer fits into the PipeWire quantum: every cycle ends in an xrun, the graph reschedules the node immediately and one CPU core stays at 100% (pw-top shows the node with BUSY +++ and a growing ERR counter), while audio is broken until the app is restarted. Enable flush-to-zero / denormals-are-zero (MXCSR bits FTZ|DAZ) once per processing thread. The same latent problem exists in the GStreamer element used by the PulseAudio flavour, so it is fixed there as well. Measured with PipeWire 1.6.11, 48 kHz / 256 frames: without the patch the data-loop thread of jamesdsp stays at 100% of a core after a stream stops and the node reports ~100 errors per second; with the patch the thread stays at 7-8% and the error counter does not move. Reproducer: start JamesDSP, play anything through the default sink (jamesdsp_sink) and stop it, then watch pw-top or the CPU usage of the data-loop thread.
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.
Symptom
After any client of the virtual sink has played and stopped, JamesDSP starts
burning exactly one CPU core and audio breaks (until the app is restarted).
It only starts once the input goes back to digital silence, which is why it
looks like a "gradual" problem: right after startup everything is fine.
pw-topin that state:The filter node permanently reports
BUSY +++and theERRcounter grows by~100 per second, i.e. every graph cycle is an xrun and the node is
immediately rescheduled.
Root cause
The DSP state of the effect chain (bass / tube / compander / limiter / ...)
decays into the denormal float range once the input is all zeros. Denormal
arithmetic is roughly an order of magnitude slower on x86, so the filter no
longer fits into the 256-frame quantum, the graph overruns and spins.
Measured with a probe on
pw_filter_get_dsp_buffer(block size is always 256):data-loopthreadThe last row is the giveaway: as long as the input never becomes pure
digital silence, the state stays out of the denormal range and the CPU stays
low. A fresh, exactly-zero state is fast too; only the decayed state is slow.
Not related to PipeWire itself -
node.passive,node.always-process,suspending the sink, and different PipeWire versions were all tested without
effect.
Fix
Enable flush-to-zero / denormals-are-zero (
MXCSRbitsFTZ|DAZ) once peraudio processing thread:
PwJamesDspPlugin::process()- the thread that runs the filter(
PW_FILTER_FLAG_RT_PROCESS), so this is the one that matters;gst_jamesdsp_transform_ip()- the GStreamer element used by thePulseAudio flavour has the same latent problem.
It is done per thread (instead of in
main()) so the UI thread keeps thedefault FP behaviour and no other code is affected. Signals quieter than
~-126 dBFS are flushed to zero; that is far below the noise floor of any
hardware.
Verification
Pipewireflavour, commit 3ea7d73, with the default preset (bass +6 dB,tube preamp 130, compander, stereo wide, limiter on).
MXCSRchange was first validated with anLD_PRELOADshim that sets
FTZ|DAZin every thread of the process: 4 play/stop cyclesin a row ->
data-loopstays at 7-8%,ERRstays at 0. Without it thefirst stop immediately goes to 100% with ~100 errors/s.
gstjamesdsp.cwas syntax-checked against GStreamer 1.x headers; thePulseAudio flavour was not run at runtime here (no GStreamer build).
Reproducer
Fixes #305.