Implement xray background effect

This commit is contained in:
Ivan Molodetskikh
2026-04-14 07:33:50 +00:00
parent 7f9c7d1415
commit fee8719299
34 changed files with 1485 additions and 82 deletions
+23
View File
@@ -1006,6 +1006,29 @@ where
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct BackgroundEffectRule {
#[knuffel(child, unwrap(argument))]
pub xray: Option<bool>,
}
/// Resolved background effect rule.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct BackgroundEffect {
/// Whether to render with xray effect (see through).
///
/// - `None`: xray if any background effect is active
/// - `Some(false)`: no xray
/// - `Some(true)`: xray even if no other background effect is active
pub xray: Option<bool>,
}
impl MergeWith<BackgroundEffectRule> for BackgroundEffect {
fn merge_with(&mut self, part: &BackgroundEffectRule) {
merge_clone_opt!((self, part), xray);
}
}
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, assert_snapshot};
+3 -1
View File
@@ -1,4 +1,4 @@
use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule};
use crate::appearance::{BackgroundEffectRule, BlockOutFrom, CornerRadius, ShadowRule};
use crate::utils::RegexEq;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -20,6 +20,8 @@ pub struct LayerRule {
pub place_within_backdrop: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
+6
View File
@@ -1845,6 +1845,9 @@ mod tests {
),
scroll_factor: None,
tiled_state: None,
background_effect: BackgroundEffectRule {
xray: None,
},
},
],
layer_rules: [
@@ -1880,6 +1883,9 @@ mod tests {
geometry_corner_radius: None,
place_within_backdrop: None,
baba_is_float: None,
background_effect: BackgroundEffectRule {
xray: None,
},
},
],
binds: Binds(
+5 -1
View File
@@ -1,6 +1,8 @@
use niri_ipc::ColumnDisplay;
use crate::appearance::{BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule};
use crate::appearance::{
BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule,
};
use crate::layout::DefaultPresetSize;
use crate::utils::RegexEq;
use crate::FloatOrInt;
@@ -72,6 +74,8 @@ pub struct WindowRule {
pub scroll_factor: Option<FloatOrInt<0, 100>>,
#[knuffel(child, unwrap(argument))]
pub tiled_state: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
+1
View File
@@ -273,6 +273,7 @@ impl TestCase for Layout {
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
self.layout
.monitor_for_output(&self.output)
+7 -3
View File
@@ -2,6 +2,7 @@ use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::xray::XrayPos;
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
@@ -124,10 +125,13 @@ impl TestCase for Tile {
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
self.tile.render(ctx, location, true, &mut |elem| {
rv.push(Box::new(elem) as _)
});
let xray_pos = XrayPos::new(location, 1.);
self.tile
.render(ctx, location, xray_pos, true, &mut |elem| {
rv.push(Box::new(elem) as _)
});
rv
}
}
+1
View File
@@ -56,6 +56,7 @@ impl TestCase for Window {
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
self.window
.render_normal(ctx, location, Scale::from(1.), 1., &mut |elem| {
+1
View File
@@ -1868,6 +1868,7 @@ impl Tty {
let ctx = RenderCtx {
renderer: &mut renderer,
target: RenderTarget::Output,
xray: None,
};
let mut elements = niri.render_to_vec(ctx, output, true);
+1
View File
@@ -185,6 +185,7 @@ impl Winit {
let ctx = RenderCtx {
renderer: self.backend.renderer(),
target: RenderTarget::Output,
xray: None,
};
let mut elements = niri.render_to_vec(ctx, output, true);
+3 -4
View File
@@ -486,11 +486,10 @@ impl CompositorHandler for State {
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
// gets most of the job done.
if let Some(root) = self.niri.root_surface.get(surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
let output = output.cloned();
self.store_unmap_snapshot(&window, output.as_ref());
}
}
+4 -7
View File
@@ -846,9 +846,7 @@ impl XdgShellHandler for State {
self.niri
.stop_casts_for_target(CastTarget::Window { id: id.get() });
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
self.store_unmap_snapshot(&window, output.as_ref());
let transaction = Transaction::new();
let blocker = transaction.blocker();
@@ -1445,7 +1443,7 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let span =
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
let Some((mapped, output)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
@@ -1547,9 +1545,8 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let window = mapped.window.clone();
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
state.niri.layout.store_unmap_snapshot(renderer, &window);
});
let output = output.cloned();
state.store_unmap_snapshot(&window, output.as_ref());
} else {
if animate {
state.backend.with_primary_renderer(|renderer| {
+1
View File
@@ -53,6 +53,7 @@ impl PickColorGrab {
renderer,
// This is an interactive operation so we can render without blocking out.
target: RenderTarget::Output,
xray: None,
};
let elements = data.niri.render_to_vec(ctx, &output, false);
+29 -6
View File
@@ -3,7 +3,7 @@ use niri_config::{Config, LayerRule};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::wayland::compositor::{remove_pre_commit_hook, HookId};
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
@@ -11,11 +11,13 @@ use super::ResolvedLayerRules;
use crate::animation::Clock;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::surface::push_elements_from_surface_tree;
use crate::render_helpers::RenderCtx;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::{background_effect, RenderCtx};
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
#[derive(Debug)]
@@ -55,6 +57,7 @@ niri_render_elements! {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
BackgroundEffect = BackgroundEffectElement,
}
}
@@ -177,15 +180,22 @@ impl MappedLayer {
pub fn render_normal<R: NiriRenderer>(
&self,
ctx: RenderCtx<R>,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
xray_pos: XrayPos,
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
) {
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
if ctx.target.should_block_out(self.rules.block_out_from) {
let bob_offset = self.bob_offset();
let location = location + bob_offset;
let xray_pos = xray_pos.offset(bob_offset);
let surface = self.surface.wl_surface();
let should_block_out = ctx.target.should_block_out(self.rules.block_out_from);
if should_block_out {
// Round to physical pixels.
let location = location.to_physical_precise_round(scale).to_logical(scale);
@@ -201,7 +211,6 @@ impl MappedLayer {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = location;
let surface = self.surface.wl_surface();
push_elements_from_surface_tree(
ctx.renderer,
surface,
@@ -216,6 +225,20 @@ impl MappedLayer {
let location = location.to_physical_precise_round(scale).to_logical(scale);
self.shadow
.render(ctx.renderer, location, &mut |elem| push(elem.into()));
let geometry = Rectangle::new(location, self.block_out_buffer.size());
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
background_effect::render_for_tile(
ctx.as_gles(),
geometry,
self.scale,
false,
surface,
radius,
self.rules.background_effect,
xray_pos,
&mut |elem| push(elem.into()),
);
}
pub fn render_popups<R: NiriRenderer>(
+8 -1
View File
@@ -1,6 +1,6 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::utils::MergeWith as _;
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
use niri_config::{BackgroundEffect, BlockOutFrom, CornerRadius, ShadowRule};
use smithay::desktop::LayerSurface;
use smithay::wayland::shell::wlr_layer::Layer;
@@ -27,6 +27,9 @@ pub struct ResolvedLayerRules {
/// Whether to bob this window up and down.
pub baba_is_float: bool,
/// Background effect configuration.
pub background_effect: BackgroundEffect,
}
impl ResolvedLayerRules {
@@ -71,6 +74,10 @@ impl ResolvedLayerRules {
}
resolved.shadow.merge_with(&rule.shadow);
resolved
.background_effect
.merge_with(&rule.background_effect);
}
resolved
+25 -1
View File
@@ -21,7 +21,7 @@ use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderCtx};
use crate::render_helpers::{render_to_encompassing_texture, RenderCtx, RenderTarget};
use crate::utils::transaction::TransactionBlocker;
#[derive(Debug)]
@@ -29,6 +29,12 @@ pub struct ClosingWindow {
/// Contents of the window.
buffer: TextureBuffer<GlesTexture>,
/// Contents that are not blocked out, but the background is blocked out.
///
/// If `None` then the background doesn't have any blocked-out surfaces, and normal `buffer`
/// can be used instead.
buffer_with_blocked_out_bg: Option<TextureBuffer<GlesTexture>>,
/// Blocked-out contents of the window.
blocked_out_buffer: TextureBuffer<GlesTexture>,
@@ -44,6 +50,9 @@ pub struct ClosingWindow {
/// How much the texture should be offset.
buffer_offset: Point<f64, Logical>,
/// How much the texture with blocked-out bg should be offset.
buffer_with_blocked_out_bg_offset: Point<f64, Logical>,
/// How much the blocked-out texture should be offset.
blocked_out_buffer_offset: Point<f64, Logical>,
@@ -121,17 +130,27 @@ impl ClosingWindow {
let (buffer, buffer_offset) =
render_to_texture(snapshot.contents).context("error rendering contents")?;
let (buffer_with_blocked_out_bg, buffer_with_blocked_out_bg_offset) =
if let Some(contents) = snapshot.contents_with_blocked_out_bg {
let (buffer, offset) = render_to_texture(contents)
.context("error rendering contents with blocked-out bg")?;
(Some(buffer), offset)
} else {
(None, Point::default())
};
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
buffer_with_blocked_out_bg,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
geo_size,
pos,
buffer_offset,
buffer_with_blocked_out_bg_offset,
blocked_out_buffer_offset,
anim_state: AnimationState::new(blocker, anim),
random_seed: fastrand::f32(),
@@ -165,6 +184,11 @@ impl ClosingWindow {
) -> ClosingWindowRenderElement {
let (buffer, offset) = if ctx.target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else if ctx.target != RenderTarget::Output && self.buffer_with_blocked_out_bg.is_some() {
(
self.buffer_with_blocked_out_bg.as_ref().unwrap(),
self.buffer_with_blocked_out_bg_offset,
)
} else {
(&self.buffer, self.buffer_offset)
};
+6 -1
View File
@@ -18,6 +18,7 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::utils::transaction::TransactionBlocker;
use crate::utils::{
@@ -1056,6 +1057,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
pub fn render<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
xray_pos: XrayPos,
view_rect: Rectangle<f64, Logical>,
focus_ring: bool,
push: &mut dyn FnMut(FloatingSpaceRenderElement<R>),
@@ -1075,7 +1077,10 @@ impl<W: LayoutElement> FloatingSpace<W> {
// For the active tile, draw the focus ring.
let focus_ring = focus_ring && Some(tile.window().id()) == active.as_ref();
tile.render(ctx.r(), tile_pos, focus_ring, &mut |elem| push(elem.into()));
let xray_pos = xray_pos.offset(tile_pos);
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
push(elem.into())
});
}
}
+66 -13
View File
@@ -59,11 +59,13 @@ use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::scrolling::ScrollDirection;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::offscreen::OffscreenData;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::texture::TextureBuffer;
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::{BakedBuffer, RenderCtx};
use crate::rubber_band::RubberBand;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -188,6 +190,20 @@ pub trait LayoutElement {
let _ = (ctx, location, scale, alpha, push);
}
/// Renders the background effect behind the main surface of the element.
#[allow(clippy::too_many_arguments)]
fn render_background_effect(
&self,
_ctx: RenderCtx<GlesRenderer>,
_geometry: Rectangle<f64, Logical>,
_scale: f64,
_clip_to_geometry: bool,
_radius: CornerRadius,
_xray_pos: XrayPos,
_push: &mut dyn FnMut(BackgroundEffectElement),
) {
}
/// Requests the element to change its size.
///
/// The size request is stored and will be continuously sent to the element on any further
@@ -4605,12 +4621,33 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
pub fn store_unmap_snapshot(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
window: &W::Id,
) {
let _span = tracy_client::span!("Layout::store_unmap_snapshot");
let zoom = self.overview_zoom();
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
move_.tile.store_unmap_snapshot_if_empty(renderer);
let pos_within_output = move_.tile_render_location(zoom);
// Computation matches update_render_elements().
let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output))
.downscale(zoom);
move_.tile.update_render_elements(false, view_rect);
move_.tile.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::new(pos_within_output, zoom),
);
return;
}
}
@@ -4618,9 +4655,15 @@ impl<W: LayoutElement> Layout<W> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for (ws, geo) in mon.workspaces_with_render_geo_mut(false) {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
ws.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::new(geo.loc, zoom),
window,
);
return;
}
}
@@ -4629,7 +4672,13 @@ impl<W: LayoutElement> Layout<W> {
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
ws.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::default(),
window,
);
return;
}
}
@@ -4748,14 +4797,18 @@ impl<W: LayoutElement> Layout<W> {
let scale = Scale::from(move_.output.current_scale().fractional_scale());
let zoom = self.overview_zoom();
let location = move_.tile_render_location(zoom);
move_.tile.render(ctx, location, true, &mut |elem| {
push(RescaleRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
zoom,
));
});
let pos_in_backdrop = move_.tile_render_location(zoom);
let xray_pos = XrayPos::new(pos_in_backdrop, zoom);
move_
.tile
.render(ctx, pos_in_backdrop, xray_pos, true, &mut |elem| {
push(RescaleRenderElement::from_element(
elem,
pos_in_backdrop.to_physical_precise_round(scale),
zoom,
));
});
}
pub fn refresh(&mut self, is_active: bool) {
+5 -2
View File
@@ -24,6 +24,7 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::SolidColorRenderElement;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
@@ -1733,7 +1734,9 @@ impl<W: LayoutElement> Monitor<W> {
}};
}
ws.render_floating(ctx.r(), focus_ring, push!());
let xray_pos = XrayPos::new(geo.loc, zoom);
ws.render_floating(ctx.r(), xray_pos, focus_ring, push!());
if let Some(loc) = insert_hint_render_loc {
if loc.workspace == InsertWorkspace::Existing(ws.id()) {
@@ -1742,7 +1745,7 @@ impl<W: LayoutElement> Monitor<W> {
}
}
ws.render_scrolling(ctx.r(), focus_ring, push!());
ws.render_scrolling(ctx.r(), xray_pos, focus_ring, push!());
}
}
+6 -1
View File
@@ -21,6 +21,7 @@ use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::ResizeEdge;
@@ -2900,6 +2901,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
pub fn render<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(ScrollingSpaceRenderElement<R>),
) {
@@ -2954,7 +2956,10 @@ impl<W: LayoutElement> ScrollingSpace<W> {
continue;
}
tile.render(ctx.r(), tile_pos, focus_ring, &mut |elem| push(elem.into()));
let xray_pos = xray_pos.offset(tile_pos);
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
push(elem.into())
});
}
}
}
+2
View File
@@ -116,10 +116,12 @@ impl TestWindow {
if self.0.animate_next_configure.get() {
self.0.animation_snapshot.replace(Some(RenderSnapshot {
contents: Vec::new(),
contents_with_blocked_out_bg: None,
blocked_out_contents: Vec::new(),
block_out_from: None,
size: self.0.bbox.get().size.to_f64(),
texture: OnceCell::new(),
texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: OnceCell::new(),
}));
}
+126 -13
View File
@@ -18,6 +18,7 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
@@ -27,6 +28,7 @@ use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::{RenderCtx, RenderTarget};
use crate::utils::transaction::Transaction;
use crate::utils::{
@@ -130,6 +132,7 @@ niri_render_elements! {
ClippedSurface = ClippedSurfaceRenderElement<R>,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
BackgroundEffect = BackgroundEffectElement,
}
}
@@ -1010,6 +1013,7 @@ impl<W: LayoutElement> Tile<W> {
&self,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
mut xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(TileRenderElement<R>),
) {
@@ -1037,13 +1041,16 @@ impl<W: LayoutElement> Tile<W> {
//
// This isn't to say that adding it here is perfect; indeed, it kind of breaks view_rect
// passed to update_render_elements(). But, it works well enough for what it is.
let location = location + self.bob_offset();
let bob_offset = self.bob_offset();
let location = location + bob_offset;
xray_pos = xray_pos.offset(bob_offset);
let window_loc = self.window_loc();
let window_size = self.window_size();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::new(window_render_loc, animated_window_size);
xray_pos = xray_pos.offset(window_loc);
let rules = self.window.rules();
@@ -1272,12 +1279,23 @@ impl<W: LayoutElement> Tile<W> {
self.shadow
.render(ctx.renderer, location, &mut |elem| push(elem.into()));
}
self.window.render_background_effect(
ctx.as_gles(),
area,
self.scale,
clip_to_geometry,
radius,
xray_pos,
&mut |elem| push(elem.into()),
);
}
pub fn render<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(TileRenderElement<R>),
) {
@@ -1296,9 +1314,13 @@ impl<W: LayoutElement> Tile<W> {
if let Some(open) = &self.open_animation {
let mut ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(ctx.r(), Point::from((0., 0.)), focus_ring, &mut |elem| {
elements.push(elem)
});
self.render_inner(
ctx.r(),
Point::new(0., 0.),
xray_pos,
focus_ring,
&mut |elem| elements.push(elem),
);
match open.render(
ctx.renderer,
&elements,
@@ -1319,9 +1341,13 @@ impl<W: LayoutElement> Tile<W> {
} else if let Some(alpha) = &self.alpha_animation {
let mut ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(ctx.r(), Point::from((0., 0.)), focus_ring, &mut |elem| {
elements.push(elem)
});
self.render_inner(
ctx.r(),
Point::new(0., 0.),
xray_pos,
focus_ring,
&mut |elem| elements.push(elem),
);
match alpha.offscreen.render(ctx.renderer, scale, &elements) {
Ok((elem, _sync, data)) => {
let offset = elem.offset();
@@ -1338,50 +1364,137 @@ impl<W: LayoutElement> Tile<W> {
}
if !pushed {
self.render_inner(ctx, location, focus_ring, &mut |elem| push(elem));
self.render_inner(ctx, location, xray_pos, focus_ring, &mut |elem| push(elem));
}
}
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
) {
if self.unmap_snapshot.is_some() {
return;
}
self.unmap_snapshot = Some(self.render_snapshot(renderer));
self.unmap_snapshot =
Some(self.render_snapshot(renderer, xray, xray_has_blocked_out_layers, xray_pos));
}
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
fn render_snapshot(
&self,
renderer: &mut GlesRenderer,
mut xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let mut contents = Vec::new();
self.render(
RenderCtx {
renderer,
target: RenderTarget::Output,
renderer,
xray: xray.as_deref(),
},
Point::from((0., 0.)),
xray_pos,
false,
&mut |elem| contents.push(elem),
);
let mut contents_with_blocked_out_bg = None;
// Do a bit of pointer surgery on Xray.
//
// The idea is to avoid the combinatorial combination of rendering snapshots for target
// (Output, Screencast) × Xray target (Output, Screencast, ScreenCapture).
//
// Our main goals:
// - Everything must look unblocked for RenderTarget::Output.
// - If anything is potentially blocked-out, it must not show up on any screen capture.
//
// Right above we rendered a fully-unblocked snapshot for the Output, so that's covered.
//
// Next, *only if Xray has any blocked-out surfaces* (which is a rare case), we will render
// a snapshot where the window itself is unblocked, but the Xray background is blocked. To
// do this, we swap the Output target buffers in Xray with the Screencast target buffers
// (which were prepared for us higher up the stack).
//
// Finally, we render a fully blocked-out snapshot. If Xray has blocked-out surfaces, then
// Xray's Screencast buffers are already filled-in, but if not, then we swap in the Output
// buffers, to avoid an extra render. This is safe since we know there are no blocked
// surfaces there.
let output_idx = RenderTarget::Output as usize;
let screencast_idx = RenderTarget::Screencast as usize;
let mut screencast_background = None;
let mut screencast_backdrop = None;
let mut output_background = None;
let mut output_backdrop = None;
if let Some(xray) = &mut xray {
screencast_background = Some(Rc::clone(&xray.background[screencast_idx]));
screencast_backdrop = Some(Rc::clone(&xray.backdrop[screencast_idx]));
output_background = Some(Rc::clone(&xray.background[output_idx]));
output_backdrop = Some(Rc::clone(&xray.backdrop[output_idx]));
if xray_has_blocked_out_layers {
xray.background[output_idx] = screencast_background.clone().unwrap();
xray.backdrop[output_idx] = screencast_backdrop.clone().unwrap();
let mut contents = Vec::new();
self.render(
RenderCtx {
target: RenderTarget::Output,
renderer,
xray: Some(xray),
},
Point::from((0., 0.)),
xray_pos,
false,
&mut |elem| contents.push(elem),
);
contents_with_blocked_out_bg = Some(contents);
} else {
xray.background[screencast_idx] = output_background.clone().unwrap();
xray.backdrop[screencast_idx] = output_backdrop.clone().unwrap();
}
}
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let mut blocked_out_contents = Vec::new();
self.render(
RenderCtx {
renderer,
target: RenderTarget::Screencast,
renderer,
xray: xray.as_deref(),
},
Point::from((0., 0.)),
xray_pos,
false,
&mut |elem| blocked_out_contents.push(elem),
);
// Put everything back to normal.
if let Some(xray) = &mut xray {
if xray_has_blocked_out_layers {
xray.background[output_idx] = output_background.take().unwrap();
xray.backdrop[output_idx] = output_backdrop.take().unwrap();
} else {
xray.background[screencast_idx] = screencast_background.take().unwrap();
xray.backdrop[screencast_idx] = screencast_backdrop.take().unwrap();
}
}
RenderSnapshot {
contents,
contents_with_blocked_out_bg,
blocked_out_contents,
block_out_from: self.window.rules().block_out_from,
size: self.animated_tile_size(),
texture: Default::default(),
texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: Default::default(),
}
}
+22 -4
View File
@@ -32,6 +32,7 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::RenderCtx;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -1627,17 +1628,21 @@ impl<W: LayoutElement> Workspace<W> {
pub fn render_scrolling<R: NiriRenderer>(
&self,
ctx: RenderCtx<R>,
xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
) {
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
self.scrolling
.render(ctx, scrolling_focus_ring, &mut |elem| push(elem.into()));
.render(ctx, xray_pos, scrolling_focus_ring, &mut |elem| {
push(elem.into())
});
}
pub fn render_floating<R: NiriRenderer>(
&self,
ctx: RenderCtx<R>,
xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
) {
@@ -1648,7 +1653,7 @@ impl<W: LayoutElement> Workspace<W> {
let view_rect = Rectangle::from_size(self.view_size);
let floating_focus_ring = focus_ring && self.floating_is_active();
self.floating
.render(ctx, view_rect, floating_focus_ring, &mut |elem| {
.render(ctx, xray_pos, view_rect, floating_focus_ring, &mut |elem| {
push(elem.into())
});
}
@@ -1682,14 +1687,27 @@ impl<W: LayoutElement> Workspace<W> {
) || !self.render_above_top_layer()
}
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
window: &W::Id,
) {
let view_size = self.view_size();
for (tile, tile_pos) in self.tiles_with_render_positions_mut(false) {
if tile.window().id() == window {
let view_pos = Point::from((-tile_pos.x, -tile_pos.y));
let view_rect = Rectangle::new(view_pos, view_size);
tile.update_render_elements(false, view_rect);
tile.store_unmap_snapshot_if_empty(renderer);
let xray_pos = xray_pos.offset(tile_pos);
tile.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
xray_pos,
);
return;
}
}
+280 -17
View File
@@ -38,7 +38,8 @@ use smithay::desktop::utils::{
bbox_from_surface_tree, output_update, send_dmabuf_feedback_surface_tree,
send_frames_surface_tree, surface_presentation_feedback_flags_from_states,
surface_primary_scanout_output, take_presentation_feedback_surface_tree,
under_from_surface_tree, update_surface_primary_scanout_output, OutputPresentationFeedback,
under_from_surface_tree, update_surface_primary_scanout_output, with_surfaces_surface_tree,
OutputPresentationFeedback,
};
use smithay::desktop::{
find_popup_root_surface, layer_map_for_output, LayerMap, LayerSurface, PopupGrab, PopupManager,
@@ -154,6 +155,7 @@ use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::surface::push_elements_from_surface_tree;
use crate::render_helpers::texture::TextureBuffer;
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::{
encompassing_geo, render_to_dmabuf, render_to_encompassing_texture, render_to_shm,
render_to_texture, render_to_vec, shaders, RenderCtx, RenderTarget,
@@ -471,6 +473,7 @@ pub struct OutputState {
/// Solid color buffer for the backdrop that we use instead of clearing to avoid damage
/// tracking issues and make screenshots easier.
pub backdrop_buffer: SolidColorBuffer,
pub xray: Xray,
pub lock_render_state: LockRenderState,
pub lock_surface: Option<LockSurface>,
pub lock_color_buffer: SolidColorBuffer,
@@ -2024,6 +2027,51 @@ impl State {
self.niri.queue_redraw_all();
}
pub fn store_unmap_snapshot(&mut self, window: &Window, output: Option<&Output>) {
// The unmapping tile may have an xray background, in which case we will render xray
// elements, so they need to be updated.
self.niri.update_xray_render_elements(output);
self.backend.with_primary_renderer(|renderer| {
if let Some(output) = output {
let mut ctx = RenderCtx {
target: RenderTarget::Output,
renderer,
xray: None,
};
self.niri.fill_xray_elements(ctx.r(), output);
// If any background layer has block_out_from, also fill the Screencast xray
// buffer so the unmap snapshot can render a buffer with blocked-out background.
//
// This will be used in Tile::render_snapshot().
let has_blocked_out = self.niri.has_blocked_out_background_layers(output);
if has_blocked_out {
let screencast_ctx = RenderCtx {
target: RenderTarget::Screencast,
..ctx.r()
};
self.niri.fill_xray_elements(screencast_ctx, output);
}
let state = self.niri.output_state.get_mut(output).unwrap();
self.niri.layout.store_unmap_snapshot(
renderer,
Some(&mut state.xray),
has_blocked_out,
window,
);
self.niri.clear_xray_elements(output);
} else {
self.niri
.layout
.store_unmap_snapshot(renderer, None, false, window);
}
});
}
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub fn set_dynamic_cast_target(&mut self, _target: CastTarget) {}
@@ -2805,6 +2853,7 @@ impl Niri {
vblank_throttle: VBlankThrottle::new(self.event_loop.clone(), name.connector.clone()),
frame_callback_sequence: 0,
backdrop_buffer: SolidColorBuffer::new(size, backdrop_color),
xray: Xray::new(),
lock_render_state,
lock_surface: None,
lock_color_buffer: SolidColorBuffer::new(size, CLEAR_COLOR_LOCKED),
@@ -3985,6 +4034,7 @@ impl Niri {
}
pub fn update_render_elements(&mut self, output: Option<&Output>) {
self.update_xray_render_elements(output);
self.layout.update_render_elements(output);
for (out, state) in self.output_state.iter_mut() {
@@ -4011,6 +4061,46 @@ impl Niri {
}
}
// Updates only those render elements that go in the xray buffer.
pub fn update_xray_render_elements(&mut self, output: Option<&Output>) {
for (out, state) in self.output_state.iter_mut() {
if output.is_none_or(|output| out == output) {
let scale = Scale::from(out.current_scale().fractional_scale());
let mode = out.current_mode().unwrap();
let transform = out.current_transform();
let size = transform.transform_size(mode.size);
state.xray.workspaces.clear();
let mon = self.layout.monitor_for_output(out).unwrap();
for (ws, geo) in mon.workspaces_with_render_geo() {
let bg_color = ws.render_background().color();
state.xray.workspaces.push((geo, bg_color));
}
state.xray.backdrop_color = state.backdrop_buffer.color();
for buf in &state.xray.background {
let mut buffer = buf.borrow_mut();
buffer.update_size(size, scale);
}
for buf in &state.xray.backdrop {
let mut buffer = buf.borrow_mut();
buffer.update_size(size, scale);
}
let layer_map = layer_map_for_output(out);
for surface in layer_map.layers_on(Layer::Background) {
let Some(mapped) = self.mapped_layer_surfaces.get_mut(surface) else {
continue;
};
let Some(geo) = layer_map.layer_geometry(surface) else {
continue;
};
mapped.update_render_elements(geo.size.to_f64());
}
}
}
}
pub fn update_shaders(&mut self) {
self.layout.update_shaders();
@@ -4050,6 +4140,25 @@ impl Niri {
}
}
self.fill_xray_elements(ctx.as_gles(), output);
// Reborrow to shorten lifetime to be able to put in xray.
let mut ctx = ctx.r();
let state = self.output_state.get(output).unwrap();
ctx.xray = Some(&state.xray);
self.render_inner(ctx, output, include_pointer, push);
self.clear_xray_elements(output);
}
fn render_inner<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
output: &Output,
include_pointer: bool,
push: &mut dyn FnMut(OutputRenderElements<R>),
) {
let state = self.output_state.get(output).unwrap();
let output_scale = Scale::from(output.current_scale().fractional_scale());
@@ -4168,17 +4277,21 @@ impl Niri {
}};
}
macro_rules! push_normal_from_layer {
($layer:expr, $backdrop:expr, $push:expr) => {{
self.render_layer_normal(ctx.r(), &layer_map, $layer, $backdrop, $push);
($layer:expr, $xray_pos:expr, $backdrop:expr, $push:expr) => {{
self.render_layer_normal(ctx.r(), &layer_map, $layer, $xray_pos, $backdrop, $push);
}};
($layer:expr, true) => {{
push_normal_from_layer!($layer, true, &mut |elem| push(elem.into()));
push_normal_from_layer!($layer, XrayPos::default(), true, &mut |elem| {
push(elem.into())
});
}};
($layer:expr, $push:expr) => {{
push_normal_from_layer!($layer, false, $push);
($layer:expr, $xray_pos:expr, $push:expr) => {{
push_normal_from_layer!($layer, $xray_pos, false, $push);
}};
($layer:expr) => {{
push_normal_from_layer!($layer, false, &mut |elem| push(elem.into()));
push_normal_from_layer!($layer, XrayPos::default(), false, &mut |elem| {
push(elem.into())
});
}};
}
@@ -4236,8 +4349,9 @@ impl Niri {
mon.render_workspaces(ctx.r(), focus_ring, &mut |elem| push(elem.into()));
for (ws, geo) in mon.workspaces_with_render_geo() {
push_normal_from_layer!(Layer::Bottom, process!(geo));
push_normal_from_layer!(Layer::Background, process!(geo));
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));
process!(geo)(ws.render_background());
}
@@ -4252,6 +4366,90 @@ impl Niri {
push(backdrop);
}
pub fn fill_xray_elements(&self, mut ctx: RenderCtx<GlesRenderer>, output: &Output) {
let _span = tracy_client::span!("Niri::fill_xray_elements");
// Make sure the xrayed elements themselves cannot use xray by mistake.
ctx.xray = None;
let state = self.output_state.get(output).unwrap();
let xray = &state.xray;
let layer_map = layer_map_for_output(output);
// FIXME: it would be cool to call this code on-demand. It's even relatively simple to do:
// move this function to after the render_inner() call, check if
// Rc::strong_count(&xray.background) > 1, and only then construct the elements. This way,
// only if something referenced the xray buffer will the elements get constructed.
//
// Unfortunately, currently this runs into an important limitation: offscreens are rendered
// immediately deep inside render_inner(), and when they are, they already need the xray
// elements filled.
//
// Perhaps in the future when offscreen rendering becomes on-demand, this optimization will
// be possible.
let mut buffer = xray.background[ctx.target as usize].borrow_mut();
{
let elements = buffer.elements();
elements.clear();
self.render_layer_normal(
ctx.r(),
&layer_map,
Layer::Background,
XrayPos::default(),
false,
&mut |elem| elements.push(elem.into()),
);
// Avoid unused capacity remaining forever.
elements.shrink_to_fit();
}
let mut buffer = xray.backdrop[ctx.target as usize].borrow_mut();
{
let elements = buffer.elements();
elements.clear();
self.render_layer_normal(
ctx.r(),
&layer_map,
Layer::Background,
XrayPos::default(),
true,
&mut |elem| elements.push(elem.into()),
);
// Avoid unused capacity remaining forever.
elements.shrink_to_fit();
}
}
pub fn clear_xray_elements(&self, output: &Output) {
let state = self.output_state.get(output).unwrap();
let xray = &state.xray;
// Clear the xray elements for all render targets after all rendering that could use them
// did so.
for buf in &xray.background {
buf.borrow_mut().elements().clear();
}
for buf in &xray.backdrop {
buf.borrow_mut().elements().clear();
}
}
/// Checks if any background layer surface has `block_out_from` set.
pub fn has_blocked_out_background_layers(&self, output: &Output) -> bool {
let layer_map = layer_map_for_output(output);
for for_backdrop in [false, true] {
for (mapped, _geo) in
self.layers_in_render_order(&layer_map, Layer::Background, for_backdrop)
{
if mapped.rules().block_out_from.is_some() {
return true;
}
}
}
false
}
fn layers_in_render_order<'a>(
&'a self,
layer_map: &'a LayerMap,
@@ -4271,16 +4469,20 @@ impl Niri {
})
}
#[allow(clippy::too_many_arguments)]
fn render_layer_normal<R: NiriRenderer>(
&self,
mut ctx: RenderCtx<R>,
layer_map: &LayerMap,
layer: Layer,
xray_pos: XrayPos,
for_backdrop: bool,
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
) {
for (mapped, geo) in self.layers_in_render_order(layer_map, layer, for_backdrop) {
mapped.render_normal(ctx.r(), geo.loc.to_f64(), push);
let loc = geo.loc.to_f64();
let xray_pos = xray_pos.offset(loc);
mapped.render_normal(ctx.r(), loc, xray_pos, push);
}
}
@@ -4558,17 +4760,65 @@ impl Niri {
});
}
for surface in layer_map_for_output(output).layers() {
surface.with_surfaces(|surface, states| {
update_surface_primary_scanout_output(
surface,
let xray = &self.output_state[output].xray;
let xray_bg = xray.background[RenderTarget::Output as usize].borrow();
let xray_bd = xray.backdrop[RenderTarget::Output as usize].borrow();
for layer in layer_map_for_output(output).layers() {
let surface = layer.wl_surface();
let is_background = layer.layer() == Layer::Background;
with_surfaces_surface_tree(surface, |surface, states| {
let primary_scanout_output = states
.data_map
.get_or_insert_threadsafe(Mutex::<PrimaryScanoutOutput>::default);
let mut primary_scanout_output = primary_scanout_output.lock().unwrap();
let mut id = Id::from_wayland_resource(surface);
// Background layers may be invisible normally but visible through an xray
// background effect. Try to find it and use the xray element's id in this case.
//
// FIXME: this won't work if there's another layer of offscreen (e.g. window with
// an xray background during its opening animation). But hopefully with the
// refactor to draw background effects outside offscreens it won't be a problem.
if is_background && !render_element_states.element_was_presented(id.clone()) {
// A layer may be present either in background or backdrop, never in both.
if xray_bg
.render_element_states()
.is_some_and(|s| s.element_was_presented(id.clone()))
{
id = xray_bg.id().clone();
} else if xray_bd
.render_element_states()
.is_some_and(|s| s.element_was_presented(id.clone()))
{
id = xray_bd.id().clone();
}
}
primary_scanout_output.update_from_render_element_states(
id,
output,
states,
render_element_states,
// Layer surfaces are shown only on one output at a time.
|_, _, output, _| output,
);
});
// Popups never go into xray buffers.
for (popup, _) in PopupManager::popups_for_surface(surface) {
let surface = popup.wl_surface();
with_surfaces_surface_tree(surface, |surface, states| {
update_surface_primary_scanout_output(
surface,
output,
states,
render_element_states,
// Layer surfaces are shown only on one output at a time.
|_, _, output, _| output,
);
});
}
}
if let Some(surface) = &self.output_state[output].lock_surface {
@@ -4920,6 +5170,7 @@ impl Niri {
let ctx = RenderCtx {
renderer,
target: RenderTarget::ScreenCapture,
xray: None,
};
self.render_to_vec(ctx, output, true)
});
@@ -4987,6 +5238,7 @@ impl Niri {
let ctx = RenderCtx {
renderer,
target: RenderTarget::ScreenCapture,
xray: None,
};
let elements = self.render_to_vec(ctx, output, screencopy.overlay_cursor());
@@ -5112,7 +5364,11 @@ impl Niri {
RenderTarget::ScreenCapture,
];
let screenshot = targets.map(|target| {
let ctx = RenderCtx { renderer, target };
let ctx = RenderCtx {
renderer,
target,
xray: None,
};
let elements = self.render_to_vec(ctx, &output, false);
let elements = elements.iter().rev();
@@ -5193,6 +5449,7 @@ impl Niri {
let ctx = RenderCtx {
renderer,
target: RenderTarget::ScreenCapture,
xray: None,
};
let elements = self.render_to_vec(ctx, output, include_pointer);
let elements = elements.iter().rev();
@@ -5247,6 +5504,7 @@ impl Niri {
let ctx = RenderCtx {
renderer,
target: RenderTarget::ScreenCapture,
xray: None,
};
mapped.render(
ctx,
@@ -5411,6 +5669,7 @@ impl Niri {
let ctx = RenderCtx {
renderer,
target: RenderTarget::ScreenCapture,
xray: None,
};
let elements = self.render_to_vec(ctx, &output, include_pointer);
let elements = elements.iter().rev();
@@ -5885,7 +6144,11 @@ impl Niri {
RenderTarget::ScreenCapture,
];
let textures = targets.map(|target| {
let ctx = RenderCtx { renderer, target };
let ctx = RenderCtx {
renderer,
target,
xray: None,
};
let elements = self.render_to_vec(ctx, &output, false);
let elements = elements.iter().rev();
+174
View File
@@ -0,0 +1,174 @@
use std::sync::Mutex;
use niri_config::CornerRadius;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Rectangle};
use smithay::wayland::compositor::{with_states, SurfaceData};
use wayland_server::protocol::wl_surface::WlSurface;
use crate::niri_render_elements;
use crate::render_helpers::damage::ExtraDamage;
use crate::render_helpers::xray::{XrayElement, XrayPos};
use crate::render_helpers::RenderCtx;
#[derive(Debug)]
pub struct BackgroundEffect {
/// Damage when options change.
damage: ExtraDamage,
/// Corner radius for clipping.
///
/// Stored here in addition to `RenderParams` to damage when it changes.
// FIXME: would be good to remove this duplication of radius.
corner_radius: CornerRadius,
options: Options,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Options {
pub xray: bool,
}
impl Options {
fn is_visible(&self) -> bool {
self.xray
}
}
/// Render-time parameters.
#[derive(Debug)]
pub struct RenderParams {
/// Geometry of the background effect.
pub geometry: Rectangle<f64, Logical>,
/// Geometry and radius for clipping in the same coordinate space as `geometry`.
pub clip: Option<(Rectangle<f64, Logical>, CornerRadius)>,
/// Scale to use for rounding to physical pixels.
pub scale: f64,
}
impl RenderParams {
fn fit_clip_radius(&mut self) {
if let Some((geo, radius)) = &mut self.clip {
// HACK: increase radius to avoid slight bleed on rounded corners.
*radius = radius.expanded_by(1.);
*radius = radius.fit_to(geo.size.w as f32, geo.size.h as f32);
}
}
}
niri_render_elements! {
BackgroundEffectElement => {
Xray = XrayElement,
ExtraDamage = ExtraDamage,
}
}
impl BackgroundEffect {
pub fn new() -> Self {
Self {
damage: ExtraDamage::new(),
corner_radius: CornerRadius::default(),
options: Options::default(),
}
}
pub fn update_render_elements(
&mut self,
corner_radius: CornerRadius,
effect: niri_config::BackgroundEffect,
) {
let options = Options {
xray: effect.xray == Some(true),
};
if self.options == options && self.corner_radius == corner_radius {
return;
}
self.options = options;
self.corner_radius = corner_radius;
self.damage.damage_all();
}
pub fn is_visible(&self) -> bool {
self.options.is_visible()
}
pub fn render(
&self,
ctx: RenderCtx<GlesRenderer>,
mut params: RenderParams,
xray_pos: XrayPos,
push: &mut dyn FnMut(BackgroundEffectElement),
) {
if !self.is_visible() {
return;
}
if let Some(clip) = &mut params.clip {
clip.1 = self.corner_radius;
}
params.fit_clip_radius();
let damage = self.damage.render(params.geometry);
if self.options.xray {
let Some(xray) = ctx.xray else {
return;
};
push(damage.into());
xray.render(ctx, params, xray_pos, &mut |elem| push(elem.into()));
} else {
// Render non-xray effect.
}
}
}
/// Per-surface background effect stored in its data map.
struct SurfaceBackgroundEffect(Mutex<BackgroundEffect>);
impl SurfaceBackgroundEffect {
fn get(states: &SurfaceData) -> &Self {
states
.data_map
.get_or_insert(|| SurfaceBackgroundEffect(Mutex::new(BackgroundEffect::new())))
}
}
// Silence, Clippy
// A Smithay user is talking
#[allow(clippy::too_many_arguments)]
pub fn render_for_tile(
ctx: RenderCtx<GlesRenderer>,
geometry: Rectangle<f64, Logical>,
scale: f64,
clip_to_geometry: bool,
surface: &WlSurface,
radius: CornerRadius,
effect: niri_config::BackgroundEffect,
xray_pos: XrayPos,
push: &mut dyn FnMut(BackgroundEffectElement),
) {
with_states(surface, |states| {
let background_effect = SurfaceBackgroundEffect::get(states);
let mut background_effect = background_effect.0.lock().unwrap();
background_effect.update_render_elements(radius, effect);
if !background_effect.is_visible() {
return;
}
// Effects not requested by the surface itself are drawn to match the geometry.
let _ = clip_to_geometry;
let params = RenderParams {
geometry,
clip: Some((geometry, CornerRadius::default())),
scale,
};
let xray_pos = xray_pos.offset(params.geometry.loc - geometry.loc);
background_effect.render(ctx, params, xray_pos, push);
});
}
+231
View File
@@ -0,0 +1,231 @@
use std::mem;
use anyhow::Context as _;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::{Id, RenderElementStates};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{
Bind as _, Color32F, ContextId, Offscreen as _, Renderer as _, Texture,
};
use smithay::utils::{Buffer, Logical, Physical, Scale, Size, Transform};
use crate::niri::OutputRenderElements;
#[derive(Debug)]
pub struct EffectBuffer {
/// Id to be used for this effect buffer's elements.
id: Id,
/// Size of the effect buffer.
size: Size<i32, Buffer>,
/// Scale of the effect buffer.
scale: Scale<f64>,
/// Elements to be rendered on demand.
elements: Elements,
/// Offscreen buffer where elements get rendered.
offscreen: Option<Offscreen>,
/// Commit counter for the offscreen texture.
commit_counter: CommitCounter,
}
#[derive(Debug)]
enum Elements {
/// Contents remain unchanged.
Unchanged(
// Storage to avoid reallocating it every time.
Vec<OutputRenderElements<GlesRenderer>>,
),
/// New contents, need to check damage and render.
New(Vec<OutputRenderElements<GlesRenderer>>),
}
#[derive(Debug)]
struct Offscreen {
/// The texture with the offscreen contents.
texture: GlesTexture,
/// Id of the renderer context that the texture comes from.
renderer_context_id: ContextId<GlesTexture>,
/// Scale of the texture.
scale: Scale<f64>,
/// Damage tracker for drawing to the texture.
damage: OutputDamageTracker,
/// Render element states from the last render into the offscreen.
states: RenderElementStates,
}
impl Default for Elements {
fn default() -> Self {
Self::Unchanged(Vec::new())
}
}
impl EffectBuffer {
pub fn new() -> Self {
Self {
id: Id::new(),
size: Size::default(),
scale: Scale::from(1.),
elements: Elements::default(),
offscreen: None,
commit_counter: CommitCounter::default(),
}
}
pub fn id(&self) -> &Id {
&self.id
}
pub fn commit(&self) -> CommitCounter {
self.commit_counter
}
pub fn logical_size(&self) -> Size<f64, Logical> {
self.size.to_f64().to_logical(self.scale, Transform::Normal)
}
pub fn scale(&self) -> Scale<f64> {
self.scale
}
pub fn render_element_states(&self) -> Option<&RenderElementStates> {
self.offscreen.as_ref().map(|o| &o.states)
}
pub fn update_size(&mut self, size: Size<i32, Physical>, scale: Scale<f64>) {
self.size = size.to_logical(1).to_buffer(1, Transform::Normal);
self.scale = scale;
}
pub fn elements(&mut self) -> &mut Vec<OutputRenderElements<GlesRenderer>> {
// Assume we're going to insert new elements, switch to New.
match mem::take(&mut self.elements) {
Elements::Unchanged(elements) | Elements::New(elements) => {
self.elements = Elements::New(elements);
}
}
let Elements::New(elements) = &mut self.elements else {
unreachable!();
};
elements
}
pub fn prepare(&mut self, renderer: &mut GlesRenderer) -> bool {
if let Err(err) = self.prepare_offscreen(renderer) {
warn!("error preparing offscreen: {err:?}");
return false;
};
true
}
fn prepare_offscreen(&mut self, renderer: &mut GlesRenderer) -> anyhow::Result<()> {
let _span = tracy_client::span!("EffectBuffer::prepare_offscreen");
// Check if we need to create or recreate the texture.
let size_string;
let mut reason = "";
if let Some(Offscreen {
texture,
renderer_context_id,
..
}) = &mut self.offscreen
{
let old_size = texture.size();
if old_size != self.size {
size_string = format!(
"size changed from {} × {} to {} × {}",
old_size.w, old_size.h, self.size.w, self.size.h
);
reason = &size_string;
self.offscreen = None;
} else if !texture.is_unique_reference() {
reason = "not unique";
self.offscreen = None;
} else if *renderer_context_id != renderer.context_id() {
reason = "renderer id changed";
self.offscreen = None;
}
} else {
reason = "first render";
}
let offscreen = if let Some(offscreen) = &mut self.offscreen {
offscreen
} else {
debug!("creating new offscreen texture: {reason}");
let span = tracy_client::span!("creating effect offscreen texture");
span.emit_text(reason);
let texture: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, self.size)
.context("error creating texture")?;
let buffer_size = self.size.to_logical(1, Transform::Normal).to_physical(1);
let damage = OutputDamageTracker::new(buffer_size, self.scale, Transform::Normal);
self.offscreen.insert(Offscreen {
texture,
renderer_context_id: renderer.context_id(),
scale: self.scale,
damage,
states: RenderElementStates::default(),
})
};
// Recreate the damage tracker if the scale changes. We already recreate it for buffer size
// changes, and transform is always Normal.
if offscreen.scale != self.scale {
offscreen.scale = self.scale;
trace!("recreating damage tracker due to scale change");
let buffer_size = self.size.to_logical(1, Transform::Normal).to_physical(1);
offscreen.damage = OutputDamageTracker::new(buffer_size, self.scale, Transform::Normal);
self.commit_counter.increment();
}
// Render the elements if any.
let mut elements = match mem::take(&mut self.elements) {
Elements::New(elements) => elements,
x @ Elements::Unchanged(_) => {
// No redrawing necessary.
self.elements = x;
return Ok(());
}
};
let res = {
let mut target = renderer
.bind(&mut offscreen.texture)
.context("error binding texture")?;
offscreen
.damage
.render_output(renderer, &mut target, 1, &elements, Color32F::TRANSPARENT)
.context("error rendering")?
};
offscreen.states = res.states;
if res.damage.is_some() {
self.commit_counter.increment();
}
// Clear and put the storage back.
elements.clear();
self.elements = Elements::Unchanged(elements);
Ok(())
}
pub fn render(&mut self) -> anyhow::Result<GlesTexture> {
let offscreen = self.offscreen.as_mut().context("offscreen is missing")?;
Ok(offscreen.texture.clone())
}
}
+10 -1
View File
@@ -22,11 +22,14 @@ use solid_color::{SolidColorBuffer, SolidColorRenderElement};
use self::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use self::texture::{TextureBuffer, TextureRenderElement};
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::xray::Xray;
pub mod background_effect;
pub mod border;
pub mod clipped_surface;
pub mod damage;
pub mod debug;
pub mod effect_buffer;
pub mod gradient_fade_texture;
pub mod memory;
pub mod offscreen;
@@ -42,6 +45,7 @@ pub mod snapshot;
pub mod solid_color;
pub mod surface;
pub mod texture;
pub mod xray;
/// A rendering context.
///
@@ -49,6 +53,7 @@ pub mod texture;
pub struct RenderCtx<'a, R> {
pub renderer: &'a mut R,
pub target: RenderTarget,
pub xray: Option<&'a Xray>,
}
impl<'a, R> RenderCtx<'a, R> {
@@ -58,6 +63,7 @@ impl<'a, R> RenderCtx<'a, R> {
RenderCtx {
renderer: self.renderer,
target: self.target,
xray: self.xray,
}
}
}
@@ -67,6 +73,7 @@ impl<'a, R: AsGlesRenderer> RenderCtx<'a, R> {
RenderCtx {
renderer: self.renderer.as_gles_renderer(),
target: self.target,
xray: self.xray,
}
}
}
@@ -75,7 +82,7 @@ impl<'a, R: AsGlesRenderer> RenderCtx<'a, R> {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderTarget {
/// Rendering to display on screen.
Output,
Output = 0,
/// Rendering for a screencast.
Screencast,
/// Rendering for any other screen capture.
@@ -104,6 +111,8 @@ pub trait ToRenderElement {
}
impl RenderTarget {
pub const COUNT: usize = 3;
pub fn should_block_out(self, block_out_from: Option<BlockOutFrom>) -> bool {
match block_out_from {
None => false,
@@ -27,6 +27,7 @@ uniform vec4 corner_radius;
uniform mat3 input_to_geo;
float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius);
vec4 postprocess(vec4 color);
void main() {
vec3 coords_geo = input_to_geo * vec3(v_coords, 1.0);
@@ -37,6 +38,8 @@ void main() {
color = vec4(color.rgb, 1.0);
#endif
color = postprocess(color);
if (coords_geo.x < 0.0 || 1.0 < coords_geo.x || coords_geo.y < 0.0 || 1.0 < coords_geo.y) {
// Clip outside geometry.
color = vec4(0.0);
+24 -1
View File
@@ -13,6 +13,7 @@ pub struct Shaders {
pub border: Option<ShaderProgram>,
pub shadow: Option<ShaderProgram>,
pub clipped_surface: Option<GlesTexProgram>,
pub postprocess_and_clip: Option<GlesTexProgram>,
pub resize: Option<ShaderProgram>,
pub gradient_fade: Option<GlesTexProgram>,
pub custom_resize: RefCell<Option<ShaderProgram>>,
@@ -86,7 +87,8 @@ impl Shaders {
.compile_custom_texture_shader(
concat!(
include_str!("clipped_surface.frag"),
include_str!("rounding_alpha.frag")
include_str!("rounding_alpha.frag"),
"\nvec4 postprocess(vec4 color) { return color; }",
),
&[
UniformName::new("niri_scale", UniformType::_1f),
@@ -100,6 +102,26 @@ impl Shaders {
})
.ok();
let postprocess_and_clip = renderer
.compile_custom_texture_shader(
concat!(
include_str!("clipped_surface.frag"),
include_str!("rounding_alpha.frag"),
include_str!("postprocess.frag"),
),
&[
UniformName::new("niri_scale", UniformType::_1f),
UniformName::new("geo_size", UniformType::_2f),
UniformName::new("corner_radius", UniformType::_4f),
UniformName::new("input_to_geo", UniformType::Matrix3x3),
UniformName::new("bg_color", UniformType::_4f),
],
)
.map_err(|err| {
warn!("error compiling postprocess_and_clip shader: {err:?}");
})
.ok();
let resize = compile_resize_program(renderer, include_str!("resize.frag"))
.map_err(|err| {
warn!("error compiling resize shader: {err:?}");
@@ -120,6 +142,7 @@ impl Shaders {
border,
shadow,
clipped_surface,
postprocess_and_clip,
resize,
gradient_fade,
custom_resize: RefCell::new(None),
@@ -0,0 +1,8 @@
uniform vec4 bg_color;
vec4 postprocess(vec4 color) {
// Mix bg_color behind the texture (both premultiplied alpha).
color = color + bg_color * (1.0 - color.a);
return color;
}
+37 -1
View File
@@ -7,7 +7,7 @@ use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::{render_to_encompassing_texture, ToRenderElement};
use crate::render_helpers::RenderCtx;
use crate::render_helpers::{RenderCtx, RenderTarget};
/// Snapshot of a render.
#[derive(Debug)]
@@ -17,6 +17,12 @@ pub struct RenderSnapshot<C, B> {
/// Relative to the geometry.
pub contents: Vec<C>,
/// Contents that are not blocked out, but the background is blocked out.
///
/// If `None` then the background doesn't have any blocked-out surfaces, and normal `contents`
/// can be used instead.
pub contents_with_blocked_out_bg: Option<Vec<C>>,
/// Blocked-out contents.
///
/// Relative to the geometry.
@@ -31,6 +37,9 @@ pub struct RenderSnapshot<C, B> {
/// Contents rendered into a texture (lazily).
pub texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
/// Contents with blocked-out bg rendered into a texture (lazily).
pub texture_with_blocked_out_bg: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
/// Blocked-out contents rendered into a texture (lazily).
pub blocked_out_texture: OnceCell<Option<(GlesTexture, Rectangle<i32, Physical>)>>,
}
@@ -73,6 +82,33 @@ where
}
}
})
} else if ctx.target != RenderTarget::Output && self.contents_with_blocked_out_bg.is_some()
{
let contents = self.contents_with_blocked_out_bg.as_ref().unwrap();
self.texture_with_blocked_out_bg.get_or_init(|| {
let _span = tracy_client::span!("RenderSnapshot::texture");
let elements: Vec<_> = contents
.iter()
.map(|baked| {
baked.to_render_element(Point::from((0., 0.)), scale, 1., Kind::Unspecified)
})
.collect();
match render_to_encompassing_texture(
ctx.renderer,
scale,
Transform::Normal,
Fourcc::Abgr8888,
&elements,
) {
Ok((texture, _sync_point, geo)) => Some((texture, geo)),
Err(err) => {
warn!("error rendering contents with blocked-out bg to texture: {err:?}");
None
}
}
})
} else {
self.texture.get_or_init(|| {
let _span = tracy_client::span!("RenderSnapshot::texture");
+328
View File
@@ -0,0 +1,328 @@
use std::array;
use std::cell::RefCell;
use std::rc::Rc;
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
use smithay::backend::renderer::element::{Element, Id, RenderElement};
use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::backend::renderer::Color32F;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::background_effect::RenderParams;
use crate::render_helpers::effect_buffer::EffectBuffer;
use crate::render_helpers::renderer::AsGlesFrame as _;
use crate::render_helpers::shaders::{mat3_uniform, Shaders};
use crate::render_helpers::{RenderCtx, RenderTarget};
#[derive(Debug)]
pub struct Xray {
// The buffers are per-render-target to avoid constant rerendering when screencasting.
pub background: [Rc<RefCell<EffectBuffer>>; RenderTarget::COUNT],
pub backdrop: [Rc<RefCell<EffectBuffer>>; RenderTarget::COUNT],
pub backdrop_color: Color32F,
pub workspaces: Vec<(Rectangle<f64, Logical>, Color32F)>,
}
/// Position for drawing xray background.
#[derive(Debug, Clone, Copy)]
pub struct XrayPos {
/// Position of geometry relative to the backdrop in zoomed coordinates.
///
/// Should be upscaled by `zoom` to get position in backdrop coordinates.
pub pos_in_backdrop: Point<f64, Logical>,
/// Zoom factor between backdrop coordinates and geometry.
pub zoom: f64,
}
impl XrayPos {
pub fn new(pos_in_backdrop: Point<f64, Logical>, zoom: f64) -> Self {
Self {
pos_in_backdrop: pos_in_backdrop.downscale(zoom),
zoom,
}
}
pub fn offset(mut self, offset: Point<f64, Logical>) -> Self {
self.pos_in_backdrop += offset;
self
}
}
impl Default for XrayPos {
fn default() -> Self {
Self {
pos_in_backdrop: Point::new(0., 0.),
zoom: 1.,
}
}
}
#[derive(Debug)]
pub struct XrayElement {
buffer: Rc<RefCell<EffectBuffer>>,
id: Id,
geometry: Rectangle<f64, Logical>,
src: Rectangle<f64, Buffer>,
input_to_clip_geo: Mat3,
clip_geo_size: Vec2,
corner_radius: CornerRadius,
scale: f32,
bg_color: Color32F,
program: Option<GlesTexProgram>,
}
impl Xray {
pub fn new() -> Self {
Self {
background: array::from_fn(|_| Rc::new(RefCell::new(EffectBuffer::new()))),
backdrop: array::from_fn(|_| Rc::new(RefCell::new(EffectBuffer::new()))),
backdrop_color: Color32F::TRANSPARENT,
workspaces: Vec::new(),
}
}
pub fn render(
&self,
ctx: RenderCtx<GlesRenderer>,
params: RenderParams,
xray_pos: XrayPos,
push: &mut dyn FnMut(XrayElement),
) {
let program = Shaders::get(ctx.renderer).postprocess_and_clip.clone();
let zoom = xray_pos.zoom;
let pos_in_backdrop = xray_pos.pos_in_backdrop.upscale(zoom);
let (clip_geo, corner_radius) = params
.clip
.unwrap_or((params.geometry, CornerRadius::default()));
let clip_offset = clip_geo.loc - params.geometry.loc;
let clip_pos_in_backdrop = pos_in_backdrop + clip_offset.upscale(zoom);
let geo_in_backdrop = Rectangle::new(pos_in_backdrop, params.geometry.size.upscale(zoom));
let mut backdrop = self.backdrop[ctx.target as usize].borrow_mut();
let backdrop_geo = Rectangle::from_size(backdrop.logical_size());
let intersection_with_backdrop = backdrop_geo.intersection(geo_in_backdrop);
let mut skip_backdrop = intersection_with_backdrop.is_none();
let mut background = self.background[ctx.target as usize].borrow_mut();
let prev = background.commit();
if background.prepare(ctx.renderer) {
if background.commit() != prev {
trace!("background damaged");
}
let clip_geo_size = Vec2::new(clip_geo.size.w as f32, clip_geo.size.h as f32);
let buf_size = background.logical_size();
for (ws_geo, bg_color) in &self.workspaces {
// If the background color is opaque, check if the workspace fully covers the
// element. In this case, we will skip the backdrop element since it's fully
// covered.
//
// FIXME: also implement some way to check if the background elements are fully
// covered in opaque regions, and not just the niri background color is opaque
let crop = if bg_color.is_opaque() && ws_geo.contains_rect(geo_in_backdrop) {
skip_backdrop = true;
// No need to intersect, we know it's fully covered.
Some(geo_in_backdrop)
} else {
ws_geo.intersection(geo_in_backdrop)
};
let Some(crop) = crop else {
continue;
};
// If crop contains the intersection with backdrop, then the workspace fully
// covers the backdrop, so we can skip the backdrop.
//
// This can happen when the overview is closed (so workspaces align left/right with
// the backdrop) and the window is peeking out off screen to the side. In this
// case, this off-screen part is on top of nothing, neither workspace nor backdrop,
// but since the window doesn't fully cover the workspace, the check above doesn't
// skip the backdrop.
if bg_color.is_opaque()
&& intersection_with_backdrop
.is_some_and(|backdrop| crop.contains_rect(backdrop))
{
skip_backdrop = true;
}
// This can be different from zoom for surfaces that do not scale with
// workspaces, e.g. layer-shell top and overlay layer.
let ws_zoom = ws_geo.size / buf_size;
let src = Rectangle::new(crop.loc - ws_geo.loc, crop.size).downscale(ws_zoom);
let src = src.to_buffer(background.scale(), Transform::Normal, &buf_size);
let buf_size = Vec2::new(buf_size.w as f32, buf_size.h as f32);
let pos_against_buf = (clip_pos_in_backdrop - ws_geo.loc).downscale(ws_zoom);
let pos_against_buf = Vec2::new(pos_against_buf.x as f32, pos_against_buf.y as f32);
let ws_zoom_vec = Vec2::new(ws_zoom.x as f32, ws_zoom.y as f32);
let input_to_clip_geo = Mat3::from_scale(ws_zoom_vec / zoom as f32)
* Mat3::from_scale(buf_size / clip_geo_size)
* Mat3::from_translation(-pos_against_buf / buf_size);
let mut geometry =
Rectangle::new(crop.loc - geo_in_backdrop.loc, crop.size).downscale(zoom);
geometry.loc += params.geometry.loc;
let elem = XrayElement {
buffer: self.background[ctx.target as usize].clone(),
id: background.id().clone(),
geometry,
src,
input_to_clip_geo,
clip_geo_size,
corner_radius,
scale: params.scale as f32,
bg_color: *bg_color,
program: program.clone(),
};
push(elem);
}
}
// If the backdrop is fully covered by opaque background, we can skip it.
if skip_backdrop {
return;
}
let prev = backdrop.commit();
if backdrop.prepare(ctx.renderer) {
if backdrop.commit() != prev {
trace!("backdrop damaged");
}
let buf_size = backdrop.logical_size();
let src = geo_in_backdrop.to_buffer(backdrop.scale(), Transform::Normal, &buf_size);
let mut clip_geo_in_backdrop = Rectangle::new(clip_offset, clip_geo.size).upscale(zoom);
clip_geo_in_backdrop.loc += geo_in_backdrop.loc;
let clip_pos_in_backdrop = Vec2::new(
clip_geo_in_backdrop.loc.x as f32,
clip_geo_in_backdrop.loc.y as f32,
);
let clip_geo_size = Vec2::new(
clip_geo_in_backdrop.size.w as f32,
clip_geo_in_backdrop.size.h as f32,
);
let buf_size = Vec2::new(buf_size.w as f32, buf_size.h as f32);
let input_to_clip_geo = Mat3::from_scale(buf_size / clip_geo_size)
* Mat3::from_translation(-clip_pos_in_backdrop / buf_size);
let elem = XrayElement {
buffer: self.backdrop[ctx.target as usize].clone(),
id: backdrop.id().clone(),
geometry: params.geometry,
src,
input_to_clip_geo,
clip_geo_size,
corner_radius: corner_radius.scaled_by(zoom as f32),
scale: params.scale as f32,
bg_color: self.backdrop_color,
program: program.clone(),
};
push(elem);
}
}
}
impl XrayElement {
fn compute_uniforms(&self) -> [Uniform<'static>; 5] {
[
Uniform::new("niri_scale", self.scale),
Uniform::new("geo_size", <[f32; 2]>::from(self.clip_geo_size)),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", self.input_to_clip_geo),
Uniform::new("bg_color", self.bg_color.components()),
]
}
}
impl Element for XrayElement {
fn id(&self) -> &Id {
&self.id
}
fn current_commit(&self) -> CommitCounter {
self.buffer.borrow().commit()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.src
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry.to_physical_precise_round(scale)
}
fn opaque_regions(&self, _scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
// FIXME: if bg_color alpha is 1 then compute opaque regions here taking corners into
// account
OpaqueRegions::default()
}
}
impl RenderElement<GlesRenderer> for XrayElement {
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
let mut buffer = self.buffer.borrow_mut();
let texture = match buffer.render() {
Ok(x) => x,
Err(err) => {
warn!("error rendering effect buffer: {err:?}");
return Ok(());
}
};
let uniforms = self.program.is_some().then(|| self.compute_uniforms());
let uniforms = uniforms.as_ref().map_or(&[][..], |x| &x[..]);
frame.render_texture_from_to(
&texture,
src,
dst,
damage,
// FIXME: opaque regions need to be filtered like damage.
&[],
Transform::Normal,
1.,
self.program.as_ref(),
uniforms,
)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for XrayElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self, gles_frame, src, dst, damage, opaque_regions)?;
Ok(())
}
}
+1
View File
@@ -578,6 +578,7 @@ impl Niri {
let ctx = RenderCtx {
renderer,
target: RenderTarget::Screencast,
xray: None,
};
self.render(ctx, output, false, &mut |elem| elements.push(elem.into()));
+29 -2
View File
@@ -28,6 +28,7 @@ use crate::layout::{
LayoutElementRenderSnapshot, SizingMode,
};
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::offscreen::OffscreenData;
use crate::render_helpers::renderer::NiriRenderer;
@@ -36,7 +37,8 @@ use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderEleme
use crate::render_helpers::surface::{
push_elements_from_surface_tree, render_snapshot_from_surface_tree,
};
use crate::render_helpers::{BakedBuffer, RenderCtx, RenderTarget};
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::{background_effect, BakedBuffer, RenderCtx, RenderTarget};
use crate::utils::id::IdCounter;
use crate::utils::transaction::Transaction;
use crate::utils::{
@@ -252,7 +254,6 @@ impl Mapped {
pub fn new(window: Window, rules: ResolvedWindowRules, hook: HookId) -> Self {
let surface = window.wl_surface().expect("no X11 support");
let credentials = get_credentials_for_surface(&surface);
let mut rv = Self {
window,
id: MappedId::next(),
@@ -407,10 +408,12 @@ impl Mapped {
RenderSnapshot {
contents,
contents_with_blocked_out_bg: None,
blocked_out_contents,
block_out_from: self.rules().block_out_from,
size,
texture: Default::default(),
texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: Default::default(),
}
}
@@ -523,6 +526,7 @@ impl Mapped {
RenderCtx {
renderer,
target: RenderTarget::Screencast,
xray: None,
},
location,
scale,
@@ -671,6 +675,29 @@ impl LayoutElement for Mapped {
}
}
fn render_background_effect(
&self,
ctx: RenderCtx<GlesRenderer>,
geometry: Rectangle<f64, Logical>,
scale: f64,
clip_to_geometry: bool,
radius: CornerRadius,
xray_pos: XrayPos,
push: &mut dyn FnMut(BackgroundEffectElement),
) {
background_effect::render_for_tile(
ctx,
geometry,
scale,
clip_to_geometry,
self.toplevel().wl_surface(),
radius,
self.rules.background_effect,
xray_pos,
push,
);
}
fn request_size(
&mut self,
size: Size<i32, Logical>,
+9 -2
View File
@@ -3,8 +3,8 @@ use std::cmp::{max, min};
use niri_config::utils::MergeWith as _;
use niri_config::window_rule::{Match, WindowRule};
use niri_config::{
BlockOutFrom, BorderRule, CornerRadius, FloatingPosition, PresetSize, ShadowRule,
TabIndicatorRule,
BackgroundEffect, BlockOutFrom, BorderRule, CornerRadius, FloatingPosition, PresetSize,
ShadowRule, TabIndicatorRule,
};
use niri_ipc::ColumnDisplay;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
@@ -119,6 +119,9 @@ pub struct ResolvedWindowRules {
/// Override whether to set the Tiled xdg-toplevel state on the window.
pub tiled_state: Option<bool>,
/// Background effect configuration.
pub background_effect: BackgroundEffect,
}
impl<'a> WindowRef<'a> {
@@ -296,6 +299,10 @@ impl ResolvedWindowRules {
if let Some(x) = rule.tiled_state {
resolved.tiled_state = Some(x);
}
resolved
.background_effect
.merge_with(&rule.background_effect);
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());