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 withWLR_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
undefinedLog output
undefinedOnline logs
No response
Activity
0No activity yet.