Skip to content

perf(linux): capture wlr screencopy frames with damage - #5748

Open
pablo-tech wants to merge 2 commits into
LizardByte:masterfrom
pablo-tech:perf-wlr-copy-with-damage
Open

pablo-tech wants to merge 2 commits into
LizardByte:masterfrom
pablo-tech:perf-wlr-copy-with-damage

Conversation

@pablo-tech

@pablo-tech pablo-tech commented Sep 18, 2026 •

Copy link
Copy Markdown

Description

Captures wlr screencopy frames with damage instead of on request.

What changes

  • zwlr_screencopy_frame_v1_copy → zwlr_screencopy_frame_v1_copy_with_damage (src/platform/linux/wayland.cpp).
  • snapshot() in src/platform/linux/wlgrab.cpp no longer requests a new frame while one is still pending, via the new wl::should_request_frame().
  • 3 unit tests (tests/unit/platform/linux/test_wayland.cpp) and one docs sentence (docs/configuration.md).

Why

  • A plain copy makes the compositor damage and repaint the whole output for every captured frame (Hyprland 0.56.2: src/protocols/Screencopy.cpp:127-128).
  • On a software renderer (llvmpipe) that cost ~420% CPU with Moonlight connected to an idle screen, and made the pointer lag.
  • With damage, the compositor completes the copy only when the output changes. An unchanged screen hits the existing 1000 ms snapshot timeout, which Sunshine already treats as "no new frame".
  • A damage-copy can outlive that timeout; requesting another would replace the buffer the pending frame is bound to, hence the guard.

Measured (Hyprland 0.56.2, llvmpipe, 1920x1080, Azure NV6ads A10 v5, Moonlight connected, idle screen)

before after
Hyprland CPU 410-440% 0%
CPU pressure (some avg10) 9.5 0.5
Sunshine CPU 10-50% 0%

With no client connected Hyprland sat at 0-1%, which isolates the cost to the capture.

Tested

  • Running on that machine since 2026-09-18: the stream updates, stays live on an idle screen, draws the cursor, and reconnects.
  • Unit tests: 593 pass, 0 fail. Three suites (Audio, MouseHID, Encoder) fail in setup identically on unpatched master (no audio/display session in the build shell).
  • Forcing should_request_frame to always return true fails WaitsOnAFramePendingDamage.

Scope

  • Linux wlr capture only; portal, KMS and X11 paths are untouched.

Screenshot

Issues Fixed or Closed

Roadmap Issues

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

See our AI usage policy.

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

pablo-tech and others added 2 commits September 18, 2026 08:05
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n damage

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@jarrodcolburn

jarrodcolburn commented Sep 24, 2026 •

Copy link
Copy Markdown

Tested this on real hardware (hardware encoding, not llvmpipe) and it helps there too.

Setup

  • Hyprland 0.56.2, capture = wlr, 1920x1080@60 output
  • Intel UHD 620 (Kaby Lake R, i7-8550U laptop), hevc_vaapi via iHD 26.2.4
  • Client: Moonlight Android on a Chromecast with Google TV, 1080p60 HEVC, 70 Mbps
  • minimum_fps_target = 1

Build
I backported the two functional changes onto the 2026.516.143833 release (the Arch/Omarchy package I run): copy → copy_with_damage in wayland.cpp, and the "don't request while WAITING" guard in wlgrab.cpp snapshot(). The patch doesn't apply as-is to 516 because wlgrab.cpp has changed since then. dmabuf_t::status starts as READY there too, so the first frame is still requested.

Results (Moonlight connected)

stock 2026.516 with this change
Static screen (empty workspace), stream FPS — ~1 (0.99, = minimum_fps_target)
Static screen, Hyprland / Sunshine CPU — 0% / ~4.5%
Terminal with a small spinner updating, stream FPS 60 ~18-22
Same, bandwidth ~6.8 Mbps ~2.5 Mbps
Same, Sunshine / Hyprland CPU ~18% / ~7% ~8% / ~2.5-3%
Terminal scrolling continuously, stream FPS 60 60
Host processing latency ~10.6 ms ~10.9-12 ms

With stock wlr capture the stream stays pinned at 60 FPS whatever is on screen, so minimum_fps_target never takes effect. With this change the frame rate follows real damage: it drops to the configured minimum on a static screen and goes back to full rate as soon as content changes.

No crashes or stalls in my session: the stream started normally, the cursor was drawn, and it returned to 60 FPS as soon as content changed.

(Edited: my first version blamed the remaining ~20 FPS on a bar widget. That was wrong. My workspace switch had silently failed, so those readings came from a terminal that was actively updating. On a truly static workspace it drops to ~1 FPS as expected.)

@jeffscottward

jeffscottward commented Sep 29, 2026 •

Copy link
Copy Markdown

The should_request_frame() guard here also fixes a real VRAM leak, independent of the switch to copy_with_damage.

  • Without the guard: when the output is powered off (DPMS), Hyprland never completes the copy but still answers every new request with buffer_done. Each 1000 ms snapshot timeout therefore allocated another full-size GBM buffer and overwrote the pending one. In a reproduction against a powered-off 2560×1440 output, 30 timeouts leaked 30 buffers of 14,745,600 bytes each (about 480 MiB).
  • With this guard: the same run produced 1 allocation.

On a real idle stream the same kind of unattributed growth filled a 12 GB card until NVENC failed; I believe it was this mechanism, but I only reproduced it outside Sunshine. Details are in #5810.

Together with #5764, which closes the leaked render-node fd, this looks likely to fix the DPMS-off case in #5810.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants