fix(linux): negotiate DMA-BUF so capture works on niri and other wlroots compositors - #299
fix(linux): negotiate DMA-BUF so capture works on niri and other wlroots compositors#299EtienneLescot wants to merge 1 commit into
Conversation
…ots compositors Capture never starts on niri: the picker appears, then the stream dies with "PipeWire stream reported an error in state error: no more input formats". Reported on Arch/CachyOS against 1.8.0 and 1.9.0-rc.3. The framerate is not the cause, despite the report showing a 59978/1000 producer against a 60fps request. The EnumFormat pod already declares CHOICE_RANGE_Fraction(30/1, 0/1, 240/1), and the requested fps never enters the pod at all — it only drives frame pacing and the encoder time_base. The cause is a missing SPA_FORMAT_VIDEO_modifier. A DMA-BUF-only compositor publishes its formats with that property flagged MANDATORY, and spa_pod_filter turns a mandatory producer property the consumer never mentions into -EINVAL for the whole object (spa/pod/filter.h:352). Every format is filtered out, and PipeWire reports that as "no more input formats". mutter is unaffected because it also publishes shm pods, several with no modifier at all — which is why GNOME has always worked. Fixing only the format would move the failure rather than remove it: video mode advertised MemPtr|MemFd in ParamBuffers, on the reasoning that not offering DmaBuf makes the compositor fall back to memfd. True of mutter and KWin, false of compositors that have no memfd path — the buffer intersection would then come back empty instead. So both halves: - A second EnumFormat object carrying the modifier, sent AFTER the existing shared-memory one. pw_stream keeps that as a preference order, so any compositor able to produce shm still negotiates shm and GNOME/KDE are unchanged. Only a producer with nothing but DMA-BUF reaches the fallback. - DmaBuf in ParamBuffers when the negotiated format carries a modifier, plus the import: pw_stream does not map dmabuf even with MAP_BUFFERS, so the fd is mmap'd once per buffer in add_buffer and CPU access is bracketed by DMA_BUF_IOCTL_SYNC around the on_frame callback. Only DRM_FORMAT_MOD_LINEAR and DRM_FORMAT_MOD_INVALID are advertised, and that bound is load-bearing: a tiled or compressed buffer read through a plain mmap is not in raster order, so accepting one would ship a scrambled recording instead of an error. Anything else needs an EGL/gbm import and two more link-time dependencies. osc_pw_enum_format_accepts_dmabuf_producer() exposes the filter to a unit test the way the cursor-meta bound already is, so the regression is asserted against the vendored headers rather than rediscovered on someone's desktop.
|
Warning Review limit reached
Next review available in: 39 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
Capture never starts on niri: the picker appears, then the stream dies with
PipeWire stream reported an error in state error: no more input formats.The framerate is not the cause
The report shows a producer at
Fraction 59978/1000against afps: 60request, which reads like a mismatch. It is not:pw_shim.calready declaresSPA_POD_CHOICE_RANGE_Fraction(30/1, 0/1, 240/1)— a range that contains 59.978.fps: 60never enters the pod. It drives frame pacing (main.rs) and the encodertime_base(encoder.rs), nothing else.Widening the range would have changed nothing.
The cause is a missing
SPA_FORMAT_VIDEO_modifierA DMA-BUF-only compositor publishes its formats with
VideoModifierflaggedMANDATORY.spa_pod_filterturns a mandatory producer property that the consumer never mentions into-EINVALfor the entire object:So every format is filtered out, and PipeWire reports that as "no more input formats". mutter is unaffected because it also publishes shm pods, several carrying no modifier at all — which is why GNOME has always worked.
Fixing only the format would have moved the failure, not removed it
Video mode advertised
MemPtr|MemFdinParamBuffers, on this reasoning from the code:True of mutter and KWin. False of niri and the other Smithay/wlroots compositors, which have no memfd path — the buffer intersection would have come back empty instead. Hence both halves in one PR.
What changed
A second
EnumFormatobject carrying the modifier, sent after the existing shared-memory one.pw_streamkeeps that as a preference order, so any compositor able to produce shm still negotiates shm — GNOME and KDE are unchanged. Only a producer with nothing but DMA-BUF reaches the fallback. NoDONT_FIXATE: we advertise two modifiers that need no GPU query, so letting the producer fixate removes a round trip that can fail.DmaBufinParamBufferswhen the negotiated format carriesSPA_VIDEO_FLAG_MODIFIER, plus the import.pw_streamdoes not map dmabuf even withMAP_BUFFERS, so the fd ismmap'd once per buffer inadd_buffer(not per frame — that would be an mmap/munmap of a full framebuffer 60×/s), and CPU access is bracketed byDMA_BUF_IOCTL_SYNCaround theon_framecallback, which is where the pixels are actually read.Only
DRM_FORMAT_MOD_LINEARandDRM_FORMAT_MOD_INVALIDare advertised, and that bound is load-bearing: a tiled or DCC-compressed buffer read through a plain mmap is not in raster order, so accepting one would ship a scrambled recording instead of an error. Anything else needs an EGL/gbm import and two more link-time dependencies in a helper that dlopens everything on purpose.Related issue
Fixes #287
Type of change
Release impact
Desktop impact
Screenshots / video
n/a
Testing
What is proven
New unit test
enum_format_survives_a_dmabuf_only_producer, built on the sameosc_pw_enum_format_accepts_dmabuf_producerhook pattern as the existing cursor-meta bound — it runs the realspa_pod_filterfrom the vendored headers against a synthetic niri-shaped producer, so no niri, portal or screen is needed:0x0300000000000001) is still rejected — the advertised set is a real set, not a wildcard.Asserting both directions matters: "the new object is accepted" alone would still pass if the old one had been made to match too, and that would mean GNOME had quietly moved onto the DMA-BUF path.
Full crate suite: 56 passed, 0 failed, 1 ignored.
npm run build:native:linuxproduces a helper that starts and probes clean ({"event":"ready","pipewireVersion":"1.0.5","cursorMetadataSupported":true}),lddfully resolved.Live-producer regression control
Ran the ignored integration test against the one live video node on this Ubuntu 24.04 / GNOME / PipeWire 1.0.5 machine (a UVC webcam,
OPENSCREEN_PIPEWIRE_TEST_NODE=54), before and after the patch:main[121ms] state error error=no more input formats[134ms] state error error=no more input formatsIdentical. The webcam fails for an unrelated, pre-existing reason — it offers YUYV/MJPG and our
EnumFormatonly accepts BGRx/RGBx/BGRA/RGBA — but it does establish that adding the second object changes nothing for a producer that was already negotiating (or failing) on its own terms. It also shows that "no more input formats" is PipeWire's generic message for an empty intersection, so the string alone never pointed at the modifier.What is NOT proven — please read before merging
The DMA-BUF import path has never executed. mutter hands this machine memfd buffers (
dataType=2), soosc_map_dmabuf, the sync bracket and the dmabuf branch ofosc_read_frameare compiled and reviewed but not run. There is no niri, no wlroots compositor and no second GPU here to force that path.Specifically unverified:
mmapon the dmabuf fd succeeds on the reporter's driver. It is optional for the exporter; failure is handled and reported through the buffer-info channel rather than producing a black recording, but it is handled blind.This wants a smoke test from @talison-cardoso or another niri user before it ships. I would rather it be tested than merged on the strength of a filter test that only proves the negotiation half.
🤖 Generated with Claude Code