From fba9d65ba19a10e40a1bb69382a2f6aca25fac9b Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 21 May 2026 15:45:32 +1000 Subject: [PATCH] Retain surface textures for animations --- docs/window-animations-plan.md | 11 ++ src/animation.rs | 139 ++++++++++++- src/ifs/wl_buffer.rs | 13 ++ src/ifs/wl_surface/x_surface/xwindow.rs | 5 + .../wl_surface/xdg_surface/xdg_toplevel.rs | 6 + src/renderer.rs | 182 +++++++++++++++++- src/state.rs | 13 +- src/tree/toplevel.rs | 15 +- 8 files changed, 365 insertions(+), 19 deletions(-) diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md index 1c99c60e..ffbfe19c 100644 --- a/docs/window-animations-plan.md +++ b/docs/window-animations-plan.md @@ -101,6 +101,17 @@ Tests: Goal: freeze visual contents during movement and enable spawn-out. +Initial retained-record implementation status: + +- Tiled animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees. +- Retained records hold both `GfxTexture` and `SurfaceBuffer` references so the + existing buffer release/sync path remains authoritative. +- Single-pixel buffers can be retained as color records. +- Async SHM textures are not retained yet because Wry's per-surface SHM + front/back textures can be reused by later commits while an animation is still + running. Those surfaces fall back to live rendering until an explicit offscreen + copy fallback exists. + Implementation shape: - Add a retained render-record tree for toplevel surfaces. diff --git a/src/animation.rs b/src/animation.rs index b0091933..f0daa804 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1,7 +1,11 @@ use { crate::{ + cmm::{cmm_description::ColorDescription, cmm_render_intent::RenderIntent}, + gfx_api::{GfxTexture, SampleRect}, + ifs::wl_surface::{SurfaceBuffer, WlSurface}, rect::Rect, state::State, + theme::Color, tree::{LatchListener, NodeId, OutputNode}, utils::{clonecell::CloneCell, event_listener::EventListener}, }, @@ -54,6 +58,112 @@ pub struct AnimationState { tick: CloneCell>>, } +pub struct RetainedToplevel { + pub offset: (i32, i32), + pub surface: RetainedSurface, +} + +pub struct RetainedSurface { + pub offset: (i32, i32), + pub size: (i32, i32), + pub content: RetainedContent, + pub below: Vec, + pub above: Vec, +} + +pub enum RetainedContent { + Texture { + texture: Rc, + buffer: Rc, + source: SampleRect, + alpha: Option, + color_description: Rc, + render_intent: RenderIntent, + alpha_mode: crate::gfx_api::AlphaMode, + opaque: bool, + }, + Color { + color: Color, + alpha: Option, + color_description: Rc, + render_intent: RenderIntent, + }, +} + +impl RetainedToplevel { + pub fn capture_surface(surface: &WlSurface, offset: (i32, i32)) -> Option> { + Some(Rc::new(Self { + offset, + surface: RetainedSurface::capture(surface, (0, 0))?, + })) + } +} + +impl RetainedSurface { + fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option { + let buffer = surface.buffer.get()?; + let size = surface.buffer_abs_pos.get().size(); + let source = *surface.buffer_points_norm.borrow(); + let color_description = surface.color_description(); + let render_intent = surface.render_intent(); + let alpha_mode = surface.alpha_mode(); + let alpha = surface.alpha(); + let content = match buffer.buffer.buf.get_stable_texture() { + Some(texture) => RetainedContent::Texture { + opaque: surface.opaque(), + texture, + buffer, + source, + alpha, + color_description, + render_intent, + alpha_mode, + }, + None => { + let color = buffer.buffer.buf.color?; + RetainedContent::Color { + color: Color::from_u32( + color_description.eotf, + alpha_mode, + color[0], + color[1], + color[2], + color[3], + ), + alpha, + color_description, + render_intent, + } + } + }; + let mut below = vec![]; + let mut above = vec![]; + if let Some(children) = surface.children.borrow().as_deref() { + for child in children.below.iter() { + if child.pending.get() { + continue; + } + let pos = child.sub_surface.position.get(); + below.push(Self::capture(&child.sub_surface.surface, pos)?); + } + for child in children.above.iter() { + if child.pending.get() { + continue; + } + let pos = child.sub_surface.position.get(); + above.push(Self::capture(&child.sub_surface.surface, pos)?); + } + } + Some(Self { + offset, + size, + content, + below, + above, + }) + } +} + impl Default for AnimationState { fn default() -> Self { Self { @@ -79,6 +189,7 @@ impl AnimationState { node_id: NodeId, old: Rect, new: Rect, + retained: Option>, now_nsec: u64, duration_ms: u32, curve: AnimationCurve, @@ -89,10 +200,10 @@ impl AnimationState { } let duration_nsec = duration_ms as u64 * 1_000_000; let mut windows = self.windows.borrow_mut(); - let from = match windows.get(&node_id) { + let (from, retained) = match windows.get(&node_id) { Some(anim) if anim.to == new => return false, - Some(anim) => anim.rect_at(now_nsec), - None => old, + Some(anim) => (anim.rect_at(now_nsec), anim.retained.clone().or(retained)), + None => (old, retained), }; if from == new { windows.remove(&node_id); @@ -107,6 +218,7 @@ impl AnimationState { duration_nsec, curve, last_damage: from, + retained, }, ); true @@ -120,6 +232,18 @@ impl AnimationState { } } + pub fn retained_snapshot( + &self, + node_id: NodeId, + now_nsec: u64, + ) -> Option> { + let windows = self.windows.borrow(); + match windows.get(&node_id) { + Some(anim) if !anim.done(now_nsec) => anim.retained.clone(), + _ => None, + } + } + fn damage_active(&self, state: &State, now_nsec: u64) -> bool { let mut damages = vec![]; let mut any_active = false; @@ -164,6 +288,7 @@ struct WindowAnimation { duration_nsec: u64, curve: AnimationCurve, last_damage: Rect, + retained: Option>, } impl WindowAnimation { @@ -286,8 +411,8 @@ mod tests { let id = NodeId(1); let a = Rect::new_sized_saturating(0, 0, 100, 100); let b = Rect::new_sized_saturating(100, 0, 100, 100); - assert!(state.set_target(id, a, b, 0, 160, AnimationCurve::Linear)); - assert!(!state.set_target(id, a, b, 80_000_000, 160, AnimationCurve::Linear)); + assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear)); + assert!(!state.set_target(id, a, b, None, 80_000_000, 160, AnimationCurve::Linear)); assert_eq!( state.visual_rect(id, b, 80_000_000), Rect::new_sized_saturating(50, 0, 100, 100) @@ -301,8 +426,8 @@ mod tests { let a = Rect::new_sized_saturating(0, 0, 100, 100); let b = Rect::new_sized_saturating(100, 0, 100, 100); let c = Rect::new_sized_saturating(200, 0, 100, 100); - assert!(state.set_target(id, a, b, 0, 160, AnimationCurve::Linear)); - assert!(state.set_target(id, a, c, 80_000_000, 160, AnimationCurve::Linear)); + assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear)); + assert!(state.set_target(id, a, c, None, 80_000_000, 160, AnimationCurve::Linear)); assert_eq!( state.visual_rect(id, c, 80_000_000), Rect::new_sized_saturating(50, 0, 100, 100) diff --git a/src/ifs/wl_buffer.rs b/src/ifs/wl_buffer.rs index 1fff5db3..678ee0c4 100644 --- a/src/ifs/wl_buffer.rs +++ b/src/ifs/wl_buffer.rs @@ -310,6 +310,19 @@ impl WlBuffer { } } + pub fn get_stable_texture(&self) -> Option> { + match &*self.storage.borrow() { + None => None, + Some(s) => match s { + WlBufferStorage::Shm { + dmabuf_buffer_params, + .. + } => dmabuf_buffer_params.tex.clone(), + WlBufferStorage::Dmabuf { tex, .. } => tex.clone(), + }, + } + } + pub fn update_texture_or_log(&self, surface: &WlSurface, sync_shm: bool) { if let Err(e) = self.update_texture(surface, sync_shm) { log::warn!("Could not update texture: {}", ErrorFmt(e)); diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index f1c68730..a4d3e88b 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -1,5 +1,6 @@ use { crate::{ + animation::RetainedToplevel, client::Client, cursor::KnownCursor, fixed::Fixed, @@ -514,6 +515,10 @@ impl ToplevelNodeBase for Xwindow { Some(self.x.surface.clone()) } + fn tl_animation_snapshot(&self) -> Option> { + RetainedToplevel::capture_surface(&self.x.surface, (0, 0)) + } + fn tl_admits_children(&self) -> bool { false } diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 768a8367..740c7a50 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -2,6 +2,7 @@ pub mod xdg_dialog_v1; use { crate::{ + animation::RetainedToplevel, bugs, bugs::Bugs, client::{Client, ClientError}, @@ -779,6 +780,11 @@ impl ToplevelNodeBase for XdgToplevel { Some(self.xdg.surface.clone()) } + fn tl_animation_snapshot(&self) -> Option> { + let geo = self.xdg.geometry(); + RetainedToplevel::capture_surface(&self.xdg.surface, (-geo.x1(), -geo.y1())) + } + fn tl_restack_popups(&self) { self.xdg.restack_popups(); } diff --git a/src/renderer.rs b/src/renderer.rs index f8174883..5a891ac8 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,7 +1,8 @@ use { crate::{ + animation::{RetainedContent, RetainedSurface, RetainedToplevel}, cmm::cmm_render_intent::RenderIntent, - gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect}, + gfx_api::{AcquireSync, AlphaMode, BufferResv, GfxApiOpt, ReleaseSync, SampleRect}, ifs::wl_surface::{ SurfaceBuffer, WlSurface, x_surface::xwindow::Xwindow, @@ -467,6 +468,167 @@ impl Renderer<'_> { visual.move_(-container.abs_x1.get(), -container.abs_y1.get()) } + fn render_child_or_snapshot( + &mut self, + child: &Rc, + x: i32, + y: i32, + bounds: Option<&Rect>, + ) { + if let Some(retained) = self + .state + .animations + .retained_snapshot(child.node_id(), self.state.now_nsec()) + { + self.render_retained_toplevel(&retained, x, y, bounds); + } else { + child.node_render(self, x, y, bounds); + } + } + + fn render_retained_toplevel( + &mut self, + retained: &RetainedToplevel, + x: i32, + y: i32, + bounds: Option<&Rect>, + ) { + let (x, y) = self + .base + .scale_point(x + retained.offset.0, y + retained.offset.1); + self.render_retained_surface_scaled(&retained.surface, x, y, None, bounds); + } + + fn render_retained_surface_scaled( + &mut self, + retained: &RetainedSurface, + x: i32, + y: i32, + pos_rel: Option<(i32, i32)>, + bounds: Option<&Rect>, + ) { + let stretch = self.stretch.take(); + let corner_radius = self.corner_radius.take(); + let mut size = retained.size; + if let Some((x_rel, y_rel)) = pos_rel { + let (x, y) = self.base.scale_point(x_rel, y_rel); + let (w, h) = self.base.scale_point(x_rel + size.0, y_rel + size.1); + size = (w - x, h - y); + } else { + size = self.base.scale_point(size.0, size.1); + } + let mut stretched_source = None; + if let Some(s) = stretch { + if let RetainedContent::Texture { source, .. } = &retained.content { + let mut source = *source; + if size.0 > 0 && size.1 > 0 { + let sx = s.0 as f32 / size.0 as f32; + let sy = s.1 as f32 / size.1 as f32; + source.x2 *= sx; + source.y2 *= sy; + } + stretched_source = Some(source); + } + size = s; + } + for child in &retained.below { + let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1); + self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds); + } + self.corner_radius = corner_radius; + self.render_retained_content(retained, stretched_source, x, y, size, bounds); + for child in &retained.above { + let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1); + self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds); + } + } + + fn render_retained_content( + &mut self, + retained: &RetainedSurface, + stretched_source: Option, + x: i32, + y: i32, + size: (i32, i32), + bounds: Option<&Rect>, + ) { + let corner_radius = self.corner_radius.take(); + match &retained.content { + RetainedContent::Texture { + texture, + buffer, + source, + alpha, + color_description, + render_intent, + alpha_mode, + opaque, + } => { + let source = stretched_source.unwrap_or(*source); + if let Some(cr) = corner_radius { + self.base.render_rounded_texture( + texture, + *alpha, + x, + y, + Some(source), + Some(size), + self.base.scale, + bounds, + Some(buffer.clone() as Rc), + AcquireSync::Unnecessary, + buffer.release_sync, + color_description, + *render_intent, + *alpha_mode, + cr, + ); + } else { + self.base.render_texture( + texture, + *alpha, + x, + y, + Some(source), + Some(size), + self.base.scale, + bounds, + Some(buffer.clone() as Rc), + AcquireSync::Unnecessary, + buffer.release_sync, + *opaque, + color_description, + *render_intent, + *alpha_mode, + ); + } + } + RetainedContent::Color { + color, + alpha, + color_description, + render_intent, + } => { + if let Some(rect) = Rect::new_sized(x, y, size.0, size.1) { + let rect = match bounds { + None => rect, + Some(bounds) => rect.intersect(*bounds), + }; + if !rect.is_empty() { + self.base.sync(); + self.base.fill_scaled_boxes( + &[rect], + color, + *alpha, + &color_description.linear, + *render_intent, + ); + } + } + } + } + } + pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) { self.render_container_decorations(container, x, y); @@ -526,9 +688,12 @@ impl Renderer<'_> { self.corner_radius = Some(inner_cr); } } - child - .node - .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); + self.render_child_or_snapshot( + &child.node, + x + content.x1(), + y + content.y1(), + Some(&body), + ); self.stretch = None; self.corner_radius = None; } else { @@ -579,9 +744,12 @@ impl Renderer<'_> { } let body = body.move_(x, y); let body = self.base.scale_rect(body); - child - .node - .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); + self.render_child_or_snapshot( + &child.node, + x + content.x1(), + y + content.y1(), + Some(&body), + ); self.stretch = None; self.corner_radius = None; } diff --git a/src/state.rs b/src/state.rs index f12a1745..ddf69fd7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,7 +2,9 @@ use { crate::{ acceptor::Acceptor, allocator::BufferObject, - animation::{AnimationCurve, AnimationState, AnimationTick, expand_damage_rect}, + animation::{ + AnimationCurve, AnimationState, AnimationTick, RetainedToplevel, expand_damage_rect, + }, async_engine::{AsyncEngine, SpawnedFuture}, backend::{ Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice, @@ -1469,7 +1471,13 @@ impl State { self.eng.now().msec() } - pub fn queue_tiled_animation(self: &Rc, node_id: NodeId, old: Rect, new: Rect) { + pub fn queue_tiled_animation( + self: &Rc, + node_id: NodeId, + old: Rect, + new: Rect, + retained: Option>, + ) { if !self.animations.enabled.get() || !self.layout_animations_active.get() || self.suppress_animations_for_next_layout.get() @@ -1494,6 +1502,7 @@ impl State { node_id, old, new, + retained, now, self.animations.duration_ms.get(), self.animations.curve.get(), diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 34d637dd..f4678729 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1,5 +1,6 @@ use { crate::{ + animation::RetainedToplevel, client::{Client, ClientId}, criteria::{ CritDestroyListener, CritMatcherId, @@ -197,9 +198,12 @@ impl ToplevelNode for T { && !self.node_is_container() && !parent_is_mono { - data.state - .clone() - .queue_tiled_animation(data.node_id, prev, *rect); + data.state.clone().queue_tiled_animation( + data.node_id, + prev, + *rect, + self.tl_animation_snapshot(), + ); } if prev.size() != rect.size() { for sc in data.jay_screencasts.lock().values() { @@ -316,6 +320,11 @@ pub trait ToplevelNodeBase: Node { fn tl_scanout_surface(&self) -> Option> { None } + + fn tl_animation_snapshot(&self) -> Option> { + None + } + fn tl_restack_popups(&self) { // nothing }