Implement non-xray background effects

This commit is contained in:
Ivan Molodetskikh
2026-03-14 07:45:06 +03:00
parent 19866f8b0b
commit 66d66d6030
7 changed files with 528 additions and 19 deletions
+11
View File
@@ -51,3 +51,14 @@ Xray is automatically enabled by default if any other background effect (like bl
This is because it's much more efficient: with xray active, niri only needs to blur the background once, and then can reuse this blurred version with no extra work (since the wallpaper changes very rarely).
If you have an animated wallpaper, xray will still have to recompute blur every frame, but that happens once and shared among all windows, rather than recomputed separately for each window.
#### Non-xray effects (experimental)
You can disable xray with `xray false` background effect window rule.
This gives you the normal kind of blur where everything below a window is blurred.
Keep in mind that non-xray blur and other non-xray effects are more expensive as niri has to recompute them any time you move the window, or the contents underneath change.
Non-xray effects are currently experimental because they have some known limitations.
- They disappear during window open/close animations and while dragging a tiled window.
Fixing this requries a refactor to the niri rendering code to defer offscreen rendering, and possibly other refactors.
+2
View File
@@ -187,6 +187,7 @@ impl MappedLayer {
pub fn render_normal<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
ns: Option<usize>,
location: Point<f64, Logical>,
xray_pos: XrayPos,
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
@@ -238,6 +239,7 @@ impl MappedLayer {
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
background_effect::render_for_tile(
ctx.as_gles(),
ns,
geometry,
self.scale,
false,
+40 -18
View File
@@ -4271,33 +4271,41 @@ impl Niri {
// We use macros instead of closures to avoid borrowing issues (renderer and push() go
// into different functions).
macro_rules! push_popups_from_layer {
($layer:expr, $backdrop:expr, $push:expr) => {{
self.render_layer_popups(ctx.r(), &layer_map, $layer, $backdrop, $push);
($layer:expr, $ns:expr, $backdrop:expr, $push:expr) => {{
self.render_layer_popups(ctx.r(), $ns, &layer_map, $layer, $backdrop, $push);
}};
($layer:expr, true) => {{
push_popups_from_layer!($layer, true, &mut |elem| push(elem.into()));
push_popups_from_layer!($layer, None, true, &mut |elem| push(elem.into()));
}};
($layer:expr, $push:expr) => {{
push_popups_from_layer!($layer, false, $push);
($layer:expr, $ns:expr, $push:expr) => {{
push_popups_from_layer!($layer, $ns, false, $push);
}};
($layer:expr) => {{
push_popups_from_layer!($layer, false, &mut |elem| push(elem.into()));
push_popups_from_layer!($layer, None, false, &mut |elem| push(elem.into()));
}};
}
macro_rules! push_normal_from_layer {
($layer:expr, $xray_pos:expr, $backdrop:expr, $push:expr) => {{
self.render_layer_normal(ctx.r(), &layer_map, $layer, $xray_pos, $backdrop, $push);
($layer:expr, $ns:expr, $xray_pos:expr, $backdrop:expr, $push:expr) => {{
self.render_layer_normal(
ctx.r(),
$ns,
&layer_map,
$layer,
$xray_pos,
$backdrop,
$push,
);
}};
($layer:expr, true) => {{
push_normal_from_layer!($layer, XrayPos::default(), true, &mut |elem| {
push_normal_from_layer!($layer, None, XrayPos::default(), true, &mut |elem| {
push(elem.into())
});
}};
($layer:expr, $xray_pos:expr, $push:expr) => {{
push_normal_from_layer!($layer, $xray_pos, false, $push);
($layer:expr, $ns:expr, $xray_pos:expr, $push:expr) => {{
push_normal_from_layer!($layer, $ns, $xray_pos, false, $push);
}};
($layer:expr) => {{
push_normal_from_layer!($layer, XrayPos::default(), false, &mut |elem| {
push_normal_from_layer!($layer, None, XrayPos::default(), false, &mut |elem| {
push(elem.into())
});
}};
@@ -4349,17 +4357,27 @@ impl Niri {
}};
}
for (_ws, geo) in mon.workspaces_with_render_geo() {
push_popups_from_layer!(Layer::Bottom, process!(geo));
push_popups_from_layer!(Layer::Background, process!(geo));
for (ws, geo) in mon.workspaces_with_render_geo() {
let ns = Some(ws.id().get() as usize);
push_popups_from_layer!(Layer::Bottom, ns, process!(geo));
push_popups_from_layer!(Layer::Background, ns, process!(geo));
}
mon.render_workspaces(ctx.r(), focus_ring, &mut |elem| push(elem.into()));
for (ws, geo) in mon.workspaces_with_render_geo() {
// The render element namespace. This will be set to the workspace index for
// elements duplicated across workspaces (i.e. background and bottom layers) in
// order to have their non-xray framebuffer effects separated from each other.
//
// This doesn't have to correspond exactly to workspace id or idx, the only
// requirement is that there's only one framebuffer effect element with a given id +
// namespace on the frame at once. Id + namespace is used as the cache key in the
// damage tracker.
let ns = Some(ws.id().get() as usize);
let xray_pos = XrayPos::new(geo.loc, zoom);
push_normal_from_layer!(Layer::Bottom, xray_pos, process!(geo));
push_normal_from_layer!(Layer::Background, xray_pos, process!(geo));
push_normal_from_layer!(Layer::Bottom, ns, xray_pos, process!(geo));
push_normal_from_layer!(Layer::Background, ns, xray_pos, process!(geo));
process!(geo)(ws.render_background());
}
@@ -4402,6 +4420,7 @@ impl Niri {
elements.clear();
self.render_layer_normal(
ctx.r(),
None,
&layer_map,
Layer::Background,
XrayPos::default(),
@@ -4418,6 +4437,7 @@ impl Niri {
elements.clear();
self.render_layer_normal(
ctx.r(),
None,
&layer_map,
Layer::Background,
XrayPos::default(),
@@ -4481,6 +4501,7 @@ impl Niri {
fn render_layer_normal<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
ns: Option<usize>,
layer_map: &LayerMap,
layer: Layer,
xray_pos: XrayPos,
@@ -4490,13 +4511,14 @@ impl Niri {
for (mapped, geo) in self.layers_in_render_order(layer_map, layer, for_backdrop) {
let loc = geo.loc.to_f64();
let xray_pos = xray_pos.offset(loc);
mapped.render_normal(ctx.r(), loc, xray_pos, push);
mapped.render_normal(ctx.r(), ns, loc, xray_pos, push);
}
}
fn render_layer_popups<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
_ns: Option<usize>,
layer_map: &LayerMap,
layer: Layer,
for_backdrop: bool,
+16 -1
View File
@@ -8,7 +8,9 @@ use wayland_server::protocol::wl_surface::WlSurface;
use crate::handlers::background_effect::get_cached_blur_region;
use crate::niri_render_elements;
use crate::render_helpers::blur::BlurOptions;
use crate::render_helpers::damage::ExtraDamage;
use crate::render_helpers::framebuffer_effect::{FramebufferEffect, FramebufferEffectElement};
use crate::render_helpers::xray::{XrayElement, XrayPos};
use crate::render_helpers::RenderCtx;
use crate::utils::region::TransformedRegion;
@@ -16,6 +18,7 @@ use crate::utils::surface_geo;
#[derive(Debug)]
pub struct BackgroundEffect {
nonxray: FramebufferEffect,
/// Damage when options change.
damage: ExtraDamage,
/// Corner radius for clipping.
@@ -72,6 +75,7 @@ impl RenderParams {
niri_render_elements! {
BackgroundEffectElement => {
FramebufferEffect = FramebufferEffectElement,
Xray = XrayElement,
ExtraDamage = ExtraDamage,
}
@@ -80,6 +84,7 @@ niri_render_elements! {
impl BackgroundEffect {
pub fn new() -> Self {
Self {
nonxray: FramebufferEffect::new(),
damage: ExtraDamage::new(),
corner_radius: CornerRadius::default(),
blur_config: niri_config::Blur::default(),
@@ -90,6 +95,7 @@ impl BackgroundEffect {
/// Damage the background effect, for example when a blur subregion changes.
pub fn damage(&mut self) {
self.damage.damage_all();
self.nonxray.damage();
}
pub fn update_config(&mut self, config: niri_config::Blur) {
@@ -99,6 +105,7 @@ impl BackgroundEffect {
self.blur_config = config;
self.damage.damage_all();
self.nonxray.damage();
}
pub fn update_render_elements(
@@ -134,6 +141,7 @@ impl BackgroundEffect {
self.options = options;
self.corner_radius = corner_radius;
self.damage.damage_all();
self.nonxray.damage();
}
pub fn is_visible(&self) -> bool {
@@ -143,6 +151,7 @@ impl BackgroundEffect {
pub fn render(
&self,
ctx: RenderCtx<GlesRenderer>,
ns: Option<usize>,
mut params: RenderParams,
xray_pos: XrayPos,
push: &mut dyn FnMut(BackgroundEffectElement),
@@ -161,6 +170,7 @@ impl BackgroundEffect {
// Use noise/saturation from options, falling back to blur defaults if blurred, and
// to no effect if not blurred.
let blur = self.options.blur && !self.blur_config.off;
let blur_options = blur.then_some(BlurOptions::from(self.blur_config));
let noise = if blur { self.blur_config.noise } else { 0. };
let noise = self.options.noise.unwrap_or(noise) as f32;
let saturation = if blur {
@@ -187,6 +197,10 @@ impl BackgroundEffect {
);
} else {
// Render non-xray effect.
let elem = self
.nonxray
.render(ns, params, blur_options, noise, saturation);
push(elem.into());
}
}
}
@@ -269,6 +283,7 @@ pub fn damage_surface(states: &SurfaceData) {
#[allow(clippy::too_many_arguments)]
pub fn render_for_tile(
ctx: RenderCtx<GlesRenderer>,
ns: Option<usize>,
geometry: Rectangle<f64, Logical>,
scale: f64,
clip_to_geometry: bool,
@@ -312,6 +327,6 @@ pub fn render_for_tile(
};
let xray_pos = xray_pos.offset(params.geometry.loc - geometry.loc);
background_effect.render(ctx, params, xray_pos, push);
background_effect.render(ctx, ns, params, xray_pos, push);
});
}
+457
View File
@@ -0,0 +1,457 @@
use std::cell::RefCell;
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Id, RenderElement};
use smithay::backend::renderer::gles::{
ffi, GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform,
};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{Frame as _, FrameContext, Offscreen, Texture as _};
use smithay::gpu_span_location;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::background_effect::RenderParams;
use crate::render_helpers::blur::{Blur, BlurOptions};
use crate::render_helpers::renderer::AsGlesFrame as _;
use crate::render_helpers::shaders::{mat3_uniform, Shaders};
use crate::utils::region::TransformedRegion;
#[derive(Debug)]
pub struct FramebufferEffect {
id: Id,
commit: CommitCounter,
}
#[derive(Debug)]
pub struct FramebufferEffectElement {
id: Id,
commit: CommitCounter,
geometry: Rectangle<f64, Logical>,
clip_geo: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
subregion: Option<TransformedRegion>,
scale: f32,
blur_options: Option<BlurOptions>,
noise: f32,
saturation: f32,
}
#[derive(Debug)]
struct Inner {
framebuffer: Option<GlesTexture>,
blur: Option<Blur>,
intermediate: Option<GlesTexture>,
/// Reusable storage for subregion-filtered damage rects.
subregion_damage: Vec<Rectangle<i32, Physical>>,
}
impl FramebufferEffect {
pub fn new() -> Self {
Self {
id: Id::new(),
commit: CommitCounter::default(),
}
}
pub fn damage(&mut self) {
self.commit.increment();
}
pub fn render(
&self,
ns: Option<usize>,
params: RenderParams,
blur_options: Option<BlurOptions>,
noise: f32,
saturation: f32,
) -> FramebufferEffectElement {
let (clip_geo, corner_radius) = params
.clip
.unwrap_or((params.geometry, CornerRadius::default()));
let mut id = self.id.clone();
if let Some(ns) = ns {
id = id.namespaced(ns);
}
FramebufferEffectElement {
id,
commit: self.commit,
geometry: params.geometry,
clip_geo,
corner_radius,
subregion: params.subregion,
scale: params.scale as f32,
blur_options,
noise,
saturation,
}
}
}
impl FramebufferEffectElement {
fn compute_uniforms(
&self,
crop: Rectangle<f64, Logical>,
transform: Transform,
) -> [Uniform<'static>; 7] {
let offset = crop.loc - (self.clip_geo.loc - self.geometry.loc);
let offset = Vec2::new(offset.x as f32, offset.y as f32);
let crop_size = Vec2::new(crop.size.w as f32, crop.size.h as f32);
let clip_size = Vec2::new(self.clip_geo.size.w as f32, self.clip_geo.size.h as f32);
// Our v_coords are [0, 1] inside crop. We want them to be [0, 1] inside clip_geo.
let input_to_clip_geo =
Mat3::from_scale(crop_size / clip_size) * Mat3::from_translation(offset / crop_size);
// Revert the effect of the texture transform.
let transform_mat = Mat3::from_translation(Vec2::new(0.5, 0.5))
* Mat3::from_cols_array(transform.matrix().as_ref())
* Mat3::from_translation(Vec2::new(-0.5, -0.5));
let input_to_clip_geo = input_to_clip_geo * transform_mat;
let clip_geo_size = (self.clip_geo.size.w as f32, self.clip_geo.size.h as f32);
[
Uniform::new("niri_scale", self.scale),
Uniform::new("geo_size", clip_geo_size),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", input_to_clip_geo),
Uniform::new("noise", self.noise),
Uniform::new("saturation", self.saturation),
Uniform::new("bg_color", [0f32, 0., 0., 0.]),
]
}
}
impl Element for FramebufferEffectElement {
fn id(&self) -> &Id {
&self.id
}
fn current_commit(&self) -> CommitCounter {
self.commit
}
fn src(&self) -> Rectangle<f64, Buffer> {
// We don't use src for drawing but we can use it to figure out how we were cropped.
let size = self.geometry.size.to_buffer(1., Transform::Normal);
Rectangle::from_size(size)
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry.to_physical_precise_round(scale)
}
fn is_framebuffer_effect(&self) -> bool {
true
}
}
impl RenderElement<GlesRenderer> for FramebufferEffectElement {
fn capture_framebuffer(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
cache: &UserDataMap,
) -> Result<(), GlesError> {
let _span = tracy_client::span!("FramebufferEffectElement::capture_framebuffer");
let location = gpu_span_location!("FramebufferEffectElement::capture_framebuffer");
frame.with_gpu_span(location, |frame| {
let output_rect = Rectangle::from_size(frame.output_size());
let transform = frame.transformation();
let mut guard = frame.renderer();
let inner = cache
.get_or_insert::<RefCell<Inner>, _>(|| RefCell::new(Inner::new(guard.as_mut())));
let mut inner = inner.borrow_mut();
let inner = &mut *inner;
inner.intermediate = None;
// We want clamp-to-edge behavior for out-of-bounds pixels. However, glBlitFramebuffer
// seems to skip out-of-bounds pixels, even though my reading of the docs suggests
// otherwise (we use GL_LINEAR filter). So, clamp dst to the framebuffer bounds
// ourselves.
let clamped_dst = match dst.intersection(output_rect) {
Some(clamped) => clamped,
None => return Ok(()),
};
let clamp_scale = clamped_dst.size.to_f64() / dst.size.to_f64();
let dst = transform.transform_rect_in(clamped_dst, &output_rect.size);
// Compute size from our geometry and scale.
//
// The "correct" size is always dst.size since that's the pixel region we're actually
// blitting. However, using dst.size causes two undesirable things when zooming out for
// the overview:
// 1. dst.size shrinks every frame, causing a texture realloaction for every fb effect
// element every frame.
// 2. The underlying blur visually expands. This is technically correct, since the
// underlying contents shrink, but it's not what you visually expect: you expect the
// blur to also shrink as the windows zoom out, to give the zooming out effect.
//
// Using size computed from geometry and scale solves both of those problems (even
// though there's a bit of a cost in that zoomed-out elements still blur the entire
// unzoomed texture size, and even though the blur ends up slightly wrong as there's two
// layers of texture resampling, up and back down).
//
// Here we use src.size rather than geometry directly because src takes into account
// cropping.
let size = src
.size
.to_logical(1., Transform::Normal)
.upscale(clamp_scale)
.to_physical_precise_round(self.scale);
let size = transform.transform_size(size);
let size = size.to_logical(1).to_buffer(1, Transform::Normal);
// Recreate framebuffer if needed.
if inner
.framebuffer
.as_ref()
.is_some_and(|fb| fb.size() != size)
{
inner.framebuffer = None;
}
let framebuffer = if let Some(fb) = &inner.framebuffer {
fb
} else {
trace!("creating framebuffer texture sized {} × {}", size.w, size.h);
let renderer = guard.as_mut();
let texture = renderer.create_buffer(Fourcc::Abgr8888, size)?;
inner.framebuffer.insert(texture)
};
// Prepare blur textures.
let mut blur = Option::zip(inner.blur.as_mut(), self.blur_options);
if let Some((b, options)) = &mut blur {
let renderer = guard.as_mut();
if let Err(err) = b.prepare_textures(
|fourcc, size| renderer.create_buffer(fourcc, size),
framebuffer,
*options,
) {
warn!("error preparing blur textures: {err:?}");
blur = None;
}
}
// We can't use renderer.with_context() as that will reset the GlesFrame binding that we
// want to blit from.
drop(guard);
// Blit the framebuffer contents.
frame.with_context(|gl| unsafe {
while gl.GetError() != ffi::NO_ERROR {}
let mut current_fbo = 0i32;
gl.GetIntegerv(ffi::DRAW_FRAMEBUFFER_BINDING, &mut current_fbo as *mut _);
// BlitFramebuffer is affected by the scissor test, we don't want that.
gl.Disable(ffi::SCISSOR_TEST);
let mut fbo = 0;
gl.GenFramebuffers(1, &mut fbo as *mut _);
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbo);
gl.FramebufferTexture2D(
ffi::DRAW_FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
framebuffer.tex_id(),
0,
);
gl.BlitFramebuffer(
dst.loc.x,
dst.loc.y,
dst.loc.x + dst.size.w,
dst.loc.y + dst.size.h,
0,
0,
size.w,
size.h,
ffi::COLOR_BUFFER_BIT,
ffi::LINEAR,
);
// Restore state set by GlesFrame that we just modified.
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, current_fbo as u32);
gl.Enable(ffi::SCISSOR_TEST);
gl.DeleteFramebuffers(1, &mut fbo as *mut _);
if gl.GetError() != ffi::NO_ERROR {
Err(GlesError::BlitError)
} else {
Ok(())
}
})??;
// If blur is off, use the unblurred texture.
if self.blur_options.is_none() {
inner.intermediate = Some(framebuffer.clone());
return Ok(());
}
if let Some((blur, options)) = blur {
let mut guard = frame.renderer();
let renderer = guard.as_mut();
match blur.render(renderer, framebuffer, options) {
Ok(blurred) => inner.intermediate = Some(blurred),
Err(err) => {
warn!("error rendering blur: {err:?}");
}
}
}
Ok(())
})
}
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
let Some(cache) = cache else {
return Ok(());
};
let Some(inner) = cache.get::<RefCell<Inner>>() else {
return Ok(());
};
let mut inner = inner.borrow_mut();
let inner = &mut *inner;
let Some(texture) = &inner.intermediate else {
return Ok(());
};
// Clamp the same way as in capture_framebuffer().
let output_rect = Rectangle::from_size(frame.output_size());
let clamped_dst = match dst.intersection(output_rect) {
Some(clamped) => clamped,
None => return Ok(()),
};
let clamp_offset = clamped_dst.loc - dst.loc;
// Filter damage by subregion, reusing the stored Vec to avoid allocation.
let filtered = &mut inner.subregion_damage;
filtered.clear();
if let Some(subregion) = &self.subregion {
// Convert to subregion coordinates.
let mut crop = src.to_logical(1., Transform::Normal, &src.size);
crop.loc += self.geometry.loc;
subregion.filter_damage(crop, dst, damage, filtered);
} else {
filtered.extend(damage.iter());
};
// Adjust for clamped dst.
if clamped_dst != dst {
let r = Rectangle::new(clamp_offset, clamped_dst.size);
filtered.retain_mut(|d| {
if let Some(mut crop) = d.intersection(r) {
crop.loc -= clamp_offset;
*d = crop;
true
} else {
false
}
});
}
if filtered.is_empty() {
return Ok(());
}
let damage = &filtered[..];
// Adjust src proportionally to the dst clamping.
let src_loc = src.loc.to_logical(1., Transform::Normal, &src.size);
let dst_to_src = src.size / dst.size.to_f64();
let crop = Rectangle::new(
src_loc + clamp_offset.to_f64().upscale(dst_to_src).to_logical(1.),
clamped_dst.size.to_f64().upscale(dst_to_src).to_logical(1.),
);
let program = Shaders::get_from_frame(frame).postprocess_and_clip.clone();
let uniforms = program
.is_some()
.then(|| self.compute_uniforms(crop, frame.transformation()));
let uniforms = uniforms.as_ref().map_or(&[][..], |x| &x[..]);
frame.render_texture_from_to(
texture,
Rectangle::from_size(texture.size().to_f64()),
clamped_dst,
damage,
&[],
// The intermediate texture has the same transform as the frame.
frame.transformation().invert(),
1.,
program.as_ref(),
uniforms,
)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for FramebufferEffectElement {
fn capture_framebuffer(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
cache: &UserDataMap,
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::capture_framebuffer(&self, gles_frame, src, dst, cache)?;
Ok(())
}
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(
&self,
gles_frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
Ok(())
}
}
impl Inner {
fn new(renderer: &mut GlesRenderer) -> Self {
Inner {
framebuffer: None,
blur: Blur::new(renderer),
intermediate: None,
subregion_damage: Vec::new(),
}
}
}
+1
View File
@@ -33,6 +33,7 @@ pub mod clipped_surface;
pub mod damage;
pub mod debug;
pub mod effect_buffer;
pub mod framebuffer_effect;
pub mod gradient_fade_texture;
pub mod memory;
pub mod offscreen;
+1
View File
@@ -697,6 +697,7 @@ impl LayoutElement for Mapped {
let should_block_out = ctx.target.should_block_out(self.rules.block_out_from);
background_effect::render_for_tile(
ctx,
None,
geometry,
scale,
clip_to_geometry,