From 0f0147430f4a76c0c2a4d3b4de00791ebc472ca2 Mon Sep 17 00:00:00 2001 From: 4st4rot Date: Tue, 10 Nov 2026 23:13:05 +0300 Subject: [PATCH] fix: flush denormals to zero in the DSP threads 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. --- src/audio/pipewire/PwJamesDspPlugin.cpp | 18 ++++++++++++++++++ src/audio/pulseaudio/wrapper/gstjamesdsp.c | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/audio/pipewire/PwJamesDspPlugin.cpp b/src/audio/pipewire/PwJamesDspPlugin.cpp index 42c7d4cc..4f9737b9 100644 --- a/src/audio/pipewire/PwJamesDspPlugin.cpp +++ b/src/audio/pipewire/PwJamesDspPlugin.cpp @@ -2,6 +2,10 @@ #include #include +#if defined(__x86_64__) || defined(__i386__) +#include +#endif + extern "C" { #ifdef DEBUG_FPE #include @@ -78,6 +82,20 @@ void PwJamesDspPlugin::process(float* left_in, float* right_out, size_t length) { +#if defined(__x86_64__) || defined(__i386__) + // Denormals are computed ~10x slower on x86. The DSP state decays into the + // denormal range once the input turns into digital silence (e.g. right after + // a stream stops), the filter no longer fits into the graph quantum and + // every cycle ends in an xrun, which keeps one CPU core busy forever. + // Enable flush-to-zero / denormals-are-zero once per processing thread. + static thread_local bool denormals_flushed = false; + + if (!denormals_flushed) { + _mm_setcsr(_mm_getcsr() | 0x8040u); // FTZ | DAZ + denormals_flushed = true; + } +#endif + if (bypass) { memcpy(left_out, left_in, length * sizeof (float)); diff --git a/src/audio/pulseaudio/wrapper/gstjamesdsp.c b/src/audio/pulseaudio/wrapper/gstjamesdsp.c index e3b86a8d..7da62f5d 100644 --- a/src/audio/pulseaudio/wrapper/gstjamesdsp.c +++ b/src/audio/pulseaudio/wrapper/gstjamesdsp.c @@ -7,6 +7,10 @@ #include #include +#if defined(__x86_64__) || defined(__i386__) +#include +#endif + GST_DEBUG_CATEGORY_STATIC (gst_jamesdsp_debug); #define GST_CAT_DEFAULT gst_jamesdsp_debug /* Filter signals and args */ @@ -241,6 +245,18 @@ gst_jamesdsp_transform_ip(GstBaseTransform *base, GstBuffer *buf) { GstClockTime timestamp, stream_time; GstMapInfo map; +#if defined(__x86_64__) || defined(__i386__) + /* See PwJamesDspPlugin::process(): denormal DSP state after the input + * turns to digital silence is ~10x slower on x86 and makes the pipeline + * overrun. Enable flush-to-zero / denormals-are-zero once per thread. */ + static __thread gboolean denormals_flushed = FALSE; + + if (!denormals_flushed) { + _mm_setcsr(_mm_getcsr() | 0x8040u); /* FTZ | DAZ */ + denormals_flushed = TRUE; + } +#endif + timestamp = GST_BUFFER_TIMESTAMP (buf); stream_time = gst_segment_to_stream_time(&base->segment, GST_FORMAT_TIME, timestamp);