Skip to content

fix(linux): negotiate DMA-BUF so capture works on niri and other wlroots compositors - #299

Open
EtienneLescot wants to merge 1 commit into
mainfrom
fix/pipewire-dmabuf-modifier
Open

fix(linux): negotiate DMA-BUF so capture works on niri and other wlroots compositors#299
EtienneLescot wants to merge 1 commit into
mainfrom
fix/pipewire-dmabuf-modifier

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

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/1000 against a fps: 60 request, which reads like a mismatch. It is not:

  • pw_shim.c already declares SPA_POD_CHOICE_RANGE_Fraction(30/1, 0/1, 240/1) — a range that contains 59.978.
  • The requested fps: 60 never enters the pod. It drives frame pacing (main.rs) and the encoder time_base (encoder.rs), nothing else.

Widening the range would have changed nothing.

The cause is a missing SPA_FORMAT_VIDEO_modifier

A DMA-BUF-only compositor publishes its formats with VideoModifier flagged MANDATORY. spa_pod_filter turns a mandatory producer property that the consumer never mentions into -EINVAL for the entire object:

else if ((p1->flags & SPA_POD_PROP_FLAG_MANDATORY) != 0)
        res = -EINVAL;          /* spa/pod/filter.h:352 */

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|MemFd in ParamBuffers, on this reasoning from the code:

not offering it is what makes the compositor fall back to memfd instead

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 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 — GNOME and KDE are unchanged. Only a producer with nothing but DMA-BUF reaches the fallback. No DONT_FIXATE: we advertise two modifiers that need no GPU query, so letting the producer fixate removes a round trip that can fail.

DmaBuf in ParamBuffers when the negotiated format carries SPA_VIDEO_FLAG_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 (not per frame — that would be an mmap/munmap of a full framebuffer 60×/s), and CPU access is bracketed by DMA_BUF_IOCTL_SYNC around the on_frame callback, which is where the pixels are actually read.

Only DRM_FORMAT_MOD_LINEAR and DRM_FORMAT_MOD_INVALID are 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

  • Bug fix

Release impact

  • Patch

Desktop impact

  • Linux

Screenshots / video

n/a

Testing

What is proven

New unit test enum_format_survives_a_dmabuf_only_producer, built on the same osc_pw_enum_format_accepts_dmabuf_producer hook pattern as the existing cursor-meta bound — it runs the real spa_pod_filter from the vendored headers against a synthetic niri-shaped producer, so no niri, portal or screen is needed:

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:linux produces a helper that starts and probes clean ({"event":"ready","pipewireVersion":"1.0.5","cursorMetadataSupported":true}), ldd fully 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:

Result
main [121ms] state error error=no more input formats
this branch [134ms] state error error=no more input formats

Identical. The webcam fails for an unrelated, pre-existing reason — it offers YUYV/MJPG and our EnumFormat only 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), so osc_map_dmabuf, the sync bracket and the dmabuf branch of osc_read_frame are compiled and reviewed but not run. There is no niri, no wlroots compositor and no second GPU here to force that path.

Specifically unverified:

  1. That niri's advertised modifier set actually includes LINEAR or INVALID. If it only offers vendor modifiers, negotiation still fails — with a different message, and the fix would need the EGL/gbm import instead.
  2. That mmap on 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

…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.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@EtienneLescot, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e1300fd9-c592-4e8c-adb7-78a9474f4b05

📥 Commits

Reviewing files that changed from the base of the PR and between 5e9a5ad and 48401f6.

📒 Files selected for processing (3)
  • electron/native/pipewire-capture/csrc/pw_shim.c
  • electron/native/pipewire-capture/csrc/pw_shim.h
  • electron/native/pipewire-capture/src/shim.rs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Screen recording fails on Niri/Wayland with PipeWire (no more input formats)

1 participant