perf(blur): limit true blur frame rate

This commit is contained in:
2025-11-30 13:29:37 +01:00
parent 758ee16b02
commit 6e61a3c100
5 changed files with 151 additions and 29 deletions

View File

@@ -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:

View File

@@ -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(),

View File

@@ -1931,6 +1931,7 @@ impl<W: LayoutElement> Tile<W> {
.and_then(|fx_buffers| {
self.blur
.render(
renderer.as_gles_renderer(),
fx_buffers,
blur_sample_area.to_i32_round(),
radius,

View File

@@ -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<Instant>,
optimized_blur_rerender_at: Option<Instant>,
// /// 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<i32, Physical>,
texture_cache: &GlesTexture,
alpha_tex: Option<&GlesTexture>,
) -> Result<GlesTexture, GlesError> {
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 _);

View File

@@ -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<RefCell<Option<Instant>>>,
},
}
@@ -109,6 +117,7 @@ impl Blur {
#[allow(clippy::too_many_arguments)]
pub fn render(
&self,
renderer: &mut GlesRenderer,
fx_buffers: EffectsFramebufffersUserData,
sample_area: Rectangle<i32, Logical>,
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<GlesRenderer> 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<GlesRenderer> 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,