From 6e61a3c100b8e797bea2b6fd73412e1e18cadee7 Mon Sep 17 00:00:00 2001 From: Naxdy Date: Sun, 30 Nov 2025 13:29:37 +0100 Subject: [PATCH] perf(blur): limit true blur frame rate --- README.md | 5 +- src/layer/mapped.rs | 1 + src/layout/tile.rs | 1 + src/render_helpers/blur.rs | 77 +++++++++++++++++++++++- src/render_helpers/blur/element.rs | 96 ++++++++++++++++++++++-------- 5 files changed, 151 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index e3036e281..1d1c260ed 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ Tiled windows will always draw "optimized" or "x-ray" blur, which is rendered fr `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. +Note that true blur is rather expensive in terms of GPU load however (even though its frame rate is limited as long as +the window isn't being resized / the layer surface doesn't update), so an option exists to also have these surfaces draw +`x-ray` blur instead. To set global defaults for blur: diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index 1b2d53232..2ddad0f9e 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -302,6 +302,7 @@ impl MappedLayer { Some( self.blur .render( + renderer.as_gles_renderer(), fx_buffers, blur_sample_area, self.rules.geometry_corner_radius.unwrap_or_default(), diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 42e7b4876..b920711ae 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -1931,6 +1931,7 @@ impl Tile { .and_then(|fx_buffers| { self.blur .render( + renderer.as_gles_renderer(), fx_buffers, blur_sample_area.to_i32_round(), radius, diff --git a/src/render_helpers/blur.rs b/src/render_helpers/blur.rs index 570efbe5e..57bc1281b 100644 --- a/src/render_helpers/blur.rs +++ b/src/render_helpers/blur.rs @@ -55,9 +55,9 @@ impl CurrentBuffer { #[derive(Debug)] pub struct EffectsFramebuffers { /// Contains the main buffer blurred contents - pub optimized_blur: GlesTexture, + optimized_blur: GlesTexture, /// Whether the optimizer blur buffer is dirty - pub optimized_blur_rerender_at: Option, + optimized_blur_rerender_at: Option, // /// Contains the original pixels before blurring to draw with in case of artifacts. // blur_saved_pixels: GlesTexture, // The blur algorithms (dual-kawase) swaps between these two whenever scaling the image @@ -317,6 +317,7 @@ pub(super) unsafe fn get_main_buffer_blur( supports_instancing: bool, // dst is the region that we want blur on dst: Rectangle, + texture_cache: &GlesTexture, alpha_tex: Option<&GlesTexture>, ) -> Result { let tex_size = fx_buffers @@ -463,6 +464,78 @@ pub(super) unsafe fn get_main_buffer_blur( } } + // Copy over cached texture + { + let mut tex_cache_fbo = 0; + + gl.GenFramebuffers(1, &mut tex_cache_fbo as *mut _); + gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, tex_cache_fbo); + gl.FramebufferTexture2D( + ffi::DRAW_FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::TEXTURE_2D, + texture_cache.tex_id(), + 0, + ); + gl.Clear(ffi::COLOR_BUFFER_BIT); + let status = gl.CheckFramebufferStatus(ffi::DRAW_FRAMEBUFFER); + if status != ffi::FRAMEBUFFER_COMPLETE { + gl.DeleteFramebuffers(1, &mut tex_cache_fbo as *mut _); + return Err(GlesError::FramebufferBindingError); + } + gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, tex_cache_fbo); + + let mut render_buffer_fbo = 0; + + gl.GenFramebuffers(1, &mut render_buffer_fbo as *mut _); + gl.BindFramebuffer(ffi::READ_FRAMEBUFFER, render_buffer_fbo); + gl.FramebufferTexture2D( + ffi::READ_FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::TEXTURE_2D, + fx_buffers.effects.tex_id(), + 0, + ); + gl.Clear(ffi::COLOR_BUFFER_BIT); + let status = gl.CheckFramebufferStatus(ffi::READ_FRAMEBUFFER); + if status != ffi::FRAMEBUFFER_COMPLETE { + gl.DeleteFramebuffers(1, &mut render_buffer_fbo as *mut _); + return Err(GlesError::FramebufferBindingError); + } + gl.BindFramebuffer(ffi::READ_FRAMEBUFFER, render_buffer_fbo); + + 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; + + let src_x0 = dst_x0; + let src_y0 = dst_y0; + let src_x1 = dst_x1; + let src_y1 = dst_y1; + + 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"); + return Err(GlesError::BlitError); + } + + gl.DeleteFramebuffers(1, &mut tex_cache_fbo as *mut _); + gl.DeleteFramebuffers(1, &mut render_buffer_fbo as *mut _); + } + // Cleanup { gl.DeleteFramebuffers(1, &mut sample_fbo as *mut _); diff --git a/src/render_helpers/blur/element.rs b/src/render_helpers/blur/element.rs index b4ad712d1..985741083 100644 --- a/src/render_helpers/blur/element.rs +++ b/src/render_helpers/blur/element.rs @@ -2,6 +2,8 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; +use std::time::Instant; use glam::{Mat3, Vec2}; use niri_config::CornerRadius; @@ -12,11 +14,12 @@ use smithay::backend::renderer::gles::{ ffi, GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform, }; use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions}; -use smithay::backend::renderer::Texture; +use smithay::backend::renderer::{Offscreen, Texture}; +use smithay::reexports::gbm::Format; 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::blur::{get_rerender_at, EffectsFramebufffersUserData}; use crate::render_helpers::render_data::RendererData; use crate::render_helpers::renderer::AsGlesFrame; use crate::render_helpers::shaders::{mat3_uniform, Shaders}; @@ -26,11 +29,16 @@ use super::{CurrentBuffer, EffectsFramebuffers}; #[derive(Debug, Clone)] enum BlurVariant { Optimized { + /// Reference to the globally cached optimized blur texture. texture: GlesTexture, }, True { + /// Individual cache of true blur texture. + texture: GlesTexture, fx_buffers: EffectsFramebufffersUserData, config: niri_config::Blur, + /// Timer to limit redraw rate of true blur. Currently set at 150ms fixed (~6.6 fps). + rerender_at: Rc>>, }, } @@ -109,6 +117,7 @@ impl Blur { #[allow(clippy::too_many_arguments)] pub fn render( &self, + renderer: &mut GlesRenderer, fx_buffers: EffectsFramebufffersUserData, sample_area: Rectangle, corner_radius: CornerRadius, @@ -129,6 +138,15 @@ impl Blur { true_blur = false; } + let mut tex_buffer = || { + renderer + .create_buffer(Format::Argb8888, fx_buffers.borrow().effects.size()) + .inspect_err(|e| { + warn!("failed to allocate buffer for cached true blur texture: {e:?}") + }) + .ok() + }; + let mut inner = self.inner.borrow_mut(); let Some(inner) = inner.as_mut() else { @@ -144,6 +162,8 @@ impl Blur { BlurVariant::True { fx_buffers: fx_buffers.clone(), config: self.config, + texture: tex_buffer()?, + rerender_at: Default::default(), } } else { BlurVariant::Optimized { @@ -163,6 +183,8 @@ impl Blur { BlurVariant::True { fx_buffers: fx_buffers.clone(), config: self.config, + texture: tex_buffer()?, + rerender_at: Default::default(), } } else { BlurVariant::Optimized { @@ -175,24 +197,34 @@ impl Blur { let fx_buffers = fx_buffers.borrow(); + let variant_needs_rerender = match &inner.variant { + BlurVariant::Optimized { texture } => { + texture.size().w != fx_buffers.output_size().w + || texture.size().h != fx_buffers.output_size().h + } + _ => true, + }; + + // if nothing about our geometry changed, we don't need to re-render blur if inner.sample_area == sample_area && inner.geometry == geometry && inner.scale == scale && inner.corner_radius == corner_radius && 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? + if variant_needs_rerender { + // If we are true blur, or if our output size changed, we need to always be + // damaged. True blur has a separate internal timer to regulate frame rate. inner.damage_all(); } return Some(inner.clone()); } + if let BlurVariant::True { rerender_at, .. } = &inner.variant { + rerender_at.set(None); + }; + inner.render_loc = render_loc; inner.sample_area = sample_area; inner.alpha_tex = self.alpha_tex.borrow().clone(); @@ -421,7 +453,12 @@ impl RenderElement for BlurRenderElement { Some(&program), &self.uniforms, ), - BlurVariant::True { fx_buffers, config } => { + BlurVariant::True { + fx_buffers, + config, + texture, + rerender_at, + } => { let mut fx_buffers = fx_buffers.borrow_mut(); fx_buffers.current_buffer = CurrentBuffer::Normal; @@ -436,24 +473,33 @@ impl RenderElement for BlurRenderElement { // 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(), - ) - })??; + if rerender_at + .borrow() + .map(|r| r < Instant::now()) + .unwrap_or(true) + { + 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, + texture, + self.alpha_tex.as_ref(), + ) + })??; + + rerender_at.set(get_rerender_at()); + }; gles_frame.render_texture_from_to( - &blurred_texture, + texture, src, downscaled_dst, damage,