pipewire: hold the thread-loop lock across blocking invokes; buffer size/stride as ranges#227
Closed
jcelerier wants to merge 1 commit into
Closed
pipewire: hold the thread-loop lock across blocking invokes; buffer size/stride as ranges#227jcelerier wants to merge 1 commit into
jcelerier wants to merge 1 commit into
Conversation
…aram ranges
Two fixes found by hardware/daemon round-trip testing of ossia score's
PipeWire video path (PipeWire 1.3.0):
1. context::invoke_sync issued pw_loop_invoke(block=true) from foreign
threads WITHOUT holding the thread-loop lock. The blocking-invoke path
in spa/plugins/support/loop.c unconditionally fires the loop-control
hooks around its wait (pw_thread_loop's impl_before/impl_after =
unlock/lock), which exist precisely to release the caller's lock while
it blocks — i.e. the API contract is that the caller holds the lock.
Without it, do_unlock underflows the recurse counter ("'this->recurse
> 0' failed" on stderr) and is refused, and impl_after then ACQUIRES
the mutex on the way out — the calling thread exits owning the loop
mutex forever. The next loop iteration blocks in do_lock and any
subsequent pw_thread_loop_stop deadlocks in pthread_join. Proven with
an instrumented thread-loop.c (abort on underflow): the guilty stack
was invoke_sync <- context::unsubscribe <- subscription dtor.
Fix: wrap both blocking pw_loop_invoke calls in with_lock (the hooks
then release/retake the lock during the wait, as designed).
invoke_async (block=false) is unaffected.
2. format_negotiation::build_buffers_param advertised
SPA_PARAM_BUFFERS_size/stride as fixed Ints, so any producer with
padded rows (GPU-allocated buffers routinely pad strides) failed the
buffers-param intersection and the link died before a single frame.
Advertise CHOICE_RANGE(tight, tight, INT32_MAX) instead; consumers
must read chunk->stride/size per-frame anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Member
Author
jcelerier
added a commit
to ossia/score
that referenced
this pull request
Jul 10, 2026
…sweep) Output device (never worked before this): - PipewireProducer::start() was never called: the producer stream was never created and the output published zero frames since inception. Now started at the end of createOutput, after the dmabuf-mode flags. - Vulkan sysmem published every frame upside-down: the readback used InvertYRenderer unconditionally, but only GL's Y-up framebuffer needs the flip. New wire renderer selects flip/copy via isYUpInFramebuffer. - Readback now targets the WIRE format instead of the window render format: fixes rgb10a2/bgr10a2 (was ~5 dB garbage) and rgba16f (frame size mismatch meant no frames at all). - BGRA: GL readback is always R,G,B,A component order; bytes went out labeled SPA BGRA. Render to RGBA8 + CPU swizzle before push. - QRhi RGB10A2 word order (A2B10G10R10) repacked to SPA xRGB_210LE. - EGL/GBM DRM fourcc mapping was inverted (RGBA memory is ABGR8888 per DRM spec); dmabuf mode restricted to format=rgba (no GPU swizzle), other tags fall back to readback with a warning. - format= tags with no faithful QRhi representation (nv12, yuyv, ...) are rejected with a warning + rgba fallback instead of advertising a format the pushed bytes don't match. - Rational framerate on the wire (29.97 stayed 29.97, was truncated). - Vulkan DMA-BUF chunk stride from vkGetImageSubresourceLayout rowPitch instead of width*bpp (compiled on Qt >= 6.6; unverified at runtime). Input device: - 10-bit packed copy used av_get_bits_per_pixel (30 for X2RGB10LE) so every row lost its right quarter; use padded bits per pixel. - RGBA16F wire is half-float but the AVFrame is RGBA64LE (u16): added per-sample half->u16 conversion (NaN->0, clamped). - YUYV/UYVY bytesPerPixel was 4 (is 2): the over-advertised buffer size killed negotiation entirely and the size validation dropped frames. - YV12 support: new Tag::YV12, negotiated as SPA YV12, U/V plane swap on copy; input negotiates by Tag (the Tag->AVPixelFormat mapping is lossy and collapsed YV12 into I420). - target.object set alongside deprecated node.target (WirePlumber 0.5+ only honors the former). Note: WirePlumber still does not auto-link stream->stream; a patchbay or explicit link is required. - sysmem AVFrame pts from spa_meta_header (was chunk->offset, i.e. 0). - Headless factories makePipewireOutput/makePipewireCapture exported for test harnesses (mirrors videoio's makeAJACapture). Teardown: stream destruction stays under with_lock — correct per the loop-control-hook contract; the deadlock observed there was caused by libremidi's invoke_sync corrupting the loop mutex (fixed in celtera/libremidi#227, pulled via the libossia bump). All fixes verified cell-by-cell with tests/PipewireRoundtrip: 26/26 PASS on Qt 6.4.2 dynamic and Qt 6.12 static, OpenGL and Vulkan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
added a commit
to ossia/score
that referenced
this pull request
Jul 10, 2026
tests/PipewireRoundtrip (SCORE_PIPEWIRE_TESTER=ON, needs a live daemon and DISPLAY): exhaustive round-trip validation of the PipeWire video devices, mirroring AJARoundtrip's index-band + PSNR methodology. - score -> score: TexgenNode -> PipewireOutputNode -> daemon -> InputStream, on OpenGL and Vulkan, sysmem SHM and EGL/GBM DMA-BUF. - raw libpipewire reference producer (video-src.c pattern, with SPA_PARAM_Buffers announcement) painting the test signal in 12 SPA formats: RGBA/BGRA/xRGB210/xBGR210/RGBA_F16/RGB24/I420/YV12/NV12/ P010/YUY2/UYVY, plus padded-stride robustness cells, validating every input copy path and the negotiation fallback ladder. - Verification via swscale to RGBA (DRM-PRIME frames via dmabuf mmap); per-cell PSNR/continuity/latency matrix, exit 1 on FAIL. - Explicit pw-link-style link creation (WirePlumber's policy does not link stream->stream video and target.object alone does not either). libossia bump pulls the libremidi PipeWire context fixes (celtera/libremidi#227): thread-loop lock held across blocking invokes (teardown deadlock) and buffers size/stride ranges (padded strides). Current matrix: 26/26 PASS on Qt 6.4.2 dynamic + Qt 6.12 static. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
added a commit
to ossia/score
that referenced
this pull request
Jul 12, 2026
…sweep) Output device (never worked before this): - PipewireProducer::start() was never called: the producer stream was never created and the output published zero frames since inception. Now started at the end of createOutput, after the dmabuf-mode flags. - Vulkan sysmem published every frame upside-down: the readback used InvertYRenderer unconditionally, but only GL's Y-up framebuffer needs the flip. New wire renderer selects flip/copy via isYUpInFramebuffer. - Readback now targets the WIRE format instead of the window render format: fixes rgb10a2/bgr10a2 (was ~5 dB garbage) and rgba16f (frame size mismatch meant no frames at all). - BGRA: GL readback is always R,G,B,A component order; bytes went out labeled SPA BGRA. Render to RGBA8 + CPU swizzle before push. - QRhi RGB10A2 word order (A2B10G10R10) repacked to SPA xRGB_210LE. - EGL/GBM DRM fourcc mapping was inverted (RGBA memory is ABGR8888 per DRM spec); dmabuf mode restricted to format=rgba (no GPU swizzle), other tags fall back to readback with a warning. - format= tags with no faithful QRhi representation (nv12, yuyv, ...) are rejected with a warning + rgba fallback instead of advertising a format the pushed bytes don't match. - Rational framerate on the wire (29.97 stayed 29.97, was truncated). - Vulkan DMA-BUF chunk stride from vkGetImageSubresourceLayout rowPitch instead of width*bpp (compiled on Qt >= 6.6; unverified at runtime). Input device: - 10-bit packed copy used av_get_bits_per_pixel (30 for X2RGB10LE) so every row lost its right quarter; use padded bits per pixel. - RGBA16F wire is half-float but the AVFrame is RGBA64LE (u16): added per-sample half->u16 conversion (NaN->0, clamped). - YUYV/UYVY bytesPerPixel was 4 (is 2): the over-advertised buffer size killed negotiation entirely and the size validation dropped frames. - YV12 support: new Tag::YV12, negotiated as SPA YV12, U/V plane swap on copy; input negotiates by Tag (the Tag->AVPixelFormat mapping is lossy and collapsed YV12 into I420). - target.object set alongside deprecated node.target (WirePlumber 0.5+ only honors the former). Note: WirePlumber still does not auto-link stream->stream; a patchbay or explicit link is required. - sysmem AVFrame pts from spa_meta_header (was chunk->offset, i.e. 0). - Headless factories makePipewireOutput/makePipewireCapture exported for test harnesses (mirrors videoio's makeAJACapture). Teardown: stream destruction stays under with_lock — correct per the loop-control-hook contract; the deadlock observed there was caused by libremidi's invoke_sync corrupting the loop mutex (fixed in celtera/libremidi#227, pulled via the libossia bump). All fixes verified cell-by-cell with tests/PipewireRoundtrip: 26/26 PASS on Qt 6.4.2 dynamic and Qt 6.12 static, OpenGL and Vulkan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
added a commit
to ossia/score
that referenced
this pull request
Jul 12, 2026
tests/PipewireRoundtrip (SCORE_PIPEWIRE_TESTER=ON, needs a live daemon and DISPLAY): exhaustive round-trip validation of the PipeWire video devices, mirroring AJARoundtrip's index-band + PSNR methodology. - score -> score: TexgenNode -> PipewireOutputNode -> daemon -> InputStream, on OpenGL and Vulkan, sysmem SHM and EGL/GBM DMA-BUF. - raw libpipewire reference producer (video-src.c pattern, with SPA_PARAM_Buffers announcement) painting the test signal in 12 SPA formats: RGBA/BGRA/xRGB210/xBGR210/RGBA_F16/RGB24/I420/YV12/NV12/ P010/YUY2/UYVY, plus padded-stride robustness cells, validating every input copy path and the negotiation fallback ladder. - Verification via swscale to RGBA (DRM-PRIME frames via dmabuf mmap); per-cell PSNR/continuity/latency matrix, exit 1 on FAIL. - Explicit pw-link-style link creation (WirePlumber's policy does not link stream->stream video and target.object alone does not either). libossia bump pulls the libremidi PipeWire context fixes (celtera/libremidi#227): thread-loop lock held across blocking invokes (teardown deadlock) and buffers size/stride ranges (padded strides). Current matrix: 26/26 PASS on Qt 6.4.2 dynamic + Qt 6.12 static. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
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.
Two fixes for the shared PipeWire context, found while round-trip-testing ossia score's PipeWire video path against a live 1.3.0 daemon.
1.
context::invoke_synccorrupts the thread-loop mutexinvoke_syncissuedpw_loop_invoke(..., block=true, ...)from foreign threads without holding the thread-loop lock. PipeWire's blocking-invoke path unconditionally fires the loop-control hooks around its wait (pw_thread_loop'simpl_before/impl_after— i.e. unlock/lock), which exist to release the caller's lock while it blocks; the API contract is that the caller holds it.Without the lock:
do_unlockunderflows the recurse counter ('this->recurse > 0' failed at thread-loop.c:62on stderr) and the unlock is refused;impl_afterthen acquires the mutex on the way out, so the calling thread exits owning the loop mutex forever;do_lockon its next iteration, and any laterpw_thread_loop_stopdeadlocks inpthread_join.Proven by rebuilding libpipewire with an instrumented
thread-loop.c(abort on recurse underflow): the guilty stack wasinvoke_sync←context::unsubscribe←subscriptiondtor. Every consumer that destroys a subscription or usesinvoke_syncoff-loop hits this eventually.Fix: wrap the two blocking
pw_loop_invokecalls inwith_lock— the hooks then release/retake the lock during the wait exactly as designed.invoke_async(block=false) is unaffected.2.
format_negotiation::build_buffers_paramrejects padded stridesSPA_PARAM_BUFFERS_size/stridewere advertised as fixed Ints, so any producer with padded rows (GPU-allocated buffers routinely pad strides) failed the buffers-param intersection and the link died before a single frame. Now advertised asCHOICE_RANGE(tight, tight, INT32_MAX); consumers must readchunk->stride/sizeper frame anyway.Both verified with score's new
PipewireRoundtripharness (26/26 format×transport cells pass, including padded-stride producers; the deadlock repro'd 100% before the fix and is gone after).🤖 Generated with Claude Code
https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE