From 2f39c9682c7d4bf1e7256c4362f250124367ee16 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Mon, 8 Dec 2025 17:58:38 +0100 Subject: [PATCH] shaders: Add clipped-surface shader --- Cargo.lock | 1 + Cargo.toml | 1 + src/backend/render/clipped_surface.rs | 268 ++++++++++++++++++ src/backend/render/mod.rs | 21 +- .../render/shaders/clipped_surface.frag | 80 ++++++ 5 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 src/backend/render/clipped_surface.rs create mode 100644 src/backend/render/shaders/clipped_surface.frag diff --git a/Cargo.lock b/Cargo.lock index 1098963e..acc4bf0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -844,6 +844,7 @@ dependencies = [ "anyhow", "bitflags 2.9.4", "calloop 0.14.3", + "cgmath", "clap_lex", "cosmic-comp-config", "cosmic-config", diff --git a/Cargo.toml b/Cargo.toml index 99d0d02e..e5cca799 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ parking_lot = "0.12.5" logind-zbus = { version = "5.3.2", optional = true } futures-executor = { version = "0.3.31", features = ["thread-pool"] } futures-util = "0.3.31" +cgmath = "0.18.0" [dependencies.id_tree] branch = "feature/copy_clone" diff --git a/src/backend/render/clipped_surface.rs b/src/backend/render/clipped_surface.rs new file mode 100644 index 00000000..955972f2 --- /dev/null +++ b/src/backend/render/clipped_surface.rs @@ -0,0 +1,268 @@ +// Taken and modified from niri, licensed GPL-3. + +use std::borrow::{Borrow, BorrowMut}; + +use cgmath::{Matrix3, Vector2}; +use smithay::backend::renderer::{ + ImportAll, ImportMem, Renderer, + element::{ + Element, Id, Kind, RenderElement, UnderlyingStorage, surface::WaylandSurfaceRenderElement, + }, + gles::{GlesFrame, GlesRenderer, GlesTexProgram, Uniform, UniformValue}, + utils::{CommitCounter, DamageSet, OpaqueRegions}, +}; +use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform}; + +use crate::backend::render::element::AsGlowRenderer; + +pub static CLIPPING_SHADER: &str = include_str!("./shaders/clipped_surface.frag"); +pub struct ClippingShader(pub GlesTexProgram); + +impl ClippingShader { + pub fn get(renderer: &R) -> GlesTexProgram { + Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data() + .get::() + .expect("Custom Shaders not initialized") + .0 + .clone() + } +} + +#[derive(Debug)] +pub struct ClippedSurfaceRenderElement +where + R: Renderer + ImportAll + ImportMem, +{ + inner: WaylandSurfaceRenderElement, + program: GlesTexProgram, + radius: [u8; 4], + geometry: Rectangle, + uniforms: Vec>, +} + +impl ClippedSurfaceRenderElement +where + R: Renderer + ImportAll + ImportMem, +{ + pub fn new( + renderer: &mut R, + elem: WaylandSurfaceRenderElement, + scale: Scale, + geometry: Rectangle, + radius: [u8; 4], + ) -> Self + where + R: AsGlowRenderer, + { + let elem_geo = elem.geometry(scale); + let geo: Rectangle = geometry.to_physical_precise_round(scale); + let buf_size = elem.buffer_size(); + let view = elem.view(); + + let transform = elem.transform(); + let transform_matrix = Matrix3::::from_translation(Vector2::new(0.5, 0.5)) + * transform.matrix() + * Matrix3::::from_translation(-Vector2::new(0.5, 0.5)); + + let geo_scale = { + let Scale { x, y } = elem_geo.size.to_f64() / geo.size.to_f64(); + Matrix3::from_nonuniform_scale(x as f32, y as f32) + }; + + let geo_translation = { + let offset = (elem_geo.loc - geo.loc).to_f64(); + Matrix3::from_translation(Vector2::new( + (offset.x / elem_geo.size.w as f64) as f32, + (offset.y / elem_geo.size.h as f64) as f32, + )) + }; + + let buf_scale = { + let Scale { x, y } = buf_size.to_f64() / view.src.size.to_f64(); + Matrix3::from_nonuniform_scale(x as f32, y as f32) + }; + + let buf_translation = Matrix3::from_translation(Vector2::new( + (view.src.loc.x as f64 / buf_size.w as f64) as f32, + (view.src.loc.y as f64 / buf_size.h as f64) as f32, + )); + + let input_to_geo = + transform_matrix * geo_scale * geo_translation * buf_scale * buf_translation; + + let uniforms = vec![ + Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)), + Uniform::new( + "corner_radius", + [ + radius[0] as f32, + radius[1] as f32, + radius[2] as f32, + radius[3] as f32, + ], + ), + Uniform::new( + "input_to_geo", + UniformValue::Matrix3x3 { + matrices: vec![*AsRef::<[f32; 9]>::as_ref(&input_to_geo)], + transpose: false, + }, + ), + ]; + + Self { + inner: elem, + program: ClippingShader::get(renderer), + radius, + geometry, + uniforms, + } + } + + pub fn will_clip( + elem: &WaylandSurfaceRenderElement, + scale: Scale, + geometry: Rectangle, + radius: [u8; 4], + ) -> bool { + let elem_geo = elem.geometry(scale); + let geo = geometry.to_physical_precise_round(scale); + + let corners = Self::rounded_corners(geometry, radius); + let corners = corners + .into_iter() + .map(|rect| rect.to_physical_precise_up(scale)); + let geo = Rectangle::subtract_rects_many([geo], corners); + !Rectangle::subtract_rects_many([elem_geo], geo).is_empty() + } + + fn rounded_corners( + geo: Rectangle, + radius: [u8; 4], + ) -> [Rectangle; 4] { + let top_left = radius[0] as f64; + let top_right = radius[1] as f64; + let bottom_right = radius[2] as f64; + let bottom_left = radius[3] as f64; + + [ + Rectangle::new(geo.loc, Size::from((top_left, top_left))), + Rectangle::new( + Point::from((geo.loc.x + geo.size.w - top_right, geo.loc.y)), + Size::from((top_right, top_right)), + ), + Rectangle::new( + Point::from(( + geo.loc.x + geo.size.w - bottom_right, + geo.loc.y + geo.size.h - bottom_right, + )), + Size::from((bottom_right, bottom_right)), + ), + Rectangle::new( + Point::from((geo.loc.x, geo.loc.y + geo.size.h - bottom_left)), + Size::from((bottom_left, bottom_left)), + ), + ] + } +} + +impl Element for ClippedSurfaceRenderElement +where + R: Renderer + ImportAll + ImportMem, +{ + fn id(&self) -> &Id { + self.inner.id() + } + + fn current_commit(&self) -> CommitCounter { + self.inner.current_commit() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.inner.geometry(scale) + } + + fn src(&self) -> Rectangle { + self.inner.src() + } + + fn transform(&self) -> Transform { + self.inner.transform() + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + // FIXME: radius changes need to cause damage. + let damage = self.inner.damage_since(scale, commit); + + // Intersect with geometry, since we're clipping by it. + let mut geo = self.geometry.to_physical_precise_round(scale); + geo.loc -= self.geometry(scale).loc; + damage + .into_iter() + .filter_map(|rect| rect.intersection(geo)) + .collect() + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + let regions = self.inner.opaque_regions(scale); + + // Intersect with geometry, since we're clipping by it. + let mut geo = self.geometry.to_physical_precise_round(scale); + geo.loc -= self.geometry(scale).loc; + let regions = regions + .into_iter() + .filter_map(|rect| rect.intersection(geo)); + + // Subtract the rounded corners. + let corners = Self::rounded_corners(self.geometry, self.radius); + + let elem_loc = self.geometry(scale).loc; + let corners = corners.into_iter().map(|rect| { + let mut rect = rect.to_physical_precise_up(scale); + rect.loc -= elem_loc; + rect + }); + + OpaqueRegions::from_slice(&Rectangle::subtract_rects_many(regions, corners)) + } + + fn alpha(&self) -> f32 { + self.inner.alpha() + } + + fn kind(&self) -> Kind { + self.inner.kind() + } +} + +impl RenderElement for ClippedSurfaceRenderElement +where + R: AsGlowRenderer + Renderer + ImportAll + ImportMem, + R::TextureId: 'static, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + ) -> Result<(), R::Error> { + BorrowMut::::borrow_mut(::glow_frame_mut(frame)) + .override_default_tex_program(self.program.clone(), self.uniforms.clone()); + self.inner.draw(frame, src, dst, damage, opaque_regions)?; + BorrowMut::::borrow_mut(::glow_frame_mut(frame)) + .clear_tex_program_override(); + Ok(()) + } + + fn underlying_storage(&self, _renderer: &mut R) -> Option> { + None + } +} diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs index 9b9faa99..17b9c672 100644 --- a/src/backend/render/mod.rs +++ b/src/backend/render/mod.rs @@ -12,7 +12,13 @@ use std::{ #[cfg(feature = "debug")] use crate::debug::fps_ui; use crate::{ - backend::{kms::render::gles::GbmGlowBackend, render::element::DamageElement}, + backend::{ + kms::render::gles::GbmGlowBackend, + render::{ + clipped_surface::{CLIPPING_SHADER, ClippingShader}, + element::DamageElement, + }, + }, config::ScreenFilter, shell::{ CosmicMappedRenderElement, OverviewMode, SeatExt, Trigger, WorkspaceDelta, @@ -75,7 +81,7 @@ use smithay::{ use smithay_egui::EguiState; pub mod animations; - +pub mod clipped_surface; pub mod cursor; pub mod element; use self::element::{AsGlowRenderer, CosmicElement}; @@ -400,6 +406,14 @@ pub fn init_shaders(renderer: &mut GlesRenderer) -> Result<(), GlesError> { UniformName::new("color_mode", UniformType::_1f), ], )?; + let clipping_shader = renderer.compile_custom_texture_shader( + CLIPPING_SHADER, + &[ + UniformName::new("geo_size", UniformType::_2f), + UniformName::new("corner_radius", UniformType::_4f), + UniformName::new("input_to_geo", UniformType::Matrix3x3), + ], + )?; let egl_context = renderer.egl_context(); egl_context @@ -411,6 +425,9 @@ pub fn init_shaders(renderer: &mut GlesRenderer) -> Result<(), GlesError> { egl_context .user_data() .insert_if_missing(|| PostprocessShader(postprocess_shader)); + egl_context + .user_data() + .insert_if_missing(|| ClippingShader(clipping_shader)); Ok(()) } diff --git a/src/backend/render/shaders/clipped_surface.frag b/src/backend/render/shaders/clipped_surface.frag new file mode 100644 index 00000000..b3dd9c21 --- /dev/null +++ b/src/backend/render/shaders/clipped_surface.frag @@ -0,0 +1,80 @@ +// Taken from niri and modified, licensed GPL-3.0 + +#version 100 + +//_DEFINES_ + +#if defined(EXTERNAL) +#extension GL_OES_EGL_image_external : require +#endif + +precision highp float; +#if defined(EXTERNAL) +uniform samplerExternalOES tex; +#else +uniform sampler2D tex; +#endif + +uniform float alpha; +varying vec2 v_coords; + +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif + +uniform vec2 geo_size; +uniform vec4 corner_radius; +uniform mat3 input_to_geo; + +float rounding_alpha(vec2 coords, vec2 size) { + vec2 center; + float radius; + + if (coords.x < corner_radius.w && coords.y < corner_radius.w) { + radius = corner_radius.w; + center = vec2(radius, radius); + } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { + radius = corner_radius.y; + center = vec2(size.x - radius, radius); + } else if (size.x - corner_radius.x < coords.x && size.y - corner_radius.x < coords.y) { + radius = corner_radius.x; + center = vec2(size.x - radius, size.y - radius); + } else if (coords.x < corner_radius.z && size.y - corner_radius.z < coords.y) { + radius = corner_radius.z; + center = vec2(radius, size.y - radius); + } else { + return 1.0; + } + + float dist = distance(coords, center); + float half_px = 0.5; + return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); +} + +void main() { + vec3 coords_geo = input_to_geo * vec3(v_coords, 1.0); + + // Sample the texture. + vec4 color = texture2D(tex, v_coords); + #if defined(NO_ALPHA) + color = vec4(color.rgb, 1.0); + #endif + + if (coords_geo.x < 0.0 || 1.0 < coords_geo.x || coords_geo.y < 0.0 || 1.0 < coords_geo.y) { + // Clip outside geometry. + color = vec4(0.0); + } else { + // Apply corner rounding inside geometry. + color = color * rounding_alpha(coords_geo.xy * geo_size, geo_size); + } + + // Apply final alpha and tint. + color = color * alpha; + + #if defined(DEBUG_FLAGS) + if (tint == 1.0) + color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; + #endif + + gl_FragColor = color; +}