feat(blur): reintroduce true blur, with x-ray config option

This commit is contained in:
2025-11-29 20:55:20 +01:00
parent bbe38aada7
commit dabe12fdf4
7 changed files with 197 additions and 172 deletions

View File

@@ -21,8 +21,13 @@ the [original readme](./README_orig.md).
Windows (both floating and tiling), as well as layer surfaces can have blur enabled on them. Blur needs to be enabled
for each window / layer surface explicitly.
Currently, all windows will draw "optimized" or "x-ray" blur, which is rendered separately using only the `bottom` and
`background` layer surfaces.
Tiled windows will always draw "optimized" or "x-ray" blur, which is rendered from a shared texture using only the
`bottom` and `background` layer surfaces, and is updated at a slower rate. Floating windows, as well as `top` or
`overlay` layer surfaces will draw "true" blur by default instead, which is rendered using all available screen
contents.
Note that true blur is rather expensive in terms of GPU load however, so an option exists to also have these surfaces
draw `x-ray` blur instead.
To set global defaults for blur:
@@ -72,17 +77,20 @@ layer-rule {
// note that this will require rendering the blurred surface twice, so if possible,
// prefer using `geometry-corner-radius` instead, for performance reasons.
ignore-alpha 0.45
// will render "x-ray" blur that is only based on `bottom` and `background` layer surfaces,
// even if the window is floating. good for minimal GPU load.
x-ray true
}
}
```
#### Caveats
- Enabling blur currently incurs a noticeable increase in GPU utilization on high frame rates (tested with 240hz)
compared to other compositors with similar functionality (e.g. Hyprland). Investigating this will require some sort of
[GPU profiling](https://github.com/Smithay/smithay/pull/1134).
- There is currently no way to enable "true" blur for floating windows or top / overlay layer surfaces. The
functionality exists in code, but is very slow / buggy at the moment.
- True blur currently only works for horizontal monitor configurations. When using any sort of 90 or 270 degree
transformations, only x-ray blur will be available.
- True blur is rather performance intensive as of right now, since it renders itself on every frame. It is recommended
to only enable it for surfaces that don't take up a lot of screen time (e.g. notifications, dialogs).
- Blur is currently only possible to be enabled through the config. Implementing both
[KDE blur](https://wayland.app/protocols/kde-blur) and
[background effect](https://wayland.app/protocols/ext-background-effect-v1) is planned though.
@@ -152,7 +160,7 @@ Since windows can be grouped on a per-tile basis, column-level tabbing is obsole
options have been removed to improve maintainability. If you have any tabbed-column related options in your niri config,
this fork will fail to parse it.
## Plans
## Future Plans
As of right now, I am trying to keep this fork "as close to upstream as is reasonable", to allow for frequent rebasing
without too many conflicts to solve.
@@ -240,8 +248,6 @@ To use it, simply import the module it provides into your config:
# optional, if using home-manager
home-manager = {
# recommended, as the niri module from this fork overrides the upstream niri package
useGlobalPkgs = true;
users.my-username = {
imports = [
niri.homeManagerModules.default
@@ -249,6 +255,8 @@ To use it, simply import the module it provides into your config:
wayland.windowManager.niri = {
enable = true;
# use the package from `programs.niri.package`, which is set by `niri.nixosModules.default`
package = null;
# fully declarative niri configuration; converted to kdl during rebuild
#
# - simple entries without arguments are declared as `name = [];`

View File

@@ -345,6 +345,7 @@ pub struct Blur {
pub radius: FloatOrInt<0, 1024>,
pub noise: FloatOrInt<0, 1024>,
pub ignore_alpha: FloatOrInt<0, 1>,
pub x_ray: bool,
}
impl Default for Blur {
@@ -355,6 +356,7 @@ impl Default for Blur {
radius: FloatOrInt(0.0),
noise: FloatOrInt(0.0),
ignore_alpha: FloatOrInt(0.0),
x_ray: false,
}
}
}
@@ -366,7 +368,7 @@ impl MergeWith<BlurRule> for Blur {
self.on = false;
}
merge_clone!((self, part), passes, radius, noise, ignore_alpha);
merge_clone!((self, part), passes, radius, noise, ignore_alpha, x_ray);
}
}
@@ -692,6 +694,8 @@ pub struct BlurRule {
pub noise: Option<FloatOrInt<0, 1024>>,
#[knuffel(child, unwrap(argument))]
pub ignore_alpha: Option<FloatOrInt<0, 1>>,
#[knuffel(child, unwrap(argument))]
pub x_ray: Option<bool>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
@@ -749,7 +753,7 @@ impl MergeWith<Self> for BlurRule {
fn merge_with(&mut self, part: &Self) {
merge_on_off!((self, part));
merge_clone_opt!((self, part), passes, radius, noise, ignore_alpha);
merge_clone_opt!((self, part), passes, radius, noise, ignore_alpha, x_ray);
}
}

View File

@@ -266,11 +266,12 @@ impl MappedLayer {
&& !target.should_block_out(self.rules.block_out_from))
.then(|| {
let fx_buffers = fx_buffers?;
let fx_buffers = fx_buffers.borrow();
// TODO: respect sync point?
let alpha_tex = gles_elems
.and_then(|gles_elems| {
let fx_buffers = fx_buffers.borrow();
let transform = fx_buffers.transform();
render_to_texture(
@@ -301,11 +302,13 @@ impl MappedLayer {
Some(
self.blur
.render(
&fx_buffers,
fx_buffers,
blur_sample_area,
self.rules.geometry_corner_radius.unwrap_or_default(),
self.scale,
geo,
!self.rules.blur.x_ray.unwrap_or_default(),
blur_sample_area.loc.to_f64(),
)
.map(Into::into),
)

View File

@@ -43,6 +43,7 @@ impl ResolvedLayerRules {
radius: None,
noise: None,
ignore_alpha: None,
x_ray: None,
},
shadow: ShadowRule {
off: false,

View File

@@ -1931,11 +1931,14 @@ impl<W: LayoutElement> Tile<W> {
.and_then(|fx_buffers| {
self.blur
.render(
&fx_buffers.borrow(),
fx_buffers,
blur_sample_area.to_i32_round(),
radius,
self.scale,
animated_geo,
self.focused_window().is_floating()
&& !self.focused_window().rules().blur.x_ray.unwrap_or_default(),
window_render_loc,
)
.map(Into::into)
})

View File

@@ -317,7 +317,6 @@ pub(super) unsafe fn get_main_buffer_blur(
supports_instancing: bool,
// dst is the region that we want blur on
dst: Rectangle<i32, Physical>,
is_tty: bool,
alpha_tex: Option<&GlesTexture>,
) -> Result<GlesTexture, GlesError> {
let tex_size = fx_buffers
@@ -335,6 +334,10 @@ pub(super) unsafe fn get_main_buffer_blur(
dst
};
// let dst_expanded = fx_buffers
// .transform()
// .transform_rect_in(dst_expanded, &tex_size);
let mut prev_fbo = 0;
gl.GetIntegerv(ffi::FRAMEBUFFER_BINDING, &mut prev_fbo as *mut _);
@@ -374,52 +377,33 @@ pub(super) unsafe fn get_main_buffer_blur(
// as the bound fbo size, so blitting uses dst immediately
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, sample_fbo);
if is_tty {
let src_x0 = dst_expanded.loc.x;
let src_y0 = dst_expanded.loc.y;
let src_x1 = dst_expanded.loc.x + dst_expanded.size.w;
let src_y1 = dst_expanded.loc.y + dst_expanded.size.h;
let dst_x0 = src_x0;
let dst_y0 = src_y0;
let dst_x1 = src_x1;
let dst_y1 = src_y1;
let dst_x0 = dst_expanded.loc.x;
let dst_y0 = dst_expanded.loc.y;
let dst_x1 = dst_expanded.loc.x + dst_expanded.size.w;
let dst_y1 = dst_expanded.loc.y + dst_expanded.size.h;
gl.BlitFramebuffer(
src_x0,
src_y0,
src_x1,
src_y1,
dst_x0,
dst_y0,
dst_x1,
dst_y1,
ffi::COLOR_BUFFER_BIT,
ffi::LINEAR,
);
} else {
let fb_height = tex_size.h;
let src_expanded = fx_buffers
.transform()
.invert()
.transform_rect_in(dst_expanded, &tex_size);
let dst_y0 = dst_expanded.loc.y + dst_expanded.size.h;
let dst_y1 = dst_expanded.loc.y;
let src_x0 = src_expanded.loc.x;
let src_y0 = src_expanded.loc.y;
let src_x1 = src_expanded.loc.x + src_expanded.size.w;
let src_y1 = src_expanded.loc.y + src_expanded.size.h;
let src_x0 = dst_expanded.loc.x;
let src_x1 = dst_expanded.loc.x + dst_expanded.size.w;
let src_y0 = fb_height - dst_y0;
let src_y1 = fb_height - dst_y1;
gl.BlitFramebuffer(
src_x0,
src_y0,
src_x1,
src_y1,
src_x0,
dst_y0,
src_x1,
dst_y1,
ffi::COLOR_BUFFER_BIT,
ffi::LINEAR,
);
}
gl.BlitFramebuffer(
src_x0,
src_y0,
src_x1,
src_y1,
dst_x0,
dst_y0,
dst_x1,
dst_y1,
ffi::COLOR_BUFFER_BIT,
ffi::LINEAR,
);
if gl.GetError() == ffi::INVALID_OPERATION {
error!("TrueBlur needs GLES3.0 for blitting");

View File

@@ -16,12 +16,24 @@ use smithay::backend::renderer::Texture;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::blur::EffectsFramebufffersUserData;
use crate::render_helpers::render_data::RendererData;
use crate::render_helpers::renderer::AsGlesFrame;
use crate::render_helpers::shaders::{mat3_uniform, Shaders};
use super::{CurrentBuffer, EffectsFramebuffers};
#[derive(Debug, Clone)]
enum BlurVariant {
Optimized {
texture: GlesTexture,
},
True {
fx_buffers: EffectsFramebufffersUserData,
config: niri_config::Blur,
},
}
/// Used for tracking commit counters of a collection of elements.
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct CommitTracker(HashMap<Id, CommitCounter>);
@@ -97,27 +109,48 @@ impl Blur {
#[allow(clippy::too_many_arguments)]
pub fn render(
&self,
fx_buffers: &EffectsFramebuffers,
fx_buffers: EffectsFramebufffersUserData,
sample_area: Rectangle<i32, Logical>,
corner_radius: CornerRadius,
scale: f64,
geometry: Rectangle<f64, Logical>,
mut true_blur: bool,
render_loc: Point<f64, Logical>,
) -> Option<BlurRenderElement> {
if !self.config.on {
return None;
}
// FIXME: true blur is broken on 90/270 transformed monitors
if !matches!(
fx_buffers.borrow().transform(),
Transform::Normal | Transform::Flipped180,
) {
true_blur = false;
}
let mut inner = self.inner.borrow_mut();
let Some(inner) = inner.as_mut() else {
let elem = BlurRenderElement::new(
fx_buffers,
&fx_buffers.borrow(),
sample_area,
corner_radius,
scale,
self.config,
geometry,
self.alpha_tex.borrow().clone(),
if true_blur {
BlurVariant::True {
fx_buffers: fx_buffers.clone(),
config: self.config,
}
} else {
BlurVariant::Optimized {
texture: fx_buffers.borrow().optimized_blur.clone(),
}
},
render_loc,
);
*inner = Some(elem.clone());
@@ -125,23 +158,48 @@ impl Blur {
return Some(elem);
};
if true_blur != matches!(&inner.variant, BlurVariant::True { .. }) {
inner.variant = if true_blur {
BlurVariant::True {
fx_buffers: fx_buffers.clone(),
config: self.config,
}
} else {
BlurVariant::Optimized {
texture: fx_buffers.borrow().optimized_blur.clone(),
}
};
inner.damage_all();
}
let fx_buffers = fx_buffers.borrow();
if inner.sample_area == sample_area
&& inner.geometry == geometry
&& inner.scale == scale
&& inner.corner_radius == corner_radius
&& fx_buffers.output_size().w == inner.texture.size().w
&& fx_buffers.output_size().h == inner.texture.size().h
&& inner.render_loc == render_loc
{
if !matches!(&inner.variant, BlurVariant::Optimized { texture }
if texture.size().w == fx_buffers.output_size().w
&& texture.size().h == fx_buffers.output_size().h)
{
// If we are true blur, or if our output size changed, we need to re-render.
// PERF: is there a better solution for true blur?
inner.damage_all();
}
return Some(inner.clone());
}
inner.texture = fx_buffers.optimized_blur.clone();
inner.render_loc = render_loc;
inner.sample_area = sample_area;
inner.alpha_tex = self.alpha_tex.borrow().clone();
inner.scale = scale;
inner.geometry = geometry;
inner.damage_all();
inner.update_uniforms(fx_buffers, &self.config);
inner.update_uniforms(&fx_buffers, &self.config);
Some(inner.clone())
}
@@ -150,7 +208,6 @@ impl Blur {
#[derive(Clone, Debug)]
pub struct BlurRenderElement {
id: Id,
texture: GlesTexture,
uniforms: Vec<Uniform<'static>>,
sample_area: Rectangle<i32, Logical>,
alpha_tex: Option<GlesTexture>,
@@ -158,6 +215,8 @@ pub struct BlurRenderElement {
commit: CommitCounter,
corner_radius: CornerRadius,
geometry: Rectangle<f64, Logical>,
variant: BlurVariant,
render_loc: Point<f64, Logical>,
}
impl BlurRenderElement {
@@ -170,7 +229,7 @@ impl BlurRenderElement {
/// - Display outdated/wrong contents
/// - Not display anything since the buffer will be empty.
#[allow(clippy::too_many_arguments)]
pub fn new(
fn new(
fx_buffers: &EffectsFramebuffers,
sample_area: Rectangle<i32, Logical>,
corner_radius: CornerRadius,
@@ -178,10 +237,11 @@ impl BlurRenderElement {
config: niri_config::Blur,
geometry: Rectangle<f64, Logical>,
alpha_tex: Option<GlesTexture>,
variant: BlurVariant,
render_loc: Point<f64, Logical>,
) -> Self {
let mut this = Self {
id: Id::new(),
texture: fx_buffers.optimized_blur.clone(),
uniforms: Vec::with_capacity(7),
alpha_tex,
sample_area,
@@ -189,6 +249,8 @@ impl BlurRenderElement {
corner_radius,
geometry,
commit: CommitCounter::default(),
variant,
render_loc,
};
this.update_uniforms(fx_buffers, &config);
@@ -270,7 +332,7 @@ impl Element for BlurRenderElement {
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
if self.alpha_tex.is_some() {
if self.alpha_tex.is_some() || matches!(&self.variant, BlurVariant::True { .. }) {
return OpaqueRegions::default();
}
@@ -296,7 +358,13 @@ impl Element for BlurRenderElement {
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.sample_area.to_f64().to_physical_precise_round(scale)
Rectangle::new(
self.render_loc.to_physical_precise_round(scale),
self.sample_area
.to_f64()
.to_physical_precise_round(scale)
.size,
)
}
fn alpha(&self) -> f32 {
@@ -308,97 +376,6 @@ impl Element for BlurRenderElement {
}
}
#[allow(unused)]
#[allow(clippy::too_many_arguments)]
fn draw_true_blur(
fx_buffers: &mut EffectsFramebuffers,
gles_frame: &mut GlesFrame,
config: &niri_config::Blur,
scale: f64,
dst: Rectangle<i32, Physical>,
corner_radius: f32,
src: Rectangle<f64, Buffer>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
alpha: f32,
is_tty: bool,
alpha_tex: Option<&GlesTexture>,
) -> Result<(), GlesError> {
fx_buffers.current_buffer = CurrentBuffer::Normal;
let shaders = Shaders::get_from_frame(gles_frame).blur.clone();
let vbos = RendererData::get_from_frame(gles_frame).vbos;
let supports_instancing = gles_frame
.capabilities()
.contains(&smithay::backend::renderer::gles::Capability::Instancing);
let debug = !gles_frame.debug_flags().is_empty();
let projection_matrix = glam::Mat3::from_cols_array(gles_frame.projection());
// Update the blur buffers.
// We use gl ffi directly to circumvent some stuff done by smithay
let blurred_texture = gles_frame.with_context(|gl| unsafe {
super::get_main_buffer_blur(
gl,
&mut *fx_buffers,
&shaders,
*config,
projection_matrix,
scale as i32,
&vbos,
debug,
supports_instancing,
dst,
is_tty,
alpha_tex,
)
})??;
let program = Shaders::get_from_frame(gles_frame).blur_finish.clone();
let additional_uniforms = vec![
Uniform::new(
"geo",
[
dst.loc.x as f32,
dst.loc.y as f32,
dst.size.w as f32,
dst.size.h as f32,
],
),
Uniform::new("alpha", alpha),
Uniform::new("noise", config.noise.0 as f32),
Uniform::new("corner_radius", corner_radius),
Uniform::new(
"output_size",
[
fx_buffers.output_size.w as f32,
fx_buffers.output_size.h as f32,
],
),
Uniform::new(
"ignore_alpha",
if alpha_tex.is_some() {
config.ignore_alpha.0 as f32
} else {
0.
},
),
Uniform::new("alpha_tex", 1),
];
gles_frame.render_texture_from_to(
&blurred_texture,
src,
dst,
damage,
opaque_regions,
Transform::Normal,
alpha,
program.as_ref(),
&additional_uniforms,
)
}
impl RenderElement<GlesRenderer> for BlurRenderElement {
fn draw(
&self,
@@ -432,17 +409,62 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
})?;
}
gles_frame.render_texture_from_to(
&self.texture,
src,
downscaled_dst,
damage,
opaque_regions,
Transform::Normal,
1.0,
Some(&program),
&self.uniforms,
)
match &self.variant {
BlurVariant::Optimized { texture } => gles_frame.render_texture_from_to(
texture,
src,
downscaled_dst,
damage,
opaque_regions,
Transform::Normal,
1.,
Some(&program),
&self.uniforms,
),
BlurVariant::True { fx_buffers, config } => {
let mut fx_buffers = fx_buffers.borrow_mut();
fx_buffers.current_buffer = CurrentBuffer::Normal;
let shaders = Shaders::get_from_frame(gles_frame).blur.clone();
let vbos = RendererData::get_from_frame(gles_frame).vbos;
let supports_instancing = gles_frame
.capabilities()
.contains(&smithay::backend::renderer::gles::Capability::Instancing);
let debug = !gles_frame.debug_flags().is_empty();
let projection_matrix = glam::Mat3::from_cols_array(gles_frame.projection());
// Update the blur buffers.
// We use gl ffi directly to circumvent some stuff done by smithay
let blurred_texture = gles_frame.with_context(|gl| unsafe {
super::get_main_buffer_blur(
gl,
&mut fx_buffers,
&shaders,
*config,
projection_matrix,
self.scale as i32,
&vbos,
debug,
supports_instancing,
downscaled_dst,
self.alpha_tex.as_ref(),
)
})??;
gles_frame.render_texture_from_to(
&blurred_texture,
src,
downscaled_dst,
damage,
opaque_regions,
fx_buffers.transform(),
1.,
Some(&program),
&self.uniforms,
)
}
}
}
fn underlying_storage(&self, _: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {