diff --git a/compositor/src/effects.rs b/compositor/src/effects.rs index 1c2d816..38adf4b 100644 --- a/compositor/src/effects.rs +++ b/compositor/src/effects.rs @@ -31,7 +31,6 @@ pub(crate) enum SemanticRole { } #[derive(Clone, Copy, Debug, PartialEq)] -#[allow(dead_code)] // Used by the material renderer in the next M7 slice. pub(crate) struct ResolvedMaterial { pub style: MaterialStyle, pub opacity: f32, @@ -41,7 +40,6 @@ pub(crate) struct ResolvedMaterial { pub noise: f32, } -#[allow(dead_code)] // Used by the material renderer in the next M7 slice. pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle) -> ResolvedMaterial { let (opacity, blur, saturation, brightness, noise) = match role { SemanticRole::Panel => (0.78, 24.0, 1.08, 1.02, 0.012), diff --git a/compositor/src/state.rs b/compositor/src/state.rs index df8cda5..4ec2f5a 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -123,6 +123,7 @@ pub struct Ferese { pub(crate) window_shadows: HashMap, pub(crate) rounded_clip_programs: HashMap, pub(crate) material_programs: HashMap, + pub(crate) backdrop_programs: HashMap, pub(crate) material_buffers: HashMap, material_scene_generation: u64, closing_windows: HashMap, @@ -438,6 +439,7 @@ impl Ferese { window_shadows: HashMap::new(), rounded_clip_programs: HashMap::new(), material_programs: HashMap::new(), + backdrop_programs: HashMap::new(), material_buffers: HashMap::new(), material_scene_generation: 0, closing_windows: HashMap::new(), diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 0e20dd2..e2c487d 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -6,8 +6,9 @@ use std::{ use smithay::{ backend::{ + allocator::Fourcc, renderer::{ - Color32F, ErasedContextId, Frame, ImportDma, Renderer, + Color32F, ErasedContextId, Frame, ImportDma, Offscreen, Renderer, Texture, damage::OutputDamageTracker, element::{ AsRenderElements, Element, Id, Kind as RenderElementKind, RenderElement, @@ -24,8 +25,8 @@ use smithay::{ }, }, gles::{ - GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, Uniform, - UniformName, UniformType, element::PixelShaderElement, + GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, + Uniform, UniformName, UniformType, element::PixelShaderElement, }, utils::{CommitCounter, DamageSet, OpaqueRegions, draw_render_elements}, }, @@ -71,6 +72,7 @@ render_elements! { Surface=SurfaceRenderElement, Memory=MemoryRenderElement, Border=PixelShaderElement, + Backdrop=BackdropRenderElement, } render_elements! { @@ -231,6 +233,62 @@ void main() { } "#; +const BACKDROP_SHADER: &str = r#"#version 100 + +//_DEFINES_ + +#if defined(EXTERNAL) +#extension GL_OES_EGL_image_external : require +#endif + +precision mediump float; +#if defined(EXTERNAL) +uniform samplerExternalOES tex; +#else +uniform sampler2D tex; +#endif +uniform float alpha; +uniform vec4 visible_rect; +uniform vec2 texture_size; +uniform float blur_radius; +uniform float saturation; +uniform float brightness; +uniform vec4 tint; +uniform float noise_amount; +varying vec2 v_coords; + +float random(vec2 point) { + return fract(sin(dot(point, vec2(12.9898, 78.233))) * 43758.5453); +} + +void main() { + vec2 point = gl_FragCoord.xy; + vec2 visible_max = visible_rect.xy + visible_rect.zw; + if (point.x < visible_rect.x || point.y < visible_rect.y + || point.x >= visible_max.x || point.y >= visible_max.y) { + gl_FragColor = vec4(0.0); + return; + } + + vec2 step_size = vec2(blur_radius) / max(texture_size, vec2(1.0)); + vec4 color = texture2D(tex, v_coords) * 0.20; + color += texture2D(tex, v_coords + vec2(step_size.x, 0.0)) * 0.12; + color += texture2D(tex, v_coords - vec2(step_size.x, 0.0)) * 0.12; + color += texture2D(tex, v_coords + vec2(0.0, step_size.y)) * 0.12; + color += texture2D(tex, v_coords - vec2(0.0, step_size.y)) * 0.12; + color += texture2D(tex, v_coords + step_size) * 0.08; + color += texture2D(tex, v_coords - step_size) * 0.08; + color += texture2D(tex, v_coords + vec2(step_size.x, -step_size.y)) * 0.08; + color += texture2D(tex, v_coords + vec2(-step_size.x, step_size.y)) * 0.08; + + float luma = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); + vec3 adjusted = mix(vec3(luma), color.rgb, saturation) * brightness; + adjusted = mix(adjusted, tint.rgb, tint.a); + adjusted += vec3((random(point) - 0.5) * noise_amount); + gl_FragColor = vec4(clamp(adjusted, 0.0, 1.0) * alpha, alpha); +} +"#; + #[derive(Clone, Debug)] pub(crate) struct RoundedClipPrograms { texture: GlesTexProgram, @@ -241,6 +299,9 @@ pub(crate) struct RoundedClipPrograms { #[derive(Clone, Debug)] pub(crate) struct MaterialProgram(GlesPixelProgram); +#[derive(Clone, Debug)] +pub(crate) struct BackdropProgram(GlesTexProgram); + #[derive(Clone, Debug, PartialEq)] struct MaterialParameters { geometry: Rectangle, @@ -249,19 +310,162 @@ struct MaterialParameters { generation: u64, scene_generation: u64, opaque: bool, + blur: f32, + saturation: f32, + brightness: f32, + sample_geometry: Rectangle, + sample_physical: Rectangle, + visible_framebuffer: [f32; 4], + sample_framebuffer: [f32; 4], } #[derive(Debug)] struct CachedMaterial { - element: PixelShaderElement, + element: CachedMaterialElement, parameters: MaterialParameters, } +#[derive(Clone, Debug)] +enum CachedMaterialElement { + Tint(PixelShaderElement), + Backdrop(BackdropRenderElement), +} + #[derive(Debug, Default)] pub(crate) struct MaterialBuffers { contexts: HashMap, } +#[derive(Clone, Debug)] +struct BackdropRenderElement { + texture: GlesTexture, + program: GlesTexProgram, + id: Id, + commit: CommitCounter, + geometry: Rectangle, + capture_rect: [i32; 4], + uniforms: Vec>, +} + +impl BackdropRenderElement { + fn new(texture: GlesTexture, program: GlesTexProgram, parameters: &MaterialParameters) -> Self { + Self { + texture, + program, + id: Id::new(), + commit: CommitCounter::default(), + geometry: parameters.sample_geometry, + capture_rect: framebuffer_capture_rect(parameters.sample_framebuffer), + uniforms: backdrop_uniforms(parameters), + } + } + + fn update(&mut self, parameters: &MaterialParameters) { + self.geometry = parameters.sample_geometry; + self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer); + self.uniforms = backdrop_uniforms(parameters); + self.commit.increment(); + } +} + +impl Element for BackdropRenderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.commit + } + + fn src(&self) -> Rectangle { + Rectangle::from_size(self.texture.size().to_f64()) + } + + fn geometry(&self, scale: RenderScale) -> Rectangle { + self.geometry.to_physical_precise_round(scale) + } + + fn damage_since( + &self, + scale: RenderScale, + commit: Option, + ) -> DamageSet { + if commit == Some(self.commit) { + DamageSet::default() + } else { + DamageSet::from_slice(&[Rectangle::from_size(self.geometry(scale).size)]) + } + } + + fn opaque_regions(&self, _scale: RenderScale) -> OpaqueRegions { + // Backdrop dependencies must remain visible to damage tracking even + // though the shader produces opaque pixels inside the visible region. + OpaqueRegions::default() + } + + fn alpha(&self) -> f32 { + 1.0 + } + + fn kind(&self) -> RenderElementKind { + RenderElementKind::Unspecified + } +} + +impl RenderElement for BackdropRenderElement { + fn draw( + &self, + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + ) -> Result<(), GlesError> { + let texture = self.texture.tex_id(); + let [x, y, width, height] = self.capture_rect; + frame.with_context(|gl| unsafe { + gl.BindTexture(smithay::backend::renderer::gles::ffi::TEXTURE_2D, texture); + gl.TexParameteri( + smithay::backend::renderer::gles::ffi::TEXTURE_2D, + smithay::backend::renderer::gles::ffi::TEXTURE_WRAP_S, + smithay::backend::renderer::gles::ffi::CLAMP_TO_EDGE as i32, + ); + gl.TexParameteri( + smithay::backend::renderer::gles::ffi::TEXTURE_2D, + smithay::backend::renderer::gles::ffi::TEXTURE_WRAP_T, + smithay::backend::renderer::gles::ffi::CLAMP_TO_EDGE as i32, + ); + gl.CopyTexSubImage2D( + smithay::backend::renderer::gles::ffi::TEXTURE_2D, + 0, + 0, + 0, + x, + y, + width, + height, + ); + gl.BindTexture(smithay::backend::renderer::gles::ffi::TEXTURE_2D, 0); + })?; + + frame.render_texture_from_to( + &self.texture, + src, + dst, + damage, + opaque_regions, + Transform::Normal, + 1.0, + Some(&self.program), + &self.uniforms, + ) + } + + fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option> { + None + } +} + #[derive(Clone, Debug, PartialEq)] struct BorderParameters { geometry: Rectangle, @@ -1179,8 +1383,8 @@ fn layer_elements( }), ); - if let Some(material) = material_element(state, renderer, &layer, geometry) { - elements.push(material.into()); + if let Some(material) = material_element(state, renderer, output, &layer, geometry) { + elements.push(material); } } @@ -1190,44 +1394,101 @@ fn layer_elements( fn material_element( state: &mut Ferese, renderer: &mut GlesRenderer, + output: &Output, layer: &LayerSurface, geometry: Rectangle, -) -> Option { +) -> Option { let surface = layer.wl_surface(); let (role, generation) = crate::effects::surface_role(surface)?; let material = crate::effects::resolve_material(role, state.theme_settings.material_style); - let program = material_program(state, renderer)?; - let tint = match material.style { - crate::config::MaterialStyle::Solid => [0.067, 0.094, 0.129, material.opacity], - crate::config::MaterialStyle::Glass | crate::config::MaterialStyle::Translucent => { - [0.067, 0.094, 0.129, material.opacity] - } - }; + let mode = output.current_mode()?; + let scale = output.current_scale().fractional_scale(); + let output_logical = smithay::utils::Size::::from(( + (f64::from(mode.size.w) / scale).ceil() as i32, + (f64::from(mode.size.h) / scale).ceil() as i32, + )); + let sample_geometry = + expanded_blur_region(geometry, material.blur.ceil() as i32, output_logical); + let sample_physical = sample_geometry.to_physical_precise_round(scale); + let transform = output.current_transform().invert(); + let tint = [0.067, 0.094, 0.129, material.opacity]; let parameters = MaterialParameters { geometry, tint, noise: material.noise, generation, - scene_generation: state.material_scene_generation(), + scene_generation: matches!(material.style, crate::config::MaterialStyle::Glass) + .then(|| state.material_scene_generation()) + .unwrap_or(0), opaque: material.opacity == 1.0, + blur: material.blur * scale as f32, + saturation: material.saturation, + brightness: material.brightness, + sample_geometry, + sample_physical, + visible_framebuffer: framebuffer_clip_rect( + geometry.to_physical_precise_round(scale), + mode.size, + transform, + ), + sample_framebuffer: framebuffer_clip_rect(sample_physical, mode.size, transform), }; let context = renderer.context_id().erased(); + let needs_replacement = state + .material_buffers + .get(surface) + .and_then(|buffers| buffers.contexts.get(&context)) + .is_none_or(|cached| match (&cached.element, material.style) { + (CachedMaterialElement::Backdrop(_), crate::config::MaterialStyle::Glass) => { + cached.parameters.sample_physical.size != parameters.sample_physical.size + } + ( + CachedMaterialElement::Tint(_), + crate::config::MaterialStyle::Translucent | crate::config::MaterialStyle::Solid, + ) => false, + _ => true, + }); + let mut replacement = needs_replacement + .then(|| match material.style { + crate::config::MaterialStyle::Glass => { + let program = backdrop_program(state, renderer)?; + let texture_size = smithay::utils::Size::::from(( + sample_physical.size.w, + sample_physical.size.h, + )); + let texture = Offscreen::::create_buffer( + renderer, + Fourcc::Abgr8888, + texture_size, + ) + .ok()?; + Some(CachedMaterialElement::Backdrop(BackdropRenderElement::new( + texture, + program.0, + ¶meters, + ))) + } + crate::config::MaterialStyle::Translucent | crate::config::MaterialStyle::Solid => { + let program = material_program(state, renderer)?; + Some(CachedMaterialElement::Tint(PixelShaderElement::new( + program.0, + geometry, + parameters + .opaque + .then(|| vec![Rectangle::from_size(geometry.size)]), + 1.0, + material_uniforms(¶meters), + RenderElementKind::Unspecified, + ))) + } + }) + .flatten(); let buffers = state.material_buffers.entry(surface.clone()).or_default(); if !buffers.contexts.contains_key(&context) { - let element = PixelShaderElement::new( - program.0, - geometry, - parameters - .opaque - .then(|| vec![Rectangle::from_size(geometry.size)]), - 1.0, - material_uniforms(¶meters), - RenderElementKind::Unspecified, - ); buffers.contexts.insert( context.clone(), CachedMaterial { - element, + element: replacement.take()?, parameters: parameters.clone(), }, ); @@ -1235,21 +1496,32 @@ fn material_element( let cached = buffers.contexts.get_mut(&context)?; if cached.parameters != parameters { - if cached.parameters.geometry != parameters.geometry - || cached.parameters.opaque != parameters.opaque - { - let opaque = parameters - .opaque - .then(|| vec![Rectangle::from_size(parameters.geometry.size)]); - cached.element.resize(parameters.geometry, opaque); + match &mut cached.element { + CachedMaterialElement::Tint(element) => { + if cached.parameters.geometry != parameters.geometry + || cached.parameters.opaque != parameters.opaque + { + let opaque = parameters + .opaque + .then(|| vec![Rectangle::from_size(parameters.geometry.size)]); + element.resize(parameters.geometry, opaque); + } + element.update_uniforms(material_uniforms(¶meters)); + } + CachedMaterialElement::Backdrop(element) + if cached.parameters.sample_physical.size == parameters.sample_physical.size => + { + element.update(¶meters); + } + _ => cached.element = replacement.take()?, } - cached - .element - .update_uniforms(material_uniforms(¶meters)); cached.parameters = parameters; } - Some(cached.element.clone()) + Some(match &cached.element { + CachedMaterialElement::Tint(element) => element.clone().into(), + CachedMaterialElement::Backdrop(element) => element.clone().into(), + }) } fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option { @@ -1275,6 +1547,34 @@ fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option Option { + let context = renderer.context_id().erased(); + if let Some(program) = state.backdrop_programs.get(&context) { + return Some(program.clone()); + } + + let uniforms = [ + UniformName::new("visible_rect", UniformType::_4f), + UniformName::new("texture_size", UniformType::_2f), + UniformName::new("blur_radius", UniformType::_1f), + UniformName::new("saturation", UniformType::_1f), + UniformName::new("brightness", UniformType::_1f), + UniformName::new("tint", UniformType::_4f), + UniformName::new("noise_amount", UniformType::_1f), + ]; + match renderer.compile_custom_texture_shader(BACKDROP_SHADER, &uniforms) { + Ok(program) => { + let program = BackdropProgram(program); + state.backdrop_programs.insert(context, program.clone()); + Some(program) + } + Err(error) => { + tracing::error!(%error, "failed to compile backdrop material shader"); + None + } + } +} + fn material_uniforms(parameters: &MaterialParameters) -> Vec> { vec![ Uniform::new("tint", parameters.tint).into_owned(), @@ -1282,6 +1582,51 @@ fn material_uniforms(parameters: &MaterialParameters) -> Vec> { ] } +fn backdrop_uniforms(parameters: &MaterialParameters) -> Vec> { + vec![ + Uniform::new("visible_rect", parameters.visible_framebuffer).into_owned(), + Uniform::new( + "texture_size", + [ + parameters.sample_physical.size.w as f32, + parameters.sample_physical.size.h as f32, + ], + ) + .into_owned(), + Uniform::new("blur_radius", parameters.blur).into_owned(), + Uniform::new("saturation", parameters.saturation).into_owned(), + Uniform::new("brightness", parameters.brightness).into_owned(), + Uniform::new("tint", parameters.tint).into_owned(), + Uniform::new("noise_amount", parameters.noise).into_owned(), + ] +} + +fn framebuffer_capture_rect(rect: [f32; 4]) -> [i32; 4] { + [ + rect[0].round() as i32, + rect[1].round() as i32, + rect[2].round() as i32, + rect[3].round() as i32, + ] +} + +fn expanded_blur_region( + visible: Rectangle, + radius: i32, + output_size: smithay::utils::Size, +) -> Rectangle { + let radius = radius.max(0); + let left = (visible.loc.x - radius).max(0); + let top = (visible.loc.y - radius).max(0); + let right = (visible.loc.x + visible.size.w + radius).min(output_size.w); + let bottom = (visible.loc.y + visible.size.h + radius).min(output_size.h); + + Rectangle::new( + (left, top).into(), + ((right - left).max(1), (bottom - top).max(1)).into(), + ) +} + fn cursor_elements( state: &Ferese, renderer: &mut GlesRenderer, @@ -1377,8 +1722,8 @@ mod tests { }; use super::{ - color_with_alpha, framebuffer_clip_rect, normalized_scale, rounded_visual_rect, - scaled_visual_rect, shadow_bounds, + color_with_alpha, expanded_blur_region, framebuffer_capture_rect, framebuffer_clip_rect, + normalized_scale, rounded_visual_rect, scaled_visual_rect, shadow_bounds, }; #[derive(Debug)] @@ -1455,6 +1800,34 @@ mod tests { ); } + #[test] + fn blur_sampling_expands_around_the_visible_surface() { + let visible = Rectangle::::new((100, 80).into(), (400, 300).into()); + + assert_eq!( + expanded_blur_region(visible, 24, (1_000, 800).into()), + Rectangle::new((76, 56).into(), (448, 348).into()) + ); + } + + #[test] + fn blur_sampling_clips_to_output_edges() { + let visible = Rectangle::::new((0, 760).into(), (300, 40).into()); + + assert_eq!( + expanded_blur_region(visible, 24, (1_000, 800).into()), + Rectangle::new((0, 736).into(), (324, 64).into()) + ); + } + + #[test] + fn framebuffer_capture_rounds_fractional_bounds() { + assert_eq!( + framebuffer_capture_rect([10.4, 20.6, 300.2, 199.8]), + [10, 21, 300, 200] + ); + } + #[test] fn shadow_bounds_include_blur_and_offset() { let geometry = Rectangle::::new((100, 80).into(), (400, 300).into()); diff --git a/tools/ferese-effects-probe/src/main.rs b/tools/ferese-effects-probe/src/main.rs index c89b8bc..4fb8e5f 100644 --- a/tools/ferese-effects-probe/src/main.rs +++ b/tools/ferese-effects-probe/src/main.rs @@ -1,6 +1,7 @@ use std::error::Error; use std::io::{Seek, SeekFrom, Write}; use std::os::fd::AsFd; +use std::time::Duration; use ferese_protocols::effects::v1::client::{ ferese_effects_manager_v1::FereseEffectsManagerV1, @@ -60,6 +61,12 @@ fn main() -> Result<(), Box> { return Err(format!("semantic role {role:?} changed layer geometry").into()); } } + + // Keep the final glass role mapped long enough for the compositor to run + // at least one presentation. This turns shader and capture failures into + // probe failures instead of disconnecting before the first rendered frame. + std::thread::sleep(Duration::from_millis(100)); + effects.clear_role(); queue.roundtrip(&mut state)?; if state.configure_count != initial_configures {