From 1021aa395528cc626ae5d023fb5f9f9fa0adcd8c Mon Sep 17 00:00:00 2001 From: Mitchell Date: Thu, 2 Jul 2026 21:26:35 -0500 Subject: [PATCH] fix: gnome window capture issues (partial fix) There are some capture issues on Gnome. The changes here fix an issue where blank frames were getting sent to the encoder. This adds more validation on the pipewire dma buffer. There is still a flickering issue in Gnome. See issue #133. The flake.nix changes fix a regression caused by the previous commit. It broke running on NixOS. --- README.md | 2 +- flake.nix | 4 + .../pipewire_frame_buffer_manager.zig | 9 +- .../video/linux/pipewire/pipewire_video.zig | 100 +++++++++++++----- 4 files changed, 85 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index bd11a6c..720d0ab 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ reboot. ```sh # Delete -rm ~/.local/share/flatpak/db/screencast ~/.local/share/flatpak/db/screencast.bak +rm ~/.local/share/flatpak/db/screencast # OR move it to a backup mv ~/.local/share/flatpak/db/screencast ~/.local/share/flatpak/db/screencast.bak diff --git a/flake.nix b/flake.nix index 412bc63..2d4c3d3 100644 --- a/flake.nix +++ b/flake.nix @@ -102,6 +102,7 @@ vulkan-validation-layers vulkan-tools wayland + libxkbcommon zlib glib @@ -133,7 +134,10 @@ # Required for linux tray icon. pkgs.gtk3 pkgs.libayatana-appindicator + + # SDL runtime libs pkgs.wayland + pkgs.libxkbcommon ]; }; } diff --git a/src/capture/video/linux/pipewire/pipewire_frame_buffer_manager.zig b/src/capture/video/linux/pipewire/pipewire_frame_buffer_manager.zig index b1d7d64..52624b6 100644 --- a/src/capture/video/linux/pipewire/pipewire_frame_buffer_manager.zig +++ b/src/capture/video/linux/pipewire/pipewire_frame_buffer_manager.zig @@ -2,10 +2,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const pw = @import("pipewire").c; -const c = @import("../../../../common/linux/pipewire_include.zig").c; const vk = @import("vulkan"); const Vulkan = @import("../../../../vulkan/vulkan.zig").Vulkan; -const BufferedChan = @import("../../../../channel.zig").BufferedChan; const pipewire_util = @import("./pipewire_util.zig"); const PipewireFrameBufferImage = struct { @@ -86,17 +84,18 @@ pub const PipewireFrameBufferManager = struct { self: *Self, pwb: *pw.struct_pw_buffer, info: pw.spa_video_info_raw, + plane_count: u32, ) !struct { frame_buffer: *PipewireFrameBuffer, wait_semaphore: vk.Semaphore } { + assert(plane_count > 0); const _frame_buffer = self.frame_buffers.getPtr(pwb); // Should never be null here. If it is, there are big problems. assert(_frame_buffer != null); const frame_buffer = _frame_buffer.?; - const n_datas = frame_buffer.pwb.buffer[0].n_datas; - var subresource_layouts = try std.ArrayList(vk.SubresourceLayout).initCapacity(self.allocator, n_datas); + var subresource_layouts = try std.ArrayList(vk.SubresourceLayout).initCapacity(self.allocator, plane_count); defer subresource_layouts.deinit(self.allocator); - for (0..n_datas) |i| { + for (0..plane_count) |i| { const buf_data = frame_buffer.pwb.buffer[0].datas[i]; const row_pitch: u64 = @intCast(buf_data.chunk[0].stride); const subresource_layout = vk.SubresourceLayout{ diff --git a/src/capture/video/linux/pipewire/pipewire_video.zig b/src/capture/video/linux/pipewire/pipewire_video.zig index e86952f..2dc3938 100644 --- a/src/capture/video/linux/pipewire/pipewire_video.zig +++ b/src/capture/video/linux/pipewire/pipewire_video.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Arc = @import("../../../../arc.zig").Arc; const pw = @import("pipewire").c; @@ -384,8 +385,10 @@ pub const PipewireVideo = struct { return; } - // Grab the newest buffer. + // Grab the most recent buffer. var pipewire_buffer: ?*pw.struct_pw_buffer = null; + var pipewire_buffer_plane_count: u32 = 0; + while (true) { const tmp: ?*pw.struct_pw_buffer = pw.pw_stream_dequeue_buffer(self.stream); @@ -393,35 +396,42 @@ pub const PipewireVideo = struct { break; } - // Only keep buffers that are dmabuf. - if (tmp.?.buffer == null or tmp.?.buffer[0].datas[0].type != pw.SPA_DATA_DmaBuf) { + const plane_count = get_dmabuf_plane_count(tmp.?.buffer); + + // Only keep dma buffers that have at least one plane. + if (plane_count == 0) { _ = pw.pw_stream_queue_buffer(self.stream.?, tmp.?); continue; } + assert(plane_count > 0); + if (pipewire_buffer) |pwb| { _ = pw.pw_stream_queue_buffer(self.stream.?, pwb); } pipewire_buffer = tmp; + pipewire_buffer_plane_count = plane_count; } - const pwb = pipewire_buffer.?; + const pwb = pipewire_buffer orelse return; + + assert(pipewire_buffer_plane_count > 0); // TODO: Should gracefully handle these errors. defer _ = pw.pw_stream_queue_buffer(self.stream.?, pwb); - const vulkan_image = self.pipewire_frame_buffer_manager.?.get_vulkan_image(pwb, self.info.?) catch |err| { - log.err("[stream_process_callback] unable to get buffer: {}", .{err}); - return; - }; - const header = pw.spa_buffer_find_meta_data(pwb.buffer, pw.SPA_META_Header, @sizeOf(pw.spa_meta_header)); if (header == null) { log.err("[stream_process_callback] unable to get metadata header. This should never happen.", .{}); return; } - const metadata = @as(*pw.spa_meta_header, @ptrCast(@alignCast(header.?))); + const metadata: *pw.spa_meta_header = @ptrCast(@alignCast(header.?)); + + if (metadata.flags & pw.SPA_META_HEADER_FLAG_CORRUPTED != 0) { + return; + } + var timestamp_ns = self.select_best_timestamp(metadata); // Pipewire can occasionally queue a buffer with the same timestamp @@ -435,6 +445,11 @@ pub const PipewireVideo = struct { self.previous_frame_timestamp_ns = timestamp_ns; + const vulkan_image = self.pipewire_frame_buffer_manager.?.get_vulkan_image(pwb, self.info.?, pipewire_buffer_plane_count) catch |err| { + log.err("[stream_process_callback] unable to get buffer: {}", .{err}); + return; + }; + const copy_data = blk: { const capture_ring_buffer = self.vulkan.capture_ring_buffer.lock(); defer capture_ring_buffer.unlock(); @@ -473,6 +488,37 @@ pub const PipewireVideo = struct { } } + /// Get the plane count from a buffer if it is a valid dmabuf, otherwise + /// return 0. + fn get_dmabuf_plane_count(buffer: ?[*c]pw.spa_buffer) u32 { + const _buffer = buffer orelse return 0; + if (_buffer[0].n_datas == 0 or + _buffer[0].datas[0].chunk == null or + _buffer[0].datas[0].chunk[0].size == 0) + { + return 0; + } + + var plane_count: u32 = 0; + while (plane_count < _buffer[0].n_datas) : (plane_count += 1) { + if (_buffer[0].datas[plane_count].type != pw.SPA_DATA_DmaBuf) { + break; + } + } + + for (0..plane_count) |i| { + const data = _buffer[0].datas[i]; + if (data.chunk == null) { + return 0; + } + if (data.chunk[0].flags & pw.SPA_CHUNK_FLAG_CORRUPTED != 0) { + return 0; + } + } + + return plane_count; + } + /// Some DE/compositors vary on where they store the presentation timestamp. fn select_best_timestamp(self: *Self, metadata: *const pw.spa_meta_header) i128 { const raw_metadata_pts_ns: i128 = @intCast(metadata.pts); @@ -490,12 +536,6 @@ pub const PipewireVideo = struct { source = .meta_pts; } - // Limit the logging otherwise it will get spammed. - if (self.timestamp_source_log_count >= 10) { - log.info("[select_best_timestamp] video timestamp source: {}", .{source}); - self.timestamp_source_log_count += 1; - } - return timestamp_ns; } @@ -636,7 +676,6 @@ pub const PipewireVideo = struct { return; }; - // damage spa_pod_params.params.append(self.allocator, @ptrCast(@alignCast(c_def.spa_pod_builder_add_object( &builder, pw.SPA_TYPE_OBJECT_ParamMeta, @@ -644,20 +683,16 @@ pub const PipewireVideo = struct { .{ pw.SPA_PARAM_META_type, "I", - pw.SPA_META_VideoDamage, + pw.SPA_META_VideoCrop, pw.SPA_PARAM_META_size, - "?ri", - @as(i32, 3), - @as(i32, @sizeOf(pw.spa_meta_region) * 16), - @as(i32, @sizeOf(pw.spa_meta_region) * 1), - @as(i32, @sizeOf(pw.spa_meta_region) * 16), + "i", + @as(i32, @intCast(@sizeOf(pw.spa_meta_region))), }, )))) catch |err| { log.err("[send_stream_params] spa_pod_params.params.append error: {}", .{err}); return; }; - // cursor spa_pod_params.params.append(self.allocator, @ptrCast(@alignCast(c_def.spa_pod_builder_add_object( &builder, pw.SPA_TYPE_OBJECT_ParamMeta, @@ -678,6 +713,23 @@ pub const PipewireVideo = struct { return; }; + spa_pod_params.params.append(self.allocator, @ptrCast(@alignCast(c_def.spa_pod_builder_add_object( + &builder, + pw.SPA_TYPE_OBJECT_ParamMeta, + pw.SPA_PARAM_Meta, + .{ + pw.SPA_PARAM_META_type, + "I", + pw.SPA_META_VideoTransform, + pw.SPA_PARAM_META_size, + "i", + @as(i32, @intCast(@sizeOf(pw.spa_meta_videotransform))), + }, + )))) catch |err| { + log.err("[send_stream_params] spa_pod_params.params.append error: {}", .{err}); + return; + }; + spa_pod_params.params.append(self.allocator, @ptrCast(@alignCast(c_def.spa_pod_builder_add_object(&builder, pw.SPA_TYPE_OBJECT_ParamBuffers, pw.SPA_PARAM_Buffers, .{ pw.SPA_PARAM_BUFFERS_dataType, "i",