From 2d1ffc409b6f69f28ce5cfa5675feadde840a089 Mon Sep 17 00:00:00 2001 From: Pablo Rodriguez Date: Fri, 18 Sep 2026 08:03:36 -0700 Subject: [PATCH 1/2] perf(linux): capture wlr screencopy frames with damage Co-Authored-By: Claude Opus 5 (1M context) --- src/platform/linux/wayland.cpp | 5 +++-- src/platform/linux/wlgrab.cpp | 13 ++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index ee3f2c3edd6..30eb773d755 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -507,9 +507,10 @@ namespace wl { // Store for cleanup self->current_wl_buffer = buffer; - // Start the actual copy + // Start the actual copy. With damage, the compositor completes it on the next change to the output instead of + // redrawing the whole output for it, so an unchanged screen costs nothing to capture. zwp_linux_buffer_params_v1_destroy(params); - zwlr_screencopy_frame_v1_copy(frame, buffer); + zwlr_screencopy_frame_v1_copy_with_damage(frame, buffer); } // Buffer params failed callback diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 30be46e5fad..e7470ee9601 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -174,8 +174,19 @@ namespace wl { inline platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto to = std::chrono::steady_clock::now() + timeout; + // A frame copied with damage stays pending until the output changes, so after a timeout it is still in flight: + // keep waiting on it rather than requesting another, which would replace the buffer it is being copied into. + if (dmabuf.status != dmabuf_t::WAITING) { + dmabuf.listen( + interface.screencopy_manager, + interface.dmabuf_interface, + &interface.supported_modifiers, + output, + cursor + ); + } + // Dispatch events until we get a new frame or the timeout expires - dmabuf.listen(interface.screencopy_manager, interface.dmabuf_interface, &interface.supported_modifiers, output, cursor); do { auto remaining_time_ms = std::chrono::duration_cast(to - std::chrono::steady_clock::now()); if (remaining_time_ms.count() < 0 || !display.dispatch(remaining_time_ms)) { From 656359c1263206d95edf4826ce18ba7fee1647aa Mon Sep 17 00:00:00 2001 From: Pablo Rodriguez Date: Fri, 18 Sep 2026 08:29:53 -0700 Subject: [PATCH 2/2] test(linux): cover the wlr pending-frame guard and document capture on damage Co-Authored-By: Claude Opus 5 (1M context) --- docs/configuration.md | 3 ++- src/platform/linux/wayland.h | 11 +++++++++++ src/platform/linux/wlgrab.cpp | 8 +++++--- tests/unit/platform/linux/test_wayland.cpp | 15 +++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7b0ff4da66b..4eb74577b4c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2204,7 +2204,8 @@ supported on the current platform. wlr Capture for wlroots based Wayland compositors via wlr-screencopy-unstable-v1. It is possible to capture - virtual displays in e.g. Hyprland using this method. + virtual displays in e.g. Hyprland using this method. Frames are captured only when the display changes, so + an idle display costs the compositor no redraws. @note{Applies to Linux only.} diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h index 5cd24bf0bc5..64f99b837a8 100644 --- a/src/platform/linux/wayland.h +++ b/src/platform/linux/wayland.h @@ -227,6 +227,17 @@ namespace wl { bool y_invert {false}; }; + /** + * @brief Determine whether a capture should request a new screencopy frame. + * + * Frames are copied with damage, so the compositor holds a request until the output changes. A capture that timed + * out still has that request in flight, and requesting another would replace the buffer it is being copied into. + * + * @param status State of the current screencopy request. + * @return `true` unless a requested frame is still pending. + */ + bool should_request_frame(dmabuf_t::status_e status); + /** * @brief Wayland output metadata used to match a configured display name. */ diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index e7470ee9601..0258492fa63 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -33,6 +33,10 @@ namespace wl { return false; } + bool should_request_frame(dmabuf_t::status_e status) { + return status != dmabuf_t::WAITING; + } + /** * @brief Captured frame buffer shared between capture and encode stages. */ @@ -174,9 +178,7 @@ namespace wl { inline platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor) { auto to = std::chrono::steady_clock::now() + timeout; - // A frame copied with damage stays pending until the output changes, so after a timeout it is still in flight: - // keep waiting on it rather than requesting another, which would replace the buffer it is being copied into. - if (dmabuf.status != dmabuf_t::WAITING) { + if (should_request_frame(dmabuf.status)) { dmabuf.listen( interface.screencopy_manager, interface.dmabuf_interface, diff --git a/tests/unit/platform/linux/test_wayland.cpp b/tests/unit/platform/linux/test_wayland.cpp index 98b221a9dde..e8d50e62b2e 100644 --- a/tests/unit/platform/linux/test_wayland.cpp +++ b/tests/unit/platform/linux/test_wayland.cpp @@ -104,6 +104,21 @@ TEST(WaylandCaptureTest, UsesVramForCudaOnlyWhenCudaSupportIsBuilt) { #endif } +TEST(WaylandCaptureTest, RequestsTheFirstFrame) { + wl::dmabuf_t dmabuf; + + EXPECT_TRUE(wl::should_request_frame(dmabuf.status)); +} + +TEST(WaylandCaptureTest, WaitsOnAFramePendingDamage) { + EXPECT_FALSE(wl::should_request_frame(wl::dmabuf_t::WAITING)); +} + +TEST(WaylandCaptureTest, RequestsAFrameOnceTheLastOneCompleted) { + EXPECT_TRUE(wl::should_request_frame(wl::dmabuf_t::READY)); + EXPECT_TRUE(wl::should_request_frame(wl::dmabuf_t::REINIT)); +} + TEST(WaylandInterfaceTest, RecordsOnlyExplicitDmabufModifiers) { constexpr std::uint32_t format = DRM_FORMAT_XRGB8888; constexpr std::uint64_t explicit_modifier = 0x100000000000004;