diff --git a/docs/wiki/Configuration:-Animations.md b/docs/wiki/Configuration:-Animations.md index 6441d33a46..fd9c60fa79 100644 --- a/docs/wiki/Configuration:-Animations.md +++ b/docs/wiki/Configuration:-Animations.md @@ -440,6 +440,52 @@ animations { } ``` +#### `screen-transition` + +Since: next release + +The cross-fade animation of the `do-screen-transition` action. + +```kdl +animations { + screen-transition { + curve "ease-out-expo" + } +} +``` + +##### `custom-shader` + +Since: next release + +You can write a custom shader for drawing the screen during a screen transition animation. + +See [this example shader](./examples/screen_transition_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. +When running niri as a systemd service, you can see the warnings in the journal: `journalctl -ef /usr/bin/niri` + +> [!WARNING] +> +> Custom shaders do not have a backwards compatibility guarantee. +> I may need to change their interface as I'm developing new features. + + + +```kdl +animations { + screen-transition { + custom-shader r" + vec4 screen_transition_color(vec3 coords_curr_geo, vec3 size_curr_geo) { + vec3 coords_tex = niri_geo_to_tex * coords_curr_geo; + vec4 color = texture2D(niri_tex_from, coords_tex.st); + return color * (1.0 - niri_clamped_progress); + } + " + } +} +``` + ### Synchronized Animations Since: 0.1.5 diff --git a/docs/wiki/examples/screen_transition_custom_shader.frag b/docs/wiki/examples/screen_transition_custom_shader.frag new file mode 100644 index 0000000000..8e8355d0d1 --- /dev/null +++ b/docs/wiki/examples/screen_transition_custom_shader.frag @@ -0,0 +1,170 @@ +// 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 screen_transition_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 output. +// +// These are homogeneous (the Z component is equal to 1) and scaled in such a +// way that the 0 to 1 coordinates cover the full output. +// +// * size_geo: size of the output in logical pixels. +// +// It is homogeneous (the Z component is equal to 1). +// +// The screen transition shader renders as an overlay above the live workspace. +// Making pixels transparent (alpha = 0) will reveal the new workspace +// underneath, while opaque pixels (alpha = 1) will show the captured old +// workspace snapshot. As the transition progresses, you should generally make +// more pixels transparent to reveal the new workspace. +// +// The function must return the color of the pixel (with premultiplied alpha). +// The pixel color will be further processed by niri (e.g. to apply final +// alpha). + +// 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 captured texture of the old workspace (before the switch). +uniform sampler2D niri_tex_from; + +// Matrix that converts geometry coordinates into the old workspace texture +// coordinates. +// +// You must always use this matrix to sample the texture, like this: +// vec3 coords_tex = niri_geo_to_tex * coords_geo; +// vec4 color = texture2D(niri_tex_from, coords_tex.st); +// This ensures correct sampling when the output has a transform (rotation). +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; + +// Mouse position in logical output coordinates (0,0 at top-left). +// Set to (-1, -1) if the cursor is not on this output. +uniform vec2 niri_mouse_pos; + +// Now let's look at some examples. You can copy everything below this line +// into your custom-shader to experiment. + +// Example: gradually fade out the old workspace, equivalent to the default +// crossfade transition. +vec4 default_crossfade(vec3 coords_geo, vec3 size_geo) { + vec3 coords_tex = niri_geo_to_tex * coords_geo; + vec4 color = texture2D(niri_tex_from, coords_tex.st); + + // Fade out the old workspace to reveal the new one underneath. + color *= (1.0 - niri_clamped_progress); + + return color; +} + +// Example: horizontal wipe from left to right revealing the new workspace. +vec4 horizontal_wipe(vec3 coords_geo, vec3 size_geo) { + vec3 coords_tex = niri_geo_to_tex * coords_geo; + vec4 color = texture2D(niri_tex_from, coords_tex.st); + + // The wipe edge moves from left (x=0) to right (x=1) with progress. + // Pixels to the left of the edge become transparent, revealing the + // new workspace underneath. + float alpha = smoothstep(niri_clamped_progress - 0.05, + niri_clamped_progress + 0.05, + coords_geo.x); + + return color * alpha; +} + +// Example: the old workspace slides upward off the screen. +vec4 slide_up(vec3 coords_geo, vec3 size_geo) { + // Shift the sampling position upward as the transition progresses. + vec3 shifted = vec3(coords_geo.x, + coords_geo.y + niri_clamped_progress, + 1.0); + + // Pixels that have shifted beyond the old workspace should be + // transparent, letting the new workspace show through. + if (shifted.y > 1.0) + return vec4(0.0); + + vec3 coords_tex = niri_geo_to_tex * shifted; + vec4 color = texture2D(niri_tex_from, coords_tex.st); + + return color; +} + +// Example: pixels randomly dissolve to reveal the new workspace. +vec4 pixel_dissolve(vec3 coords_geo, vec3 size_geo) { + vec3 coords_tex = niri_geo_to_tex * coords_geo; + vec4 color = texture2D(niri_tex_from, coords_tex.st); + + // Generate a pseudo-random value per pixel, offset by the random seed + // so each transition looks different. + vec2 seed = coords_geo.xy + vec2(niri_random_seed); + float random = fract(sin(dot(seed, vec2(12.9898, 78.233))) * 43758.5453); + + // As progress increases, more pixels cross the threshold and become + // transparent, revealing the new workspace. + float threshold = niri_clamped_progress * 1.2; + float alpha = step(threshold, random); + + return color * alpha; +} + +// Example: expanding circle from mouse position revealing the new workspace. +// Recommended setting: duration-ms 300 +vec4 circle_reveal(vec3 coords_geo, vec3 size_geo) { + vec3 coords_tex = niri_geo_to_tex * coords_geo; + vec4 color = texture2D(niri_tex_from, coords_tex.st); + + // Use the mouse position as the circle center, or the screen center + // if the cursor is off this output (sentinel value -1, -1). + vec2 center = (niri_mouse_pos.x < 0.0) + ? vec2(0.5) + : niri_mouse_pos / size_geo.xy; + + // Correct for aspect ratio so the reveal is a circle, not an ellipse. + vec2 aspect = size_geo.xy / length(size_geo.xy); + vec2 delta = (coords_geo.xy - center) * aspect; + float dist = length(delta); + + // The circle expands with progress. Scale by ~1.5 to ensure + // it covers the full screen even when starting from a corner. + float radius = niri_clamped_progress * 1.5; + + // Pixels inside the expanding circle become transparent (new workspace), + // pixels outside stay opaque (old workspace). + float alpha = smoothstep(radius - 0.05, radius, dist); + + return color * alpha; +} + +// This is the function that you must define. +vec4 screen_transition_color(vec3 coords_geo, vec3 size_geo) { + // You can pick one of the example functions or write your own. + return circle_reveal(coords_geo, size_geo); +} diff --git a/niri-config/src/animations.rs b/niri-config/src/animations.rs index 346b62517a..80f8f85b79 100644 --- a/niri-config/src/animations.rs +++ b/niri-config/src/animations.rs @@ -19,6 +19,7 @@ pub struct Animations { pub screenshot_ui_open: ScreenshotUiOpenAnim, pub overview_open_close: OverviewOpenCloseAnim, pub recent_windows_close: RecentWindowsCloseAnim, + pub screen_transition: ScreenTransitionAnim, } impl Default for Animations { @@ -37,6 +38,7 @@ impl Default for Animations { screenshot_ui_open: Default::default(), overview_open_close: Default::default(), recent_windows_close: Default::default(), + screen_transition: Default::default(), } } } @@ -71,6 +73,8 @@ pub struct AnimationsPart { pub overview_open_close: Option, #[knuffel(child)] pub recent_windows_close: Option, + #[knuffel(child)] + pub screen_transition: Option, } impl MergeWith for Animations { @@ -97,6 +101,7 @@ impl MergeWith for Animations { screenshot_ui_open, overview_open_close, recent_windows_close, + screen_transition, ); } } @@ -326,6 +331,27 @@ impl Default for RecentWindowsCloseAnim { } } +#[derive(Debug, Clone, PartialEq)] +pub struct ScreenTransitionAnim { + pub anim: Animation, + pub custom_shader: Option, +} + +impl Default for ScreenTransitionAnim { + fn default() -> Self { + Self { + anim: Animation { + off: false, + kind: Kind::Easing(EasingParams { + duration_ms: 300, + curve: Curve::EaseOutExpo, + }), + }, + custom_shader: None, + } + } +} + impl knuffel::Decode for WorkspaceSwitchAnim where S: knuffel::traits::ErrorSpan, @@ -524,6 +550,32 @@ where } } +impl knuffel::Decode for ScreenTransitionAnim +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode, + ctx: &mut knuffel::decode::Context, + ) -> Result> { + 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, + }) + } +} + impl Animation { pub fn new_off() -> Self { Self { diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index b61fe1c1a0..acc4e81c76 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1615,6 +1615,18 @@ mod tests { ), }, ), + screen_transition: ScreenTransitionAnim { + anim: Animation { + off: false, + kind: Easing( + EasingParams { + duration_ms: 300, + curve: EaseOutExpo, + }, + ), + }, + custom_shader: None, + }, }, gestures: Gestures { dnd_edge_view_scroll: DndEdgeViewScroll { diff --git a/src/animation/mod.rs b/src/animation/mod.rs index 73a79c5e43..80bb6203d7 100644 --- a/src/animation/mod.rs +++ b/src/animation/mod.rs @@ -238,6 +238,19 @@ impl Animation { self.clock.now() >= self.start_time + self.duration } + /// Returns whether the animation is done as seen from a custom time `at`. + /// + /// Useful for delayed animations that derive their effective clock time from an offset + /// (e.g. `clock.now().saturating_sub(delay)`). Handles `should_complete_instantly` + /// the same way as `is_done`. + pub fn is_done_at(&self, at: Duration) -> bool { + if self.clock.should_complete_instantly() { + return true; + } + + at >= self.start_time + self.duration + } + pub fn is_clamped_done(&self) -> bool { if self.clock.should_complete_instantly() { return true; diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 259dde3091..8fd5d487d0 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -826,6 +826,9 @@ impl Tty { if let Some(src) = config.animations.window_open.custom_shader.as_deref() { shaders::set_custom_open_program(gles_renderer, Some(src)); } + if let Some(src) = config.animations.screen_transition.custom_shader.as_deref() { + shaders::set_custom_screen_transition_program(gles_renderer, Some(src)); + } drop(config); niri.update_shaders(); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index beace1dce5..8064d25470 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -160,6 +160,9 @@ impl Winit { if let Some(src) = config.animations.window_open.custom_shader.as_deref() { shaders::set_custom_open_program(renderer, Some(src)); } + if let Some(src) = config.animations.screen_transition.custom_shader.as_deref() { + shaders::set_custom_screen_transition_program(renderer, Some(src)); + } drop(config); niri.update_shaders(); diff --git a/src/niri.rs b/src/niri.rs index 414f702d80..b17e3a728e 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -164,7 +164,7 @@ use crate::ui::config_error_notification::ConfigErrorNotification; use crate::ui::exit_confirm_dialog::{ExitConfirmDialog, ExitConfirmDialogRenderElement}; use crate::ui::hotkey_overlay::HotkeyOverlay; use crate::ui::mru::{MruCloseRequest, WindowMruUi, WindowMruUiRenderElement}; -use crate::ui::screen_transition::{self, ScreenTransition}; +use crate::ui::screen_transition::{self, ScreenTransition, ScreenTransitionRenderElement}; use crate::ui::screenshot_ui::{OutputScreenshot, ScreenshotUi, ScreenshotUiRenderElement}; use crate::utils::scale::{closest_representable_scale, guess_monitor_scale}; use crate::utils::spawning::{CHILD_DISPLAY, CHILD_ENV}; @@ -1563,6 +1563,16 @@ impl State { shaders_changed = true; } + if config.animations.screen_transition.custom_shader + != old_config.animations.screen_transition.custom_shader + { + let src = config.animations.screen_transition.custom_shader.as_deref(); + self.backend.with_primary_renderer(|renderer| { + shaders::set_custom_screen_transition_program(renderer, src); + }); + shaders_changed = true; + } + if config.cursor.hide_after_inactive_ms != old_config.cursor.hide_after_inactive_ms { cursor_inactivity_timeout_changed = true; } @@ -4078,7 +4088,25 @@ impl Niri { { let state = self.output_state.get(output).unwrap(); if let Some(transition) = &state.screen_transition { - push(transition.render(target).into()); + let mouse_pos = self.global_space.output_geometry(output).and_then(|geo| { + let pos = self + .tablet_cursor_location + .unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location()); + let local = + Point::from((pos.x - f64::from(geo.loc.x), pos.y - f64::from(geo.loc.y))); + + if local.x < 0. + || local.y < 0. + || f64::from(geo.size.w) <= local.x + || f64::from(geo.size.h) <= local.y + { + None + } else { + Some(local) + } + }); + + push(transition.render(renderer, target, mouse_pos).into()); } } @@ -5880,6 +5908,8 @@ impl Niri { pub fn do_screen_transition(&mut self, renderer: &mut GlesRenderer, delay_ms: Option) { let _span = tracy_client::span!("Niri::do_screen_transition"); + let anim_config = self.config.borrow().animations.screen_transition.anim; + self.update_render_elements(None); let textures: Vec<_> = self @@ -5910,13 +5940,21 @@ impl Niri { ); if let Err(err) = &res { - warn!("error rendering output {}: {err:?}", output.name()); + warn!( + "error rendering {:?} for output {}: {err:?}", + target, + output.name() + ); } res }); if textures.iter().any(|res| res.is_err()) { + warn!( + "skipping screen transition for output {} due to render errors", + output.name() + ); return None; } @@ -5944,6 +5982,7 @@ impl Niri { state.screen_transition = Some(ScreenTransition::new( from_texture, delay, + anim_config, self.clock.clone(), )); } @@ -6164,6 +6203,7 @@ niri_render_elements! { ScreenshotUi = ScreenshotUiRenderElement, WindowMruUi = WindowMruUiRenderElement, ExitConfirmDialog = ExitConfirmDialogRenderElement, + ScreenTransition = ScreenTransitionRenderElement, Texture = PrimaryGpuTextureRenderElement, // Used for the CPU-rendered panels. RelocatedMemoryBuffer = RelocateRenderElement>, diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 6fccce7f59..9bfc999aba 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -18,6 +18,7 @@ pub struct Shaders { pub custom_resize: RefCell>, pub custom_close: RefCell>, pub custom_open: RefCell>, + pub custom_screen_transition: RefCell>, } #[derive(Debug, Clone, Copy)] @@ -27,6 +28,7 @@ pub enum ProgramType { Resize, Close, Open, + ScreenTransition, } impl Shaders { @@ -116,6 +118,7 @@ impl Shaders { custom_resize: RefCell::new(None), custom_close: RefCell::new(None), custom_open: RefCell::new(None), + custom_screen_transition: RefCell::new(None), } } @@ -153,6 +156,13 @@ impl Shaders { self.custom_open.replace(program) } + pub fn replace_custom_screen_transition_program( + &self, + program: Option, + ) -> Option { + self.custom_screen_transition.replace(program) + } + pub fn program(&self, program: ProgramType) -> Option { match program { ProgramType::Border => self.border.clone(), @@ -164,6 +174,7 @@ impl Shaders { .or_else(|| self.resize.clone()), ProgramType::Close => self.custom_close.borrow().clone(), ProgramType::Open => self.custom_open.borrow().clone(), + ProgramType::ScreenTransition => self.custom_screen_transition.borrow().clone(), } } } @@ -309,6 +320,50 @@ pub fn set_custom_open_program(renderer: &mut GlesRenderer, src: Option<&str>) { } } +fn compile_screen_transition_program( + renderer: &mut GlesRenderer, + src: &str, +) -> Result { + let mut program = include_str!("screen_transition_prelude.frag").to_string(); + program.push_str(src); + program.push_str(include_str!("screen_transition_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_mouse_pos", UniformType::_2f), + UniformName::new("niri_random_seed", UniformType::_1f), + ], + &["niri_tex_from"], + ) +} + +pub fn set_custom_screen_transition_program(renderer: &mut GlesRenderer, src: Option<&str>) { + let program = if let Some(src) = src { + match compile_screen_transition_program(renderer, src) { + Ok(program) => Some(program), + Err(err) => { + warn!("error compiling custom transition shader: {err:?}"); + return; + } + } + } else { + None + }; + + if let Some(prev) = Shaders::get(renderer).replace_custom_screen_transition_program(program) { + if let Err(err) = prev.destroy(renderer) { + warn!("error destroying previous custom transition shader: {err:?}"); + } + } +} + pub fn mat3_uniform(name: &str, mat: Mat3) -> Uniform<'_> { Uniform::new( name, diff --git a/src/render_helpers/shaders/screen_transition_epilogue.frag b/src/render_helpers/shaders/screen_transition_epilogue.frag new file mode 100644 index 0000000000..77fa695251 --- /dev/null +++ b/src/render_helpers/shaders/screen_transition_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 = screen_transition_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/screen_transition_prelude.frag b/src/render_helpers/shaders/screen_transition_prelude.frag new file mode 100644 index 0000000000..d417632015 --- /dev/null +++ b/src/render_helpers/shaders/screen_transition_prelude.frag @@ -0,0 +1,25 @@ +precision highp 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_from; +uniform mat3 niri_geo_to_tex; + +uniform float niri_progress; +uniform float niri_clamped_progress; +uniform float niri_alpha; +uniform float niri_scale; + +// Mouse position in logical output coordinates (0,0 at top-left). +uniform vec2 niri_mouse_pos; + +// Random seed generated once per transition. +uniform float niri_random_seed; diff --git a/src/ui/screen_transition.rs b/src/ui/screen_transition.rs index 7d26f85f03..de2f1db8b6 100644 --- a/src/ui/screen_transition.rs +++ b/src/ui/screen_transition.rs @@ -1,42 +1,62 @@ +use std::collections::HashMap; +use std::rc::Rc; use std::time::Duration; +use glam::{Mat3, Vec2, Vec3}; use smithay::backend::renderer::element::Kind; -use smithay::backend::renderer::gles::GlesTexture; -use smithay::utils::{Scale, Transform}; +use smithay::backend::renderer::gles::{GlesTexture, Uniform}; +use smithay::utils::{Logical, Point, Scale, Size, Transform}; -use crate::animation::Clock; +use crate::animation::{Animation, Clock}; +use crate::niri_render_elements; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; +use crate::render_helpers::renderer::NiriRenderer; +use crate::render_helpers::shader_element::ShaderRenderElement; +use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders}; use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement}; use crate::render_helpers::RenderTarget; pub const DELAY: Duration = Duration::from_millis(250); -pub const DURATION: Duration = Duration::from_millis(500); #[derive(Debug)] pub struct ScreenTransition { /// Texture to crossfade from for each render target. from_texture: [TextureBuffer; 3], - /// Monotonic time when to start the crossfade. - start_at: Duration, + delay: Duration, + anim: Animation, + /// Random seed for the shader. + random_seed: f32, /// Clock to drive animations. clock: Clock, } +niri_render_elements! { + ScreenTransitionRenderElement => { + Texture = PrimaryGpuTextureRenderElement, + Shader = ShaderRenderElement, + } +} + impl ScreenTransition { pub fn new( from_texture: [TextureBuffer; 3], delay: Duration, + config: niri_config::Animation, clock: Clock, ) -> Self { + let anim = Animation::new(clock.clone(), 1., 0., 0., config); Self { from_texture, - start_at: clock.now_unadjusted() + delay, + delay, + anim, + random_seed: fastrand::f32(), clock, } } pub fn is_done(&self) -> bool { - self.start_at + DURATION <= self.clock.now_unadjusted() + self.anim + .is_done_at(self.clock.now().saturating_sub(self.delay)) } pub fn update_render_elements(&mut self, scale: Scale, transform: Transform) { @@ -47,17 +67,22 @@ impl ScreenTransition { } } - pub fn render(&self, target: RenderTarget) -> PrimaryGpuTextureRenderElement { - // Screen transition ignores animation slowdown. - let now = self.clock.now_unadjusted(); + pub fn render( + &self, + renderer: &mut impl NiriRenderer, + target: RenderTarget, + mouse_pos: Option>, + ) -> ScreenTransitionRenderElement { + // Animation start_time is set from clock.now(), so we must use clock.now() here + // too (not now_unadjusted) to keep the time domain consistent. This means screen + // transitions now respect animation slowdown, unlike the original hardcoded crossfade. + let now = self.clock.now().saturating_sub(self.delay); - let alpha = if self.start_at + DURATION <= now { - 0. - } else if self.start_at <= now { - 1. - (now - self.start_at).as_secs_f32() / DURATION.as_secs_f32() - } else { - 1. - }; + let alpha = self.anim.value_at(now); + let clamped_alpha = alpha.clamp(0., 1.); + + let progress = 1. - alpha; + let clamped_progress = progress.clamp(0., 1.); let idx = match target { RenderTarget::Output => 0, @@ -65,13 +90,75 @@ impl ScreenTransition { RenderTarget::ScreenCapture => 2, }; + let texture_scale = self.from_texture[idx].texture_scale(); + + if Shaders::get(renderer) + .program(ProgramType::ScreenTransition) + .is_some() + { + let mouse_pos = mouse_pos + .map(|pos| [pos.x as f32, pos.y as f32]) + .unwrap_or([-1., -1.]); + + // For a full-screen transition the element IS the geometry, so input_to_geo + // is identity. Shader authors get coords in [0,1] matching the close/open + // convention, with geo_size providing the actual output dimensions. + let input_to_geo = Mat3::IDENTITY; + let logical_size = self.from_texture[idx].logical_size(); + let geo_size = [logical_size.w as f32, logical_size.h as f32]; + + // We need to transform the geometry coordinates to texture coordinates in the + // shader, so we calculate a matrix for that here. + let transform = self.from_texture[idx].texture_transform(); + let size: Size = Size::from((1., 1.)); + + let p00 = transform.transform_point_in(Point::from((0., 0.)), &size); + let p10 = transform.transform_point_in(Point::from((1., 0.)), &size); + let p01 = transform.transform_point_in(Point::from((0., 1.)), &size); + + let origin = Vec2::new(p00.x as f32, p00.y as f32); + let basis_x = Vec2::new((p10.x - p00.x) as f32, (p10.y - p00.y) as f32); + let basis_y = Vec2::new((p01.x - p00.x) as f32, (p01.y - p00.y) as f32); + + let geo_to_tex = Mat3::from_cols( + basis_x.extend(0.), + basis_y.extend(0.), + Vec3::new(origin.x, origin.y, 1.), + ); + + return ShaderRenderElement::new( + ProgramType::ScreenTransition, + self.from_texture[idx].logical_size(), + None, + texture_scale.x as f32, + 1., + Rc::new([ + mat3_uniform("niri_input_to_geo", input_to_geo), + Uniform::new("niri_geo_size", geo_size), + 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_mouse_pos", mouse_pos), + Uniform::new("niri_random_seed", self.random_seed), + ]), + HashMap::from([( + String::from("niri_tex_from"), + self.from_texture[idx].texture().clone(), + )]), + Kind::Unspecified, + ) + .with_location(Point::from((0., 0.))) + .into(); + } + PrimaryGpuTextureRenderElement(TextureRenderElement::from_texture_buffer( self.from_texture[idx].clone(), (0., 0.), - alpha, + clamped_alpha as f32, None, None, Kind::Unspecified, )) + .into() } }