feat(blur): implement for windows & layer surfaces

Co-authored-by: visualglitch91 <visualglitch91@proton.me>
Co-authored-by: nferhat <nferhat20@gmail.com>
This commit is contained in:
2025-11-23 13:36:20 +01:00
parent 1b7b2b5542
commit 99e8ceafaa
32 changed files with 2213 additions and 26 deletions

View File

@@ -338,6 +338,36 @@ impl MergeWith<BorderRule> for FocusRing {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Blur {
pub on: bool,
pub passes: u32,
pub radius: FloatOrInt<0, 1024>,
pub noise: FloatOrInt<0, 1024>,
}
impl Default for Blur {
fn default() -> Self {
Self {
on: false,
passes: 0,
radius: FloatOrInt(0.0),
noise: FloatOrInt(0.0),
}
}
}
impl MergeWith<BlurRule> for Blur {
fn merge_with(&mut self, part: &BlurRule) {
self.on |= part.on;
if part.off {
self.on = false;
}
merge_clone!((self, part), passes, radius, noise);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Shadow {
pub on: bool,
@@ -637,6 +667,20 @@ pub struct BorderRule {
pub urgent_gradient: Option<Gradient>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct BlurRule {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub on: bool,
#[knuffel(child, unwrap(argument))]
pub passes: Option<u32>,
#[knuffel(child, unwrap(argument))]
pub radius: Option<FloatOrInt<0, 1024>>,
#[knuffel(child, unwrap(argument))]
pub noise: Option<FloatOrInt<0, 1024>>,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct ShadowRule {
#[knuffel(child)]
@@ -688,6 +732,14 @@ impl MergeWith<Self> for BorderRule {
}
}
impl MergeWith<Self> for BlurRule {
fn merge_with(&mut self, part: &Self) {
merge_on_off!((self, part));
merge_clone_opt!((self, part), passes, radius, noise);
}
}
impl MergeWith<Self> for ShadowRule {
fn merge_with(&mut self, part: &Self) {
merge_on_off!((self, part));

View File

@@ -1,5 +1,6 @@
use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule};
use crate::utils::RegexEq;
use crate::BlurRule;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
@@ -14,6 +15,8 @@ pub struct LayerRule {
pub block_out_from: Option<BlockOutFrom>,
#[knuffel(child, default)]
pub shadow: ShadowRule,
#[knuffel(child, default)]
pub blur: BlurRule,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, unwrap(argument))]

View File

@@ -5,12 +5,15 @@ use crate::appearance::{
Border, FocusRing, InsertHint, Shadow, TabIndicator, DEFAULT_BACKGROUND_COLOR,
};
use crate::utils::{expect_only_children, Flag, MergeWith};
use crate::{BorderRule, Color, FloatOrInt, InsertHintPart, ShadowRule, TabIndicatorPart};
use crate::{
Blur, BlurRule, BorderRule, Color, FloatOrInt, InsertHintPart, ShadowRule, TabIndicatorPart,
};
#[derive(Debug, Clone, PartialEq)]
pub struct Layout {
pub focus_ring: FocusRing,
pub border: Border,
pub blur: Blur,
pub shadow: Shadow,
pub tab_indicator: TabIndicator,
pub insert_hint: InsertHint,
@@ -30,6 +33,7 @@ impl Default for Layout {
Self {
focus_ring: FocusRing::default(),
border: Border::default(),
blur: Blur::default(),
shadow: Shadow::default(),
tab_indicator: TabIndicator::default(),
insert_hint: InsertHint::default(),
@@ -60,6 +64,7 @@ impl MergeWith<LayoutPart> for Layout {
(self, part),
focus_ring,
border,
blur,
shadow,
tab_indicator,
insert_hint,
@@ -98,6 +103,8 @@ pub struct LayoutPart {
#[knuffel(child)]
pub border: Option<BorderRule>,
#[knuffel(child)]
pub blur: Option<BlurRule>,
#[knuffel(child)]
pub shadow: Option<ShadowRule>,
#[knuffel(child)]
pub tab_indicator: Option<TabIndicatorPart>,

View File

@@ -1,7 +1,7 @@
use crate::appearance::{BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule};
use crate::layout::DefaultPresetSize;
use crate::utils::RegexEq;
use crate::FloatOrInt;
use crate::{BlurRule, FloatOrInt};
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct WindowRule {
@@ -45,6 +45,8 @@ pub struct WindowRule {
#[knuffel(child, default)]
pub border: BorderRule,
#[knuffel(child, default)]
pub blur: BlurRule,
#[knuffel(child, default)]
pub shadow: ShadowRule,
#[knuffel(child, default)]
pub tab_indicator: TabIndicatorRule,

View File

@@ -120,7 +120,7 @@ impl TestCase for Tile {
Rectangle::new(Point::from((-location.x, -location.y)), size.to_logical(1.)),
);
self.tile
.render(renderer, location, true, RenderTarget::Output)
.render(renderer, location, true, RenderTarget::Output, None)
.map(|elem| Box::new(elem) as _)
.collect()
}

View File

@@ -222,6 +222,10 @@ impl LayoutElement for TestWindow {
fn set_floating(&mut self, _floating: bool) {}
fn is_floating(&self) -> bool {
false
}
fn set_bounds(&self, _bounds: Size<i32, Logical>) {}
fn is_ignoring_opacity_window_rule(&self) -> bool {

View File

@@ -65,7 +65,9 @@ use super::{IpcOutputMap, RenderResult};
use crate::backend::OutputId;
use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::blur::EffectsFramebuffers;
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::render_data::RendererData;
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation};
@@ -800,6 +802,7 @@ impl Tty {
let gles_renderer = renderer.as_gles_renderer();
resources::init(gles_renderer);
shaders::init(gles_renderer);
RendererData::init(gles_renderer);
let config = self.config.borrow();
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {
@@ -1321,7 +1324,8 @@ impl Tty {
}
let render_node = device.render_node.unwrap_or(self.primary_render_node);
let renderer = self.gpu_manager.single_renderer(&render_node)?;
let mut renderer = self.gpu_manager.single_renderer(&render_node)?;
EffectsFramebuffers::init_for_output(output.clone(), &mut renderer);
let egl_context = renderer.as_ref().egl_context();
let render_formats = egl_context.dmabuf_render_formats();

View File

@@ -19,7 +19,9 @@ use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::blur::EffectsFramebuffers;
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::render_data::RendererData;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
@@ -122,6 +124,15 @@ impl Winit {
}
state.niri.output_resized(&winit.output);
if let Err(e) = EffectsFramebuffers::update_for_output(
winit.output.clone(),
winit.backend.renderer(),
) {
warn!("failed to update fx buffers on output resize: {e:?}");
} else {
EffectsFramebuffers::set_dirty(&winit.output);
}
}
WinitEvent::Input(event) => state.process_input_event(event),
WinitEvent::Focus(_) => (),
@@ -147,6 +158,8 @@ impl Winit {
resources::init(renderer);
shaders::init(renderer);
RendererData::init(renderer);
EffectsFramebuffers::init_for_output(self.output.clone(), renderer);
let config = self.config.borrow();
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {

View File

@@ -12,6 +12,7 @@ use smithay::wayland::shell::xdg::PopupSurface;
use crate::layer::{MappedLayer, ResolvedLayerRules};
use crate::niri::State;
use crate::render_helpers::blur::EffectsFramebuffers;
use crate::utils::{is_mapped, output_size, send_scale_transform};
impl WlrLayerShellHandler for State {
@@ -23,7 +24,7 @@ impl WlrLayerShellHandler for State {
&mut self,
surface: WlrLayerSurface,
wl_output: Option<WlOutput>,
_layer: Layer,
wlr_layer: Layer,
namespace: String,
) {
let output = if let Some(wl_output) = &wl_output {
@@ -37,6 +38,10 @@ impl WlrLayerShellHandler for State {
return;
};
if matches!(wlr_layer, Layer::Background | Layer::Bottom) {
EffectsFramebuffers::set_dirty(&output);
}
let wl_surface = surface.wl_surface().clone();
let is_new = self.niri.unmapped_layer_surfaces.insert(wl_surface);
assert!(is_new);
@@ -59,6 +64,10 @@ impl WlrLayerShellHandler for State {
.cloned();
layer.map(|layer| (o.clone(), map, layer))
}) {
if matches!(layer.layer(), Layer::Background | Layer::Bottom) {
EffectsFramebuffers::set_dirty(&output);
}
map.unmap_layer(&layer);
self.niri.mapped_layer_surfaces.remove(&layer);
Some(output)
@@ -113,6 +122,10 @@ impl State {
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
.unwrap();
if matches!(layer.layer(), Layer::Bottom | Layer::Background) {
EffectsFramebuffers::set_dirty(&output);
}
if is_mapped(surface) {
let was_unmapped = self.niri.unmapped_layer_surfaces.remove(surface);

View File

@@ -1,17 +1,19 @@
use niri_config::utils::MergeWith as _;
use niri_config::{Config, LayerRule};
use niri_config::{Blur, Config, LayerRule};
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
use super::ResolvedLayerRules;
use crate::animation::Clock;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::blur::element::BlurRenderElement;
use crate::render_helpers::blur::EffectsFramebufffersUserData;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
@@ -32,6 +34,13 @@ pub struct MappedLayer {
/// The shadow around the surface.
shadow: Shadow,
/// Configuration for this layer's blur.
blur_config: Blur,
/// Size (used for blur).
// TODO: move to standalone blur struct
size: Size<f64, Logical>,
/// The view size for the layer surface's output.
view_size: Size<f64, Logical>,
@@ -47,6 +56,7 @@ niri_render_elements! {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
Blur = BlurRenderElement,
}
}
@@ -64,6 +74,8 @@ impl MappedLayer {
shadow_config.on = false;
shadow_config.merge_with(&rules.shadow);
let blur_config = config.layout.blur.merged_with(&rules.blur);
Self {
surface,
rules,
@@ -72,6 +84,8 @@ impl MappedLayer {
scale,
shadow: Shadow::new(shadow_config),
clock,
blur_config,
size: Size::default(),
}
}
@@ -81,6 +95,8 @@ impl MappedLayer {
shadow_config.on = false;
shadow_config.merge_with(&self.rules.shadow);
self.shadow.update_config(shadow_config);
self.blur_config = config.layout.blur.merged_with(&self.rules.blur);
}
pub fn update_shaders(&mut self) {
@@ -98,6 +114,8 @@ impl MappedLayer {
.to_physical_precise_round(self.scale)
.to_logical(self.scale);
self.size = size;
self.block_out_buffer.resize(size);
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
@@ -161,6 +179,7 @@ impl MappedLayer {
renderer: &mut R,
location: Point<f64, Logical>,
target: RenderTarget,
fx_buffers: Option<EffectsFramebufffersUserData>,
) -> SplitElements<LayerSurfaceRenderElement<R>> {
let mut rv = SplitElements::default();
@@ -209,10 +228,35 @@ impl MappedLayer {
);
}
let blur_elem = (self.blur_config.on
&& matches!(self.surface.layer(), Layer::Top | Layer::Overlay))
.then(|| {
let fx_buffers = fx_buffers?;
Some(
BlurRenderElement::new_true(
fx_buffers,
Rectangle::new(location, self.size).to_i32_round(),
location.to_physical_precise_round(self.scale),
self.rules
.geometry_corner_radius
.unwrap_or_default()
.top_left,
self.scale,
self.blur_config,
)
.into(),
)
})
.flatten()
.into_iter();
let location = location.to_physical_precise_round(scale).to_logical(scale);
rv.normal
.extend(self.shadow.render(renderer, location).map(Into::into));
rv.normal.extend(blur_elem);
rv
}
}

View File

@@ -1,6 +1,6 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::utils::MergeWith as _;
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
use niri_config::{BlockOutFrom, BlurRule, CornerRadius, ShadowRule};
use smithay::desktop::LayerSurface;
pub mod mapped;
@@ -18,6 +18,9 @@ pub struct ResolvedLayerRules {
/// Shadow overrides.
pub shadow: ShadowRule,
/// Blur overrides
pub blur: BlurRule,
/// Corner radius to assume this layer surface has.
pub geometry_corner_radius: Option<CornerRadius>,
@@ -33,6 +36,13 @@ impl ResolvedLayerRules {
Self {
opacity: None,
block_out_from: None,
blur: BlurRule {
off: false,
on: false,
passes: None,
radius: None,
noise: None,
},
shadow: ShadowRule {
off: false,
on: false,
@@ -90,6 +100,7 @@ impl ResolvedLayerRules {
}
resolved.shadow.merge_with(&rule.shadow);
resolved.blur.merge_with(&rule.blur);
}
resolved

View File

@@ -17,6 +17,7 @@ use super::{
};
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::blur::EffectsFramebufffersUserData;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::utils::transaction::TransactionBlocker;
@@ -1140,6 +1141,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
view_rect: Rectangle<f64, Logical>,
target: RenderTarget,
focus_ring: bool,
fx_buffers: Option<EffectsFramebufffersUserData>,
) -> Vec<FloatingSpaceRenderElement<R>> {
let mut rv = Vec::new();
@@ -1159,7 +1161,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
let focus_ring = focus_ring && Some(tile.focused_window().id()) == active.as_ref();
rv.extend(
tile.render(renderer, tile_pos, focus_ring, target)
tile.render(renderer, tile_pos, focus_ring, target, fx_buffers.clone())
.map(Into::into),
);
}

View File

@@ -60,6 +60,7 @@ use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::scrolling::ScrollDirection;
use crate::niri_render_elements;
use crate::render_helpers::blur::EffectsFramebuffers;
use crate::render_helpers::offscreen::OffscreenData;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::snapshot::RenderSnapshot;
@@ -214,6 +215,7 @@ pub trait LayoutElement {
fn set_activated(&mut self, active: bool);
fn set_active_in_column(&mut self, active: bool);
fn set_floating(&mut self, floating: bool);
fn is_floating(&self) -> bool;
fn set_bounds(&self, bounds: Size<i32, Logical>);
fn is_ignoring_opacity_window_rule(&self) -> bool;
@@ -4845,9 +4847,10 @@ impl<W: LayoutElement> Layout<W> {
let scale = Scale::from(move_.output.current_scale().fractional_scale());
let zoom = self.overview_zoom();
let location = move_.tile_render_location(zoom);
let fx_buffers = EffectsFramebuffers::get_user_data(output);
let iter = move_
.tile
.render(renderer, location, true, target)
.render(renderer, location, true, target, fx_buffers)
.map(move |elem| {
RescaleRenderElement::from_element(
elem,

View File

@@ -19,6 +19,7 @@ use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::blur::EffectsFramebufffersUserData;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -3240,6 +3241,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
renderer: &mut R,
target: RenderTarget,
focus_ring: bool,
fx_buffers: Option<EffectsFramebufffersUserData>,
) -> Vec<ScrollingSpaceRenderElement<R>> {
let mut rv = vec![];
@@ -3277,7 +3279,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
first = false;
rv.extend(
tile.render(renderer, tile_pos, focus_ring, target)
tile.render(renderer, tile_pos, focus_ring, target, fx_buffers.clone())
.map(Into::into),
);
}

View File

@@ -240,6 +240,10 @@ impl LayoutElement for TestWindow {
fn set_floating(&mut self, _floating: bool) {}
fn is_floating(&self) -> bool {
false
}
fn sizing_mode(&self) -> SizingMode {
self.0.sizing_mode.get()
}

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use std::sync::atomic::Ordering;
use niri_config::utils::MergeWith as _;
use niri_config::{Color, CornerRadius, GradientInterpolation};
use niri_config::{Blur, Color, CornerRadius, GradientInterpolation};
use niri_ipc::WindowLayout;
use portable_atomic::AtomicU8;
use smithay::backend::renderer::element::{Element, Kind};
@@ -21,6 +21,8 @@ use crate::animation::{Animation, Clock};
use crate::layout::tab_indicator::{TabIndicator, TabIndicatorRenderElement, TabInfo};
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::blur::element::BlurRenderElement;
use crate::render_helpers::blur::EffectsFramebufffersUserData;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
@@ -403,6 +405,10 @@ pub struct Tile<W: LayoutElement> {
/// window still having to adjust, which causes a jerking visual without this compensation.
window_size_override: WindowSizeOverride,
/// This tile's blur settings.
// TODO: create a proper struct for this, like e.g. Shadow
blur_config: Blur,
/// Clock for driving animations.
pub(super) clock: Clock,
@@ -419,6 +425,7 @@ niri_render_elements! {
Resize = ResizeRenderElement,
Border = BorderRenderElement,
Shadow = ShadowRenderElement,
Blur = BlurRenderElement,
ClippedSurface = ClippedSurfaceRenderElement<R>,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
@@ -478,12 +485,14 @@ impl<W: LayoutElement> Tile<W> {
let shadow_config = options.layout.shadow.merged_with(&rules.shadow);
let sizing_mode = window.sizing_mode();
let tab_indicator_config = options.layout.tab_indicator;
let blur_config = options.layout.blur.merged_with(&rules.blur);
Self {
window: WindowInner::Single(Some(window)),
border: FocusRing::new(border_config.into()),
focus_ring: FocusRing::new(focus_ring_config),
shadow: Shadow::new(shadow_config),
blur_config,
sizing_mode,
fullscreen_backdrop: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
restore_to_floating: false,
@@ -547,6 +556,8 @@ impl<W: LayoutElement> Tile<W> {
self.tab_indicator
.update_config(self.options.layout.tab_indicator);
self.blur_config = self.options.layout.blur.merged_with(&rules.blur);
}
pub fn update_shaders(&mut self) {
@@ -1524,6 +1535,7 @@ impl<W: LayoutElement> Tile<W> {
location: Point<f64, Logical>,
focus_ring: bool,
target: RenderTarget,
fx_buffers: Option<EffectsFramebufffersUserData>,
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
let _span = tracy_client::span!("Tile::render_inner");
@@ -1841,9 +1853,47 @@ impl<W: LayoutElement> Tile<W> {
.then(|| self.focus_ring.render(renderer, location).map(Into::into));
let rv = rv.chain(elem.into_iter().flatten());
let blur_elem = self
.blur_config
.on
.then(|| {
let fx_buffers = fx_buffers?;
if self.focused_window().is_floating() {
Some(
BlurRenderElement::new_true(
fx_buffers,
area.to_i32_round(),
window_render_loc.to_physical(self.scale).to_i32_round(),
radius.top_left,
self.scale,
self.blur_config,
)
.into(),
)
} else {
Some(
BlurRenderElement::new_optimized(
renderer,
&fx_buffers.borrow_mut(),
area.to_i32_round(),
window_render_loc.to_physical(self.scale).to_i32_round(),
radius.top_left,
self.scale,
self.blur_config,
)
.into(),
)
}
})
.flatten()
.into_iter();
let elem = (expanded_progress < 1.)
.then(|| self.shadow.render(renderer, location).map(Into::into));
rv.chain(elem.into_iter().flatten())
let rv = rv.chain(elem.into_iter().flatten());
rv.chain(blur_elem)
}
pub fn render<'a, R: NiriRenderer + 'a>(
@@ -1852,6 +1902,7 @@ impl<W: LayoutElement> Tile<W> {
location: Point<f64, Logical>,
focus_ring: bool,
target: RenderTarget,
fx_buffers: Option<EffectsFramebufffersUserData>,
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
let _span = tracy_client::span!("Tile::render");
@@ -1870,7 +1921,13 @@ impl<W: LayoutElement> Tile<W> {
if let Some(open) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
let elements = self.render_inner(
renderer,
Point::from((0., 0.)),
focus_ring,
target,
fx_buffers.clone(),
);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match open.render(
renderer,
@@ -1890,7 +1947,13 @@ impl<W: LayoutElement> Tile<W> {
}
} else if let Some(alpha) = &self.alpha_animation {
let renderer = renderer.as_gles_renderer();
let elements = self.render_inner(renderer, Point::from((0., 0.)), focus_ring, target);
let elements = self.render_inner(
renderer,
Point::from((0., 0.)),
focus_ring,
target,
fx_buffers.clone(),
);
let elements = elements.collect::<Vec<TileRenderElement<_>>>();
match alpha.offscreen.render(renderer, scale, &elements) {
Ok((elem, _sync, data)) => {
@@ -1907,7 +1970,8 @@ impl<W: LayoutElement> Tile<W> {
}
if open_anim_elem.is_none() && alpha_anim_elem.is_none() {
window_elems = Some(self.render_inner(renderer, location, focus_ring, target));
window_elems =
Some(self.render_inner(renderer, location, focus_ring, target, fx_buffers));
}
open_anim_elem
@@ -1927,7 +1991,13 @@ impl<W: LayoutElement> Tile<W> {
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let contents = self.render(renderer, Point::from((0., 0.)), false, RenderTarget::Output);
let contents = self.render(
renderer,
Point::from((0., 0.)),
false,
RenderTarget::Output,
None,
);
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let blocked_out_contents = self.render(
@@ -1935,6 +2005,7 @@ impl<W: LayoutElement> Tile<W> {
Point::from((0., 0.)),
false,
RenderTarget::Screencast,
None,
);
RenderSnapshot {

View File

@@ -30,6 +30,7 @@ use super::{
};
use crate::animation::Clock;
use crate::niri_render_elements;
use crate::render_helpers::blur::EffectsFramebuffers;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
@@ -1727,18 +1728,29 @@ impl<W: LayoutElement> Workspace<W> {
impl Iterator<Item = WorkspaceRenderElement<R>>,
impl Iterator<Item = WorkspaceRenderElement<R>>,
) {
let fx_buffers = self
.current_output()
.and_then(EffectsFramebuffers::get_user_data);
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
let scrolling = self
.scrolling
.render_elements(renderer, target, scrolling_focus_ring);
let scrolling = self.scrolling.render_elements(
renderer,
target,
scrolling_focus_ring,
fx_buffers.clone(),
);
let scrolling = scrolling.into_iter().map(WorkspaceRenderElement::from);
let floating_focus_ring = focus_ring && self.floating_is_active();
let floating = self.is_floating_visible().then(|| {
let view_rect = Rectangle::from_size(self.view_size);
let floating =
self.floating
.render_elements(renderer, view_rect, target, floating_focus_ring);
let floating = self.floating.render_elements(
renderer,
view_rect,
target,
floating_focus_ring,
fx_buffers.clone(),
);
floating.into_iter().map(WorkspaceRenderElement::from)
});
let floating = floating.into_iter().flatten();

View File

@@ -153,6 +153,7 @@ use crate::protocols::virtual_pointer::VirtualPointerManagerState;
use crate::pw_utils::{Cast, PipeWire};
#[cfg(feature = "xdp-gnome-screencast")]
use crate::pw_utils::{CastSizeChange, PwToNiri};
use crate::render_helpers::blur::{EffectsFramebuffers, EffectsFramebufffersUserData};
use crate::render_helpers::debug::draw_opaque_regions;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
@@ -1676,6 +1677,11 @@ impl State {
*CHILD_DISPLAY.write().unwrap() = display_name;
}
self.niri
.global_space
.outputs()
.for_each(EffectsFramebuffers::set_dirty);
// Can't really update xdg-decoration settings since we have to hide the globals for CSD
// due to the SDL2 bug... I don't imagine clients are prepared for the xdg-decoration
// global suddenly appearing? Either way, right now it's live-reloaded in a sense that new
@@ -4449,9 +4455,18 @@ impl Niri {
// Get layer-shell elements.
let layer_map = layer_map_for_output(output);
let fx_buffers = EffectsFramebuffers::get_user_data(output);
let mut extend_from_layer =
|elements: &mut SplitElements<LayerSurfaceRenderElement<R>>, layer, for_backdrop| {
self.render_layer(renderer, target, &layer_map, layer, elements, for_backdrop);
self.render_layer(
renderer,
target,
&layer_map,
layer,
elements,
for_backdrop,
fx_buffers.clone(),
);
};
// The overlay layer elements go next.
@@ -4566,6 +4581,20 @@ impl Niri {
draw_opaque_regions(&mut elements, output_scale);
}
if let Some(mut fx_buffers) = EffectsFramebuffers::get(output) {
let blur_config = self.config.borrow().layout.blur;
if blur_config.on && blur_config.passes > 0 {
if let Err(e) = fx_buffers.update_optimized_blur_buffer(
renderer.as_gles_renderer(),
layer_map,
output_scale,
blur_config,
) {
error!("failed to update optimized blur buffer: {e:?}");
};
}
}
elements
}
@@ -4578,6 +4607,7 @@ impl Niri {
layer: Layer,
elements: &mut SplitElements<LayerSurfaceRenderElement<R>>,
for_backdrop: bool,
fx_buffers: Option<EffectsFramebufffersUserData>,
) {
// LayerMap returns layers in reverse stacking order.
let iter = layer_map.layers_on(layer).rev().filter_map(|surface| {
@@ -4591,7 +4621,7 @@ impl Niri {
Some((mapped, geo))
});
for (mapped, geo) in iter {
elements.extend(mapped.render(renderer, geo.loc.to_f64(), target));
elements.extend(mapped.render(renderer, geo.loc.to_f64(), target, fx_buffers.clone()));
}
}

844
src/render_helpers/blur.rs Normal file
View File

@@ -0,0 +1,844 @@
// Ported from https://github.com/nferhat/fht-compositor/blob/main/src/renderer/blur/mod.rs
pub mod element;
pub mod optimized_blur_texture_element;
pub(super) mod shader;
use anyhow::Context;
use std::cell::{RefCell, RefMut};
use std::rc::Rc;
use glam::{Mat3, Vec2};
use niri_config::Blur;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::AsRenderElements;
use smithay::backend::renderer::gles::format::fourcc_to_gl_formats;
use smithay::backend::renderer::gles::{ffi, Capability, GlesError, GlesRenderer, GlesTexture};
use smithay::backend::renderer::{Bind, Blit, Frame, Offscreen, Renderer, Texture, TextureFilter};
use smithay::desktop::LayerMap;
use smithay::output::Output;
use smithay::reexports::gbm::Format;
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::shell::wlr_layer::Layer;
use crate::render_helpers::renderer::NiriRenderer;
use shader::BlurShaders;
use super::render_data::RendererData;
use super::render_elements;
use super::shaders::Shaders;
use std::sync::MutexGuard;
use std::time::{Duration, Instant};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum CurrentBuffer {
/// We are currently sampling from normal buffer, and rendering in the swapped/alternative.
#[default]
Normal,
/// We are currently sampling from swapped buffer, and rendering in the normal.
Swapped,
}
impl CurrentBuffer {
pub fn swap(&mut self) {
*self = match self {
// sampled from normal, render to swapped
Self::Normal => Self::Swapped,
// sampled fro swapped, render to normal next
Self::Swapped => Self::Normal,
}
}
}
/// Effect framebuffers associated with each output.
#[derive(Debug)]
pub struct EffectsFramebuffers {
/// Contains the main buffer blurred contents
pub optimized_blur: GlesTexture,
/// Whether the optimizer blur buffer is dirty
pub optimized_blur_rerender_at: Option<Instant>,
// /// Contains the original pixels before blurring to draw with in case of artifacts.
// blur_saved_pixels: GlesTexture,
// The blur algorithms (dual-kawase) swaps between these two whenever scaling the image
effects: GlesTexture,
effects_swapped: GlesTexture,
/// The buffer we are currently rendering/sampling from.
///
/// In order todo the up/downscaling, we render into different buffers. On each pass, we render
/// into a different buffer with downscaling/upscaling (depending on which pass we are at)
///
/// One exception is that if we are on the first pass, we are on [`CurrentBuffer::Initial`], we
/// are sampling from [`Self::blit_buffer`] from initial screen contents.
current_buffer: CurrentBuffer,
/// Size of the output that this object runs on.
output_size: Size<i32, Physical>,
}
pub type EffectsFramebufffersUserData = Rc<RefCell<EffectsFramebuffers>>;
fn get_rerender_at() -> Option<Instant> {
Some(Instant::now() + Duration::from_millis(150))
}
impl EffectsFramebuffers {
/// Get the associated [`EffectsFramebuffers`] with this output.
pub fn get<'a>(output: &'a Output) -> Option<RefMut<'a, Self>> {
output
.user_data()
.get::<EffectsFramebufffersUserData>()
.map(|e| e.borrow_mut())
}
pub fn get_user_data(output: &Output) -> Option<EffectsFramebufffersUserData> {
output.user_data().get().cloned()
}
pub fn set_dirty(output: &Output) {
let Some(mut fx_buffers) = Self::get(output) else {
warn!("attempting to set fx buffer to dirty on output that has none: {output:?}");
return;
};
if fx_buffers.optimized_blur_rerender_at.is_none() {
fx_buffers.optimized_blur_rerender_at = get_rerender_at();
}
}
/// Initialize the [`EffectsFramebuffers`] for an [`Output`].
///
/// The framebuffers handles live inside the Output's user data, use [`Self::get`] to access
/// them.
pub fn init_for_output(output: Output, renderer: &mut impl NiriRenderer) {
let renderer = renderer.as_gles_renderer();
let output_size = output.current_mode().unwrap().size;
fn create_buffer(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Result<GlesTexture, GlesError> {
renderer.create_buffer(
Format::Abgr8888,
size.to_logical(1).to_buffer(1, Transform::Normal),
)
}
let this = EffectsFramebuffers {
optimized_blur: create_buffer(renderer, output_size).unwrap(),
optimized_blur_rerender_at: get_rerender_at(),
effects: create_buffer(renderer, output_size).unwrap(),
effects_swapped: create_buffer(renderer, output_size).unwrap(),
current_buffer: CurrentBuffer::Normal,
output_size: output.current_mode().unwrap().size,
};
let user_data = output.user_data();
assert!(
user_data.insert_if_missing(|| Rc::new(RefCell::new(this))),
"EffectsFrambuffers::init_for_output should only be called once!"
);
}
/// Update the [`EffectsFramebuffers`] for an [`Output`].
///
/// You should call this if the output's scale/size changes
pub fn update_for_output(
output: Output,
renderer: &mut impl NiriRenderer,
) -> Result<(), GlesError> {
let renderer = renderer.as_gles_renderer();
let Some(mut fx_buffers) = Self::get(&output) else {
warn!("attempting to update fx buffer on output that has none: {output:?}");
return Ok(()); // TODO: error?
};
let output_size = output.current_mode().unwrap().size;
fn create_buffer(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
) -> Result<GlesTexture, GlesError> {
renderer.create_buffer(
Format::Abgr8888,
size.to_logical(1).to_buffer(1, Transform::Normal),
)
}
*fx_buffers = EffectsFramebuffers {
optimized_blur: create_buffer(renderer, output_size)?,
optimized_blur_rerender_at: get_rerender_at(),
effects: create_buffer(renderer, output_size)?,
effects_swapped: create_buffer(renderer, output_size)?,
current_buffer: CurrentBuffer::Normal,
output_size: output.current_mode().unwrap().size,
};
Ok(())
}
/// Render the optimized blur buffer again
pub fn update_optimized_blur_buffer(
&mut self,
renderer: &mut GlesRenderer,
layer_map: MutexGuard<'_, LayerMap>,
scale: Scale<f64>,
config: Blur,
) -> anyhow::Result<()> {
if self.optimized_blur_rerender_at.is_none()
|| matches!(self.optimized_blur_rerender_at, Some(t) if t > Instant::now())
{
return Ok(());
}
self.optimized_blur_rerender_at = None;
// first render layer shell elements
// NOTE: We use Blur::DISABLED since we should not include blur with Background/Bottom
// layer shells
let mut elements = vec![];
for layer in layer_map
.layers_on(Layer::Background)
.chain(layer_map.layers_on(Layer::Bottom))
.rev()
{
let layer_geo = layer_map.layer_geometry(layer).unwrap();
let location = layer_geo.loc.to_physical_precise_round(scale);
elements.extend(
layer.render_elements::<WaylandSurfaceRenderElement<_>>(
renderer, location, scale, 1.0,
),
);
}
let mut fb = renderer.bind(&mut self.effects).unwrap();
let _ = render_elements(
renderer,
&mut fb,
self.output_size,
scale,
Transform::Normal,
elements.iter(),
)
.expect("failed to render for optimized blur buffer");
drop(fb);
self.current_buffer = CurrentBuffer::Normal;
let shaders = Shaders::get(renderer).blur.clone();
// NOTE: If we only do one pass its kinda ugly, there must be at least
// n=2 passes in order to have good sampling
let half_pixel = [
0.5 / (self.output_size.w as f32 / 2.0),
0.5 / (self.output_size.h as f32 / 2.0),
];
for _ in 0..config.passes {
let (sample_buffer, render_buffer) = self.buffers();
render_blur_pass_with_frame(
renderer,
sample_buffer,
render_buffer,
&shaders.down,
half_pixel,
config,
)?;
self.current_buffer.swap();
}
let half_pixel = [
0.5 / (self.output_size.w as f32 * 2.0),
0.5 / (self.output_size.h as f32 * 2.0),
];
// FIXME: Why we need inclusive here but down is exclusive?
for _ in 0..config.passes {
let (sample_buffer, render_buffer) = self.buffers();
render_blur_pass_with_frame(
renderer,
sample_buffer,
render_buffer,
&shaders.up,
half_pixel,
config,
)?;
self.current_buffer.swap();
}
// Now blit from the last render buffer into optimized_blur
// We are already bound so its just a blit
let tex_fb = renderer.bind(&mut self.effects).unwrap();
let mut optimized_blur_fb = renderer.bind(&mut self.optimized_blur).unwrap();
let _ = renderer.blit(
&tex_fb,
&mut optimized_blur_fb,
Rectangle::from_size(self.output_size),
Rectangle::from_size(self.output_size),
TextureFilter::Linear,
)?;
Ok(())
}
/// Get the sample and render buffers.
pub fn buffers(&mut self) -> (&GlesTexture, &mut GlesTexture) {
match self.current_buffer {
CurrentBuffer::Normal => (&self.effects, &mut self.effects_swapped),
CurrentBuffer::Swapped => (&self.effects_swapped, &mut self.effects),
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) unsafe fn get_main_buffer_blur(
gl: &ffi::Gles2,
fx_buffers: &mut EffectsFramebuffers,
shaders: &BlurShaders,
blur_config: Blur,
projection_matrix: Mat3,
scale: i32,
vbos: &[u32; 2],
debug: bool,
supports_instancing: bool,
// dst is the region that we want blur on
dst: Rectangle<i32, Physical>,
is_tty: bool,
) -> Result<GlesTexture, GlesError> {
let tex_size = fx_buffers
.effects
.size()
.to_logical(1, Transform::Normal)
.to_physical(scale);
let dst_expanded = {
let mut dst = dst;
let size =
(2f32.powi(blur_config.passes as i32 + 1) * blur_config.radius.0 as f32).ceil() as i32;
dst.loc -= Point::from((size, size)).upscale(8);
dst.size += Size::from((size, size)).upscale(16);
dst
};
let mut prev_fbo = 0;
gl.GetIntegerv(ffi::FRAMEBUFFER_BINDING, &mut prev_fbo as *mut _);
let (sample_buffer, _) = fx_buffers.buffers();
// First get a fbo for the texture we are about to read into
let mut sample_fbo = 0u32;
{
gl.GenFramebuffers(1, &mut sample_fbo as *mut _);
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, sample_fbo);
gl.FramebufferTexture2D(
ffi::FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
sample_buffer.tex_id(),
0,
);
gl.Clear(ffi::COLOR_BUFFER_BIT);
let status = gl.CheckFramebufferStatus(ffi::FRAMEBUFFER);
if status != ffi::FRAMEBUFFER_COMPLETE {
gl.DeleteFramebuffers(1, &mut sample_fbo as *mut _);
return Err(GlesError::FramebufferBindingError);
}
}
{
// NOTE: We are assured that the size of the effects texture is the same
// as the bound fbo size, so blitting uses dst immediately
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, sample_fbo);
if is_tty {
let src_x0 = dst_expanded.loc.x;
let src_y0 = dst_expanded.loc.y;
let src_x1 = dst_expanded.loc.x + dst_expanded.size.w;
let src_y1 = dst_expanded.loc.y + dst_expanded.size.h;
let dst_x0 = src_x0;
let dst_y0 = src_y0;
let dst_x1 = src_x1;
let dst_y1 = src_y1;
gl.BlitFramebuffer(
src_x0,
src_y0,
src_x1,
src_y1,
dst_x0,
dst_y0,
dst_x1,
dst_y1,
ffi::COLOR_BUFFER_BIT,
ffi::LINEAR,
);
} else {
let fb_height = tex_size.h;
let dst_y0 = dst_expanded.loc.y + dst_expanded.size.h;
let dst_y1 = dst_expanded.loc.y;
let src_x0 = dst_expanded.loc.x;
let src_x1 = dst_expanded.loc.x + dst_expanded.size.w;
let src_y0 = fb_height - dst_y0;
let src_y1 = fb_height - dst_y1;
gl.BlitFramebuffer(
src_x0,
src_y0,
src_x1,
src_y1,
src_x0,
dst_y0,
src_x1,
dst_y1,
ffi::COLOR_BUFFER_BIT,
ffi::LINEAR,
);
}
if gl.GetError() == ffi::INVALID_OPERATION {
error!("TrueBlur needs GLES3.0 for blitting");
return Err(GlesError::BlitError);
}
}
{
let passes = blur_config.passes;
let half_pixel = [
0.5 / (tex_size.w as f32 / 2.0),
0.5 / (tex_size.h as f32 / 2.0),
];
for i in 0..passes {
let (sample_buffer, render_buffer) = fx_buffers.buffers();
let damage = dst_expanded.downscale(1 << (i + 1));
render_blur_pass_with_gl(
gl,
vbos,
debug,
supports_instancing,
projection_matrix,
sample_buffer,
render_buffer,
scale,
&shaders.down,
half_pixel,
blur_config,
damage,
)?;
fx_buffers.current_buffer.swap();
}
let half_pixel = [
0.5 / (tex_size.w as f32 * 2.0),
0.5 / (tex_size.h as f32 * 2.0),
];
for i in 0..passes {
let (sample_buffer, render_buffer) = fx_buffers.buffers();
let damage = dst_expanded.downscale(1 << (passes - 1 - i));
render_blur_pass_with_gl(
gl,
vbos,
debug,
supports_instancing,
projection_matrix,
sample_buffer,
render_buffer,
scale,
&shaders.up,
half_pixel,
blur_config,
damage,
)?;
fx_buffers.current_buffer.swap();
}
}
// Cleanup
{
gl.DeleteFramebuffers(1, &mut sample_fbo as *mut _);
gl.BindFramebuffer(ffi::FRAMEBUFFER, prev_fbo as u32);
}
Ok(fx_buffers.effects.clone())
}
// Renders a blur pass using a GlesFrame with syncing and fencing provided by smithay. Used for
// updating optimized blur buffer since we are not yet rendering.
fn render_blur_pass_with_frame(
renderer: &mut GlesRenderer,
sample_buffer: &GlesTexture,
render_buffer: &mut GlesTexture,
blur_program: &shader::BlurShader,
half_pixel: [f32; 2],
config: Blur,
) -> anyhow::Result<()> {
// We use a texture render element with a custom GlesTexProgram in order todo the blurring
// At least this is what swayfx/scenefx do, but they just use gl calls directly.
let size = sample_buffer.size().to_logical(1, Transform::Flipped);
let vbos = RendererData::get(renderer).vbos;
let is_shared = renderer.egl_context().is_shared();
let mut fb = renderer.bind(render_buffer)?;
// Using GlesFrame since I want to use a custom program
let mut frame = renderer
.render(&mut fb, size.to_physical(1), Transform::Normal)
.context("failed to create frame")?;
let supports_instaning = frame.capabilities().contains(&Capability::Instancing);
let debug = !frame.debug_flags().is_empty();
let projection = Mat3::from_cols_array(frame.projection());
let tex_size = sample_buffer.size();
let src = Rectangle::from_size(sample_buffer.size()).to_f64();
let dst = Rectangle::from_size(size).to_physical(1);
frame.with_context(|gl| unsafe {
// We are doing basically what Frame::render_texture_from_to does, but our own shader struct
// instead. This allows me to get into the gl plumbing.
// NOTE: We are rendering at the origin of the texture, no need to translate
let mut mat = Mat3::IDENTITY;
let src_size = sample_buffer.size().to_f64();
if tex_size.is_empty() || src_size.is_empty() {
return Ok(());
}
let mut tex_mat = build_texture_mat(src, dst, tex_size, Transform::Normal);
if sample_buffer.is_y_inverted() {
tex_mat *= Mat3::from_cols_array(&[1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0]);
}
// NOTE: We know that this texture is always opaque so skip on some logic checks and
// directly render. The following code is from GlesRenderer::render_texture
gl.Disable(ffi::BLEND);
// Since we are just rendering onto the offsreen buffer, the vertices to draw are only 4
let damage = [
dst.loc.x as f32,
dst.loc.y as f32,
dst.size.w as f32,
dst.size.h as f32,
];
let mut vertices = Vec::with_capacity(4);
let damage_len = if supports_instaning {
vertices.extend(damage);
vertices.len() / 4
} else {
for _ in 0..6 {
// Add the 4 f32s per damage rectangle for each of the 6 vertices.
vertices.extend_from_slice(&damage);
}
1
};
mat *= projection;
// SAFETY: internal texture should always have a format
// We also use Abgr8888 which is known and confirmed
let (internal_format, _, _) =
fourcc_to_gl_formats(sample_buffer.format().unwrap()).unwrap();
let variant = blur_program.variant_for_format(Some(internal_format), false);
let program = if debug {
&variant.debug
} else {
&variant.normal
};
gl.ActiveTexture(ffi::TEXTURE0);
gl.BindTexture(ffi::TEXTURE_2D, sample_buffer.tex_id());
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.UseProgram(program.program);
gl.Uniform1i(program.uniform_tex, 0);
gl.UniformMatrix3fv(
program.uniform_matrix,
1,
ffi::FALSE,
mat.as_ref() as *const f32,
);
gl.UniformMatrix3fv(
program.uniform_tex_matrix,
1,
ffi::FALSE,
tex_mat.as_ref() as *const f32,
);
gl.Uniform1f(program.uniform_alpha, 1.0);
gl.Uniform1f(program.uniform_radius, config.radius.0 as f32);
gl.Uniform2f(program.uniform_half_pixel, half_pixel[0], half_pixel[1]);
gl.EnableVertexAttribArray(program.attrib_vert as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[0]);
gl.VertexAttribPointer(
program.attrib_vert as u32,
2,
ffi::FLOAT,
ffi::FALSE,
0,
std::ptr::null(),
);
// vert_position
gl.EnableVertexAttribArray(program.attrib_vert_position as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
gl.VertexAttribPointer(
program.attrib_vert_position as u32,
4,
ffi::FLOAT,
ffi::FALSE,
0,
vertices.as_ptr() as *const _,
);
if supports_instaning {
gl.VertexAttribDivisor(program.attrib_vert as u32, 0);
gl.VertexAttribDivisor(program.attrib_vert_position as u32, 1);
gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len as i32);
} else {
let count = damage_len * 6;
gl.DrawArrays(ffi::TRIANGLES, 0, count as i32);
}
gl.BindTexture(ffi::TEXTURE_2D, 0);
gl.DisableVertexAttribArray(program.attrib_vert as u32);
gl.DisableVertexAttribArray(program.attrib_vert_position as u32);
gl.Enable(ffi::BLEND);
gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA);
// FIXME: Check for Fencing support
if is_shared {
gl.Finish();
}
Result::<_, GlesError>::Ok(())
})??;
let _sync_point = frame.finish()?;
Ok(())
}
// Renders a blur pass using gl code bypassing smithay's Frame mechanisms
//
// When rendering blur in real-time (for windows, for example) there should not be a wait for
// fencing/finishing since this will be done when sending the fb to the output. Using a Frame
// forces us to do that.
#[allow(clippy::too_many_arguments)]
unsafe fn render_blur_pass_with_gl(
gl: &ffi::Gles2,
vbos: &[u32; 2],
debug: bool,
supports_instancing: bool,
projection_matrix: Mat3,
// The buffers used for blurring
sample_buffer: &GlesTexture,
render_buffer: &mut GlesTexture,
scale: i32,
// The current blur program + config
blur_program: &shader::BlurShader,
half_pixel: [f32; 2],
config: Blur,
// dst is the region that should have blur
// it gets up/downscaled with passes
_damage: Rectangle<i32, Physical>,
) -> Result<(), GlesError> {
let tex_size = sample_buffer.size();
let src = Rectangle::from_size(tex_size.to_f64());
let dest = src
.to_logical(1.0, Transform::Normal, &src.size)
.to_physical(scale as f64)
.to_i32_round();
let damage = dest;
// FIXME: Should we call gl.Finish() when done rendering this pass? If yes, should we check
// if the gl context is shared or not? What about fencing, we don't have access to that
// PERF: Instead of taking the whole src/dst as damage, adapt the code to run with only the
// damaged window? This would cause us to make a custom WaylandSurfaceRenderElement to blur out
// stuff. Complicated.
// First bind to our render buffer
let mut render_buffer_fbo = 0;
{
gl.GenFramebuffers(1, &mut render_buffer_fbo as *mut _);
gl.BindFramebuffer(ffi::FRAMEBUFFER, render_buffer_fbo);
gl.FramebufferTexture2D(
ffi::FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
render_buffer.tex_id(),
0,
);
let status = gl.CheckFramebufferStatus(ffi::FRAMEBUFFER);
if status != ffi::FRAMEBUFFER_COMPLETE {
return Err(GlesError::FramebufferBindingError);
}
}
{
let mat = projection_matrix;
// NOTE: We are assured that tex_size != 0, and src.size != too (by damage tracker)
let tex_mat = build_texture_mat(src, dest, tex_size, Transform::Normal);
gl.Disable(ffi::BLEND);
// FIXME: Use actual damage for this? Would require making a custom window render element
// that includes blur and whatnot to get the damage for the window only
let damage = [
damage.loc.x as f32,
damage.loc.y as f32,
damage.size.w as f32,
damage.size.h as f32,
];
let mut vertices = Vec::with_capacity(4);
let damage_len = if supports_instancing {
vertices.extend(damage);
vertices.len() / 4
} else {
for _ in 0..6 {
// Add the 4 f32s per damage rectangle for each of the 6 vertices.
vertices.extend_from_slice(&damage);
}
1
};
// SAFETY: internal texture should always have a format
// We also use Abgr8888 which is known and confirmed
let (internal_format, _, _) =
fourcc_to_gl_formats(sample_buffer.format().unwrap()).unwrap();
let variant = blur_program.variant_for_format(Some(internal_format), false);
let program = if debug {
&variant.debug
} else {
&variant.normal
};
gl.ActiveTexture(ffi::TEXTURE0);
gl.BindTexture(ffi::TEXTURE_2D, sample_buffer.tex_id());
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.UseProgram(program.program);
gl.Uniform1i(program.uniform_tex, 0);
gl.UniformMatrix3fv(
program.uniform_matrix,
1,
ffi::FALSE,
mat.as_ref() as *const f32,
);
gl.UniformMatrix3fv(
program.uniform_tex_matrix,
1,
ffi::FALSE,
tex_mat.as_ref() as *const f32,
);
gl.Uniform1f(program.uniform_alpha, 1.0);
gl.Uniform1f(program.uniform_radius, config.radius.0 as f32);
gl.Uniform2f(program.uniform_half_pixel, half_pixel[0], half_pixel[1]);
gl.EnableVertexAttribArray(program.attrib_vert as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[0]);
gl.VertexAttribPointer(
program.attrib_vert as u32,
2,
ffi::FLOAT,
ffi::FALSE,
0,
std::ptr::null(),
);
// vert_position
gl.EnableVertexAttribArray(program.attrib_vert_position as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
gl.VertexAttribPointer(
program.attrib_vert_position as u32,
4,
ffi::FLOAT,
ffi::FALSE,
0,
vertices.as_ptr() as *const _,
);
if supports_instancing {
gl.VertexAttribDivisor(program.attrib_vert as u32, 0);
gl.VertexAttribDivisor(program.attrib_vert_position as u32, 1);
gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len as i32);
} else {
let count = damage_len * 6;
gl.DrawArrays(ffi::TRIANGLES, 0, count as i32);
}
gl.BindTexture(ffi::TEXTURE_2D, 0);
gl.DisableVertexAttribArray(program.attrib_vert as u32);
gl.DisableVertexAttribArray(program.attrib_vert_position as u32);
}
// Clean up
{
gl.Enable(ffi::BLEND);
gl.DeleteFramebuffers(1, &render_buffer_fbo as *const _);
gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA);
gl.BindFramebuffer(ffi::FRAMEBUFFER, 0);
}
Ok(())
}
// Copied from smithay, adapted to use glam structs
fn build_texture_mat(
src: Rectangle<f64, Buffer>,
dest: Rectangle<i32, Physical>,
texture: Size<i32, Buffer>,
transform: Transform,
) -> Mat3 {
let dst_src_size = transform.transform_size(src.size);
let scale = dst_src_size.to_f64() / dest.size.to_f64();
let mut tex_mat = Mat3::IDENTITY;
// first bring the damage into src scale
tex_mat = Mat3::from_scale(Vec2::new(scale.x as f32, scale.y as f32)) * tex_mat;
// then compensate for the texture transform
let transform_mat = Mat3::from_cols_array(transform.matrix().as_ref());
let translation = match transform {
Transform::Normal => Mat3::IDENTITY,
Transform::_90 => Mat3::from_translation(Vec2::new(0f32, dst_src_size.w as f32)),
Transform::_180 => {
Mat3::from_translation(Vec2::new(dst_src_size.w as f32, dst_src_size.h as f32))
}
Transform::_270 => Mat3::from_translation(Vec2::new(dst_src_size.h as f32, 0f32)),
Transform::Flipped => Mat3::from_translation(Vec2::new(dst_src_size.w as f32, 0f32)),
Transform::Flipped90 => Mat3::IDENTITY,
Transform::Flipped180 => Mat3::from_translation(Vec2::new(0f32, dst_src_size.h as f32)),
Transform::Flipped270 => {
Mat3::from_translation(Vec2::new(dst_src_size.h as f32, dst_src_size.w as f32))
}
};
tex_mat = transform_mat * tex_mat;
tex_mat = translation * tex_mat;
// now we can add the src crop loc, the size already done implicit by the src size
tex_mat = Mat3::from_translation(Vec2::new(src.loc.x as f32, src.loc.y as f32)) * tex_mat;
// at last we have to normalize the values for UV space
tex_mat = Mat3::from_scale(Vec2::new(
(1.0f64 / texture.w as f64) as f32,
(1.0f64 / texture.h as f64) as f32,
)) * tex_mat;
tex_mat
}

View File

@@ -0,0 +1,462 @@
// Originally ported from https://github.com/nferhat/fht-compositor/blob/main/src/renderer/blur/element.rs
use niri_config::Blur;
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::backend::renderer::Renderer;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::blur::EffectsFramebufffersUserData;
use crate::render_helpers::render_data::RendererData;
use crate::render_helpers::renderer::{AsGlesFrame, NiriRenderer};
use crate::render_helpers::shaders::Shaders;
use super::optimized_blur_texture_element::OptimizedBlurTextureElement;
use super::{CurrentBuffer, EffectsFramebuffers};
#[derive(Debug)]
pub enum BlurRenderElement {
/// Use optimized blur, aka X-ray blur.
///
/// This technique relies on [`EffectsFramebuffers::optimized_blur`] to be populated. It will
/// render this texture no matter what is below the blur render element.
Optimized {
tex: OptimizedBlurTextureElement,
corner_radius: f32,
noise: f32,
scale: f64,
output_size: Size<i32, Physical>,
},
/// Use true blur.
///
/// When using this technique, the compositor will blur the current framebuffer ccontents that
/// are below the [`BlurElement`] in order to display them. This adds an additional render step
/// but provides true results with the blurred contents.
TrueBlur {
// we are just a funny texture element that generates the texture on RenderElement::draw
id: Id,
scale: f64,
transform: Transform,
src: Rectangle<f64, Logical>,
size: Size<i32, Logical>,
corner_radius: f32,
loc: Point<i32, Physical>,
config: Blur,
// FIXME: Use DamageBag and expand it as needed?
commit_counter: CommitCounter,
fx_buffers: EffectsFramebufffersUserData,
},
}
impl BlurRenderElement {
/// Create a new [`BlurElement`]. You are supposed to put this **below** the translucent surface
/// that you want to blur. `area` is assumed to be relative to the `output` you are rendering
/// in.
///
/// If you don't update the blur optimized buffer
/// [`EffectsFramebuffers::update_optimized_blur_buffer`] this element will either
/// - Display outdated/wrong contents
/// - Not display anything since the buffer will be empty.
pub fn new_optimized(
renderer: &mut impl NiriRenderer,
fx_buffers: &EffectsFramebuffers,
sample_area: Rectangle<i32, Logical>,
loc: Point<i32, Physical>,
corner_radius: f32,
scale: f64,
config: Blur,
) -> Self {
let texture = fx_buffers.optimized_blur.clone();
let scaled = sample_area.to_f64().upscale(scale);
let texture = TextureRenderElement::from_static_texture(
Id::new(),
renderer.as_gles_renderer().context_id(),
loc.to_f64(),
texture,
1,
Transform::Normal,
Some(1.0),
Some(scaled),
Some(scaled.size.to_i32_ceil()),
// NOTE: Since this is "optimized" blur, anything below the window will not be
// rendered
Some(vec![Rectangle::new(
scaled.loc.to_i32_ceil(),
scaled.size.to_i32_ceil(),
)
.to_buffer(1, Transform::Normal, &sample_area.size)]),
Kind::Unspecified,
);
Self::Optimized {
tex: texture.into(),
corner_radius,
noise: config.noise.0 as f32,
scale,
output_size: fx_buffers.output_size,
}
}
pub fn new_true(
fx_buffers: EffectsFramebufffersUserData,
sample_area: Rectangle<i32, Logical>,
loc: Point<i32, Physical>,
corner_radius: f32,
scale: f64,
config: Blur,
) -> Self {
Self::TrueBlur {
id: Id::new(),
scale,
src: sample_area.to_f64(),
transform: Transform::Normal,
size: sample_area.size,
corner_radius,
loc,
config,
fx_buffers,
commit_counter: CommitCounter::default(),
}
}
}
impl Element for BlurRenderElement {
fn id(&self) -> &Id {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.id(),
BlurRenderElement::TrueBlur { id, .. } => id,
}
}
fn current_commit(&self) -> CommitCounter {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.current_commit(),
BlurRenderElement::TrueBlur { commit_counter, .. } => *commit_counter,
}
}
fn location(&self, scale: Scale<f64>) -> Point<i32, Physical> {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.location(scale),
BlurRenderElement::TrueBlur { loc, .. } => *loc,
}
}
fn src(&self) -> Rectangle<f64, Buffer> {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.src(),
BlurRenderElement::TrueBlur {
src,
transform,
size,
scale,
..
} => src.to_buffer(*scale, *transform, &size.to_f64()),
}
}
fn transform(&self) -> Transform {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.transform(),
BlurRenderElement::TrueBlur { transform, .. } => *transform,
}
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.damage_since(scale, commit),
BlurRenderElement::TrueBlur { config, .. } => {
let passes = config.passes;
let radius = config.radius.0 as f32;
// Since the blur element samples from around itself, we must expand the damage it
// induces to include any potential changes.
let mut geometry = Rectangle::from_size(self.geometry(scale).size);
let size = (2f32.powi(passes as i32 + 1) * radius).ceil() as i32;
geometry.loc -= Point::from((size, size));
geometry.size += Size::from((size, size)).upscale(2);
// FIXME: Damage tracking?
DamageSet::from_slice(&[geometry])
}
}
}
fn opaque_regions(&self, scale: Scale<f64>) -> OpaqueRegions<i32, Physical> {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.opaque_regions(scale),
BlurRenderElement::TrueBlur { .. } => {
// Since we are rendering as true blur, we will draw whatever is behind the window
OpaqueRegions::default()
}
}
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
match self {
BlurRenderElement::Optimized { tex, .. } => tex.geometry(scale),
BlurRenderElement::TrueBlur { loc, size, .. } => {
Rectangle::new(*loc, size.to_physical_precise_round(scale))
}
}
}
fn alpha(&self) -> f32 {
1.0
}
fn kind(&self) -> Kind {
Kind::Unspecified
}
}
#[allow(clippy::too_many_arguments)]
fn draw_true_blur(
fx_buffers: &mut EffectsFramebuffers,
gles_frame: &mut GlesFrame,
config: &Blur,
scale: f64,
dst: Rectangle<i32, Physical>,
corner_radius: f32,
src: Rectangle<f64, Buffer>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
alpha: f32,
is_tty: bool,
) -> Result<(), GlesError> {
fx_buffers.current_buffer = CurrentBuffer::Normal;
let shaders = Shaders::get_from_frame(gles_frame).blur.clone();
let vbos = RendererData::get_from_frame(gles_frame).vbos;
let supports_instancing = gles_frame
.capabilities()
.contains(&smithay::backend::renderer::gles::Capability::Instancing);
let debug = !gles_frame.debug_flags().is_empty();
let projection_matrix = glam::Mat3::from_cols_array(gles_frame.projection());
// Update the blur buffers.
// We use gl ffi directly to circumvent some stuff done by smithay
let blurred_texture = gles_frame.with_context(|gl| unsafe {
super::get_main_buffer_blur(
gl,
&mut *fx_buffers,
&shaders,
*config,
projection_matrix,
scale as i32,
&vbos,
debug,
supports_instancing,
dst,
is_tty,
)
})??;
let (program, additional_uniforms) = if corner_radius == 0.0 {
(None, vec![])
} else {
let program = Shaders::get_from_frame(gles_frame).blur_finish.clone();
(
program,
vec![
Uniform::new(
"geo",
[
dst.loc.x as f32,
dst.loc.y as f32,
dst.size.w as f32,
dst.size.h as f32,
],
),
Uniform::new("alpha", alpha),
Uniform::new("noise", config.noise.0 as f32),
Uniform::new("corner_radius", corner_radius),
Uniform::new(
"output_size",
[
fx_buffers.output_size.w as f32,
fx_buffers.output_size.h as f32,
],
),
],
)
};
gles_frame.render_texture_from_to(
&blurred_texture,
src,
dst,
damage,
opaque_regions,
Transform::Normal,
alpha,
program.as_ref(),
&additional_uniforms,
)
}
impl RenderElement<GlesRenderer> for BlurRenderElement {
fn draw(
&self,
gles_frame: &mut GlesFrame,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
match self {
Self::Optimized {
tex,
corner_radius,
noise,
scale,
output_size,
} => {
let downscaled_dst = Rectangle::new(
dst.loc,
Size::from((
(dst.size.w as f64 / *scale) as i32,
(dst.size.h as f64 / *scale) as i32,
)),
);
if *corner_radius == 0.0 {
<OptimizedBlurTextureElement as RenderElement<GlesRenderer>>::draw(
tex,
gles_frame,
src,
downscaled_dst,
damage,
opaque_regions,
)
} else {
let program = Shaders::get_from_frame(gles_frame).blur_finish.clone();
let gles_frame: &mut GlesFrame = gles_frame;
gles_frame.override_default_tex_program(
program.unwrap(),
vec![
Uniform::new(
"geo",
[
dst.loc.x as f32,
dst.loc.y as f32,
dst.size.w as f32,
dst.size.h as f32,
],
),
Uniform::new("corner_radius", *corner_radius),
Uniform::new(
"output_size",
[output_size.w as f32, output_size.h as f32],
),
Uniform::new("noise", *noise),
Uniform::new("alpha", self.alpha()),
],
);
let res =
<TextureRenderElement<GlesTexture> as RenderElement<GlesRenderer>>::draw(
&tex.0,
gles_frame,
src,
downscaled_dst,
damage,
opaque_regions,
);
gles_frame.clear_tex_program_override();
res
}
}
Self::TrueBlur {
fx_buffers,
scale,
corner_radius,
config,
..
} => draw_true_blur(
&mut fx_buffers.borrow_mut(),
gles_frame,
config,
*scale,
dst,
*corner_radius,
src,
damage,
opaque_regions,
self.alpha(),
false,
),
}
}
fn underlying_storage(&self, _: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
None
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for BlurRenderElement {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
match self {
Self::Optimized { .. } => {
<BlurRenderElement as RenderElement<GlesRenderer>>::draw(
self,
frame.as_gles_frame(),
src,
dst,
damage,
opaque_regions,
)?;
}
Self::TrueBlur {
fx_buffers,
scale,
corner_radius,
config,
..
} => {
draw_true_blur(
&mut fx_buffers.borrow_mut(),
frame.as_gles_frame(),
config,
*scale,
dst,
*corner_radius,
src,
damage,
opaque_regions,
self.alpha(),
true,
)?;
}
}
Ok(())
}
fn underlying_storage(
&'_ self,
_renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage<'_>> {
None
}
}

View File

@@ -0,0 +1,122 @@
use smithay::backend::renderer::element::texture::TextureRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::Texture;
use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::renderer::{AsGlesFrame, AsGlesRenderer};
#[derive(Debug)]
pub struct OptimizedBlurTextureElement<E = GlesTexture>(pub TextureRenderElement<E>)
where
E: Texture + Clone + 'static;
impl<E: Texture + Clone + 'static> From<TextureRenderElement<E>>
for OptimizedBlurTextureElement<E>
{
fn from(value: TextureRenderElement<E>) -> Self {
Self(value)
}
}
impl<E: Texture + Clone + 'static> Element for OptimizedBlurTextureElement<E> {
fn id(&self) -> &Id {
self.0.id()
}
fn current_commit(&self) -> CommitCounter {
self.0.current_commit()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.0.src()
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.0.geometry(scale)
}
fn location(&self, scale: Scale<f64>) -> Point<i32, Physical> {
self.geometry(scale).loc
}
fn transform(&self) -> Transform {
Transform::Normal
}
fn damage_since(
&self,
scale: Scale<f64>,
commit: Option<CommitCounter>,
) -> smithay::backend::renderer::utils::DamageSet<i32, Physical> {
self.0.damage_since(scale, commit)
}
fn alpha(&self) -> f32 {
self.0.alpha()
}
fn kind(&self) -> Kind {
self.0.kind()
}
}
impl RenderElement<GlesRenderer> for OptimizedBlurTextureElement<GlesTexture> {
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
<TextureRenderElement<GlesTexture> as RenderElement<GlesRenderer>>::draw(
&self.0,
frame,
src,
dst,
damage,
opaque_regions,
)
}
fn underlying_storage(
&'_ self,
renderer: &mut GlesRenderer,
) -> Option<smithay::backend::renderer::element::UnderlyingStorage<'_>> {
self.0.underlying_storage(renderer)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for OptimizedBlurTextureElement<GlesTexture> {
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
) -> Result<(), TtyRendererError<'render>> {
let frame = frame.as_gles_frame();
<TextureRenderElement<GlesTexture> as RenderElement<GlesRenderer>>::draw(
&self.0,
frame,
src,
dst,
damage,
opaque_regions,
)?;
Ok(())
}
fn underlying_storage(
&'_ self,
renderer: &mut TtyRenderer<'render>,
) -> Option<UnderlyingStorage<'_>> {
self.0.underlying_storage(renderer.as_gles_renderer())
}
}

View File

@@ -0,0 +1,181 @@
// Originally ported from https://github.com/nferhat/fht-compositor/blob/main/src/renderer/blur/shaders.rs
// The notes below are from the original code.
//! Since I need more control over how stuff is done when blurring, I use my own shader and
//! ffi::Gles2 directly. Gives me the most control but needs some re-writing of existing smithay
//! code.
//!
//! NOTE: Since we are assured that this shader lives as long as the compositor does, there's no
//! need to add a destruction callback sender like smithay does.
use std::fmt::Write;
use std::sync::Arc;
use smithay::backend::renderer::gles::{ffi, link_program, GlesError, GlesRenderer};
const BLUR_DOWN_SRC: &str = include_str!("../shaders/blur_down.frag");
const BLUR_UP_SRC: &str = include_str!("../shaders/blur_up.frag");
const VERTEX_SRC: &str = include_str!("../shaders/texture.vert");
/// The set of blur shaders used to render the blur.
#[derive(Clone, Debug)]
pub struct BlurShaders {
pub down: BlurShader,
pub up: BlurShader,
}
impl BlurShaders {
pub fn compile(renderer: &mut GlesRenderer) -> Result<Self, GlesError> {
renderer.with_context(|gl| unsafe {
let down = BlurShader::compile(gl, BLUR_DOWN_SRC)?;
let up = BlurShader::compile(gl, BLUR_UP_SRC)?;
Result::<Self, GlesError>::Ok(Self { down, up })
})?
}
}
#[derive(Clone, Debug)]
pub struct BlurShader(Arc<[BlurShaderVariant; 2]>);
impl BlurShader {
pub(super) fn variant_for_format(
&self,
format: Option<ffi::types::GLenum>,
has_alpha: bool,
) -> &BlurShaderVariant {
match format {
Some(ffi::BGRA_EXT) | Some(ffi::RGBA) | Some(ffi::RGBA8) | Some(ffi::RGB10_A2)
| Some(ffi::RGBA16F) => {
if has_alpha {
&self.0[0]
} else {
&self.0[1]
}
}
// SAFETY: Since we create the blur textures ourselves (through
// Offscreen::create_buffer) they should always be local to the renderer
None => panic!("Blur textures should not be external!"),
_ => panic!("Unknown texture type"),
}
}
pub(super) unsafe fn compile(gl: &ffi::Gles2, src: &str) -> Result<Self, GlesError> {
let create_variant = |defines: &[&str]| -> Result<BlurShaderVariant, GlesError> {
let shader = src.replace(
"//_DEFINES_",
&defines.iter().fold(String::new(), |mut shader, define| {
let _ = writeln!(&mut shader, "#define {}", define);
shader
}),
);
let debug_shader = src.replace(
"//_DEFINES_",
&defines.iter().chain(&["DEBUG_FLAGS"]).fold(
String::new(),
|mut shader, define| {
let _ = writeln!(shader, "#define {}", define);
shader
},
),
);
let program = unsafe { link_program(gl, VERTEX_SRC, &shader)? };
let debug_program = unsafe { link_program(gl, VERTEX_SRC, debug_shader.as_ref())? };
let vert = c"vert";
let vert_position = c"vert_position";
let tex = c"tex";
let matrix = c"matrix";
let tex_matrix = c"tex_matrix";
let alpha = c"alpha";
let radius = c"radius";
let half_pixel = c"half_pixel";
Ok(BlurShaderVariant {
normal: BlurShaderProgram {
program,
uniform_tex: gl
.GetUniformLocation(program, tex.as_ptr() as *const ffi::types::GLchar),
uniform_matrix: gl
.GetUniformLocation(program, matrix.as_ptr() as *const ffi::types::GLchar),
uniform_tex_matrix: gl.GetUniformLocation(
program,
tex_matrix.as_ptr() as *const ffi::types::GLchar,
),
uniform_alpha: gl
.GetUniformLocation(program, alpha.as_ptr() as *const ffi::types::GLchar),
uniform_radius: gl
.GetUniformLocation(program, radius.as_ptr() as *const ffi::types::GLchar),
uniform_half_pixel: gl.GetUniformLocation(
program,
half_pixel.as_ptr() as *const ffi::types::GLchar,
),
attrib_vert: gl
.GetAttribLocation(program, vert.as_ptr() as *const ffi::types::GLchar),
attrib_vert_position: gl.GetAttribLocation(
program,
vert_position.as_ptr() as *const ffi::types::GLchar,
),
},
debug: BlurShaderProgram {
program: debug_program,
uniform_tex: gl.GetUniformLocation(
debug_program,
tex.as_ptr() as *const ffi::types::GLchar,
),
uniform_matrix: gl.GetUniformLocation(
debug_program,
matrix.as_ptr() as *const ffi::types::GLchar,
),
uniform_tex_matrix: gl.GetUniformLocation(
debug_program,
tex_matrix.as_ptr() as *const ffi::types::GLchar,
),
uniform_alpha: gl.GetUniformLocation(
debug_program,
alpha.as_ptr() as *const ffi::types::GLchar,
),
uniform_radius: gl.GetUniformLocation(
debug_program,
radius.as_ptr() as *const ffi::types::GLchar,
),
uniform_half_pixel: gl.GetUniformLocation(
debug_program,
half_pixel.as_ptr() as *const ffi::types::GLchar,
),
attrib_vert: gl.GetAttribLocation(
debug_program,
vert.as_ptr() as *const ffi::types::GLchar,
),
attrib_vert_position: gl.GetAttribLocation(
debug_program,
vert_position.as_ptr() as *const ffi::types::GLchar,
),
},
})
};
Ok(BlurShader(Arc::new([
create_variant(&[])?,
create_variant(&["NO_ALPHA"])?,
])))
}
}
#[derive(Clone, Debug)]
pub struct BlurShaderVariant {
pub(super) normal: BlurShaderProgram,
pub(super) debug: BlurShaderProgram,
}
#[derive(Copy, Clone, Debug)]
pub struct BlurShaderProgram {
pub(super) program: ffi::types::GLuint,
pub(super) uniform_tex: ffi::types::GLint,
pub(super) uniform_tex_matrix: ffi::types::GLint,
pub(super) uniform_matrix: ffi::types::GLint,
pub(super) uniform_alpha: ffi::types::GLint,
pub(super) uniform_radius: ffi::types::GLint,
pub(super) uniform_half_pixel: ffi::types::GLint,
pub(super) attrib_vert: ffi::types::GLint,
pub(super) attrib_vert_position: ffi::types::GLint,
}

View File

@@ -18,6 +18,7 @@ use solid_color::{SolidColorBuffer, SolidColorRenderElement};
use self::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use self::texture::{TextureBuffer, TextureRenderElement};
pub mod blur;
pub mod border;
pub mod clipped_surface;
pub mod damage;
@@ -26,6 +27,7 @@ pub mod gradient_fade_texture;
pub mod memory;
pub mod offscreen;
pub mod primary_gpu_texture;
pub mod render_data;
pub mod render_elements;
pub mod renderer;
pub mod resize;

View File

@@ -0,0 +1,114 @@
use smithay::backend::renderer::gles::{ffi, Capability, GlesFrame, GlesRenderer};
/// Extra renderer data used for custom drawing with gles FFI.
///
/// [`GlesRenderer`] creates these, but keeps them private, so we create our own.
pub struct RendererData {
pub vbos: [u32; 2],
}
impl RendererData {
pub fn init(renderer: &mut GlesRenderer) {
let capabilities = renderer.capabilities();
let vertices: &[ffi::types::GLfloat] = if capabilities.contains(&Capability::Instancing) {
&INSTANCED_VERTS
} else {
&TRIANGLE_VERTS
};
let this = renderer
.with_context(|gl| unsafe {
let mut vbos = [0; 2];
gl.GenBuffers(vbos.len() as i32, vbos.as_mut_ptr());
gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[0]);
gl.BufferData(
ffi::ARRAY_BUFFER,
std::mem::size_of_val(vertices) as isize,
vertices.as_ptr() as *const _,
ffi::STATIC_DRAW,
);
gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[1]);
gl.BufferData(
ffi::ARRAY_BUFFER,
(std::mem::size_of::<ffi::types::GLfloat>() * OUTPUT_VERTS.len()) as isize,
OUTPUT_VERTS.as_ptr() as *const _,
ffi::STATIC_DRAW,
);
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
Self { vbos }
})
.unwrap();
assert!(
renderer
.egl_context()
.user_data()
.insert_if_missing(|| this),
"RendererData should only be initialized once"
);
}
pub fn get(renderer: &mut GlesRenderer) -> &Self {
renderer.egl_context().user_data().get().unwrap()
}
pub fn get_from_frame<'a>(frame: &'a mut GlesFrame<'_, '_>) -> &'a Self {
frame.egl_context().user_data().get().unwrap()
}
}
/// Vertices for instanced rendering.
static INSTANCED_VERTS: [ffi::types::GLfloat; 8] = [
1.0, 0.0, // top right
0.0, 0.0, // top left
1.0, 1.0, // bottom right
0.0, 1.0, // bottom left
];
/// Vertices for rendering individual triangles.
const MAX_RECTS_PER_DRAW: usize = 10;
const TRIANGLE_VERTS: [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] = triangle_verts();
const fn triangle_verts() -> [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] {
let mut verts = [0.; 12 * MAX_RECTS_PER_DRAW];
let mut i = 0;
loop {
// Top Left.
verts[i * 12] = 0.0;
verts[i * 12 + 1] = 0.0;
// Bottom left.
verts[i * 12 + 2] = 0.0;
verts[i * 12 + 3] = 1.0;
// Bottom right.
verts[i * 12 + 4] = 1.0;
verts[i * 12 + 5] = 1.0;
// Top left.
verts[i * 12 + 6] = 0.0;
verts[i * 12 + 7] = 0.0;
// Bottom right.
verts[i * 12 + 8] = 1.0;
verts[i * 12 + 9] = 1.0;
// Top right.
verts[i * 12 + 10] = 1.0;
verts[i * 12 + 11] = 0.0;
i += 1;
if i == MAX_RECTS_PER_DRAW {
break;
}
}
verts
}
/// Vertices for output rendering.
static OUTPUT_VERTS: [ffi::types::GLfloat; 8] = [
-1.0, 1.0, // top right
-1.0, -1.0, // top left
1.0, 1.0, // bottom right
1.0, -1.0, // bottom left
];

View File

@@ -0,0 +1,26 @@
// Ported from https://github.com/nferhat/fht-compositor/blob/main/src/renderer/shaders/blur-down.frag
precision highp float;
#if defined(EXTERNAL)
#extension GL_OES_EGL_image_external : require
uniform samplerExternalOES tex;
#else
uniform sampler2D tex;
#endif
varying vec2 niri_v_coords;
uniform float radius;
uniform vec2 half_pixel;
void main() {
vec2 uv = niri_v_coords * 2.0;
vec4 sum = texture2D(tex, uv) * 4.0;
sum += texture2D(tex, uv - half_pixel * radius);
sum += texture2D(tex, uv + half_pixel * radius);
sum += texture2D(tex, uv + vec2(half_pixel.x, -half_pixel.y) * radius);
sum += texture2D(tex, uv - vec2(half_pixel.x, -half_pixel.y) * radius);
gl_FragColor = sum / 8.0;
}

View File

@@ -0,0 +1,98 @@
// Ported from https://github.com/nferhat/fht-compositor/blob/main/src/renderer/shaders/blur-finish.frag
//
// Implementation from pinnacle-comp/pinnacle (GPL-3.0)
// Thank you very much!
#version 100
//_DEFINES_
#if defined(EXTERNAL)
#extension GL_OES_EGL_image_external : require
#endif
precision highp float;
#if defined(EXTERNAL)
uniform samplerExternalOES tex;
#else
uniform sampler2D tex;
#endif
uniform float alpha;
varying vec2 v_coords;
#if defined(DEBUG_FLAGS)
uniform float tint;
#endif
uniform vec4 geo;
uniform vec2 output_size;
uniform float corner_radius;
uniform float noise;
float rounding_alpha(vec2 coords, vec2 size, float radius) {
vec2 center;
if (coords.x < radius && coords.y < radius) {
center = vec2(radius, radius);
} else if (coords.x > size.x - radius && coords.y < radius) {
center = vec2(size.x - radius, radius);
} else if (coords.x > size.x - radius && coords.y > size.y - radius) {
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < radius && coords.y > size.y - radius) {
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float dist = distance(coords, center);
float half_px = 0.5 ;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
// Noise function copied from hyprland.
// I like the effect it gave, can be tweaked further
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 727.727); // wysi :wink: :wink:
p3 += dot(p3, p3.xyz + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
void main() {
vec2 texCoords;
// Sample the texture.
vec4 color = texture2D(tex, v_coords);
#if defined(NO_ALPHA)
color = vec4(color.rgb, 1.0);
#endif
// This shader exists to make blur rounding correct.
//
// Since we are scr-ing a texture that is the size of the output, the v_coords are always
// relative to the output. This corresponds to gl_FragCoord.
vec2 size = geo.zw;
vec2 loc;
loc.x = gl_FragCoord.x - geo.x;
// FIXME: y inverted
loc.y = output_size.y - (gl_FragCoord.y + geo.y);
// Add noise fx
// This can be used to achieve a glass look
float noiseHash = hash(loc / size);
float noiseAmount = (mod(noiseHash, 1.0) - 0.5);
color.rgb += noiseAmount * noise;
// Apply corner rounding inside geometry.
color *= rounding_alpha(loc, size, corner_radius);
// Apply final alpha and tint.
color *= alpha;
#if defined(DEBUG_FLAGS)
if (tint == 1.0)
color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8;
#endif
gl_FragColor = color;
}
// vim: ft=glsl

View File

@@ -0,0 +1,29 @@
// Ported from https://github.com/nferhat/fht-compositor/blob/main/src/renderer/shaders/blur-up.frag
precision highp float;
#if defined(EXTERNAL)
#extension GL_OES_EGL_image_external : require
uniform samplerExternalOES tex;
#else
uniform sampler2D tex;
#endif
varying vec2 niri_v_coords;
uniform vec2 half_pixel;
uniform float radius;
void main() {
vec2 uv = niri_v_coords / 2.0;
vec4 sum = texture2D(tex, uv + vec2(-half_pixel.x * 2.0, 0.0) * radius);
sum += texture2D(tex, uv + vec2(-half_pixel.x, half_pixel.y) * radius) * 2.0;
sum += texture2D(tex, uv + vec2(0.0, half_pixel.y * 2.0) * radius);
sum += texture2D(tex, uv + vec2(half_pixel.x, half_pixel.y) * radius) * 2.0;
sum += texture2D(tex, uv + vec2(half_pixel.x * 2.0, 0.0) * radius);
sum += texture2D(tex, uv + vec2(half_pixel.x, -half_pixel.y) * radius) * 2.0;
sum += texture2D(tex, uv + vec2(0.0, -half_pixel.y * 2.0) * radius);
sum += texture2D(tex, uv + vec2(-half_pixel.x, -half_pixel.y) * radius) * 2.0;
gl_FragColor = sum / 12.0;
}

View File

@@ -6,6 +6,8 @@ use smithay::backend::renderer::gles::{
UniformValue,
};
use super::blur::shader::BlurShaders;
use super::renderer::NiriRenderer;
use super::shader_element::ShaderProgram;
@@ -18,6 +20,8 @@ pub struct Shaders {
pub custom_resize: RefCell<Option<ShaderProgram>>,
pub custom_close: RefCell<Option<ShaderProgram>>,
pub custom_open: RefCell<Option<ShaderProgram>>,
pub blur_finish: Option<GlesTexProgram>,
pub blur: BlurShaders,
}
#[derive(Debug, Clone, Copy)]
@@ -97,6 +101,22 @@ impl Shaders {
})
.ok();
let blur_finish = renderer
.compile_custom_texture_shader(
include_str!("blur_finish.frag"),
&[
UniformName::new("output_size", UniformType::_2f),
UniformName::new("corner_radius", UniformType::_1f),
UniformName::new("alpha", UniformType::_1f),
UniformName::new("noise", UniformType::_1f),
UniformName::new("geo", UniformType::_4f),
],
)
.map_err(|e| warn!("error compiling clipped surface shader: {e:?}"))
.ok();
let blur = BlurShaders::compile(renderer).expect("blur shaders should always compile");
let gradient_fade = renderer
.compile_custom_texture_shader(
include_str!("gradient_fade.frag"),
@@ -116,6 +136,8 @@ impl Shaders {
custom_resize: RefCell::new(None),
custom_close: RefCell::new(None),
custom_open: RefCell::new(None),
blur_finish,
blur,
}
}

View File

@@ -941,6 +941,10 @@ impl LayoutElement for Mapped {
self.need_to_recompute_rules |= changed;
}
fn is_floating(&self) -> bool {
self.is_floating
}
fn set_bounds(&self, bounds: Size<i32, Logical>) {
self.toplevel().with_pending_state(|state| {
state.bounds = Some(bounds);

View File

@@ -3,7 +3,7 @@ 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,
BlockOutFrom, BlurRule, BorderRule, CornerRadius, FloatingPosition, PresetSize, ShadowRule,
TabIndicatorRule,
};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
@@ -84,6 +84,8 @@ pub struct ResolvedWindowRules {
pub border: BorderRule,
/// Shadow overrides.
pub shadow: ShadowRule,
/// Blur overrides.
pub blur: BlurRule,
/// Tab indicator overrides.
pub tab_indicator: TabIndicatorRule,
@@ -260,6 +262,7 @@ impl ResolvedWindowRules {
resolved.border.merge_with(&rule.border);
resolved.shadow.merge_with(&rule.shadow);
resolved.tab_indicator.merge_with(&rule.tab_indicator);
resolved.blur.merge_with(&rule.blur);
if let Some(x) = rule.draw_border_with_background {
resolved.draw_border_with_background = Some(x);

View File

@@ -20,6 +20,8 @@ in
"*.md"
"*.nix"
"*.rs"
"*.vert"
"*.frag"
];
};
}

View File

@@ -3,3 +3,4 @@ datas = "datas"
arange = "arange"
localed = "localed"
WRONLY = "WRONLY"
utput = "utput"