#5971

WLR capture: black frames on NVIDIA with wlroots Vulkan renderer

Openewancoderopened 9d ago

Is there an existing issue for this?

  • I have searched the existing issues

Is your issue described in the documentation?

  • I have read the documentation

Is your issue present in the latest beta/pre-release?

I didn't read the issue template

Describe the Bug

Disclaimer: I have fixed this issue for myself using LLM that wrote a patch. The issue is gone, and it completely satisfies me, I'm going to apply this patch going forward after every Sunshine update. However, sharing the issue here in case anyone had the same issue or someone might want to take a look at it. This patch is probably unoptimal, it makes CPU thread wait, introduces latency/delays, untested on other hardware or other software configuration, and might not even be applicable to future versions of wlr / renderers. Or maybe even unrelated to Sunshine and should be fixed on wlr/vulkan/nvidia side.

The problem is: I'm having severe flickering when streaming something with Vulkan renderer from Sway.

Environment

  • Arch Linux, kernel 7.2.11
  • RTX 5070 Ti, nvidia-open 615.78.08
  • sway 1.12 / wlroots 0.20.2, WLR_RENDERER=vulkan
  • Sunshine v2026.914.233613 (AUR, built with CUDA 13.4.3, but without CUDA same issue), capture = wlr, hevc_nvenc
  • Capturing headless outputs (1340x800@60), Artemis on Android

Problem

Streams flash black frames. It looks like severe flickering, especially during heavy gameplay: XPlane12, Minecraft. I'm trying to stream instrument panels of XPlane from HEADLESS outputs but I think it's irrelevant which ones to stream. Rate scales with GPU load: occasional at idle, tens per second while a game is running, especially with an XWayland game focused and the cursor over it (though might be unrelated to cursor).

  • WLR_RENDERER=gles2: no flicker (the gles2 renderer stalls on the GPU instead, which is unusable under load). This only happens with WLR_RENDERER=vulkan.
  • Non-CUDA build (GPU -> RAM -> GPU) flickers too.

Cause (likely) (written by LLM)

dmabuf_t::ready() uses the buffer as soon as ready arrives. The wlroots Vulkan renderer sends ready after submitting the copy and relies on the implicit fence on the dma-buf. NVIDIA's import doesn't wait on implicit fences, so Sunshine reads a freshly allocated, not-yet-written GBM buffer, which shows up as black.

Proof of concept (written by LLM)

Wait for the dma-buf write fence (poll(POLLIN)) in ready(). On timeout, repeat the previous frame. This fixes it for me. It's a CPU-side wait; a proper fix would probably export the fence (DMA_BUF_IOCTL_EXPORT_SYNC_FILE) and have the GPU wait on it via EGL. The patch is against v2026.914.233613; wayland.cpp has been refactored on master since.

Patch
--- a/src/platform/linux/wayland.cpp
+++ b/src/platform/linux/wayland.cpp
@@ -536,15 +536,35 @@
     std::uint32_t tv_sec_lo,
     std::uint32_t tv_nsec
   ) {
-    // Frame is ready for use, GBM buffer now contains screen content
-    current_frame->destroy();
-    current_frame = get_next_frame();
+    // The compositor may send `ready` before its GPU copy has finished, relying
+    // on the implicit fence attached to the dma-buf. NVIDIA's EGL/CUDA import
+    // ignores implicit fences, so wait for the write fence (POLLIN on a dma-buf
+    // fd blocks until pending writes are done). If the copy still isn't done,
+    // drop the new frame and keep the previous one instead of sending a
+    // half-copied (usually black) frame.
+    auto next_frame = get_next_frame();
+    bool copy_done = true;
+    if (next_frame->sd.fds[0] >= 0) {
+      struct pollfd pfd = {};
+      pfd.fd = next_frame->sd.fds[0];
+      pfd.events = POLLIN;
+      copy_done = poll(&pfd, 1, 100) == 1;
+    }
 
-    std::uint64_t sec = (std::uint64_t(tv_sec_hi) << 32) | tv_sec_lo;
-    auto ready_ts = std::chrono::seconds(sec) + std::chrono::nanoseconds(tv_nsec);
-    current_frame->frame_timestamp = std::chrono::steady_clock::time_point {
-      std::chrono::duration_cast<std::chrono::steady_clock::duration>(ready_ts)
-    };
+    if (!copy_done && current_frame->sd.fds[0] >= 0) {
+      BOOST_LOG(debug) << "[wayland] Screencopy not finished in time, repeating previous frame"sv;
+      next_frame->destroy();
+    } else {
+      // Frame is ready for use, GBM buffer now contains screen content
+      current_frame->destroy();
+      current_frame = next_frame;
+
+      std::uint64_t sec = (std::uint64_t(tv_sec_hi) << 32) | tv_sec_lo;
+      auto ready_ts = std::chrono::seconds(sec) + std::chrono::nanoseconds(tv_nsec);
+      current_frame->frame_timestamp = std::chrono::steady_clock::time_point {
+        std::chrono::duration_cast<std::chrono::steady_clock::duration>(ready_ts)
+      };
+    }
 
     // Keep the GBM buffer alive but destroy the Wayland objects
     if (current_wl_buffer) {

Expected Behavior

No response

Additional Context

No response

Host Operating System

Linux

Operating System Version

Arch Linux

Architecture

amd64/x86_64

Package

Linux - AUR (Third Party)

GPU Type

NVIDIA

GPU Model

5070Ti

GPU Driver/Mesa Version

615.78.08

Capture Method

No response

Apps

undefined

Log output

undefined

Online logs

No response

Activity

0

No activity yet.