use std::{ borrow::{Borrow, BorrowMut}, sync::{LazyLock, Mutex}, }; use cgmath::{Matrix3, SquareMatrix, Vector2}; use smithay::{ backend::{ allocator::Fourcc, renderer::{ Bind, BlitFrame, Color32F, ContextId, Frame, FrameContext, ImportAll, Offscreen, Renderer, Texture, TextureFilter, element::{Element, Id, Kind, RenderElement}, gles::{ GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName, UniformType, UniformValue, ffi, }, sync::SyncPoint, utils::{CommitCounter, DamageSet}, }, }, utils::{ Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform, user_data::UserDataMap, }, wayland::compositor::SurfaceData, }; use tracing::trace; use crate::{ backend::render::{element::AsGlowRenderer, wayland::clipped_surface::ClippingShader}, wayland::handlers::background_effect::ComputedBlurRegionCachedState, }; pub static BLUR_DOWNSAMPLE_SHADER: &str = include_str!("../shaders/blur_downsample.frag"); pub static BLUR_UPSAMPLE_SHADER: &str = include_str!("../shaders/blur_upsample.frag"); const NOISE: f32 = 0.03; const MAX_STEPS: usize = 15; #[derive(Debug, Clone, Copy, PartialEq)] struct BlurParameters { passes: usize, offset: f64, extended_radius: i32, } static BLUR_PARAMS: LazyLock> = LazyLock::new(|| { let mut params = Vec::new(); let mut remaining_steps = MAX_STEPS as isize; let offsets = [ // min offset, max offset, extended radius to avoid artifacts (1.0, 2.0, 10), (2.0, 3.0, 20), (2.0, 5.0, 50), (3.0, 8.0, 150), ]; let sum = offsets.iter().map(|(min, max, _)| *max - *min).sum::(); for (i, (min, max, extended_radius)) in offsets.into_iter().enumerate() { let mut iter_num = f64::ceil((max - min) / sum * (MAX_STEPS as f64)) as usize; remaining_steps -= iter_num as isize; if remaining_steps < 0 { iter_num = iter_num.saturating_add_signed(remaining_steps); } let diff = max - min; for j in 1..=iter_num { params.push(BlurParameters { passes: i + 1, offset: min + (diff / iter_num as f64) * j as f64, extended_radius, }); } } trace!("Computed blur values: {:#?}", ¶ms); params }); #[derive(Debug, Clone)] pub struct BlurShaders { down: GlesTexProgram, up: GlesTexProgram, } impl BlurShaders { pub fn compile(renderer: &mut GlesRenderer) -> Result { let up = renderer.compile_custom_texture_shader( BLUR_UPSAMPLE_SHADER, &[ UniformName::new("half_pixel", UniformType::_2f), UniformName::new("offset", UniformType::_1f), ], )?; let down = renderer.compile_custom_texture_shader( BLUR_DOWNSAMPLE_SHADER, &[ UniformName::new("half_pixel", UniformType::_2f), UniformName::new("offset", UniformType::_1f), ], )?; Ok(BlurShaders { up, down }) } pub fn get(renderer: &R) -> Self { Borrow::::borrow(renderer.glow_renderer()) .egl_context() .user_data() .get::() .expect("Custom Shaders not initialized") .clone() } } type BlurTexture = Mutex>; struct BlurState { id: Id, renderer_id: Option>, src: Size, offset: f64, passes: usize, region: Vec>, commit: CommitCounter, } unsafe impl Send for BlurState {} unsafe impl Sync for BlurState {} impl BlurState { fn new() -> Self { BlurState { id: Id::new(), renderer_id: None, src: Size::new(0., 0.), offset: 0., passes: 0, region: Vec::new(), commit: CommitCounter::default(), } } } pub struct BlurElement { id: Id, commit: CommitCounter, src: Size, geometry: Rectangle, scaling_shaders: BlurShaders, render_shader: GlesTexProgram, region: Vec>, offset: f64, passes: usize, uniforms: Vec>, } impl BlurElement { pub fn from_surface( renderer: &mut R, states: &SurfaceData, geometry: Rectangle, output_scale: f64, radii: [u8; 4], strength: usize, ) -> Result, R::Error> { let mut blur_region_state = states.cached_state.get::(); let Some(region) = blur_region_state.current().blur_region.as_ref() else { return Ok(None); }; let geo = geometry.to_physical_precise_round(output_scale); let mut extended_geo = geo; let radius = BLUR_PARAMS[strength.min(MAX_STEPS - 1)].extended_radius as f64; extended_geo.loc -= Point::::new(radius, radius); extended_geo.size += Size::::new(radius, radius).upscale(2.); // compute input_to_geo so that it crops the extended capture radius let geo_scale = { let Scale { x, y } = geo.size / extended_geo.size; Matrix3::from_nonuniform_scale(x as f32, y as f32) .invert() .unwrap() }; let geo_translation = { let offset = geo.loc - extended_geo.loc; Matrix3::from_translation(Vector2::new( (offset.x / extended_geo.size.w) as f32, (offset.y / extended_geo.size.h) as f32, )) .invert() .unwrap() }; let input_to_geo = geo_scale * geo_translation; let uniforms = vec![ Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)), Uniform::new( "corner_radius", [ radii[3] as f32, radii[1] as f32, radii[0] as f32, radii[2] as f32, ], ), Uniform::new( "input_to_geo", UniformValue::Matrix3x3 { matrices: vec![*AsRef::<[f32; 9]>::as_ref(&input_to_geo)], transpose: false, }, ), Uniform::new("noise", UniformValue::_1f(NOISE)), ]; let state = states .data_map .get_or_insert_threadsafe::, _>(|| Mutex::new(BlurState::new())); Ok(Some(Self::from_state( renderer, extended_geo.to_logical(output_scale), region, output_scale, &mut state.lock().unwrap(), uniforms, strength, )?)) } fn from_state( renderer: &mut R, geometry: Rectangle, region: &Vec>, output_scale: f64, state: &mut BlurState, uniforms: Vec>, strength: usize, ) -> Result { let renderer_id = renderer.glow_renderer().context_id(); let src = geometry.size.to_buffer(output_scale, Transform::Normal); let params = &BLUR_PARAMS[strength.min(MAX_STEPS - 1)]; let dirty = !(state .renderer_id .as_ref() .is_some_and(|id| id == &renderer_id) && state.offset == params.offset && state.passes == params.passes && &state.region == region && state.src == src); state.renderer_id = Some(renderer_id); state.offset = params.offset; state.passes = params.passes; state.region = region.clone(); state.src = src; if dirty { state.commit.increment(); } Ok(BlurElement { id: state.id.clone(), commit: state.commit, src, geometry, scaling_shaders: BlurShaders::get(renderer), render_shader: ClippingShader::get(renderer), offset: state.offset, passes: state.passes, region: region .iter() .map(|rect| rect.to_physical_precise_round(output_scale)) .collect(), uniforms, }) } } impl Element for BlurElement { fn id(&self) -> &Id { &self.id } fn current_commit(&self) -> CommitCounter { self.commit } fn src(&self) -> Rectangle { Rectangle::from_size(self.src) } fn geometry(&self, scale: Scale) -> Rectangle { self.geometry.to_physical_precise_round(scale) } fn transform(&self) -> Transform { Transform::Normal } fn damage_since( &self, scale: Scale, commit: Option, ) -> DamageSet { if self.commit.distance(commit).is_none_or(|d| d > 0) { DamageSet::from_slice(&[self.geometry.to_physical_precise_round(scale)]) } else { DamageSet::default() } } fn alpha(&self) -> f32 { 1.0 } fn kind(&self) -> Kind { Kind::default() } fn is_framebuffer_effect(&self) -> bool { true } } impl RenderElement for BlurElement { fn capture_framebuffer( &self, frame: &mut ::Frame<'_, '_>, src: Rectangle, dst: Rectangle, cache: &UserDataMap, ) -> Result<(), ::Error> { let glow_frame = ::glow_frame_mut(frame); let gles_frame = BorrowMut::>::borrow_mut(glow_frame); let transform = gles_frame.transformation(); let tex_size = self.src.to_i32_round(); let texture_ref = cache.get_or_insert_threadsafe(BlurTexture::default); let mut texture_entry = texture_ref.lock().unwrap(); if texture_entry .as_ref() .is_some_and(|tex| tex.size() != tex_size) { texture_entry.take(); } let mut renderer = gles_frame.renderer(); if texture_entry.is_none() { *texture_entry = Some( renderer .as_mut() .create_buffer(Fourcc::Abgr8888, tex_size) .map_err(R::from_gles_error)?, ); } let texture = texture_entry.as_mut().unwrap(); let mut off_texture = renderer .as_mut() .create_buffer(Fourcc::Abgr8888, tex_size) .map_err(R::from_gles_error)?; std::mem::drop(renderer); let sync = blit_from_active_fb(gles_frame, src, dst, transform, texture) .map_err(R::from_gles_error)?; gles_frame.wait(&sync).map_err(R::from_gles_error)?; let mut textures = [texture, &mut off_texture]; render_blur( gles_frame.renderer().as_mut(), &self.scaling_shaders, &mut textures, self.offset, self.passes, ) .map_err(R::from_gles_error)?; Ok(()) } fn draw( &self, frame: &mut R::Frame<'_, '_>, src: Rectangle, dst: Rectangle, damage: &[Rectangle], opaque_regions: &[Rectangle], cache: Option<&UserDataMap>, ) -> Result<(), R::Error> { let glow_frame = ::glow_frame_mut(frame); let damage = self .region .iter() .flat_map(|rect| damage.iter().flat_map(|r| r.intersection(*rect))) .collect::>(); let cache = cache.expect("Framebuffer element without cache?"); let Some(texture) = cache.get::() else { return Err(R::from_gles_error(GlesError::BlitError)); }; let texture_ref = texture.lock().unwrap(); BorrowMut::>::borrow_mut(glow_frame) .render_texture_from_to( texture_ref .as_ref() .ok_or(R::from_gles_error(GlesError::BlitError))?, src, dst, &damage, opaque_regions, Transform::Normal, 1.0, Some(&self.render_shader), &self.uniforms, ) .map_err(R::from_gles_error) } } fn blit_from_active_fb( frame: &mut GlesFrame<'_, '_>, src: Rectangle, dst: Rectangle, transform: Transform, to_texture: &mut GlesTexture, ) -> Result { if transform != Transform::Normal { let tex_size = to_texture .size() .to_logical(1, Transform::Normal) .to_physical(1); let mut renderer = frame.renderer(); let mut tmp_texture = renderer.as_mut().create_buffer( Fourcc::Abgr8888, dst.size.to_logical(1).to_buffer(1, Transform::Normal), )?; let mut fb_tmp = renderer.as_mut().bind(&mut tmp_texture)?; let mut fb = renderer.as_mut().bind(to_texture)?; std::mem::drop(renderer); frame.blit_to( &mut fb_tmp, dst, Rectangle::from_size(dst.size), TextureFilter::Linear, )?; std::mem::drop(fb_tmp); let mut renderer = frame.renderer(); let mut frame = renderer .as_mut() .render(&mut fb, tex_size, Transform::Normal)?; Frame::render_texture_from_to( &mut frame, &tmp_texture, Rectangle::from_size( dst.size .to_logical(1) .to_buffer(1, Transform::Normal) .to_f64(), ), src.to_logical(1., Transform::Normal, &src.size) .to_physical(1.) .to_i32_round(), &[Rectangle::from_size(dst.size)], &[Rectangle::from_size(dst.size)], transform, 1.0, )?; std::mem::drop(tmp_texture); frame.finish() } else { let mut fb = frame.renderer().as_mut().bind(to_texture)?; frame .blit_to( &mut fb, dst, src.to_logical(1., Transform::Normal, &src.size) .to_physical(1.) .to_i32_round(), TextureFilter::Linear, ) .map(|_| SyncPoint::signaled()) } } fn render_blur( renderer: &mut GlesRenderer, shaders: &BlurShaders, textures: &mut [&mut GlesTexture; 2], offset: f64, passes: usize, ) -> Result<(), GlesError> { for i in 0..passes { let tex_size = textures[0].size(); let [src_tex, target_tex] = textures; let mut fb = renderer.bind(*target_tex)?; let adjusted_tex_size = tex_size.downscale(1 << i); let target_tex_size = tex_size .downscale(1 << (i + 1)) .to_logical(1, Transform::Normal) .to_physical(1); let half_pixel = [ 0.5 / (adjusted_tex_size.w as f32), 0.5 / (adjusted_tex_size.h as f32), ]; let mut frame = renderer.render( &mut fb, tex_size.to_logical(1, Transform::Normal).to_physical(1), Transform::Normal, )?; frame.clear( Color32F::new(0., 0., 0., 0.), &[Rectangle::from_size( tex_size.to_logical(1, Transform::Normal).to_physical(1), )], )?; frame.with_context(|gl| unsafe { gl.TexParameteri( ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::CLAMP_TO_EDGE as i32, ); gl.TexParameteri( ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::CLAMP_TO_EDGE as i32, ); })?; frame.render_texture_from_to( src_tex, Rectangle::from_size(adjusted_tex_size.to_f64()), Rectangle::from_size(target_tex_size), &[Rectangle::from_size(target_tex_size)], &[Rectangle::from_size(target_tex_size)], Transform::Normal, 1.0, Some(&shaders.down), &[ Uniform::new("half_pixel", half_pixel), Uniform::new("offset", (offset / (1 << i) as f64) as f32), ], )?; frame.with_context(|gl| unsafe { gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::REPEAT as i32); gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::REPEAT as i32); })?; let sync = frame.finish()?; std::mem::drop(fb); renderer.wait(&sync)?; textures.swap(0, 1); } for i in 0..passes { let tex_size = textures[0].size(); let [src_tex, target_tex] = textures; let mut fb = renderer.bind(*target_tex)?; let adjusted_tex_size = tex_size.downscale(1 << (passes - i)); let target_tex_size = tex_size .downscale(1 << (passes - i - 1)) .to_logical(1, Transform::Normal) .to_physical(1); let half_pixel = [ 0.5 / (adjusted_tex_size.w as f32), 0.5 / (adjusted_tex_size.h as f32), ]; let mut frame = renderer.render( &mut fb, tex_size.to_logical(1, Transform::Normal).to_physical(1), Transform::Normal, )?; frame.clear( Color32F::new(0., 0., 0., 0.), &[Rectangle::from_size( tex_size.to_logical(1, Transform::Normal).to_physical(1), )], )?; frame.with_context(|gl| unsafe { gl.TexParameteri( ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::CLAMP_TO_EDGE as i32, ); gl.TexParameteri( ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::CLAMP_TO_EDGE as i32, ); })?; frame.render_texture_from_to( src_tex, Rectangle::from_size(adjusted_tex_size.to_f64()), Rectangle::from_size(target_tex_size), &[Rectangle::from_size(target_tex_size)], &[Rectangle::from_size(target_tex_size)], Transform::Normal, 1.0, Some(&shaders.up), &[ Uniform::new("half_pixel", half_pixel), Uniform::new("offset", (offset / (1 << (passes - i)) as f64) as f32), ], )?; frame.with_context(|gl| unsafe { gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::REPEAT as i32); gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::REPEAT as i32); })?; let sync = frame.finish()?; std::mem::drop(fb); renderer.wait(&sync)?; textures.swap(0, 1); } // textures always end up the right way around with `self.texture` containing our final render, // since we render PASSES * 2 (downscale and upscale), so the number of swaps is always even. Ok(()) }