1
0
Fork 0
forked from wry/wry

Retain surface textures for animations

This commit is contained in:
atagen 2026-05-21 15:45:32 +10:00
parent 3540cdc4be
commit fba9d65ba1
8 changed files with 365 additions and 19 deletions

View file

@ -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.

View file

@ -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<Option<Rc<AnimationTick>>>,
}
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<RetainedSurface>,
pub above: Vec<RetainedSurface>,
}
pub enum RetainedContent {
Texture {
texture: Rc<dyn GfxTexture>,
buffer: Rc<SurfaceBuffer>,
source: SampleRect,
alpha: Option<f32>,
color_description: Rc<ColorDescription>,
render_intent: RenderIntent,
alpha_mode: crate::gfx_api::AlphaMode,
opaque: bool,
},
Color {
color: Color,
alpha: Option<f32>,
color_description: Rc<ColorDescription>,
render_intent: RenderIntent,
},
}
impl RetainedToplevel {
pub fn capture_surface(surface: &WlSurface, offset: (i32, i32)) -> Option<Rc<Self>> {
Some(Rc::new(Self {
offset,
surface: RetainedSurface::capture(surface, (0, 0))?,
}))
}
}
impl RetainedSurface {
fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option<Self> {
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<Rc<RetainedToplevel>>,
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<Rc<RetainedToplevel>> {
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<Rc<RetainedToplevel>>,
}
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)

View file

@ -310,6 +310,19 @@ impl WlBuffer {
}
}
pub fn get_stable_texture(&self) -> Option<Rc<dyn GfxTexture>> {
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));

View file

@ -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<Rc<RetainedToplevel>> {
RetainedToplevel::capture_surface(&self.x.surface, (0, 0))
}
fn tl_admits_children(&self) -> bool {
false
}

View file

@ -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<Rc<RetainedToplevel>> {
let geo = self.xdg.geometry();
RetainedToplevel::capture_surface(&self.xdg.surface, (-geo.x1(), -geo.y1()))
}
fn tl_restack_popups(&self) {
self.xdg.restack_popups();
}

View file

@ -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<dyn ToplevelNode>,
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<SampleRect>,
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<dyn BufferResv>),
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<dyn BufferResv>),
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;
}

View file

@ -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<Self>, node_id: NodeId, old: Rect, new: Rect) {
pub fn queue_tiled_animation(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
retained: Option<Rc<RetainedToplevel>>,
) {
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(),

View file

@ -1,5 +1,6 @@
use {
crate::{
animation::RetainedToplevel,
client::{Client, ClientId},
criteria::{
CritDestroyListener, CritMatcherId,
@ -197,9 +198,12 @@ impl<T: ToplevelNodeBase> 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<Rc<WlSurface>> {
None
}
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
None
}
fn tl_restack_popups(&self) {
// nothing
}