diff options
Diffstat (limited to 'src/render_helpers')
| -rw-r--r-- | src/render_helpers/mod.rs | 1 | ||||
| -rw-r--r-- | src/render_helpers/shaders/mod.rs | 24 | ||||
| -rw-r--r-- | src/render_helpers/shaders/shadow.frag | 142 | ||||
| -rw-r--r-- | src/render_helpers/shadow.rs | 257 |
4 files changed, 424 insertions, 0 deletions
diff --git a/src/render_helpers/mod.rs b/src/render_helpers/mod.rs index 98ccf19e..45e6e527 100644 --- a/src/render_helpers/mod.rs +++ b/src/render_helpers/mod.rs @@ -31,6 +31,7 @@ pub mod resize; pub mod resources; pub mod shader_element; pub mod shaders; +pub mod shadow; pub mod snapshot; pub mod solid_color; pub mod surface; diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 91ba32d1..ddc58cdb 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -11,6 +11,7 @@ use super::shader_element::ShaderProgram; pub struct Shaders { pub border: Option<ShaderProgram>, + pub shadow: Option<ShaderProgram>, pub clipped_surface: Option<GlesTexProgram>, pub resize: Option<ShaderProgram>, pub custom_resize: RefCell<Option<ShaderProgram>>, @@ -21,6 +22,7 @@ pub struct Shaders { #[derive(Debug, Clone, Copy)] pub enum ProgramType { Border, + Shadow, Resize, Close, Open, @@ -53,6 +55,26 @@ impl Shaders { }) .ok(); + let shadow = ShaderProgram::compile( + renderer, + include_str!("shadow.frag"), + &[ + UniformName::new("shadow_color", UniformType::_4f), + UniformName::new("sigma", UniformType::_1f), + UniformName::new("input_to_geo", UniformType::Matrix3x3), + UniformName::new("geo_size", UniformType::_2f), + UniformName::new("corner_radius", UniformType::_4f), + UniformName::new("window_input_to_geo", UniformType::Matrix3x3), + UniformName::new("window_geo_size", UniformType::_2f), + UniformName::new("window_corner_radius", UniformType::_4f), + ], + &[], + ) + .map_err(|err| { + warn!("error compiling shadow shader: {err:?}"); + }) + .ok(); + let clipped_surface = renderer .compile_custom_texture_shader( include_str!("clipped_surface.frag"), @@ -76,6 +98,7 @@ impl Shaders { Self { border, + shadow, clipped_surface, resize, custom_resize: RefCell::new(None), @@ -121,6 +144,7 @@ impl Shaders { pub fn program(&self, program: ProgramType) -> Option<ShaderProgram> { match program { ProgramType::Border => self.border.clone(), + ProgramType::Shadow => self.shadow.clone(), ProgramType::Resize => self .custom_resize .borrow() diff --git a/src/render_helpers/shaders/shadow.frag b/src/render_helpers/shaders/shadow.frag new file mode 100644 index 00000000..3912d71f --- /dev/null +++ b/src/render_helpers/shaders/shadow.frag @@ -0,0 +1,142 @@ +precision highp float; + +#if defined(DEBUG_FLAGS) +uniform float niri_tint; +#endif + +uniform float niri_alpha; +uniform float niri_scale; + +uniform vec2 niri_size; +varying vec2 niri_v_coords; + +uniform vec4 shadow_color; +uniform float sigma; + +uniform mat3 input_to_geo; +uniform vec2 geo_size; +uniform vec4 corner_radius; + +uniform mat3 window_input_to_geo; +uniform vec2 window_geo_size; +uniform vec4 window_corner_radius; + +// Based on: https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/ +// +// License: CC0 (http://creativecommons.org/publicdomain/zero/1.0/) + +// A standard gaussian function, used for weighting samples +float gaussian(float x, float sigma) { + const float pi = 3.141592653589793; + return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +vec2 erf(vec2 x) { + vec2 s = sign(x), a = abs(x); + x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + x *= x; + return s - s / (x * x); +} + +// Return the blurred mask along the x dimension +float roundedBoxShadowX(float x, float y, float sigma, float corner, vec2 halfSize) { + float delta = min(halfSize.y - corner - abs(y), 0.0); + float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); + vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +// Return the mask for the shadow of a box from lower to upper +float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner) { + // Center everything to make the math easier + vec2 center = (lower + upper) * 0.5; + vec2 halfSize = (upper - lower) * 0.5; + point -= center; + + // The signal is only non-zero in a limited range, so don't waste samples + float low = point.y - halfSize.y; + float high = point.y + halfSize.y; + float start = clamp(-3.0 * sigma, low, high); + float end = clamp(3.0 * sigma, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.0; + float y = start + step * 0.5; + float value = 0.0; + for (int i = 0; i < 4; i++) { + value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step; + y += step; + } + + return value; +} + +float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { + vec2 center; + float radius; + + if (coords.x < corner_radius.x && coords.y < corner_radius.x) { + radius = corner_radius.x; + 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.z < coords.x && size.y - corner_radius.z < coords.y) { + radius = corner_radius.z; + center = vec2(size.x - radius, size.y - radius); + } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { + radius = corner_radius.w; + center = vec2(radius, size.y - radius); + } else { + return 1.0; + } + + float dist = distance(coords, center); + float half_px = 0.5 / niri_scale; + return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); +} + +void main() { + vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0); + vec3 coords_window_geo = window_input_to_geo * vec3(niri_v_coords, 1.0); + + vec4 color = shadow_color; + + float shadow_value; + if (sigma < 0.1) { + // With low enough sigma just draw a rounded rectangle. + shadow_value = rounding_alpha(coords_geo.xy, geo_size, corner_radius); + } else { + shadow_value = roundedBoxShadow( + vec2(0.0, 0.0), + geo_size, + coords_geo.xy, + sigma, + // FIXME: figure out how to blur with different corner radii. + // + // GTK seems to call blurring separately for the rect and for the 4 corners: + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gsk/gpu/shaders/gskgpuboxshadow.glsl + corner_radius.x + ); + } + color = color * shadow_value; + + // Cut out the inside of the window geometry if requested. + if (window_geo_size != vec2(0.0, 0.0)) { + if (0.0 <= coords_window_geo.x && coords_window_geo.x <= window_geo_size.x + && 0.0 <= coords_window_geo.y && coords_window_geo.y <= window_geo_size.y) { + float alpha = rounding_alpha(coords_window_geo.xy, window_geo_size, window_corner_radius); + color = color * (1.0 - alpha); + } + } + + 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/shadow.rs b/src/render_helpers/shadow.rs new file mode 100644 index 00000000..b98202e4 --- /dev/null +++ b/src/render_helpers/shadow.rs @@ -0,0 +1,257 @@ +use std::collections::HashMap; + +use glam::{Mat3, Vec2}; +use niri_config::{Color, CornerRadius}; +use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage}; +use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform}; +use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions}; +use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform}; + +use super::renderer::NiriRenderer; +use super::shader_element::ShaderRenderElement; +use super::shaders::{mat3_uniform, ProgramType, Shaders}; +use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError}; + +/// Renders a rounded rectangle shadow. +#[derive(Debug, Clone)] +pub struct ShadowRenderElement { + inner: ShaderRenderElement, + params: Parameters, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct Parameters { + size: Size<f64, Logical>, + geometry: Rectangle<f64, Logical>, + color: Color, + sigma: f32, + corner_radius: CornerRadius, + // Should only be used for visual improvements, i.e. corner radius anti-aliasing. + scale: f32, + + window_geometry: Rectangle<f64, Logical>, + window_corner_radius: CornerRadius, +} + +impl ShadowRenderElement { + #[allow(clippy::too_many_arguments)] + pub fn new( + size: Size<f64, Logical>, + geometry: Rectangle<f64, Logical>, + color: Color, + sigma: f32, + corner_radius: CornerRadius, + scale: f32, + window_geometry: Rectangle<f64, Logical>, + window_corner_radius: CornerRadius, + ) -> Self { + let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified); + let mut rv = Self { + inner, + params: Parameters { + size, + geometry, + color, + sigma, + corner_radius, + scale, + window_geometry, + window_corner_radius, + }, + }; + rv.update_inner(); + rv + } + + pub fn empty() -> Self { + let inner = ShaderRenderElement::empty(ProgramType::Shadow, Kind::Unspecified); + Self { + inner, + params: Parameters { + size: Default::default(), + geometry: Default::default(), + color: Default::default(), + sigma: 0., + corner_radius: Default::default(), + scale: 1., + window_geometry: Default::default(), + window_corner_radius: Default::default(), + }, + } + } + + pub fn damage_all(&mut self) { + self.inner.damage_all(); + } + + #[allow(clippy::too_many_arguments)] + pub fn update( + &mut self, + size: Size<f64, Logical>, + geometry: Rectangle<f64, Logical>, + color: Color, + sigma: f32, + corner_radius: CornerRadius, + scale: f32, + window_geometry: Rectangle<f64, Logical>, + window_corner_radius: CornerRadius, + ) { + let params = Parameters { + size, + geometry, + color, + sigma, + corner_radius, + scale, + window_geometry, + window_corner_radius, + }; + if self.params == params { + return; + } + + self.params = params; + self.update_inner(); + } + + fn update_inner(&mut self) { + let Parameters { + size, + geometry, + color, + sigma, + corner_radius, + scale, + window_geometry, + window_corner_radius, + } = self.params; + + let area_size = Vec2::new(size.w as f32, size.h as f32); + + let geo_loc = Vec2::new(geometry.loc.x as f32, geometry.loc.y as f32); + let geo_size = Vec2::new(geometry.size.w as f32, geometry.size.h as f32); + + let input_to_geo = + Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size); + + let window_geo_loc = Vec2::new(window_geometry.loc.x as f32, window_geometry.loc.y as f32); + let window_geo_size = + Vec2::new(window_geometry.size.w as f32, window_geometry.size.h as f32); + + let window_input_to_geo = + Mat3::from_scale(area_size) * Mat3::from_translation(-window_geo_loc / area_size); + + self.inner.update( + size, + None, + scale, + vec![ + Uniform::new("shadow_color", color.to_array_premul()), + Uniform::new("sigma", sigma), + mat3_uniform("input_to_geo", input_to_geo), + Uniform::new("geo_size", geo_size.to_array()), + Uniform::new("corner_radius", <[f32; 4]>::from(corner_radius)), + mat3_uniform("window_input_to_geo", window_input_to_geo), + Uniform::new("window_geo_size", window_geo_size.to_array()), + Uniform::new( + "window_corner_radius", + <[f32; 4]>::from(window_corner_radius), + ), + ], + HashMap::new(), + ); + } + + pub fn with_location(mut self, location: Point<f64, Logical>) -> Self { + self.inner = self.inner.with_location(location); + self + } + + pub fn has_shader(renderer: &mut impl NiriRenderer) -> bool { + Shaders::get(renderer) + .program(ProgramType::Shadow) + .is_some() + } +} + +impl Default for ShadowRenderElement { + fn default() -> Self { + Self::empty() + } +} + +impl Element for ShadowRenderElement { + fn id(&self) -> &Id { + self.inner.id() + } + + fn current_commit(&self) -> CommitCounter { + self.inner.current_commit() + } + + fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> { + self.inner.geometry(scale) + } + + fn transform(&self) -> Transform { + self.inner.transform() + } + + fn src(&self) -> Rectangle<f64, Buffer> { + self.inner.src() + } + + fn damage_since( + &self, + scale: Scale<f64>, + commit: Option<CommitCounter>, + ) -> DamageSet<i32, Physical> { + self.inner.damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> { + self.inner.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.inner.alpha() + } + + fn kind(&self) -> Kind { + self.inner.kind() + } +} + +impl RenderElement<GlesRenderer> for ShadowRenderElement { + fn draw( + &self, + frame: &mut GlesFrame<'_>, + src: Rectangle<f64, Buffer>, + dst: Rectangle<i32, Physical>, + damage: &[Rectangle<i32, Physical>], + opaque_regions: &[Rectangle<i32, Physical>], + ) -> Result<(), GlesError> { + RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions) + } + + fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage> { + self.inner.underlying_storage(renderer) + } +} + +impl<'render> RenderElement<TtyRenderer<'render>> for ShadowRenderElement { + fn draw( + &self, + frame: &mut TtyFrame<'_, '_>, + src: Rectangle<f64, Buffer>, + dst: Rectangle<i32, Physical>, + damage: &[Rectangle<i32, Physical>], + opaque_regions: &[Rectangle<i32, Physical>], + ) -> Result<(), TtyRendererError<'render>> { + RenderElement::<TtyRenderer<'_>>::draw(&self.inner, frame, src, dst, damage, opaque_regions) + } + + fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option<UnderlyingStorage> { + self.inner.underlying_storage(renderer) + } +} |
