improv: clear image_cache on refresh

This commit is contained in:
Hojjat 2026-08-24 10:57:29 -06:00 • committed by Victoria Brekenfeld
parent 80b8eb47d7
commit 31827ed240
2 changed files with 80 additions and 29 deletions

View file

@ -460,9 +460,7 @@ pub struct CursorStateInner {
cursors: HashMap<CursorIcon, Cursor>, cursors: HashMap<CursorIcon, Cursor>,
current_image: Option<Image>, current_image: Option<Image>,
/// Rasterized cursor frames keyed by `(shape, pixel size, frame index)` so image_cache: Vec<CachedFrame>,
/// each SVG frame is rasterized (and uploaded) at most once per size.
image_cache: Vec<((CursorIcon, u32, usize), Image, MemoryRenderBuffer)>,
hidden: bool, hidden: bool,
idle_timer: Option<RegistrationToken>, idle_timer: Option<RegistrationToken>,
@ -476,6 +474,15 @@ pub struct CursorStateInner {
magnification: f32, magnification: f32,
anim_from: f32, anim_from: f32,
anim_start: Option<Instant>, anim_start: Option<Instant>,
rest_started: Option<Instant>,
}
/// A rasterized cursor frame, keyed by `(shape, pixel size, frame index)`.
struct CachedFrame {
key: (CursorIcon, u32, usize),
image: Image,
buffer: MemoryRenderBuffer,
unmagnified: bool,
} }
/// One sampled pointer position on the recent motion path. /// One sampled pointer position on the recent motion path.
@ -485,6 +492,9 @@ struct PathSample {
time: Instant, time: Instant,
} }
/// How long everything must stay unmagnified before the enlarged frames go.
const MAGNIFIED_FRAME_GRACE: Duration = Duration::from_secs(10);
/// How far back the motion path is considered when looking for a shake. /// How far back the motion path is considered when looking for a shake.
const SHAKE_INTERVAL: Duration = Duration::from_millis(1000); const SHAKE_INTERVAL: Duration = Duration::from_millis(1000);
/// Path-length / bounding-box-diagonal ratio required to count as a shake. /// Path-length / bounding-box-diagonal ratio required to count as a shake.
@ -529,6 +539,21 @@ impl CursorStateInner {
self.cursor_size self.cursor_size
} }
/// Drop the rasterizations only a magnified cursor needed, once nothing has
/// magnified it for [`MAGNIFIED_FRAME_GRACE`].
pub fn drop_magnified_frames(&mut self, now: Instant, zoomed: bool) {
if zoomed || self.is_magnifying() {
self.rest_started = None;
return;
}
let rest_started = *self.rest_started.get_or_insert(now);
if now.duration_since(rest_started) < MAGNIFIED_FRAME_GRACE {
return;
}
self.image_cache.retain(|frame| frame.unmagnified);
}
/// Feed one relative-motion event into the shake detector. /// Feed one relative-motion event into the shake detector.
pub fn detect_shake(&mut self, delta: Point<f64, Logical>, now: Instant) { pub fn detect_shake(&mut self, delta: Point<f64, Logical>, now: Instant) {
// Drop samples that have aged out of the time window. // Drop samples that have aged out of the time window.
@ -680,6 +705,7 @@ impl Default for CursorStateInner {
magnification: 1.0, magnification: 1.0,
anim_from: 1.0, anim_from: 1.0,
anim_start: None, anim_start: None,
rest_started: None,
} }
} }
} }
@ -748,34 +774,36 @@ pub fn draw_cursor<R>(
let key = (current_cursor, size_px, frame_idx); let key = (current_cursor, size_px, frame_idx);
// Rasterize and upload this (shape, size, frame) only if not cached. // Rasterize and upload this (shape, size, frame) only if not cached.
if !state let index = match state.image_cache.iter().position(|frame| frame.key == key) {
.image_cache Some(index) => index,
.iter() None => {
.any(|(cached, _, _)| *cached == key) let image = {
{ let cursor = state.get_named_cursor(current_cursor);
let frame = { cursor.render_frame(size_px, frame_idx)
let cursor = state.get_named_cursor(current_cursor); };
cursor.render_frame(size_px, frame_idx) let actual_scale = (image.size / state.size()).max(1);
}; let buffer = MemoryRenderBuffer::from_slice(
let actual_scale = (frame.size / state.size()).max(1); &image.pixels_rgba,
let buffer = MemoryRenderBuffer::from_slice( Fourcc::Argb8888,
&frame.pixels_rgba, (image.width as i32, image.height as i32),
Fourcc::Argb8888, actual_scale as i32,
(frame.width as i32, frame.height as i32), Transform::Normal,
actual_scale as i32, None,
Transform::Normal, );
None, state.image_cache.push(CachedFrame {
); key,
state.image_cache.push((key, frame, buffer)); image,
} buffer,
unmagnified: size_px == unmagnified_px,
});
state.image_cache.len() - 1
}
};
let (frame, pointer_image) = { let (frame, pointer_image) = {
let (_, frame, buffer) = state let entry = &mut state.image_cache[index];
.image_cache entry.unmagnified |= size_px == unmagnified_px;
.iter() (entry.image.clone(), entry.buffer.clone())
.find(|(cached, _, _)| *cached == key)
.unwrap();
(frame.clone(), buffer.clone())
}; };
let actual_scale = (frame.size / state.size()).max(1); let actual_scale = (frame.size / state.size()).max(1);

View file

@ -1614,6 +1614,29 @@ impl Common {
a11y_keyboard_monitor.refresh(); a11y_keyboard_monitor.refresh();
} }
self.image_copy_capture_state.cleanup(); self.image_copy_capture_state.cleanup();
self.cleanup_cursor_images();
}
/// Release the enlarged cursor frames a finished shake or zoom left behind.
fn cleanup_cursor_images(&mut self) {
let shell = self.shell.read();
let zoomed = shell.zoom_state.as_ref().is_some_and(|zoom_state| {
shell
.outputs()
.any(|output| zoom_state.animating_level(output) > 1.0)
});
let now = Instant::now();
for seat in shell.seats.iter() {
if let Some(cursor_state) = seat
.user_data()
.get::<crate::backend::render::cursor::CursorState>()
{
cursor_state
.lock()
.unwrap()
.drop_magnified_frames(now, zoomed);
}
}
} }
pub fn refresh_idle_inhibit(&mut self) { pub fn refresh_idle_inhibit(&mut self) {