atagen

monolith

public
Star0
Watch0

Commit 96db497859dc

tarball

atagen <boss@atagen.co>2026-10-16 11:42 UTC

9 files changed+305-38
render-vk, monolith: route spawn and close through a transition shader

Spawning windows and closing ghosts are drawn by an effect pipeline whose effect() function receives the transition kind and progress, so custom shaders later only replace that function. The built-in effect fades windows in and out. Subsurfaces and popups of a spawning window fade with the same progress through plain alpha, since only the root surface goes through the shader.
Modified file: crates/monolith/src/anim.rs+35 additions−1 deletions
@@ -6,7 +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::VulkanTexture;
9+use monolith_render_vk::{Effect, Transition, VulkanTexture};
1010 use smithay::utils::{Logical, Rectangle, Transform};
1111 use std::collections::HashMap;
1212
@@ -17,6 +17,7 @@ pub struct Animator {
1717 tracks: HashMap<OutputId, Track>,
1818 glides: HashMap<WindowId, Glide>,
1919 ghosts: Vec<Ghost>,
20+ spawns: HashMap<WindowId, u64>,
2021 }
2122
2223 /// A closed tiled window that stays in its output's plan, shrinking to
@@ -30,6 +31,7 @@ pub struct Ghost {
3031 pub transform: Transform,
3132 /// Window geometry inside the buffer.
3233 pub geometry: Rectangle<i32, Logical>,
34+ pub born: u64,
3335 }
3436
3537 struct Track {
@@ -56,6 +58,7 @@ impl Animator {
5658 tracks: HashMap::new(),
5759 glides: HashMap::new(),
5860 ghosts: Vec::new(),
61+ spawns: HashMap::new(),
5962 }
6063 }
6164
@@ -201,13 +204,44 @@ impl Animator {
201204 &self.ghosts
202205 }
203206
207+ pub fn spawned(&mut self, id: WindowId, now: u64) {
208+ if self.phase_ns() > 0 {
209+ self.spawns.insert(id, now);
210+ }
211+ }
212+
213+ /// The transition effect on a window, lasting one phase from its spawn.
214+ pub fn effect(&self, id: WindowId, now: u64) -> Option<Effect> {
215+ let start = *self.spawns.get(&id)?;
216+ self.progress(start, now).map(|progress| Effect {
217+ kind: Transition::Spawn,
218+ progress,
219+ })
220+ }
221+
222+ pub fn ghost_effect(&self, g: &Ghost, now: u64) -> Effect {
223+ Effect {
224+ kind: Transition::Close,
225+ progress: self.progress(g.born, now).unwrap_or(1.0),
226+ }
227+ }
228+
229+ fn progress(&self, start: u64, now: u64) -> Option<f32> {
230+ let p = now.saturating_sub(start) as f64 / self.phase_ns() as f64;
231+ (p < 1.0).then_some(p as f32)
232+ }
233+
204234 pub fn active(&self, now: u64) -> bool {
205235 self.tracks.values().any(|t| t.timeline.end_ns() > now)
206236 || self.glides.values().any(|g| g.timeline.end_ns() > now)
237+ || !self.spawns.is_empty()
207238 }
208239
209240 pub fn prune(&mut self, now: u64) {
210241 self.glides.retain(|_, g| g.timeline.end_ns() > now);
242+ let phase = self.phase_ns();
243+ self.spawns
244+ .retain(|_, start| now.saturating_sub(*start) < phase);
211245 let tracks = &self.tracks;
212246 self.ghosts.retain(|g| {
213247 tracks
Modified file: crates/monolith/src/scene.rs+141 additions−11 deletions
@@ -3,15 +3,17 @@
33
44 use crate::anim::Animator;
55 use monolith_layout::{Layout, Rect, WindowId};
6-use monolith_render_vk::{VulkanRenderer, VulkanTexture};
6+use monolith_render_vk::{Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
77 use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
88 use smithay::backend::renderer::element::texture::TextureRenderElement;
99 use smithay::backend::renderer::element::utils::CropRenderElement;
10-use smithay::backend::renderer::element::{AsRenderElements, Id, Kind};
10+use smithay::backend::renderer::element::{AsRenderElements, Element, Id, Kind, RenderElement};
11+use smithay::backend::renderer::utils::CommitCounter;
1112 use smithay::backend::renderer::utils::with_renderer_surface_state;
12-use smithay::backend::renderer::{ContextId, Renderer};
13+use smithay::backend::renderer::{ContextId, Renderer, Texture};
1314 use smithay::desktop::Window;
14-use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
15+use smithay::utils::user_data::UserDataMap;
16+use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
1517 use std::collections::HashMap;
1618
1719 smithay::backend::renderer::element::render_elements! {
@@ -19,6 +21,7 @@ smithay::backend::renderer::element::render_elements! {
1921 Surface=WaylandSurfaceRenderElement<VulkanRenderer>,
2022 Cropped=CropRenderElement<WaylandSurfaceRenderElement<VulkanRenderer>>,
2123 Texture=TextureRenderElement<VulkanTexture>,
24+ Effect=EffectElement,
2225 }
2326
2427 /// Elements for every output, front to back.
@@ -64,10 +67,9 @@ pub fn build(
6467 let mut out = Vec::new();
6568 for (id, r) in tiled.into_iter().chain(above).rev() {
6669 if let Some(window) = windows.get(&id) {
67- window_elements(&mut out, renderer, window, r);
70+ window_elements(&mut out, renderer, window, r, anim.effect(id, now));
6871 }
6972 }
70- let context = renderer.context_id();
7173 for (g, r) in ghosts.into_iter().rev() {
7274 let Some((at, size)) = pixels(r) else {
7375 continue;
@@ -76,15 +78,15 @@ pub fn build(
7678 g.geometry.loc.to_f64(),
7779 (f64::from(size.0), f64::from(size.1)).into(),
7880 );
79- out.push(texture(
80- &context,
81+ out.push(Draw::Effect(EffectElement::new(
8182 &g.texture,
8283 g.scale,
8384 g.transform,
8485 src,
8586 at,
8687 size,
87- ));
88+ anim.ghost_effect(g, now),
89+ )));
8890 }
8991 out
9092 }
@@ -92,14 +94,25 @@ pub fn build(
9294 /// Draws `window` filling `r`. A buffer bigger than `r` is cropped, and one
9395 /// smaller has its last row and column stretched over the rest, so a resize
9496 /// in flight never shows a scaled or missing client.
95-fn window_elements(out: &mut Vec<Draw>, renderer: &mut VulkanRenderer, window: &Window, r: Rect) {
97+fn window_elements(
98+ out: &mut Vec<Draw>,
99+ renderer: &mut VulkanRenderer,
100+ window: &Window,
101+ r: Rect,
102+ effect: Option<Effect>,
103+) {
96104 let Some(((x, y), (w, h))) = pixels(r) else {
97105 return;
98106 };
99107 let geo = window.geometry();
100108 let loc = Point::<i32, Physical>::from((x - geo.loc.x, y - geo.loc.y));
109+ let alpha = effect.map_or(1.0, |e| e.progress);
101110 let elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>> =
102- window.render_elements(renderer, loc, Scale::from(1.0), 1.0);
111+ window.render_elements(renderer, loc, Scale::from(1.0), alpha);
112+ if let Some(effect) = effect {
113+ effect_elements(out, renderer, window, elems, ((x, y), (w, h)), effect);
114+ return;
115+ }
103116 if (w, h) == (geo.size.w, geo.size.h) {
104117 out.extend(elems.into_iter().map(Draw::Surface));
105118 return;
@@ -182,3 +195,120 @@ fn texture(
182195 Kind::Unspecified,
183196 ))
184197 }
198+
199+/// A window in a spawn transition. Its root surface goes through the effect
200+/// shader, clamped to fill its rect, and its subsurfaces and popups fade with it.
201+fn effect_elements(
202+ out: &mut Vec<Draw>,
203+ renderer: &mut VulkanRenderer,
204+ window: &Window,
205+ elems: Vec<WaylandSurfaceRenderElement<VulkanRenderer>>,
206+ (at, size): ((i32, i32), (i32, i32)),
207+ effect: Effect,
208+) {
209+ let Some(root) = window.toplevel().map(|t| t.wl_surface().clone()) else {
210+ return;
211+ };
212+ let root_id = Id::from_wayland_resource(&root);
213+ let crop = Rectangle::new(at.into(), size.into());
214+ let context = renderer.context_id();
215+ let root_tex = with_renderer_surface_state(&root, |s| {
216+ s.texture::<VulkanTexture>(context.clone())
217+ .cloned()
218+ .map(|t| (t, s.buffer_scale(), s.buffer_transform()))
219+ })
220+ .flatten();
221+ out.extend(
222+ elems
223+ .into_iter()
224+ .filter(|e| root_tex.is_none() || *e.id() != root_id)
225+ .filter_map(|e| CropRenderElement::from_element(e, 1.0, crop))
226+ .map(Draw::Cropped),
227+ );
228+ if let Some((tex, scale, transform)) = root_tex {
229+ let geo = window.geometry();
230+ let src = Rectangle::new(
231+ geo.loc.to_f64(),
232+ (f64::from(size.0), f64::from(size.1)).into(),
233+ );
234+ out.push(Draw::Effect(EffectElement::new(
235+ &tex, scale, transform, src, at, size, effect,
236+ )));
237+ }
238+}
239+
240+/// A texture drawn through the transition hook in the effect shader.
241+pub struct EffectElement {
242+ id: Id,
243+ texture: VulkanTexture,
244+ src: Rectangle<f64, Buffer>,
245+ geometry: Rectangle<i32, Physical>,
246+ transform: Transform,
247+ effect: Effect,
248+}
249+
250+impl EffectElement {
251+ fn new(
252+ texture: &VulkanTexture,
253+ scale: i32,
254+ transform: Transform,
255+ src: Rectangle<f64, Logical>,
256+ at: (i32, i32),
257+ size: (i32, i32),
258+ effect: Effect,
259+ ) -> Self {
260+ let bounds = texture.size().to_logical(scale, transform).to_f64();
261+ Self {
262+ id: Id::new(),
263+ texture: texture.clone(),
264+ src: src.to_buffer(f64::from(scale), transform, &bounds),
265+ geometry: Rectangle::new(at.into(), size.into()),
266+ transform,
267+ effect,
268+ }
269+ }
270+}
271+
272+impl Element for EffectElement {
273+ fn id(&self) -> &Id {
274+ &self.id
275+ }
276+
277+ fn current_commit(&self) -> CommitCounter {
278+ CommitCounter::default()
279+ }
280+
281+ fn src(&self) -> Rectangle<f64, Buffer> {
282+ self.src
283+ }
284+
285+ fn geometry(&self, _scale: Scale<f64>) -> Rectangle<i32, Physical> {
286+ self.geometry
287+ }
288+
289+ fn transform(&self) -> Transform {
290+ self.transform
291+ }
292+}
293+
294+impl RenderElement<VulkanRenderer> for EffectElement {
295+ fn draw(
296+ &self,
297+ frame: &mut VulkanFrame<'_, '_>,
298+ src: Rectangle<f64, Buffer>,
299+ dst: Rectangle<i32, Physical>,
300+ damage: &[Rectangle<i32, Physical>],
301+ _opaque_regions: &[Rectangle<i32, Physical>],
302+ _cache: Option<&UserDataMap>,
303+ ) -> Result<(), monolith_render_vk::Error> {
304+ frame.render_effect(
305+ &self.texture,
306+ src,
307+ dst,
308+ damage,
309+ self.transform,
310+ 1.0,
311+ self.effect,
312+ )
313+ }
314+}
Modified file: crates/monolith/src/state.rs+2 additions
@@ -210,6 +210,7 @@ impl State {
210210 self.layout.scratchpad_add(id, name);
211211 }
212212 self.windows.insert(id, window);
213+ self.anim.spawned(id, now_ns());
213214 if self.layout.visible(id) {
214215 self.layout.focus(id);
215216 }
@@ -271,6 +272,7 @@ impl State {
271272 scale,
272273 transform,
273274 geometry: window.geometry(),
275+ born: now_ns(),
274276 });
275277 }
276278 }
Modified file: crates/render-vk/build.rs+1 additions−1 deletions
@@ -4,7 +4,7 @@ use std::process::Command;
44 fn main() {
55 let out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
66 println!("cargo:rerun-if-changed=shaders");
7- for name in ["quad.vert", "solid.frag", "texture.frag"] {
7+ for name in ["quad.vert", "solid.frag", "texture.frag", "effect.frag"] {
88 let status = Command::new("glslangValidator")
99 .args(["-V", "--target-env", "vulkan1.3", "-o"])
1010 .arg(out.join(format!("{name}.spv")))
Added file: crates/render-vk/shaders/effect.frag+27 additions
@@ -0,0 +1,27 @@
1+#version 450
2+#extension GL_GOOGLE_include_directive : require
3+#include "push.glsl"
4+
5+layout(set = 0, binding = 0) uniform sampler2D tex;
6+layout(location = 0) in vec2 v_uv;
7+layout(location = 0) out vec4 out_color;
8+
9+const int MOVE = 0;
10+const int SPAWN = 1;
11+const int CLOSE = 2;
12+
13+// The transition hook. Custom shaders will replace this function.
14+vec4 effect(vec4 color, vec2 uv, float progress, int kind) {
15+ if (kind == SPAWN) {
16+ return color * progress;
17+ }
18+ if (kind == CLOSE) {
19+ return color * (1.0 - progress);
20+ }
21+ return color;
22+}
23+
24+void main() {
25+ vec4 color = texture(tex, v_uv) * pc.color.a;
26+ out_color = effect(color, v_uv, pc.effect.x, int(pc.effect.y));
27+}
Modified file: crates/render-vk/shaders/push.glsl+1 additions
@@ -5,4 +5,5 @@ layout(push_constant) uniform Push {
55 vec4 color;
66 vec4 dst;
77 vec2 uv[4];
8+ vec4 effect;
89 } pc;
Modified file: crates/render-vk/src/frame.rs+89 additions−24 deletions
@@ -90,6 +90,70 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
9090 .collect()
9191 }
9292
93+ /// Draws a texture through the transition hook in `shaders/effect.frag`.
94+ #[expect(clippy::too_many_arguments)]
95+ pub fn render_effect(
96+ &mut self,
97+ texture: &VulkanTexture,
98+ src: Rectangle<f64, Buffer>,
99+ dst: Rectangle<i32, Physical>,
100+ damage: &[Rectangle<i32, Physical>],
101+ src_transform: Transform,
102+ alpha: f32,
103+ effect: Effect,
104+ ) -> Result<(), Error> {
105+ let params = [effect.progress, effect.kind as u32 as f32, 0.0, 0.0];
106+ self.draw_texture(
107+ texture,
108+ src,
109+ dst,
110+ damage,
111+ src_transform,
112+ alpha,
113+ Kind::Effect,
114+ params,
115+ )
116+ }
117+
118+ #[expect(clippy::too_many_arguments)]
119+ fn draw_texture(
120+ &mut self,
121+ texture: &VulkanTexture,
122+ src: Rectangle<f64, Buffer>,
123+ dst: Rectangle<i32, Physical>,
124+ damage: &[Rectangle<i32, Physical>],
125+ src_transform: Transform,
126+ alpha: f32,
127+ kind: Kind,
128+ effect: [f32; 4],
129+ ) -> Result<(), Error> {
130+ if !Arc::ptr_eq(&texture.0.shared, self.renderer.shared()) {
131+ return Err(Error::ForeignTexture);
132+ }
133+ if dst.size.w <= 0 || dst.size.h <= 0 || Arc::ptr_eq(&texture.0, &self.target.0) {
134+ return Ok(());
135+ }
136+ let scissors = self.scissors(dst, damage);
137+ if scissors.is_empty() {
138+ return Ok(());
139+ }
140+ let uvs = texture_uvs(src, dst.size, texture.size(), src_transform);
141+ let (fb, uv) = to_framebuffer(dst, uvs, self.output, self.transform);
142+ let push = Push {
143+ dst: self.ndc(fb),
144+ uv,
145+ color: [1.0, 1.0, 1.0, alpha],
146+ effect,
147+ };
148+ self.ops.push(Op::Draw {
149+ kind,
150+ texture: Some(texture.clone()),
151+ push,
152+ scissors,
153+ });
154+ Ok(())
155+ }
156+
93157 fn ndc(&self, r: Rectangle<i32, Physical>) -> [f32; 4] {
94158 let fb = self.fb_size();
95159 let (w, h) = (fb.w as f32, fb.h as f32);
@@ -136,6 +200,7 @@ impl Frame for VulkanFrame<'_, '_> {
136200 dst: self.ndc(fb),
137201 uv: [[0.0; 2]; 4],
138202 color: color.components(),
203+ effect: [0.0; 4],
139204 };
140205 self.ops.push(Op::Draw {
141206 kind: Kind::Solid,
@@ -156,30 +221,16 @@ impl Frame for VulkanFrame<'_, '_> {
156221 src_transform: Transform,
157222 alpha: f32,
158223 ) -> Result<(), Error> {
159- if !Arc::ptr_eq(&texture.0.shared, self.renderer.shared()) {
160- return Err(Error::ForeignTexture);
161- }
162- if dst.size.w <= 0 || dst.size.h <= 0 || Arc::ptr_eq(&texture.0, &self.target.0) {
163- return Ok(());
164- }
165- let scissors = self.scissors(dst, damage);
166- if scissors.is_empty() {
167- return Ok(());
168- }
169- let uvs = texture_uvs(src, dst.size, texture.size(), src_transform);
170- let (fb, uv) = to_framebuffer(dst, uvs, self.output, self.transform);
171- let push = Push {
172- dst: self.ndc(fb),
173- uv,
174- color: [1.0, 1.0, 1.0, alpha],
175- };
176- self.ops.push(Op::Draw {
177- kind: Kind::Texture,
178- texture: Some(texture.clone()),
179- push,
180- scissors,
181- });
182- Ok(())
224+ self.draw_texture(
225+ texture,
226+ src,
227+ dst,
228+ damage,
229+ src_transform,
230+ alpha,
231+ Kind::Texture,
232+ [0.0; 4],
233+ )
183234 }
184235
185236 fn transformation(&self) -> Transform {
@@ -199,3 +250,17 @@ impl Frame for VulkanFrame<'_, '_> {
199250 self.renderer.submit(self.target, ops)
200251 }
201252 }
253+
254+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
255+pub enum Transition {
256+ Move = 0,
257+ Spawn = 1,
258+ Close = 2,
259+}
260+
261+/// A window transition for the effect shader, with `progress` in [0, 1].
262+#[derive(Clone, Copy, Debug, PartialEq)]
263+pub struct Effect {
264+ pub kind: Transition,
265+ pub progress: f32,
266+}
Modified file: crates/render-vk/src/lib.rs+1 additions−1 deletions
@@ -16,6 +16,6 @@ mod transform;
1616
1717 pub use dmabuf::VulkanAllocator;
1818 pub use error::Error;
19-pub use frame::VulkanFrame;
19+pub use frame::{Effect, Transition, VulkanFrame};
2020 pub use image::{VulkanImage, VulkanMapping, VulkanTarget, VulkanTexture};
2121 pub use renderer::VulkanRenderer;
Modified file: crates/render-vk/src/pipeline.rs+8 additions
@@ -13,6 +13,8 @@ pub(crate) struct Push {
1313 pub color: [f32; 4],
1414 pub dst: [f32; 4],
1515 pub uv: [[f32; 2]; 4],
16+ /// Transition progress and kind, read only by `shaders/effect.frag`.
17+ pub effect: [f32; 4],
1618 }
1719
1820 impl Push {
@@ -25,6 +27,7 @@ impl Push {
2527 pub(crate) enum Kind {
2628 Solid,
2729 Texture,
30+ Effect,
2831 }
2932
3033 pub(crate) struct Pipelines {
@@ -34,12 +37,14 @@ pub(crate) struct Pipelines {
3437 vert: vk::ShaderModule,
3538 solid: vk::ShaderModule,
3639 texture: vk::ShaderModule,
40+ effect: vk::ShaderModule,
3741 cache: HashMap<(vk::Format, Kind), vk::Pipeline>,
3842 }
3943
4044 const QUAD_VERT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/quad.vert.spv"));
4145 const SOLID_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/solid.frag.spv"));
4246 const TEXTURE_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/texture.frag.spv"));
47+const EFFECT_FRAG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/effect.frag.spv"));
4348
4449 impl Pipelines {
4550 pub fn new(shared: &Arc<Shared>) -> Result<Self, Error> {
@@ -83,6 +88,7 @@ impl Pipelines {
8388 vert: module(QUAD_VERT)?,
8489 solid: module(SOLID_FRAG)?,
8590 texture: module(TEXTURE_FRAG)?,
91+ effect: module(EFFECT_FRAG)?,
8692 cache: HashMap::new(),
8793 })
8894 }
@@ -95,6 +101,7 @@ impl Pipelines {
95101 let frag = match kind {
96102 Kind::Solid => self.solid,
97103 Kind::Texture => self.texture,
104+ Kind::Effect => self.effect,
98105 };
99106 let stages = [
100107 vk::PipelineShaderStageCreateInfo::default()
@@ -162,6 +169,7 @@ impl Drop for Pipelines {
162169 d.destroy_shader_module(self.vert, None);
163170 d.destroy_shader_module(self.solid, None);
164171 d.destroy_shader_module(self.texture, None);
172+ d.destroy_shader_module(self.effect, None);
165173 d.destroy_pipeline_layout(self.layout, None);
166174 d.destroy_descriptor_set_layout(self.set_layout, None);
167175 }