From a5e8ba65e295ca7e8b107bec6504bba5919a2704 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Wed, 14 Oct 2026 13:16:24 +0100 Subject: [PATCH] Fix fractional-scale capture sizing and release idle capture caches --- compositor/src/backends/direct/capture.rs | 39 +++++++++++++++++++++++ compositor/src/handlers/screencopy.rs | 20 ++++++++---- compositor/src/render/scene.rs | 1 + compositor/src/state.rs | 4 +++ compositor/src/winit.rs | 3 +- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/compositor/src/backends/direct/capture.rs b/compositor/src/backends/direct/capture.rs index 853f157..1da5b1b 100644 --- a/compositor/src/backends/direct/capture.rs +++ b/compositor/src/backends/direct/capture.rs @@ -3,12 +3,51 @@ use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::{GlesError, GlesRenderer, GlesTarget, GlesTexture}; use smithay::backend::renderer::{Bind, Offscreen, Texture}; use smithay::output::Output; +use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; use std::fmt::Display; +use std::time::{Duration, Instant}; use crate::Ferese; use crate::render::{capture_output_elements, redraw_output}; use crate::state::FrameScene; +const CACHE_IDLE: Duration = Duration::from_secs(2); + +pub(crate) fn touch_cache(state: &mut Ferese) { + if state.capture_last_used.replace(Instant::now()).is_some() { + return; + } + if let Err(error) = state + .loop_handle + .insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| { + let Some(last_used) = state.capture_last_used else { + return TimeoutAction::Drop; + }; + let idle = last_used.elapsed(); + if idle < CACHE_IDLE { + return TimeoutAction::ToDuration(CACHE_IDLE - idle); + } + // A stream can pause or fail without another repaint. Release its + // offscreen targets and filtered-scene caches from the event loop. + state.capture_last_used = None; + state.capture_render = Default::default(); + state.nested_capture_texture = None; + if let Some(backend) = state.direct_backend.as_mut() { + for device in backend.devices.values_mut() { + for output in device.outputs.values_mut() { + output.capture_texture = None; + output.mirror_capture_texture = None; + } + } + } + TimeoutAction::Drop + }) + { + state.capture_last_used = None; + tracing::warn!(%error, "could not schedule capture cache cleanup"); + } +} + pub(crate) fn capture_output( state: &mut Ferese, renderer: &mut GlesRenderer, diff --git a/compositor/src/handlers/screencopy.rs b/compositor/src/handlers/screencopy.rs index 6080692..d44809d 100644 --- a/compositor/src/handlers/screencopy.rs +++ b/compositor/src/handlers/screencopy.rs @@ -447,6 +447,15 @@ pub(crate) fn capture_region_for_geometry( transform: smithay::utils::Transform, requested: Option>, ) -> Option> { + let buffer_bounds = Rectangle::from_size(Size::::from((mode_size.w, mode_size.h))); + if mode_size.w <= 0 || mode_size.h <= 0 { + return None; + } + // Full-output captures must match the framebuffer exactly. Rounding the + // logical size and scaling it back can add a pixel at fractional scales. + if requested.is_none() { + return Some(buffer_bounds); + } let logical_size = output_logical_size(mode_size, scale, transform); let output_region = Rectangle::from_size(logical_size); let logical_region = requested.unwrap_or(output_region).intersection(output_region)?; @@ -455,12 +464,11 @@ pub(crate) fn capture_region_for_geometry( return None; } - Some( - logical_region - .to_f64() - .to_buffer(scale, transform, &logical_size.to_f64()) - .to_i32_round(), - ) + logical_region + .to_f64() + .to_buffer(scale, transform, &logical_size.to_f64()) + .to_i32_round() + .intersection(buffer_bounds) } pub(super) fn valid_shm_buffer(buffer: &WlBuffer, expected: Size) -> bool { diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index 91ae6af..056d6f7 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -44,6 +44,7 @@ pub(crate) fn capture_output_elements( ) -> Vec { // Blur textures and resize handoff snapshots must never be shared with // display rendering. Only the capture scene can populate these caches. + crate::backends::direct::capture::touch_cache(state); std::mem::swap(&mut state.render, &mut state.capture_render); let elements = scene_elements(state, renderer, output, include_cursor, frame, true); std::mem::swap(&mut state.render, &mut state.capture_render); diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 8a3dbf1..9107dff 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -440,6 +440,8 @@ pub struct Ferese { pub(crate) windows: WindowRegistry, pub(crate) render: crate::render::RenderResources, pub(crate) capture_render: crate::render::RenderResources, + pub(crate) capture_last_used: Option, + pub(crate) nested_capture_texture: Option, pub(crate) capture_epoch: u64, capture_protected_windows: HashSet, capture_protected_cursor: Option, @@ -742,6 +744,8 @@ impl Ferese { windows: WindowRegistry::default(), render: Default::default(), capture_render: Default::default(), + capture_last_used: None, + nested_capture_texture: None, capture_epoch: 0, capture_protected_windows: HashSet::new(), capture_protected_cursor: None, diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 89e0f40..290fafb 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -94,7 +94,6 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() } })?; - let mut capture_texture = None; event_loop .handle() .insert_source(event_source, move |event, _, state| { @@ -157,6 +156,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() || state.has_pending_screencopy(&output, true) { let scene = state.sample_frame(&output, Duration::ZERO); + let mut capture_texture = state.nested_capture_texture.take(); crate::backends::direct::capture::capture_output( state, renderer, @@ -164,6 +164,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() &output, &scene, ); + state.nested_capture_texture = capture_texture; changed = true; }