diff --git a/Cargo.lock b/Cargo.lock index fde5b21ffc..4dece413dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -940,7 +940,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1068,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1416,7 +1416,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1735,7 +1735,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2210,6 +2210,7 @@ dependencies = [ "tracy-client", "wayland-backend", "wayland-client", + "wayland-protocols-plasma", "wayland-scanner", "xcursor", "xshell", @@ -3177,7 +3178,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3402,7 +3403,7 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smithay" version = "0.7.0" -source = "git+https://github.com/Smithay/smithay.git#ae14fa12f6ee3fc4dd4c766ffb21848f991a6a18" +source = "git+https://github.com/YaLTeR/smithay.git?rev=5f784c9ad6ae619c880e4713e120b281e2ce5383#5f784c9ad6ae619c880e4713e120b281e2ce5383" dependencies = [ "aliasable", "appendlist", @@ -3477,7 +3478,7 @@ dependencies = [ [[package]] name = "smithay-drm-extras" version = "0.1.0" -source = "git+https://github.com/Smithay/smithay.git#ae14fa12f6ee3fc4dd4c766ffb21848f991a6a18" +source = "git+https://github.com/Smithay/smithay.git#599857c6a030ac09e998078ddf1bda9f3f46db2a" dependencies = [ "drm", "libdisplay-info", @@ -3583,7 +3584,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4086,6 +4087,7 @@ dependencies = [ "wayland-client", "wayland-protocols", "wayland-scanner", + "wayland-server", ] [[package]] @@ -4182,7 +4184,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b19b364633..fb2040e4f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,9 @@ tracy-client = { version = "0.18.4", default-features = false } [workspace.dependencies.smithay] # version = "0.4.1" -git = "https://github.com/Smithay/smithay.git" +git = "https://github.com/YaLTeR/smithay.git" # path = "../smithay" +rev = "5f784c9ad6ae619c880e4713e120b281e2ce5383" default-features = false [workspace.dependencies.smithay-drm-extras] @@ -93,6 +94,7 @@ tracing-subscriber.workspace = true tracing.workspace = true tracy-client.workspace = true wayland-backend = "0.3.12" +wayland-protocols-plasma = { version = "0.3.10", features = ["server"] } wayland-scanner = "0.31.8" xcursor = "0.3.10" zbus = { version = "5.13.0", optional = true } diff --git a/docs/mkdocs.yaml b/docs/mkdocs.yaml index bfc3b364fe..1f04459b38 100644 --- a/docs/mkdocs.yaml +++ b/docs/mkdocs.yaml @@ -85,6 +85,7 @@ nav: - Xwayland: Xwayland.md - Gestures: Gestures.md - Fullscreen and Maximize: Fullscreen-and-Maximize.md + - Window Effects: Window-Effects.md - Packaging niri: Packaging-niri.md - Integrating niri: Integrating-niri.md - Accessibility: Accessibility.md diff --git a/docs/wiki/Configuration:-Layer-Rules.md b/docs/wiki/Configuration:-Layer-Rules.md index 04055bd958..c68dd87832 100644 --- a/docs/wiki/Configuration:-Layer-Rules.md +++ b/docs/wiki/Configuration:-Layer-Rules.md @@ -14,6 +14,7 @@ Here are all matchers and properties that a layer rule could have: layer-rule { match namespace="waybar" match at-startup=true + match layer="top" // Properties that apply continuously. opacity 0.5 @@ -34,6 +35,13 @@ layer-rule { geometry-corner-radius 12 place-within-backdrop true baba-is-float true + + background-effect { + xray true + blur true + noise 0.05 + saturation 3 + } } ``` @@ -69,6 +77,22 @@ layer-rule { } ``` +#### `layer` + +Since: next release + +Matches surfaces on this layer-shell layer. +Can be `"background"`, `"bottom"`, `"top"`, or `"overlay"`. + +```kdl +// Make all overlay-layer surfaces FLOAT. +layer-rule { + match layer="overlay" + + baba-is-float true +} +``` + ### Dynamic Properties These properties apply continuously to open layer-shell surfaces. @@ -191,3 +215,29 @@ layer-rule { baba-is-float true } ``` + +#### `background-effect` + +Since: next release + +Override the background effect options for this surface. + +- `xray`: set to `true` to enable the xray effect, or `false` to disable it. +- `blur`: set to `true` to enable blur behind this surface, or `false` to force-disable it. +- `noise`: amount of pixel noise added to the background (helps with color banding from blur). +- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation). + +See the [window effects page](./Window-Effects.md) for an overview of background effects. + +```kdl +// Make top and overlay layers use the regular blur (if enabled), +// while bottom and background layers keep using the efficient xray blur. +layer-rule { + match layer="top" + match layer="overlay" + + background-effect { + xray false + } +} +``` diff --git a/docs/wiki/Configuration:-Miscellaneous.md b/docs/wiki/Configuration:-Miscellaneous.md index 87f001a7d7..fb366c1508 100644 --- a/docs/wiki/Configuration:-Miscellaneous.md +++ b/docs/wiki/Configuration:-Miscellaneous.md @@ -54,6 +54,14 @@ hotkey-overlay { config-notification { disable-failed } + +blur { + // off + passes 3 + offset 3.0 + noise 0.02 + saturation 1.5 +} ``` ### `spawn-at-startup` @@ -320,3 +328,81 @@ config-notification { disable-failed } ``` + +### `blur` + +Since: next release + +Blur configuration that affects all background blur. + +See the [window effects page](./Window-Effects.md) for an overview of background effects. + +```kdl +blur { + // off + passes 3 + offset 3 + noise 0.02 + saturation 1.5 +} +``` + +#### `off` + +By default, blur is available on request by a window or layer surface (via the `ext-background-effect` protocol). +You can also enable it manually with the `blur true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule. + +Setting the `off` flag will disable all blur, both requested by the window, and configured in window rules. + +```kdl +blur { + off +} +``` + +#### `passes` and `offset` + +`passes` contols the number of downsample/upsample passes for dual kawase blur. +More passes produce a larger, smoother blur, but cost more GPU resources. + +`offset` is the pixel offset multiplier for each pass. +Offset `1` is the original dual kawase blur. +Larger values produce a smoother blur, at no additional GPU cost. + +However, setting `offset` too big will produce visual artifacts. +You will need to increase `passes` to be able to use a bigger `offset` without artifacts. + +When configuring blur, try increasing `offset` first (since it doesn't cause any extra GPU load) until you start getting artifacts. +Then, if you still need smoother blur, increase `passes` by 1. +Keep doing this until you get the desired visuals. + +```kdl +blur { + passes 3 + offset 3.0 +} +``` + +#### `noise` + +Amount of noise to add on top of the blur. + +This is helpful to reduce color banding artifacts. + +```kdl +blur { + noise 0.02 +} +``` + +#### `saturation` + +Color saturation applied to the blurred background. + +Values above `1` increase saturation; values below `1` reduce it. + +```kdl +blur { + saturation 1.5 +} +``` diff --git a/docs/wiki/Configuration:-Window-Rules.md b/docs/wiki/Configuration:-Window-Rules.md index 5c7dd28e65..8566470a83 100644 --- a/docs/wiki/Configuration:-Window-Rules.md +++ b/docs/wiki/Configuration:-Window-Rules.md @@ -100,6 +100,13 @@ window-rule { tiled-state true baba-is-float true + background-effect { + xray true + blur true + noise 0.05 + saturation 3 + } + min-width 100 max-width 200 min-height 300 @@ -909,6 +916,31 @@ https://github.com/user-attachments/assets/3f4cb1a4-40b2-4766-98b7-eec014c19509 +#### `background-effect` + +Since: next release + +Override the background effect options for this window. + +- `xray`: set to `true` to enable the xray effect, or `false` to disable it. +- `blur`: set to `true` to enable blur behind this window, or `false` to force-disable it. +- `noise`: amount of pixel noise added to the background (helps with color banding from blur). +- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation). + +See the [window effects page](./Window-Effects.md) for an overview of background effects. + +```kdl +// Make floating windows use the regular blur (if enabled), +// while tiled windows keep using the efficient xray blur. +window-rule { + match is-floating=true + + background-effect { + xray false + } +} +``` + #### Size Overrides You can amend the window's minimum and maximum size in logical pixels. diff --git a/docs/wiki/Window-Effects.md b/docs/wiki/Window-Effects.md new file mode 100644 index 0000000000..bb94707357 --- /dev/null +++ b/docs/wiki/Window-Effects.md @@ -0,0 +1,67 @@ +### Overview + +Since: next release + +You can apply background effects to windows and layer-shell surfaces. +These include blur, xray, saturation, and noise. +They can be enabled in the `background-effect {}` section of [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rules. + +The window needs to be semitransparent for you to see the background effect (otherwise it's fully covered by the opaque window). +Focus ring and border can also cover the background effect, see [this FAQ entry](./FAQ.md#why-are-transparent-windows-tinted-why-is-the-borderfocus-ring-showing-up-through-semitransparent-windows) for how to change this. + +### Blur + +Windows and layer surfaces can request their background to be blurred via the [`ext-background-effect` protocol](https://wayland.app/protocols/ext-background-effect-v1). +In this case, the application will usually offer some "background blur" setting that you'll need to enable in its configuration. + +You can also enable blur on the niri side with the `blur true` background effect window rule: + +```kdl +// Enable blur behind the foot terminal. +window-rule { + match app-id="^foot$" + + background-effect { + blur true + } +} + +// Enable blur behind the fuzzel launcher. +layer-rule { + match namespace="^launcher$" + + background-effect { + blur true + } +} +``` + +Blur enabled via the window rule will follow the window corner radius set via [`geometry-corner-radius`](./Configuration:-Window-Rules.md#geometry-corner-radius). +On the other hand, blur enabled through `ext-background-effect` will exactly follow the shape requested by the window. +If the window or layer has clientside rounded corners or other complex shape, it should set a corresponding blur shape through `ext-background-effect`, then it will get correctly shaped background blur without any manual niri configuration. + +Global blur settings are configured in the [`blur {}` config section](./Configuration:-Miscellaneous.md#blur) and apply to all background blur. + +### Xray + +Xray makes the window background "see through" to your wallpaper, ignoring any other windows below. +You can enable it with `xray true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule. + +Xray is automatically enabled by default if any other background effect (like blur) is active. +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). + +#### 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 subframe support in the Smithay rendering code. + +- Multiple clones of a non-xray background effect will interfere with each other and cause visual glitches. +You can see this if you enable non-xray effects on a bottom or background layer surface, then open the [Overview](./Overview.md). +Bottom and background layer surfaces are cloned on all workspaces that you can see in the Overview, causing interference. +Fixing this requires support for framebuffer effect clones in the Smithay rendering code. diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 4b5c830d54..570e4204e6 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -14,6 +14,7 @@ * [Xwayland](./Xwayland.md) * [Gestures](./Gestures.md) * [Fullscreen and Maximize](./Fullscreen-and-Maximize.md) +* [Window Effects](./Window-Effects.md) * [Packaging niri](./Packaging-niri.md) * [Integrating niri](./Integrating-niri.md) * [Accessibility](./Accessibility.md) diff --git a/niri-config/src/appearance.rs b/niri-config/src/appearance.rs index bf600feb70..6633430dc1 100644 --- a/niri-config/src/appearance.rs +++ b/niri-config/src/appearance.rs @@ -1006,6 +1006,110 @@ where } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Blur { + pub off: bool, + pub passes: u8, + pub offset: f64, + pub noise: f64, + pub saturation: f64, +} + +impl Default for Blur { + fn default() -> Self { + Self { + off: false, + // TODO: tune, reduce passes + passes: 3, + offset: 3., + noise: 0.02, + saturation: 1.5, + } + } +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct BlurPart { + #[knuffel(child)] + pub off: bool, + #[knuffel(child)] + pub on: bool, + #[knuffel(child, unwrap(argument))] + pub passes: Option, + #[knuffel(child, unwrap(argument))] + pub offset: Option>, + #[knuffel(child, unwrap(argument))] + pub noise: Option>, + #[knuffel(child, unwrap(argument))] + pub saturation: Option>, +} + +impl MergeWith for Blur { + fn merge_with(&mut self, part: &BlurPart) { + self.off |= part.off; + if part.on { + self.off = false; + } + + merge_clone!((self, part), passes); + merge!((self, part), offset, noise, saturation); + } +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct BackgroundEffectRule { + #[knuffel(child, unwrap(argument))] + pub xray: Option, + #[knuffel(child, unwrap(argument))] + pub blur: Option, + #[knuffel(child, unwrap(argument))] + pub noise: Option>, + #[knuffel(child, unwrap(argument))] + pub saturation: Option>, +} + +impl MergeWith for BackgroundEffectRule { + fn merge_with(&mut self, part: &Self) { + merge_clone_opt!((self, part), xray, blur, noise, saturation); + } +} + +/// 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, + + /// Whether to blur the background. + /// + /// - `None`: blur when the window/layer requests it (e.g. through ext-background-effect + /// protocol) + /// - `Some(false)`: never blur + /// - `Some(true)`: always blur + pub blur: Option, + + pub noise: Option, + pub saturation: Option, +} + +impl MergeWith for BackgroundEffect { + fn merge_with(&mut self, part: &BackgroundEffectRule) { + merge_clone_opt!((self, part), xray, blur); + + if let Some(x) = part.noise { + self.noise = Some(x.0); + } + + if let Some(x) = part.saturation { + self.saturation = Some(x.0); + } + } +} + #[cfg(test)] mod tests { use insta::{assert_debug_snapshot, assert_snapshot}; diff --git a/niri-config/src/layer_rule.rs b/niri-config/src/layer_rule.rs index c11edc137e..d0249358a1 100644 --- a/niri-config/src/layer_rule.rs +++ b/niri-config/src/layer_rule.rs @@ -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, #[knuffel(child, unwrap(argument))] pub baba_is_float: Option, + #[knuffel(child, default)] + pub background_effect: BackgroundEffectRule, } #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] @@ -28,4 +30,6 @@ pub struct Match { pub namespace: Option, #[knuffel(property)] pub at_startup: Option, + #[knuffel(property, str)] + pub layer: Option, } diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index b61fe1c1a0..a12c6be36c 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -78,6 +78,7 @@ pub struct Config { pub hotkey_overlay: HotkeyOverlay, pub config_notification: ConfigNotification, pub animations: Animations, + pub blur: Blur, pub gestures: Gestures, pub overview: Overview, pub environment: Environment, @@ -194,6 +195,7 @@ where "hotkey-overlay" => m_merge!(hotkey_overlay), "config-notification" => m_merge!(config_notification), "animations" => m_merge!(animations), + "blur" => m_merge!(blur), "gestures" => m_merge!(gestures), "overview" => m_merge!(overview), "xwayland-satellite" => m_merge!(xwayland_satellite), @@ -1616,6 +1618,13 @@ mod tests { }, ), }, + blur: Blur { + off: false, + passes: 3, + offset: 3.0, + noise: 0.02, + saturation: 1.5, + }, gestures: Gestures { dnd_edge_view_scroll: DndEdgeViewScroll { trigger_width: 10.0, @@ -1845,6 +1854,12 @@ mod tests { ), scroll_factor: None, tiled_state: None, + background_effect: BackgroundEffectRule { + xray: None, + blur: None, + noise: None, + saturation: None, + }, }, ], layer_rules: [ @@ -1859,6 +1874,7 @@ mod tests { ), ), at_startup: None, + layer: None, }, ], excludes: [], @@ -1879,6 +1895,12 @@ mod tests { geometry_corner_radius: None, place_within_backdrop: None, baba_is_float: None, + background_effect: BackgroundEffectRule { + xray: None, + blur: None, + noise: None, + saturation: None, + }, }, ], binds: Binds( diff --git a/niri-config/src/window_rule.rs b/niri-config/src/window_rule.rs index 0465d28fad..e6d2700cba 100644 --- a/niri-config/src/window_rule.rs +++ b/niri-config/src/window_rule.rs @@ -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>, #[knuffel(child, unwrap(argument))] pub tiled_state: Option, + #[knuffel(child, default)] + pub background_effect: BackgroundEffectRule, } #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 508456333d..b05f9994c0 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -1868,6 +1868,20 @@ impl FromStr for Transform { } } +impl FromStr for Layer { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "background" => Ok(Self::Background), + "bottom" => Ok(Self::Bottom), + "top" => Ok(Self::Top), + "overlay" => Ok(Self::Overlay), + _ => Err("invalid layer, can be \"background\", \"bottom\", \"top\" or \"overlay\""), + } + } +} + impl FromStr for ModeToSet { type Err = &'static str; diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs index 36ecd22008..81a936d1ed 100644 --- a/niri-visual-tests/src/cases/layout.rs +++ b/niri-visual-tests/src/cases/layout.rs @@ -3,7 +3,7 @@ use std::time::Duration; use niri::animation::Clock; use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options, SizingMode}; -use niri::render_helpers::RenderTarget; +use niri::render_helpers::{RenderCtx, RenderTarget}; use niri_config::{Color, OutputName, PresetSize}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; @@ -270,12 +270,15 @@ impl TestCase for Layout { self.layout.update_render_elements(Some(&self.output)); let mut rv = Vec::new(); + let ctx = RenderCtx { + renderer, + target: RenderTarget::Output, + xray: None, + }; self.layout .monitor_for_output(&self.output) .unwrap() - .render_workspaces(renderer, RenderTarget::Output, true, &mut |elem| { - rv.push(Box::new(elem) as _) - }); + .render_workspaces(ctx, true, &mut |elem| rv.push(Box::new(elem) as _)); rv } } diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index bc29a3509b..1f86681a92 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use std::time::Duration; use niri::layout::Options; -use niri::render_helpers::RenderTarget; +use niri::render_helpers::{RenderCtx, RenderTarget}; use niri_config::Color; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; @@ -121,13 +121,15 @@ impl TestCase for Tile { ); let mut rv = Vec::new(); - self.tile.render( + let ctx = RenderCtx { renderer, - location, - true, - RenderTarget::Output, - &mut |elem| rv.push(Box::new(elem) as _), - ); + target: RenderTarget::Output, + xray: None, + }; + self.tile + .render(ctx, location, location, 1., true, &mut |elem| { + rv.push(Box::new(elem) as _) + }); rv } } diff --git a/niri-visual-tests/src/cases/window.rs b/niri-visual-tests/src/cases/window.rs index b5abfa0d36..b9e141d920 100644 --- a/niri-visual-tests/src/cases/window.rs +++ b/niri-visual-tests/src/cases/window.rs @@ -1,5 +1,5 @@ use niri::layout::{LayoutElement, SizingMode}; -use niri::render_helpers::RenderTarget; +use niri::render_helpers::{RenderCtx, RenderTarget}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Physical, Point, Scale, Size}; @@ -53,14 +53,15 @@ impl TestCase for Window { .downscale(2.); let mut rv = Vec::new(); - self.window.render_normal( + let ctx = RenderCtx { renderer, - location, - Scale::from(1.), - 1., - RenderTarget::Output, - &mut |elem| rv.push(Box::new(elem) as _), - ); + target: RenderTarget::Output, + xray: None, + }; + self.window + .render_normal(ctx, location, Scale::from(1.), 1., &mut |elem| { + rv.push(Box::new(elem) as _) + }); rv } } diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index c08d7e8d90..efb71f833f 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -9,7 +9,7 @@ use niri::layout::{ use niri::render_helpers::offscreen::OffscreenData; use niri::render_helpers::renderer::NiriRenderer; use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; -use niri::render_helpers::RenderTarget; +use niri::render_helpers::RenderCtx; use niri::utils::transaction::Transaction; use niri::window::ResolvedWindowRules; use smithay::backend::renderer::element::Kind; @@ -151,11 +151,10 @@ impl LayoutElement for TestWindow { fn render_normal( &self, - _renderer: &mut R, + _ctx: RenderCtx, location: Point, _scale: Scale, alpha: f32, - _target: RenderTarget, push: &mut dyn FnMut(LayoutElementRenderElement), ) { let inner = self.inner.borrow(); diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 259dde3091..c1cb419bad 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -67,7 +67,7 @@ use crate::frame_clock::FrameClock; use crate::niri::{Niri, RedrawState, State}; use crate::render_helpers::debug::draw_damage; use crate::render_helpers::renderer::AsGlesRenderer; -use crate::render_helpers::{resources, shaders, RenderTarget}; +use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget}; use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation}; const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [ @@ -1839,8 +1839,12 @@ impl Tty { }; // Render the elements. - let mut elements = - niri.render::(&mut renderer, output, true, RenderTarget::Output); + let ctx = RenderCtx { + renderer: &mut renderer, + target: RenderTarget::Output, + xray: None, + }; + let mut elements = niri.render_to_vec(ctx, output, true); // Visualize the damage, if enabled. if niri.debug_draw_damage { diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 92132bd416..cf55889485 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -20,7 +20,7 @@ use smithay::wayland::presentation::Refresh; use super::{IpcOutputMap, OutputId, RenderResult}; use crate::niri::{Niri, RedrawState, State}; use crate::render_helpers::debug::draw_damage; -use crate::render_helpers::{resources, shaders, RenderTarget}; +use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget}; use crate::utils::{get_monotonic_time, logical_output}; pub struct Winit { @@ -180,12 +180,12 @@ impl Winit { let _span = tracy_client::span!("Winit::render"); // Render the elements. - let mut elements = niri.render::( - self.backend.renderer(), - output, - true, - RenderTarget::Output, - ); + let ctx = RenderCtx { + renderer: self.backend.renderer(), + target: RenderTarget::Output, + xray: None, + }; + let mut elements = niri.render_to_vec(ctx, output, true); // Visualize the damage, if enabled. if niri.debug_draw_damage { diff --git a/src/handlers/background_effect.rs b/src/handlers/background_effect.rs new file mode 100644 index 0000000000..87e3c4feb1 --- /dev/null +++ b/src/handlers/background_effect.rs @@ -0,0 +1,158 @@ +use std::sync::{Arc, Mutex}; + +use smithay::delegate_background_effect; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Logical, Point, Rectangle, Size}; +use smithay::wayland::background_effect::{ + self, BackgroundEffectSurfaceCachedState, ExtBackgroundEffectHandler, +}; +use smithay::wayland::compositor::{ + add_post_commit_hook, with_states, RegionAttributes, SurfaceData, +}; + +use crate::delegate_kde_blur; +use crate::niri::State; +use crate::protocols::kde_blur::{KdeBlurHandler, KdeBlurRegion, KdeBlurSurfaceCachedState}; +use crate::utils::region::region_to_non_overlapping_rects; + +/// Per-surface cache for processed blur region (non-overlapping rects). +#[derive(Default)] +struct CachedBlurRegionUserData(Mutex); + +#[derive(Default)] +struct CachedBlurRegionInner { + /// Whether a region change is pending to be committed. + pending_dirty: bool, + /// Whether the region must be recomputed. + dirty: bool, + /// Whether the post-commit hook has been registered for this surface. + hook_registered: bool, + /// Cached non-overlapping rects in surface-local coordinates. + /// + /// `None` means there's no blur region. + rects: Option>>>, +} + +/// Gets the cached blur region for a surface, lazily recomputing if dirty. +pub fn get_cached_blur_region(states: &SurfaceData) -> Option>>> { + let cache = states + .data_map + .get_or_insert_threadsafe(CachedBlurRegionUserData::default); + let mut guard = cache.0.lock().unwrap(); + + if guard.dirty { + guard.dirty = false; + recompute_blur_region(states, &mut guard); + } + + guard.rects.clone() +} + +fn recompute_blur_region(states: &SurfaceData, inner: &mut CachedBlurRegionInner) { + let cached = &states.cached_state; + + let rects = if let Some(arc) = &mut inner.rects { + if Arc::strong_count(arc) > 1 { + debug!("cloning rects due to non-unique reference"); + } + arc + } else { + inner.rects.insert(Arc::new(Vec::new())) + }; + let rects = Arc::make_mut(rects); + + // Prefer ext-background-effect. + if cached.has::() { + let mut guard = cached.get::(); + if let Some(region) = &guard.current().blur_region { + region_to_non_overlapping_rects(region, rects); + } else { + inner.rects = None; + } + return; + } + + if cached.has::() { + let mut guard = cached.get::(); + match &guard.current().blur_region { + Some(KdeBlurRegion::WholeSurface) => { + // Store a single "infinite" rect that gets naturally clipped. + let infinite = Rectangle::new( + Point::new(-i32::MAX / 2, -i32::MAX / 2), + Size::new(i32::MAX, i32::MAX), + ); + rects.clear(); + rects.push(infinite); + } + Some(KdeBlurRegion::Region(region)) => { + region_to_non_overlapping_rects(region, rects); + } + None => { + inner.rects = None; + } + } + return; + } + + // Neither is present. + inner.rects = None; +} + +fn mark_blur_region_pending_dirty(wl_surface: &WlSurface) { + let register_hook = with_states(wl_surface, |states| { + let cache = states + .data_map + .get_or_insert_threadsafe(CachedBlurRegionUserData::default); + let mut guard = cache.0.lock().unwrap(); + guard.pending_dirty = true; + + if guard.hook_registered { + false + } else { + guard.hook_registered = true; + true + } + }); + + if register_hook { + add_post_commit_hook::(wl_surface, |_state, _dh, surface| { + with_states(surface, |states| { + if let Some(cache) = states.data_map.get::() { + let mut guard = cache.0.lock().unwrap(); + if guard.pending_dirty { + guard.pending_dirty = false; + guard.dirty = true; + } + } else { + error!("unexpected missing CachedBlurRegionUserData"); + } + }); + }); + } +} + +impl ExtBackgroundEffectHandler for State { + fn capabilities(&self) -> background_effect::Capability { + background_effect::Capability::Blur + } + + fn set_blur_region(&mut self, wl_surface: WlSurface, _region: RegionAttributes) { + mark_blur_region_pending_dirty(&wl_surface); + } + + fn unset_blur_region(&mut self, wl_surface: WlSurface) { + mark_blur_region_pending_dirty(&wl_surface); + } +} +delegate_background_effect!(State); + +impl KdeBlurHandler for State { + fn set_blur_region(&mut self, wl_surface: WlSurface) { + mark_blur_region_pending_dirty(&wl_surface); + } + + fn unset_blur_region(&mut self, wl_surface: WlSurface) { + mark_blur_region_pending_dirty(&wl_surface); + } +} +delegate_kde_blur!(State); diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index daf342296f..d70ed5f9a2 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -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()); } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 8d67e6a047..4a48778130 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,3 +1,4 @@ +pub mod background_effect; mod compositor; mod layer_shell; mod xdg_shell; diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 8512f85107..1182713bf1 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -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| { diff --git a/src/input/pick_color_grab.rs b/src/input/pick_color_grab.rs index 100224c991..32132358c7 100644 --- a/src/input/pick_color_grab.rs +++ b/src/input/pick_color_grab.rs @@ -13,7 +13,7 @@ use smithay::input::SeatHandler; use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform}; use crate::niri::State; -use crate::render_helpers::{render_and_download, RenderTarget}; +use crate::render_helpers::{render_and_download, RenderCtx, RenderTarget}; pub struct PickColorGrab { start_data: PointerGrabStartData, @@ -49,13 +49,13 @@ impl PickColorGrab { let pos = pos_within_output.to_physical_precise_floor(scale); let size = Size::::from((1, 1)); - let elements = data.niri.render( + let ctx = RenderCtx { renderer, - &output, - false, // This is an interactive operation so we can render without blocking out. - RenderTarget::Output, - ); + target: RenderTarget::Output, + xray: None, + }; + let elements = data.niri.render_to_vec(ctx, &output, false); let mapping = match render_and_download( renderer, diff --git a/src/layer/mapped.rs b/src/layer/mapped.rs index c59baf13f1..af37e62c1f 100644 --- a/src/layer/mapped.rs +++ b/src/layer/mapped.rs @@ -1,20 +1,26 @@ +use std::sync::Arc; + use niri_config::utils::MergeWith as _; -use niri_config::{Config, LayerRule}; +use niri_config::{Config, CornerRadius, LayerRule}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::Kind; +use smithay::backend::renderer::utils::RendererSurfaceStateUserData; use smithay::desktop::{LayerSurface, PopupManager}; -use smithay::utils::{Logical, Point, Scale, Size}; +use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; +use smithay::wayland::compositor::with_states; use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer}; use super::ResolvedLayerRules; use crate::animation::Clock; +use crate::handlers::background_effect::get_cached_blur_region; use crate::layout::shadow::Shadow; use crate::niri_render_elements; +use crate::render_helpers::background_effect::{BackgroundEffect, 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::RenderTarget; +use crate::render_helpers::{background_effect, RenderCtx}; use crate::utils::{baba_is_float_offset, round_logical_in_physical}; #[derive(Debug)] @@ -31,6 +37,9 @@ pub struct MappedLayer { /// The shadow around the surface. shadow: Shadow, + /// The background effect, like blur, behind the layer-surface. + background_effect: BackgroundEffect, + /// The view size for the layer surface's output. view_size: Size, @@ -46,6 +55,7 @@ niri_render_elements! { Wayland = WaylandSurfaceRenderElement, SolidColor = SolidColorRenderElement, Shadow = ShadowRenderElement, + BackgroundEffect = BackgroundEffectElement, } } @@ -70,6 +80,7 @@ impl MappedLayer { view_size, scale, shadow: Shadow::new(shadow_config), + background_effect: BackgroundEffect::new(), clock, } } @@ -80,6 +91,8 @@ impl MappedLayer { shadow_config.on = false; shadow_config.merge_with(&self.rules.shadow); self.shadow.update_config(shadow_config); + + self.background_effect.update_config(config.blur); } pub fn update_shaders(&mut self) { @@ -103,6 +116,13 @@ impl MappedLayer { // FIXME: is_active based on keyboard focus? self.shadow .update_render_elements(size, true, radius, self.scale, 1.); + + let has_blur_region = self.blur_region().is_some_and(|r| !r.is_empty()); + self.background_effect.update_render_elements( + radius, + self.rules.background_effect, + has_blur_region, + ); } pub fn are_animations_ongoing(&self) -> bool { @@ -157,16 +177,18 @@ impl MappedLayer { pub fn render_normal( &self, - renderer: &mut R, + mut ctx: RenderCtx, location: Point, - target: RenderTarget, + mut pos_in_backdrop: Point, + zoom: f64, push: &mut dyn FnMut(LayerSurfaceRenderElement), ) { let scale = Scale::from(self.scale); let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.); let location = location + self.bob_offset(); + pos_in_backdrop += self.bob_offset().upscale(zoom); - if target.should_block_out(self.rules.block_out_from) { + if ctx.target.should_block_out(self.rules.block_out_from) { // Round to physical pixels. let location = location.to_physical_precise_round(scale).to_logical(scale); @@ -184,7 +206,7 @@ impl MappedLayer { let surface = self.surface.wl_surface(); push_elements_from_surface_tree( - renderer, + ctx.renderer, surface, buf_pos.to_physical_precise_round(scale), scale, @@ -196,21 +218,76 @@ impl MappedLayer { let location = location.to_physical_precise_round(scale).to_logical(scale); self.shadow - .render(renderer, location, &mut |elem| push(elem.into())); + .render(ctx.renderer, location, &mut |elem| push(elem.into())); + + if self.background_effect.is_visible() { + let area = Rectangle::new(location, self.block_out_buffer.size()); + // Effects not requested by the surface itself are drawn to match the geometry. + let mut clip = true; + + // FIXME: support blur regions on subsurfaces in addition to the main surface. + let mut subregion = None; + let blur_geometry = if let Some(rects) = self.blur_region() { + if rects.is_empty() { + // Surface has a set, but empty blur region. + None + } else { + // If the surface itself requests the effects, apply different defaults. + clip = false; + + // Use geometry-shaped blur for blocked-out layers to avoid unintentionally + // leaking any surface shapes. We render those layers as geometry-shaped solid + // rectangles anyway. + if ctx.target.should_block_out(self.rules.block_out_from) { + clip = true; + Some(area) + } else { + let mut main_surface_geo = self.main_surface_geo().to_f64(); + main_surface_geo.loc += area.loc; + + subregion = Some(background_effect::EffectSubregion { + rects, + scale: Scale::from(1.), + offset: main_surface_geo.loc, + }); + + main_surface_geo = main_surface_geo + .to_physical_precise_round(self.scale) + .to_logical(self.scale); + Some(main_surface_geo) + } + } + } else { + Some(area) + }; + + if let Some(geometry) = blur_geometry { + pos_in_backdrop += (geometry.loc - area.loc).upscale(zoom); + let params = background_effect::RenderParams { + geometry, + subregion, + clip: clip.then_some((area, CornerRadius::default())), + pos_in_backdrop, + zoom, + scale: self.scale, + }; + self.background_effect + .render(ctx.as_gles(), params, &mut |elem| push(elem.into())); + } + } } pub fn render_popups( &self, - renderer: &mut R, + ctx: RenderCtx, location: Point, - target: RenderTarget, push: &mut dyn FnMut(LayerSurfaceRenderElement), ) { let scale = Scale::from(self.scale); let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.); let location = location + self.bob_offset(); - if target.should_block_out(self.rules.block_out_from) { + if ctx.target.should_block_out(self.rules.block_out_from) { return; } @@ -223,7 +300,7 @@ impl MappedLayer { let offset = popup_offset - popup.geometry().loc; push_elements_from_surface_tree( - renderer, + ctx.renderer, popup.wl_surface(), (buf_pos + offset.to_f64()).to_physical_precise_round(scale), scale, @@ -233,4 +310,20 @@ impl MappedLayer { ); } } + + fn main_surface_geo(&self) -> Rectangle { + with_states(self.surface.wl_surface(), |states| { + let data = states.data_map.get::(); + data.and_then(|d| d.lock().unwrap().view()) + .map(|view| Rectangle { + loc: view.offset, + size: view.dst, + }) + }) + .unwrap_or_default() + } + + fn blur_region(&self) -> Option>>> { + with_states(self.surface.wl_surface(), get_cached_blur_region) + } } diff --git a/src/layer/mod.rs b/src/layer/mod.rs index b74b56088e..aedf4bf7f6 100644 --- a/src/layer/mod.rs +++ b/src/layer/mod.rs @@ -1,7 +1,8 @@ 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; pub mod mapped; pub use mapped::MappedLayer; @@ -26,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 { @@ -70,6 +74,10 @@ impl ResolvedLayerRules { } resolved.shadow.merge_with(&rule.shadow); + + resolved + .background_effect + .merge_with(&rule.background_effect); } resolved @@ -83,5 +91,17 @@ fn surface_matches(surface: &LayerSurface, m: &Match) -> bool { } } + if let Some(layer) = m.layer { + let surface_layer = match surface.layer() { + Layer::Background => niri_ipc::Layer::Background, + Layer::Bottom => niri_ipc::Layer::Bottom, + Layer::Top => niri_ipc::Layer::Top, + Layer::Overlay => niri_ipc::Layer::Overlay, + }; + if layer != surface_layer { + return false; + } + } + true } diff --git a/src/layout/closing_window.rs b/src/layout/closing_window.rs index b61b6c8dfb..ca28f5982f 100644 --- a/src/layout/closing_window.rs +++ b/src/layout/closing_window.rs @@ -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, RenderTarget}; +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, + /// 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>, + /// Blocked-out contents of the window. blocked_out_buffer: TextureBuffer, @@ -44,6 +50,9 @@ pub struct ClosingWindow { /// How much the texture should be offset. buffer_offset: Point, + /// How much the texture with blocked-out bg should be offset. + buffer_with_blocked_out_bg_offset: Point, + /// How much the blocked-out texture should be offset. blocked_out_buffer_offset: Point, @@ -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(), @@ -159,13 +178,17 @@ impl ClosingWindow { pub fn render( &self, - renderer: &mut GlesRenderer, + ctx: RenderCtx, view_rect: Rectangle, scale: Scale, - target: RenderTarget, ) -> ClosingWindowRenderElement { - let (buffer, offset) = if target.should_block_out(self.block_out_from) { + 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) }; @@ -200,7 +223,10 @@ impl ClosingWindow { let progress = anim.value(); let clamped_progress = anim.clamped_value().clamp(0., 1.); - if Shaders::get(renderer).program(ProgramType::Close).is_some() { + if Shaders::get(ctx.renderer) + .program(ProgramType::Close) + .is_some() + { let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32); let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32); diff --git a/src/layout/floating.rs b/src/layout/floating.rs index 3fac30f22b..6fee6ad40c 100644 --- a/src/layout/floating.rs +++ b/src/layout/floating.rs @@ -18,7 +18,7 @@ use super::{ use crate::animation::{Animation, Clock}; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; -use crate::render_helpers::RenderTarget; +use crate::render_helpers::RenderCtx; use crate::utils::transaction::TransactionBlocker; use crate::utils::{ center_preferring_top_left_in_area, clamp_preferring_top_left_in_area, ensure_min_max_size, @@ -1055,9 +1055,10 @@ impl FloatingSpace { pub fn render( &self, - renderer: &mut R, + mut ctx: RenderCtx, + pos_in_backdrop: Point, + zoom: f64, view_rect: Rectangle, - target: RenderTarget, focus_ring: bool, push: &mut dyn FnMut(FloatingSpaceRenderElement), ) { @@ -1067,7 +1068,7 @@ impl FloatingSpace { // // FIXME: I guess this should rather preserve the stacking order when the window is closed. for closing in self.closing_windows.iter().rev() { - let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target); + let elem = closing.render(ctx.as_gles(), view_rect, scale); push(elem.into()); } @@ -1076,9 +1077,15 @@ impl FloatingSpace { // For the active tile, draw the focus ring. let focus_ring = focus_ring && Some(tile.window().id()) == active.as_ref(); - tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| { - push(elem.into()) - }); + let pos_in_backdrop = pos_in_backdrop + tile_pos.upscale(zoom); + tile.render( + ctx.r(), + tile_pos, + pos_in_backdrop, + zoom, + focus_ring, + &mut |elem| push(elem.into()), + ); } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 010fe65b7c..80abbb93c4 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -34,6 +34,7 @@ use std::collections::HashMap; use std::mem; use std::rc::Rc; +use std::sync::Arc; use std::time::Duration; use monitor::{InsertHint, InsertPosition, InsertWorkspace, MonitorAddWindowTarget}; @@ -64,7 +65,8 @@ 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::{BakedBuffer, RenderTarget}; +use crate::render_helpers::xray::Xray; +use crate::render_helpers::{BakedBuffer, RenderCtx}; use crate::rubber_band::RubberBand; use crate::utils::transaction::{Transaction, TransactionBlocker}; use crate::utils::{ @@ -97,6 +99,12 @@ const INTERACTIVE_MOVE_START_THRESHOLD: f64 = 256. * 256.; /// Opacity of interactively moved tiles targeting the scrolling layout. const INTERACTIVE_MOVE_ALPHA: f64 = 0.75; +/// Mutter-like resistance threshold for dragging floating windows towards working area edges. +const FLOATING_EDGE_RESISTANCE_TOWARDS_WORKING_AREA: f64 = 32.; + +/// Resistance threshold for movement away from working area edges. +const FLOATING_EDGE_RESISTANCE_AWAY_WORKING_AREA: f64 = 0.; + /// Amount of touchpad movement to toggle the overview. const OVERVIEW_GESTURE_MOVEMENT: f64 = 300.; @@ -154,41 +162,38 @@ pub trait LayoutElement { /// location. fn render( &self, - renderer: &mut R, + mut ctx: RenderCtx, location: Point, scale: Scale, alpha: f32, - target: RenderTarget, push: &mut dyn FnMut(LayoutElementRenderElement), ) { - self.render_popups(renderer, location, scale, alpha, target, push); - self.render_normal(renderer, location, scale, alpha, target, push); + self.render_popups(ctx.r(), location, scale, alpha, push); + self.render_normal(ctx.r(), location, scale, alpha, push); } /// Renders the non-popup parts of the element. fn render_normal( &self, - renderer: &mut R, + ctx: RenderCtx, location: Point, scale: Scale, alpha: f32, - target: RenderTarget, push: &mut dyn FnMut(LayoutElementRenderElement), ) { - let _ = (renderer, location, scale, alpha, target, push); + let _ = (ctx, location, scale, alpha, push); } /// Renders the popups of the element. fn render_popups( &self, - renderer: &mut R, + ctx: RenderCtx, location: Point, scale: Scale, alpha: f32, - target: RenderTarget, push: &mut dyn FnMut(LayoutElementRenderElement), ) { - let _ = (renderer, location, scale, alpha, target, push); + let _ = (ctx, location, scale, alpha, push); } /// Requests the element to change its size. @@ -288,6 +293,16 @@ pub trait LayoutElement { fn cancel_interactive_resize(&mut self); fn interactive_resize_data(&self) -> Option; + /// Blur region (non-overlapping rects) under the main surface of this window. + fn blur_region(&self) -> Option>>> { + None + } + + /// Returns the geometry of this window's main surface relative to the visual geometry. + fn main_surface_geo(&self) -> Rectangle { + Rectangle::from_size(self.size()) + } + fn on_commit(&mut self, serial: Serial); } @@ -351,6 +366,7 @@ pub struct Options { pub animations: niri_config::Animations, pub gestures: niri_config::Gestures, pub overview: niri_config::Overview, + pub blur: niri_config::Blur, // Debug flags. pub disable_resize_throttling: bool, pub disable_transactions: bool, @@ -550,19 +566,148 @@ impl InteractiveMoveState { } impl InteractiveMoveData { - fn tile_render_location(&self, zoom: f64) -> Point { - let scale = Scale::from(self.output.current_scale().fractional_scale()); + fn pointer_offset_within_output(&self, zoom: f64) -> Point { let window_size = self.tile.window_size(); let pointer_offset_within_window = Point::from(( window_size.w * self.pointer_ratio_within_window.0, window_size.h * self.pointer_ratio_within_window.1, )); - let pos = self.pointer_pos_within_output - - (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset()) - .upscale(zoom); + + (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset()) + .upscale(zoom) + } + + fn tile_render_location_for_pointer( + &self, + pointer_pos_within_output: Point, + zoom: f64, + ) -> Point { + let scale = Scale::from(self.output.current_scale().fractional_scale()); + let pos = pointer_pos_within_output - self.pointer_offset_within_output(zoom); // Round to physical pixels. pos.to_physical_precise_round(scale).to_logical(scale) } + + fn pointer_pos_for_tile_render_location( + &self, + tile_render_location: Point, + zoom: f64, + ) -> Point { + tile_render_location + self.pointer_offset_within_output(zoom) + } + + fn tile_render_location(&self, zoom: f64) -> Point { + self.tile_render_location_for_pointer(self.pointer_pos_within_output, zoom) + } +} + +#[derive(Debug, Clone, Copy)] +enum ResistanceSide { + Left, + Right, + Top, + Bottom, +} + +fn is_moving_towards_side(side: ResistanceSide, old_pos: f64, new_pos: f64) -> bool { + match side { + ResistanceSide::Left | ResistanceSide::Top => new_pos < old_pos, + ResistanceSide::Right | ResistanceSide::Bottom => new_pos > old_pos, + } +} + +fn apply_axis_edge_resistance( + side: ResistanceSide, + old_pos: f64, + new_pos: f64, + edge_pos: f64, +) -> f64 { + if old_pos == new_pos { + return new_pos; + } + + let moving_towards = is_moving_towards_side(side, old_pos, new_pos); + let threshold = if moving_towards { + FLOATING_EDGE_RESISTANCE_TOWARDS_WORKING_AREA + } else { + FLOATING_EDGE_RESISTANCE_AWAY_WORKING_AREA + }; + let crossed_edge = match side { + ResistanceSide::Left | ResistanceSide::Top => old_pos >= edge_pos && new_pos <= edge_pos, + ResistanceSide::Right | ResistanceSide::Bottom => { + old_pos <= edge_pos && new_pos >= edge_pos + } + }; + + if moving_towards && crossed_edge && f64::abs(edge_pos - new_pos) < threshold { + edge_pos + } else { + new_pos + } +} + +fn pick_stricter_translation(a: f64, b: f64) -> f64 { + if f64::abs(a) < f64::abs(b) { + a + } else { + b + } +} + +fn ranges_overlap_or_touch(start_a: f64, end_a: f64, start_b: f64, end_b: f64) -> bool { + start_a <= end_b && start_b <= end_a +} + +fn apply_floating_working_area_edge_resistance( + old_rect: Rectangle, + new_rect: Rectangle, + working_area: Rectangle, +) -> Point { + let old_left = old_rect.loc.x; + let old_right = old_rect.loc.x + old_rect.size.w; + let old_top = old_rect.loc.y; + let old_bottom = old_rect.loc.y + old_rect.size.h; + + let mut new_left = new_rect.loc.x; + let mut new_right = new_rect.loc.x + new_rect.size.w; + let mut new_top = new_rect.loc.y; + let new_bottom = new_rect.loc.y + new_rect.size.h; + + let area_left = working_area.loc.x; + let area_right = working_area.loc.x + working_area.size.w; + let area_top = working_area.loc.y; + let area_bottom = working_area.loc.y + working_area.size.h; + + let old_vert_overlaps_area = + ranges_overlap_or_touch(old_top, old_bottom, area_top, area_bottom); + let new_vert_overlaps_area = + ranges_overlap_or_touch(new_top, new_bottom, area_top, area_bottom); + if old_vert_overlaps_area || new_vert_overlaps_area { + let resisted_left = + apply_axis_edge_resistance(ResistanceSide::Left, old_left, new_left, area_left); + let resisted_right = + apply_axis_edge_resistance(ResistanceSide::Right, old_right, new_right, area_right); + let x_change = + pick_stricter_translation(resisted_left - old_left, resisted_right - old_right); + new_left = old_left + x_change; + new_right = old_right + x_change; + } + + let old_horiz_overlaps_area = + ranges_overlap_or_touch(old_left, old_right, area_left, area_right); + let new_horiz_overlaps_area = + ranges_overlap_or_touch(new_left, new_right, area_left, area_right); + if old_horiz_overlaps_area || new_horiz_overlaps_area { + let resisted_top = + apply_axis_edge_resistance(ResistanceSide::Top, old_top, new_top, area_top); + let resisted_bottom = + apply_axis_edge_resistance(ResistanceSide::Bottom, old_bottom, new_bottom, area_bottom); + let y_change = + pick_stricter_translation(resisted_top - old_top, resisted_bottom - old_bottom); + new_top = old_top + y_change; + } + + Point::from((new_left, new_top)) } impl ActivateWindow { @@ -611,6 +756,7 @@ impl Options { animations: config.animations.clone(), gestures: config.gestures, overview: config.overview, + blur: config.blur, disable_resize_throttling: config.debug.disable_resize_throttling, disable_transactions: config.debug.disable_transactions, deactivate_unfocused_windows: config.debug.deactivate_unfocused_windows, @@ -3988,6 +4134,7 @@ impl Layout { } } + let mut output_changed = false; if output != move_.output { move_.tile.window().output_leave(&move_.output); move_.tile.window().output_enter(&output); @@ -3997,6 +4144,7 @@ impl Layout { ); move_.output = output.clone(); self.focus_output(&output); + output_changed = true; move_.output_config = self .monitor_for_output(&output) @@ -4015,6 +4163,46 @@ impl Layout { move_.tile.update_config(view_size, scale, Rc::new(options)); } + let mut pointer_pos_within_output = pointer_pos_within_output; + if move_.is_floating && self.overview_progress.is_none() { + if let Some(mon) = self.monitor_for_output(&output) { + // Keep resistance anchored to the previous workspace while sticking to its + // edge, unless the output changed and we cannot use the previous pointer. + let workspace_lookup_pos = if output_changed { + pointer_pos_within_output + } else { + move_.pointer_pos_within_output + }; + + if let Some((ws, ws_geo)) = mon.workspace_under(workspace_lookup_pos) { + let zoom = mon.overview_zoom(); + let ws_working_area = ws.working_area(); + let working_area = Rectangle::new( + ws_geo.loc + ws_working_area.loc.upscale(zoom), + ws_working_area.size.upscale(zoom), + ); + + let old_tile_pos = move_.tile_render_location(zoom); + let new_tile_pos = move_ + .tile_render_location_for_pointer(pointer_pos_within_output, zoom); + let tile_size = move_.tile.tile_size().upscale(zoom); + let old_rect = Rectangle::new(old_tile_pos, tile_size); + let new_rect = Rectangle::new(new_tile_pos, tile_size); + + let resisted_tile_pos = apply_floating_working_area_edge_resistance( + old_rect, + new_rect, + working_area, + ); + + if resisted_tile_pos != new_tile_pos { + pointer_pos_within_output = move_ + .pointer_pos_for_tile_render_location(resisted_tile_pos, zoom); + } + } + } + } + move_.pointer_pos_within_output = pointer_pos_within_output; self.interactive_move = Some(InteractiveMoveState::Moving(move_)); @@ -4596,12 +4784,27 @@ impl Layout { } } - 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_in_backdrop = move_.tile_render_location(zoom); + move_.tile.store_unmap_snapshot_if_empty( + renderer, + xray, + xray_has_blocked_out_layers, + pos_in_backdrop, + zoom, + ); return; } } @@ -4609,9 +4812,16 @@ impl Layout { 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, + window, + geo.loc, + zoom, + ); return; } } @@ -4620,7 +4830,14 @@ impl Layout { 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, + window, + Point::new(0., 0.), + zoom, + ); return; } } @@ -4721,9 +4938,8 @@ impl Layout { pub fn render_interactive_move_for_output( &self, - renderer: &mut R, + ctx: RenderCtx, output: &Output, - target: RenderTarget, push: &mut dyn FnMut(RescaleRenderElement>), ) { if self.update_render_elements_time != self.clock.now() { @@ -4740,16 +4956,22 @@ impl Layout { 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(renderer, location, true, target, &mut |elem| { + let pos_in_backdrop = move_.tile_render_location(zoom); + + move_.tile.render( + ctx, + pos_in_backdrop, + pos_in_backdrop, + zoom, + true, + &mut |elem| { push(RescaleRenderElement::from_element( elem, - location.to_physical_precise_round(scale), + pos_in_backdrop.to_physical_precise_round(scale), zoom, )); - }); + }, + ); } pub fn refresh(&mut self, is_active: bool) { diff --git a/src/layout/monitor.rs b/src/layout/monitor.rs index 420dad6706..4ed22dd977 100644 --- a/src/layout/monitor.rs +++ b/src/layout/monitor.rs @@ -24,7 +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::RenderTarget; +use crate::render_helpers::RenderCtx; use crate::rubber_band::RubberBand; use crate::utils::transaction::Transaction; use crate::utils::{ @@ -1669,8 +1669,7 @@ impl Monitor { pub fn render_workspaces( &self, - renderer: &mut R, - target: RenderTarget, + mut ctx: RenderCtx, focus_ring: bool, push: &mut dyn FnMut(MonitorRenderElement), ) { @@ -1734,16 +1733,16 @@ impl Monitor { }}; } - ws.render_floating(renderer, target, focus_ring, push!()); + ws.render_floating(ctx.r(), geo.loc, zoom, focus_ring, push!()); if let Some(loc) = insert_hint_render_loc { if loc.workspace == InsertWorkspace::Existing(ws.id()) { self.insert_hint_element - .render(renderer, loc.location, push!()); + .render(ctx.renderer, loc.location, push!()); } } - ws.render_scrolling(renderer, target, focus_ring, push!()); + ws.render_scrolling(ctx.r(), geo.loc, zoom, focus_ring, push!()); } } diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index 69d0ed62a7..801f1b3f2d 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -21,7 +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::RenderTarget; +use crate::render_helpers::RenderCtx; use crate::utils::transaction::{Transaction, TransactionBlocker}; use crate::utils::ResizeEdge; use crate::window::ResolvedWindowRules; @@ -2899,8 +2899,9 @@ impl ScrollingSpace { pub fn render( &self, - renderer: &mut R, - target: RenderTarget, + mut ctx: RenderCtx, + pos_in_backdrop: Point, + zoom: f64, focus_ring: bool, push: &mut dyn FnMut(ScrollingSpaceRenderElement), ) { @@ -2909,7 +2910,7 @@ impl ScrollingSpace { // Draw the closing windows on top of the other windows. let view_rect = Rectangle::new(Point::from((self.view_pos(), 0.)), self.view_size); for closing in self.closing_windows.iter().rev() { - let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target); + let elem = closing.render(ctx.as_gles(), view_rect, scale); push(elem.into()); } @@ -2930,7 +2931,7 @@ impl ScrollingSpace { let pos = view_off + col_off + col_render_off; let pos = pos.to_physical_precise_round(scale).to_logical(scale); col.tab_indicator - .render(renderer, pos, &mut |elem| push(elem.into())); + .render(ctx.renderer, pos, &mut |elem| push(elem.into())); } for (tile, tile_off, visible) in col.tiles_in_render_order() { @@ -2955,9 +2956,15 @@ impl ScrollingSpace { continue; } - tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| { - push(elem.into()) - }); + let pos_in_backdrop = pos_in_backdrop + tile_pos.upscale(zoom); + tile.render( + ctx.r(), + tile_pos, + pos_in_backdrop, + zoom, + focus_ring, + &mut |elem| push(elem.into()), + ); } } } diff --git a/src/layout/tests.rs b/src/layout/tests.rs index e22e860e5d..7af7d22f38 100644 --- a/src/layout/tests.rs +++ b/src/layout/tests.rs @@ -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(), })); } @@ -1648,6 +1650,14 @@ fn check_ops_with_options( layout } +#[track_caller] +fn moving_pointer_pos(layout: &Layout) -> Point { + let Some(InteractiveMoveState::Moving(move_)) = &layout.interactive_move else { + panic!("interactive move must be in moving state"); + }; + move_.pointer_pos_within_output +} + #[test] fn operations_dont_panic() { if std::env::var_os("RUN_SLOW_TESTS").is_none() { @@ -3029,6 +3039,158 @@ fn interactive_move_from_workspace_with_layout_config() { check_ops(ops); } +#[test] +fn floating_edge_resistance_sticks_and_releases() { + let area = Rectangle::from_size(Size::from((1280., 720.))); + let size = Size::from((100., 200.)); + + let old = Rectangle::new(Point::from((40., 100.)), size); + let new = Rectangle::new(Point::from((20., 100.)), size); + let resisted = apply_floating_working_area_edge_resistance(old, new, area); + assert_eq!(resisted, Point::from((20., 100.))); + + let old = Rectangle::new(Point::from((20., 100.)), size); + let new = Rectangle::new(Point::from((-10., 100.)), size); + let resisted = apply_floating_working_area_edge_resistance(old, new, area); + assert_eq!(resisted, Point::from((0., 100.))); + + let old = Rectangle::new(Point::from((0., 100.)), size); + let new = Rectangle::new(Point::from((-31., 100.)), size); + let resisted = apply_floating_working_area_edge_resistance(old, new, area); + assert_eq!(resisted, Point::from((0., 100.))); + + let old = Rectangle::new(Point::from((0., 100.)), size); + let new = Rectangle::new(Point::from((-33., 100.)), size); + let resisted = apply_floating_working_area_edge_resistance(old, new, area); + assert_eq!(resisted, Point::from((-33., 100.))); +} + +#[test] +fn floating_edge_resistance_does_not_apply_away_from_edge() { + let area = Rectangle::from_size(Size::from((1280., 720.))); + let size = Size::from((100., 200.)); + + let old = Rectangle::new(Point::from((10., 100.)), size); + let new = Rectangle::new(Point::from((20., 100.)), size); + let resisted = apply_floating_working_area_edge_resistance(old, new, area); + assert_eq!(resisted, Point::from((20., 100.))); +} + +#[test] +fn pick_stricter_translation_prefers_smaller_magnitude() { + assert_eq!(pick_stricter_translation(-5., -12.), -5.); + assert_eq!(pick_stricter_translation(7., -3.), -3.); + assert_eq!(pick_stricter_translation(4., -4.), -4.); +} + +#[test] +fn floating_interactive_move_resists_working_area_edges() { + let mut layout = Layout::default(); + Op::AddOutput(1).apply(&mut layout); + Op::AddOutput(2).apply(&mut layout); + Op::AddWindow { + params: TestWindowParams { + is_floating: true, + ..TestWindowParams::new(1) + }, + } + .apply(&mut layout); + + Op::InteractiveMoveBegin { + window: 1, + output_idx: 1, + px: 0., + py: 0., + } + .apply(&mut layout); + Op::InteractiveMoveUpdate { + window: 1, + dx: 0., + dy: 0., + output_idx: 2, + px: 40., + py: 40., + } + .apply(&mut layout); + + Op::InteractiveMoveUpdate { + window: 1, + dx: 0., + dy: 0., + output_idx: 2, + px: 20., + py: 20., + } + .apply(&mut layout); + assert_eq!(moving_pointer_pos(&layout), Point::from((20., 20.))); + + Op::InteractiveMoveUpdate { + window: 1, + dx: 0., + dy: 0., + output_idx: 2, + px: -10., + py: -10., + } + .apply(&mut layout); + assert_eq!(moving_pointer_pos(&layout), Point::from((0., 0.))); + + Op::InteractiveMoveUpdate { + window: 1, + dx: 0., + dy: 0., + output_idx: 2, + px: -40., + py: -40., + } + .apply(&mut layout); + assert_eq!(moving_pointer_pos(&layout), Point::from((-40., -40.))); +} + +#[test] +fn floating_interactive_move_has_no_resistance_in_overview() { + let mut layout = Layout::default(); + Op::AddOutput(1).apply(&mut layout); + Op::AddOutput(2).apply(&mut layout); + Op::AddWindow { + params: TestWindowParams { + is_floating: true, + ..TestWindowParams::new(1) + }, + } + .apply(&mut layout); + + Op::InteractiveMoveBegin { + window: 1, + output_idx: 1, + px: 0., + py: 0., + } + .apply(&mut layout); + Op::InteractiveMoveUpdate { + window: 1, + dx: 0., + dy: 0., + output_idx: 2, + px: 40., + py: 0., + } + .apply(&mut layout); + + layout.toggle_overview(); + + Op::InteractiveMoveUpdate { + window: 1, + dx: 0., + dy: 0., + output_idx: 2, + px: 20., + py: 0., + } + .apply(&mut layout); + assert_eq!(moving_pointer_pos(&layout), Point::from((20., 0.))); +} + #[test] fn set_width_fixed_negative() { let ops = [ diff --git a/src/layout/tile.rs b/src/layout/tile.rs index bb0d61e873..3db767729a 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -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::{self, BackgroundEffect, BackgroundEffectElement}; use crate::render_helpers::border::BorderRenderElement; use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage}; use crate::render_helpers::damage::ExtraDamage; @@ -27,7 +28,8 @@ 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::RenderTarget; +use crate::render_helpers::xray::Xray; +use crate::render_helpers::{RenderCtx, RenderTarget}; use crate::utils::transaction::Transaction; use crate::utils::{ baba_is_float_offset, round_logical_in_physical, round_logical_in_physical_max1, @@ -57,6 +59,9 @@ pub struct Tile { /// The black backdrop for fullscreen windows. fullscreen_backdrop: SolidColorBuffer, + /// The background effect, like blur, behind the window. + background_effect: BackgroundEffect, + /// Whether the tile should float upon unfullscreening. pub(super) restore_to_floating: bool, @@ -130,6 +135,7 @@ niri_render_elements! { ClippedSurface = ClippedSurfaceRenderElement, Offscreen = OffscreenRenderElement, ExtraDamage = ExtraDamage, + BackgroundEffect = BackgroundEffectElement, } } @@ -192,6 +198,7 @@ impl Tile { shadow: Shadow::new(shadow_config), sizing_mode, fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]), + background_effect: BackgroundEffect::new(), restore_to_floating: false, floating_window_size: None, floating_pos: None, @@ -248,6 +255,8 @@ impl Tile { let shadow_config = self.options.layout.shadow.merged_with(&rules.shadow); self.shadow.update_config(shadow_config); + + self.background_effect.update_config(self.options.blur); } pub fn update_shaders(&mut self) { @@ -403,7 +412,6 @@ impl Tile { .unwrap_or_default() .fit_to(window_size.w as f32, window_size.h as f32); self.rounded_corner_damage.set_corner_radius(radius); - self.rounded_corner_damage.set_size(window_size); } pub fn advance_animations(&mut self) { @@ -457,6 +465,17 @@ impl Tile { let animated_tile_size = self.animated_tile_size(); let expanded_progress = self.expanded_progress(); + let radius = rules + .geometry_corner_radius + .unwrap_or_default() + .scaled_by(1. - expanded_progress as f32); + let has_blur_region = self.window.blur_region().is_some_and(|r| !r.is_empty()); + self.background_effect.update_render_elements( + radius, + rules.background_effect, + has_blur_region, + ); + let draw_border_with_background = rules .draw_border_with_background .unwrap_or_else(|| !self.window.has_ssd()); @@ -1009,10 +1028,11 @@ impl Tile { fn render_inner( &self, - renderer: &mut R, + mut ctx: RenderCtx, location: Point, + mut pos_in_backdrop: Point, + zoom: f64, focus_ring: bool, - target: RenderTarget, push: &mut dyn FnMut(TileRenderElement), ) { let _span = tracy_client::span!("Tile::render_inner"); @@ -1040,11 +1060,13 @@ impl Tile { // 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(); + pos_in_backdrop += self.bob_offset().upscale(zoom); 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; + pos_in_backdrop += window_loc.upscale(zoom); let area = Rectangle::new(window_render_loc, animated_window_size); let rules = self.window.rules(); @@ -1058,48 +1080,43 @@ impl Tile { .scaled_by(1. - expanded_progress as f32); // Popups go on top, whether it's resize or not. - self.window.render_popups( - renderer, - window_render_loc, - scale, - win_alpha, - target, - &mut |elem| push(elem.into()), - ); + self.window + .render_popups(ctx.r(), window_render_loc, scale, win_alpha, &mut |elem| { + push(elem.into()) + }); // If we're resizing, try to render a shader, or a fallback. let mut pushed_resize = false; if let Some(resize) = &self.resize_animation { - if ResizeRenderElement::has_shader(renderer) { - let gles_renderer = renderer.as_gles_renderer(); + if ResizeRenderElement::has_shader(ctx.renderer) { + let mut ctx = ctx.as_gles(); - if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) { + if let Some(texture_from) = resize.snapshot.texture(ctx.r(), scale) { let mut window_elements = Vec::new(); self.window.render_normal( - gles_renderer, + ctx.r(), Point::from((0., 0.)), scale, 1., - target, &mut |elem| window_elements.push(elem), ); let current = resize .offscreen - .render(gles_renderer, scale, &window_elements) + .render(ctx.renderer, scale, &window_elements) .map_err(|err| warn!("error rendering window to texture: {err:?}")) .ok(); // Clip blocked-out resizes unconditionally because they use solid color render // elements. - let clip_to_geometry = if target - .should_block_out(resize.snapshot.block_out_from) - && target.should_block_out(rules.block_out_from) - { - true - } else { - clip_to_geometry - }; + let clip_to_geometry = + if ctx.target.should_block_out(resize.snapshot.block_out_from) + && ctx.target.should_block_out(rules.block_out_from) + { + true + } else { + clip_to_geometry + }; if let Some((elem_current, _sync_point, mut data)) = current { let texture_current = elem_current.texture().clone(); @@ -1148,12 +1165,12 @@ impl Tile { } // If we're not resizing, render the window itself. - let has_border_shader = BorderRenderElement::has_shader(renderer); + let has_border_shader = BorderRenderElement::has_shader(ctx.renderer); if !pushed_resize { let geo = Rectangle::new(window_render_loc, window_size); let radius = radius.fit_to(window_size.w as f32, window_size.h as f32); - let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned(); + let clip_shader = ClippedSurfaceRenderElement::shader(ctx.renderer).cloned(); let clip = |elem| match elem { LayoutElementRenderElement::Wayland(elem) => { // If we should clip to geometry, render a clipped window. @@ -1206,18 +1223,14 @@ impl Tile { }; if clip_to_geometry && clip_shader.is_some() { - let damage = self.rounded_corner_damage.element(); - push(damage.with_location(window_render_loc).into()); + let damage = self.rounded_corner_damage.render(geo); + push(damage.into()); } - self.window.render_normal( - renderer, - window_render_loc, - scale, - win_alpha, - target, - &mut |elem| push(clip(elem)), - ); + self.window + .render_normal(ctx.r(), window_render_loc, scale, win_alpha, &mut |elem| { + push(clip(elem)) + }); } if fullscreen_progress > 0. { @@ -1264,7 +1277,7 @@ impl Tile { if let Some(width) = self.visual_border_width() { self.border.render( - renderer, + ctx.renderer, location + Point::from((width, width)), &mut |elem| push(elem.into()), ); @@ -1276,21 +1289,79 @@ impl Tile { // a bit weird). if focus_ring && expanded_progress < 1. { self.focus_ring - .render(renderer, location, &mut |elem| push(elem.into())); + .render(ctx.renderer, location, &mut |elem| push(elem.into())); } if expanded_progress < 1. { self.shadow - .render(renderer, location, &mut |elem| push(elem.into())); + .render(ctx.renderer, location, &mut |elem| push(elem.into())); + } + + if self.background_effect.is_visible() { + // Effects not requested by the surface itself are drawn to match the geometry. + let mut clip = true; + + // FIXME: support blur regions on subsurfaces in addition to the main surface. + let mut subregion = None; + let blur_geometry = if let Some(rects) = self.window.blur_region() { + if rects.is_empty() { + // Surface has a set, but empty blur region. + None + } else { + // If the surface itself requests the effects, apply different defaults. + clip = rules.clip_to_geometry == Some(true); + + // Use geometry-shaped blur for blocked-out windows to avoid unintentionally + // leaking any surface shapes. We render those windows as geometry-shaped solid + // rectangles anyway. + if ctx.target.should_block_out(rules.block_out_from) { + clip = true; + Some(area) + } else { + let anim_scale = animated_window_size / window_size; + let mut main_surface_geo = + self.window.main_surface_geo().to_f64().upscale(anim_scale); + main_surface_geo.loc += area.loc; + + subregion = Some(background_effect::EffectSubregion { + rects, + scale: anim_scale, + offset: main_surface_geo.loc, + }); + + main_surface_geo = main_surface_geo + .to_physical_precise_round(self.scale) + .to_logical(self.scale); + Some(main_surface_geo) + } + } + } else { + Some(area) + }; + + if let Some(geometry) = blur_geometry { + pos_in_backdrop += (geometry.loc - area.loc).upscale(zoom); + let params = background_effect::RenderParams { + geometry, + subregion, + clip: clip.then_some((area, CornerRadius::default())), + pos_in_backdrop, + zoom, + scale: self.scale, + }; + self.background_effect + .render(ctx.as_gles(), params, &mut |elem| push(elem.into())); + } } } pub fn render( &self, - renderer: &mut R, + mut ctx: RenderCtx, location: Point, + pos_in_backdrop: Point, + zoom: f64, focus_ring: bool, - target: RenderTarget, push: &mut dyn FnMut(TileRenderElement), ) { let _span = tracy_client::span!("Tile::render"); @@ -1306,17 +1377,18 @@ impl Tile { self.window().set_offscreen_data(None); if let Some(open) = &self.open_animation { - let renderer = renderer.as_gles_renderer(); + let mut ctx = ctx.as_gles(); let mut elements = Vec::new(); self.render_inner( - renderer, - Point::from((0., 0.)), + ctx.r(), + Point::new(0., 0.), + pos_in_backdrop, + zoom, focus_ring, - target, &mut |elem| elements.push(elem), ); match open.render( - renderer, + ctx.renderer, &elements, self.animated_tile_size(), location, @@ -1333,16 +1405,17 @@ impl Tile { } } } else if let Some(alpha) = &self.alpha_animation { - let renderer = renderer.as_gles_renderer(); + let mut ctx = ctx.as_gles(); let mut elements = Vec::new(); self.render_inner( - renderer, - Point::from((0., 0.)), + ctx.r(), + Point::new(0., 0.), + pos_in_backdrop, + zoom, focus_ring, - target, &mut |elem| elements.push(elem), ); - match alpha.offscreen.render(renderer, scale, &elements) { + match alpha.offscreen.render(ctx.renderer, scale, &elements) { Ok((elem, _sync, data)) => { let offset = elem.offset(); let elem = elem.with_alpha(tile_alpha).with_offset(location + offset); @@ -1358,48 +1431,154 @@ impl Tile { } if !pushed { - self.render_inner(renderer, location, focus_ring, target, &mut |elem| { - push(elem) - }); + self.render_inner( + ctx, + location, + pos_in_backdrop, + zoom, + 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, + pos_in_backdrop: Point, + zoom: f64, + ) { 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, + pos_in_backdrop, + zoom, + )); } - 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, + pos_in_backdrop: Point, + zoom: f64, + ) -> TileRenderSnapshot { let _span = tracy_client::span!("Tile::render_snapshot"); let mut contents = Vec::new(); self.render( - renderer, + RenderCtx { + target: RenderTarget::Output, + renderer, + xray: xray.as_deref(), + }, Point::from((0., 0.)), + pos_in_backdrop, + zoom, false, - RenderTarget::Output, &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.)), + pos_in_backdrop, + zoom, + 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( - renderer, + RenderCtx { + target: RenderTarget::Screencast, + renderer, + xray: xray.as_deref(), + }, Point::from((0., 0.)), + pos_in_backdrop, + zoom, false, - RenderTarget::Screencast, &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(), } } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 0df4662096..15b0712587 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -32,7 +32,8 @@ 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::RenderTarget; +use crate::render_helpers::xray::Xray; +use crate::render_helpers::RenderCtx; use crate::utils::id::IdCounter; use crate::utils::transaction::{Transaction, TransactionBlocker}; use crate::utils::{ @@ -1626,22 +1627,27 @@ impl Workspace { pub fn render_scrolling( &self, - renderer: &mut R, - target: RenderTarget, + ctx: RenderCtx, + pos_in_backdrop: Point, + zoom: f64, focus_ring: bool, push: &mut dyn FnMut(WorkspaceRenderElement), ) { let scrolling_focus_ring = focus_ring && !self.floating_is_active(); - self.scrolling - .render(renderer, target, scrolling_focus_ring, &mut |elem| { - push(elem.into()) - }); + self.scrolling.render( + ctx, + pos_in_backdrop, + zoom, + scrolling_focus_ring, + &mut |elem| push(elem.into()), + ); } pub fn render_floating( &self, - renderer: &mut R, - target: RenderTarget, + ctx: RenderCtx, + pos_in_backdrop: Point, + zoom: f64, focus_ring: bool, push: &mut dyn FnMut(WorkspaceRenderElement), ) { @@ -1652,9 +1658,10 @@ impl Workspace { let view_rect = Rectangle::from_size(self.view_size); let floating_focus_ring = focus_ring && self.floating_is_active(); self.floating.render( - renderer, + ctx, + pos_in_backdrop, + zoom, view_rect, - target, floating_focus_ring, &mut |elem| push(elem.into()), ); @@ -1689,14 +1696,29 @@ impl Workspace { ) || !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, + window: &W::Id, + pos_in_backdrop: Point, + zoom: f64, + ) { 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 pos_in_backdrop = pos_in_backdrop + tile_pos.upscale(zoom); + tile.store_unmap_snapshot_if_empty( + renderer, + xray, + xray_has_blocked_out_layers, + pos_in_backdrop, + zoom, + ); return; } } diff --git a/src/niri.rs b/src/niri.rs index 414f702d80..241c5dcbd0 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -70,6 +70,7 @@ use smithay::utils::{ ClockSource, IsAlive as _, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, }; +use smithay::wayland::background_effect::BackgroundEffectState; use smithay::wayland::compositor::{ with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler, CompositorState, HookId, SurfaceData, TraversalAction, @@ -144,19 +145,22 @@ use crate::niri_render_elements; use crate::protocols::ext_workspace::{self, ExtWorkspaceManagerState}; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::protocols::gamma_control::GammaControlManagerState; +use crate::protocols::kde_blur::KdeBlurState; use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState; use crate::protocols::output_management::OutputManagementManagerState; use crate::protocols::screencopy::{Screencopy, ScreencopyBuffer, ScreencopyManagerState}; use crate::protocols::virtual_pointer::VirtualPointerManagerState; +use crate::render_helpers::blur::BlurOptions; use crate::render_helpers::debug::push_opaque_regions; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; 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; use crate::render_helpers::{ encompassing_geo, render_to_dmabuf, render_to_encompassing_texture, render_to_shm, - render_to_texture, render_to_vec, shaders, RenderTarget, + render_to_texture, render_to_vec, shaders, RenderCtx, RenderTarget, }; #[cfg(feature = "xdp-gnome-screencast")] use crate::screencasting::Screencasting; @@ -277,6 +281,8 @@ pub struct Niri { pub screencopy_state: ScreencopyManagerState, pub output_management_state: OutputManagementManagerState, pub viewporter_state: ViewporterState, + pub background_effect_state: BackgroundEffectState, + pub kde_blur_state: KdeBlurState, pub xdg_foreign_state: XdgForeignState, pub shm_state: ShmState, pub output_manager_state: OutputManagerState, @@ -476,6 +482,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, pub lock_color_buffer: SolidColorBuffer, @@ -2029,6 +2036,50 @@ impl State { self.niri.queue_redraw_all(); } + pub fn store_unmap_snapshot(&mut self, window: &Window, output: Option<&Output>) { + // We'll be rendering Tiles and other stuff that needs their render elements up to date. + self.niri.update_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) {} @@ -2285,6 +2336,8 @@ impl Niri { let screencopy_state = ScreencopyManagerState::new::(&display_handle, client_is_unrestricted); let viewporter_state = ViewporterState::new::(&display_handle); + let background_effect_state = BackgroundEffectState::new::(&display_handle); + let kde_blur_state = KdeBlurState::new::(&display_handle); let xdg_foreign_state = XdgForeignState::new::(&display_handle); let is_tty = matches!(backend, Backend::Tty(_)); @@ -2468,6 +2521,8 @@ impl Niri { output_management_state, screencopy_state, viewporter_state, + background_effect_state, + kde_blur_state, xdg_foreign_state, text_input_state, input_method_state, @@ -2811,6 +2866,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), @@ -3997,6 +4053,27 @@ impl Niri { if output.is_none_or(|output| out == output) { let scale = Scale::from(out.current_scale().fractional_scale()); let transform = out.current_transform(); + let mode = out.current_mode().unwrap(); + 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(); + let blur_options = BlurOptions::from(self.config.borrow().blur); + for buf in &state.xray.background { + let mut buffer = buf.borrow_mut(); + buffer.update_size(size, scale); + buffer.update_blur_options(blur_options); + } + for buf in &state.xray.backdrop { + let mut buffer = buf.borrow_mut(); + buffer.update_size(size, scale); + buffer.update_blur_options(blur_options); + } if let Some(transition) = &mut state.screen_transition { transition.update_render_elements(scale, transform); @@ -4025,39 +4102,57 @@ impl Niri { } } - pub fn render( + pub fn render_to_vec( &self, - renderer: &mut R, + ctx: RenderCtx, output: &Output, include_pointer: bool, - target: RenderTarget, ) -> Vec> { let mut elements = Vec::new(); - self.render_inner(renderer, output, include_pointer, target, &mut |elem| { + self.render(ctx, output, include_pointer, &mut |elem| { elements.push(elem) }); elements } - pub fn render_inner( + pub fn render( &self, - renderer: &mut R, + mut ctx: RenderCtx, output: &Output, include_pointer: bool, - mut target: RenderTarget, push: &mut dyn FnMut(OutputRenderElements), ) { let _span = tracy_client::span!("Niri::render"); - if target == RenderTarget::Output { + if ctx.target == RenderTarget::Output { if let Some(preview) = self.config.borrow().debug.preview_render { - target = match preview { + ctx.target = match preview { PreviewRender::Screencast => RenderTarget::Screencast, PreviewRender::ScreenCapture => RenderTarget::ScreenCapture, }; } } + 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( + &self, + mut ctx: RenderCtx, + output: &Output, + include_pointer: bool, + push: &mut dyn FnMut(OutputRenderElements), + ) { + let state = self.output_state.get(output).unwrap(); let output_scale = Scale::from(output.current_scale().fractional_scale()); let push = if self.debug_draw_opaque_regions { @@ -4071,32 +4166,30 @@ impl Niri { // The pointer goes on the top. if include_pointer && self.pointer_visibility.is_visible() { - self.render_pointer(renderer, output, &mut |elem| push(elem.into())); + self.render_pointer(ctx.renderer, output, &mut |elem| push(elem.into())); } // Next, the screen transition texture. { - let state = self.output_state.get(output).unwrap(); if let Some(transition) = &state.screen_transition { - push(transition.render(target).into()); + push(transition.render(ctx.target).into()); } } // Next, the exit confirm dialog. self.exit_confirm_dialog - .render(renderer, output, &mut |elem| push(elem.into())); + .render(ctx.renderer, output, &mut |elem| push(elem.into())); // Next, the config error notification too. - if let Some(element) = self.config_error_notification.render(renderer, output) { + if let Some(element) = self.config_error_notification.render(ctx.renderer, output) { push(element.into()); } // If the session is locked, draw the lock surface. if self.is_locked() { - let state = self.output_state.get(output).unwrap(); if let Some(surface) = state.lock_surface.as_ref() { push_elements_from_surface_tree( - renderer, + ctx.renderer, surface.wl_surface(), Point::new(0, 0), output_scale, @@ -4121,7 +4214,6 @@ impl Niri { } // Prepare the background elements. - let state = self.output_state.get(output).unwrap(); let backdrop = SolidColorRenderElement::from_buffer( &state.backdrop_buffer, (0., 0.), @@ -4133,7 +4225,7 @@ impl Niri { // If the screenshot UI is open, draw it. if self.screenshot_ui.is_open() { self.screenshot_ui - .render_output(output, target, &mut |elem| push(elem.into())); + .render_output(output, ctx.target, &mut |elem| push(elem.into())); // Add the backdrop for outputs that were connected while the screenshot UI was open. push(backdrop); @@ -4142,15 +4234,13 @@ impl Niri { } // Draw the hotkey overlay on top. - if let Some(element) = self.hotkey_overlay.render(renderer, output) { + if let Some(element) = self.hotkey_overlay.render(ctx.renderer, output) { push(element.into()); } // Then, the Alt-Tab switcher. self.window_mru_ui - .render_output(self, output, renderer, target, &mut |elem| { - push(elem.into()) - }); + .render_output(self, output, ctx.r(), &mut |elem| push(elem.into())); // Don't draw the focus ring on the workspaces while interactively moving above those // workspaces, since the interactively-moved window already has a focus ring. @@ -4167,7 +4257,7 @@ impl Niri { // into different functions). macro_rules! push_popups_from_layer { ($layer:expr, $backdrop:expr, $push:expr) => {{ - self.render_layer_popups(renderer, target, &layer_map, $layer, $backdrop, $push); + self.render_layer_popups(ctx.r(), &layer_map, $layer, $backdrop, $push); }}; ($layer:expr, true) => {{ push_popups_from_layer!($layer, true, &mut |elem| push(elem.into())); @@ -4181,7 +4271,15 @@ impl Niri { } macro_rules! push_normal_from_layer { ($layer:expr, $backdrop:expr, $push:expr) => {{ - self.render_layer_normal(renderer, target, &layer_map, $layer, $backdrop, $push); + self.render_layer_normal( + ctx.r(), + &layer_map, + $layer, + Point::new(0., 0.), + 1., + $backdrop, + $push, + ); }}; ($layer:expr, true) => {{ push_normal_from_layer!($layer, true, &mut |elem| push(elem.into())); @@ -4202,13 +4300,11 @@ impl Niri { // Otherwise, we will render all layer-shell pop-ups and the top layer on top. if mon.render_above_top_layer() { self.layout - .render_interactive_move_for_output(renderer, output, target, &mut |elem| { - push(elem.into()) - }); + .render_interactive_move_for_output(ctx.r(), output, &mut |elem| push(elem.into())); - mon.render_insert_hint_between_workspaces(renderer, &mut |elem| push(elem.into())); + mon.render_insert_hint_between_workspaces(ctx.renderer, &mut |elem| push(elem.into())); - mon.render_workspaces(renderer, target, focus_ring, &mut |elem| push(elem.into())); + mon.render_workspaces(ctx.r(), focus_ring, &mut |elem| push(elem.into())); push_popups_from_layer!(Layer::Top); push_normal_from_layer!(Layer::Top); @@ -4227,11 +4323,9 @@ impl Niri { push_normal_from_layer!(Layer::Top); self.layout - .render_interactive_move_for_output(renderer, output, target, &mut |elem| { - push(elem.into()) - }); + .render_interactive_move_for_output(ctx.r(), output, &mut |elem| push(elem.into())); - mon.render_insert_hint_between_workspaces(renderer, &mut |elem| push(elem.into())); + mon.render_insert_hint_between_workspaces(ctx.renderer, &mut |elem| push(elem.into())); // Macro instead of closure to avoid borrowing push(). macro_rules! process { @@ -4249,17 +4343,28 @@ impl Niri { push_popups_from_layer!(Layer::Background, process!(geo)); } - mon.render_workspaces(renderer, target, focus_ring, &mut |elem| push(elem.into())); + 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 mut push_normal = |layer| { + self.render_layer_normal( + ctx.r(), + &layer_map, + layer, + geo.loc, + zoom, + false, + process!(geo), + ) + }; + push_normal(Layer::Bottom); + push_normal(Layer::Background); process!(geo)(ws.render_background()); } } - mon.render_workspace_shadows(renderer, &mut |elem| push(elem.into())); + mon.render_workspace_shadows(ctx.renderer, &mut |elem| push(elem.into())); // Then the backdrop. push_popups_from_layer!(Layer::Background, true); @@ -4268,6 +4373,92 @@ impl Niri { push(backdrop); } + pub fn fill_xray_elements(&self, mut ctx: RenderCtx, 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, + Point::new(0., 0.), + 1., + 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, + Point::new(0., 0.), + 1., + 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, @@ -4287,31 +4478,34 @@ impl Niri { }) } + #[allow(clippy::too_many_arguments)] fn render_layer_normal( &self, - renderer: &mut R, - target: RenderTarget, + mut ctx: RenderCtx, layer_map: &LayerMap, layer: Layer, + pos_in_backdrop: Point, + zoom: f64, for_backdrop: bool, push: &mut dyn FnMut(LayerSurfaceRenderElement), ) { for (mapped, geo) in self.layers_in_render_order(layer_map, layer, for_backdrop) { - mapped.render_normal(renderer, geo.loc.to_f64(), target, push); + let loc = geo.loc.to_f64(); + let pos_in_backdrop = pos_in_backdrop + loc.upscale(zoom); + mapped.render_normal(ctx.r(), loc, pos_in_backdrop, zoom, push); } } fn render_layer_popups( &self, - renderer: &mut R, - target: RenderTarget, + mut ctx: RenderCtx, layer_map: &LayerMap, layer: Layer, for_backdrop: bool, push: &mut dyn FnMut(LayerSurfaceRenderElement), ) { for (mapped, geo) in self.layers_in_render_order(layer_map, layer, for_backdrop) { - mapped.render_popups(renderer, geo.loc.to_f64(), target, push); + mapped.render_popups(ctx.r(), geo.loc.to_f64(), push); } } @@ -4935,7 +5129,12 @@ impl Niri { if let Some(screencopy) = screencopy { if screencopy.output() == output { let elements = elements.get_or_init(|| { - self.render(renderer, output, true, RenderTarget::ScreenCapture) + let ctx = RenderCtx { + renderer, + target: RenderTarget::ScreenCapture, + xray: None, + }; + self.render_to_vec(ctx, output, true) }); // FIXME: skip elements if not including pointers let render_result = Self::render_for_screencopy_internal( @@ -4998,12 +5197,13 @@ impl Niri { self.update_render_elements(Some(output)); - let elements = self.render( + let ctx = RenderCtx { renderer, - output, - screencopy.overlay_cursor(), - RenderTarget::ScreenCapture, - ); + target: RenderTarget::ScreenCapture, + xray: None, + }; + let elements = self.render_to_vec(ctx, output, screencopy.overlay_cursor()); + let Some(damage_tracker) = self.screencopy_state.damage_tracker(manager) else { error!("screencopy queue must not be deleted as long as frames exist"); bail!("screencopy queue missing"); @@ -5126,7 +5326,12 @@ impl Niri { RenderTarget::ScreenCapture, ]; let screenshot = targets.map(|target| { - let elements = self.render::(renderer, &output, false, target); + let ctx = RenderCtx { + renderer, + target, + xray: None, + }; + let elements = self.render_to_vec(ctx, &output, false); let elements = elements.iter().rev(); let res = render_to_texture( @@ -5203,12 +5408,12 @@ impl Niri { let size = transform.transform_size(size); let scale = Scale::from(output.current_scale().fractional_scale()); - let elements = self.render::( + let ctx = RenderCtx { renderer, - output, - include_pointer, - RenderTarget::ScreenCapture, - ); + target: RenderTarget::ScreenCapture, + xray: None, + }; + let elements = self.render_to_vec(ctx, output, include_pointer); let elements = elements.iter().rev(); let pixels = render_to_vec( renderer, @@ -5258,12 +5463,16 @@ impl Niri { } let pointer_count = elements.len(); - mapped.render( + let ctx = RenderCtx { renderer, + target: RenderTarget::ScreenCapture, + xray: None, + }; + mapped.render( + ctx, mapped.window.geometry().loc.to_f64(), scale, alpha, - RenderTarget::ScreenCapture, &mut |elem| elements.push(elem.into()), ); @@ -5419,12 +5628,12 @@ impl Niri { let transform = output.current_transform(); let size = transform.transform_size(size); - let elements = self.render::( + let ctx = RenderCtx { renderer, - &output, - include_pointer, - RenderTarget::ScreenCapture, - ); + target: RenderTarget::ScreenCapture, + xray: None, + }; + let elements = self.render_to_vec(ctx, &output, include_pointer); let elements = elements.iter().rev(); let pixels = render_to_vec( renderer, @@ -5897,7 +6106,12 @@ impl Niri { RenderTarget::ScreenCapture, ]; let textures = targets.map(|target| { - let elements = self.render::(renderer, &output, false, target); + let ctx = RenderCtx { + renderer, + target, + xray: None, + }; + let elements = self.render_to_vec(ctx, &output, false); let elements = elements.iter().rev(); let res = render_to_texture( diff --git a/src/protocols/kde_blur.rs b/src/protocols/kde_blur.rs new file mode 100644 index 0000000000..e019f90400 --- /dev/null +++ b/src/protocols/kde_blur.rs @@ -0,0 +1,214 @@ +use std::sync::Mutex; + +use smithay::reexports::wayland_server; +use smithay::wayland::compositor::{ + get_region_attributes, with_states, Cacheable, RegionAttributes, +}; +use wayland_protocols_plasma::blur::server::org_kde_kwin_blur::{self, OrgKdeKwinBlur}; +use wayland_protocols_plasma::blur::server::org_kde_kwin_blur_manager::{ + self, OrgKdeKwinBlurManager, +}; +use wayland_server::backend::GlobalId; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, +}; + +pub trait KdeBlurHandler: + GlobalDispatch + + Dispatch + + Dispatch + + 'static +{ + /// Called when a blur region becomes pending on a surface, awaiting a commit. + fn set_blur_region(&mut self, wl_surface: WlSurface) { + let _ = wl_surface; + } + + /// Called when a blur region unset becomes pending on a surface, awaiting a commit. + fn unset_blur_region(&mut self, wl_surface: WlSurface) { + let _ = wl_surface; + } +} + +#[derive(Debug, Clone, Default)] +pub struct KdeBlurSurfaceCachedState { + /// Region of the surface that will have its background blurred. + /// + /// `None` means no blurring. + pub blur_region: Option, +} + +#[derive(Debug, Clone, Default)] +pub enum KdeBlurRegion { + #[default] + WholeSurface, + Region(RegionAttributes), +} + +impl Cacheable for KdeBlurSurfaceCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + self.clone() + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +#[derive(Debug)] +pub struct KdeBlurSurfaceUserData { + surface: Weak, + pending_region: Mutex, +} + +impl KdeBlurSurfaceUserData { + fn new(surface: WlSurface) -> Self { + Self { + surface: surface.downgrade(), + pending_region: Mutex::new(KdeBlurRegion::WholeSurface), + } + } + + fn wl_surface(&self) -> Option { + self.surface.upgrade().ok() + } +} + +#[derive(Debug)] +pub struct KdeBlurState { + global: GlobalId, +} + +impl KdeBlurState { + pub fn new(display: &DisplayHandle) -> KdeBlurState { + let global = display.create_global::(1, ()); + KdeBlurState { global } + } + + pub fn global(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch for KdeBlurState { + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &(), + data_init: &mut DataInit<'_, D>, + ) { + let _manager = data_init.init(resource, ()); + } +} + +impl Dispatch for KdeBlurState { + fn request( + state: &mut D, + _client: &Client, + _manager: &OrgKdeKwinBlurManager, + request: org_kde_kwin_blur_manager::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + org_kde_kwin_blur_manager::Request::Create { id, surface } => { + data_init.init(id, KdeBlurSurfaceUserData::new(surface)); + } + org_kde_kwin_blur_manager::Request::Unset { surface } => { + with_states(&surface, |states| { + let mut cached = states.cached_state.get::(); + let pending = cached.pending(); + pending.blur_region = None; + }); + state.unset_blur_region(surface); + } + _ => {} + } + } +} + +impl Dispatch for KdeBlurState { + fn request( + state: &mut D, + _client: &Client, + _obj: &OrgKdeKwinBlur, + request: org_kde_kwin_blur::Request, + data: &KdeBlurSurfaceUserData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + org_kde_kwin_blur::Request::SetRegion { region } => { + let region = region.as_ref().map(get_region_attributes); + + // In the KDE blur protocol, an empty region means whole surface. + let region = match region { + Some(region) if !region.rects.is_empty() => KdeBlurRegion::Region(region), + _ => KdeBlurRegion::WholeSurface, + }; + + *data.pending_region.lock().unwrap() = region; + } + org_kde_kwin_blur::Request::Commit => { + let Some(surface) = data.wl_surface() else { + return; + }; + + with_states(&surface, |states| { + let mut cached = states.cached_state.get::(); + let pending = cached.pending(); + let region = data.pending_region.lock().unwrap().clone(); + pending.blur_region = Some(region); + }); + state.set_blur_region(surface); + } + org_kde_kwin_blur::Request::Release => { + // No-op. + } + _ => {} + } + } + + fn destroyed( + _state: &mut D, + _client_id: wayland_server::backend::ClientId, + _object: &OrgKdeKwinBlur, + _data: &KdeBlurSurfaceUserData, + ) { + // No-op: cleanup is handled by double-buffering and surface destruction + } +} + +#[macro_export] +macro_rules! delegate_kde_blur { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + const _: () = { + use smithay::reexports::wayland_server; + use wayland_protocols_plasma::blur::server::{ + org_kde_kwin_blur_manager::OrgKdeKwinBlurManager, + org_kde_kwin_blur::OrgKdeKwinBlur, + }; + use wayland_server::{delegate_dispatch, delegate_global_dispatch}; + use $crate::protocols::kde_blur::{KdeBlurState, KdeBlurSurfaceUserData}; + + delegate_global_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [OrgKdeKwinBlurManager: ()] => KdeBlurState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [OrgKdeKwinBlurManager: ()] => KdeBlurState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [OrgKdeKwinBlur: KdeBlurSurfaceUserData] => KdeBlurState + ); + }; + }; +} diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 1a81750d88..167a932f7e 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -1,6 +1,7 @@ pub mod ext_workspace; pub mod foreign_toplevel; pub mod gamma_control; +pub mod kde_blur; pub mod mutter_x11_interop; pub mod output_management; pub mod screencopy; diff --git a/src/render_helpers/background_effect.rs b/src/render_helpers/background_effect.rs new file mode 100644 index 0000000000..85cb8882c7 --- /dev/null +++ b/src/render_helpers/background_effect.rs @@ -0,0 +1,275 @@ +use std::array; +use std::sync::Arc; + +use niri_config::CornerRadius; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Point, Rectangle, Scale}; + +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; +use crate::render_helpers::{RenderCtx, RenderTarget}; + +#[derive(Debug)] +pub struct BackgroundEffect { + // Framebuffer effects are per-render-target because they store the framebuffer contents in a + // texture, and those differ per render target. + nonxray: [FramebufferEffect; RenderTarget::COUNT], + /// 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, + blur_config: niri_config::Blur, + options: Options, +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Options { + pub blur: bool, + pub xray: bool, + pub noise: Option, + pub saturation: Option, +} + +impl Options { + fn is_visible(&self) -> bool { + self.xray + || self.blur + || self.noise.is_some_and(|x| x > 0.) + || self.saturation.is_some_and(|x| x != 1.) + } +} + +/// Render-time parameters. +#[derive(Debug)] +pub struct RenderParams { + /// Geometry of the background effect. + pub geometry: Rectangle, + /// Effect subregion, will be clipped to `geometry`. + /// + /// `subregion.iter()` should return `geometry`-relative rectangles. + pub subregion: Option, + /// Position of `geometry` relative to the backdrop. + pub pos_in_backdrop: Point, + /// Geometry and radius for clipping in the same coordinate space as `geometry`. + pub clip: Option<(Rectangle, CornerRadius)>, + /// Zoom factor between backdrop coordinates and geometry. + pub zoom: f64, + /// 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); + } + } +} + +#[derive(Debug, Clone)] +pub struct EffectSubregion { + /// Non-overlapping rects in surface-local coordinates. + pub rects: Arc>>, + /// Scale to apply to each rect. + pub scale: Scale, + /// Translation to apply to each rect after scaling. + pub offset: Point, +} + +impl EffectSubregion { + /// Returns an iterator over the top-left and bottom-right corners of transformed rects. + pub fn iter(&self) -> impl Iterator, Point)> + '_ { + self.rects.iter().map(|r| { + // Here we start in a happy i32 world where everything lines up, and rectangle loc + + // size is exactly equal to the adjacent rectangle's loc. + // + // Unfortunately, we're about to descend to the floating point hell. And we *really* + // want adjacent rects to remain adjacent no matter what. So we'll convert our rects to + // their extremities (rather than loc and size), and operate on those. Coordinates from + // adjacent rects will undergo exactly the same floating point operations, so when + // they're ultimately rounded to physical pixels, they will remain adjacent. + let r = r.to_f64(); + + let mut a = r.loc; + // f64 is enough to represent this i32 addition exactly. + let mut b = r.loc + r.size.to_point(); + + a = a.upscale(self.scale); + b = b.upscale(self.scale); + + a += self.offset; + b += self.offset; + + (a, b) + }) + } + + pub fn filter_damage( + &self, + // Same coordinate space as self.iter(). + crop: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + filtered: &mut Vec>, + ) { + let scale = dst.size.to_f64() / crop.size; + + let cs = crop.size.to_point(); + + for (mut a, mut b) in self.iter() { + // Convert to dst-relative. + a -= crop.loc; + b -= crop.loc; + + // Intersect with crop. + let ia = Point::new(f64::max(a.x, 0.), f64::max(a.y, 0.)); + let ib = Point::new(f64::min(b.x, cs.x), f64::min(b.y, cs.y)); + if ib.x <= ia.x || ib.y <= ia.y { + // No intersection. + continue; + } + + // Round extremities to physical pixels, ensuring that adjacent rectangles stay adjacent + // at fractional scales. + let ia = ia.to_physical_precise_round(scale); + let ib = ib.to_physical_precise_round(scale); + + let r = Rectangle::from_extremities(ia, ib); + + // Intersect with each damage rect. + for d in damage { + if let Some(intersection) = r.intersection(*d) { + filtered.push(intersection); + } + } + } + } +} + +niri_render_elements! { + BackgroundEffectElement => { + FramebufferEffect = FramebufferEffectElement, + Xray = XrayElement, + ExtraDamage = ExtraDamage, + } +} + +impl BackgroundEffect { + pub fn new() -> Self { + Self { + nonxray: array::from_fn(|_| FramebufferEffect::new()), + damage: ExtraDamage::new(), + corner_radius: CornerRadius::default(), + blur_config: niri_config::Blur::default(), + options: Options::default(), + } + } + + pub fn update_config(&mut self, config: niri_config::Blur) { + if self.blur_config == config { + return; + } + + self.blur_config = config; + self.damage.damage_all(); + } + + pub fn update_render_elements( + &mut self, + corner_radius: CornerRadius, + effect: niri_config::BackgroundEffect, + has_blur_region: bool, + ) { + // If the surface explicitly requests a blur region, default blur to true. + let blur = if has_blur_region { + effect.blur != Some(false) + } else { + effect.blur == Some(true) + }; + + let mut options = Options { + blur, + xray: effect.xray == Some(true), + noise: effect.noise, + saturation: effect.saturation, + }; + + // If we have some background effect but xray wasn't explicitly set, default it to true + // since it's cheaper. + if options.is_visible() && effect.xray.is_none() { + options.xray = true; + } + + // FIXME: do we also need to damage when subregion changes? Then we'll need to pass + // subregion in update_render_elements(). + 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, + mut params: RenderParams, + 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); + + // 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 { + self.blur_config.saturation + } else { + 1. + }; + let saturation = self.options.saturation.unwrap_or(saturation) as f32; + + if self.options.xray { + let Some(xray) = ctx.xray else { + return; + }; + + push(damage.into()); + xray.render(ctx, params, blur, noise, saturation, &mut |elem| { + push(elem.into()) + }); + } else { + // Render non-xray effect. + let elem = &self.nonxray[ctx.target as usize]; + if let Some(elem) = elem.render(ctx.renderer, params, blur_options, noise, saturation) { + push(damage.into()); + push(elem.into()); + } + } + } +} diff --git a/src/render_helpers/blur.rs b/src/render_helpers/blur.rs new file mode 100644 index 0000000000..17041d0857 --- /dev/null +++ b/src/render_helpers/blur.rs @@ -0,0 +1,355 @@ +use std::cmp::max; +use std::iter::{once, zip}; +use std::rc::Rc; + +use anyhow::{ensure, Context as _}; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::gles::{ + ffi, link_program, GlesError, GlesFrame, GlesRenderer, GlesTexture, +}; +use smithay::backend::renderer::{ContextId, Frame as _, Renderer as _, Texture as _}; +use smithay::gpu_span_location; +use smithay::utils::{Buffer, Size}; + +use crate::render_helpers::shaders::Shaders; + +#[derive(Debug)] +pub struct Blur { + program: BlurProgram, + /// Context ID of the renderer that created the program and the textures. + renderer_context_id: ContextId, + /// Output texture followed by intermediate textures, large to small. + /// + /// Created lazily and stored here to avoid recreating blur textures frequently. + textures: Vec, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct BlurOptions { + pub passes: u8, + pub offset: f64, +} + +impl From for BlurOptions { + fn from(config: niri_config::Blur) -> Self { + Self { + passes: config.passes, + offset: config.offset, + } + } +} + +#[derive(Debug, Clone)] +pub struct BlurProgram(Rc); + +#[derive(Debug)] +struct BlurProgramInner { + down: BlurProgramInternal, + up: BlurProgramInternal, +} + +#[derive(Debug)] +struct BlurProgramInternal { + program: ffi::types::GLuint, + uniform_tex: ffi::types::GLint, + uniform_half_pixel: ffi::types::GLint, + uniform_offset: ffi::types::GLint, + attrib_vert: ffi::types::GLint, +} + +unsafe fn compile_program(gl: &ffi::Gles2, src: &str) -> Result { + let program = unsafe { link_program(gl, include_str!("shaders/blur.vert"), src)? }; + + let vert = c"vert"; + let tex = c"tex"; + let half_pixel = c"half_pixel"; + let offset = c"offset"; + + Ok(BlurProgramInternal { + program, + uniform_tex: gl.GetUniformLocation(program, tex.as_ptr()), + uniform_half_pixel: gl.GetUniformLocation(program, half_pixel.as_ptr()), + uniform_offset: gl.GetUniformLocation(program, offset.as_ptr()), + attrib_vert: gl.GetAttribLocation(program, vert.as_ptr()), + }) +} + +impl BlurProgram { + pub fn compile(renderer: &mut GlesRenderer) -> anyhow::Result { + renderer + .with_context(move |gl| unsafe { + let down = compile_program(gl, include_str!("shaders/blur_down.frag")) + .context("error compiling blur_down shader")?; + let up = compile_program(gl, include_str!("shaders/blur_up.frag")) + .context("error compiling blur_up shader")?; + Ok(Self(Rc::new(BlurProgramInner { down, up }))) + }) + .context("error making GL context current")? + } + + pub fn destroy(self, renderer: &mut GlesRenderer) -> Result<(), GlesError> { + renderer.with_context(move |gl| unsafe { + gl.DeleteProgram(self.0.down.program); + gl.DeleteProgram(self.0.up.program); + }) + } +} + +impl Blur { + pub fn new(renderer: &mut GlesRenderer) -> Option { + let program = Shaders::get(renderer).blur.clone()?; + Some(Self { + program, + renderer_context_id: renderer.context_id(), + textures: Vec::new(), + }) + } + + pub fn context_id(&self) -> ContextId { + self.renderer_context_id.clone() + } + + pub fn prepare_textures( + &mut self, + mut create_texture: impl FnMut(Fourcc, Size) -> Result, + source: &GlesTexture, + options: BlurOptions, + ) -> anyhow::Result<()> { + let _span = tracy_client::span!("Blur::prepare_textures"); + + let passes = options.passes.clamp(1, 31) as usize; + let size = source.size(); + + if let Some(output) = self.textures.first_mut() { + let old_size = output.size(); + if old_size != size { + trace!( + "recreating textures: output size changed from {} × {} to {} × {}", + old_size.w, + old_size.h, + size.w, + size.h + ); + self.textures.clear(); + } else if !output.is_unique_reference() { + debug!("recreating textures: not unique",); + // We only need to recreate the output texture here, but this case shouldn't really + // happen anyway, and this is simpler. + self.textures.clear(); + } + } + + // Create any missing textures. + let mut w = size.w; + let mut h = size.h; + for i in 0..=passes { + let size = Size::new(w, h); + w = max(1, w / 2); + h = max(1, h / 2); + + if self.textures.len() > i { + // This texture already exists. + continue; + } + + // debug!("creating texture for step {i} sized {w} × {h}"); + + let texture: GlesTexture = + create_texture(Fourcc::Abgr8888, size).context("error creating texture")?; + self.textures.push(texture); + } + + // Drop any no longer needed textures. + self.textures.drain(passes + 1..); + + Ok(()) + } + + pub fn render( + &mut self, + frame: &mut GlesFrame, + source: &GlesTexture, + options: BlurOptions, + ) -> anyhow::Result { + let _span = tracy_client::span!("Blur::render"); + trace!("rendering blur"); + + ensure!( + frame.context_id() == self.renderer_context_id, + "wrong renderer" + ); + + let passes = options.passes.clamp(1, 31) as usize; + let size = source.size(); + + ensure!( + self.textures.len() == passes + 1, + "wrong textures len: expected {}, got {}", + passes + 1, + self.textures.len() + ); + + let output = &mut self.textures[0]; + ensure!( + output.size() == size, + "wrong output texture size: expected {size:?}, got {:?}", + output.size() + ); + + ensure!( + output.is_unique_reference(), + "output texture has a non-unique reference" + ); + + frame.with_profiled_context(gpu_span_location!("Blur::render"), |gl| unsafe { + while gl.GetError() != ffi::NO_ERROR {} + + let mut current_fbo = 0i32; + let mut viewport = [0i32; 4]; + gl.GetIntegerv(ffi::FRAMEBUFFER_BINDING, &mut current_fbo as *mut _); + gl.GetIntegerv(ffi::VIEWPORT, viewport.as_mut_ptr()); + + gl.Disable(ffi::BLEND); + gl.Disable(ffi::SCISSOR_TEST); + + gl.ActiveTexture(ffi::TEXTURE0); + + let mut fbos = [0; 2]; + gl.GenFramebuffers(fbos.len() as _, fbos.as_mut_ptr()); + gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbos[0]); + + let program = &self.program.0.down; + gl.UseProgram(program.program); + gl.Uniform1i(program.uniform_tex, 0); + gl.Uniform1f(program.uniform_offset, options.offset as f32); + + let vertices: [f32; 12] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0]; + gl.EnableVertexAttribArray(program.attrib_vert as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, 0); + gl.VertexAttribPointer( + program.attrib_vert as u32, + 2, + ffi::FLOAT, + ffi::FALSE, + 0, + vertices.as_ptr().cast(), + ); + + let src = once(source).chain(&self.textures[1..]); + let dst = &self.textures[1..]; + for (src, dst) in zip(src, dst) { + let dst_size = dst.size(); + let w = dst_size.w; + let h = dst_size.h; + gl.Viewport(0, 0, w, h); + + // During downsampling, half_pixel is half of the destination pixel. + gl.Uniform2f(program.uniform_half_pixel, 0.5 / w as f32, 0.5 / h as f32); + + let src = src.tex_id(); + let dst = dst.tex_id(); + + trace!("drawing down {src} to {dst}"); + gl.FramebufferTexture2D( + ffi::DRAW_FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::TEXTURE_2D, + dst, + 0, + ); + + gl.BindTexture(ffi::TEXTURE_2D, src); + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32); + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_S, + ffi::CLAMP_TO_EDGE as i32, + ); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_T, + ffi::CLAMP_TO_EDGE as i32, + ); + + gl.DrawArrays(ffi::TRIANGLES, 0, 6); + } + + gl.DisableVertexAttribArray(program.attrib_vert as u32); + + // Up + let program = &self.program.0.up; + gl.UseProgram(program.program); + gl.Uniform1i(program.uniform_tex, 0); + gl.Uniform1f(program.uniform_offset, options.offset as f32); + + let vertices: [f32; 12] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0]; + gl.EnableVertexAttribArray(program.attrib_vert as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, 0); + gl.VertexAttribPointer( + program.attrib_vert as u32, + 2, + ffi::FLOAT, + ffi::FALSE, + 0, + vertices.as_ptr().cast(), + ); + + let src = self.textures.iter().rev(); + let dst = self.textures.iter().rev().skip(1); + for (src, dst) in zip(src, dst) { + let dst_size = dst.size(); + let w = dst_size.w; + let h = dst_size.h; + gl.Viewport(0, 0, w, h); + + // During upsampling, half_pixel is half of the source pixel. + let src_size = src.size(); + let src_w = src_size.w as f32; + let src_h = src_size.h as f32; + gl.Uniform2f(program.uniform_half_pixel, 0.5 / src_w, 0.5 / src_h); + + let src = src.tex_id(); + let dst = dst.tex_id(); + + trace!("drawing up {src} to {dst}"); + gl.FramebufferTexture2D( + ffi::DRAW_FRAMEBUFFER, + ffi::COLOR_ATTACHMENT0, + ffi::TEXTURE_2D, + dst, + 0, + ); + + gl.BindTexture(ffi::TEXTURE_2D, src); + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32); + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_S, + ffi::CLAMP_TO_EDGE as i32, + ); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_T, + ffi::CLAMP_TO_EDGE as i32, + ); + + gl.DrawArrays(ffi::TRIANGLES, 0, 6); + } + + gl.DisableVertexAttribArray(program.attrib_vert as u32); + + gl.BindFramebuffer(ffi::FRAMEBUFFER, 0); + gl.DeleteFramebuffers(fbos.len() as _, fbos.as_ptr()); + + // Restore state set by GlesFrame that we just modified. + gl.Enable(ffi::BLEND); + gl.Enable(ffi::SCISSOR_TEST); + gl.BindFramebuffer(ffi::FRAMEBUFFER, current_fbo as u32); + gl.Viewport(viewport[0], viewport[1], viewport[2], viewport[3]); + })?; + + Ok(self.textures[0].clone()) + } +} diff --git a/src/render_helpers/clipped_surface.rs b/src/render_helpers/clipped_surface.rs index dc84d07953..7eeb0aae2a 100644 --- a/src/render_helpers/clipped_surface.rs +++ b/src/render_helpers/clipped_surface.rs @@ -272,10 +272,6 @@ impl<'render> RenderElement> } impl RoundedCornerDamage { - pub fn set_size(&mut self, size: Size) { - self.damage.set_size(size); - } - pub fn set_corner_radius(&mut self, corner_radius: CornerRadius) { if self.corner_radius == corner_radius { return; @@ -286,7 +282,7 @@ impl RoundedCornerDamage { self.damage.damage_all(); } - pub fn element(&self) -> ExtraDamage { - self.damage.clone() + pub fn render(&self, geometry: Rectangle) -> ExtraDamage { + self.damage.render(geometry) } } diff --git a/src/render_helpers/damage.rs b/src/render_helpers/damage.rs index d5d899de33..4cc3982081 100644 --- a/src/render_helpers/damage.rs +++ b/src/render_helpers/damage.rs @@ -1,7 +1,7 @@ use smithay::backend::renderer::element::{Element, Id, RenderElement}; use smithay::backend::renderer::utils::CommitCounter; use smithay::backend::renderer::Renderer; -use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size}; +use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size}; #[derive(Debug, Clone)] pub struct ExtraDamage { @@ -19,22 +19,14 @@ impl ExtraDamage { } } - pub fn set_size(&mut self, size: Size) { - if self.geometry.size == size { - return; - } - - self.geometry.size = size; - self.commit.increment(); - } - pub fn damage_all(&mut self) { self.commit.increment(); } - pub fn with_location(mut self, location: Point) -> Self { - self.geometry.loc = location; - self + pub fn render(&self, geometry: Rectangle) -> Self { + let mut this = self.clone(); + this.geometry = geometry; + this } } diff --git a/src/render_helpers/effect_buffer.rs b/src/render_helpers/effect_buffer.rs new file mode 100644 index 0000000000..fc9a4220b4 --- /dev/null +++ b/src/render_helpers/effect_buffer.rs @@ -0,0 +1,314 @@ +use std::mem; + +use anyhow::{ensure, Context as _}; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::damage::OutputDamageTracker; +use smithay::backend::renderer::element::Id; +use smithay::backend::renderer::gles::{GlesFrame, 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; +use crate::render_helpers::blur::{Blur, BlurOptions}; + +#[derive(Debug)] +pub struct EffectBuffer { + /// Id to be used for this effect buffer's elements. + id: Id, + + /// Size of the effect buffer. + size: Size, + /// Scale of the effect buffer. + scale: Scale, + /// Options for blurring. + blur_options: BlurOptions, + + /// Elements to be rendered on demand. + elements: Elements, + /// Offscreen buffer where elements get rendered. + offscreen: Option, + /// Blurring program, if available. + blur: Option, + + /// Commit counter that takes into account both original and blurred texture changes. + commit_counter: CommitCounter, +} + +#[derive(Debug)] +enum Elements { + /// Contents remain unchanged. + Unchanged( + // Storage to avoid reallocating it every time. + Vec>, + ), + /// New contents, need to check damage and render. + New(Vec>), +} + +#[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, + /// Scale of the texture. + scale: Scale, + /// Damage tracker for drawing to the texture. + damage: OutputDamageTracker, + /// Rendered blurred version of the texture. + /// + /// When texture needs to be reblurred, this field must be reset to `None`. + blurred: Option, +} + +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.), + blur_options: BlurOptions::default(), + elements: Elements::default(), + offscreen: None, + blur: 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 { + self.size.to_f64().to_logical(self.scale, Transform::Normal) + } + + pub fn scale(&self) -> Scale { + self.scale + } + + pub fn update_size(&mut self, size: Size, scale: Scale) { + self.size = size.to_logical(1).to_buffer(1, Transform::Normal); + self.scale = scale; + } + + pub fn update_blur_options(&mut self, options: BlurOptions) { + if self.blur_options == options { + return; + } + + self.blur_options = options; + + if let Some(offscreen) = &mut self.offscreen { + if offscreen.blurred.is_some() { + offscreen.blurred = None; + self.commit_counter.increment(); + } + } + } + + pub fn elements(&mut self) -> &mut Vec> { + // 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, blur: bool) -> bool { + if let Err(err) = self.prepare_offscreen(renderer) { + warn!("error preparing offscreen: {err:?}"); + return false; + }; + + if blur { + if let Err(err) = self.prepare_blur(renderer) { + warn!("error preparing blur: {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, + blurred: None, + }) + }; + + // 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(); + offscreen.blurred = None; + } + + // 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")? + }; + + if res.damage.is_some() { + self.commit_counter.increment(); + + // Original texture changed; reset the blurred texture. + offscreen.blurred = None; + } + + // Clear and put the storage back. + elements.clear(); + self.elements = Elements::Unchanged(elements); + + Ok(()) + } + + fn prepare_blur(&mut self, renderer: &mut GlesRenderer) -> anyhow::Result<()> { + let offscreen = self.offscreen.as_mut().context("missing offscreen")?; + if offscreen.blurred.is_some() { + // Already rendered. + return Ok(()); + } + + if let Some(blur) = &self.blur { + if blur.context_id() != renderer.context_id() { + debug!("recreating blur: renderer changed"); + self.blur = None; + } + } + + let blur = if let Some(blur) = &mut self.blur { + blur + } else { + let Some(blur) = Blur::new(renderer) else { + // Missing blur shader. + return Ok(()); + }; + self.blur.insert(blur) + }; + + ensure!( + offscreen.renderer_context_id == renderer.context_id(), + "wrong renderer context id" + ); + + blur.prepare_textures( + |fourcc, size| renderer.create_buffer(fourcc, size), + &offscreen.texture, + self.blur_options, + ) + .context("error preparing blur textures")?; + + Ok(()) + } + + pub fn render(&mut self, frame: &mut GlesFrame, blur: bool) -> anyhow::Result { + let offscreen = self.offscreen.as_mut().context("offscreen is missing")?; + + if !blur { + return Ok(offscreen.texture.clone()); + } + + let texture = if let Some(texture) = &offscreen.blurred { + texture.clone() + } else { + let blur = self.blur.as_mut().context("blur is missing")?; + let blurred = blur + .render(frame, &offscreen.texture, self.blur_options) + .context("error rendering blur")?; + offscreen.blurred.insert(blurred).clone() + }; + + Ok(texture) + } +} diff --git a/src/render_helpers/framebuffer_effect.rs b/src/render_helpers/framebuffer_effect.rs new file mode 100644 index 0000000000..9860eeee56 --- /dev/null +++ b/src/render_helpers/framebuffer_effect.rs @@ -0,0 +1,441 @@ +use std::cell::RefCell; +use std::rc::Rc; + +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, GlesTexProgram, GlesTexture, Uniform, +}; +use smithay::backend::renderer::utils::CommitCounter; +use smithay::backend::renderer::{Frame as _, Texture as _}; +use smithay::gpu_span_location; +use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; + +use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError}; +use crate::render_helpers::background_effect::{EffectSubregion, RenderParams}; +use crate::render_helpers::blur::{Blur, BlurOptions}; +use crate::render_helpers::renderer::AsGlesFrame as _; +use crate::render_helpers::shaders::{mat3_uniform, Shaders}; + +#[derive(Debug)] +pub struct FramebufferEffect { + id: Id, + inner: Rc>>, +} + +#[derive(Debug)] +pub struct FramebufferEffectElement { + id: Id, + geometry: Rectangle, + clip_geo: Rectangle, + corner_radius: CornerRadius, + subregion: Option, + scale: f32, + blur_options: Option, + noise: f32, + saturation: f32, + inner: Rc>>, +} + +#[derive(Debug)] +struct Inner { + program: Option, + framebuffer: Option, + blur: Option, + intermediate: Option, + /// Reusable storage for subregion-filtered damage rects. + subregion_damage: Vec>, +} + +impl FramebufferEffect { + pub fn new() -> Self { + Self { + id: Id::new(), + inner: Rc::new(RefCell::new(None)), + } + } + + pub fn render( + &self, + renderer: &mut GlesRenderer, + params: RenderParams, + blur_options: Option, + noise: f32, + saturation: f32, + ) -> Option { + let (clip_geo, corner_radius) = params + .clip + .unwrap_or((params.geometry, CornerRadius::default())); + + let element = FramebufferEffectElement { + id: self.id.clone(), + geometry: params.geometry, + clip_geo, + corner_radius, + subregion: params.subregion, + scale: params.scale as f32, + blur_options, + noise, + saturation, + inner: self.inner.clone(), + }; + + { + let mut inner = element.inner.borrow_mut(); + + let inner = if let Some(inner) = &*inner { + inner + } else { + let blur = Blur::new(renderer); + inner.insert(Inner::new(renderer, blur)) + }; + + if blur_options.is_some() && inner.blur.is_none() { + // Blur is requested but the shader is unavailable. + return None; + } + } + + Some(element) + } +} + +impl FramebufferEffectElement { + fn compute_uniforms( + &self, + crop: Rectangle, + 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 { + CommitCounter::default() + } + + fn src(&self) -> Rectangle { + // 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) -> Rectangle { + self.geometry.to_physical_precise_round(scale) + } + + fn is_framebuffer_effect(&self) -> bool { + true + } +} + +impl RenderElement for FramebufferEffectElement { + fn capture_framebuffer( + &self, + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + ) -> Result<(), GlesError> { + let mut inner = self.inner.borrow_mut(); + let Some(inner) = &mut *inner else { + return Ok(()); + }; + let _span = tracy_client::span!("FramebufferEffectElement::capture_framebuffer"); + + 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 output_rect = Rectangle::from_size(frame.output_size()); + 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 transform = frame.transformation(); + 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); + + let location = gpu_span_location!("FramebufferEffectElement::capture_framebuffer"); + frame.with_gpu_span(location, |frame| { + // 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 texture = frame.create_texture(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 { + if let Err(err) = b.prepare_textures( + |fourcc, size| frame.create_texture(fourcc, size), + framebuffer, + *options, + ) { + warn!("error preparing blur textures: {err:?}"); + blur = None; + } + } + + // 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 { + match blur.render(frame, framebuffer, options) { + Ok(blurred) => inner.intermediate = Some(blurred), + Err(err) => { + warn!("error rendering blur: {err:?}"); + } + } + } + + Ok(()) + }) + } + + fn draw( + &self, + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque_regions: &[Rectangle], + ) -> Result<(), GlesError> { + let mut inner = self.inner.borrow_mut(); + let Some(inner) = &mut *inner else { + return Ok(()); + }; + + 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 uniforms = inner + .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., + inner.program.as_ref(), + uniforms, + ) + } +} + +impl<'render> RenderElement> for FramebufferEffectElement { + fn capture_framebuffer( + &self, + frame: &mut TtyFrame<'_, '_, '_>, + src: Rectangle, + dst: Rectangle, + ) -> Result<(), TtyRendererError<'render>> { + let gles_frame = frame.as_gles_frame(); + RenderElement::::capture_framebuffer(&self, gles_frame, src, dst)?; + Ok(()) + } + + fn draw( + &self, + frame: &mut TtyFrame<'_, '_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + ) -> Result<(), TtyRendererError<'render>> { + let gles_frame = frame.as_gles_frame(); + RenderElement::::draw(&self, gles_frame, src, dst, damage, opaque_regions)?; + Ok(()) + } +} + +impl Inner { + fn new(renderer: &mut GlesRenderer, blur: Option) -> Self { + let program = Shaders::get(renderer).postprocess_and_clip.clone(); + + Self { + program, + framebuffer: None, + blur, + intermediate: None, + subregion_damage: Vec::new(), + } + } +} diff --git a/src/render_helpers/mod.rs b/src/render_helpers/mod.rs index 3fe56268bb..46b6bc2003 100644 --- a/src/render_helpers/mod.rs +++ b/src/render_helpers/mod.rs @@ -1,6 +1,6 @@ use std::ptr; -use anyhow::{ensure, Context}; +use anyhow::{ensure, Context as _}; use niri_config::BlockOutFrom; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::allocator::{Buffer, Fourcc}; @@ -17,11 +17,17 @@ 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 blur; pub mod border; 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; @@ -37,12 +43,44 @@ pub mod snapshot; pub mod solid_color; pub mod surface; pub mod texture; +pub mod xray; + +/// A rendering context. +/// +/// Bundles together things needed by most rendering code. +pub struct RenderCtx<'a, R> { + pub renderer: &'a mut R, + pub target: RenderTarget, + pub xray: Option<&'a Xray>, +} + +impl<'a, R> RenderCtx<'a, R> { + /// Reborrows this context with a smaller lifetime. + #[inline] + pub fn r<'b>(&'b mut self) -> RenderCtx<'b, R> { + RenderCtx { + renderer: self.renderer, + target: self.target, + xray: self.xray, + } + } +} + +impl<'a, R: AsGlesRenderer> RenderCtx<'a, R> { + pub fn as_gles<'b>(&'b mut self) -> RenderCtx<'b, GlesRenderer> { + RenderCtx { + renderer: self.renderer.as_gles_renderer(), + target: self.target, + xray: self.xray, + } + } +} /// What we're rendering for. #[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. @@ -71,6 +109,8 @@ pub trait ToRenderElement { } impl RenderTarget { + pub const COUNT: usize = 3; + pub fn should_block_out(self, block_out_from: Option) -> bool { match block_out_from { None => false, @@ -306,6 +346,11 @@ fn render_elements( if let Some(mut damage) = output_rect.intersection(dst) { damage.loc -= dst.loc; + if element.is_framebuffer_effect() { + element + .capture_framebuffer(&mut frame, src, dst) + .context("error in capture_framebuffer()")?; + } element .draw(&mut frame, src, dst, &[damage], &[]) .context("error drawing element")?; diff --git a/src/render_helpers/offscreen.rs b/src/render_helpers/offscreen.rs index 2a7b9699dc..a275a81407 100644 --- a/src/render_helpers/offscreen.rs +++ b/src/render_helpers/offscreen.rs @@ -77,6 +77,7 @@ impl OffscreenBuffer { let _span = tracy_client::span!("OffscreenBuffer::render"); let geo = encompassing_geo(scale, elements.iter()); + // TODO: check for zero size. let elements = Vec::from_iter(elements.iter().map(|ele| { RelocateRenderElement::from_element(ele, geo.loc.upscale(-1), Relocate::Relative) })); @@ -157,13 +158,10 @@ impl OffscreenBuffer { let res = { let mut target = renderer.bind(&mut inner.texture)?; - inner.damage.render_output( - renderer, - &mut target, - 1, - &elements, - Color32F::TRANSPARENT, - )? + inner + .damage + .render_output(renderer, &mut target, 1, &elements, Color32F::TRANSPARENT) + .context("error rendering")? }; // Add the resulting damage to the outer tracker. diff --git a/src/render_helpers/render_elements.rs b/src/render_helpers/render_elements.rs index e030c29bb2..2c1a270822 100644 --- a/src/render_helpers/render_elements.rs +++ b/src/render_helpers/render_elements.rs @@ -93,11 +93,30 @@ macro_rules! niri_render_elements { $($name::$variant(elem) => elem.kind()),+ } } + + fn is_framebuffer_effect(&self) -> bool { + match self { + $($name::$variant(elem) => elem.is_framebuffer_effect()),+ + } + } } impl smithay::backend::renderer::element::RenderElement for $($name_R)? $($name_no_R)? { + fn capture_framebuffer( + &self, + frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>, + src: smithay::utils::Rectangle, + dst: smithay::utils::Rectangle, + ) -> Result<(), smithay::backend::renderer::gles::GlesError> { + match self { + $($name::$variant(elem) => { + smithay::backend::renderer::element::RenderElement::::capture_framebuffer(elem, frame, src, dst) + })+ + } + } + fn draw( &self, frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>, @@ -123,6 +142,19 @@ macro_rules! niri_render_elements { impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>> for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)? { + fn capture_framebuffer( + &self, + frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>, + src: smithay::utils::Rectangle, + dst: smithay::utils::Rectangle, + ) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> { + match self { + $($name::$variant(elem) => { + smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::capture_framebuffer(elem, frame, src, dst) + })+ + } + } + fn draw( &self, frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>, diff --git a/src/render_helpers/shaders/blur.vert b/src/render_helpers/shaders/blur.vert new file mode 100644 index 0000000000..c796897c26 --- /dev/null +++ b/src/render_helpers/shaders/blur.vert @@ -0,0 +1,11 @@ +#version 100 + +attribute vec2 vert; +varying vec2 v_coords; + +void main() { + v_coords = vert; + // vert goes from 0 to 1; position must be from -1 to 1. + vec2 position = vert * 2.0 - 1.0; + gl_Position = vec4(position, 1.0, 1.0); +} diff --git a/src/render_helpers/shaders/blur_down.frag b/src/render_helpers/shaders/blur_down.frag new file mode 100644 index 0000000000..bc4c5fa2de --- /dev/null +++ b/src/render_helpers/shaders/blur_down.frag @@ -0,0 +1,21 @@ +#version 100 + +precision highp float; + +varying vec2 v_coords; + +uniform sampler2D tex; +uniform vec2 half_pixel; +uniform float offset; + +void main() { + vec2 o = half_pixel * offset; + + vec4 sum = texture2D(tex, v_coords) * 4.0; + sum += texture2D(tex, v_coords + vec2(-o.x, -o.y)); + sum += texture2D(tex, v_coords + vec2( o.x, -o.y)); + sum += texture2D(tex, v_coords + vec2(-o.x, o.y)); + sum += texture2D(tex, v_coords + vec2( o.x, o.y)); + + gl_FragColor = sum / 8.0; +} diff --git a/src/render_helpers/shaders/blur_up.frag b/src/render_helpers/shaders/blur_up.frag new file mode 100644 index 0000000000..d565158211 --- /dev/null +++ b/src/render_helpers/shaders/blur_up.frag @@ -0,0 +1,29 @@ +#version 100 + +precision highp float; + +varying vec2 v_coords; + +uniform sampler2D tex; +uniform vec2 half_pixel; +uniform float offset; + +void main() { + vec2 o = half_pixel * offset; + + vec4 sum = vec4(0.0); + + // Four edge centers + sum += texture2D(tex, v_coords + vec2(-o.x * 2.0, 0.0)); + sum += texture2D(tex, v_coords + vec2( o.x * 2.0, 0.0)); + sum += texture2D(tex, v_coords + vec2(0.0, -o.y * 2.0)); + sum += texture2D(tex, v_coords + vec2(0.0, o.y * 2.0)); + + // Four diagonal corners + sum += texture2D(tex, v_coords + vec2(-o.x, o.y)) * 2.0; + sum += texture2D(tex, v_coords + vec2( o.x, o.y)) * 2.0; + sum += texture2D(tex, v_coords + vec2(-o.x, -o.y)) * 2.0; + sum += texture2D(tex, v_coords + vec2( o.x, -o.y)) * 2.0; + + gl_FragColor = sum / 12.0; +} diff --git a/src/render_helpers/shaders/border.frag b/src/render_helpers/shaders/border.frag index 522ab7c43e..ed0e08877f 100644 --- a/src/render_helpers/shaders/border.frag +++ b/src/render_helpers/shaders/border.frag @@ -208,35 +208,12 @@ vec4 gradient_color(vec2 coords) { return color_mix(color_from, color_to, frac); } -float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { - vec2 center; - float radius; - - if (coords.x < corner_radius.x && coords.y < corner_radius.x) { - radius = corner_radius.x; - center = vec2(radius, radius); - } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { - radius = corner_radius.y; - center = vec2(size.x - radius, radius); - } else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) { - radius = corner_radius.z; - center = vec2(size.x - radius, size.y - radius); - } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { - radius = corner_radius.w; - center = vec2(radius, size.y - radius); - } else { - return 1.0; - } - - float dist = distance(coords, center); - float half_px = 0.5 / niri_scale; - return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); -} +float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius); void main() { vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0); vec4 color = gradient_color(coords_geo.xy); - color = color * rounding_alpha(coords_geo.xy, geo_size, outer_radius); + color = color * niri_rounding_alpha(coords_geo.xy, geo_size, outer_radius); if (border_width > 0.0) { coords_geo -= vec3(border_width); @@ -245,7 +222,7 @@ void main() { && 0.0 <= coords_geo.y && coords_geo.y <= inner_geo_size.y) { vec4 inner_radius = max(outer_radius - vec4(border_width), 0.0); - color = color * (1.0 - rounding_alpha(coords_geo.xy, inner_geo_size, inner_radius)); + color = color * (1.0 - niri_rounding_alpha(coords_geo.xy, inner_geo_size, inner_radius)); } } diff --git a/src/render_helpers/shaders/clipped_surface.frag b/src/render_helpers/shaders/clipped_surface.frag index 42a0a0f6f9..e97f18c07b 100644 --- a/src/render_helpers/shaders/clipped_surface.frag +++ b/src/render_helpers/shaders/clipped_surface.frag @@ -26,30 +26,8 @@ uniform vec2 geo_size; uniform vec4 corner_radius; uniform mat3 input_to_geo; -float rounding_alpha(vec2 coords, vec2 size) { - vec2 center; - float radius; - - if (coords.x < corner_radius.x && coords.y < corner_radius.x) { - radius = corner_radius.x; - center = vec2(radius, radius); - } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { - radius = corner_radius.y; - center = vec2(size.x - radius, radius); - } else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) { - radius = corner_radius.z; - center = vec2(size.x - radius, size.y - radius); - } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { - radius = corner_radius.w; - center = vec2(radius, size.y - radius); - } else { - return 1.0; - } - - float dist = distance(coords, center); - float half_px = 0.5 / niri_scale; - return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); -} +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); @@ -60,12 +38,14 @@ 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); } else { // Apply corner rounding inside geometry. - color = color * rounding_alpha(coords_geo.xy * geo_size, geo_size); + color = color * niri_rounding_alpha(coords_geo.xy * geo_size, geo_size, corner_radius); } // Apply final alpha and tint. diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index 6fccce7f59..7be9b159fd 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -8,13 +8,16 @@ use smithay::backend::renderer::gles::{ use super::renderer::NiriRenderer; use super::shader_element::ShaderProgram; +use crate::render_helpers::blur::BlurProgram; pub struct Shaders { pub border: Option, pub shadow: Option, pub clipped_surface: Option, + pub postprocess_and_clip: Option, pub resize: Option, pub gradient_fade: Option, + pub blur: Option, pub custom_resize: RefCell>, pub custom_close: RefCell>, pub custom_open: RefCell>, @@ -35,7 +38,10 @@ impl Shaders { let border = ShaderProgram::compile( renderer, - include_str!("border.frag"), + concat!( + include_str!("border.frag"), + include_str!("rounding_alpha.frag") + ), &[ UniformName::new("colorspace", UniformType::_1f), UniformName::new("hue_interpolation", UniformType::_1f), @@ -58,7 +64,10 @@ impl Shaders { let shadow = ShaderProgram::compile( renderer, - include_str!("shadow.frag"), + concat!( + include_str!("shadow.frag"), + include_str!("rounding_alpha.frag") + ), &[ UniformName::new("shadow_color", UniformType::_4f), UniformName::new("sigma", UniformType::_1f), @@ -78,7 +87,11 @@ impl Shaders { let clipped_surface = renderer .compile_custom_texture_shader( - include_str!("clipped_surface.frag"), + concat!( + include_str!("clipped_surface.frag"), + include_str!("rounding_alpha.frag"), + "\nvec4 postprocess(vec4 color) { return color; }", + ), &[ UniformName::new("niri_scale", UniformType::_1f), UniformName::new("geo_size", UniformType::_2f), @@ -91,6 +104,28 @@ 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("noise", UniformType::_1f), + UniformName::new("saturation", UniformType::_1f), + 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:?}"); @@ -107,12 +142,20 @@ impl Shaders { }) .ok(); + let blur = BlurProgram::compile(renderer) + .map_err(|err| { + warn!("error compiling blur shaders: {err:?}"); + }) + .ok(); + Self { border, shadow, clipped_surface, + postprocess_and_clip, resize, gradient_fade, + blur, custom_resize: RefCell::new(None), custom_close: RefCell::new(None), custom_open: RefCell::new(None), @@ -183,6 +226,7 @@ fn compile_resize_program( let mut program = include_str!("resize_prelude.frag").to_string(); program.push_str(src); program.push_str(include_str!("resize_epilogue.frag")); + program.push_str(include_str!("rounding_alpha.frag")); ShaderProgram::compile( renderer, diff --git a/src/render_helpers/shaders/postprocess.frag b/src/render_helpers/shaders/postprocess.frag new file mode 100644 index 0000000000..92bf656c0d --- /dev/null +++ b/src/render_helpers/shaders/postprocess.frag @@ -0,0 +1,30 @@ +uniform float noise; +uniform float saturation; +uniform vec4 bg_color; + +// Interleaved Gradient Noise +float gradient_noise(vec2 uv) { + const vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189); + return fract(magic.z * fract(dot(uv, magic.xy))); +} + +vec3 saturate(vec3 color, float sat) { + const vec3 w = vec3(0.2126, 0.7152, 0.0722); + return mix(vec3(dot(color, w)), color, sat); +} + +vec4 postprocess(vec4 color) { + if (saturation != 1.0) { + color.rgb = saturate(color.rgb, saturation); + } + + if (noise > 0.0) { + vec2 uv = gl_FragCoord.xy; + color.rgb += (gradient_noise(uv) - 0.5) * noise; + } + + // Mix bg_color behind the texture (both premultiplied alpha). + color = color + bg_color * (1.0 - color.a); + + return color; +} diff --git a/src/render_helpers/shaders/resize_epilogue.frag b/src/render_helpers/shaders/resize_epilogue.frag index 12ed890ff8..82c20d67d6 100644 --- a/src/render_helpers/shaders/resize_epilogue.frag +++ b/src/render_helpers/shaders/resize_epilogue.frag @@ -12,7 +12,7 @@ void main() { color = vec4(0.0); } else { // Apply corner rounding inside geometry. - color = color * niri_rounding_alpha(coords_curr_geo.xy * size_curr_geo.xy, size_curr_geo.xy); + color = color * niri_rounding_alpha(coords_curr_geo.xy * size_curr_geo.xy, size_curr_geo.xy, niri_corner_radius); } } diff --git a/src/render_helpers/shaders/resize_prelude.frag b/src/render_helpers/shaders/resize_prelude.frag index ffb46c9386..6e672f423f 100644 --- a/src/render_helpers/shaders/resize_prelude.frag +++ b/src/render_helpers/shaders/resize_prelude.frag @@ -27,27 +27,4 @@ uniform float niri_clip_to_geometry; uniform float niri_alpha; uniform float niri_scale; -float niri_rounding_alpha(vec2 coords, vec2 size) { - vec2 center; - float radius; - - if (coords.x < niri_corner_radius.x && coords.y < niri_corner_radius.x) { - radius = niri_corner_radius.x; - center = vec2(radius, radius); - } else if (size.x - niri_corner_radius.y < coords.x && coords.y < niri_corner_radius.y) { - radius = niri_corner_radius.y; - center = vec2(size.x - radius, radius); - } else if (size.x - niri_corner_radius.z < coords.x && size.y - niri_corner_radius.z < coords.y) { - radius = niri_corner_radius.z; - center = vec2(size.x - radius, size.y - radius); - } else if (coords.x < niri_corner_radius.w && size.y - niri_corner_radius.w < coords.y) { - radius = niri_corner_radius.w; - center = vec2(radius, size.y - radius); - } else { - return 1.0; - } - - float dist = distance(coords, center); - float half_px = 0.5 / niri_scale; - return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); -} +float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius); diff --git a/src/render_helpers/shaders/rounding_alpha.frag b/src/render_helpers/shaders/rounding_alpha.frag new file mode 100644 index 0000000000..e1c9527e2e --- /dev/null +++ b/src/render_helpers/shaders/rounding_alpha.frag @@ -0,0 +1,27 @@ +float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { + vec2 center; + float radius; + + if (coords.x < corner_radius.x && coords.y < corner_radius.x) { + radius = corner_radius.x; + center = vec2(radius, radius); + } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { + radius = corner_radius.y; + center = vec2(size.x - radius, radius); + } else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) { + radius = corner_radius.z; + center = vec2(size.x - radius, size.y - radius); + } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { + radius = corner_radius.w; + center = vec2(radius, size.y - radius); + } else { + return 1.0; + } + + float dist = distance(coords, center); + + // Manual smoothstep() between radius - half_px and radius + half_px + // to avoid a division in clamp(). + float t = clamp((dist - radius) * niri_scale + 0.5, 0.0, 1.0); + return 1.0 - t * t * (3.0 - 2.0 * t); +} diff --git a/src/render_helpers/shaders/shadow.frag b/src/render_helpers/shaders/shadow.frag index 3912d71ff9..98d5fd60f6 100644 --- a/src/render_helpers/shaders/shadow.frag +++ b/src/render_helpers/shaders/shadow.frag @@ -72,30 +72,7 @@ float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float co return value; } -float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { - vec2 center; - float radius; - - if (coords.x < corner_radius.x && coords.y < corner_radius.x) { - radius = corner_radius.x; - center = vec2(radius, radius); - } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { - radius = corner_radius.y; - center = vec2(size.x - radius, radius); - } else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) { - radius = corner_radius.z; - center = vec2(size.x - radius, size.y - radius); - } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { - radius = corner_radius.w; - center = vec2(radius, size.y - radius); - } else { - return 1.0; - } - - float dist = distance(coords, center); - float half_px = 0.5 / niri_scale; - return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); -} +float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius); void main() { vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0); @@ -106,7 +83,7 @@ void main() { float shadow_value; if (sigma < 0.1) { // With low enough sigma just draw a rounded rectangle. - shadow_value = rounding_alpha(coords_geo.xy, geo_size, corner_radius); + shadow_value = niri_rounding_alpha(coords_geo.xy, geo_size, corner_radius); } else { shadow_value = roundedBoxShadow( vec2(0.0, 0.0), @@ -126,7 +103,7 @@ void main() { if (window_geo_size != vec2(0.0, 0.0)) { if (0.0 <= coords_window_geo.x && coords_window_geo.x <= window_geo_size.x && 0.0 <= coords_window_geo.y && coords_window_geo.y <= window_geo_size.y) { - float alpha = rounding_alpha(coords_window_geo.xy, window_geo_size, window_corner_radius); + float alpha = niri_rounding_alpha(coords_window_geo.xy, window_geo_size, window_corner_radius); color = color * (1.0 - alpha); } } diff --git a/src/render_helpers/snapshot.rs b/src/render_helpers/snapshot.rs index 9b4cd4247a..d0e24fea07 100644 --- a/src/render_helpers/snapshot.rs +++ b/src/render_helpers/snapshot.rs @@ -6,7 +6,8 @@ use smithay::backend::renderer::element::{Kind, RenderElement}; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform}; -use super::{render_to_encompassing_texture, RenderTarget, ToRenderElement}; +use super::{render_to_encompassing_texture, ToRenderElement}; +use crate::render_helpers::{RenderCtx, RenderTarget}; /// Snapshot of a render. #[derive(Debug)] @@ -16,6 +17,12 @@ pub struct RenderSnapshot { /// Relative to the geometry. pub contents: Vec, + /// 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>, + /// Blocked-out contents. /// /// Relative to the geometry. @@ -30,6 +37,9 @@ pub struct RenderSnapshot { /// Contents rendered into a texture (lazily). pub texture: OnceCell)>>, + /// Contents with blocked-out bg rendered into a texture (lazily). + pub texture_with_blocked_out_bg: OnceCell)>>, + /// Blocked-out contents rendered into a texture (lazily). pub blocked_out_texture: OnceCell)>>, } @@ -43,11 +53,10 @@ where { pub fn texture( &self, - renderer: &mut GlesRenderer, + ctx: RenderCtx, scale: Scale, - target: RenderTarget, ) -> Option<&(GlesTexture, Rectangle)> { - if target.should_block_out(self.block_out_from) { + if ctx.target.should_block_out(self.block_out_from) { self.blocked_out_texture.get_or_init(|| { let _span = tracy_client::span!("RenderSnapshot::texture"); @@ -60,7 +69,7 @@ where .collect(); match render_to_encompassing_texture( - renderer, + ctx.renderer, scale, Transform::Normal, Fourcc::Abgr8888, @@ -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"); @@ -86,7 +122,7 @@ where .collect(); match render_to_encompassing_texture( - renderer, + ctx.renderer, scale, Transform::Normal, Fourcc::Abgr8888, diff --git a/src/render_helpers/xray.rs b/src/render_helpers/xray.rs new file mode 100644 index 0000000000..5b87d74351 --- /dev/null +++ b/src/render_helpers/xray.rs @@ -0,0 +1,317 @@ +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, Rectangle, Scale, Size, Transform}; + +use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError}; +use crate::render_helpers::background_effect::{EffectSubregion, 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>; RenderTarget::COUNT], + pub backdrop: [Rc>; RenderTarget::COUNT], + pub backdrop_color: Color32F, + pub workspaces: Vec<(Rectangle, Color32F)>, +} + +#[derive(Debug)] +pub struct XrayElement { + buffer: Rc>, + id: Id, + geometry: Rectangle, + src: Rectangle, + subregion: Option, + input_to_clip_geo: Mat3, + clip_geo_size: Vec2, + corner_radius: CornerRadius, + scale: f32, + blur: bool, + noise: f32, + saturation: f32, + bg_color: Color32F, + program: Option, +} + +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, + params: RenderParams, + blur: bool, + noise: f32, + saturation: f32, + push: &mut dyn FnMut(XrayElement), + ) { + let program = Shaders::get(ctx.renderer).postprocess_and_clip.clone(); + + let (clip_geo, corner_radius) = params + .clip + .unwrap_or((params.geometry, CornerRadius::default())); + + let clip_pos_in_backdrop = + params.pos_in_backdrop + (clip_geo.loc - params.geometry.loc).upscale(params.zoom); + + let geo_in_backdrop = Rectangle::new( + params.pos_in_backdrop, + params.geometry.size.upscale(params.zoom), + ); + + let mut skip_backdrop = false; + + let mut background = self.background[ctx.target as usize].borrow_mut(); + let prev = background.commit(); + if background.prepare(ctx.renderer, blur) { + if background.commit() != prev { + debug!("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; + }; + + // This can be different from params.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 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 / params.zoom as f32) + * Mat3::from_scale(buf_size / clip_geo_size) + * Mat3::from_translation(-pos_against_buf / 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, + &background.logical_size(), + ); + + let mut geometry = Rectangle::new(crop.loc - params.pos_in_backdrop, crop.size) + .downscale(params.zoom); + geometry.loc += params.geometry.loc; + + let elem = XrayElement { + buffer: self.background[ctx.target as usize].clone(), + id: background.id().clone(), + geometry, + src, + subregion: params.subregion.clone(), + input_to_clip_geo, + clip_geo_size, + corner_radius, + scale: params.scale as f32, + blur, + noise, + saturation, + 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 mut backdrop = self.backdrop[ctx.target as usize].borrow_mut(); + let prev = backdrop.commit(); + if backdrop.prepare(ctx.renderer, blur) { + if backdrop.commit() != prev { + debug!("backdrop damaged"); + } + + let src = geo_in_backdrop.to_buffer( + backdrop.scale(), + Transform::Normal, + &backdrop.logical_size(), + ); + + let clip_pos_in_backdrop = + Vec2::new(clip_pos_in_backdrop.x as f32, clip_pos_in_backdrop.y as f32); + + let clip_size_in_backdrop = clip_geo.size.upscale(params.zoom); + let clip_geo_size = Vec2::new( + clip_size_in_backdrop.w as f32, + clip_size_in_backdrop.h as f32, + ); + let buf_size = backdrop.logical_size(); + 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, + subregion: params.subregion.clone(), + input_to_clip_geo, + clip_geo_size, + corner_radius: corner_radius.scaled_by(params.zoom as f32), + scale: params.scale as f32, + blur, + noise, + saturation, + bg_color: self.backdrop_color, + program: program.clone(), + }; + push(elem); + } + } +} + +impl XrayElement { + fn compute_uniforms(&self) -> [Uniform<'static>; 7] { + [ + 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("noise", self.noise), + Uniform::new("saturation", self.saturation), + 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 { + self.src + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.geometry.to_physical_precise_round(scale) + } + + fn opaque_regions(&self, _scale: Scale) -> OpaqueRegions { + // TODO: if bg_color alpha is 1 then compute opaque regions here taking corners into account + OpaqueRegions::default() + } +} + +impl RenderElement for XrayElement { + fn draw( + &self, + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + _opaque_regions: &[Rectangle], + ) -> Result<(), GlesError> { + let mut buffer = self.buffer.borrow_mut(); + let texture = match buffer.render(frame, self.blur) { + Ok(x) => x, + Err(err) => { + warn!("error rendering effect buffer: {err:?}"); + return Ok(()); + } + }; + + let mut filtered_damage = Vec::new(); + let damage = if let Some(subregion) = &self.subregion { + let src_to_geo = self.geometry.size / self.src.size; + + // Compute crop in geometry coordinates. + let mut crop = src; + crop.loc -= self.src.loc; + crop = crop.upscale(src_to_geo); + let mut crop = crop.to_logical(1., Transform::Normal, &Size::default()); + + // Then convert to subregion coordinates. + crop.loc += self.geometry.loc; + + subregion.filter_damage(crop, dst, damage, &mut filtered_damage); + + if filtered_damage.is_empty() { + return Ok(()); + } + &filtered_damage[..] + } else { + damage + }; + + 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, + // TODO: opaque regions need to be filtered like damage. + &[], + Transform::Normal, + 1., + self.program.as_ref(), + uniforms, + ) + } +} + +impl<'render> RenderElement> for XrayElement { + fn draw( + &self, + frame: &mut TtyFrame<'_, '_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + ) -> Result<(), TtyRendererError<'render>> { + let gles_frame = frame.as_gles_frame(); + RenderElement::::draw(&self, gles_frame, src, dst, damage, opaque_regions)?; + Ok(()) + } +} diff --git a/src/screencasting/mod.rs b/src/screencasting/mod.rs index ca5cee3309..53ac5d2ff5 100644 --- a/src/screencasting/mod.rs +++ b/src/screencasting/mod.rs @@ -19,7 +19,7 @@ use zbus::object_server::SignalEmitter; use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri, StreamTargetId}; use crate::niri::{CastTarget, Niri, OutputRenderElements, PointerRenderElements, State}; use crate::niri_render_elements; -use crate::render_helpers::RenderTarget; +use crate::render_helpers::{RenderCtx, RenderTarget}; use crate::utils::{get_monotonic_time, CastSessionId, CastStreamId}; use crate::window::mapped::{MappedId, WindowCastRenderElements}; @@ -575,13 +575,12 @@ impl Niri { } if cursor_data.is_none() { - self.render_inner( + let ctx = RenderCtx { renderer, - output, - false, - RenderTarget::Screencast, - &mut |elem| elements.push(elem.into()), - ); + target: RenderTarget::Screencast, + xray: None, + }; + self.render(ctx, output, false, &mut |elem| elements.push(elem.into())); let mut pointer_pos = Point::default(); if self.pointer_visibility.is_visible() { diff --git a/src/ui/mru.rs b/src/ui/mru.rs index 1170d33e71..90d2487c70 100644 --- a/src/ui/mru.rs +++ b/src/ui/mru.rs @@ -36,7 +36,7 @@ use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement}; use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement}; -use crate::render_helpers::RenderTarget; +use crate::render_helpers::RenderCtx; use crate::utils::{ baba_is_float_offset, output_size, round_logical_in_physical, to_physical_precise_round, with_toplevel_role, @@ -338,14 +338,13 @@ impl Thumbnail { #[allow(clippy::too_many_arguments)] fn render( &self, - renderer: &mut R, + mut ctx: RenderCtx, config: &niri_config::RecentWindows, mapped: &Mapped, preview_geo: Rectangle, scale: f64, is_active: bool, bob_y: f64, - target: RenderTarget, push: &mut dyn FnMut(WindowMruUiRenderElement), ) { let _span = tracy_client::span!("Thumbnail::render"); @@ -377,8 +376,8 @@ impl Thumbnail { } .unwrap_or_default(); - let has_border_shader = BorderRenderElement::has_shader(renderer); - let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned(); + let has_border_shader = BorderRenderElement::has_shader(ctx.renderer); + let clip_shader = ClippedSurfaceRenderElement::shader(ctx.renderer).cloned(); let geo = Rectangle::from_size(self.size.to_f64()); // FIXME: deduplicate code with Tile::render_inner() let clip = move |elem| match elem { @@ -444,21 +443,14 @@ impl Thumbnail { }; // FIXME: this could use mipmaps, for that it should be rendered through an offscreen. - mapped.render_normal( - renderer, - Point::new(0., 0.), - s, - preview_alpha, - target, - &mut |elem| { - let elem = clip(elem); - let elem = downscale(elem); - push(elem) - }, - ); + mapped.render_normal(ctx.r(), Point::new(0., 0.), s, preview_alpha, &mut |elem| { + let elem = clip(elem); + let elem = downscale(elem); + push(elem) + }); let mut title_size = None; - let title_texture = self.title_texture(renderer.as_gles_renderer(), mapped, scale); + let title_texture = self.title_texture(ctx.as_gles().renderer, mapped, scale); let title_texture = title_texture.map(|texture| { let mut size = texture.logical_size(); size.w = f64::min(size.w, preview_geo.size.w); @@ -469,7 +461,7 @@ impl Thumbnail { // Hide title for blocked-out windows, but only after computing the title size. This way, // the background and the border won't have to oscillate in size between normal and // screencast renders, causing excessive damage. - let should_block_out = target.should_block_out(mapped.rules().block_out_from); + let should_block_out = ctx.target.should_block_out(mapped.rules().block_out_from); let title_texture = title_texture.filter(|_| !should_block_out); if let Some((texture, size)) = title_texture { @@ -491,8 +483,8 @@ impl Thumbnail { Kind::Unspecified, ); - let renderer = renderer.as_gles_renderer(); - if let Some(program) = GradientFadeTextureRenderElement::shader(renderer) { + let ctx = ctx.as_gles(); + if let Some(program) = GradientFadeTextureRenderElement::shader(ctx.renderer) { let elem = GradientFadeTextureRenderElement::new(texture, program); push(WindowMruUiRenderElement::GradientFadeElem(elem)); } else { @@ -542,7 +534,7 @@ impl Thumbnail { scale, 0.5, ); - background.render(renderer, loc, &mut |elem| { + background.render(ctx.renderer, loc, &mut |elem| { push(WindowMruUiRenderElement::FocusRing(elem)) }); @@ -564,7 +556,7 @@ impl Thumbnail { 1., ); - border.render(renderer, loc, &mut |elem| { + border.render(ctx.renderer, loc, &mut |elem| { push(WindowMruUiRenderElement::FocusRing(elem)) }); } @@ -1100,8 +1092,7 @@ impl WindowMruUi { &self, niri: &Niri, output: &Output, - renderer: &mut R, - target: RenderTarget, + mut ctx: RenderCtx, push: &mut dyn FnMut(WindowMruUiRenderElement), ) { let (inner, progress) = match &self.state { @@ -1139,14 +1130,17 @@ impl WindowMruUi { // During the closing fade, use an offscreen to avoid transparent compositing artifacts. let mut pushed_offscreen = false; if *output == inner.output && alpha < 1. { - let renderer = renderer.as_gles_renderer(); + let mut ctx = ctx.as_gles(); let mut elems = Vec::new(); - inner.render(niri, renderer, target, &mut |elem| elems.push(elem)); + inner.render(niri, ctx.r(), &mut |elem| elems.push(elem)); elems.push(WindowMruUiRenderElement::SolidColor(render_backdrop(1.))); let scale = output.current_scale().fractional_scale(); - match inner.offscreen.render(renderer, Scale::from(scale), &elems) { + match inner + .offscreen + .render(ctx.renderer, Scale::from(scale), &elems) + { Ok((elem, _sync, _data)) => { // FIXME: would be good to passthrough offscreen data to visible windows here. // As is, during the closing fade, windows from other workspaces stop receiving @@ -1172,7 +1166,7 @@ impl WindowMruUi { // This is not used as fallback when offscreen fails to render because it looks better to // hide the previews immediately than to render them with alpha = 1. during a fade-out. if *output == inner.output && alpha == 1. { - inner.render(niri, renderer, target, &mut |elem| push(elem)); + inner.render(niri, ctx, &mut |elem| push(elem)); } // This is used for both normal elems and for other outputs. @@ -1554,8 +1548,7 @@ impl Inner { fn render( &self, niri: &Niri, - renderer: &mut R, - target: RenderTarget, + mut ctx: RenderCtx, push: &mut dyn FnMut(WindowMruUiRenderElement), ) { let output_size = output_size(&self.output); @@ -1564,7 +1557,7 @@ impl Inner { let panel_texture = self.scope_panel .borrow_mut() - .get(renderer.as_gles_renderer(), scale, self.wmru.scope); + .get(ctx.as_gles().renderer, scale, self.wmru.scope); if let Some(texture) = panel_texture { let padding = round_logical_in_physical(scale, f64::from(PANEL_PADDING)); @@ -1598,9 +1591,7 @@ impl Inner { let config = &config.recent_windows; let is_active = Some(id) == current_id; - thumbnail.render( - renderer, config, mapped, geo, scale, is_active, bob_y, target, push, - ); + thumbnail.render(ctx.r(), config, mapped, geo, scale, is_active, bob_y, push); } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 84804a0237..c677f8f09e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -35,6 +35,7 @@ use crate::handlers::KdeDecorationsModeState; use crate::niri::ClientState; pub mod id; +pub mod region; pub mod scale; pub mod signals; pub mod spawning; diff --git a/src/utils/region.rs b/src/utils/region.rs new file mode 100644 index 0000000000..451ec73ae5 --- /dev/null +++ b/src/utils/region.rs @@ -0,0 +1,233 @@ +use std::cmp::{max, min}; +use std::collections::BTreeSet; + +use smithay::utils::{Logical, Rectangle}; +use smithay::wayland::compositor::{RectangleKind, RegionAttributes}; + +pub fn region_to_non_overlapping_rects( + region: &RegionAttributes, + output: &mut Vec>, +) { + let _span = tracy_client::span!("region_to_non_overlapping_rects"); + + output.clear(); + + // Collect all unique Y coordinates. + let ys = BTreeSet::from_iter( + region + .rects + .iter() + .flat_map(|(_, r)| [r.loc.y, r.loc.y + r.size.h]), + ); + + let mut ys = ys.into_iter(); + let Some(mut lo) = ys.next() else { + // The region was empty. + return; + }; + + // Sorted list of non-overlapping [start, end) tuples. + let mut spans = Vec::<(i32, i32)>::new(); + + // Iterate over Y bands. + for hi in ys { + spans.clear(); + + 'region: for (kind, r) in ®ion.rects { + // Skip rects that don't overlap with the Y band. + if hi <= r.loc.y || r.loc.y + r.size.h <= lo { + continue; + } + + let mut x1 = r.loc.x; + let mut x2 = r.loc.x + r.size.w; + if x1 == x2 { + // Empty rect. + continue; + } + + match *kind { + RectangleKind::Add => { + // Iterate over existing spans backwards. + for i in (0..spans.len()).rev() { + let (start, end) = spans[i]; + + // New span is to the right. + if end < x1 { + spans.insert(i + 1, (x1, x2)); + continue 'region; + } + + // New span is to the left. + if x2 < start { + continue; + } + + // New span overlaps this span; merge them. + spans.remove(i); + x1 = min(x1, start); + x2 = max(x2, end); + } + + spans.insert(0, (x1, x2)); + } + RectangleKind::Subtract => { + // Iterate over existing spans backwards. + for i in (0..spans.len()).rev() { + let (start, end) = spans[i]; + + // Subtract span is to the right. + if end <= x1 { + continue 'region; + } + + // Subtract span is to the left. + if x2 <= start { + continue; + } + + // Subtract span overlaps this span. + spans.remove(i); + if x2 < end { + spans.insert(i, (x2, end)); + } + if start < x1 { + spans.insert(i, (start, x1)); + } + } + } + } + } + + for (x1, x2) in spans.drain(..) { + output.push(Rectangle::from_extremities((x1, lo), (x2, hi))); + } + + lo = hi; + } +} + +#[cfg(test)] +mod tests { + use std::fmt::Write as _; + + use insta::assert_snapshot; + use proptest::prelude::*; + use smithay::utils::{Logical, Point, Rectangle, Size}; + use smithay::wayland::compositor::{RectangleKind, RegionAttributes}; + + use super::region_to_non_overlapping_rects; + + #[allow(clippy::type_complexity)] + fn check(rects: &[(RectangleKind, (i32, i32, i32, i32))]) -> String { + let region = RegionAttributes { + rects: rects + .iter() + .map(|(kind, (x1, y1, x2, y2))| { + (*kind, Rectangle::from_extremities((*x1, *y1), (*x2, *y2))) + }) + .collect(), + }; + let mut output = Vec::new(); + region_to_non_overlapping_rects(®ion, &mut output); + let mut s = String::new(); + for r in &output { + let x1 = r.loc.x; + let y1 = r.loc.y; + let x2 = x1 + r.size.w; + let y2 = y1 + r.size.h; + writeln!(s, "{x1:2} {y1:2} - {x2:2} {y2:2}").unwrap(); + } + s + } + + #[test] + fn test_region_to_non_overlapping_rects() { + use RectangleKind::*; + + // empty_region + assert_snapshot!(check(&[]), @""); + + // single_rectangle + assert_snapshot!(check(&[(Add, (0, 0, 10, 10))]), @" 0 0 - 10 10"); + + // empty_rectangle + assert_snapshot!(check(&[(Add, (0, 0, 0, 1))]), @""); + assert_snapshot!(check(&[(Add, (0, 0, 1, 0))]), @""); + + // two_non_overlapping + assert_snapshot!( + check(&[(Add, (0, 0, 5, 10)), (Add, (7, 0, 12, 10))]), + @" + 0 0 - 5 10 + 7 0 - 12 10 + " + ); + + // two_overlapping + assert_snapshot!( + check(&[(Add, (0, 0, 10, 10)), (Add, (5, 5, 15, 15))]), + @" + 0 0 - 10 5 + 0 5 - 15 10 + 5 10 - 15 15 + " + ); + + // subtraction + assert_snapshot!( + check(&[(Add, (0, 0, 20, 20)), (Subtract, (5, 5, 15, 15))]), + @" + 0 0 - 20 5 + 0 5 - 5 15 + 15 5 - 20 15 + 0 15 - 20 20 + " + ); + + // adjacent_rectangles + assert_snapshot!( + check(&[(Add, (0, 0, 10, 10)), (Add, (10, 0, 20, 10))]), + @" 0 0 - 20 10" + ); + } + + proptest! { + #[test] + fn non_overlapping_output( + rects in proptest::collection::vec( + ( + prop_oneof![Just(RectangleKind::Add), Just(RectangleKind::Subtract)], + (0..20i32, 0..20i32, 0..20i32, 0..20i32), + ), + 1..10, + ) + ) { + let region = RegionAttributes { + rects: rects + .into_iter() + .map(|(kind, (x, y, w, h))| { + (kind, Rectangle::new(Point::new(x, y), Size::new(w, h))) + }) + .collect(), + }; + + let mut output: Vec> = Vec::new(); + region_to_non_overlapping_rects(®ion, &mut output); + + for i in 0..output.len() { + prop_assert!(!output[i].is_empty()); + + // Verify no pair of output rectangles overlaps. + for j in (i + 1)..output.len() { + prop_assert!( + !output[i].overlaps(output[j]), + "rectangles overlap: {:?} and {:?}", + output[i], + output[j], + ); + } + } + } + } +} diff --git a/src/window/mapped.rs b/src/window/mapped.rs index e951e3324d..565ad31316 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -1,10 +1,12 @@ use std::cell::{Cell, Ref, RefCell}; +use std::sync::Arc; use std::time::Duration; use niri_config::{Color, CornerRadius, GradientInterpolation, WindowRule}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::gles::GlesRenderer; +use smithay::backend::renderer::utils::RendererSurfaceStateUserData; use smithay::desktop::space::SpaceElement as _; use smithay::desktop::{PopupManager, Window}; use smithay::output::{self, Output}; @@ -22,6 +24,7 @@ use smithay::wayland::shell::xdg::{ use wayland_backend::server::Credentials; use super::{ResolvedWindowRules, WindowRef}; +use crate::handlers::background_effect::get_cached_blur_region; use crate::handlers::KdeDecorationsModeState; use crate::layout::{ ConfigureIntent, InteractiveResizeData, LayoutElement, LayoutElementRenderElement, @@ -36,7 +39,7 @@ 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, RenderTarget}; +use crate::render_helpers::{BakedBuffer, RenderCtx, RenderTarget}; use crate::utils::id::IdCounter; use crate::utils::transaction::Transaction; use crate::utils::{ @@ -407,10 +410,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(), } } @@ -520,11 +525,14 @@ impl Mapped { }; self.render( - renderer, + RenderCtx { + renderer, + target: RenderTarget::Screencast, + xray: None, + }, location, scale, 1., - RenderTarget::Screencast, &mut |elem| push(use_border(elem)), ); } @@ -613,14 +621,13 @@ impl LayoutElement for Mapped { fn render_normal( &self, - renderer: &mut R, + ctx: RenderCtx, location: Point, scale: Scale, alpha: f32, - target: RenderTarget, push: &mut dyn FnMut(LayoutElementRenderElement), ) { - if target.should_block_out(self.rules.block_out_from) { + if ctx.target.should_block_out(self.rules.block_out_from) { let mut buffer = self.block_out_buffer.borrow_mut(); buffer.resize(self.window.geometry().size.to_f64()); let elem = @@ -631,7 +638,7 @@ impl LayoutElement for Mapped { let surface = self.toplevel().wl_surface(); let mut push = |elem: WaylandSurfaceRenderElement| push(elem.into()); push_elements_from_surface_tree( - renderer, + ctx.renderer, surface, buf_pos.to_physical_precise_round(scale), scale, @@ -644,14 +651,13 @@ impl LayoutElement for Mapped { fn render_popups( &self, - renderer: &mut R, + ctx: RenderCtx, location: Point, scale: Scale, alpha: f32, - target: RenderTarget, push: &mut dyn FnMut(LayoutElementRenderElement), ) { - if target.should_block_out(self.rules.block_out_from) { + if ctx.target.should_block_out(self.rules.block_out_from) { return; } @@ -662,7 +668,7 @@ impl LayoutElement for Mapped { let offset = self.window.geometry().loc + popup_offset - popup.geometry().loc; push_elements_from_surface_tree( - renderer, + ctx.renderer, popup.wl_surface(), (buf_pos + offset.to_f64()).to_physical_precise_round(scale), scale, @@ -1294,6 +1300,30 @@ impl LayoutElement for Mapped { Some(self.interactive_resize.as_ref()?.data()) } + fn main_surface_geo(&self) -> Rectangle { + with_states(self.toplevel().wl_surface(), |states| { + let geo_loc = states + .cached_state + .get::() + .current() + .geometry + .unwrap_or_default() + .loc; + + let data = states.data_map.get::(); + data.and_then(|d| d.lock().unwrap().view()) + .map(|view| Rectangle { + loc: view.offset - geo_loc, + size: view.dst, + }) + }) + .unwrap_or_default() + } + + fn blur_region(&self) -> Option>>> { + with_states(self.toplevel().wl_surface(), get_cached_blur_region) + } + fn on_commit(&mut self, commit_serial: Serial) { if let Some(InteractiveResize::WaitingForLastCommit { serial, .. }) = &self.interactive_resize diff --git a/src/window/mod.rs b/src/window/mod.rs index c8c358f156..c917d8d2ae 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -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, + + /// 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());