Skip to content

Wayland capture is black: pipewiresrc always-copy=true yields NULL-memory buffers on DMA-BUF portals #40

Description

@ocewers

Summary

On a DMA-BUF screencast portal (xdg-desktop-portal-hyprland), pipewiresrc always-copy=true produces buffers with no memory attached. Every captured frame is empty, so the receiver shows a black screen while pairing, RTSP, NTP timing and the feedback loop all report success. Nothing in the normal (non--debug) output indicates a problem.

This is a regression from 07ff8d2 ("Fix video hangs on Wayland"), which introduced always-copy=true.

Environment

  • Arch Linux, kernel 7.1.9, Hyprland v0.56.2, xdg-desktop-portal-hyprland 1.4.1
  • Intel Panther Lake (Arc B390), iHD 26.2.4, vah264enc via gst-plugin-va 1.28.6
  • GStreamer 1.28.6, PipeWire 1.6.8, gst-plugin-pipewire 1.6.8
  • doubletake at ae06722
  • Receiver: Samsung LS03B (The Frame), laptop panel 2880x1800

Symptom

2026/09/03 12:07:48 [CAPTURE] gst-launch-1.0 (wayland) --quiet pipewiresrc fd=3 path=160
  do-timestamp=true keepalive-time=33 always-copy=true ! vapostproc ! compositor force-live=true ...
2026/09/03 12:07:48 screen capture started at 1920x1080 using h264
2026/09/03 12:07:49 [GST] (gst-launch-1.0:2109652): GStreamer-CRITICAL **:
  gst_buffer_insert_memory: assertion 'mem != NULL' failed

6291 of those assertions in roughly 100 seconds — one per frame at 30 fps, and no other [GST] output at all. pipewiresrc cannot map the portal's DMA-BUF in order to copy it, so it emits buffers with NULL memory.

Root cause and why the copy exists

I traced the history before proposing anything:

  • 79fafe6 (2026-08-08, "Fix streaming on Wayland") added keepalive-time, which closed Mirroring only sends one frame #21 ("Mirroring only sends one frame").

  • 07ff8d2 (2026-08-20, "Fix video hangs on Wayland") added always-copy=true, with this comment:

    The compositor and pipewiresrc's keepalive path both retain the latest GstBuffer. With a small portal pool that can keep every PipeWire buffer checked out and freeze screencopy. Copying here returns the portal buffer as soon as pipewiresrc pulls it while downstream retains only the copy.

That reasoning is correct, and simply setting always-copy=false reproduces exactly the starvation it describes — I measured 0.97 frames/s, media time advancing at 0.032x real time, and lag growing without bound to 37.8 s. So the copy is load-bearing and should not just be removed.

The problem is only where the release happens. pipewiresrc's copy path cannot handle DMA-BUF, but vapostproc — which the pipeline already places immediately downstream, and whose own comment says it "imports the portal's DMA-BUF via VA-API" — can.

Proposed fix

Drop the copy and force vapostproc out of passthrough by requiring plain system-memory output. It then downloads the DMA-BUF into its own pool and the portal buffer is released immediately, which preserves the intent of 07ff8d2 without relying on pipewiresrc's copy:

@@ func pipeWireVideoSourceStage
-		"always-copy=true",
+		"always-copy=false",

@@ func startPreparedWaylandCapture
 	var beforeConvert []gstStage
 	if hasGstElement("vapostproc") {
-		beforeConvert = append(beforeConvert, gstStage{"vapostproc"})
+		beforeConvert = append(beforeConvert, gstStage{"vapostproc"}, gstStage{"video/x-raw,format=NV12"})
 	} else {

Result on this machine: 0 assertions, mirroring works, media time 0.767x with 0.5 s lag at 30 fps.

What I could not explain

I want to be straight about the limits of this, because the fix is not a complete answer:

  • Throughput is still marginal. 0.767x real time at 30 fps is better than 0.032x but not 1.0x, and I could not determine why.
  • At -fps 60 it collapses to ~1 frame/s with lag growing to 27 s. I changed -fps and -target-latency-ms in the same test run and never isolated them, so I cannot say which triggers it. Worth treating as a separate unknown rather than part of this report.
  • The pipeline itself is not the bottleneck: benchmarked standalone with videotestsrc it sustains 89 frames/s at 30 fps caps and 120 frames/s at 60 fps caps. Network is not either (5 GHz, -50 dBm, 4.9 ms RTT to the receiver).

So the remaining deficit is somewhere in portal frame delivery, and this patch addresses the black screen rather than that.

Possibly related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions