From 71be19b234d58f4ec447e921633506beb81a52c0 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Sat, 13 Apr 2024 11:07:23 +0400 Subject: Implement window resize animations --- Cargo.lock | 4 +- niri-config/src/lib.rs | 14 + niri-visual-tests/src/cases/tile.rs | 9 +- niri-visual-tests/src/cases/window.rs | 14 +- niri-visual-tests/src/smithay_view.rs | 3 +- niri-visual-tests/src/test_window.rs | 16 +- src/backend/tty.rs | 3 +- src/backend/winit.rs | 3 +- src/handlers/xdg_shell.rs | 33 +- src/layout/mod.rs | 56 ++- src/layout/tile.rs | 358 +++++++++++++++-- src/layout/workspace.rs | 127 +++++-- src/render_helpers/crossfade.rs | 156 ++++++++ src/render_helpers/mod.rs | 3 + .../primary_gpu_pixel_shader_with_textures.rs | 423 +++++++++++++++++++++ src/render_helpers/resources.rs | 106 ++++++ src/render_helpers/shaders/crossfade.frag | 31 ++ src/render_helpers/shaders/mod.rs | 24 +- src/render_helpers/shaders/texture.vert | 25 ++ src/window/mapped.rs | 91 ++++- wiki/Configuration:-Animations.md | 21 + 21 files changed, 1432 insertions(+), 88 deletions(-) create mode 100644 src/render_helpers/crossfade.rs create mode 100644 src/render_helpers/primary_gpu_pixel_shader_with_textures.rs create mode 100644 src/render_helpers/resources.rs create mode 100644 src/render_helpers/shaders/crossfade.frag create mode 100644 src/render_helpers/shaders/texture.vert diff --git a/Cargo.lock b/Cargo.lock index 9eecafa9..f5c20301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3115,7 +3115,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay" version = "0.3.0" -source = "git+https://github.com/Smithay/smithay.git#e237b077bd922e17849eff91ba05853c7a68f958" +source = "git+https://github.com/Smithay/smithay.git#c5e9a697e41f50dc56b918d9ef1e3d2e52c84ac0" dependencies = [ "appendlist", "bitflags 2.5.0", @@ -3187,7 +3187,7 @@ dependencies = [ [[package]] name = "smithay-drm-extras" version = "0.1.0" -source = "git+https://github.com/Smithay/smithay.git#e237b077bd922e17849eff91ba05853c7a68f958" +source = "git+https://github.com/Smithay/smithay.git#c5e9a697e41f50dc56b918d9ef1e3d2e52c84ac0" dependencies = [ "drm", "edid-rs", diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index e1ad0b2b..59cf01cf 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -489,6 +489,8 @@ pub struct Animations { pub window_open: Animation, #[knuffel(child, default = Animation::default_window_close())] pub window_close: Animation, + #[knuffel(child, default = Animation::default_window_resize())] + pub window_resize: Animation, #[knuffel(child, default = Animation::default_config_notification_open_close())] pub config_notification_open_close: Animation, } @@ -503,6 +505,7 @@ impl Default for Animations { window_movement: Animation::default_window_movement(), window_open: Animation::default_window_open(), window_close: Animation::default_window_close(), + window_resize: Animation::default_window_resize(), config_notification_open_close: Animation::default_config_notification_open_close(), } } @@ -592,6 +595,17 @@ impl Animation { }), } } + + pub const fn default_window_resize() -> Self { + Self { + off: false, + kind: AnimationKind::Spring(SpringParams { + damping_ratio: 1., + stiffness: 800, + epsilon: 0.0001, + }), + } + } } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index 5b2f7cb8..93aa38a0 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -20,7 +20,7 @@ impl Tile { pub fn freeform(size: Size) -> Self { let window = TestWindow::freeform(0); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size); + rv.tile.request_tile_size(size, false); rv.window.communicate(); rv } @@ -28,7 +28,7 @@ impl Tile { pub fn fixed_size(size: Size) -> Self { let window = TestWindow::fixed_size(0); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size); + rv.tile.request_tile_size(size, false); rv.window.communicate(); rv } @@ -37,7 +37,7 @@ impl Tile { let window = TestWindow::fixed_size(0); window.set_csd_shadow_width(64); let mut rv = Self::with_window(window); - rv.tile.request_tile_size(size); + rv.tile.request_tile_size(size, false); rv.window.communicate(); rv } @@ -84,7 +84,8 @@ impl Tile { impl TestCase for Tile { fn resize(&mut self, width: i32, height: i32) { - self.tile.request_tile_size(Size::from((width, height))); + self.tile + .request_tile_size(Size::from((width, height)), false); self.window.communicate(); } diff --git a/niri-visual-tests/src/cases/window.rs b/niri-visual-tests/src/cases/window.rs index 3cd4c0b8..f19ec5ba 100644 --- a/niri-visual-tests/src/cases/window.rs +++ b/niri-visual-tests/src/cases/window.rs @@ -13,23 +13,23 @@ pub struct Window { impl Window { pub fn freeform(size: Size) -> Self { - let window = TestWindow::freeform(0); - window.request_size(size); + let mut window = TestWindow::freeform(0); + window.request_size(size, false); window.communicate(); Self { window } } pub fn fixed_size(size: Size) -> Self { - let window = TestWindow::fixed_size(0); - window.request_size(size); + let mut window = TestWindow::fixed_size(0); + window.request_size(size, false); window.communicate(); Self { window } } pub fn fixed_size_with_csd_shadow(size: Size) -> Self { - let window = TestWindow::fixed_size(0); + let mut window = TestWindow::fixed_size(0); window.set_csd_shadow_width(64); - window.request_size(size); + window.request_size(size, false); window.communicate(); Self { window } } @@ -37,7 +37,7 @@ impl Window { impl TestCase for Window { fn resize(&mut self, width: i32, height: i32) { - self.window.request_size(Size::from((width, height))); + self.window.request_size(Size::from((width, height)), false); self.window.communicate(); } diff --git a/niri-visual-tests/src/smithay_view.rs b/niri-visual-tests/src/smithay_view.rs index 0dcd3935..91a8929a 100644 --- a/niri-visual-tests/src/smithay_view.rs +++ b/niri-visual-tests/src/smithay_view.rs @@ -11,7 +11,7 @@ mod imp { use anyhow::{ensure, Context}; use gtk::gdk; use gtk::prelude::*; - use niri::render_helpers::shaders; + use niri::render_helpers::{resources, shaders}; use niri::utils::get_monotonic_time; use smithay::backend::egl::ffi::egl; use smithay::backend::egl::EGLContext; @@ -194,6 +194,7 @@ mod imp { let mut renderer = GlesRenderer::with_capabilities(egl_context, capabilities) .context("error creating GlesRenderer")?; + resources::init(&mut renderer); shaders::init(&mut renderer); Ok(renderer) diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index 9b8b80a2..ce325534 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -2,7 +2,9 @@ use std::cell::RefCell; use std::cmp::{max, min}; use std::rc::Rc; -use niri::layout::{LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot}; +use niri::layout::{ + AnimationSnapshot, LayoutElement, LayoutElementRenderElement, LayoutElementRenderSnapshot, +}; use niri::render_helpers::renderer::NiriRenderer; use niri::render_helpers::{RenderSnapshot, RenderTarget}; use niri::window::ResolvedWindowRules; @@ -177,7 +179,7 @@ impl LayoutElement for TestWindow { RenderSnapshot::default() } - fn request_size(&self, size: Size) { + fn request_size(&mut self, size: Size, _animate: bool) { self.inner.borrow_mut().requested_size = Some(size); self.inner.borrow_mut().pending_fullscreen = false; } @@ -214,7 +216,7 @@ impl LayoutElement for TestWindow { fn set_bounds(&self, _bounds: Size) {} - fn send_pending_configure(&self) {} + fn send_pending_configure(&mut self) {} fn is_fullscreen(&self) -> bool { false @@ -230,4 +232,12 @@ impl LayoutElement for TestWindow { static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty(); &EMPTY } + + fn animation_snapshot(&self) -> Option<&AnimationSnapshot> { + None + } + + fn take_animation_snapshot(&mut self) -> Option { + None + } } diff --git a/src/backend/tty.rs b/src/backend/tty.rs index eedb47a2..0d2aaed2 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -59,7 +59,7 @@ use super::{IpcOutputMap, RenderResult}; use crate::frame_clock::FrameClock; use crate::niri::{Niri, RedrawState, State}; use crate::render_helpers::renderer::AsGlesRenderer; -use crate::render_helpers::{shaders, RenderTarget}; +use crate::render_helpers::{resources, shaders, RenderTarget}; use crate::utils::{get_monotonic_time, logical_output}; const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888]; @@ -490,6 +490,7 @@ impl Tty { warn!("error binding wl-display in EGL: {err:?}"); } + resources::init(renderer.as_gles_renderer()); shaders::init(renderer.as_gles_renderer()); // Create the dmabuf global. diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 0c5fc1fc..9e05822f 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -19,7 +19,7 @@ use smithay::reexports::winit::window::WindowBuilder; use super::{IpcOutputMap, RenderResult}; use crate::niri::{Niri, RedrawState, State}; -use crate::render_helpers::{shaders, RenderTarget}; +use crate::render_helpers::{resources, shaders, RenderTarget}; use crate::utils::{get_monotonic_time, logical_output}; pub struct Winit { @@ -130,6 +130,7 @@ impl Winit { warn!("error binding renderer wl_display: {err}"); } + resources::init(renderer); shaders::init(renderer); niri.add_output(self.output.clone(), None); diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 85ce5915..ab9d7072 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -815,16 +815,34 @@ fn unconstrain_with_padding( pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId { add_pre_commit_hook::(toplevel.wl_surface(), move |state, _dh, surface| { + let _span = tracy_client::span!("mapped toplevel pre-commit"); + let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else { error!("pre-commit hook for mapped surfaces must be removed upon unmapping"); return; }; - let got_unmapped = with_states(surface, |states| { - let attrs = states.cached_state.current::(); - matches!(attrs.buffer, Some(BufferAssignment::Removed)) + let (got_unmapped, commit_serial) = with_states(surface, |states| { + let attrs = states.cached_state.pending::(); + let got_unmapped = matches!(attrs.buffer, Some(BufferAssignment::Removed)); + + let role = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + (got_unmapped, role.configure_serial) }); + let animate = if let Some(serial) = commit_serial { + mapped.should_animate_commit(serial) + } else { + error!("commit on a mapped surface without a configured serial"); + false + }; + if got_unmapped { state.backend.with_primary_renderer(|renderer| { mapped.render_and_store_snapshot(renderer); @@ -832,6 +850,15 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId } else { // The toplevel remains mapped; clear any cached render snapshot. let _ = mapped.take_last_render(); + + if animate { + state.backend.with_primary_renderer(|renderer| { + mapped.store_animation_snapshot(renderer); + }); + + let window = mapped.window.clone(); + state.niri.layout.prepare_for_resize_animation(&window); + } } }) } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 073ae804..efcc8167 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -60,6 +60,9 @@ pub mod monitor; pub mod tile; pub mod workspace; +/// Size changes up to this many pixels don't animate. +pub const RESIZE_ANIMATION_THRESHOLD: i32 = 10; + niri_render_elements! { LayoutElementRenderElement => { Wayland = WaylandSurfaceRenderElement, @@ -70,6 +73,15 @@ niri_render_elements! { pub type LayoutElementRenderSnapshot = RenderSnapshot>, BakedBuffer>; +/// Snapshot of an element for animation. +#[derive(Debug)] +pub struct AnimationSnapshot { + /// Snapshot of the render. + pub render: LayoutElementRenderSnapshot, + /// Visual size of the element at the point of the snapshot. + pub size: Size, +} + pub trait LayoutElement { /// Type that can be used as a unique ID of this element. type Id: PartialEq; @@ -108,7 +120,7 @@ pub trait LayoutElement { fn take_last_render(&self) -> LayoutElementRenderSnapshot; - fn request_size(&self, size: Size); + fn request_size(&mut self, size: Size, animate: bool); fn request_fullscreen(&self, size: Size); fn min_size(&self) -> Size; fn max_size(&self) -> Size; @@ -121,7 +133,7 @@ pub trait LayoutElement { fn set_activated(&mut self, active: bool); fn set_bounds(&self, bounds: Size); - fn send_pending_configure(&self); + fn send_pending_configure(&mut self); /// Whether the element is currently fullscreen. /// @@ -137,6 +149,9 @@ pub trait LayoutElement { /// Runs periodic clean-up tasks. fn refresh(&self); + + fn animation_snapshot(&self) -> Option<&AnimationSnapshot>; + fn take_animation_snapshot(&mut self) -> Option; } #[derive(Debug)] @@ -1787,6 +1802,31 @@ impl Layout { } } + pub fn prepare_for_resize_animation(&mut self, window: &W::Id) { + let _span = tracy_client::span!("Layout::prepare_for_resize_animation"); + + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + if ws.has_window(window) { + ws.prepare_for_resize_animation(window); + return; + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + ws.prepare_for_resize_animation(window); + return; + } + } + } + } + } + pub fn refresh(&mut self) { let _span = tracy_client::span!("Layout::refresh"); @@ -1930,7 +1970,7 @@ mod tests { RenderSnapshot::default() } - fn request_size(&self, size: Size) { + fn request_size(&mut self, size: Size, _animate: bool) { self.0.requested_size.set(Some(size)); self.0.pending_fullscreen.set(false); } @@ -1967,7 +2007,7 @@ mod tests { fn set_bounds(&self, _bounds: Size) {} - fn send_pending_configure(&self) {} + fn send_pending_configure(&mut self) {} fn is_fullscreen(&self) -> bool { false @@ -1983,6 +2023,14 @@ mod tests { static EMPTY: ResolvedWindowRules = ResolvedWindowRules::empty(); &EMPTY } + + fn animation_snapshot(&self) -> Option<&AnimationSnapshot> { + None + } + + fn take_animation_snapshot(&mut self) -> Option { + None + } } fn arbitrary_bbox() -> impl Strategy> { diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 1fcf4f1b..578de86a 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -1,21 +1,31 @@ +use std::cell::OnceCell; use std::cmp::max; use std::rc::Rc; use std::time::Duration; +use niri_config::BlockOutFrom; +use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; use smithay::backend::renderer::element::utils::RescaleRenderElement; use smithay::backend::renderer::element::{Element, Kind}; -use smithay::backend::renderer::gles::GlesRenderer; -use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; +use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; +use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform}; use super::focus_ring::{FocusRing, FocusRingRenderElement}; -use super::{LayoutElement, LayoutElementRenderElement, Options}; +use super::{ + AnimationSnapshot, LayoutElement, LayoutElementRenderElement, Options, + RESIZE_ANIMATION_THRESHOLD, +}; use crate::animation::Animation; use crate::niri_render_elements; +use crate::render_helpers::crossfade::CrossfadeRenderElement; use crate::render_helpers::offscreen::OffscreenRenderElement; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::renderer::NiriRenderer; -use crate::render_helpers::{RenderSnapshot, RenderTarget, ToRenderElement}; +use crate::render_helpers::shaders::Shaders; +use crate::render_helpers::{ + render_to_encompassing_texture, RenderSnapshot, RenderTarget, ToRenderElement, +}; /// Toplevel window with decorations. #[derive(Debug)] @@ -47,6 +57,12 @@ pub struct Tile { /// The animation upon opening a window. open_animation: Option, + /// The animation of the window resizing. + resize_animation: Option, + + /// The animation of a tile visually moving. + move_animation: Option, + /// Configurable properties of the layout. options: Rc, } @@ -57,17 +73,41 @@ niri_render_elements! { FocusRing = FocusRingRenderElement, SolidColor = SolidColorRenderElement, Offscreen = RescaleRenderElement, + Crossfade = CrossfadeRenderElement, } } niri_render_elements! { - TileSnapshotRenderElement => { + TileSnapshotContentsRenderElement => { Texture = PrimaryGpuTextureRenderElement, + SolidColor = SolidColorRenderElement, + } +} + +niri_render_elements! { + TileSnapshotRenderElement => { + Contents = RescaleRenderElement, FocusRing = FocusRingRenderElement, SolidColor = SolidColorRenderElement, } } +#[derive(Debug)] +struct ResizeAnimation { + anim: Animation, + size_from: Size, + snapshot: AnimationSnapshot, + /// Snapshot rendered into a texture (happens lazily). + snapshot_texture: OnceCell)>>, + snapshot_blocked_out_texture: OnceCell)>>, +} + +#[derive(Debug)] +struct MoveAnimation { + anim: Animation, + from: Point, +} + impl Tile { pub fn new(window: W, options: Rc) -> Self { Self { @@ -78,6 +118,8 @@ impl Tile { fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]), fullscreen_size: Default::default(), open_animation: None, + resize_animation: None, + move_animation: None, options, } } @@ -93,16 +135,76 @@ impl Tile { if self.fullscreen_size != Size::from((0, 0)) { self.is_fullscreen = self.window.is_fullscreen(); } + + if let Some(animate_from) = self.window.take_animation_snapshot() { + let size_from = if let Some(resize) = self.resize_animation.take() { + // Compute like in animated_window_size(), but using the snapshot geometry (since + // the current one is already overwritten). + let mut size = animate_from.size; + + let val = resize.anim.value(); + let size_from = resize.size_from; + + size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32; + size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32; + + size + } else { + animate_from.size + }; + + let change = self.window.size().to_point() - size_from.to_point(); + let change = max(change.x.abs(), change.y.abs()); + if change > RESIZE_ANIMATION_THRESHOLD { + let anim = Animation::new( + 0., + 1., + 0., + self.options.animations.window_resize, + niri_config::Animation::default_window_resize(), + ); + self.resize_animation = Some(ResizeAnimation { + anim, + size_from, + snapshot: animate_from, + snapshot_texture: OnceCell::new(), + snapshot_blocked_out_texture: OnceCell::new(), + }); + } else { + self.resize_animation = None; + } + } } pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) { + if let Some(anim) = &mut self.open_animation { + anim.set_current_time(current_time); + if anim.is_done() { + self.open_animation = None; + } + } + + if let Some(resize) = &mut self.resize_animation { + resize.anim.set_current_time(current_time); + if resize.anim.is_done() { + self.resize_animation = None; + } + } + + if let Some(move_) = &mut self.move_animation { + move_.anim.set_current_time(current_time); + if move_.anim.is_done() { + self.move_animation = None; + } + } + let draw_border_with_background = self .window .rules() .draw_border_with_background .unwrap_or_else(|| !self.window.has_ssd()); self.border - .update(self.window.size(), !draw_border_with_background); + .update(self.animated_window_size(), !draw_border_with_background); self.border.set_active(is_active); let draw_focus_ring_with_background = if self.effective_border_width().is_some() { @@ -111,22 +213,24 @@ impl Tile { draw_border_with_background }; self.focus_ring - .update(self.tile_size(), !draw_focus_ring_with_background); + .update(self.animated_tile_size(), !draw_focus_ring_with_background); self.focus_ring.set_active(is_active); - - match &mut self.open_animation { - Some(anim) => { - anim.set_current_time(current_time); - if anim.is_done() { - self.open_animation = None; - } - } - None => (), - } } pub fn are_animations_ongoing(&self) -> bool { self.open_animation.is_some() + || self.resize_animation.is_some() + || self.move_animation.is_some() + } + + pub fn render_offset(&self) -> Point { + let mut offset = Point::from((0., 0.)); + + if let Some(move_) = &self.move_animation { + offset += move_.from.to_f64().upscale(move_.anim.value()); + } + + offset.to_i32_round() } pub fn start_open_animation(&mut self) { @@ -143,6 +247,24 @@ impl Tile { &self.open_animation } + pub fn resize_animation(&self) -> Option<&Animation> { + self.resize_animation.as_ref().map(|resize| &resize.anim) + } + + pub fn animate_move_from_with_config( + &mut self, + from: Point, + config: niri_config::Animation, + default: niri_config::Animation, + ) { + let current_offset = self.render_offset(); + + self.move_animation = Some(MoveAnimation { + anim: Animation::new(1., 0., 0., config, default), + from: from + current_offset, + }); + } + pub fn window(&self) -> &W { &self.window } @@ -217,6 +339,39 @@ impl Tile { self.window.size() } + fn animated_window_size(&self) -> Size { + let mut size = self.window.size(); + + if let Some(resize) = &self.resize_animation { + let val = resize.anim.value(); + let size_from = resize.size_from; + + size.w = (size_from.w as f64 + (size.w - size_from.w) as f64 * val).round() as i32; + size.h = (size_from.h as f64 + (size.h - size_from.h) as f64 * val).round() as i32; + } + + size + } + + fn animated_tile_size(&self) -> Size { + let mut size = self.animated_window_size(); + + if self.is_fullscreen { + // Normally we'd just return the fullscreen size here, but this makes things a bit + // nicer if a fullscreen window is bigger than the fullscreen size for some reason. + size.w = max(size.w, self.fullscreen_size.w); + size.h = max(size.h, self.fullscreen_size.h); + return size; + } + + if let Some(width) = self.effective_border_width() { + size.w = size.w.saturating_add(width * 2); + size.h = size.h.saturating_add(width * 2); + } + + size + } + pub fn buf_loc(&self) -> Point { let mut loc = Point::from((0, 0)); loc += self.window_loc(); @@ -234,7 +389,7 @@ impl Tile { activation_region.to_f64().contains(point) } - pub fn request_tile_size(&mut self, mut size: Size) { + pub fn request_tile_size(&mut self, mut size: Size, animate: bool) { // Can't go through effective_border_width() because we might be fullscreen. if !self.border.is_off() { let width = self.border.width(); @@ -242,7 +397,7 @@ impl Tile { size.h = max(1, size.h - width * 2); } - self.window.request_size(size); + self.window.request_size(size, animate); } pub fn tile_width_for_window_width(&self, size: i32) -> i32 { @@ -330,11 +485,85 @@ impl Tile { self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.) }; - let rv = self - .window - .render(renderer, location + self.window_loc(), scale, alpha, target) + let window_loc = self.window_loc(); + let window_size = self.window_size(); + let animated_window_size = self.animated_window_size(); + let window_render_loc = location + window_loc; + let area = Rectangle::from_loc_and_size(window_render_loc, animated_window_size); + + let gles_renderer = renderer.as_gles_renderer(); + + // If we're resizing, try to render a crossfade, or a fallback. + let mut crossfade = None; + let mut crossfade_fallback = None; + + if let Some(resize) = &self.resize_animation { + if Shaders::get(gles_renderer).crossfade.is_some() { + if let Some(texture_from) = resize.rendered_texture(gles_renderer, scale, target) { + let window_elements = + self.window + .render(gles_renderer, Point::from((0, 0)), scale, 1., target); + let current = render_to_encompassing_texture( + gles_renderer, + scale, + Transform::Normal, + Fourcc::Abgr8888, + &window_elements, + ) + .map_err(|err| warn!("error rendering window to texture: {err:?}")) + .ok(); + + if let Some((texture_current, _sync_point, texture_current_geo)) = current { + let elem = CrossfadeRenderElement::new( + gles_renderer, + area, + scale, + texture_from.clone(), + resize.snapshot.size, + (texture_current, texture_current_geo), + window_size, + resize.anim.clamped_value().clamp(0., 1.) as f32, + alpha, + ) + .expect("we checked the crossfade shader above"); + self.window + .set_offscreen_element_id(Some(elem.id().clone())); + crossfade = Some(elem.into()); + } + } + } + + if crossfade.is_none() { + let fallback_buffer = SolidColorBuffer::new(area.size, [1., 0., 0., 1.]); + crossfade_fallback = Some( + SolidColorRenderElement::from_buffer( + &fallback_buffer, + area.loc.to_physical_precise_round(scale), + scale, + alpha, + Kind::Unspecified, + ) + .into(), + ); + self.window.set_offscreen_element_id(None); + } + } + + // If we're not resizing, render the window itself. + let mut window = None; + if crossfade.is_none() && crossfade_fallback.is_none() { + window = Some( + self.window + .render(renderer, window_render_loc, scale, alpha, target) + .into_iter() + .map(Into::into), + ); + } + + let rv = crossfade .into_iter() - .map(Into::into); + .chain(crossfade_fallback) + .chain(window.into_iter().flatten()); let elem = self.effective_border_width().map(|width| { self.border @@ -422,7 +651,7 @@ impl Tile { contents: Vec, ) -> Vec where - E: Into, + E: Into, C: ToRenderElement, { let alpha = if self.is_fullscreen { @@ -431,10 +660,18 @@ impl Tile { self.window.rules().opacity.unwrap_or(1.).clamp(0., 1.) }; + let window_size = self.window_size(); + let animated_window_size = self.animated_window_size(); + let animated_scale = animated_window_size.to_f64() / window_size.to_f64(); + let mut rv = vec![]; for baked in contents { let elem = baked.to_render_element(self.window_loc(), scale, alpha, Kind::Unspecified); + let elem: TileSnapshotContentsRenderElement = elem.into(); + + let origin = self.window_loc().to_physical_precise_round(scale); + let elem = RescaleRenderElement::from_element(elem, origin, animated_scale); rv.push(elem.into()); } @@ -483,3 +720,76 @@ impl Tile { } } } + +impl ResizeAnimation { + fn rendered_texture( + &self, + renderer: &mut GlesRenderer, + scale: Scale, + target: RenderTarget, + ) -> &Option<(GlesTexture, Rectangle)> { + let block_out = match self.snapshot.render.block_out_from { + None => false, + Some(BlockOutFrom::Screencast) => target == RenderTarget::Screencast, + Some(BlockOutFrom::ScreenCapture) => target != RenderTarget::Output, + }; + + if block_out { + self.snapshot_blocked_out_texture.get_or_init(|| { + let _span = tracy_client::span!("ResizeAnimation::rendered_texture"); + + let elements: Vec<_> = self + .snapshot + .render + .blocked_out_contents + .iter() + .map(|baked| { + baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified) + }) + .collect(); + + match render_to_encompassing_texture( + renderer, + scale, + Transform::Normal, + Fourcc::Abgr8888, + &elements, + ) { + Ok((texture, _sync_point, geo)) => Some((texture, geo)), + Err(err) => { + warn!("error rendering snapshot to texture: {err:?}"); + None + } + } + }) + } else { + self.snapshot_texture.get_or_init(|| { + let _span = tracy_client::span!("ResizeAnimation::rendered_texture"); + + let elements: Vec<_> = self + .snapshot + .render + .contents + .iter() + .map(|baked| { + baked.to_render_element(Point::from((0, 0)), scale, 1., Kind::Unspecified) + }) + .collect(); + + match render_to_encompassing_texture( + renderer, + scale, + Transform::Normal, + Fourcc::Abgr8888, + &elements, + ) { + Ok((texture, _sync_point, geo)) => Some((texture, geo)), + Err(err) => { + warn!("error rendering snapshot to texture: {err:?}"); + None + } + } + }) + } + } +} diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index d5735bbd..202867af 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -190,6 +190,9 @@ pub struct Column { /// Animation of the render offset during window swapping. move_animation: Option, + /// Width right before a resize animation on one of the windows of the column. + pub width_before_resize: Option, + /// Latest known view size for this column's workspace. view_size: Size, @@ -815,7 +818,7 @@ impl Workspace { } column.active_tile_idx = min(column.active_tile_idx, column.tiles.len() - 1); - column.update_tile_sizes(); + column.update_tile_sizes(false); window } @@ -879,16 +882,48 @@ impl Workspace { } pub fn update_window(&mut self, window: &W::Id) { - let (idx, column) = self + let (col_idx, column) = self .columns .iter_mut() .enumerate() .find(|(_, col)| col.contains(window)) .unwrap(); + let tile_idx = column + .tiles + .iter() + .position(|tile| tile.window().id() == window) + .unwrap(); + + let offset = column + .width_before_resize + .take() + .map_or(0, |prev| prev - column.width()); + column.update_window(window); - column.update_tile_sizes(); + column.update_tile_sizes(false); - if idx == self.active_column_idx + // Move other columns in tandem with resizing. + if column.tiles[tile_idx].resize_animation().is_some() && offset != 0 { + if self.active_column_idx <= col_idx { + for col in &mut self.columns[col_idx + 1..] { + col.animate_move_from_with_config( + offset, + self.options.animations.window_resize, + niri_config::Animation::default_window_resize(), + ); + } + } else { + for col in &mut self.columns[..col_idx] { + col.animate_move_from_with_config( + -offset, + self.options.animations.window_resize, + niri_config::Animation::default_window_resize(), + ); + } + } + } + + if col_idx == self.active_column_idx && !matches!(self.view_offset_adj, Some(ViewOffsetAdjustment::Gesture(_))) { // We might need to move the view to ensure the resized window is still visible. @@ -896,7 +931,7 @@ impl Workspace { // FIXME: we will want to skip the animation in some cases here to make continuously // resizing windows not look janky. - self.animate_view_offset_to_column(current_x, idx, None); + self.animate_view_offset_to_column(current_x, col_idx, None); } } @@ -1005,6 +1040,16 @@ impl Workspace { } } + pub fn prepare_for_resize_animation(&mut self, window: &W::Id) { + let column = self + .columns + .iter_mut() + .find(|col| col.contains(window)) + .unwrap(); + + column.width_before_resize = Some(column.width()); + } + #[cfg(test)] pub fn verify_invariants(&self) { assert!(self.view_size.w > 0); @@ -1265,8 +1310,9 @@ impl Workspace { // Start with the active window since it's drawn on top. let col = &self.columns[self.active_column_idx]; let tile = &col.tiles[col.active_tile_idx]; - let tile_pos = - Point::from((-self.view_offset, col.tile_y(col.active_tile_idx))) + col.render_offset(); + let tile_pos = Point::from((-self.view_offset, col.tile_y(col.active_tile_idx))) + + col.render_offset() + + tile.render_offset(); let first = iter::once((tile, tile_pos)); // Next, the rest of the tiles in the active column, since it should be drawn on top as a @@ -1280,7 +1326,9 @@ impl Workspace { return None; } - let tile_pos = Point::from((-self.view_offset, y)) + col.render_offset(); + let tile_pos = Point::from((-self.view_offset, y)) + + col.render_offset() + + tile.render_offset(); Some((tile, tile_pos)) }); @@ -1305,7 +1353,7 @@ impl Workspace { }) .flat_map(move |(col, x)| { zip(&col.tiles, col.tile_ys()).map(move |(tile, y)| { - let tile_pos = Point::from((x, y)) + col.render_offset(); + let tile_pos = Point::from((x, y)) + col.render_offset() + tile.render_offset(); (tile, tile_pos) }) }); @@ -1409,7 +1457,7 @@ impl Workspace { let window = col.tiles.remove(tile_idx).into_window(); col.heights.remove(tile_idx); col.active_tile_idx = min(col.active_tile_idx, col.tiles.len() - 1); - col.update_tile_sizes(); + col.update_tile_sizes(false); let width = col.width; let is_full_width = col.is_full_width; @@ -1744,6 +1792,7 @@ impl Column { is_full_width, is_fullscreen: false, move_animation: None, + width_before_resize: None, view_size, working_area, options, @@ -1768,7 +1817,7 @@ impl Column { self.view_size = size; self.working_area = working_area; - self.update_tile_sizes(); + self.update_tile_sizes(false); } fn update_config(&mut self, options: Rc) { @@ -1798,14 +1847,14 @@ impl Column { self.options = options; if update_sizes { - self.update_tile_sizes(); + self.update_tile_sizes(false); } } fn set_width(&mut self, width: ColumnWidth) { self.width = width; self.is_full_width = false; - self.update_tile_sizes(); + self.update_tile_sizes(true); } pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) { @@ -1840,14 +1889,27 @@ impl Column { } pub fn animate_move_from(&mut self, from_x_offset: i32) { + self.animate_move_from_with_config( + from_x_offset, + self.options.animations.window_movement, + niri_config::Animation::default_window_movement(), + ); + } + + pub fn animate_move_from_with_config( + &mut self, + from_x_offset: i32, + config: niri_config::Animation, + default: niri_config::Animation, + ) { let current_offset = self.move_animation.as_ref().map_or(0., Animation::value); self.move_animation = Some(Animation::new( f64::from(from_x_offset) + current_offset, 0., 0., - self.options.animations.window_movement, - niri_config::Animation::default_window_movement(), + config, + default, )); } @@ -1875,19 +1937,38 @@ impl Column { self.is_fullscreen = false; self.tiles.push(tile); self.heights.push(WindowHeight::Auto); - self.update_tile_sizes(); + self.update_tile_sizes(false); } fn update_window(&mut self, window: &W::Id) { - let tile = self + let (tile_idx, tile) = self .tiles .iter_mut() - .find(|tile| tile.window().id() == window) + .enumerate() + .find(|(_, tile)| tile.window().id() == window) .unwrap(); + + let height = tile.window().size().h; + let offset = tile + .window() + .animation_snapshot() + .map_or(0, |from| from.size.h - height); + tile.update_window(); + + // Move windows below in tandem with resizing. + if tile.resize_animation().is_some() && offset != 0 { + for tile in &mut self.tiles[tile_idx + 1..] { + tile.animate_move_from_with_config( + Point::from((0, offset)), + self.options.animations.window_resize, + niri_config::Animation::default_window_resize(), + ); + } + } } - fn update_tile_sizes(&mut self) { + fn update_tile_sizes(&mut self, animate: bool) { if self.is_fullscreen { self.tiles[0].request_fullscreen(self.view_size); return; @@ -2040,7 +2121,7 @@ impl Column { }; let size = Size::from((width, height)); - tile.request_tile_size(size); + tile.request_tile_size(size, animate); } } @@ -2127,7 +2208,7 @@ impl Column { fn toggle_full_width(&mut self) { self.is_full_width = !self.is_full_width; - self.update_tile_sizes(); + self.update_tile_sizes(true); } fn set_column_width(&mut self, change: SizeChange) { @@ -2226,13 +2307,13 @@ impl Column { } self.heights[self.active_tile_idx] = WindowHeight::Fixed(window_height.clamp(1, MAX_PX)); - self.update_tile_sizes(); + self.update_tile_sizes(true); } fn set_fullscreen(&mut self, is_fullscreen: bool) { assert_eq!(self.tiles.len(), 1); self.is_fullscreen = is_fullscreen; - self.update_tile_sizes(); + self.update_tile_sizes(false); } pub fn window_y(&self, tile_idx: usize) -> i32 { diff --git a/src/render_helpers/crossfade.rs b/src/render_helpers/crossfade.rs new file mode 100644 index 00000000..65f96482 --- /dev/null +++ b/src/render_helpers/crossfade.rs @@ -0,0 +1,156 @@ +use std::collections::HashMap; + +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}; +use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform}; + +use super::primary_gpu_pixel_shader_with_textures::PrimaryGpuPixelShaderWithTexturesRenderElement; +use super::renderer::AsGlesFrame; +use super::shaders::Shaders; +use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError}; + +#[derive(Debug)] +pub struct CrossfadeRenderElement(PrimaryGpuPixelShaderWithTexturesRenderElement); + +impl CrossfadeRenderElement { + #[allow(clippy::too_many_arguments)] + pub fn new( + renderer: &mut GlesRenderer, + area: Rectangle, + scale: Scale, + texture_from: (GlesTexture, Rectangle), + size_from: Size, + texture_to: (GlesTexture, Rectangle), + size_to: Size, + amount: f32, + result_alpha: f32, + ) -> Option { + let (texture_from, texture_from_geo) = texture_from; + let (texture_to, texture_to_geo) = texture_to; + + let scale_from = area.size.to_f64() / size_from.to_f64(); + let scale_to = area.size.to_f64() / size_to.to_f64(); + + let tex_from_geo = texture_from_geo.to_f64().upscale(scale_from); + let tex_to_geo = texture_to_geo.to_f64().upscale(scale_to); + let combined_geo = tex_from_geo.merge(tex_to_geo); + + let area = Rectangle::from_loc_and_size( + area.loc + combined_geo.loc.to_logical(scale).to_i32_round(), + combined_geo.size.to_logical(scale).to_i32_round(), + ); + + let tex_from_loc = (tex_from_geo.loc - combined_geo.loc) + .downscale((combined_geo.size.w, combined_geo.size.h)); + let tex_to_loc = (tex_to_geo.loc - combined_geo.loc) + .downscale((combined_geo.size.w, combined_geo.size.h)); + let tex_from_size = tex_from_geo.size / combined_geo.size; + let tex_to_size = tex_to_geo.size / combined_geo.size; + + // FIXME: cropping this element will mess up the coordinates. + Shaders::get(renderer).crossfade.clone().map(|shader| { + Self(PrimaryGpuPixelShaderWithTexturesRenderElement::new( + shader, + HashMap::from([ + (String::from("tex_from"), texture_from), + (String::from("tex_to"), texture_to), + ]), + area, + None, + result_alpha, + vec![ + Uniform::new( + "tex_from_loc", + (tex_from_loc.x as f32, tex_from_loc.y as f32), + ), + Uniform::new( + "tex_from_size", + (tex_from_size.x as f32, tex_from_size.y as f32), + ), + Uniform::new("tex_to_loc", (tex_to_loc.x as f32, tex_to_loc.y as f32)), + Uniform::new("tex_to_size", (tex_to_size.x as f32, tex_to_size.y as f32)), + Uniform::new("amount", amount), + ], + Kind::Unspecified, + )) + }) + } +} + +impl Element for CrossfadeRenderElement { + fn id(&self) -> &Id { + self.0.id() + } + + fn current_commit(&self) -> CommitCounter { + self.0.current_commit() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.0.geometry(scale) + } + + fn transform(&self) -> Transform { + self.0.transform() + } + + fn src(&self) -> Rectangle { + self.0.src() + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + self.0.damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale) -> Vec> { + self.0.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.0.alpha() + } + + fn kind(&self) -> Kind { + self.0.kind() + } +} + +impl RenderElement for CrossfadeRenderElement { + fn draw( + &self, + frame: &mut GlesFrame<'_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + ) -> Result<(), GlesError> { + RenderElement::::draw(&self.0, frame, src, dst, damage)?; + Ok(()) + } + + fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option { + self.0.underlying_storage(renderer) + } +} + +impl<'render> RenderElement> for CrossfadeRenderElement { + fn draw( + &self, + frame: &mut TtyFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + ) -> Result<(), TtyRendererError<'render>> { + let gles_frame = frame.as_gles_frame(); + RenderElement::::draw(&self.0, gles_frame, src, dst, damage)?; + Ok(()) + } + + fn underlying_storage(&self, renderer: &mut TtyRenderer<'render>) -> Option { + self.0.underlying_storage(renderer) + } +} diff --git a/src/render_helpers/mod.rs b/src/render_helpers/mod.rs index c445240d..ad3e91b8 100644 --- a/src/render_helpers/mod.rs +++ b/src/render_helpers/mod.rs @@ -17,12 +17,15 @@ use smithay::wayland::shm; use self::primary_gpu_texture::PrimaryGpuTextureRenderElement; +pub mod crossfade; pub mod gradient; pub mod offscreen; pub mod primary_gpu_pixel_shader; +pub mod primary_gpu_pixel_shader_with_textures; pub mod primary_gpu_texture; pub mod render_elements; pub mod renderer; +pub mod resources; pub mod shaders; pub mod surface; diff --git a/src/render_helpers/primary_gpu_pixel_shader_with_textures.rs b/src/render_helpers/primary_gpu_pixel_shader_with_textures.rs new file mode 100644 index 00000000..9f17cfdb --- /dev/null +++ b/src/render_helpers/primary_gpu_pixel_shader_with_textures.rs @@ -0,0 +1,423 @@ +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::rc::Rc; + +use glam::{Mat3, Vec2}; +use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage}; +use smithay::backend::renderer::gles::{ + ffi, link_program, Capability, GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform, + UniformDesc, UniformName, +}; +use smithay::backend::renderer::utils::CommitCounter; +use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; + +use super::renderer::AsGlesFrame; +use super::resources::Resources; +use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError}; + +/// Wrapper for a pixel shader from the primary GPU for rendering with the primary GPU. +/// +/// The shader accepts textures as input. +#[derive(Debug)] +pub struct PrimaryGpuPixelShaderWithTexturesRenderElement { + shader: PixelWithTexturesProgram, + textures: HashMap, + id: Id, + commit_counter: CommitCounter, + area: Rectangle, + opaque_regions: Vec>, + alpha: f32, + additional_uniforms: Vec>, + kind: Kind, +} + +#[derive(Debug, Clone)] +pub struct PixelWithTexturesProgram(Rc); + +#[derive(Debug)] +struct PixelWithTexturesProgramInner { + program: ffi::types::GLuint, + uniform_tex_matrix: ffi::types::GLint, + uniform_matrix: ffi::types::GLint, + uniform_size: ffi::types::GLint, + uniform_alpha: ffi::types::GLint, + attrib_vert: ffi::types::GLint, + attrib_vert_position: ffi::types::GLint, + additional_uniforms: HashMap, + texture_uniforms: HashMap, +} + +unsafe fn compile_program( + gl: &ffi::Gles2, + src: &str, + additional_uniforms: &[UniformName<'_>], + texture_uniforms: &[&str], + // destruction_callback_sender: Sender, +) -> Result { + let shader = src; + + let program = unsafe { link_program(gl, include_str!("shaders/texture.vert"), shader)? }; + + let vert = CStr::from_bytes_with_nul(b"vert\0").expect("NULL terminated"); + let vert_position = CStr::from_bytes_with_nul(b"vert_position\0").expect("NULL terminated"); + let matrix = CStr::from_bytes_with_nul(b"matrix\0").expect("NULL terminated"); + let tex_matrix = CStr::from_bytes_with_nul(b"tex_matrix\0").expect("NULL terminated"); + let size = CStr::from_bytes_with_nul(b"size\0").expect("NULL terminated"); + let alpha = CStr::from_bytes_with_nul(b"alpha\0").expect("NULL terminated"); + + Ok(PixelWithTexturesProgram(Rc::new( + PixelWithTexturesProgramInner { + program, + 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_size: gl + .GetUniformLocation(program, size.as_ptr() as *const ffi::types::GLchar), + uniform_alpha: gl + .GetUniformLocation(program, alpha.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), + additional_uniforms: additional_uniforms + .iter() + .map(|uniform| { + let name = + CString::new(uniform.name.as_bytes()).expect("Interior null in name"); + let location = + gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar); + ( + uniform.name.clone().into_owned(), + UniformDesc { + location, + type_: uniform.type_, + }, + ) + }) + .collect(), + texture_uniforms: texture_uniforms + .iter() + .map(|name_| { + let name = CString::new(name_.as_bytes()).expect("Interior null in name"); + let location = + gl.GetUniformLocation(program, name.as_ptr() as *const ffi::types::GLchar); + (name_.to_string(), location) + }) + .collect(), + }, + ))) +} + +impl PixelWithTexturesProgram { + pub fn compile( + renderer: &mut GlesRenderer, + src: &str, + additional_uniforms: &[UniformName<'_>], + texture_uniforms: &[&str], + ) -> Result { + renderer.with_context(move |gl| unsafe { + compile_program(gl, src, additional_uniforms, texture_uniforms) + })? + } +} + +impl PrimaryGpuPixelShaderWithTexturesRenderElement { + pub fn new( + shader: PixelWithTexturesProgram, + textures: HashMap, + area: Rectangle, + opaque_regions: Option>>, + alpha: f32, + additional_uniforms: Vec>, + kind: Kind, + ) -> Self { + Self { + shader, + textures, + id: Id::new(), + commit_counter: CommitCounter::default(), + area, + opaque_regions: opaque_regions.unwrap_or_default(), + alpha, + additional_uniforms: additional_uniforms + .into_iter() + .map(|u| u.into_owned()) + .collect(), + kind, + } + } +} + +impl Element for PrimaryGpuPixelShaderWithTexturesRenderElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.commit_counter + } + + fn src(&self) -> Rectangle { + self.area + .to_f64() + .to_buffer(1.0, Transform::Normal, &self.area.size.to_f64()) + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.area.to_physical_precise_round(scale) + } + + fn opaque_regions(&self, scale: Scale) -> Vec> { + self.opaque_regions + .iter() + .map(|region| region.to_physical_precise_round(scale)) + .collect() + } + + fn alpha(&self) -> f32 { + 1.0 + } + + fn kind(&self) -> Kind { + self.kind + } +} + +impl RenderElement for PrimaryGpuPixelShaderWithTexturesRenderElement { + fn draw( + &self, + frame: &mut GlesFrame<'_>, + _src: Rectangle, + dest: Rectangle, + damage: &[Rectangle], + ) -> Result<(), GlesError> { + let frame = frame.as_gles_frame(); + + let Some(resources) = Resources::get(frame) else { + return Ok(()); + }; + let mut resources = resources.borrow_mut(); + + let supports_instancing = frame.capabilities().contains(&Capability::Instancing); + + // prepare the vertices + resources.vertices.clear(); + if supports_instancing { + resources.vertices.extend(damage.iter().flat_map(|rect| { + let dest_size = dest.size; + + let rect_constrained_loc = rect + .loc + .constrain(Rectangle::from_extemities((0, 0), dest_size.to_point())); + let rect_clamped_size = rect.size.clamp( + (0, 0), + (dest_size.to_point() - rect_constrained_loc).to_size(), + ); + + let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size); + [ + rect.loc.x as f32, + rect.loc.y as f32, + rect.size.w as f32, + rect.size.h as f32, + ] + })); + } else { + resources.vertices.extend(damage.iter().flat_map(|rect| { + let dest_size = dest.size; + + let rect_constrained_loc = rect + .loc + .constrain(Rectangle::from_extemities((0, 0), dest_size.to_point())); + let rect_clamped_size = rect.size.clamp( + (0, 0), + (dest_size.to_point() - rect_constrained_loc).to_size(), + ); + + let rect = Rectangle::from_loc_and_size(rect_constrained_loc, rect_clamped_size); + // Add the 4 f32s per damage rectangle for each of the 6 vertices. + (0..6).flat_map(move |_| { + [ + rect.loc.x as f32, + rect.loc.y as f32, + rect.size.w as f32, + rect.size.h as f32, + ] + }) + })); + } + + if resources.vertices.is_empty() { + return Ok(()); + } + + // dest position and scale + let mut matrix = Mat3::from_translation(Vec2::new(dest.loc.x as f32, dest.loc.y as f32)); + let tex_matrix = Mat3::from_scale(Vec2::new( + (1.0f64 / dest.size.w as f64) as f32, + (1.0f64 / dest.size.h as f64) as f32, + )); + + //apply output transformation + matrix = Mat3::from_cols_array(frame.projection()) * matrix; + + let program = &self.shader.0; + + // render + frame.with_context(move |gl| -> Result<(), GlesError> { + unsafe { + for (i, texture) in self.textures.values().enumerate() { + gl.ActiveTexture(ffi::TEXTURE0 + i as u32); + gl.BindTexture(ffi::TEXTURE_2D, texture.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.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_S, + ffi::CLAMP_TO_BORDER as i32, + ); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_T, + ffi::CLAMP_TO_BORDER as i32, + ); + } + + gl.UseProgram(program.program); + + for (i, name) in self.textures.keys().enumerate() { + gl.Uniform1i(program.texture_uniforms[name], i as i32); + } + + gl.UniformMatrix3fv( + program.uniform_matrix, + 1, + ffi::FALSE, + matrix.as_ref().as_ptr(), + ); + gl.UniformMatrix3fv( + program.uniform_tex_matrix, + 1, + ffi::FALSE, + tex_matrix.as_ref().as_ptr(), + ); + gl.Uniform2f(program.uniform_size, dest.size.w as f32, dest.size.h as f32); + gl.Uniform1f(program.uniform_alpha, self.alpha); + + for uniform in &self.additional_uniforms { + let desc = + program + .additional_uniforms + .get(&*uniform.name) + .ok_or_else(|| { + GlesError::UnknownUniform(uniform.name.clone().into_owned()) + })?; + uniform.value.set(gl, desc)?; + } + + gl.EnableVertexAttribArray(program.attrib_vert as u32); + gl.BindBuffer(ffi::ARRAY_BUFFER, resources.vbos[0]); + gl.VertexAttribPointer( + program.attrib_vert as u32, + 2, + ffi::FLOAT, + ffi::FALSE, + 0, + std::ptr::null(), +