atagen

monolith

public
Star0
Watch0

Commit 82c94edb8c50

tarball

atagen <boss@atagen.co>2026-10-17 05:00 UTC

5 files changed+60-25
monolith: keep closing windows' last frames in the window model

Ghost carried a VulkanTexture, so the animator, which otherwise only plans geometry and timing, depended on the GPU. Wm::last_frames now holds the frame, keyed by window and pruned with the ghosts.
Modified file: crates/monolith/src/anim.rs+2 additions−8 deletions
@@ -6,8 +6,7 @@ use monolith_config::{Animation, AnimationStyle};
66 use monolith_layout::{OutputId, Rect, WindowId};
77 use monolith_motion::{CubicBezier, Edges, Position, Timeline, group_initial_slope};
88 use monolith_planner::{Config as PlanConfig, Plan, Style};
9-use monolith_render_vk::{Effect, Transition, VulkanTexture};
10-use smithay::utils::{Logical, Rectangle, Transform};
9+use monolith_render_vk::{Effect, Transition};
1110 use std::collections::HashMap;
1211
1312 const VELOCITY_DT_NS: u64 = 1_000_000;
@@ -21,16 +20,11 @@ pub struct Animator {
2120 }
2221
2322 /// A closed tiled window that stays in its output's plan, shrinking to
24-/// `seed` with the last frame its client showed.
23+/// `seed`. `Wm::last_frames` holds what it shows.
2524 pub struct Ghost {
2625 pub id: WindowId,
2726 pub output: OutputId,
2827 pub seed: Rect,
29- pub texture: VulkanTexture,
30- pub scale: i32,
31- pub transform: Transform,
32- /// Window geometry inside the buffer.
33- pub geometry: Rectangle<i32, Logical>,
3428 pub born: u64,
3529 }
3630
Modified file: crates/monolith/src/scene.rs+13 additions−8 deletions
@@ -44,7 +44,12 @@ pub fn build(
4444 output: Option<&Output>,
4545 unmanaged: &[X11Surface],
4646 ) -> Vec<Draw> {
47- let (layout, anim, windows) = wm.parts();
47+ let crate::wm::Parts {
48+ layout,
49+ anim,
50+ windows,
51+ last_frames,
52+ } = wm.parts();
4853 let Some(area) = layout.output(o).map(|out| out.area) else {
4954 return Vec::new();
5055 };
@@ -53,8 +58,8 @@ pub fn build(
5358 let mut tiled = Vec::new();
5459 let mut above = Vec::new();
5560 for g in anim.ghosts().iter().filter(|g| g.output == o) {
56- if let Some(r) = anim.tiled(o, g.id, now) {
57- ghosts.push((g, local(r)));
61+ if let (Some(r), Some(frame)) = (anim.tiled(o, g.id, now), last_frames.get(&g.id)) {
62+ ghosts.push((g, frame, local(r)));
5863 }
5964 }
6065 let arr = layout.arrange(o);
@@ -118,18 +123,18 @@ pub fn build(
118123 border_elements(&mut out, borders, r, deco.border as i32, color, alpha);
119124 }
120125 }
121- for (g, r) in ghosts.into_iter().rev() {
126+ for (g, frame, r) in ghosts.into_iter().rev() {
122127 let Some((at, size)) = pixels(r) else {
123128 continue;
124129 };
125130 let src = Rectangle::new(
126- g.geometry.loc.to_f64(),
131+ frame.geometry.loc.to_f64(),
127132 (f64::from(size.0), f64::from(size.1)).into(),
128133 );
129134 out.push(Draw::Effect(EffectElement::new(
130- &g.texture,
131- g.scale,
132- g.transform,
135+ &frame.texture,
136+ frame.scale,
137+ frame.transform,
133138 src,
134139 at,
135140 size,
Modified file: crates/monolith/src/state.rs+1 additions−1 deletions
@@ -197,7 +197,7 @@ impl State {
197197 crate::backend::Backend::Nested(_) => crate::nested::render(self),
198198 crate::backend::Backend::Starting => {}
199199 }
200- self.wm.anim.prune(now_ns());
200+ self.wm.prune(now_ns());
201201 self.confirm_lock();
202202 self.complete_captures();
203203 if std::mem::take(&mut self.dirty.snapshot) {
Modified file: crates/monolith/src/windows.rs+10 additions−5 deletions
@@ -1,6 +1,6 @@
11 use crate::anim::Ghost;
22 use crate::state::{State, now_ns};
3-use crate::wm::{identity, root};
3+use crate::wm::{LastFrame, identity, root};
44 use monolith_layout::{Rect, WindowId};
55 use monolith_render_vk::VulkanTexture;
66 use smithay::backend::renderer::Renderer;
@@ -157,14 +157,19 @@ impl State {
157157 })
158158 .flatten();
159159 if let Some((texture, scale, transform)) = snapshot {
160+ self.wm.last_frames.insert(
161+ id,
162+ LastFrame {
163+ texture,
164+ scale,
165+ transform,
166+ geometry: window.geometry(),
167+ },
168+ );
160169 self.wm.anim.add_ghost(Ghost {
161170 id,
162171 output: o,
163172 seed: seed.rect,
164- texture,
165- scale,
166- transform,
167- geometry: window.geometry(),
168173 born: now_ns(),
169174 });
170175 }
Modified file: crates/monolith/src/wm.rs+34 additions−3 deletions
@@ -4,12 +4,13 @@
44 use crate::anim::Animator;
55
66 use monolith_layout::{Layout, OutputId, Rect, WindowId};
7+use monolith_render_vk::VulkanTexture;
78 use slotmap::SecondaryMap;
89 use smithay::backend::renderer::element::solid::SolidColorBuffer;
910 use smithay::desktop::{PopupManager, Space, Window};
1011 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
1112 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
12-use smithay::utils::Rectangle;
13+use smithay::utils::{Logical, Rectangle, Transform};
1314 use smithay::wayland::compositor::with_states;
1415 use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle;
1516 use smithay::wayland::seat::WaylandFocus;
@@ -28,6 +29,23 @@ pub struct Wm {
2829 pub anim: Animator,
2930 /// A layer surface given the keyboard by a click, like a launcher.
3031 pub layer_focus: Option<WlSurface>,
32+ /// What closed windows still show while their ghost animates.
33+ pub last_frames: HashMap<WindowId, LastFrame>,
34+}
35+
36+pub struct LastFrame {
37+ pub texture: VulkanTexture,
38+ pub scale: i32,
39+ pub transform: Transform,
40+ /// Window geometry inside the buffer.
41+ pub geometry: Rectangle<i32, Logical>,
42+}
43+
44+pub struct Parts<'a> {
45+ pub layout: &'a Layout,
46+ pub anim: &'a Animator,
47+ pub windows: &'a mut SecondaryMap<WindowId, WindowState>,
48+ pub last_frames: &'a HashMap<WindowId, LastFrame>,
3149 }
3250
3351 pub struct WindowState {
@@ -74,6 +92,7 @@ impl Wm {
7492 popups: PopupManager::default(),
7593 anim,
7694 layer_focus: None,
95+ last_frames: HashMap::new(),
7796 }
7897 }
7998
@@ -152,8 +171,20 @@ impl Wm {
152171
153172 /// The layout and animator to read alongside the records to write, which
154173 /// a frame needs at once.
155- pub fn parts(&mut self) -> (&Layout, &Animator, &mut SecondaryMap<WindowId, WindowState>) {
156- (&self.layout, &self.anim, &mut self.windows)
174+ pub fn parts(&mut self) -> Parts<'_> {
175+ Parts {
176+ layout: &self.layout,
177+ anim: &self.anim,
178+ windows: &mut self.windows,
179+ last_frames: &self.last_frames,
180+ }
181+ }
182+
183+ pub fn prune(&mut self, now: u64) {
184+ self.anim.prune(now);
185+ let ghosts = self.anim.ghosts();
186+ self.last_frames
187+ .retain(|id, _| ghosts.iter().any(|g| g.id == *id));
157188 }
158189
159190 pub fn any_awaiting(&self) -> bool {