diff --git a/Cargo.lock b/Cargo.lock index 4e0d301..4249bfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,7 +186,7 @@ dependencies = [ "async-lock 3.3.0", "async-task", "concurrent-queue", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-lite 2.3.0", "slab", ] @@ -434,7 +434,7 @@ dependencies = [ "async-channel", "async-lock 3.3.0", "async-task", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-io", "futures-lite 2.3.0", "piper", @@ -1081,9 +1081,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" @@ -1225,7 +1225,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -2203,6 +2203,7 @@ dependencies = [ "clap", "directories", "drm-ffi 0.7.1", + "fastrand 2.1.0", "futures-util", "git-version", "glam", @@ -2583,7 +2584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-io", ] @@ -3337,7 +3338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.2", + "fastrand 2.1.0", "rustix 0.38.32", "windows-sys 0.52.0", ] diff --git a/Cargo.toml b/Cargo.toml index dd03a2a..e2cf2f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ calloop = { version = "0.13.0", features = ["executor", "futures-io"] } clap = { workspace = true, features = ["string"] } directories = "5.0.1" drm-ffi = "0.7.1" +fastrand = "2.1.0" futures-util = { version = "0.3.30", default-features = false, features = ["std", "io"] } git-version = "0.3.9" glam = "0.27.0" diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 14119dc..0388268 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -552,18 +552,24 @@ impl Default for WindowOpenAnim { } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct WindowCloseAnim(pub Animation); +#[derive(Debug, Clone, PartialEq)] +pub struct WindowCloseAnim { + pub anim: Animation, + pub custom_shader: Option, +} impl Default for WindowCloseAnim { fn default() -> Self { - Self(Animation { - off: false, - kind: AnimationKind::Easing(EasingParams { - duration_ms: 150, - curve: AnimationCurve::EaseOutQuad, - }), - }) + Self { + anim: Animation { + off: false, + kind: AnimationKind::Easing(EasingParams { + duration_ms: 150, + curve: AnimationCurve::EaseOutQuad, + }), + }, + custom_shader: None, + } } } @@ -1420,10 +1426,21 @@ where node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { - let default = Self::default().0; - Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { - Ok(false) - })?)) + let default = Self::default().anim; + let mut custom_shader = None; + let anim = Animation::decode_node(node, ctx, default, |child, ctx| { + if &**child.node_name == "custom-shader" { + custom_shader = parse_arg_node("custom-shader", child, ctx)?; + Ok(true) + } else { + Ok(false) + } + })?; + + Ok(Self { + anim, + custom_shader, + }) } } diff --git a/src/backend/tty.rs b/src/backend/tty.rs index ddf6c54..d35106e 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -500,6 +500,9 @@ impl Tty { if let Some(src) = config.animations.window_resize.custom_shader.as_deref() { shaders::set_custom_resize_program(gles_renderer, Some(src)); } + if let Some(src) = config.animations.window_close.custom_shader.as_deref() { + shaders::set_custom_close_program(gles_renderer, Some(src)); + } drop(config); niri.layout.update_shaders(); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index ab71ea2..047ebb0 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -140,6 +140,9 @@ impl Winit { if let Some(src) = config.animations.window_resize.custom_shader.as_deref() { shaders::set_custom_resize_program(renderer, Some(src)); } + if let Some(src) = config.animations.window_close.custom_shader.as_deref() { + shaders::set_custom_close_program(renderer, Some(src)); + } drop(config); niri.layout.update_shaders(); diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs index c1f4e59..bdeb73a 100644 --- a/src/layout/closing_window.rs +++ b/src/layout/closing_window.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; use std::time::Duration; use anyhow::Context as _; +use glam::{Mat3, Vec2}; use niri_config::BlockOutFrom; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::texture::TextureRenderElement; @@ -8,13 +10,15 @@ use smithay::backend::renderer::element::utils::{ Relocate, RelocateRenderElement, RescaleRenderElement, }; use smithay::backend::renderer::element::{Id, Kind, RenderElement}; -use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; -use smithay::backend::renderer::Renderer as _; +use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform}; +use smithay::backend::renderer::{Renderer as _, Texture}; use smithay::utils::{Logical, Point, Rectangle, Scale, Size, Transform}; use crate::animation::Animation; use crate::niri_render_elements; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; +use crate::render_helpers::shader_element::ShaderRenderElement; +use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders}; use crate::render_helpers::snapshot::RenderSnapshot; use crate::render_helpers::{render_to_encompassing_texture, RenderTarget}; @@ -49,11 +53,15 @@ pub struct ClosingWindow { /// The closing animation. anim: Animation, + + /// Random seed for the shader. + random_seed: f32, } niri_render_elements! { ClosingWindowRenderElement => { Texture = RelocateRenderElement>, + Shader = ShaderRenderElement, } } @@ -100,6 +108,7 @@ impl ClosingWindow { texture_offset, blocked_out_texture_offset, anim, + random_seed: fastrand::f32(), }) } @@ -108,16 +117,18 @@ impl ClosingWindow { } pub fn are_animations_ongoing(&self) -> bool { - !self.anim.is_clamped_done() + !self.anim.is_done() } pub fn render( &self, + renderer: &mut GlesRenderer, view_rect: Rectangle, scale: Scale, target: RenderTarget, ) -> ClosingWindowRenderElement { - let val = self.anim.clamped_value(); + let progress = self.anim.value(); + let clamped_progress = self.anim.clamped_value().clamp(0., 1.); let (texture, offset) = if target.should_block_out(self.block_out_from) { (&self.blocked_out_texture, self.blocked_out_texture_offset) @@ -125,6 +136,43 @@ impl ClosingWindow { (&self.texture, self.texture_offset) }; + if Shaders::get(renderer).program(ProgramType::Close).is_some() { + let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32); + let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32); + + let geo_loc = Vec2::new(self.pos.x as f32, self.pos.y as f32); + let geo_size = Vec2::new(self.geo_size.w as f32, self.geo_size.h as f32); + + let input_to_geo = Mat3::from_scale(area_size / geo_size) + * Mat3::from_translation((area_loc - geo_loc) / area_size); + + let tex_scale = Vec2::new(self.texture_scale.x as f32, self.texture_scale.y as f32); + let tex_loc = Vec2::new(offset.x as f32, offset.y as f32); + let tex_size = Vec2::new(texture.width() as f32, texture.height() as f32) / tex_scale; + + let geo_to_tex = + Mat3::from_translation(-tex_loc / tex_size) * Mat3::from_scale(geo_size / tex_size); + + return ShaderRenderElement::new( + ProgramType::Close, + view_rect.size, + None, + 1., + vec![ + mat3_uniform("niri_input_to_geo", input_to_geo), + Uniform::new("niri_geo_size", geo_size.to_array()), + mat3_uniform("niri_geo_to_tex", geo_to_tex), + Uniform::new("niri_progress", progress as f32), + Uniform::new("niri_clamped_progress", clamped_progress as f32), + Uniform::new("niri_random_seed", self.random_seed), + ], + HashMap::from([(String::from("niri_tex"), texture.clone())]), + Kind::Unspecified, + ) + .with_location(Point::from((0, 0))) + .into(); + } + let elem = TextureRenderElement::from_static_texture( Id::new(), self.texture_renderer_id, @@ -132,7 +180,7 @@ impl ClosingWindow { texture.clone(), self.texture_scale.x as i32, Transform::Normal, - Some(val.clamp(0., 1.) as f32), + Some(1. - clamped_progress as f32), None, None, None, @@ -145,7 +193,7 @@ impl ClosingWindow { let elem = RescaleRenderElement::from_element( elem, (center - offset).to_physical_precise_round(scale), - (val / 5. + 0.8).max(0.), + ((1. - clamped_progress) / 5. + 0.8).max(0.), ); let mut location = self.pos.to_f64() + offset; diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index a2515a7..08f170f 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -1344,7 +1344,7 @@ impl Workspace { tile_pos.x -= offset; } - let anim = Animation::new(1., 0., 0., self.options.animations.window_close.0); + let anim = Animation::new(0., 1., 0., self.options.animations.window_close.anim); let res = ClosingWindow::new( renderer, @@ -2095,7 +2095,7 @@ impl Workspace { // Draw the closing windows on top. let view_rect = Rectangle::from_loc_and_size((self.view_pos(), 0), self.view_size); for closing in &self.closing_windows { - let elem = closing.render(view_rect, output_scale, target); + let elem = closing.render(renderer.as_gles_renderer(), view_rect, output_scale, target); rv.push(elem.into()); } diff --git a/src/niri.rs b/src/niri.rs index 6605cc7..7fc9436 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -941,6 +941,16 @@ impl State { shaders_changed = true; } + if config.animations.window_close.custom_shader + != old_config.animations.window_close.custom_shader + { + let src = config.animations.window_close.custom_shader.as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_close_program(renderer, src); + }); + shaders_changed = true; + } + if config.debug != old_config.debug { debug_config_changed = true; } diff --git a/src/render_helpers/shaders/close_epilogue.frag b/src/render_helpers/shaders/close_epilogue.frag new file mode 100644 index 0000000..ed05c11 --- /dev/null +++ b/src/render_helpers/shaders/close_epilogue.frag @@ -0,0 +1,16 @@ + +void main() { + vec3 coords_geo = niri_input_to_geo * vec3(niri_v_coords, 1.0); + vec3 size_geo = vec3(niri_geo_size, 1.0); + + vec4 color = close_color(coords_geo, size_geo); + + color = color * niri_alpha; + +#if defined(DEBUG_FLAGS) + if (niri_tint == 1.0) + color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; +#endif + + gl_FragColor = color; +} diff --git a/src/render_helpers/shaders/close_prelude.frag b/src/render_helpers/shaders/close_prelude.frag new file mode 100644 index 0000000..db42190 --- /dev/null +++ b/src/render_helpers/shaders/close_prelude.frag @@ -0,0 +1,21 @@ +precision mediump float; + +#if defined(DEBUG_FLAGS) +uniform float niri_tint; +#endif + +varying vec2 niri_v_coords; +uniform vec2 niri_size; + +uniform mat3 niri_input_to_geo; +uniform vec2 niri_geo_size; + +uniform sampler2D niri_tex; +uniform mat3 niri_geo_to_tex; + +uniform float niri_progress; +uniform float niri_clamped_progress; +uniform float niri_random_seed; + +uniform float niri_alpha; + diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 034d8db..21287a5 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -14,12 +14,14 @@ pub struct Shaders { pub clipped_surface: Option, pub resize: Option, pub custom_resize: RefCell>, + pub custom_close: RefCell>, } #[derive(Debug, Clone, Copy)] pub enum ProgramType { Border, Resize, + Close, } impl Shaders { @@ -72,6 +74,7 @@ impl Shaders { clipped_surface, resize, custom_resize: RefCell::new(None), + custom_close: RefCell::new(None), } } @@ -95,6 +98,13 @@ impl Shaders { self.custom_resize.replace(program) } + pub fn replace_custom_close_program( + &self, + program: Option, + ) -> Option { + self.custom_close.replace(program) + } + pub fn program(&self, program: ProgramType) -> Option { match program { ProgramType::Border => self.border.clone(), @@ -103,6 +113,7 @@ impl Shaders { .borrow() .clone() .or_else(|| self.resize.clone()), + ProgramType::Close => self.custom_close.borrow().clone(), } } } @@ -162,6 +173,49 @@ pub fn set_custom_resize_program(renderer: &mut GlesRenderer, src: Option<&str>) } } +fn compile_close_program( + renderer: &mut GlesRenderer, + src: &str, +) -> Result { + let mut program = include_str!("close_prelude.frag").to_string(); + program.push_str(src); + program.push_str(include_str!("close_epilogue.frag")); + + ShaderProgram::compile( + renderer, + &program, + &[ + UniformName::new("niri_input_to_geo", UniformType::Matrix3x3), + UniformName::new("niri_geo_size", UniformType::_2f), + UniformName::new("niri_geo_to_tex", UniformType::Matrix3x3), + UniformName::new("niri_progress", UniformType::_1f), + UniformName::new("niri_clamped_progress", UniformType::_1f), + UniformName::new("niri_random_seed", UniformType::_1f), + ], + &["niri_tex"], + ) +} + +pub fn set_custom_close_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_close_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom close shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_close_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom close shader: {err:?}"); + } + } +} + pub fn mat3_uniform(name: &str, mat: Mat3) -> Uniform { Uniform::new( name, diff --git a/wiki/Configuration:-Animations.md b/wiki/Configuration:-Animations.md index f228a5a..9bc184e 100644 --- a/wiki/Configuration:-Animations.md +++ b/wiki/Configuration:-Animations.md @@ -169,6 +169,47 @@ animations { } ``` +##### `custom-shader` + +Since: 0.1.6, experimental + +You can write a custom shader for drawing the window during a close animation. + +See [this example shader](./examples/close-custom-shader.frag) for a full documentation with several animations to experiment with. + +If a custom shader fails to compile, niri will print a warning and fall back to the default, or previous successfully compiled shader. + +> [!WARNING] +> +> Custom shaders do not have a backwards compatibility guarantee. +> I may need to change their interface as I'm developing new features. + +Example: close will fill the current geometry with a solid gradient that gradually fades away. + +``` +animations { + window-resize { + spring damping-ratio=1.0 stiffness=800 epsilon=0.0001 + + custom-shader r" + vec4 close_color(vec3 coords_geo, vec3 size_geo) { + vec4 color = vec4(0.0); + + if (0.0 <= coords_geo.x && coords_geo.x <= 1.0 + && 0.0 <= coords_geo.y && coords_geo.y <= 1.0) + { + vec4 from = vec4(1.0, 0.0, 0.0, 1.0); + vec4 to = vec4(0.0, 1.0, 0.0, 1.0); + color = mix(from, to, coords_geo.y); + } + + return color * (1.0 - niri_clamped_progress); + } + " + } +} +``` + #### `horizontal-view-movement` All horizontal camera view movement animations, such as: diff --git a/wiki/examples/close_custom_shader.frag b/wiki/examples/close_custom_shader.frag new file mode 100644 index 0000000..8a777b9 --- /dev/null +++ b/wiki/examples/close_custom_shader.frag @@ -0,0 +1,113 @@ +// Your shader must contain one function (see the bottom of this file). +// +// It should not contain any uniform definitions or anything else, as niri +// provides them for you. +// +// All symbols defined by niri will have a niri_ prefix, so don't use it for +// your own variables and functions. + +// The function that you must define looks like this: +vec4 close_color(vec3 coords_geo, vec3 size_geo) { + vec4 color = /* ...compute the color... */; + return color; +} + +// It takes as input: +// +// * coords_geo: coordinates of the current pixel relative to the window +// geometry. +// +// These are homogeneous (the Z component is equal to 1) and scaled in such a +// way that the 0 to 1 coordinates lie within the window geometry. Pixels +// outside the window geometry will have coordinates below 0 or above 1. +// +// The window geometry is its "visible bounds" from the user's perspective. +// +// The shader runs over the full screen area, so you must expect and handle +// coordinates outside the [0, 1] range. If the window is scrolled off-screen, +// all of the coordinates to the shader can fall outside the [0, 1] range. +// +// * size_geo: size of the window geometry in logical pixels. +// +// It is homogeneous (the Z component is equal to 1). +// +// The function must return the color of the pixel (with premultiplied alpha). +// The pixel color will be further processed by niri (for example, to apply the +// final opacity from window rules). + +// Now let's go over the uniforms that niri defines. +// +// You should only rely on the uniforms documented here. Any other uniforms can +// change or be removed without notice. + +// The window texture. +uniform sampler2D niri_tex; + +// Matrix that converts geometry coordinates into the window texture +// coordinates. +// +// The window texture can and will go outside the geometry (for client-side +// decoration shadows for example), which is why this matrix is necessary. +uniform mat3 niri_geo_to_tex; + + +// Unclamped progress of the animation. +// +// Goes from 0 to 1 but may overshoot and oscillate. +uniform float niri_progress; + +// Clamped progress of the animation. +// +// Goes from 0 to 1, but will stop at 1 as soon as it first reaches 1. Will not +// overshoot or oscillate. +uniform float niri_clamped_progress; + +// Random float in [0; 1), consistent for the duration of the animation. +uniform float niri_random_seed; + +// Now let's look at some examples. You can copy everything below this line +// into your custom-shader to experiment. + +// Example: fill the current geometry with a solid vertical gradient and +// gradually make transparent. +vec4 solid_gradient(vec3 coords_geo, vec3 size_geo) { + vec4 color = vec4(0.0); + + // Paint only the area inside the current geometry. + if (0.0 <= coords_geo.x && coords_geo.x <= 1.0 + && 0.0 <= coords_geo.y && coords_geo.y <= 1.0) + { + vec4 from = vec4(1.0, 0.0, 0.0, 1.0); + vec4 to = vec4(0.0, 1.0, 0.0, 1.0); + color = mix(from, to, coords_geo.y); + } + + // Make it transparent. + color *= (1.0 - niri_clamped_progress); + + return color; +} + +// Example: gradually scale down and make transparent, equivalent to the +// default closing animation. +vec4 default_close(vec3 coords_geo, vec3 size_geo) { + // Scale down the window. + float scale = max(0.0, ((1.0 - niri_clamped_progress) / 5.0 + 0.8)); + coords_geo = vec3((coords_geo.xy - vec2(0.5)) / scale + vec2(0.5), 1.0); + + // Get color from the window texture. + vec3 coords_tex = niri_geo_to_tex * coords_geo; + vec4 color = texture2D(niri_tex, coords_tex.st); + + // Make the window transparent. + color *= (1.0 - niri_clamped_progress); + + return color; +} + +// This is the function that you must define. +vec4 close_color(vec3 coords_geo, vec3 size_geo) { + // You can pick one of the example functions or write your own. + return solid_gradient(coords_geo, size_geo); +} + diff --git a/wiki/examples/resize_custom_shader.frag b/wiki/examples/resize_custom_shader.frag index 71710a9..a95e678 100644 --- a/wiki/examples/resize_custom_shader.frag +++ b/wiki/examples/resize_custom_shader.frag @@ -37,6 +37,9 @@ vec4 resize_color(vec3 coords_curr_geo, vec3 size_curr_geo) { // final opacity from window rules). // Now let's go over the uniforms that niri defines. +// +// You should only rely on the uniforms documented here. Any other uniforms can +// change or be removed without notice. // Previous (before resize) window texture. uniform sampler2D niri_tex_prev; @@ -65,12 +68,12 @@ uniform mat3 niri_curr_geo_to_prev_geo; uniform mat3 niri_curr_geo_to_next_geo; -// Unclamped progress of the resize. +// Unclamped progress of the animation. // // Goes from 0 to 1 but may overshoot and oscillate. uniform float niri_progress; -// Clamped progress of the resize. +// Clamped progress of the animation. // // Goes from 0 to 1, but will stop at 1 as soon as it first reaches 1. Will not // overshoot or oscillate.