From b05e61f5c8ae61c9f3c7cc08cded53901ebbccfd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 21:07:54 +0200 Subject: Redesign `iced_wgpu` layering architecture --- core/src/rectangle.rs | 27 +- core/src/renderer.rs | 8 +- core/src/renderer/null.rs | 4 +- core/src/size.rs | 14 + core/src/transformation.rs | 6 + examples/geometry/src/main.rs | 6 +- examples/integration/src/main.rs | 35 +-- graphics/src/cached.rs | 8 +- graphics/src/geometry/cache.rs | 10 +- graphics/src/image.rs | 154 ++++++---- graphics/src/layer.rs | 47 +++ graphics/src/lib.rs | 6 +- graphics/src/mesh.rs | 90 +++++- graphics/src/renderer.rs | 4 +- graphics/src/text.rs | 57 +++- renderer/Cargo.toml | 1 - renderer/src/fallback.rs | 32 +- wgpu/Cargo.toml | 3 - wgpu/src/backend.rs | 432 -------------------------- wgpu/src/engine.rs | 79 +++++ wgpu/src/geometry.rs | 175 ++++++----- wgpu/src/image.rs | 644 --------------------------------------- wgpu/src/image/cache.rs | 107 +++++++ wgpu/src/image/mod.rs | 571 ++++++++++++++++++++++++++++++++++ wgpu/src/image/null.rs | 10 + wgpu/src/layer.rs | 617 ++++++++++++++++++------------------- wgpu/src/layer/image.rs | 30 -- wgpu/src/layer/mesh.rs | 97 ------ wgpu/src/layer/pipeline.rs | 17 -- wgpu/src/layer/text.rs | 70 ----- wgpu/src/lib.rs | 578 ++++++++++++++++++++++++++++++++++- wgpu/src/primitive.rs | 20 +- wgpu/src/quad.rs | 294 +++++++++++++----- wgpu/src/text.rs | 628 +++++++++++++++++++++++++------------- wgpu/src/triangle.rs | 396 ++++++++++++++++-------- wgpu/src/window/compositor.rs | 96 +++--- widget/src/scrollable.rs | 6 +- 37 files changed, 3056 insertions(+), 2323 deletions(-) create mode 100644 graphics/src/layer.rs delete mode 100644 wgpu/src/backend.rs create mode 100644 wgpu/src/engine.rs delete mode 100644 wgpu/src/image.rs create mode 100644 wgpu/src/image/cache.rs create mode 100644 wgpu/src/image/mod.rs create mode 100644 wgpu/src/image/null.rs delete mode 100644 wgpu/src/layer/image.rs delete mode 100644 wgpu/src/layer/mesh.rs delete mode 100644 wgpu/src/layer/pipeline.rs delete mode 100644 wgpu/src/layer/text.rs diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index c1c2eeac..45acd5ac 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -16,24 +16,29 @@ pub struct Rectangle { pub height: T, } -impl Rectangle { - /// Creates a new [`Rectangle`] with its top-left corner in the given - /// [`Point`] and with the provided [`Size`]. - pub fn new(top_left: Point, size: Size) -> Self { +impl Rectangle +where + T: Default, +{ + /// Creates a new [`Rectangle`] with its top-left corner at the origin + /// and with the provided [`Size`]. + pub fn with_size(size: Size) -> Self { Self { - x: top_left.x, - y: top_left.y, + x: T::default(), + y: T::default(), width: size.width, height: size.height, } } +} - /// Creates a new [`Rectangle`] with its top-left corner at the origin - /// and with the provided [`Size`]. - pub fn with_size(size: Size) -> Self { +impl Rectangle { + /// Creates a new [`Rectangle`] with its top-left corner in the given + /// [`Point`] and with the provided [`Size`]. + pub fn new(top_left: Point, size: Size) -> Self { Self { - x: 0.0, - y: 0.0, + x: top_left.x, + y: top_left.y, width: size.width, height: size.height, } diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 6712314e..f5ef8f68 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -9,7 +9,7 @@ use crate::{ /// A component that can be used by widgets to draw themselves on a screen. pub trait Renderer { /// Starts recording a new layer. - fn start_layer(&mut self); + fn start_layer(&mut self, bounds: Rectangle); /// Ends recording a new layer. /// @@ -20,13 +20,13 @@ pub trait Renderer { /// /// The layer will clip its contents to the provided `bounds`. fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - self.start_layer(); + self.start_layer(bounds); f(self); self.end_layer(bounds); } /// Starts recording with a new [`Transformation`]. - fn start_transformation(&mut self); + fn start_transformation(&mut self, transformation: Transformation); /// Ends recording a new layer. /// @@ -39,7 +39,7 @@ pub trait Renderer { transformation: Transformation, f: impl FnOnce(&mut Self), ) { - self.start_transformation(); + self.start_transformation(transformation); f(self); self.end_transformation(transformation); } diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 1caf71b3..f36d19aa 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -10,11 +10,11 @@ use crate::{ use std::borrow::Cow; impl Renderer for () { - fn start_layer(&mut self) {} + fn start_layer(&mut self, _bounds: Rectangle) {} fn end_layer(&mut self, _bounds: Rectangle) {} - fn start_transformation(&mut self) {} + fn start_transformation(&mut self, _transformation: Transformation) {} fn end_transformation(&mut self, _transformation: Transformation) {} diff --git a/core/src/size.rs b/core/src/size.rs index 55db759d..c2b5671a 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -99,3 +99,17 @@ where } } } + +impl std::ops::Mul for Size +where + T: std::ops::Mul + Copy, +{ + type Output = Size; + + fn mul(self, rhs: T) -> Self::Output { + Size { + width: self.width * rhs, + height: self.height * rhs, + } + } +} diff --git a/core/src/transformation.rs b/core/src/transformation.rs index b2c488b0..74183147 100644 --- a/core/src/transformation.rs +++ b/core/src/transformation.rs @@ -42,6 +42,12 @@ impl Transformation { } } +impl Default for Transformation { + fn default() -> Self { + Transformation::IDENTITY + } +} + impl Mul for Transformation { type Output = Self; diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 16cdb86f..9532a24a 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -6,7 +6,10 @@ mod rainbow { use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; use iced::mouse; - use iced::{Element, Length, Rectangle, Renderer, Size, Theme, Vector}; + use iced::{ + Element, Length, Rectangle, Renderer, Size, Theme, Transformation, + Vector, + }; #[derive(Debug, Clone, Copy, Default)] pub struct Rainbow; @@ -130,6 +133,7 @@ mod rainbow { 0, 8, 1, // L ], }, + transformation: Transformation::IDENTITY, }; renderer.with_translation( diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 9cd801b2..c292709f 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -5,12 +5,12 @@ use controls::Controls; use scene::Scene; use iced_wgpu::graphics::Viewport; -use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_wgpu::{wgpu, Engine, Renderer, Settings}; use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; use iced_winit::core::window; -use iced_winit::core::{Color, Font, Pixels, Size, Theme}; +use iced_winit::core::{Color, Size, Theme}; use iced_winit::futures; use iced_winit::runtime::program; use iced_winit::runtime::Debug; @@ -155,11 +155,8 @@ pub fn main() -> Result<(), Box> { // Initialize iced let mut debug = Debug::new(); - let mut renderer = Renderer::new( - Backend::new(&adapter, &device, &queue, Settings::default(), format), - Font::default(), - Pixels(16.0), - ); + let mut engine = Engine::new(&adapter, &device, &queue, format, None); + let mut renderer = Renderer::new(Settings::default(), &engine); let mut state = program::State::new( controls, @@ -228,19 +225,17 @@ pub fn main() -> Result<(), Box> { } // And then iced on top - renderer.with_primitives(|backend, primitive| { - backend.present( - &device, - &queue, - &mut encoder, - None, - frame.texture.format(), - &view, - primitive, - &viewport, - &debug.overlay(), - ); - }); + renderer.present( + &mut engine, + &device, + &queue, + &mut encoder, + None, + frame.texture.format(), + &view, + &viewport, + &debug.overlay(), + ); // Then we submit the work queue.submit(Some(encoder.finish())); diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs index b52f9d9d..1ba82f9f 100644 --- a/graphics/src/cached.rs +++ b/graphics/src/cached.rs @@ -5,7 +5,7 @@ use std::sync::Arc; /// A piece of data that can be cached. pub trait Cached: Sized { /// The type of cache produced. - type Cache; + type Cache: Clone; /// Loads the [`Cache`] into a proper instance. /// @@ -15,7 +15,7 @@ pub trait Cached: Sized { /// Caches this value, producing its corresponding [`Cache`]. /// /// [`Cache`]: Self::Cache - fn cache(self) -> Self::Cache; + fn cache(self, previous: Option) -> Self::Cache; } impl Cached for Primitive { @@ -27,7 +27,7 @@ impl Cached for Primitive { } } - fn cache(self) -> Arc { + fn cache(self, _previous: Option>) -> Arc { Arc::new(self) } } @@ -38,5 +38,5 @@ impl Cached for () { fn load(_cache: &Self::Cache) -> Self {} - fn cache(self) -> Self::Cache {} + fn cache(self, _previous: Option) -> Self::Cache {} } diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs index 37d433c2..ebbafd14 100644 --- a/graphics/src/geometry/cache.rs +++ b/graphics/src/geometry/cache.rs @@ -49,7 +49,7 @@ where ) -> Renderer::Geometry { use std::ops::Deref; - if let State::Filled { + let previous = if let State::Filled { bounds: cached_bounds, geometry, } = self.state.borrow().deref() @@ -57,12 +57,16 @@ where if *cached_bounds == bounds { return Cached::load(geometry); } - } + + Some(geometry.clone()) + } else { + None + }; let mut frame = Frame::new(renderer, bounds); draw_fn(&mut frame); - let geometry = frame.into_geometry().cache(); + let geometry = frame.into_geometry().cache(previous); let result = Cached::load(&geometry); *self.state.borrow_mut() = State::Filled { bounds, geometry }; diff --git a/graphics/src/image.rs b/graphics/src/image.rs index d89caace..e8626717 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -1,14 +1,94 @@ //! Load and operate on images. -use crate::core::image::{Data, Handle}; +#[cfg(feature = "image")] +pub use ::image as image_rs; -use bitflags::bitflags; +use crate::core::image; +use crate::core::svg; +use crate::core::{Color, Rectangle}; -pub use ::image as image_rs; +/// A raster or vector image. +#[derive(Debug, Clone)] +pub enum Image { + /// A raster image. + Raster { + /// The handle of a raster image. + handle: image::Handle, + + /// The filter method of a raster image. + filter_method: image::FilterMethod, + + /// The bounds of the image. + bounds: Rectangle, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The [`Color`] filter + color: Option, + /// The bounds of the image. + bounds: Rectangle, + }, +} + +#[cfg(feature = "image")] /// Tries to load an image by its [`Handle`]. -pub fn load(handle: &Handle) -> image_rs::ImageResult { +pub fn load( + handle: &image::Handle, +) -> ::image::ImageResult<::image::DynamicImage> { + use bitflags::bitflags; + + bitflags! { + struct Operation: u8 { + const FLIP_HORIZONTALLY = 0b001; + const ROTATE_180 = 0b010; + const FLIP_DIAGONALLY = 0b100; + } + } + + impl Operation { + // Meaning of the returned value is described e.g. at: + // https://magnushoff.com/articles/jpeg-orientation/ + fn from_exif(reader: &mut R) -> Result + where + R: std::io::BufRead + std::io::Seek, + { + let exif = exif::Reader::new().read_from_container(reader)?; + + Ok(exif + .get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|field| field.value.get_uint(0)) + .and_then(|value| u8::try_from(value).ok()) + .and_then(|value| Self::from_bits(value.saturating_sub(1))) + .unwrap_or_else(Self::empty)) + } + + fn perform( + self, + mut image: ::image::DynamicImage, + ) -> ::image::DynamicImage { + use ::image::imageops; + + if self.contains(Self::FLIP_DIAGONALLY) { + imageops::flip_vertical_in_place(&mut image); + } + + if self.contains(Self::ROTATE_180) { + imageops::rotate180_in_place(&mut image); + } + + if self.contains(Self::FLIP_HORIZONTALLY) { + imageops::flip_horizontal_in_place(&mut image); + } + + image + } + } + match handle.data() { - Data::Path(path) => { + image::Data::Path(path) => { let image = ::image::open(path)?; let operation = std::fs::File::open(path) @@ -19,7 +99,7 @@ pub fn load(handle: &Handle) -> image_rs::ImageResult { Ok(operation.perform(image)) } - Data::Bytes(bytes) => { + image::Data::Bytes(bytes) => { let image = ::image::load_from_memory(bytes)?; let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) @@ -28,68 +108,22 @@ pub fn load(handle: &Handle) -> image_rs::ImageResult { Ok(operation.perform(image)) } - Data::Rgba { + image::Data::Rgba { width, height, pixels, } => { - if let Some(image) = image_rs::ImageBuffer::from_vec( - *width, - *height, - pixels.to_vec(), - ) { - Ok(image_rs::DynamicImage::ImageRgba8(image)) + if let Some(image) = + ::image::ImageBuffer::from_vec(*width, *height, pixels.to_vec()) + { + Ok(::image::DynamicImage::ImageRgba8(image)) } else { - Err(image_rs::error::ImageError::Limits( - image_rs::error::LimitError::from_kind( - image_rs::error::LimitErrorKind::DimensionError, + Err(::image::error::ImageError::Limits( + ::image::error::LimitError::from_kind( + ::image::error::LimitErrorKind::DimensionError, ), )) } } } } - -bitflags! { - struct Operation: u8 { - const FLIP_HORIZONTALLY = 0b001; - const ROTATE_180 = 0b010; - const FLIP_DIAGONALLY = 0b100; - } -} - -impl Operation { - // Meaning of the returned value is described e.g. at: - // https://magnushoff.com/articles/jpeg-orientation/ - fn from_exif(reader: &mut R) -> Result - where - R: std::io::BufRead + std::io::Seek, - { - let exif = exif::Reader::new().read_from_container(reader)?; - - Ok(exif - .get_field(exif::Tag::Orientation, exif::In::PRIMARY) - .and_then(|field| field.value.get_uint(0)) - .and_then(|value| u8::try_from(value).ok()) - .and_then(|value| Self::from_bits(value.saturating_sub(1))) - .unwrap_or_else(Self::empty)) - } - - fn perform(self, mut image: image::DynamicImage) -> image::DynamicImage { - use image::imageops; - - if self.contains(Self::FLIP_DIAGONALLY) { - imageops::flip_vertical_in_place(&mut image); - } - - if self.contains(Self::ROTATE_180) { - imageops::rotate180_in_place(&mut image); - } - - if self.contains(Self::FLIP_HORIZONTALLY) { - imageops::flip_horizontal_in_place(&mut image); - } - - image - } -} diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs new file mode 100644 index 00000000..5b8aacab --- /dev/null +++ b/graphics/src/layer.rs @@ -0,0 +1,47 @@ +pub trait Layer { + type Cache; + + fn new() -> Self; + + fn clear(&mut self); +} + +pub struct Recorder { + layers: Vec, + caches: Vec, + stack: Vec, + current: usize, +} + +enum Kind { + Fresh(usize), + Cache(usize), +} + +impl Recorder { + pub fn new() -> Self { + Self { + layers: vec![Layer::new()], + caches: Vec::new(), + stack: Vec::new(), + current: 0, + } + } + + pub fn fill_quad(&mut self) {} + + pub fn push_cache(&mut self, cache: T::Cache) { + self.caches.push(cache); + } + + pub fn clear(&mut self) { + self.caches.clear(); + self.stack.clear(); + + for mut layer in self.layers { + layer.clear(); + } + + self.current = 0; + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index d7f2f439..5857aea5 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -28,6 +28,7 @@ pub mod compositor; pub mod damage; pub mod error; pub mod gradient; +pub mod image; pub mod mesh; pub mod renderer; pub mod text; @@ -35,9 +36,6 @@ pub mod text; #[cfg(feature = "geometry")] pub mod geometry; -#[cfg(feature = "image")] -pub mod image; - pub use antialiasing::Antialiasing; pub use backend::Backend; pub use cached::Cached; @@ -45,10 +43,12 @@ pub use compositor::Compositor; pub use damage::Damage; pub use error::Error; pub use gradient::Gradient; +pub use image::Image; pub use mesh::Mesh; pub use primitive::Primitive; pub use renderer::Renderer; pub use settings::Settings; +pub use text::Text; pub use viewport::Viewport; pub use iced_core as core; diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs index 20692b07..d3e7ffaf 100644 --- a/graphics/src/mesh.rs +++ b/graphics/src/mesh.rs @@ -1,8 +1,7 @@ //! Draw triangles! use crate::color; -use crate::core::{Rectangle, Size}; +use crate::core::{Rectangle, Size, Transformation}; use crate::gradient; -use crate::Damage; use bytemuck::{Pod, Zeroable}; @@ -14,9 +13,10 @@ pub enum Mesh { /// The vertices and indices of the mesh. buffers: Indexed, - /// The size of the drawable region of the mesh. - /// - /// Any geometry that falls out of this region will be clipped. + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The [`Size`] of the [`Mesh`]. size: Size, }, /// A mesh with a gradient. @@ -24,19 +24,44 @@ pub enum Mesh { /// The vertices and indices of the mesh. buffers: Indexed, - /// The size of the drawable region of the mesh. - /// - /// Any geometry that falls out of this region will be clipped. + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The [`Size`] of the [`Mesh`]. size: Size, }, } -impl Damage for Mesh { - fn bounds(&self) -> Rectangle { +impl Mesh { + /// Returns the indices of the [`Mesh`]. + pub fn indices(&self) -> &[u32] { + match self { + Self::Solid { buffers, .. } => &buffers.indices, + Self::Gradient { buffers, .. } => &buffers.indices, + } + } + + /// Returns the [`Transformation`] of the [`Mesh`]. + pub fn transformation(&self) -> Transformation { + match self { + Self::Solid { transformation, .. } + | Self::Gradient { transformation, .. } => *transformation, + } + } + + /// Returns the clip bounds of the [`Mesh`]. + pub fn clip_bounds(&self) -> Rectangle { match self { - Self::Solid { size, .. } | Self::Gradient { size, .. } => { - Rectangle::with_size(*size) + Self::Solid { + size, + transformation, + .. } + | Self::Gradient { + size, + transformation, + .. + } => Rectangle::with_size(*size) * *transformation, } } } @@ -75,6 +100,47 @@ pub struct GradientVertex2D { pub gradient: gradient::Packed, } +/// The result of counting the attributes of a set of meshes. +#[derive(Debug, Clone, Copy, Default)] +pub struct AttributeCount { + /// The total amount of solid vertices. + pub solid_vertices: usize, + + /// The total amount of solid meshes. + pub solids: usize, + + /// The total amount of gradient vertices. + pub gradient_vertices: usize, + + /// The total amount of gradient meshes. + pub gradients: usize, + + /// The total amount of indices. + pub indices: usize, +} + +/// Returns the number of total vertices & total indices of all [`Mesh`]es. +pub fn attribute_count_of(meshes: &[Mesh]) -> AttributeCount { + meshes + .iter() + .fold(AttributeCount::default(), |mut count, mesh| { + match mesh { + Mesh::Solid { buffers, .. } => { + count.solids += 1; + count.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradients += 1; + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} + /// A renderer capable of drawing a [`Mesh`]. pub trait Renderer { /// Draws the given [`Mesh`]. diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index fb1a0d73..d4f91dab 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -62,7 +62,7 @@ impl Renderer { } impl iced_core::Renderer for Renderer { - fn start_layer(&mut self) { + fn start_layer(&mut self, _bounds: Rectangle) { self.stack.push(std::mem::take(&mut self.primitives)); } @@ -75,7 +75,7 @@ impl iced_core::Renderer for Renderer { self.primitives.push(Primitive::group(layer).clip(bounds)); } - fn start_transformation(&mut self) { + fn start_transformation(&mut self, _transformation: Transformation) { self.stack.push(std::mem::take(&mut self.primitives)); } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 0310ead7..c9c821c0 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -9,14 +9,67 @@ pub use paragraph::Paragraph; pub use cosmic_text; +use crate::core::alignment; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; -use crate::core::{Color, Point, Rectangle, Size}; +use crate::core::text::{LineHeight, Shaping}; +use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; use std::borrow::Cow; use std::sync::{Arc, RwLock, Weak}; +/// A text primitive. +#[derive(Debug, Clone)] +pub enum Text { + /// A paragraph. + #[allow(missing_docs)] + Paragraph { + paragraph: paragraph::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// An editor. + #[allow(missing_docs)] + Editor { + editor: editor::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// Some cached text. + Cached { + /// The contents of the text. + content: String, + /// The bounds of the text. + bounds: Rectangle, + /// The color of the text. + color: Color, + /// The size of the text in logical pixels. + size: Pixels, + /// The line height of the text. + line_height: LineHeight, + /// The font of the text. + font: Font, + /// The horizontal alignment of the text. + horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the text. + vertical_alignment: alignment::Vertical, + /// The shaping strategy of the text. + shaping: Shaping, + /// The clip bounds of the text. + clip_bounds: Rectangle, + }, + /// Some raw text. + #[allow(missing_docs)] + Raw { + raw: Raw, + transformation: Transformation, + }, +} + /// The regular variant of the [Fira Sans] font. /// /// It is loaded as part of the default fonts in Wasm builds. diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 39c19fa3..946a8ebb 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -16,7 +16,6 @@ tiny-skia = ["iced_tiny_skia"] image = ["iced_tiny_skia?/image", "iced_wgpu?/image"] svg = ["iced_tiny_skia?/svg", "iced_wgpu?/svg"] geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/geometry"] -tracing = ["iced_wgpu?/tracing"] web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] fira-sans = ["iced_graphics/fira-sans"] diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index b9ceb4b2..b6459243 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -39,16 +39,20 @@ where delegate!(self, renderer, renderer.clear()); } - fn start_layer(&mut self) { - delegate!(self, renderer, renderer.start_layer()); + fn start_layer(&mut self, bounds: Rectangle) { + delegate!(self, renderer, renderer.start_layer(bounds)); } fn end_layer(&mut self, bounds: Rectangle) { delegate!(self, renderer, renderer.end_layer(bounds)); } - fn start_transformation(&mut self) { - delegate!(self, renderer, renderer.start_transformation()); + fn start_transformation(&mut self, transformation: Transformation) { + delegate!( + self, + renderer, + renderer.start_transformation(transformation) + ); } fn end_transformation(&mut self, transformation: Transformation) { @@ -433,6 +437,7 @@ mod geometry { } } + #[derive(Clone)] pub enum Geometry { Left(L), Right(R), @@ -452,10 +457,21 @@ mod geometry { } } - fn cache(self) -> Self::Cache { - match self { - Self::Left(geometry) => Geometry::Left(geometry.cache()), - Self::Right(geometry) => Geometry::Right(geometry.cache()), + fn cache(self, previous: Option) -> Self::Cache { + match (self, previous) { + (Self::Left(geometry), Some(Geometry::Left(previous))) => { + Geometry::Left(geometry.cache(Some(previous))) + } + (Self::Left(geometry), None) => { + Geometry::Left(geometry.cache(None)) + } + (Self::Right(geometry), Some(Geometry::Right(previous))) => { + Geometry::Right(geometry.cache(Some(previous))) + } + (Self::Right(geometry), None) => { + Geometry::Right(geometry.cache(None)) + } + _ => unreachable!(), } } } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 0b713784..75be53b5 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -41,6 +41,3 @@ lyon.optional = true resvg.workspace = true resvg.optional = true - -tracing.workspace = true -tracing.optional = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs deleted file mode 100644 index 6ccf4111..00000000 --- a/wgpu/src/backend.rs +++ /dev/null @@ -1,432 +0,0 @@ -use crate::buffer; -use crate::core::{Color, Size, Transformation}; -use crate::graphics::backend; -use crate::graphics::color; -use crate::graphics::Viewport; -use crate::primitive::pipeline; -use crate::primitive::{self, Primitive}; -use crate::quad; -use crate::text; -use crate::triangle; -use crate::window; -use crate::{Layer, Settings}; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -#[cfg(any(feature = "image", feature = "svg"))] -use crate::image; - -use std::borrow::Cow; - -/// A [`wgpu`] graphics backend for [`iced`]. -/// -/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -/// [`iced`]: https://github.com/iced-rs/iced -#[allow(missing_debug_implementations)] -pub struct Backend { - quad_pipeline: quad::Pipeline, - text_pipeline: text::Pipeline, - triangle_pipeline: triangle::Pipeline, - pipeline_storage: pipeline::Storage, - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline: image::Pipeline, - staging_belt: wgpu::util::StagingBelt, -} - -impl Backend { - /// Creates a new [`Backend`]. - pub fn new( - _adapter: &wgpu::Adapter, - device: &wgpu::Device, - queue: &wgpu::Queue, - settings: Settings, - format: wgpu::TextureFormat, - ) -> Self { - let text_pipeline = text::Pipeline::new(device, queue, format); - let quad_pipeline = quad::Pipeline::new(device, format); - let triangle_pipeline = - triangle::Pipeline::new(device, format, settings.antialiasing); - - #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = { - let backend = _adapter.get_info().backend; - - image::Pipeline::new(device, format, backend) - }; - - Self { - quad_pipeline, - text_pipeline, - triangle_pipeline, - pipeline_storage: pipeline::Storage::default(), - - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, - - // TODO: Resize belt smartly (?) - // It would be great if the `StagingBelt` API exposed methods - // for introspection to detect when a resize may be worth it. - staging_belt: wgpu::util::StagingBelt::new( - buffer::MAX_WRITE_SIZE as u64, - ), - } - } - - /// Draws the provided primitives in the given `TextureView`. - /// - /// The text provided as overlay will be rendered on top of the primitives. - /// This is useful for rendering debug information. - pub fn present>( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - clear_color: Option, - format: wgpu::TextureFormat, - frame: &wgpu::TextureView, - primitives: &[Primitive], - viewport: &Viewport, - overlay_text: &[T], - ) { - log::debug!("Drawing"); - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Backend", "PRESENT").entered(); - - let target_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; - let transformation = viewport.projection(); - - let mut layers = Layer::generate(primitives, viewport); - - if !overlay_text.is_empty() { - layers.push(Layer::overlay(overlay_text, viewport)); - } - - self.prepare( - device, - queue, - format, - encoder, - scale_factor, - target_size, - transformation, - &layers, - ); - - self.staging_belt.finish(); - - self.render( - device, - encoder, - frame, - clear_color, - scale_factor, - target_size, - &layers, - ); - - self.quad_pipeline.end_frame(); - self.text_pipeline.end_frame(); - self.triangle_pipeline.end_frame(); - - #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.end_frame(); - } - - /// Recalls staging memory for future uploads. - /// - /// This method should be called after the command encoder - /// has been submitted. - pub fn recall(&mut self) { - self.staging_belt.recall(); - } - - fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - encoder: &mut wgpu::CommandEncoder, - scale_factor: f32, - target_size: Size, - transformation: Transformation, - layers: &[Layer<'_>], - ) { - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.prepare( - device, - encoder, - &mut self.staging_belt, - &layer.quads, - transformation, - scale_factor, - ); - } - - if !layer.meshes.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.triangle_pipeline.prepare( - device, - encoder, - &mut self.staging_belt, - &layer.meshes, - scaled, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.image_pipeline.prepare( - device, - encoder, - &mut self.staging_belt, - &layer.images, - scaled, - scale_factor, - ); - } - } - - if !layer.text.is_empty() { - self.text_pipeline.prepare( - device, - queue, - encoder, - &layer.text, - layer.bounds, - scale_factor, - target_size, - ); - } - - if !layer.pipelines.is_empty() { - for pipeline in &layer.pipelines { - pipeline.primitive.prepare( - format, - device, - queue, - pipeline.bounds, - target_size, - scale_factor, - &mut self.pipeline_storage, - ); - } - } - } - } - - fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - clear_color: Option, - scale_factor: f32, - target_size: Size, - layers: &[Layer<'_>], - ) { - use std::mem::ManuallyDrop; - - let mut quad_layer = 0; - let mut triangle_layer = 0; - #[cfg(any(feature = "image", feature = "svg"))] - let mut image_layer = 0; - let mut text_layer = 0; - - let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: match clear_color { - Some(background_color) => wgpu::LoadOp::Clear({ - let [r, g, b, a] = - color::pack(background_color).components(); - - wgpu::Color { - r: f64::from(r), - g: f64::from(g), - b: f64::from(b), - a: f64::from(a), - } - }), - None => wgpu::LoadOp::Load, - }, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.render( - quad_layer, - bounds, - &layer.quads, - &mut render_pass, - ); - - quad_layer += 1; - } - - if !layer.meshes.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - self.triangle_pipeline.render( - device, - encoder, - target, - triangle_layer, - target_size, - &layer.meshes, - scale_factor, - ); - - triangle_layer += 1; - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - self.image_pipeline.render( - image_layer, - bounds, - &mut render_pass, - ); - - image_layer += 1; - } - } - - if !layer.text.is_empty() { - self.text_pipeline - .render(text_layer, bounds, &mut render_pass); - - text_layer += 1; - } - - if !layer.pipelines.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - for pipeline in &layer.pipelines { - let viewport = (pipeline.viewport * scale_factor).snap(); - - if viewport.width < 1 || viewport.height < 1 { - continue; - } - - pipeline.primitive.render( - &self.pipeline_storage, - target, - target_size, - viewport, - encoder, - ); - } - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - } - - let _ = ManuallyDrop::into_inner(render_pass); - } -} - -impl backend::Backend for Backend { - type Primitive = primitive::Custom; - type Compositor = window::Compositor; -} - -impl backend::Text for Backend { - fn load_font(&mut self, font: Cow<'static, [u8]>) { - self.text_pipeline.load_font(font); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size { - self.image_pipeline.dimensions(handle) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> Size { - self.image_pipeline.viewport_dimensions(handle) - } -} - -#[cfg(feature = "geometry")] -impl crate::graphics::geometry::Backend for Backend { - type Frame = crate::geometry::Frame; - - fn new_frame(&self, size: Size) -> Self::Frame { - crate::geometry::Frame::new(size) - } -} diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs new file mode 100644 index 00000000..e45b62b2 --- /dev/null +++ b/wgpu/src/engine.rs @@ -0,0 +1,79 @@ +use crate::buffer; +use crate::graphics::Antialiasing; +use crate::primitive::pipeline; +use crate::quad; +use crate::text; +use crate::triangle; + +#[allow(missing_debug_implementations)] +pub struct Engine { + pub(crate) quad_pipeline: quad::Pipeline, + pub(crate) text_pipeline: text::Pipeline, + pub(crate) triangle_pipeline: triangle::Pipeline, + pub(crate) _pipeline_storage: pipeline::Storage, + #[cfg(any(feature = "image", feature = "svg"))] + pub(crate) image_pipeline: crate::image::Pipeline, + pub(crate) staging_belt: wgpu::util::StagingBelt, +} + +impl Engine { + pub fn new( + _adapter: &wgpu::Adapter, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + antialiasing: Option, // TODO: Initialize AA pipelines lazily + ) -> Self { + let text_pipeline = text::Pipeline::new(device, queue, format); + let quad_pipeline = quad::Pipeline::new(device, format); + let triangle_pipeline = + triangle::Pipeline::new(device, format, antialiasing); + + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = { + let backend = _adapter.get_info().backend; + + crate::image::Pipeline::new(device, format, backend) + }; + + Self { + // TODO: Resize belt smartly (?) + // It would be great if the `StagingBelt` API exposed methods + // for introspection to detect when a resize may be worth it. + staging_belt: wgpu::util::StagingBelt::new( + buffer::MAX_WRITE_SIZE as u64, + ), + quad_pipeline, + text_pipeline, + triangle_pipeline, + _pipeline_storage: pipeline::Storage::default(), + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, + } + } + + #[cfg(any(feature = "image", feature = "svg"))] + pub fn image_cache(&self) -> &crate::image::cache::Shared { + self.image_pipeline.cache() + } + + pub fn submit( + &mut self, + queue: &wgpu::Queue, + encoder: wgpu::CommandEncoder, + ) -> wgpu::SubmissionIndex { + self.staging_belt.finish(); + let index = queue.submit(Some(encoder.finish())); + self.staging_belt.recall(); + + self.quad_pipeline.end_frame(); + self.text_pipeline.end_frame(); + self.triangle_pipeline.end_frame(); + + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.end_frame(); + + index + } +} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index ba56c59d..7c8c0a35 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -10,31 +10,82 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::primitive::{self, Primitive}; +use crate::graphics::{self, Cached}; +use crate::layer; +use crate::text; use lyon::geom::euclid; use lyon::tessellation; use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; /// A frame for drawing some geometry. #[allow(missing_debug_implementations)] pub struct Frame { size: Size, buffers: BufferStack, - primitives: Vec, + layers: Vec, + text: text::Batch, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, stroke_tessellator: tessellation::StrokeTessellator, } +pub enum Geometry { + Live(Vec), + Cached(Rc<[Rc>]>), +} + +impl Cached for Geometry { + type Cache = Rc<[Rc>]>; + + fn load(cache: &Self::Cache) -> Self { + Geometry::Cached(cache.clone()) + } + + fn cache(self, previous: Option) -> Self::Cache { + match self { + Self::Live(live) => { + let mut layers = live.into_iter(); + + let mut new: Vec<_> = previous + .map(|previous| { + previous + .iter() + .cloned() + .zip(layers.by_ref()) + .map(|(cached, live)| { + cached.borrow_mut().update(live); + cached + }) + .collect() + }) + .unwrap_or_default(); + + new.extend( + layers + .map(layer::Live::into_cached) + .map(RefCell::new) + .map(Rc::new), + ); + + Rc::from(new) + } + Self::Cached(cache) => cache, + } + } +} + impl Frame { /// Creates a new [`Frame`] with the given [`Size`]. pub fn new(size: Size) -> Frame { Frame { size, buffers: BufferStack::new(), - primitives: Vec::new(), + layers: Vec::new(), + text: text::Batch::new(), transforms: Transforms { previous: Vec::new(), current: Transform(lyon::math::Transform::identity()), @@ -44,49 +95,54 @@ impl Frame { } } - fn into_primitives(mut self) -> Vec { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } - Buffer::Gradient(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } - } + fn into_layers(mut self) -> Vec { + if !self.text.is_empty() || !self.buffers.stack.is_empty() { + let clip_bounds = Rectangle::with_size(self.size); + let transformation = Transformation::IDENTITY; + + // TODO: Generate different meshes for different transformations (?) + // Instead of transforming each path + let meshes = self + .buffers + .stack + .into_iter() + .map(|buffer| match buffer { + Buffer::Solid(buffer) => Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + transformation: Transformation::IDENTITY, + size: self.size, + }, + Buffer::Gradient(buffer) => Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + transformation: Transformation::IDENTITY, + size: self.size, + }, + }) + .collect(); + + let layer = layer::Live { + bounds: Some(clip_bounds), + transformation, + meshes, + text: self.text, + ..layer::Live::default() + }; + + self.layers.push(layer); } - self.primitives + self.layers } } impl geometry::frame::Backend for Frame { - type Geometry = Primitive; - - /// Creates a new empty [`Frame`] with the given dimensions. - /// - /// The default coordinate system of a [`Frame`] has its origin at the - /// top-left corner of its bounds. + type Geometry = Geometry; #[inline] fn width(&self) -> f32 { @@ -246,8 +302,7 @@ impl geometry::frame::Backend for Frame { height: f32::INFINITY, }; - // TODO: Honor layering! - self.primitives.push(Primitive::Text { + self.text.push(graphics::Text::Cached { content: text.content, bounds, color: text.color, @@ -313,37 +368,17 @@ impl geometry::frame::Backend for Frame { } fn paste(&mut self, frame: Frame, at: Point) { - let size = frame.size(); - let primitives = frame.into_primitives(); - let transformation = Transformation::translate(at.x, at.y); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Transform { - transformation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Transform { - transformation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); + let translation = Transformation::translate(at.x, at.y); + + self.layers + .extend(frame.into_layers().into_iter().map(|mut layer| { + layer.transformation = layer.transformation * translation; + layer + })); } fn into_geometry(self) -> Self::Geometry { - Primitive::Group { - primitives: self.into_primitives(), - } + Geometry::Live(self.into_layers()) } } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs deleted file mode 100644 index d0bf1182..00000000 --- a/wgpu/src/image.rs +++ /dev/null @@ -1,644 +0,0 @@ -mod atlas; - -#[cfg(feature = "image")] -mod raster; - -#[cfg(feature = "svg")] -mod vector; - -use atlas::Atlas; - -use crate::core::{Rectangle, Size, Transformation}; -use crate::layer; -use crate::Buffer; - -use std::cell::RefCell; -use std::mem; - -use bytemuck::{Pod, Zeroable}; - -#[cfg(feature = "image")] -use crate::core::image; - -#[cfg(feature = "svg")] -use crate::core::svg; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -#[derive(Debug)] -pub struct Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell, - #[cfg(feature = "svg")] - vector_cache: RefCell, - - pipeline: wgpu::RenderPipeline, - nearest_sampler: wgpu::Sampler, - linear_sampler: wgpu::Sampler, - texture: wgpu::BindGroup, - texture_version: usize, - texture_atlas: Atlas, - texture_layout: wgpu::BindGroupLayout, - constant_layout: wgpu::BindGroupLayout, - - layers: Vec, - prepare_layer: usize, -} - -#[derive(Debug)] -struct Layer { - uniforms: wgpu::Buffer, - nearest: Data, - linear: Data, -} - -impl Layer { - fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - nearest_sampler: &wgpu::Sampler, - linear_sampler: &wgpu::Sampler, - ) -> Self { - let uniforms = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::image uniforms buffer"), - size: mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let nearest = - Data::new(device, constant_layout, nearest_sampler, &uniforms); - - let linear = - Data::new(device, constant_layout, linear_sampler, &uniforms); - - Self { - uniforms, - nearest, - linear, - } - } - - fn prepare( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - nearest_instances: &[Instance], - linear_instances: &[Instance], - transformation: Transformation, - ) { - let uniforms = Uniforms { - transform: transformation.into(), - }; - - let bytes = bytemuck::bytes_of(&uniforms); - - belt.write_buffer( - encoder, - &self.uniforms, - 0, - (bytes.len() as u64).try_into().expect("Sized uniforms"), - device, - ) - .copy_from_slice(bytes); - - self.nearest - .upload(device, encoder, belt, nearest_instances); - - self.linear.upload(device, encoder, belt, linear_instances); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - self.nearest.render(render_pass); - self.linear.render(render_pass); - } -} - -#[derive(Debug)] -struct Data { - constants: wgpu::BindGroup, - instances: Buffer, - instance_count: usize, -} - -impl Data { - pub fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - sampler: &wgpu::Sampler, - uniforms: &wgpu::Buffer, - ) -> Self { - let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image constants bind group"), - layout: constant_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: uniforms, - offset: 0, - size: None, - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(sampler), - }, - ], - }); - - let instances = Buffer::new( - device, - "iced_wgpu::image instance buffer", - Instance::INITIAL, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - Self { - constants, - instances, - instance_count: 0, - } - } - - fn upload( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - instances: &[Instance], - ) { - self.instance_count = instances.len(); - - if self.instance_count == 0 { - return; - } - - let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(device, encoder, belt, 0, instances); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - if self.instance_count == 0 { - return; - } - - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_vertex_buffer(0, self.instances.slice(..)); - - render_pass.draw(0..6, 0..self.instance_count as u32); - } -} - -impl Pipeline { - pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - backend: wgpu::Backend, - ) -> Self { - let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - min_filter: wgpu::FilterMode::Nearest, - mag_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - - let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - min_filter: wgpu::FilterMode::Linear, - mag_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, - ..Default::default() - }); - - let constant_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("iced_wgpu::image constants layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: wgpu::BufferSize::new( - mem::size_of::() as u64, - ), - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::Filtering, - ), - count: None, - }, - ], - }); - - let texture_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("iced_wgpu::image texture atlas layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { - filterable: true, - }, - view_dimension: wgpu::TextureViewDimension::D2Array, - multisampled: false, - }, - count: None, - }], - }); - - let layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("iced_wgpu::image pipeline layout"), - push_constant_ranges: &[], - bind_group_layouts: &[&constant_layout, &texture_layout], - }); - - let shader = - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("iced_wgpu image shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( - concat!( - include_str!("shader/vertex.wgsl"), - "\n", - include_str!("shader/image.wgsl"), - ), - )), - }); - - let pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("iced_wgpu::image pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as u64, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &wgpu::vertex_attr_array!( - // Position - 0 => Float32x2, - // Scale - 1 => Float32x2, - // Atlas position - 2 => Float32x2, - // Atlas scale - 3 => Float32x2, - // Layer - 4 => Sint32, - ), - }], - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - front_face: wgpu::FrontFace::Cw, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - }); - - let texture_atlas = Atlas::new(device, backend); - - let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - texture_atlas.view(), - ), - }], - }); - - Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell::new(raster::Cache::default()), - - #[cfg(feature = "svg")] - vector_cache: RefCell::new(vector::Cache::default()), - - pipeline, - nearest_sampler, - linear_sampler, - texture, - texture_version: texture_atlas.layer_count(), - texture_atlas, - texture_layout, - constant_layout, - - layers: Vec::new(), - prepare_layer: 0, - } - } - - #[cfg(feature = "image")] - pub fn dimensions(&self, handle: &image::Handle) -> Size { - let mut cache = self.raster_cache.borrow_mut(); - let memory = cache.load(handle); - - memory.dimensions() - } - - #[cfg(feature = "svg")] - pub fn viewport_dimensions(&self, handle: &svg::Handle) -> Size { - let mut cache = self.vector_cache.borrow_mut(); - let svg = cache.load(handle); - - svg.viewport_dimensions() - } - - pub fn prepare( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - images: &[layer::Image], - transformation: Transformation, - _scale: f32, - ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "PREPARE").entered(); - - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "DRAW").entered(); - - let nearest_instances: &mut Vec = &mut Vec::new(); - let linear_instances: &mut Vec = &mut Vec::new(); - - #[cfg(feature = "image")] - let mut raster_cache = self.raster_cache.borrow_mut(); - - #[cfg(feature = "svg")] - let mut vector_cache = self.vector_cache.borrow_mut(); - - for image in images { - match &image { - #[cfg(feature = "image")] - layer::Image::Raster { - handle, - filter_method, - bounds, - } => { - if let Some(atlas_entry) = raster_cache.upload( - device, - encoder, - handle, - &mut self.texture_atlas, - ) { - add_instances( - [bounds.x, bounds.y], - [bounds.width, bounds.height], - atlas_entry, - match filter_method { - image::FilterMethod::Nearest => { - nearest_instances - } - image::FilterMethod::Linear => linear_instances, - }, - ); - } - } - #[cfg(not(feature = "image"))] - layer::Image::Raster { .. } => {} - - #[cfg(feature = "svg")] - layer::Image::Vector { - handle, - color, - bounds, - } => { - let size = [bounds.width, bounds.height]; - - if let Some(atlas_entry) = vector_cache.upload( - device, - encoder, - handle, - *color, - size, - _scale, - &mut self.texture_atlas, - ) { - add_instances( - [bounds.x, bounds.y], - size, - atlas_entry, - nearest_instances, - ); - } - } - #[cfg(not(feature = "svg"))] - layer::Image::Vector { .. } => {} - } - } - - if nearest_instances.is_empty() && linear_instances.is_empty() { - return; - } - - let texture_version = self.texture_atlas.layer_count(); - - if self.texture_version != texture_version { - log::info!("Atlas has grown. Recreating bind group..."); - - self.texture = - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &self.texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - self.texture_atlas.view(), - ), - }], - }); - - self.texture_version = texture_version; - } - - if self.layers.len() <= self.prepare_layer { - self.layers.push(Layer::new( - device, - &self.constant_layout, - &self.nearest_sampler, - &self.linear_sampler, - )); - } - - let layer = &mut self.layers[self.prepare_layer]; - - layer.prepare( - device, - encoder, - belt, - nearest_instances, - linear_instances, - transformation, - ); - - self.prepare_layer += 1; - } - - pub fn render<'a>( - &'a self, - layer: usize, - bounds: Rectangle, - render_pass: &mut wgpu::RenderPass<'a>, - ) { - if let Some(layer) = self.layers.get(layer) { - render_pass.set_pipeline(&self.pipeline); - - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - - render_pass.set_bind_group(1, &self.texture, &[]); - - layer.render(render_pass); - } - } - - pub fn end_frame(&mut self) { - #[cfg(feature = "image")] - self.raster_cache.borrow_mut().trim(&mut self.texture_atlas); - - #[cfg(feature = "svg")] - self.vector_cache.borrow_mut().trim(&mut self.texture_atlas); - - self.prepare_layer = 0; - } -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, Zeroable, Pod)] -struct Instance { - _position: [f32; 2], - _size: [f32; 2], - _position_in_atlas: [f32; 2], - _size_in_atlas: [f32; 2], - _layer: u32, -} - -impl Instance { - pub const INITIAL: usize = 20; -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, Zeroable, Pod)] -struct Uniforms { - transform: [f32; 16], -} - -fn add_instances( - image_position: [f32; 2], - image_size: [f32; 2], - entry: &atlas::Entry, - instances: &mut Vec, -) { - match entry { - atlas::Entry::Contiguous(allocation) => { - add_instance(image_position, image_size, allocation, instances); - } - atlas::Entry::Fragmented { fragments, size } => { - let scaling_x = image_size[0] / size.width as f32; - let scaling_y = image_size[1] / size.height as f32; - - for fragment in fragments { - let allocation = &fragment.allocation; - - let [x, y] = image_position; - let (fragment_x, fragment_y) = fragment.position; - let Size { - width: fragment_width, - height: fragment_height, - } = allocation.size(); - - let position = [ - x + fragment_x as f32 * scaling_x, - y + fragment_y as f32 * scaling_y, - ]; - - let size = [ - fragment_width as f32 * scaling_x, - fragment_height as f32 * scaling_y, - ]; - - add_instance(position, size, allocation, instances); - } - } - } -} - -#[inline] -fn add_instance( - position: [f32; 2], - size: [f32; 2], - allocation: &atlas::Allocation, - instances: &mut Vec, -) { - let (x, y) = allocation.position(); - let Size { width, height } = allocation.size(); - let layer = allocation.layer(); - - let instance = Instance { - _position: position, - _size: size, - _position_in_atlas: [ - (x as f32 + 0.5) / atlas::SIZE as f32, - (y as f32 + 0.5) / atlas::SIZE as f32, - ], - _size_in_atlas: [ - (width as f32 - 1.0) / atlas::SIZE as f32, - (height as f32 - 1.0) / atlas::SIZE as f32, - ], - _layer: layer as u32, - }; - - instances.push(instance); -} diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs new file mode 100644 index 00000000..32118ed6 --- /dev/null +++ b/wgpu/src/image/cache.rs @@ -0,0 +1,107 @@ +use crate::core::{self, Size}; +use crate::image::atlas::{self, Atlas}; + +use std::cell::{RefCell, RefMut}; +use std::rc::Rc; + +#[derive(Debug)] +pub struct Cache { + atlas: Atlas, + #[cfg(feature = "image")] + raster: crate::image::raster::Cache, + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache, +} + +impl Cache { + pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self { + Self { + atlas: Atlas::new(device, backend), + #[cfg(feature = "image")] + raster: crate::image::raster::Cache::default(), + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache::default(), + } + } + + pub fn layer_count(&self) -> usize { + self.atlas.layer_count() + } + + #[cfg(feature = "image")] + pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size { + self.raster.load(handle).dimensions() + } + + #[cfg(feature = "svg")] + pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size { + self.vector.load(handle).viewport_dimensions() + } + + #[cfg(feature = "image")] + pub fn upload_raster( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::image::Handle, + ) -> Option<&atlas::Entry> { + self.raster.upload(device, encoder, handle, &mut self.atlas) + } + + #[cfg(feature = "svg")] + pub fn upload_vector( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::svg::Handle, + color: Option, + size: [f32; 2], + scale: f32, + ) -> Option<&atlas::Entry> { + self.vector.upload( + device, + encoder, + handle, + color, + size, + scale, + &mut self.atlas, + ) + } + + pub fn create_bind_group( + &self, + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, + ) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image texture atlas bind group"), + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(self.atlas.view()), + }], + }) + } + + pub fn trim(&mut self) { + #[cfg(feature = "image")] + self.raster.trim(&mut self.atlas); + + #[cfg(feature = "svg")] + self.vector.trim(&mut self.atlas); + } +} + +#[derive(Debug, Clone)] +pub struct Shared(Rc>); + +impl Shared { + pub fn new(cache: Cache) -> Self { + Self(Rc::new(RefCell::new(cache))) + } + + pub fn lock(&self) -> RefMut<'_, Cache> { + self.0.borrow_mut() + } +} diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs new file mode 100644 index 00000000..88e6bdb9 --- /dev/null +++ b/wgpu/src/image/mod.rs @@ -0,0 +1,571 @@ +pub(crate) mod cache; +pub(crate) use cache::Cache; + +mod atlas; + +#[cfg(feature = "image")] +mod raster; + +#[cfg(feature = "svg")] +mod vector; + +use crate::core::image; +use crate::core::{Rectangle, Size, Transformation}; +use crate::Buffer; + +use bytemuck::{Pod, Zeroable}; +use std::mem; + +pub use crate::graphics::Image; + +pub type Batch = Vec; + +#[derive(Debug)] +pub struct Pipeline { + pipeline: wgpu::RenderPipeline, + nearest_sampler: wgpu::Sampler, + linear_sampler: wgpu::Sampler, + texture: wgpu::BindGroup, + texture_version: usize, + texture_layout: wgpu::BindGroupLayout, + constant_layout: wgpu::BindGroupLayout, + cache: cache::Shared, + layers: Vec, + prepare_layer: usize, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + backend: wgpu::Backend, + ) -> Self { + let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + min_filter: wgpu::FilterMode::Nearest, + mag_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + min_filter: wgpu::FilterMode::Linear, + mag_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let constant_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu::image constants layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new( + mem::size_of::() as u64, + ), + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::Filtering, + ), + count: None, + }, + ], + }); + + let texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu::image texture atlas layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::D2Array, + multisampled: false, + }, + count: None, + }], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu::image pipeline layout"), + push_constant_ranges: &[], + bind_group_layouts: &[&constant_layout, &texture_layout], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu image shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + concat!( + include_str!("../shader/vertex.wgsl"), + "\n", + include_str!("../shader/image.wgsl"), + ), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu::image pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Position + 0 => Float32x2, + // Scale + 1 => Float32x2, + // Atlas position + 2 => Float32x2, + // Atlas scale + 3 => Float32x2, + // Layer + 4 => Sint32, + ), + }], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + + let cache = Cache::new(device, backend); + let texture = cache.create_bind_group(device, &texture_layout); + + Pipeline { + pipeline, + nearest_sampler, + linear_sampler, + texture, + texture_version: cache.layer_count(), + texture_layout, + constant_layout, + cache: cache::Shared::new(cache), + layers: Vec::new(), + prepare_layer: 0, + } + } + + pub fn cache(&self) -> &cache::Shared { + &self.cache + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + images: &Batch, + transformation: Transformation, + scale: f32, + ) { + let transformation = transformation * Transformation::scale(scale); + + let nearest_instances: &mut Vec = &mut Vec::new(); + let linear_instances: &mut Vec = &mut Vec::new(); + + let mut cache = self.cache.lock(); + + for image in images { + match &image { + #[cfg(feature = "image")] + Image::Raster { + handle, + filter_method, + bounds, + } => { + if let Some(atlas_entry) = + cache.upload_raster(device, encoder, handle) + { + add_instances( + [bounds.x, bounds.y], + [bounds.width, bounds.height], + atlas_entry, + match filter_method { + image::FilterMethod::Nearest => { + nearest_instances + } + image::FilterMethod::Linear => linear_instances, + }, + ); + } + } + #[cfg(not(feature = "image"))] + Image::Raster { .. } => {} + + #[cfg(feature = "svg")] + Image::Vector { + handle, + color, + bounds, + } => { + let size = [bounds.width, bounds.height]; + + if let Some(atlas_entry) = cache.upload_vector( + device, encoder, handle, *color, size, scale, + ) { + add_instances( + [bounds.x, bounds.y], + size, + atlas_entry, + nearest_instances, + ); + } + } + #[cfg(not(feature = "svg"))] + Image::Vector { .. } => {} + } + } + + if nearest_instances.is_empty() && linear_instances.is_empty() { + return; + } + + let texture_version = cache.layer_count(); + + if self.texture_version != texture_version { + log::info!("Atlas has grown. Recreating bind group..."); + + self.texture = + cache.create_bind_group(device, &self.texture_layout); + self.texture_version = texture_version; + } + + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &self.constant_layout, + &self.nearest_sampler, + &self.linear_sampler, + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + + layer.prepare( + device, + encoder, + belt, + nearest_instances, + linear_instances, + transformation, + ); + + self.prepare_layer += 1; + } + + pub fn render<'a>( + &'a self, + layer: usize, + bounds: Rectangle, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + if let Some(layer) = self.layers.get(layer) { + render_pass.set_pipeline(&self.pipeline); + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + render_pass.set_bind_group(1, &self.texture, &[]); + + layer.render(render_pass); + } + } + + pub fn end_frame(&mut self) { + self.cache.lock().trim(); + self.prepare_layer = 0; + } +} + +#[derive(Debug)] +struct Layer { + uniforms: wgpu::Buffer, + nearest: Data, + linear: Data, +} + +impl Layer { + fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + nearest_sampler: &wgpu::Sampler, + linear_sampler: &wgpu::Sampler, + ) -> Self { + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::image uniforms buffer"), + size: mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let nearest = + Data::new(device, constant_layout, nearest_sampler, &uniforms); + + let linear = + Data::new(device, constant_layout, linear_sampler, &uniforms); + + Self { + uniforms, + nearest, + linear, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + nearest_instances: &[Instance], + linear_instances: &[Instance], + transformation: Transformation, + ) { + let uniforms = Uniforms { + transform: transformation.into(), + }; + + let bytes = bytemuck::bytes_of(&uniforms); + + belt.write_buffer( + encoder, + &self.uniforms, + 0, + (bytes.len() as u64).try_into().expect("Sized uniforms"), + device, + ) + .copy_from_slice(bytes); + + self.nearest + .upload(device, encoder, belt, nearest_instances); + + self.linear.upload(device, encoder, belt, linear_instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + self.nearest.render(render_pass); + self.linear.render(render_pass); + } +} + +#[derive(Debug)] +struct Data { + constants: wgpu::BindGroup, + instances: Buffer, + instance_count: usize, +} + +impl Data { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + uniforms: &wgpu::Buffer, + ) -> Self { + let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image constants bind group"), + layout: constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: uniforms, + offset: 0, + size: None, + }, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(sampler), + }, + ], + }); + + let instances = Buffer::new( + device, + "iced_wgpu::image instance buffer", + Instance::INITIAL, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + constants, + instances, + instance_count: 0, + } + } + + fn upload( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + instances: &[Instance], + ) { + self.instance_count = instances.len(); + + if self.instance_count == 0 { + return; + } + + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(device, encoder, belt, 0, instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + if self.instance_count == 0 { + return; + } + + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_vertex_buffer(0, self.instances.slice(..)); + + render_pass.draw(0..6, 0..self.instance_count as u32); + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +struct Instance { + _position: [f32; 2], + _size: [f32; 2], + _position_in_atlas: [f32; 2], + _size_in_atlas: [f32; 2], + _layer: u32, +} + +impl Instance { + pub const INITIAL: usize = 20; +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +struct Uniforms { + transform: [f32; 16], +} + +fn add_instances( + image_position: [f32; 2], + image_size: [f32; 2], + entry: &atlas::Entry, + instances: &mut Vec, +) { + match entry { + atlas::Entry::Contiguous(allocation) => { + add_instance(image_position, image_size, allocation, instances); + } + atlas::Entry::Fragmented { fragments, size } => { + let scaling_x = image_size[0] / size.width as f32; + let scaling_y = image_size[1] / size.height as f32; + + for fragment in fragments { + let allocation = &fragment.allocation; + + let [x, y] = image_position; + let (fragment_x, fragment_y) = fragment.position; + let Size { + width: fragment_width, + height: fragment_height, + } = allocation.size(); + + let position = [ + x + fragment_x as f32 * scaling_x, + y + fragment_y as f32 * scaling_y, + ]; + + let size = [ + fragment_width as f32 * scaling_x, + fragment_height as f32 * scaling_y, + ]; + + add_instance(position, size, allocation, instances); + } + } + } +} + +#[inline] +fn add_instance( + position: [f32; 2], + size: [f32; 2], + allocation: &atlas::Allocation, + instances: &mut Vec, +) { + let (x, y) = allocation.position(); + let Size { width, height } = allocation.size(); + let layer = allocation.layer(); + + let instance = Instance { + _position: position, + _size: size, + _position_in_atlas: [ + (x as f32 + 0.5) / atlas::SIZE as f32, + (y as f32 + 0.5) / atlas::SIZE as f32, + ], + _size_in_atlas: [ + (width as f32 - 1.0) / atlas::SIZE as f32, + (height as f32 - 1.0) / atlas::SIZE as f32, + ], + _layer: layer as u32, + }; + + instances.push(instance); +} diff --git a/wgpu/src/image/null.rs b/wgpu/src/image/null.rs new file mode 100644 index 00000000..900841a6 --- /dev/null +++ b/wgpu/src/image/null.rs @@ -0,0 +1,10 @@ +pub use crate::graphics::Image; + +#[derive(Default)] +pub struct Batch; + +impl Batch { + pub fn push(&mut self, _image: Image) {} + + pub fn clear(&mut self) {} +} diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index cc767c25..5c23669a 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,343 +1,326 @@ -//! Organize rendering primitives into a flattened list of layers. -mod image; -mod pipeline; -mod text; - -pub mod mesh; - -pub use image::Image; -pub use mesh::Mesh; -pub use pipeline::Pipeline; -pub use text::Text; - -use crate::core; -use crate::core::alignment; -use crate::core::{ - Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector, -}; -use crate::graphics; +use crate::core::renderer; +use crate::core::{Background, Color, Point, Rectangle, Transformation}; use crate::graphics::color; -use crate::graphics::Viewport; -use crate::primitive::{self, Primitive}; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Mesh; +use crate::image::{self, Image}; use crate::quad::{self, Quad}; +use crate::text::{self, Text}; +use crate::triangle; -/// A group of primitives that should be clipped together. -#[derive(Debug)] -pub struct Layer<'a> { - /// The clipping bounds of the [`Layer`]. - pub bounds: Rectangle, +use std::cell::{self, RefCell}; +use std::rc::Rc; - /// The quads of the [`Layer`]. - pub quads: quad::Batch, - - /// The triangle meshes of the [`Layer`]. - pub meshes: Vec>, - - /// The text of the [`Layer`]. - pub text: Vec>, +pub enum Layer<'a> { + Live(&'a Live), + Cached(cell::Ref<'a, Cached>), +} - /// The images of the [`Layer`]. - pub images: Vec, +pub enum LayerMut<'a> { + Live(&'a mut Live), + Cached(cell::RefMut<'a, Cached>), +} - /// The custom pipelines of this [`Layer`]. - pub pipelines: Vec, +pub struct Stack { + live: Vec, + cached: Vec>>, + order: Vec, + transformations: Vec, + previous: Vec, + current: usize, + live_count: usize, } -impl<'a> Layer<'a> { - /// Creates a new [`Layer`] with the given clipping bounds. - pub fn new(bounds: Rectangle) -> Self { +impl Stack { + pub fn new() -> Self { Self { - bounds, - quads: quad::Batch::default(), - meshes: Vec::new(), - text: Vec::new(), - images: Vec::new(), - pipelines: Vec::new(), + live: vec![Live::default()], + cached: Vec::new(), + order: vec![Kind::Live], + transformations: vec![Transformation::IDENTITY], + previous: Vec::new(), + current: 0, + live_count: 1, } } - /// Creates a new [`Layer`] for the provided overlay text. - /// - /// This can be useful for displaying debug information. - pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { - let mut overlay = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - for (i, line) in lines.iter().enumerate() { - let text = text::Cached { - content: line.as_ref(), - bounds: Rectangle::new( - Point::new(11.0, 11.0 + 25.0 * i as f32), - Size::INFINITY, - ), - color: Color::new(0.9, 0.9, 0.9, 1.0), - size: Pixels(20.0), - line_height: core::text::LineHeight::default(), - font: Font::MONOSPACE, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: core::text::Shaping::Basic, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }; - - overlay.text.push(Text::Cached(text.clone())); - - overlay.text.push(Text::Cached(text::Cached { - bounds: text.bounds + Vector::new(-1.0, -1.0), - color: Color::BLACK, - ..text - })); - } + pub fn draw_quad(&mut self, quad: renderer::Quad, background: Background) { + let transformation = self.transformations.last().unwrap(); + let bounds = quad.bounds * *transformation; + + let quad = Quad { + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + border_color: color::pack(quad.border.color), + border_radius: quad.border.radius.into(), + border_width: quad.border.width, + shadow_color: color::pack(quad.shadow.color), + shadow_offset: quad.shadow.offset.into(), + shadow_blur_radius: quad.shadow.blur_radius, + }; + + self.live[self.current].quads.add(quad, &background); + } - overlay + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation: self.transformations.last().copied().unwrap(), + }; + + self.live[self.current].text.push(paragraph); } - /// Distributes the given [`Primitive`] and generates a list of layers based - /// on its contents. - pub fn generate( - primitives: &'a [Primitive], - viewport: &Viewport, - ) -> Vec { - let first_layer = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - let mut layers = vec![first_layer]; - - for primitive in primitives { - Self::process_primitive( - &mut layers, - Transformation::IDENTITY, - primitive, - 0, - ); - } + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let paragraph = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation: self.transformations.last().copied().unwrap(), + }; + + self.live[self.current].text.push(paragraph); + } - layers + pub fn draw_text( + &mut self, + text: crate::core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let transformation = self.transformation(); + + let paragraph = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.live[self.current].text.push(paragraph); } - fn process_primitive( - layers: &mut Vec, - transformation: Transformation, - primitive: &'a Primitive, - current_layer: usize, + pub fn draw_image( + &mut self, + handle: crate::core::image::Handle, + filter_method: crate::core::image::FilterMethod, + bounds: Rectangle, ) { - match primitive { - Primitive::Paragraph { - paragraph, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Paragraph { - paragraph: paragraph.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Editor { - editor, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Editor { - editor: editor.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Text { - content, - bounds, - size, - line_height, - color, - font, - horizontal_alignment, - vertical_alignment, - shaping, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Cached(text::Cached { - content, - bounds: *bounds + transformation.translation(), - size: *size * transformation.scale_factor(), - line_height: *line_height, - color: *color, - font: *font, - horizontal_alignment: *horizontal_alignment, - vertical_alignment: *vertical_alignment, - shaping: *shaping, - clip_bounds: *clip_bounds * transformation, - })); - } - graphics::Primitive::RawText(raw) => { - let layer = &mut layers[current_layer]; + let image = Image::Raster { + handle, + filter_method, + bounds: bounds * self.transformation(), + }; - layer.text.push(Text::Raw { - raw: raw.clone(), - transformation, - }); - } - Primitive::Quad { - bounds, - background, - border, - shadow, - } => { - let layer = &mut layers[current_layer]; - let bounds = *bounds * transformation; - - let quad = Quad { - position: [bounds.x, bounds.y], - size: [bounds.width, bounds.height], - border_color: color::pack(border.color), - border_radius: border.radius.into(), - border_width: border.width, - shadow_color: shadow.color.into_linear(), - shadow_offset: shadow.offset.into(), - shadow_blur_radius: shadow.blur_radius, - }; - - layer.quads.add(quad, background); - } - Primitive::Image { - handle, - filter_method, - bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Raster { - handle: handle.clone(), - filter_method: *filter_method, - bounds: *bounds * transformation, - }); + self.live[self.current].images.push(image); + } + + pub fn draw_svg( + &mut self, + handle: crate::core::svg::Handle, + color: Option, + bounds: Rectangle, + ) { + let svg = Image::Vector { + handle, + color, + bounds: bounds * self.transformation(), + }; + + self.live[self.current].images.push(svg); + } + + pub fn draw_mesh(&mut self, mut mesh: Mesh) { + match &mut mesh { + Mesh::Solid { transformation, .. } + | Mesh::Gradient { transformation, .. } => { + *transformation = *transformation * self.transformation(); } - Primitive::Svg { - handle, - color, + } + + self.live[self.current].meshes.push(mesh); + } + + pub fn draw_layer(&mut self, mut layer: Live) { + layer.transformation = layer.transformation * self.transformation(); + + if self.live_count == self.live.len() { + self.live.push(layer); + } else { + self.live[self.live_count] = layer; + } + + self.live_count += 1; + self.order.push(Kind::Live); + } + + pub fn draw_cached_layer(&mut self, layer: &Rc>) { + { + let mut layer = layer.borrow_mut(); + layer.transformation = self.transformation() * layer.transformation; + } + + self.cached.push(layer.clone()); + self.order.push(Kind::Cache); + } + + pub fn push_clip(&mut self, bounds: Option) { + self.previous.push(self.current); + self.order.push(Kind::Live); + + self.current = self.live_count; + self.live_count += 1; + + let bounds = bounds.map(|bounds| bounds * self.transformation()); + + if self.current == self.live.len() { + self.live.push(Live { bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Vector { - handle: handle.clone(), - color: *color, - bounds: *bounds * transformation, - }); - } - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - Self::process_primitive( - layers, - transformation, - primitive, - current_layer, - ); - } - } - Primitive::Clip { bounds, content } => { - let layer = &mut layers[current_layer]; - let translated_bounds = *bounds * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&translated_bounds) - { - let clip_layer = Layer::new(clip_bounds); - layers.push(clip_layer); - - Self::process_primitive( - layers, - transformation, - content, - layers.len() - 1, - ); - } - } - Primitive::Transform { - transformation: new_transformation, - content, - } => { - Self::process_primitive( - layers, - transformation * *new_transformation, - content, - current_layer, - ); - } - Primitive::Cache { content } => { - Self::process_primitive( - layers, - transformation, - content, - current_layer, - ); + ..Live::default() + }); + } else { + self.live[self.current].bounds = bounds; + } + } + + pub fn pop_clip(&mut self) { + self.current = self.previous.pop().unwrap(); + } + + pub fn push_transformation(&mut self, transformation: Transformation) { + self.transformations + .push(self.transformation() * transformation); + } + + pub fn pop_transformation(&mut self) { + let _ = self.transformations.pop(); + } + + fn transformation(&self) -> Transformation { + self.transformations.last().copied().unwrap() + } + + pub fn iter_mut(&mut self) -> impl Iterator> { + let mut live = self.live.iter_mut(); + let mut cached = self.cached.iter_mut(); + + self.order.iter().map(move |kind| match kind { + Kind::Live => LayerMut::Live(live.next().unwrap()), + Kind::Cache => { + LayerMut::Cached(cached.next().unwrap().borrow_mut()) } - Primitive::Custom(custom) => match custom { - primitive::Custom::Mesh(mesh) => match mesh { - graphics::Mesh::Solid { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Solid { - transformation, - buffers, - clip_bounds, - }); - } - } - graphics::Mesh::Gradient { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Gradient { - transformation, - buffers, - clip_bounds, - }); - } - } - }, - primitive::Custom::Pipeline(pipeline) => { - let layer = &mut layers[current_layer]; - let bounds = pipeline.bounds * transformation; - - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.pipelines.push(Pipeline { - bounds, - viewport: clip_bounds, - primitive: pipeline.primitive.clone(), - }); - } - } - }, + }) + } + + pub fn iter(&self) -> impl Iterator> { + let mut live = self.live.iter(); + let mut cached = self.cached.iter(); + + self.order.iter().map(move |kind| match kind { + Kind::Live => Layer::Live(live.next().unwrap()), + Kind::Cache => Layer::Cached(cached.next().unwrap().borrow()), + }) + } + + pub fn clear(&mut self) { + for live in &mut self.live[..self.live_count] { + live.bounds = None; + live.transformation = Transformation::IDENTITY; + + live.quads.clear(); + live.meshes.clear(); + live.text.clear(); + live.images.clear(); } + + self.current = 0; + self.live_count = 1; + + self.order.clear(); + self.order.push(Kind::Live); + + self.cached.clear(); + self.previous.clear(); + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() } } + +#[derive(Default)] +pub struct Live { + pub bounds: Option, + pub transformation: Transformation, + pub quads: quad::Batch, + pub meshes: triangle::Batch, + pub text: text::Batch, + pub images: image::Batch, +} + +impl Live { + pub fn into_cached(self) -> Cached { + Cached { + bounds: self.bounds, + transformation: self.transformation, + last_transformation: None, + quads: quad::Cache::Staged(self.quads), + meshes: triangle::Cache::Staged(self.meshes), + text: text::Cache::Staged(self.text), + images: self.images, + } + } +} + +#[derive(Default)] +pub struct Cached { + pub bounds: Option, + pub transformation: Transformation, + pub last_transformation: Option, + pub quads: quad::Cache, + pub meshes: triangle::Cache, + pub text: text::Cache, + pub images: image::Batch, +} + +impl Cached { + pub fn update(&mut self, live: Live) { + self.bounds = live.bounds; + self.transformation = live.transformation; + + self.quads.update(live.quads); + self.meshes.update(live.meshes); + self.text.update(live.text); + self.images = live.images; + } +} + +enum Kind { + Live, + Cache, +} diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs deleted file mode 100644 index facbe192..00000000 --- a/wgpu/src/layer/image.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::core::image; -use crate::core::svg; -use crate::core::{Color, Rectangle}; - -/// A raster or vector image. -#[derive(Debug, Clone)] -pub enum Image { - /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, - - /// The filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - }, - /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option, - - /// The bounds of the image. - bounds: Rectangle, - }, -} diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs deleted file mode 100644 index 5ed7c654..00000000 --- a/wgpu/src/layer/mesh.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! A collection of triangle primitives. -use crate::core::{Rectangle, Transformation}; -use crate::graphics::mesh; - -/// A mesh of triangles. -#[derive(Debug, Clone, Copy)] -pub enum Mesh<'a> { - /// A mesh of triangles with a solid color. - Solid { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle, - }, - /// A mesh of triangles with a gradient color. - Gradient { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle, - }, -} - -impl Mesh<'_> { - /// Returns the origin of the [`Mesh`]. - pub fn transformation(&self) -> Transformation { - match self { - Self::Solid { transformation, .. } - | Self::Gradient { transformation, .. } => *transformation, - } - } - - /// Returns the indices of the [`Mesh`]. - pub fn indices(&self) -> &[u32] { - match self { - Self::Solid { buffers, .. } => &buffers.indices, - Self::Gradient { buffers, .. } => &buffers.indices, - } - } - - /// Returns the clip bounds of the [`Mesh`]. - pub fn clip_bounds(&self) -> Rectangle { - match self { - Self::Solid { clip_bounds, .. } - | Self::Gradient { clip_bounds, .. } => *clip_bounds, - } - } -} - -/// The result of counting the attributes of a set of meshes. -#[derive(Debug, Clone, Copy, Default)] -pub struct AttributeCount { - /// The total amount of solid vertices. - pub solid_vertices: usize, - - /// The total amount of solid meshes. - pub solids: usize, - - /// The total amount of gradient vertices. - pub gradient_vertices: usize, - - /// The total amount of gradient meshes. - pub gradients: usize, - - /// The total amount of indices. - pub indices: usize, -} - -/// Returns the number of total vertices & total indices of all [`Mesh`]es. -pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount { - meshes - .iter() - .fold(AttributeCount::default(), |mut count, mesh| { - match mesh { - Mesh::Solid { buffers, .. } => { - count.solids += 1; - count.solid_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - Mesh::Gradient { buffers, .. } => { - count.gradients += 1; - count.gradient_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - } - - count - }) -} diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs deleted file mode 100644 index 6dfe6750..00000000 --- a/wgpu/src/layer/pipeline.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::core::Rectangle; -use crate::primitive::pipeline::Primitive; - -use std::sync::Arc; - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Pipeline { - /// The bounds of the [`Pipeline`]. - pub bounds: Rectangle, - - /// The viewport of the [`Pipeline`]. - pub viewport: Rectangle, - - /// The [`Primitive`] to render. - pub primitive: Arc, -} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs deleted file mode 100644 index b3a00130..00000000 --- a/wgpu/src/layer/text.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::core::alignment; -use crate::core::text; -use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation}; -use crate::graphics; -use crate::graphics::text::editor; -use crate::graphics::text::paragraph; - -/// A text primitive. -#[derive(Debug, Clone)] -pub enum Text<'a> { - /// A paragraph. - #[allow(missing_docs)] - Paragraph { - paragraph: paragraph::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// An editor. - #[allow(missing_docs)] - Editor { - editor: editor::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// Some cached text. - Cached(Cached<'a>), - /// Some raw text. - #[allow(missing_docs)] - Raw { - raw: graphics::text::Raw, - transformation: Transformation, - }, -} - -#[derive(Debug, Clone)] -pub struct Cached<'a> { - /// The content of the [`Text`]. - pub content: &'a str, - - /// The layout bounds of the [`Text`]. - pub bounds: Rectangle, - - /// The color of the [`Text`], in __linear RGB_. - pub color: Color, - - /// The size of the [`Text`] in logical pixels. - pub size: Pixels, - - /// The line height of the [`Text`]. - pub line_height: text::LineHeight, - - /// The font of the [`Text`]. - pub font: Font, - - /// The horizontal alignment of the [`Text`]. - pub horizontal_alignment: alignment::Horizontal, - - /// The vertical alignment of the [`Text`]. - pub vertical_alignment: alignment::Vertical, - - /// The shaping strategy of the text. - pub shaping: text::Shaping, - - /// The clip bounds of the text. - pub clip_bounds: Rectangle, -} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b00e5c3c..0e173e0a 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -22,8 +22,8 @@ )] #![forbid(rust_2018_idioms)] #![deny( - missing_debug_implementations, - missing_docs, + // missing_debug_implementations, + //missing_docs, unsafe_code, unused_results, rustdoc::broken_intra_doc_links @@ -37,13 +37,21 @@ pub mod window; #[cfg(feature = "geometry")] pub mod geometry; -mod backend; mod buffer; mod color; +mod engine; mod quad; mod text; mod triangle; +#[cfg(any(feature = "image", feature = "svg"))] +#[path = "image/mod.rs"] +mod image; + +#[cfg(not(any(feature = "image", feature = "svg")))] +#[path = "image/null.rs"] +mod image; + use buffer::Buffer; pub use iced_graphics as graphics; @@ -51,16 +59,570 @@ pub use iced_graphics::core; pub use wgpu; -pub use backend::Backend; -pub use layer::Layer; +pub use engine::Engine; +pub use layer::{Layer, LayerMut}; pub use primitive::Primitive; pub use settings::Settings; -#[cfg(any(feature = "image", feature = "svg"))] -mod image; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, +}; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Viewport; + +use std::borrow::Cow; /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = iced_graphics::Renderer; +#[allow(missing_debug_implementations)] +pub struct Renderer { + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + + // TODO: Centralize all the image feature handling + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: image::cache::Shared, +} + +impl Renderer { + pub fn new(settings: Settings, _engine: &Engine) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + layers: layer::Stack::new(), + + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: _engine.image_cache().clone(), + } + } + + pub fn draw_primitive(&mut self, _primitive: Primitive) {} + + pub fn present>( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + clear_color: Option, + format: wgpu::TextureFormat, + frame: &wgpu::TextureView, + viewport: &Viewport, + overlay: &[T], + ) { + let target_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + let transformation = viewport.projection(); + + for line in overlay { + println!("{}", line.as_ref()); + } + + self.prepare( + engine, + device, + queue, + format, + encoder, + scale_factor, + target_size, + transformation, + ); + + self.render( + engine, + device, + encoder, + frame, + clear_color, + scale_factor, + target_size, + ); + } + + fn prepare( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + queue: &wgpu::Queue, + _format: wgpu::TextureFormat, + encoder: &mut wgpu::CommandEncoder, + scale_factor: f32, + target_size: Size, + transformation: Transformation, + ) { + for layer in self.layers.iter_mut() { + match layer { + LayerMut::Live(live) => { + if !live.quads.is_empty() { + engine.quad_pipeline.prepare_batch( + device, + encoder, + &mut engine.staging_belt, + &live.quads, + transformation, + scale_factor, + ); + } + + if !live.meshes.is_empty() { + engine.triangle_pipeline.prepare_batch( + device, + encoder, + &mut engine.staging_belt, + &live.meshes, + transformation + * Transformation::scale(scale_factor), + ); + } + + if !live.text.is_empty() { + engine.text_pipeline.prepare_batch( + device, + queue, + encoder, + &live.text, + live.bounds.unwrap_or(Rectangle::with_size( + Size::INFINITY, + )), + scale_factor, + target_size, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !live.images.is_empty() { + engine.image_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &live.images, + transformation, + scale_factor, + ); + } + } + LayerMut::Cached(mut cached) => { + if !cached.quads.is_empty() { + engine.quad_pipeline.prepare_cache( + device, + encoder, + &mut engine.staging_belt, + &mut cached.quads, + transformation, + scale_factor, + ); + } + + if !cached.meshes.is_empty() { + engine.triangle_pipeline.prepare_cache( + device, + encoder, + &mut engine.staging_belt, + &mut cached.meshes, + transformation + * Transformation::scale(scale_factor), + ); + } + + if !cached.text.is_empty() { + let bounds = cached + .bounds + .unwrap_or(Rectangle::with_size(Size::INFINITY)); + + engine.text_pipeline.prepare_cache( + device, + queue, + encoder, + &mut cached.text, + bounds, + scale_factor, + target_size, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !cached.images.is_empty() { + engine.image_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &cached.images, + transformation, + scale_factor, + ); + } + } + } + } + } + + fn render( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + frame: &wgpu::TextureView, + clear_color: Option, + scale_factor: f32, + target_size: Size, + ) { + use std::mem::ManuallyDrop; + + let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: match clear_color { + Some(background_color) => wgpu::LoadOp::Clear({ + let [r, g, b, a] = + graphics::color::pack(background_color) + .components(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }), + None => wgpu::LoadOp::Load, + }, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + + let mut quad_layer = 0; + let mut mesh_layer = 0; + let mut text_layer = 0; + + #[cfg(any(feature = "svg", feature = "image"))] + let mut image_layer = 0; + + // TODO: Can we avoid collecting here? + let layers: Vec<_> = self.layers.iter().collect(); + + for layer in &layers { + match layer { + Layer::Live(live) => { + let bounds = live + .bounds + .map(|bounds| bounds * scale_factor) + .map(Rectangle::snap) + .unwrap_or(Rectangle::with_size(target_size)); + + if !live.quads.is_empty() { + engine.quad_pipeline.render_batch( + quad_layer, + bounds, + &live.quads, + &mut render_pass, + ); + + quad_layer += 1; + } + + if !live.meshes.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + engine.triangle_pipeline.render_batch( + device, + encoder, + frame, + mesh_layer, + target_size, + &live.meshes, + bounds, + scale_factor, + ); + + mesh_layer += 1; + + render_pass = + ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !live.text.is_empty() { + engine.text_pipeline.render_batch( + text_layer, + bounds, + &mut render_pass, + ); + + text_layer += 1; + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !live.images.is_empty() { + engine.image_pipeline.render( + image_layer, + bounds, + &mut render_pass, + ); + + image_layer += 1; + } + } + Layer::Cached(cached) => { + let bounds = cached + .bounds + .map(|bounds| bounds * scale_factor) + .map(Rectangle::snap) + .unwrap_or(Rectangle::with_size(target_size)); + + if !cached.quads.is_empty() { + engine.quad_pipeline.render_cache( + &cached.quads, + bounds, + &mut render_pass, + ); + } + + if !cached.meshes.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + engine.triangle_pipeline.render_cache( + device, + encoder, + frame, + target_size, + &cached.meshes, + bounds, + scale_factor, + ); + + render_pass = + ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !cached.text.is_empty() { + engine.text_pipeline.render_cache( + &cached.text, + bounds, + &mut render_pass, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !cached.images.is_empty() { + engine.image_pipeline.render( + image_layer, + bounds, + &mut render_pass, + ); + + image_layer += 1; + } + } + } + } + + let _ = ManuallyDrop::into_inner(render_pass); + } +} + +impl core::Renderer for Renderer { + fn start_layer(&mut self, bounds: Rectangle) { + self.layers.push_clip(Some(bounds)); + } + + fn end_layer(&mut self, _bounds: Rectangle) { + self.layers.pop_clip(); + } + + fn start_transformation(&mut self, transformation: Transformation) { + self.layers.push_transformation(transformation); + } + + fn end_transformation(&mut self, _transformation: Transformation) { + self.layers.pop_transformation(); + } + + fn fill_quad( + &mut self, + quad: core::renderer::Quad, + background: impl Into, + ) { + self.layers.draw_quad(quad, background.into()); + } + + fn clear(&mut self) { + self.layers.clear(); + } +} + +impl core::text::Renderer for Renderer { + type Font = Font; + type Paragraph = Paragraph; + type Editor = Editor; + + const ICON_FONT: Font = Font::with_name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Self::Font { + self.default_font + } + + fn default_size(&self) -> Pixels { + self.default_text_size + } + + fn load_font(&mut self, font: Cow<'static, [u8]>) { + graphics::text::font_system() + .write() + .expect("Write font system") + .load_font(font); + + // TODO: Invalidate buffer cache + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + self.layers + .draw_paragraph(text, position, color, clip_bounds); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + self.layers + .draw_editor(editor, position, color, clip_bounds); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + self.layers.draw_text(text, position, color, clip_bounds); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> Size { + self.image_cache.lock().measure_image(handle) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: core::image::FilterMethod, + bounds: Rectangle, + ) { + self.layers.draw_image(handle, filter_method, bounds); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg(&self, handle: &core::svg::Handle) -> Size { + self.image_cache.lock().measure_svg(handle) + } + + fn draw_svg( + &mut self, + handle: core::svg::Handle, + color_filter: Option, + bounds: Rectangle, + ) { + self.layers.draw_svg(handle, color_filter, bounds); + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, mesh: graphics::Mesh) { + self.layers.draw_mesh(mesh); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, size: Size) -> Self::Frame { + geometry::Frame::new(size) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + match geometry { + Geometry::Live(layers) => { + for layer in layers { + self.layers.draw_layer(layer); + } + } + Geometry::Cached(layers) => { + for layer in layers.as_ref() { + self.layers.draw_cached_layer(layer); + } + } + } + } +} + +impl graphics::compositor::Default for crate::Renderer { + type Compositor = window::Compositor; +} diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index ee9af93c..8e311d2b 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -3,8 +3,7 @@ pub mod pipeline; pub use pipeline::Pipeline; -use crate::core::Rectangle; -use crate::graphics::{Damage, Mesh}; +use crate::graphics::Mesh; use std::fmt::Debug; @@ -19,20 +18,3 @@ pub enum Custom { /// A custom pipeline primitive. Pipeline(Pipeline), } - -impl Damage for Custom { - fn bounds(&self) -> Rectangle { - match self { - Self::Mesh(mesh) => mesh.bounds(), - Self::Pipeline(pipeline) => pipeline.bounds, - } - } -} - -impl TryFrom for Custom { - type Error = &'static str; - - fn try_from(mesh: Mesh) -> Result { - Ok(Custom::Mesh(mesh)) - } -} diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 0717a031..16d50b04 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -12,11 +12,37 @@ use bytemuck::{Pod, Zeroable}; use std::mem; -#[cfg(feature = "tracing")] -use tracing::info_span; - const INITIAL_INSTANCES: usize = 2_000; +/// The properties of a quad. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: color::Packed, + + /// The border radii of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, + + /// The shadow color of the [`Quad`]. + pub shadow_color: color::Packed, + + /// The shadow offset of the [`Quad`]. + pub shadow_offset: [f32; 2], + + /// The shadow blur radius of the [`Quad`]. + pub shadow_blur_radius: f32, +} + #[derive(Debug)] pub struct Pipeline { solid: solid::Pipeline, @@ -54,7 +80,7 @@ impl Pipeline { } } - pub fn prepare( + pub fn prepare_batch( &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, @@ -73,7 +99,64 @@ impl Pipeline { self.prepare_layer += 1; } - pub fn render<'a>( + pub fn prepare_cache( + &self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + cache: &mut Cache, + transformation: Transformation, + scale: f32, + ) { + match cache { + Cache::Staged(_) => { + let Cache::Staged(batch) = + std::mem::replace(cache, Cache::Staged(Batch::default())) + else { + unreachable!() + }; + + let mut layer = Layer::new(device, &self.constant_layout); + layer.prepare( + device, + encoder, + belt, + &batch, + transformation, + scale, + ); + + *cache = Cache::Uploaded { + layer, + batch, + needs_reupload: false, + } + } + + Cache::Uploaded { + batch, + layer, + needs_reupload, + } => { + if *needs_reupload { + layer.prepare( + device, + encoder, + belt, + batch, + transformation, + scale, + ); + + *needs_reupload = false; + } else { + layer.update(device, encoder, belt, transformation, scale); + } + } + } + } + + pub fn render_batch<'a>( &'a self, layer: usize, bounds: Rectangle, @@ -81,38 +164,59 @@ impl Pipeline { render_pass: &mut wgpu::RenderPass<'a>, ) { if let Some(layer) = self.layers.get(layer) { - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - - let mut solid_offset = 0; - let mut gradient_offset = 0; - - for (kind, count) in &quads.order { - match kind { - Kind::Solid => { - self.solid.render( - render_pass, - &layer.constants, - &layer.solid, - solid_offset..(solid_offset + count), - ); - - solid_offset += count; - } - Kind::Gradient => { - self.gradient.render( - render_pass, - &layer.constants, - &layer.gradient, - gradient_offset..(gradient_offset + count), - ); - - gradient_offset += count; - } + self.render(bounds, layer, &quads.order, render_pass); + } + } + + pub fn render_cache<'a>( + &'a self, + cache: &'a Cache, + bounds: Rectangle, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + if let Cache::Uploaded { layer, batch, .. } = cache { + self.render(bounds, layer, &batch.order, render_pass); + } + } + + fn render<'a>( + &'a self, + bounds: Rectangle, + layer: &'a Layer, + order: &Order, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + let mut solid_offset = 0; + let mut gradient_offset = 0; + + for (kind, count) in order { + match kind { + Kind::Solid => { + self.solid.render( + render_pass, + &layer.constants, + &layer.solid, + solid_offset..(solid_offset + count), + ); + + solid_offset += count; + } + Kind::Gradient => { + self.gradient.render( + render_pass, + &layer.constants, + &layer.gradient, + gradient_offset..(gradient_offset + count), + ); + + gradient_offset += count; } } } @@ -124,7 +228,49 @@ impl Pipeline { } #[derive(Debug)] -struct Layer { +pub enum Cache { + Staged(Batch), + Uploaded { + batch: Batch, + layer: Layer, + needs_reupload: bool, + }, +} + +impl Cache { + pub fn is_empty(&self) -> bool { + match self { + Cache::Staged(batch) | Cache::Uploaded { batch, .. } => { + batch.is_empty() + } + } + } + + pub fn update(&mut self, new_batch: Batch) { + match self { + Self::Staged(batch) => { + *batch = new_batch; + } + Self::Uploaded { + batch, + needs_reupload, + .. + } => { + *batch = new_batch; + *needs_reupload = true; + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::Staged(Batch::default()) + } +} + +#[derive(Debug)] +pub struct Layer { constants: wgpu::BindGroup, constants_buffer: wgpu::Buffer, solid: solid::Layer, @@ -169,9 +315,26 @@ impl Layer { transformation: Transformation, scale: f32, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Quad", "PREPARE").entered(); + self.update(device, encoder, belt, transformation, scale); + + if !quads.solids.is_empty() { + self.solid.prepare(device, encoder, belt, &quads.solids); + } + + if !quads.gradients.is_empty() { + self.gradient + .prepare(device, encoder, belt, &quads.gradients); + } + } + pub fn update( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + transformation: Transformation, + scale: f32, + ) { let uniforms = Uniforms::new(transformation, scale); let bytes = bytemuck::bytes_of(&uniforms); @@ -183,47 +346,9 @@ impl Layer { device, ) .copy_from_slice(bytes); - - if !quads.solids.is_empty() { - self.solid.prepare(device, encoder, belt, &quads.solids); - } - - if !quads.gradients.is_empty() { - self.gradient - .prepare(device, encoder, belt, &quads.gradients); - } } } -/// The properties of a quad. -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -#[repr(C)] -pub struct Quad { - /// The position of the [`Quad`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: color::Packed, - - /// The border radii of the [`Quad`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Quad`]. - pub border_width: f32, - - /// The shadow color of the [`Quad`]. - pub shadow_color: [f32; 4], - - /// The shadow offset of the [`Quad`]. - pub shadow_offset: [f32; 2], - - /// The shadow blur radius of the [`Quad`]. - pub shadow_blur_radius: f32, -} - /// A group of [`Quad`]s rendered together. #[derive(Default, Debug)] pub struct Batch { @@ -233,10 +358,13 @@ pub struct Batch { /// The gradient quads of the [`Layer`]. gradients: Vec, - /// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count. - order: Vec<(Kind, usize)>, + /// The quad order of the [`Layer`]. + order: Order, } +/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count. +type Order = Vec<(Kind, usize)>; + impl Batch { /// Returns true if there are no quads of any type in [`Quads`]. pub fn is_empty(&self) -> bool { @@ -276,6 +404,12 @@ impl Batch { } } } + + pub fn clear(&mut self) { + self.solids.clear(); + self.gradients.clear(); + self.order.clear(); + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 97ff77f5..016ac92a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,20 +1,67 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::cache::{self, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; -use crate::layer::Text; -use std::borrow::Cow; -use std::cell::RefCell; use std::sync::Arc; +pub use crate::graphics::Text; + +pub type Batch = Vec; + #[allow(missing_debug_implementations)] pub struct Pipeline { - renderers: Vec, + format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, + renderers: Vec, prepare_layer: usize, - cache: RefCell, + cache: BufferCache, +} + +pub enum Cache { + Staged(Batch), + Uploaded { + batch: Batch, + renderer: glyphon::TextRenderer, + atlas: Option, + buffer_cache: Option, + scale_factor: f32, + target_size: Size, + needs_reupload: bool, + }, +} + +impl Cache { + pub fn is_empty(&self) -> bool { + match self { + Cache::Staged(batch) | Cache::Uploaded { batch, .. } => { + batch.is_empty() + } + } + } + + pub fn update(&mut self, new_batch: Batch) { + match self { + Self::Staged(batch) => { + *batch = new_batch; + } + Self::Uploaded { + batch, + needs_reupload, + .. + } => { + *batch = new_batch; + *needs_reupload = true; + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::Staged(Batch::default()) + } } impl Pipeline { @@ -24,6 +71,7 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { + format, renderers: Vec::new(), atlas: glyphon::TextAtlas::with_color_mode( device, @@ -36,25 +84,16 @@ impl Pipeline { }, ), prepare_layer: 0, - cache: RefCell::new(Cache::new()), + cache: BufferCache::new(), } } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - font_system() - .write() - .expect("Write font system") - .load_font(bytes); - - self.cache = RefCell::new(Cache::new()); - } - - pub fn prepare( + pub fn prepare_batch( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, - sections: &[Text<'_>], + sections: &Batch, layer_bounds: Rectangle, scale_factor: f32, target_size: Size, @@ -68,210 +107,18 @@ impl Pipeline { )); } - let mut font_system = font_system().write().expect("Write font system"); - let font_system = font_system.raw(); - let renderer = &mut self.renderers[self.prepare_layer]; - let cache = self.cache.get_mut(); - - enum Allocation { - Paragraph(Paragraph), - Editor(Editor), - Cache(cache::KeyHash), - Raw(Arc), - } - - let allocations: Vec<_> = sections - .iter() - .map(|section| match section { - Text::Paragraph { paragraph, .. } => { - paragraph.upgrade().map(Allocation::Paragraph) - } - Text::Editor { editor, .. } => { - editor.upgrade().map(Allocation::Editor) - } - Text::Cached(text) => { - let (key, _) = cache.allocate( - font_system, - cache::Key { - content: text.content, - size: text.size.into(), - line_height: f32::from( - text.line_height.to_absolute(text.size), - ), - font: text.font, - bounds: Size { - width: text.bounds.width, - height: text.bounds.height, - }, - shaping: text.shaping, - }, - ); - - Some(Allocation::Cache(key)) - } - Text::Raw { raw, .. } => { - raw.buffer.upgrade().map(Allocation::Raw) - } - }) - .collect(); - - let layer_bounds = layer_bounds * scale_factor; - - let text_areas = sections.iter().zip(allocations.iter()).filter_map( - |(section, allocation)| { - let ( - buffer, - bounds, - horizontal_alignment, - vertical_alignment, - color, - clip_bounds, - transformation, - ) = match section { - Text::Paragraph { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Paragraph as _; - - let Some(Allocation::Paragraph(paragraph)) = allocation - else { - return None; - }; - - ( - paragraph.buffer(), - Rectangle::new(*position, paragraph.min_bounds()), - paragraph.horizontal_alignment(), - paragraph.vertical_alignment(), - *color, - *clip_bounds, - *transformation, - ) - } - Text::Editor { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Editor as _; - - let Some(Allocation::Editor(editor)) = allocation - else { - return None; - }; - - ( - editor.buffer(), - Rectangle::new(*position, editor.bounds()), - alignment::Horizontal::Left, - alignment::Vertical::Top, - *color, - *clip_bounds, - *transformation, - ) - } - Text::Cached(text) => { - let Some(Allocation::Cache(key)) = allocation else { - return None; - }; - - let entry = cache.get(key).expect("Get cached buffer"); - - ( - &entry.buffer, - Rectangle::new( - text.bounds.position(), - entry.min_bounds, - ), - text.horizontal_alignment, - text.vertical_alignment, - text.color, - text.clip_bounds, - Transformation::IDENTITY, - ) - } - Text::Raw { - raw, - transformation, - } => { - let Some(Allocation::Raw(buffer)) = allocation else { - return None; - }; - - let (width, height) = buffer.size(); - - ( - buffer.as_ref(), - Rectangle::new( - raw.position, - Size::new(width, height), - ), - alignment::Horizontal::Left, - alignment::Vertical::Top, - raw.color, - raw.clip_bounds, - *transformation, - ) - } - }; - - let bounds = bounds * transformation * scale_factor; - - let left = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - let top = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - let clip_bounds = layer_bounds.intersection( - &(clip_bounds * transformation * scale_factor), - )?; - - Some(glyphon::TextArea { - buffer, - left, - top, - scale: scale_factor * transformation.scale_factor(), - bounds: glyphon::TextBounds { - left: clip_bounds.x as i32, - top: clip_bounds.y as i32, - right: (clip_bounds.x + clip_bounds.width) as i32, - bottom: (clip_bounds.y + clip_bounds.height) as i32, - }, - default_color: to_color(color), - }) - }, - ); - - let result = renderer.prepare( + let result = prepare( device, queue, encoder, - font_system, + renderer, &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - text_areas, - &mut glyphon::SwashCache::new(), + &mut self.cache, + sections, + layer_bounds, + scale_factor, + target_size, ); match result { @@ -286,7 +133,109 @@ impl Pipeline { } } - pub fn render<'a>( + pub fn prepare_cache( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + cache: &mut Cache, + layer_bounds: Rectangle, + new_scale_factor: f32, + new_target_size: Size, + ) { + match cache { + Cache::Staged(_) => { + let Cache::Staged(batch) = + std::mem::replace(cache, Cache::Staged(Batch::default())) + else { + unreachable!() + }; + + // TODO: Find a better heuristic (?) + let (mut atlas, mut buffer_cache) = if batch.len() > 10 { + ( + Some(glyphon::TextAtlas::with_color_mode( + device, + queue, + self.format, + if color::GAMMA_CORRECTION { + glyphon::ColorMode::Accurate + } else { + glyphon::ColorMode::Web + }, + )), + Some(BufferCache::new()), + ) + } else { + (None, None) + }; + + let mut renderer = glyphon::TextRenderer::new( + atlas.as_mut().unwrap_or(&mut self.atlas), + device, + wgpu::MultisampleState::default(), + None, + ); + + let _ = prepare( + device, + queue, + encoder, + &mut renderer, + atlas.as_mut().unwrap_or(&mut self.atlas), + buffer_cache.as_mut().unwrap_or(&mut self.cache), + &batch, + layer_bounds, + new_scale_factor, + new_target_size, + ); + + *cache = Cache::Uploaded { + batch, + needs_reupload: false, + renderer, + atlas, + buffer_cache, + scale_factor: new_scale_factor, + target_size: new_target_size, + } + } + Cache::Uploaded { + batch, + needs_reupload, + renderer, + atlas, + buffer_cache, + scale_factor, + target_size, + } => { + if *needs_reupload + || atlas.is_none() + || buffer_cache.is_none() + || new_scale_factor != *scale_factor + || new_target_size != *target_size + { + let _ = prepare( + device, + queue, + encoder, + renderer, + atlas.as_mut().unwrap_or(&mut self.atlas), + buffer_cache.as_mut().unwrap_or(&mut self.cache), + batch, + layer_bounds, + *scale_factor, + *target_size, + ); + + *scale_factor = new_scale_factor; + *target_size = new_target_size; + } + } + } + } + + pub fn render_batch<'a>( &'a self, layer: usize, bounds: Rectangle, @@ -306,10 +255,251 @@ impl Pipeline { .expect("Render text"); } + pub fn render_cache<'a>( + &'a self, + cache: &'a Cache, + bounds: Rectangle, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + let Cache::Uploaded { + renderer, atlas, .. + } = cache + else { + return; + }; + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + renderer + .render(atlas.as_ref().unwrap_or(&self.atlas), render_pass) + .expect("Render text"); + } + pub fn end_frame(&mut self) { self.atlas.trim(); - self.cache.get_mut().trim(); + self.cache.trim(); self.prepare_layer = 0; } } + +fn prepare( + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut glyphon::TextRenderer, + atlas: &mut glyphon::TextAtlas, + buffer_cache: &mut BufferCache, + sections: &Batch, + layer_bounds: Rectangle, + scale_factor: f32, + target_size: Size, +) -> Result<(), glyphon::PrepareError> { + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + + enum Allocation { + Paragraph(Paragraph), + Editor(Editor), + Cache(cache::KeyHash), + Raw(Arc), + } + + let allocations: Vec<_> = sections + .iter() + .map(|section| match section { + Text::Paragraph { paragraph, .. } => { + paragraph.upgrade().map(Allocation::Paragraph) + } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } + Text::Cached { + content, + bounds, + size, + line_height, + font, + shaping, + .. + } => { + let (key, _) = buffer_cache.allocate( + font_system, + cache::Key { + content, + size: (*size).into(), + line_height: f32::from(line_height.to_absolute(*size)), + font: *font, + bounds: Size { + width: bounds.width, + height: bounds.height, + }, + shaping: *shaping, + }, + ); + + Some(Allocation::Cache(key)) + } + Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw), + }) + .collect(); + + let layer_bounds = layer_bounds * scale_factor; + + let text_areas = sections.iter().zip(allocations.iter()).filter_map( + |(section, allocation)| { + let ( + buffer, + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + transformation, + ) = match section { + Text::Paragraph { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Paragraph as _; + + let Some(Allocation::Paragraph(paragraph)) = allocation + else { + return None; + }; + + ( + paragraph.buffer(), + Rectangle::new(*position, paragraph.min_bounds()), + paragraph.horizontal_alignment(), + paragraph.vertical_alignment(), + *color, + *clip_bounds, + *transformation, + ) + } + Text::Editor { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + *clip_bounds, + *transformation, + ) + } + Text::Cached { + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + .. + } => { + let Some(Allocation::Cache(key)) = allocation else { + return None; + }; + + let entry = + buffer_cache.get(key).expect("Get cached buffer"); + + ( + &entry.buffer, + Rectangle::new(bounds.position(), entry.min_bounds), + *horizontal_alignment, + *vertical_alignment, + *color, + *clip_bounds, + Transformation::IDENTITY, + ) + } + Text::Raw { + raw, + transformation, + } => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new(raw.position, Size::new(width, height)), + alignment::Horizontal::Left, + alignment::Vertical::Top, + raw.color, + raw.clip_bounds, + *transformation, + ) + } + }; + + let bounds = bounds * transformation * scale_factor; + + let left = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - bounds.width / 2.0, + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let top = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - bounds.height / 2.0, + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let clip_bounds = layer_bounds + .intersection(&(clip_bounds * transformation * scale_factor))?; + + Some(glyphon::TextArea { + buffer, + left, + top, + scale: scale_factor * transformation.scale_factor(), + bounds: glyphon::TextBounds { + left: clip_bounds.x as i32, + top: clip_bounds.y as i32, + right: (clip_bounds.x + clip_bounds.width) as i32, + bottom: (clip_bounds.y + clip_bounds.height) as i32, + }, + default_color: to_color(color), + }) + }, + ); + + renderer.prepare( + device, + queue, + encoder, + font_system, + atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, + }, + text_areas, + &mut glyphon::SwashCache::new(), + ) +} diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index b6be54d4..6df97a7b 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,14 +1,16 @@ //! Draw meshes of triangles. mod msaa; -use crate::core::{Size, Transformation}; +use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::mesh::{self, Mesh}; use crate::graphics::Antialiasing; -use crate::layer::mesh::{self, Mesh}; use crate::Buffer; const INITIAL_INDEX_COUNT: usize = 1_000; const INITIAL_VERTEX_COUNT: usize = 1_000; +pub type Batch = Vec; + #[derive(Debug)] pub struct Pipeline { blit: Option, @@ -18,8 +20,270 @@ pub struct Pipeline { prepare_layer: usize, } +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option, + ) -> Pipeline { + Pipeline { + blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + solid: solid::Pipeline::new(device, format, antialiasing), + gradient: gradient::Pipeline::new(device, format, antialiasing), + layers: Vec::new(), + prepare_layer: 0, + } + } + + pub fn prepare_batch( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + meshes: &Batch, + transformation: Transformation, + ) { + if self.layers.len() <= self.prepare_layer { + self.layers + .push(Layer::new(device, &self.solid, &self.gradient)); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + meshes, + transformation, + ); + + self.prepare_layer += 1; + } + + pub fn prepare_cache( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + cache: &mut Cache, + transformation: Transformation, + ) { + match cache { + Cache::Staged(_) => { + let Cache::Staged(batch) = + std::mem::replace(cache, Cache::Staged(Batch::default())) + else { + unreachable!() + }; + + let mut layer = Layer::new(device, &self.solid, &self.gradient); + layer.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + &batch, + transformation, + ); + + *cache = Cache::Uploaded { + layer, + batch, + needs_reupload: false, + } + } + + Cache::Uploaded { + batch, + layer, + needs_reupload, + } => { + if *needs_reupload { + layer.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + batch, + transformation, + ); + + *needs_reupload = false; + } + } + } + } + + pub fn render_batch( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + layer: usize, + target_size: Size, + meshes: &Batch, + bounds: Rectangle, + scale_factor: f32, + ) { + Self::render( + device, + encoder, + target, + self.blit.as_mut(), + &self.solid, + &self.gradient, + &self.layers[layer], + target_size, + meshes, + bounds, + scale_factor, + ); + } + + pub fn render_cache( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + target_size: Size, + cache: &Cache, + bounds: Rectangle, + scale_factor: f32, + ) { + let Cache::Uploaded { batch, layer, .. } = cache else { + return; + }; + + Self::render( + device, + encoder, + target, + self.blit.as_mut(), + &self.solid, + &self.gradient, + layer, + target_size, + batch, + bounds, + scale_factor, + ); + } + + fn render( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + mut blit: Option<&mut msaa::Blit>, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + layer: &Layer, + target_size: Size, + meshes: &Batch, + bounds: Rectangle, + scale_factor: f32, + ) { + { + let (attachment, resolve_target, load) = if let Some(blit) = + &mut blit + { + let (attachment, resolve_target) = + blit.targets(device, target_size.width, target_size.height); + + ( + attachment, + Some(resolve_target), + wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + ) + } else { + (target, None, wgpu::LoadOp::Load) + }; + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + layer.render( + solid, + gradient, + meshes, + bounds, + scale_factor, + &mut render_pass, + ); + } + + if let Some(blit) = blit { + blit.draw(encoder, target); + } + } + + pub fn end_frame(&mut self) { + self.prepare_layer = 0; + } +} + +#[derive(Debug)] +pub enum Cache { + Staged(Batch), + Uploaded { + batch: Batch, + layer: Layer, + needs_reupload: bool, + }, +} + +impl Cache { + pub fn is_empty(&self) -> bool { + match self { + Cache::Staged(batch) | Cache::Uploaded { batch, .. } => { + batch.is_empty() + } + } + } + + pub fn update(&mut self, new_batch: Batch) { + match self { + Self::Staged(batch) => { + *batch = new_batch; + } + Self::Uploaded { + batch, + needs_reupload, + .. + } => { + *batch = new_batch; + *needs_reupload = true; + } + } + } +} + +impl Default for Cache { + fn default() -> Self { + Self::Staged(Batch::default()) + } +} + #[derive(Debug)] -struct Layer { +pub struct Layer { index_buffer: Buffer, index_strides: Vec, solid: solid::Layer, @@ -52,7 +316,7 @@ impl Layer { belt: &mut wgpu::util::StagingBelt, solid: &solid::Pipeline, gradient: &gradient::Pipeline, - meshes: &[Mesh<'_>], + meshes: &Batch, transformation: Transformation, ) { // Count the total amount of vertices & indices we need to handle @@ -100,7 +364,6 @@ impl Layer { for mesh in meshes { let indices = mesh.indices(); - let uniforms = Uniforms::new(transformation * mesh.transformation()); @@ -157,7 +420,8 @@ impl Layer { &'a self, solid: &'a solid::Pipeline, gradient: &'a gradient::Pipeline, - meshes: &[Mesh<'_>], + meshes: &Batch, + layer_bounds: Rectangle, scale_factor: f32, render_pass: &mut wgpu::RenderPass<'a>, ) { @@ -166,7 +430,12 @@ impl Layer { let mut last_is_solid = None; for (index, mesh) in meshes.iter().enumerate() { - let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); + let Some(clip_bounds) = Rectangle::::from(layer_bounds) + .intersection(&(mesh.clip_bounds() * scale_factor)) + .map(Rectangle::snap) + else { + continue; + }; if clip_bounds.width < 1 || clip_bounds.height < 1 { continue; @@ -234,119 +503,6 @@ impl Layer { } } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - antialiasing: Option, - ) -> Pipeline { - Pipeline { - blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), - solid: solid::Pipeline::new(device, format, antialiasing), - gradient: gradient::Pipeline::new(device, format, antialiasing), - layers: Vec::new(), - prepare_layer: 0, - } - } - - pub fn prepare( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - meshes: &[Mesh<'_>], - transformation: Transformation, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered(); - - if self.layers.len() <= self.prepare_layer { - self.layers - .push(Layer::new(device, &self.solid, &self.gradient)); - } - - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare( - device, - encoder, - belt, - &self.solid, - &self.gradient, - meshes, - transformation, - ); - - self.prepare_layer += 1; - } - - pub fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - layer: usize, - target_size: Size, - meshes: &[Mesh<'_>], - scale_factor: f32, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered(); - - { - let (attachment, resolve_target, load) = if let Some(blit) = - &mut self.blit - { - let (attachment, resolve_target) = - blit.targets(device, target_size.width, target_size.height); - - ( - attachment, - Some(resolve_target), - wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - ) - } else { - (target, None, wgpu::LoadOp::Load) - }; - - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu.triangle.render_pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: attachment, - resolve_target, - ops: wgpu::Operations { - load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - let layer = &mut self.layers[layer]; - - layer.render( - &self.solid, - &self.gradient, - meshes, - scale_factor, - &mut render_pass, - ); - } - - if let Some(blit) = &mut self.blit { - blit.draw(encoder, target); - } - } - - pub fn end_frame(&mut self) { - self.prepare_layer = 0; - } -} - fn fragment_target( texture_format: wgpu::TextureFormat, ) -> wgpu::ColorTargetState { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 482d705b..e2b01477 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -4,18 +4,19 @@ use crate::graphics::color; use crate::graphics::compositor; use crate::graphics::error; use crate::graphics::{self, Viewport}; -use crate::{Backend, Primitive, Renderer, Settings}; +use crate::{Engine, Renderer, Settings}; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { - settings: Settings, instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat, alpha_mode: wgpu::CompositeAlphaMode, + engine: Engine, + settings: Settings, } /// A compositor error. @@ -167,15 +168,24 @@ impl Compositor { match result { Ok((device, queue)) => { + let engine = Engine::new( + &adapter, + &device, + &queue, + format, + settings.antialiasing, + ); + return Ok(Compositor { instance, - settings, adapter, device, queue, format, alpha_mode, - }) + engine, + settings, + }); } Err(error) => { errors.push((required_limits, error)); @@ -185,17 +195,6 @@ impl Compositor { Err(Error::RequestDeviceFailed(errors)) } - - /// Creates a new rendering [`Backend`] for this [`Compositor`]. - pub fn create_backend(&self) -> Backend { - Backend::new( - &self.adapter, - &self.device, - &self.queue, - self.settings, - self.format, - ) - } } /// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and @@ -210,9 +209,8 @@ pub async fn new( /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. pub fn present>( compositor: &mut Compositor, - backend: &mut Backend, + renderer: &mut Renderer, surface: &mut wgpu::Surface<'static>, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, overlay: &[T], @@ -229,21 +227,21 @@ pub fn present>( .texture .create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), frame.texture.format(), view, - primitives, viewport, overlay, ); - // Submit work - let _submission = compositor.queue.submit(Some(encoder.finish())); - backend.recall(); + let _ = compositor.engine.submit(&compositor.queue, encoder); + + // Present the frame frame.present(); Ok(()) @@ -292,11 +290,7 @@ impl graphics::Compositor for Compositor { } fn create_renderer(&self) -> Self::Renderer { - Renderer::new( - self.create_backend(), - self.settings.default_font, - self.settings.default_text_size, - ) + Renderer::new(self.settings, &self.engine) } fn create_surface( @@ -328,7 +322,7 @@ impl graphics::Compositor for Compositor { &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: self.format, - present_mode: self.settings.present_mode, + present_mode: wgpu::PresentMode::Immediate, width, height, alpha_mode: self.alpha_mode, @@ -355,17 +349,7 @@ impl graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { - renderer.with_primitives(|backend, primitives| { - present( - self, - backend, - surface, - primitives, - viewport, - background_color, - overlay, - ) - }) + present(self, renderer, surface, viewport, background_color, overlay) } fn screenshot>( @@ -376,16 +360,7 @@ impl graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Vec { - renderer.with_primitives(|backend, primitives| { - screenshot( - self, - backend, - primitives, - viewport, - background_color, - overlay, - ) - }) + screenshot(self, renderer, viewport, background_color, overlay) } } @@ -393,19 +368,12 @@ impl graphics::Compositor for Compositor { /// /// Returns RGBA bytes of the texture data. pub fn screenshot>( - compositor: &Compositor, - backend: &mut Backend, - primitives: &[Primitive], + compositor: &mut Compositor, + renderer: &mut Renderer, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Vec { - let mut encoder = compositor.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu.offscreen.encoder"), - }, - ); - let dimensions = BufferDimensions::new(viewport.physical_size()); let texture_extent = wgpu::Extent3d { @@ -429,14 +397,20 @@ pub fn screenshot>( let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu.offscreen.encoder"), + }, + ); + + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), texture.format(), &view, - primitives, viewport, overlay, ); @@ -474,7 +448,7 @@ pub fn screenshot>( texture_extent, ); - let index = compositor.queue.submit(Some(encoder.finish())); + let index = compositor.engine.submit(&compositor.queue, encoder); let slice = output_buffer.slice(..); slice.map_async(wgpu::MapMode::Read, |_| {}); diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 84e9ac15..668c5372 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -651,7 +651,7 @@ where defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let state = tree.state.downcast_ref::(); @@ -767,8 +767,8 @@ where renderer.with_layer( Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, + width: (bounds.width + 2.0).min(viewport.width), + height: (bounds.height + 2.0).min(viewport.height), ..bounds }, |renderer| { -- cgit From 09af6773bdfe3039f6bf1720da945ae874496b81 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 21:09:59 +0200 Subject: Remove unused `layer` module in `iced_graphics` --- graphics/src/layer.rs | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 graphics/src/layer.rs diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs deleted file mode 100644 index 5b8aacab..00000000 --- a/graphics/src/layer.rs +++ /dev/null @@ -1,47 +0,0 @@ -pub trait Layer { - type Cache; - - fn new() -> Self; - - fn clear(&mut self); -} - -pub struct Recorder { - layers: Vec, - caches: Vec, - stack: Vec, - current: usize, -} - -enum Kind { - Fresh(usize), - Cache(usize), -} - -impl Recorder { - pub fn new() -> Self { - Self { - layers: vec![Layer::new()], - caches: Vec::new(), - stack: Vec::new(), - current: 0, - } - } - - pub fn fill_quad(&mut self) {} - - pub fn push_cache(&mut self, cache: T::Cache) { - self.caches.push(cache); - } - - pub fn clear(&mut self) { - self.caches.clear(); - self.stack.clear(); - - for mut layer in self.layers { - layer.clear(); - } - - self.current = 0; - } -} -- cgit From 88b72de282441367092b07f8075eb931eff495ad Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 22:13:00 +0200 Subject: Implement preliminary cache grouping for mesh primitives Due to AA, it's very expensive to render every cached layer independently. --- wgpu/src/lib.rs | 95 +++++++++++++++++++++++++++++++++------------------- wgpu/src/triangle.rs | 63 ++++++++++++++++++++++++---------- 2 files changed, 106 insertions(+), 52 deletions(-) diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 0e173e0a..b9869583 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -315,9 +315,10 @@ impl Renderer { // TODO: Can we avoid collecting here? let layers: Vec<_> = self.layers.iter().collect(); + let mut i = 0; - for layer in &layers { - match layer { + while i < layers.len() { + match layers[i] { Layer::Live(live) => { let bounds = live .bounds @@ -393,32 +394,54 @@ impl Renderer { image_layer += 1; } + + i += 1; } - Layer::Cached(cached) => { - let bounds = cached - .bounds - .map(|bounds| bounds * scale_factor) - .map(Rectangle::snap) - .unwrap_or(Rectangle::with_size(target_size)); + Layer::Cached(_) => { + let group_len = layers[i..] + .iter() + .position(|layer| matches!(layer, Layer::Live(_))) + .unwrap_or(layers.len()); - if !cached.quads.is_empty() { - engine.quad_pipeline.render_cache( - &cached.quads, - bounds, - &mut render_pass, - ); + let group = layers[i..i + group_len].iter().map(|layer| { + let Layer::Cached(cached) = layer else { + unreachable!() + }; + + let bounds = cached + .bounds + .map(|bounds| bounds * scale_factor) + .map(Rectangle::snap) + .unwrap_or(Rectangle::with_size(target_size)); + + (cached, bounds) + }); + + for (cached, bounds) in group.clone() { + if !cached.quads.is_empty() { + engine.quad_pipeline.render_cache( + &cached.quads, + bounds, + &mut render_pass, + ); + } } - if !cached.meshes.is_empty() { + let group_has_meshes = group + .clone() + .any(|(cached, _)| !cached.meshes.is_empty()); + + if group_has_meshes { let _ = ManuallyDrop::into_inner(render_pass); - engine.triangle_pipeline.render_cache( + engine.triangle_pipeline.render_cache_group( device, encoder, frame, target_size, - &cached.meshes, - bounds, + group.clone().map(|(cached, bounds)| { + (&cached.meshes, bounds) + }), scale_factor, ); @@ -443,24 +466,28 @@ impl Renderer { )); } - if !cached.text.is_empty() { - engine.text_pipeline.render_cache( - &cached.text, - bounds, - &mut render_pass, - ); + for (cached, bounds) in group { + if !cached.text.is_empty() { + engine.text_pipeline.render_cache( + &cached.text, + bounds, + &mut render_pass, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !cached.images.is_empty() { + engine.image_pipeline.render( + image_layer, + bounds, + &mut render_pass, + ); + + image_layer += 1; + } } - #[cfg(any(feature = "svg", feature = "image"))] - if !cached.images.is_empty() { - engine.image_pipeline.render( - image_layer, - bounds, - &mut render_pass, - ); - - image_layer += 1; - } + i += group_len; } } } diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 6df97a7b..9cd02f72 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -136,14 +136,13 @@ impl Pipeline { self.blit.as_mut(), &self.solid, &self.gradient, - &self.layers[layer], target_size, - meshes, - bounds, + std::iter::once((&self.layers[layer], meshes, bounds)), scale_factor, ); } + #[allow(dead_code)] pub fn render_cache( &mut self, device: &wgpu::Device, @@ -165,25 +164,51 @@ impl Pipeline { self.blit.as_mut(), &self.solid, &self.gradient, - layer, target_size, - batch, - bounds, + std::iter::once((layer, batch, bounds)), scale_factor, ); } - fn render( + pub fn render_cache_group<'a>( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + target_size: Size, + group: impl Iterator)>, + scale_factor: f32, + ) { + let group = group.filter_map(|(cache, bounds)| { + if let Cache::Uploaded { batch, layer, .. } = cache { + Some((layer, batch, bounds)) + } else { + None + } + }); + + Self::render( + device, + encoder, + target, + self.blit.as_mut(), + &self.solid, + &self.gradient, + target_size, + group, + scale_factor, + ); + } + + fn render<'a>( device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, mut blit: Option<&mut msaa::Blit>, solid: &solid::Pipeline, gradient: &gradient::Pipeline, - layer: &Layer, target_size: Size, - meshes: &Batch, - bounds: Rectangle, + group: impl Iterator)>, scale_factor: f32, ) { { @@ -220,14 +245,16 @@ impl Pipeline { occlusion_query_set: None, }); - layer.render( - solid, - gradient, - meshes, - bounds, - scale_factor, - &mut render_pass, - ); + for (layer, meshes, bounds) in group { + layer.render( + solid, + gradient, + meshes, + bounds, + scale_factor, + &mut render_pass, + ); + } } if let Some(blit) = blit { -- cgit From 1672b0d619a87fcbe8aa6c6699ac6b20f9a6181b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 22:57:28 +0200 Subject: Reintroduce debug overlay in `iced_wgpu` --- wgpu/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b9869583..2d023d8b 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -120,9 +120,7 @@ impl Renderer { let scale_factor = viewport.scale_factor() as f32; let transformation = viewport.projection(); - for line in overlay { - println!("{}", line.as_ref()); - } + self.draw_overlay(overlay, viewport); self.prepare( engine, @@ -494,6 +492,50 @@ impl Renderer { let _ = ManuallyDrop::into_inner(render_pass); } + + fn draw_overlay( + &mut self, + overlay: &[impl AsRef], + viewport: &Viewport, + ) { + use crate::core::alignment; + use crate::core::text::Renderer as _; + use crate::core::Renderer as _; + use crate::core::Vector; + + self.with_layer( + Rectangle::with_size(viewport.logical_size()), + |renderer| { + for (i, line) in overlay.iter().enumerate() { + let text = crate::core::Text { + content: line.as_ref().to_owned(), + bounds: viewport.logical_size(), + size: Pixels(20.0), + line_height: core::text::LineHeight::default(), + font: Font::MONOSPACE, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: core::text::Shaping::Basic, + }; + + renderer.fill_text( + text.clone(), + Point::new(11.0, 11.0 + 25.0 * i as f32), + Color::new(0.9, 0.9, 0.9, 1.0), + Rectangle::with_size(Size::INFINITY), + ); + + renderer.fill_text( + text, + Point::new(11.0, 11.0 + 25.0 * i as f32) + + Vector::new(-1.0, -1.0), + Color::BLACK, + Rectangle::with_size(Size::INFINITY), + ); + } + }, + ); + } } impl core::Renderer for Renderer { -- cgit From e2c129c057b521009d451e474a6820601877cf11 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 23:14:16 +0200 Subject: Fix `geometry::Cache` not reusing previous geometry --- graphics/src/geometry/cache.rs | 39 ++++++++++++++++++++++++--------------- wgpu/src/triangle.rs | 1 - 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs index ebbafd14..665e996b 100644 --- a/graphics/src/geometry/cache.rs +++ b/graphics/src/geometry/cache.rs @@ -22,13 +22,20 @@ where /// Creates a new empty [`Cache`]. pub fn new() -> Self { Cache { - state: RefCell::new(State::Empty), + state: RefCell::new(State::Empty { previous: None }), } } /// Clears the [`Cache`], forcing a redraw the next time it is used. pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; + use std::ops::Deref; + + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { geometry, .. } => Some(geometry.clone()), + }; + + *self.state.borrow_mut() = State::Empty { previous }; } /// Draws geometry using the provided closure and stores it in the @@ -49,18 +56,18 @@ where ) -> Renderer::Geometry { use std::ops::Deref; - let previous = if let State::Filled { - bounds: cached_bounds, - geometry, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Cached::load(geometry); - } + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { + bounds: cached_bounds, + geometry, + } => { + if *cached_bounds == bounds { + return Cached::load(geometry); + } - Some(geometry.clone()) - } else { - None + Some(geometry.clone()) + } }; let mut frame = Frame::new(renderer, bounds); @@ -83,7 +90,7 @@ where let state = self.state.borrow(); match *state { - State::Empty => write!(f, "Cache::Empty"), + State::Empty { .. } => write!(f, "Cache::Empty"), State::Filled { bounds, .. } => { write!(f, "Cache::Filled {{ bounds: {bounds:?} }}") } @@ -104,7 +111,9 @@ enum State where Geometry: Cached, { - Empty, + Empty { + previous: Option, + }, Filled { bounds: Size, geometry: Geometry::Cache, diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 9cd02f72..be7bb867 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -95,7 +95,6 @@ impl Pipeline { needs_reupload: false, } } - Cache::Uploaded { batch, layer, -- cgit From 346ea313fd41d893627bfe1a4dba351713c07ea4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 23:26:08 +0200 Subject: Set proper `present_mode` in `iced_wgpu` --- wgpu/src/window/compositor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index e2b01477..55490c39 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -322,12 +322,12 @@ impl graphics::Compositor for Compositor { &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: self.format, - present_mode: wgpu::PresentMode::Immediate, + present_mode: self.settings.present_mode, width, height, alpha_mode: self.alpha_mode, view_formats: vec![], - desired_maximum_frame_latency: 2, + desired_maximum_frame_latency: 1, }, ); } -- cgit From d461f23e8dd34c5de73e0aa176a3301b01564652 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 23:31:13 +0200 Subject: Use default tolerance for dashed paths in `iced_wgpu` --- wgpu/src/geometry.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 7c8c0a35..d153c764 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -626,7 +626,9 @@ pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { let mut draw_line = false; walk_along_path( - path.raw().iter().flattened(0.01), + path.raw().iter().flattened( + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + ), 0.0, lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, &mut RepeatedPattern { -- cgit From cc05cb9be4a1de5f0427f93ce64e658be0703f8b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 3 Apr 2024 23:39:38 +0200 Subject: Fix broken doc links in `iced_wgpu` and `iced_graphics` --- graphics/src/image.rs | 2 ++ wgpu/src/settings.rs | 14 +++++++------- wgpu/src/window/compositor.rs | 11 +++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/graphics/src/image.rs b/graphics/src/image.rs index e8626717..47a7b30e 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -35,6 +35,8 @@ pub enum Image { #[cfg(feature = "image")] /// Tries to load an image by its [`Handle`]. +/// +/// [`Handle`]: image::Handle pub fn load( handle: &image::Handle, ) -> ::image::ImageResult<::image::DynamicImage> { diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 828d9e09..a6aea0a5 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -2,18 +2,18 @@ use crate::core::{Font, Pixels}; use crate::graphics::{self, Antialiasing}; -/// The settings of a [`Backend`]. +/// The settings of a [`Renderer`]. /// -/// [`Backend`]: crate::Backend +/// [`Renderer`]: crate::Renderer #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { - /// The present mode of the [`Backend`]. + /// The present mode of the [`Renderer`]. /// - /// [`Backend`]: crate::Backend + /// [`Renderer`]: crate::Renderer pub present_mode: wgpu::PresentMode, - /// The internal graphics backend to use. - pub internal_backend: wgpu::Backends, + /// The graphics backends to use. + pub backends: wgpu::Backends, /// The default [`Font`] to use. pub default_font: Font, @@ -33,7 +33,7 @@ impl Default for Settings { fn default() -> Settings { Settings { present_mode: wgpu::PresentMode::AutoVsync, - internal_backend: wgpu::Backends::all(), + backends: wgpu::Backends::all(), default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: None, diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 55490c39..d546a6dc 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -54,7 +54,7 @@ impl Compositor { compatible_window: Option, ) -> Result { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: settings.internal_backend, + backends: settings.backends, ..Default::default() }); @@ -63,7 +63,7 @@ impl Compositor { #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance - .enumerate_adapters(settings.internal_backend) + .enumerate_adapters(settings.backends) .iter() .map(wgpu::Adapter::get_info) .collect(); @@ -197,8 +197,7 @@ impl Compositor { } } -/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and -/// window. +/// Creates a [`Compositor`] with the given [`Settings`] and window. pub async fn new( settings: Settings, compatible_window: W, @@ -206,7 +205,7 @@ pub async fn new( Compositor::request(settings, Some(compatible_window)).await } -/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. +/// Presents the given primitives with the given [`Compositor`]. pub fn present>( compositor: &mut Compositor, renderer: &mut Renderer, @@ -273,7 +272,7 @@ impl graphics::Compositor for Compositor { match backend { None | Some("wgpu") => Ok(new( Settings { - internal_backend: wgpu::util::backend_bits_from_env() + backends: wgpu::util::backend_bits_from_env() .unwrap_or(wgpu::Backends::all()), ..settings.into() }, -- cgit From 394e599c3a096b036aabdd6f850c4a7c518d44fa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 5 Apr 2024 00:40:39 +0200 Subject: Fix layer transformations --- wgpu/src/geometry.rs | 39 +++++++------ wgpu/src/layer.rs | 65 ++++++++++----------- wgpu/src/lib.rs | 158 +++++++++++++++++++++++++++------------------------ wgpu/src/text.rs | 36 ++++++------ wgpu/src/triangle.rs | 72 +++++++++++++++-------- widget/src/canvas.rs | 6 +- 6 files changed, 209 insertions(+), 167 deletions(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index d153c764..611e81f1 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -106,23 +106,28 @@ impl Frame { .buffers .stack .into_iter() - .map(|buffer| match buffer { - Buffer::Solid(buffer) => Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - transformation: Transformation::IDENTITY, - size: self.size, - }, - Buffer::Gradient(buffer) => Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - transformation: Transformation::IDENTITY, - size: self.size, - }, + .filter_map(|buffer| match buffer { + Buffer::Solid(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + transformation: Transformation::IDENTITY, + size: self.size, + }) + } + Buffer::Gradient(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + transformation: Transformation::IDENTITY, + size: self.size, + }) + } + _ => None, }) .collect(); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 5c23669a..1c9638b0 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -13,17 +13,17 @@ use std::rc::Rc; pub enum Layer<'a> { Live(&'a Live), - Cached(cell::Ref<'a, Cached>), + Cached(Transformation, cell::Ref<'a, Cached>), } pub enum LayerMut<'a> { Live(&'a mut Live), - Cached(cell::RefMut<'a, Cached>), + Cached(Transformation, cell::RefMut<'a, Cached>), } pub struct Stack { live: Vec, - cached: Vec>>, + cached: Vec<(Transformation, Rc>)>, order: Vec, transformations: Vec, previous: Vec, @@ -92,7 +92,7 @@ impl Stack { position, color, clip_bounds, - transformation: self.transformations.last().copied().unwrap(), + transformation: self.transformation(), }; self.live[self.current].text.push(paragraph); @@ -178,36 +178,31 @@ impl Stack { } pub fn draw_cached_layer(&mut self, layer: &Rc>) { - { - let mut layer = layer.borrow_mut(); - layer.transformation = self.transformation() * layer.transformation; - } - - self.cached.push(layer.clone()); + self.cached.push((self.transformation(), layer.clone())); self.order.push(Kind::Cache); } pub fn push_clip(&mut self, bounds: Option) { - self.previous.push(self.current); - self.order.push(Kind::Live); - - self.current = self.live_count; - self.live_count += 1; - - let bounds = bounds.map(|bounds| bounds * self.transformation()); - - if self.current == self.live.len() { - self.live.push(Live { - bounds, - ..Live::default() - }); - } else { - self.live[self.current].bounds = bounds; - } + // self.previous.push(self.current); + // self.order.push(Kind::Live); + + // self.current = self.live_count; + // self.live_count += 1; + + // let bounds = bounds.map(|bounds| bounds * self.transformation()); + + // if self.current == self.live.len() { + // self.live.push(Live { + // bounds, + // ..Live::default() + // }); + // } else { + // self.live[self.current].bounds = bounds; + // } } pub fn pop_clip(&mut self) { - self.current = self.previous.pop().unwrap(); + // self.current = self.previous.pop().unwrap(); } pub fn push_transformation(&mut self, transformation: Transformation) { @@ -224,13 +219,17 @@ impl Stack { } pub fn iter_mut(&mut self) -> impl Iterator> { + dbg!(self.order.len()); let mut live = self.live.iter_mut(); let mut cached = self.cached.iter_mut(); self.order.iter().map(move |kind| match kind { Kind::Live => LayerMut::Live(live.next().unwrap()), Kind::Cache => { - LayerMut::Cached(cached.next().unwrap().borrow_mut()) + let (transformation, layer) = cached.next().unwrap(); + let layer = layer.borrow_mut(); + + LayerMut::Cached(*transformation * layer.transformation, layer) } }) } @@ -241,7 +240,12 @@ impl Stack { self.order.iter().map(move |kind| match kind { Kind::Live => Layer::Live(live.next().unwrap()), - Kind::Cache => Layer::Cached(cached.next().unwrap().borrow()), + Kind::Cache => { + let (transformation, layer) = cached.next().unwrap(); + let layer = layer.borrow(); + + Layer::Cached(*transformation * layer.transformation, layer) + } }) } @@ -288,7 +292,6 @@ impl Live { Cached { bounds: self.bounds, transformation: self.transformation, - last_transformation: None, quads: quad::Cache::Staged(self.quads), meshes: triangle::Cache::Staged(self.meshes), text: text::Cache::Staged(self.text), @@ -301,7 +304,6 @@ impl Live { pub struct Cached { pub bounds: Option, pub transformation: Transformation, - pub last_transformation: Option, pub quads: quad::Cache, pub meshes: triangle::Cache, pub text: text::Cache, @@ -311,7 +313,6 @@ pub struct Cached { impl Cached { pub fn update(&mut self, live: Live) { self.bounds = live.bounds; - self.transformation = live.transformation; self.quads.update(live.quads); self.meshes.update(live.meshes); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 2d023d8b..4705cfa0 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -116,32 +116,10 @@ impl Renderer { viewport: &Viewport, overlay: &[T], ) { - let target_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; - let transformation = viewport.projection(); - self.draw_overlay(overlay, viewport); - self.prepare( - engine, - device, - queue, - format, - encoder, - scale_factor, - target_size, - transformation, - ); - - self.render( - engine, - device, - encoder, - frame, - clear_color, - scale_factor, - target_size, - ); + self.prepare(engine, device, queue, format, encoder, viewport); + self.render(engine, device, encoder, frame, clear_color, viewport); } fn prepare( @@ -151,10 +129,10 @@ impl Renderer { queue: &wgpu::Queue, _format: wgpu::TextureFormat, encoder: &mut wgpu::CommandEncoder, - scale_factor: f32, - target_size: Size, - transformation: Transformation, + viewport: &Viewport, ) { + let scale_factor = viewport.scale_factor() as f32; + for layer in self.layers.iter_mut() { match layer { LayerMut::Live(live) => { @@ -164,7 +142,7 @@ impl Renderer { encoder, &mut engine.staging_belt, &live.quads, - transformation, + viewport.projection(), scale_factor, ); } @@ -175,7 +153,7 @@ impl Renderer { encoder, &mut engine.staging_belt, &live.meshes, - transformation + viewport.projection() * Transformation::scale(scale_factor), ); } @@ -187,10 +165,11 @@ impl Renderer { encoder, &live.text, live.bounds.unwrap_or(Rectangle::with_size( - Size::INFINITY, + viewport.logical_size(), )), - scale_factor, - target_size, + live.transformation + * Transformation::scale(scale_factor), + viewport.physical_size(), ); } @@ -201,38 +180,46 @@ impl Renderer { encoder, &mut engine.staging_belt, &live.images, - transformation, + viewport.projection(), scale_factor, ); } } - LayerMut::Cached(mut cached) => { + LayerMut::Cached(layer_transformation, mut cached) => { if !cached.quads.is_empty() { engine.quad_pipeline.prepare_cache( device, encoder, &mut engine.staging_belt, &mut cached.quads, - transformation, + viewport.projection(), scale_factor, ); } if !cached.meshes.is_empty() { + let transformation = + Transformation::scale(scale_factor) + * layer_transformation; + engine.triangle_pipeline.prepare_cache( device, encoder, &mut engine.staging_belt, &mut cached.meshes, - transformation - * Transformation::scale(scale_factor), + viewport.projection(), + transformation, ); } if !cached.text.is_empty() { - let bounds = cached - .bounds - .unwrap_or(Rectangle::with_size(Size::INFINITY)); + let bounds = cached.bounds.unwrap_or( + Rectangle::with_size(viewport.logical_size()), + ); + + let transformation = + Transformation::scale(scale_factor) + * layer_transformation; engine.text_pipeline.prepare_cache( device, @@ -240,8 +227,8 @@ impl Renderer { encoder, &mut cached.text, bounds, - scale_factor, - target_size, + transformation, + viewport.physical_size(), ); } @@ -252,7 +239,7 @@ impl Renderer { encoder, &mut engine.staging_belt, &cached.images, - transformation, + viewport.projection(), scale_factor, ); } @@ -268,8 +255,7 @@ impl Renderer { encoder: &mut wgpu::CommandEncoder, frame: &wgpu::TextureView, clear_color: Option, - scale_factor: f32, - target_size: Size, + viewport: &Viewport, ) { use std::mem::ManuallyDrop; @@ -312,22 +298,37 @@ impl Renderer { let mut image_layer = 0; // TODO: Can we avoid collecting here? + let scale_factor = viewport.scale_factor() as f32; + let screen_bounds = Rectangle::with_size(viewport.logical_size()); + let physical_bounds = Rectangle::::from(Rectangle::with_size( + viewport.physical_size(), + )); + let layers: Vec<_> = self.layers.iter().collect(); let mut i = 0; + // println!("RENDER"); + while i < layers.len() { match layers[i] { Layer::Live(live) => { - let bounds = live - .bounds - .map(|bounds| bounds * scale_factor) + let layer_transformation = + Transformation::scale(scale_factor) + * live.transformation; + + let layer_bounds = live.bounds.unwrap_or(screen_bounds); + + let Some(physical_bounds) = physical_bounds + .intersection(&(layer_bounds * layer_transformation)) .map(Rectangle::snap) - .unwrap_or(Rectangle::with_size(target_size)); + else { + continue; + }; if !live.quads.is_empty() { engine.quad_pipeline.render_batch( quad_layer, - bounds, + physical_bounds, &live.quads, &mut render_pass, ); @@ -336,6 +337,7 @@ impl Renderer { } if !live.meshes.is_empty() { + // println!("LIVE PASS"); let _ = ManuallyDrop::into_inner(render_pass); engine.triangle_pipeline.render_batch( @@ -343,10 +345,10 @@ impl Renderer { encoder, frame, mesh_layer, - target_size, + viewport.physical_size(), &live.meshes, - bounds, - scale_factor, + physical_bounds, + &layer_transformation, ); mesh_layer += 1; @@ -375,7 +377,7 @@ impl Renderer { if !live.text.is_empty() { engine.text_pipeline.render_batch( text_layer, - bounds, + physical_bounds, &mut render_pass, ); @@ -386,7 +388,7 @@ impl Renderer { if !live.images.is_empty() { engine.image_pipeline.render( image_layer, - bounds, + physical_bounds, &mut render_pass, ); @@ -395,25 +397,35 @@ impl Renderer { i += 1; } - Layer::Cached(_) => { + Layer::Cached(_, _) => { let group_len = layers[i..] .iter() .position(|layer| matches!(layer, Layer::Live(_))) - .unwrap_or(layers.len()); - - let group = layers[i..i + group_len].iter().map(|layer| { - let Layer::Cached(cached) = layer else { - unreachable!() - }; - - let bounds = cached - .bounds - .map(|bounds| bounds * scale_factor) - .map(Rectangle::snap) - .unwrap_or(Rectangle::with_size(target_size)); - - (cached, bounds) - }); + .unwrap_or(layers.len() - i); + + let group = + layers[i..i + group_len].iter().filter_map(|layer| { + let Layer::Cached(transformation, cached) = layer + else { + unreachable!() + }; + + let physical_bounds = cached + .bounds + .and_then(|bounds| { + physical_bounds.intersection( + &(bounds + * *transformation + * Transformation::scale( + scale_factor, + )), + ) + }) + .unwrap_or(physical_bounds) + .snap(); + + Some((cached, physical_bounds)) + }); for (cached, bounds) in group.clone() { if !cached.quads.is_empty() { @@ -430,17 +442,17 @@ impl Renderer { .any(|(cached, _)| !cached.meshes.is_empty()); if group_has_meshes { + // println!("CACHE PASS"); let _ = ManuallyDrop::into_inner(render_pass); engine.triangle_pipeline.render_cache_group( device, encoder, frame, - target_size, + viewport.physical_size(), group.clone().map(|(cached, bounds)| { (&cached.meshes, bounds) }), - scale_factor, ); render_pass = diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 016ac92a..e0a5355c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -26,7 +26,7 @@ pub enum Cache { renderer: glyphon::TextRenderer, atlas: Option, buffer_cache: Option, - scale_factor: f32, + transformation: Transformation, target_size: Size, needs_reupload: bool, }, @@ -95,7 +95,7 @@ impl Pipeline { encoder: &mut wgpu::CommandEncoder, sections: &Batch, layer_bounds: Rectangle, - scale_factor: f32, + layer_transformation: Transformation, target_size: Size, ) { if self.renderers.len() <= self.prepare_layer { @@ -117,7 +117,7 @@ impl Pipeline { &mut self.cache, sections, layer_bounds, - scale_factor, + layer_transformation, target_size, ); @@ -140,7 +140,7 @@ impl Pipeline { encoder: &mut wgpu::CommandEncoder, cache: &mut Cache, layer_bounds: Rectangle, - new_scale_factor: f32, + new_transformation: Transformation, new_target_size: Size, ) { match cache { @@ -186,7 +186,7 @@ impl Pipeline { buffer_cache.as_mut().unwrap_or(&mut self.cache), &batch, layer_bounds, - new_scale_factor, + new_transformation, new_target_size, ); @@ -196,7 +196,7 @@ impl Pipeline { renderer, atlas, buffer_cache, - scale_factor: new_scale_factor, + transformation: new_transformation, target_size: new_target_size, } } @@ -206,13 +206,13 @@ impl Pipeline { renderer, atlas, buffer_cache, - scale_factor, + transformation, target_size, } => { if *needs_reupload || atlas.is_none() || buffer_cache.is_none() - || new_scale_factor != *scale_factor + || new_transformation != *transformation || new_target_size != *target_size { let _ = prepare( @@ -224,11 +224,11 @@ impl Pipeline { buffer_cache.as_mut().unwrap_or(&mut self.cache), batch, layer_bounds, - *scale_factor, - *target_size, + new_transformation, + new_target_size, ); - *scale_factor = new_scale_factor; + *transformation = new_transformation; *target_size = new_target_size; } } @@ -297,7 +297,7 @@ fn prepare( buffer_cache: &mut BufferCache, sections: &Batch, layer_bounds: Rectangle, - scale_factor: f32, + layer_transformation: Transformation, target_size: Size, ) -> Result<(), glyphon::PrepareError> { let mut font_system = font_system().write().expect("Write font system"); @@ -349,7 +349,7 @@ fn prepare( }) .collect(); - let layer_bounds = layer_bounds * scale_factor; + let layer_bounds = layer_bounds * layer_transformation; let text_areas = sections.iter().zip(allocations.iter()).filter_map( |(section, allocation)| { @@ -456,7 +456,7 @@ fn prepare( } }; - let bounds = bounds * transformation * scale_factor; + let bounds = bounds * transformation * layer_transformation; let left = match horizontal_alignment { alignment::Horizontal::Left => bounds.x, @@ -470,14 +470,16 @@ fn prepare( alignment::Vertical::Bottom => bounds.y - bounds.height, }; - let clip_bounds = layer_bounds - .intersection(&(clip_bounds * transformation * scale_factor))?; + let clip_bounds = layer_bounds.intersection( + &(clip_bounds * transformation * layer_transformation), + )?; Some(glyphon::TextArea { buffer, left, top, - scale: scale_factor * transformation.scale_factor(), + scale: transformation.scale_factor() + * layer_transformation.scale_factor(), bounds: glyphon::TextBounds { left: clip_bounds.x as i32, top: clip_bounds.y as i32, diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index be7bb867..3a184da2 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -68,8 +68,11 @@ impl Pipeline { encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, cache: &mut Cache, - transformation: Transformation, + new_projection: Transformation, + new_transformation: Transformation, ) { + let new_projection = new_projection * new_transformation; + match cache { Cache::Staged(_) => { let Cache::Staged(batch) = @@ -86,21 +89,25 @@ impl Pipeline { &self.solid, &self.gradient, &batch, - transformation, + new_projection, ); *cache = Cache::Uploaded { layer, batch, + transformation: new_transformation, + projection: new_projection, needs_reupload: false, } } Cache::Uploaded { batch, layer, + transformation, + projection, needs_reupload, } => { - if *needs_reupload { + if *needs_reupload || new_projection != *projection { layer.prepare( device, encoder, @@ -108,9 +115,11 @@ impl Pipeline { &self.solid, &self.gradient, batch, - transformation, + new_projection, ); + *transformation = new_transformation; + *projection = new_projection; *needs_reupload = false; } } @@ -126,7 +135,7 @@ impl Pipeline { target_size: Size, meshes: &Batch, bounds: Rectangle, - scale_factor: f32, + transformation: &Transformation, ) { Self::render( device, @@ -136,8 +145,12 @@ impl Pipeline { &self.solid, &self.gradient, target_size, - std::iter::once((&self.layers[layer], meshes, bounds)), - scale_factor, + std::iter::once(( + &self.layers[layer], + meshes, + transformation, + bounds, + )), ); } @@ -150,9 +163,14 @@ impl Pipeline { target_size: Size, cache: &Cache, bounds: Rectangle, - scale_factor: f32, ) { - let Cache::Uploaded { batch, layer, .. } = cache else { + let Cache::Uploaded { + batch, + layer, + transformation, + .. + } = cache + else { return; }; @@ -164,8 +182,7 @@ impl Pipeline { &self.solid, &self.gradient, target_size, - std::iter::once((layer, batch, bounds)), - scale_factor, + std::iter::once((layer, batch, transformation, bounds)), ); } @@ -176,11 +193,16 @@ impl Pipeline { target: &wgpu::TextureView, target_size: Size, group: impl Iterator)>, - scale_factor: f32, ) { let group = group.filter_map(|(cache, bounds)| { - if let Cache::Uploaded { batch, layer, .. } = cache { - Some((layer, batch, bounds)) + if let Cache::Uploaded { + batch, + layer, + transformation, + .. + } = cache + { + Some((layer, batch, transformation, bounds)) } else { None } @@ -195,7 +217,6 @@ impl Pipeline { &self.gradient, target_size, group, - scale_factor, ); } @@ -207,8 +228,9 @@ impl Pipeline { solid: &solid::Pipeline, gradient: &gradient::Pipeline, target_size: Size, - group: impl Iterator)>, - scale_factor: f32, + group: impl Iterator< + Item = (&'a Layer, &'a Batch, &'a Transformation, Rectangle), + >, ) { { let (attachment, resolve_target, load) = if let Some(blit) = @@ -244,13 +266,13 @@ impl Pipeline { occlusion_query_set: None, }); - for (layer, meshes, bounds) in group { + for (layer, meshes, transformation, bounds) in group { layer.render( solid, gradient, meshes, bounds, - scale_factor, + *transformation, &mut render_pass, ); } @@ -272,6 +294,8 @@ pub enum Cache { Uploaded { batch: Batch, layer: Layer, + transformation: Transformation, + projection: Transformation, needs_reupload: bool, }, } @@ -390,6 +414,7 @@ impl Layer { for mesh in meshes { let indices = mesh.indices(); + let uniforms = Uniforms::new(transformation * mesh.transformation()); @@ -448,7 +473,7 @@ impl Layer { gradient: &'a gradient::Pipeline, meshes: &Batch, layer_bounds: Rectangle, - scale_factor: f32, + transformation: Transformation, render_pass: &mut wgpu::RenderPass<'a>, ) { let mut num_solids = 0; @@ -457,15 +482,12 @@ impl Layer { for (index, mesh) in meshes.iter().enumerate() { let Some(clip_bounds) = Rectangle::::from(layer_bounds) - .intersection(&(mesh.clip_bounds() * scale_factor)) - .map(Rectangle::snap) + .intersection(&(mesh.clip_bounds() * transformation)) else { continue; }; - if clip_bounds.width < 1 || clip_bounds.height < 1 { - continue; - } + let clip_bounds = clip_bounds.snap(); render_pass.set_scissor_rect( clip_bounds.x, diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 7a21895a..42f92de0 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -17,7 +17,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Length, Rectangle, Shell, Size, Transformation, Widget, + Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; use crate::graphics::geometry; @@ -222,8 +222,8 @@ where let state = tree.state.downcast_ref::(); - renderer.with_transformation( - Transformation::translate(bounds.x, bounds.y), + renderer.with_translation( + Vector::new(bounds.x, bounds.y), |renderer| { let layers = self.program.draw(state, renderer, theme, bounds, cursor); -- cgit From 4a356cfc16f3b45d64826732009d9feeac016b28 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 5 Apr 2024 01:24:34 +0200 Subject: Enable clipping and disable v-sync for now --- wgpu/src/layer.rs | 35 +++++++++++++++++------------------ wgpu/src/text.rs | 1 + wgpu/src/window/compositor.rs | 4 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 1c9638b0..d415da72 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -183,26 +183,26 @@ impl Stack { } pub fn push_clip(&mut self, bounds: Option) { - // self.previous.push(self.current); - // self.order.push(Kind::Live); - - // self.current = self.live_count; - // self.live_count += 1; - - // let bounds = bounds.map(|bounds| bounds * self.transformation()); - - // if self.current == self.live.len() { - // self.live.push(Live { - // bounds, - // ..Live::default() - // }); - // } else { - // self.live[self.current].bounds = bounds; - // } + self.previous.push(self.current); + self.order.push(Kind::Live); + + self.current = self.live_count; + self.live_count += 1; + + let bounds = bounds.map(|bounds| bounds * self.transformation()); + + if self.current == self.live.len() { + self.live.push(Live { + bounds, + ..Live::default() + }); + } else { + self.live[self.current].bounds = bounds; + } } pub fn pop_clip(&mut self) { - // self.current = self.previous.pop().unwrap(); + self.current = self.previous.pop().unwrap(); } pub fn push_transformation(&mut self, transformation: Transformation) { @@ -219,7 +219,6 @@ impl Stack { } pub fn iter_mut(&mut self) -> impl Iterator> { - dbg!(self.order.len()); let mut live = self.live.iter_mut(); let mut cached = self.cached.iter_mut(); diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e0a5355c..a7695b74 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -230,6 +230,7 @@ impl Pipeline { *transformation = new_transformation; *target_size = new_target_size; + *needs_reupload = false; } } } diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index d546a6dc..ab441fa0 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -321,12 +321,12 @@ impl graphics::Compositor for Compositor { &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: self.format, - present_mode: self.settings.present_mode, + present_mode: wgpu::PresentMode::Immediate, width, height, alpha_mode: self.alpha_mode, view_formats: vec![], - desired_maximum_frame_latency: 1, + desired_maximum_frame_latency: 0, }, ); } -- cgit From 6d3e1d835e1688fbc58622a03a784ed25ed3f0e1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 5 Apr 2024 23:59:21 +0200 Subject: Decouple caching from layering and simplify everything --- core/src/rectangle.rs | 5 +- examples/geometry/src/main.rs | 2 +- graphics/src/geometry/frame.rs | 16 +- graphics/src/mesh.rs | 16 +- graphics/src/text.rs | 4 +- renderer/src/fallback.rs | 8 +- tiny_skia/src/geometry.rs | 4 +- wgpu/src/geometry.rs | 188 +++++++-------- wgpu/src/image/mod.rs | 7 +- wgpu/src/layer.rs | 258 +++++++++++---------- wgpu/src/lib.rs | 444 +++++++++++------------------------- wgpu/src/quad.rs | 188 +++------------ wgpu/src/text.rs | 427 +++++++++++++++++----------------- wgpu/src/triangle.rs | 503 +++++++++++++++++++++-------------------- widget/src/text_input.rs | 9 +- 15 files changed, 888 insertions(+), 1191 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 45acd5ac..446d3769 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -33,9 +33,12 @@ where } impl Rectangle { + /// A rectangle starting at [`Point::ORIGIN`] with infinite width and height. + pub const INFINITE: Self = Self::new(Point::ORIGIN, Size::INFINITY); + /// Creates a new [`Rectangle`] with its top-left corner in the given /// [`Point`] and with the provided [`Size`]. - pub fn new(top_left: Point, size: Size) -> Self { + pub const fn new(top_left: Point, size: Size) -> Self { Self { x: top_left.x, y: top_left.y, diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 9532a24a..31b8a4ce 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -82,7 +82,6 @@ mod rainbow { let posn_l = [0.0, bounds.height / 2.0]; let mesh = Mesh::Solid { - size: bounds.size(), buffers: mesh::Indexed { vertices: vec![ SolidVertex2D { @@ -134,6 +133,7 @@ mod rainbow { ], }, transformation: Transformation::IDENTITY, + clip_bounds: Rectangle::INFINITE, }; renderer.with_translation( diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index ad35e8ec..377589d7 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -113,13 +113,11 @@ where region: Rectangle, f: impl FnOnce(&mut Self) -> R, ) -> R { - let mut frame = self.draft(region.size()); + let mut frame = self.draft(region); let result = f(&mut frame); - let origin = Point::new(region.x, region.y); - - self.paste(frame, origin); + self.paste(frame, Point::new(region.x, region.y)); result } @@ -129,14 +127,14 @@ where /// Draw its contents back to this [`Frame`] with [`paste`]. /// /// [`paste`]: Self::paste - pub fn draft(&mut self, size: Size) -> Self { + fn draft(&mut self, clip_bounds: Rectangle) -> Self { Self { - raw: self.raw.draft(size), + raw: self.raw.draft(clip_bounds), } } /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. - pub fn paste(&mut self, frame: Self, at: Point) { + fn paste(&mut self, frame: Self, at: Point) { self.raw.paste(frame.raw, at); } @@ -187,7 +185,7 @@ pub trait Backend: Sized { fn scale(&mut self, scale: impl Into); fn scale_nonuniform(&mut self, scale: impl Into); - fn draft(&mut self, size: Size) -> Self; + fn draft(&mut self, clip_bounds: Rectangle) -> Self; fn paste(&mut self, frame: Self, at: Point); fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>); @@ -232,7 +230,7 @@ impl Backend for () { fn scale(&mut self, _scale: impl Into) {} fn scale_nonuniform(&mut self, _scale: impl Into) {} - fn draft(&mut self, _size: Size) -> Self {} + fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} fn paste(&mut self, _frame: Self, _at: Point) {} fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into>) {} diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs index d3e7ffaf..76602319 100644 --- a/graphics/src/mesh.rs +++ b/graphics/src/mesh.rs @@ -1,6 +1,6 @@ //! Draw triangles! use crate::color; -use crate::core::{Rectangle, Size, Transformation}; +use crate::core::{Rectangle, Transformation}; use crate::gradient; use bytemuck::{Pod, Zeroable}; @@ -16,8 +16,8 @@ pub enum Mesh { /// The [`Transformation`] for the vertices of the [`Mesh`]. transformation: Transformation, - /// The [`Size`] of the [`Mesh`]. - size: Size, + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, }, /// A mesh with a gradient. Gradient { @@ -27,8 +27,8 @@ pub enum Mesh { /// The [`Transformation`] for the vertices of the [`Mesh`]. transformation: Transformation, - /// The [`Size`] of the [`Mesh`]. - size: Size, + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, }, } @@ -53,15 +53,15 @@ impl Mesh { pub fn clip_bounds(&self) -> Rectangle { match self { Self::Solid { - size, + clip_bounds, transformation, .. } | Self::Gradient { - size, + clip_bounds, transformation, .. - } => Rectangle::with_size(*size) * *transformation, + } => *clip_bounds * *transformation, } } } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index c9c821c0..f9fc1fec 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -11,7 +11,7 @@ pub use cosmic_text; use crate::core::alignment; use crate::core::font::{self, Font}; -use crate::core::text::{LineHeight, Shaping}; +use crate::core::text::Shaping; use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; @@ -50,7 +50,7 @@ pub enum Text { /// The size of the text in logical pixels. size: Pixels, /// The line height of the text. - line_height: LineHeight, + line_height: Pixels, /// The font of the text. font: Font, /// The horizontal alignment of the text. diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index b6459243..60b57604 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -405,7 +405,7 @@ where #[cfg(feature = "geometry")] mod geometry { use super::Renderer; - use crate::core::{Point, Radians, Size, Vector}; + use crate::core::{Point, Radians, Rectangle, Size, Vector}; use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; use crate::graphics::Cached; @@ -533,10 +533,10 @@ mod geometry { delegate!(self, frame, frame.pop_transform()); } - fn draft(&mut self, size: Size) -> Self { + fn draft(&mut self, bounds: Rectangle) -> Self { match self { - Self::Left(frame) => Self::Left(frame.draft(size)), - Self::Right(frame) => Self::Right(frame.draft(size)), + Self::Left(frame) => Self::Left(frame.draft(bounds)), + Self::Right(frame) => Self::Right(frame.draft(bounds)), } } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 76482e12..f1d11dce 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -195,8 +195,8 @@ impl geometry::frame::Backend for Frame { self.transform = self.stack.pop().expect("Pop transform"); } - fn draft(&mut self, size: Size) -> Self { - Self::new(size) + fn draft(&mut self, clip_bounds: Rectangle) -> Self { + Self::new(clip_bounds.size()) } fn paste(&mut self, frame: Self, at: Point) { diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 611e81f1..c8c350c5 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -6,40 +6,44 @@ use crate::core::{ use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ - self, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, + self, LineCap, LineDash, LineJoin, Path, Stroke, Style, }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Cached}; -use crate::layer; +use crate::graphics::{self, Cached, Text}; use crate::text; +use crate::triangle; use lyon::geom::euclid; use lyon::tessellation; use std::borrow::Cow; -use std::cell::RefCell; -use std::rc::Rc; /// A frame for drawing some geometry. #[allow(missing_debug_implementations)] pub struct Frame { - size: Size, + clip_bounds: Rectangle, buffers: BufferStack, - layers: Vec, - text: text::Batch, + meshes: Vec, + text: Vec, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, stroke_tessellator: tessellation::StrokeTessellator, } pub enum Geometry { - Live(Vec), - Cached(Rc<[Rc>]>), + Live { meshes: Vec, text: Vec }, + Cached(Cache), +} + +#[derive(Clone)] +pub struct Cache { + pub meshes: triangle::Cache, + pub text: text::Cache, } impl Cached for Geometry { - type Cache = Rc<[Rc>]>; + type Cache = Cache; fn load(cache: &Self::Cache) -> Self { Geometry::Cached(cache.clone()) @@ -47,31 +51,18 @@ impl Cached for Geometry { fn cache(self, previous: Option) -> Self::Cache { match self { - Self::Live(live) => { - let mut layers = live.into_iter(); - - let mut new: Vec<_> = previous - .map(|previous| { - previous - .iter() - .cloned() - .zip(layers.by_ref()) - .map(|(cached, live)| { - cached.borrow_mut().update(live); - cached - }) - .collect() - }) - .unwrap_or_default(); - - new.extend( - layers - .map(layer::Live::into_cached) - .map(RefCell::new) - .map(Rc::new), - ); - - Rc::from(new) + Self::Live { meshes, text } => { + if let Some(mut previous) = previous { + previous.meshes.update(meshes); + previous.text.update(text); + + previous + } else { + Cache { + meshes: triangle::Cache::new(meshes), + text: text::Cache::new(text), + } + } } Self::Cached(cache) => cache, } @@ -81,69 +72,26 @@ impl Cached for Geometry { impl Frame { /// Creates a new [`Frame`] with the given [`Size`]. pub fn new(size: Size) -> Frame { + Self::with_clip(Rectangle::with_size(size)) + } + + /// Creates a new [`Frame`] with the given clip bounds. + pub fn with_clip(bounds: Rectangle) -> Frame { Frame { - size, + clip_bounds: bounds, buffers: BufferStack::new(), - layers: Vec::new(), - text: text::Batch::new(), + meshes: Vec::new(), + text: Vec::new(), transforms: Transforms { previous: Vec::new(), - current: Transform(lyon::math::Transform::identity()), + current: Transform(lyon::math::Transform::translation( + bounds.x, bounds.y, + )), }, fill_tessellator: tessellation::FillTessellator::new(), stroke_tessellator: tessellation::StrokeTessellator::new(), } } - - fn into_layers(mut self) -> Vec { - if !self.text.is_empty() || !self.buffers.stack.is_empty() { - let clip_bounds = Rectangle::with_size(self.size); - let transformation = Transformation::IDENTITY; - - // TODO: Generate different meshes for different transformations (?) - // Instead of transforming each path - let meshes = self - .buffers - .stack - .into_iter() - .filter_map(|buffer| match buffer { - Buffer::Solid(buffer) if !buffer.indices.is_empty() => { - Some(Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - transformation: Transformation::IDENTITY, - size: self.size, - }) - } - Buffer::Gradient(buffer) if !buffer.indices.is_empty() => { - Some(Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - transformation: Transformation::IDENTITY, - size: self.size, - }) - } - _ => None, - }) - .collect(); - - let layer = layer::Live { - bounds: Some(clip_bounds), - transformation, - meshes, - text: self.text, - ..layer::Live::default() - }; - - self.layers.push(layer); - } - - self.layers - } } impl geometry::frame::Backend for Frame { @@ -151,22 +99,22 @@ impl geometry::frame::Backend for Frame { #[inline] fn width(&self) -> f32 { - self.size.width + self.clip_bounds.width } #[inline] fn height(&self) -> f32 { - self.size.height + self.clip_bounds.height } #[inline] fn size(&self) -> Size { - self.size + self.clip_bounds.size() } #[inline] fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) } fn fill(&mut self, path: &Path, fill: impl Into) { @@ -269,7 +217,7 @@ impl geometry::frame::Backend for Frame { .expect("Stroke path"); } - fn fill_text(&mut self, text: impl Into) { + fn fill_text(&mut self, text: impl Into) { let text = text.into(); let (scale_x, scale_y) = self.transforms.current.scale(); @@ -312,12 +260,12 @@ impl geometry::frame::Backend for Frame { bounds, color: text.color, size, - line_height, + line_height: line_height.to_absolute(size), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), + clip_bounds: self.clip_bounds, }); } else { text.draw_with(|path, color| self.fill(&path, color)); @@ -368,22 +316,25 @@ impl geometry::frame::Backend for Frame { self.transforms.current = self.transforms.previous.pop().unwrap(); } - fn draft(&mut self, size: Size) -> Frame { - Frame::new(size) + fn draft(&mut self, clip_bounds: Rectangle) -> Frame { + Frame::with_clip(clip_bounds) } - fn paste(&mut self, frame: Frame, at: Point) { - let translation = Transformation::translate(at.x, at.y); + fn paste(&mut self, frame: Frame, _at: Point) { + self.meshes + .extend(frame.buffers.into_meshes(frame.clip_bounds)); - self.layers - .extend(frame.into_layers().into_iter().map(|mut layer| { - layer.transformation = layer.transformation * translation; - layer - })); + self.text.extend(frame.text); } - fn into_geometry(self) -> Self::Geometry { - Geometry::Live(self.into_layers()) + fn into_geometry(mut self) -> Self::Geometry { + self.meshes + .extend(self.buffers.into_meshes(self.clip_bounds)); + + Geometry::Live { + meshes: self.meshes, + text: self.text, + } } } @@ -469,6 +420,27 @@ impl BufferStack { _ => unreachable!(), } } + + fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator { + self.stack.into_iter().map(move |buffer| match buffer { + Buffer::Solid(buffer) => Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }, + Buffer::Gradient(buffer) => Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }, + }) + } } #[derive(Debug)] diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 88e6bdb9..86731cbf 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -9,7 +9,6 @@ mod raster; #[cfg(feature = "svg")] mod vector; -use crate::core::image; use crate::core::{Rectangle, Size, Transformation}; use crate::Buffer; @@ -234,10 +233,12 @@ impl Pipeline { [bounds.width, bounds.height], atlas_entry, match filter_method { - image::FilterMethod::Nearest => { + crate::core::image::FilterMethod::Nearest => { nearest_instances } - image::FilterMethod::Linear => linear_instances, + crate::core::image::FilterMethod::Linear => { + linear_instances + } }, ); } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index d415da72..4c864cb0 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -8,39 +8,46 @@ use crate::quad::{self, Quad}; use crate::text::{self, Text}; use crate::triangle; -use std::cell::{self, RefCell}; -use std::rc::Rc; - -pub enum Layer<'a> { - Live(&'a Live), - Cached(Transformation, cell::Ref<'a, Cached>), +pub struct Layer { + pub bounds: Rectangle, + pub quads: quad::Batch, + pub triangles: triangle::Batch, + pub text: text::Batch, + pub images: image::Batch, } -pub enum LayerMut<'a> { - Live(&'a mut Live), - Cached(Transformation, cell::RefMut<'a, Cached>), +impl Default for Layer { + fn default() -> Self { + Self { + bounds: Rectangle::INFINITE, + quads: quad::Batch::default(), + triangles: triangle::Batch::default(), + text: text::Batch::default(), + images: image::Batch::default(), + } + } } pub struct Stack { - live: Vec, - cached: Vec<(Transformation, Rc>)>, - order: Vec, + layers: Vec, transformations: Vec, previous: Vec, + pending_meshes: Vec>, + pending_text: Vec>, current: usize, - live_count: usize, + active_count: usize, } impl Stack { pub fn new() -> Self { Self { - live: vec![Live::default()], - cached: Vec::new(), - order: vec![Kind::Live], + layers: vec![Layer::default()], transformations: vec![Transformation::IDENTITY], - previous: Vec::new(), + previous: vec![], + pending_meshes: vec![Vec::new()], + pending_text: vec![Vec::new()], current: 0, - live_count: 1, + active_count: 1, } } @@ -59,7 +66,7 @@ impl Stack { shadow_blur_radius: quad.shadow.blur_radius, }; - self.live[self.current].quads.add(quad, &background); + self.layers[self.current].quads.add(quad, &background); } pub fn draw_paragraph( @@ -77,7 +84,7 @@ impl Stack { transformation: self.transformations.last().copied().unwrap(), }; - self.live[self.current].text.push(paragraph); + self.pending_text[self.current].push(paragraph); } pub fn draw_editor( @@ -87,7 +94,7 @@ impl Stack { color: Color, clip_bounds: Rectangle, ) { - let paragraph = Text::Editor { + let editor = Text::Editor { editor: editor.downgrade(), position, color, @@ -95,7 +102,7 @@ impl Stack { transformation: self.transformation(), }; - self.live[self.current].text.push(paragraph); + self.pending_text[self.current].push(editor); } pub fn draw_text( @@ -107,12 +114,13 @@ impl Stack { ) { let transformation = self.transformation(); - let paragraph = Text::Cached { + let text = Text::Cached { content: text.content, bounds: Rectangle::new(position, text.bounds) * transformation, color, size: text.size * transformation.scale_factor(), - line_height: text.line_height, + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, @@ -120,7 +128,7 @@ impl Stack { clip_bounds: clip_bounds * transformation, }; - self.live[self.current].text.push(paragraph); + self.pending_text[self.current].push(text); } pub fn draw_image( @@ -135,7 +143,7 @@ impl Stack { bounds: bounds * self.transformation(), }; - self.live[self.current].images.push(image); + self.layers[self.current].images.push(image); } pub fn draw_svg( @@ -150,7 +158,7 @@ impl Stack { bounds: bounds * self.transformation(), }; - self.live[self.current].images.push(svg); + self.layers[self.current].images.push(svg); } pub fn draw_mesh(&mut self, mut mesh: Mesh) { @@ -161,51 +169,86 @@ impl Stack { } } - self.live[self.current].meshes.push(mesh); + self.pending_meshes[self.current].push(mesh); } - pub fn draw_layer(&mut self, mut layer: Live) { - layer.transformation = layer.transformation * self.transformation(); + pub fn draw_mesh_group(&mut self, meshes: Vec) { + self.flush_pending(); - if self.live_count == self.live.len() { - self.live.push(layer); - } else { - self.live[self.live_count] = layer; - } + let transformation = self.transformation(); - self.live_count += 1; - self.order.push(Kind::Live); + self.layers[self.current] + .triangles + .push(triangle::Item::Group { + transformation, + meshes, + }); } - pub fn draw_cached_layer(&mut self, layer: &Rc>) { - self.cached.push((self.transformation(), layer.clone())); - self.order.push(Kind::Cache); + pub fn draw_mesh_cache(&mut self, cache: triangle::Cache) { + self.flush_pending(); + + let transformation = self.transformation(); + + self.layers[self.current] + .triangles + .push(triangle::Item::Cached { + transformation, + cache, + }); + } + + pub fn draw_text_group(&mut self, text: Vec) { + self.flush_pending(); + + let transformation = self.transformation(); + + self.layers[self.current].text.push(text::Item::Group { + transformation, + text, + }); + } + + pub fn draw_text_cache(&mut self, cache: text::Cache) { + self.flush_pending(); + + let transformation = self.transformation(); + + self.layers[self.current].text.push(text::Item::Cached { + transformation, + cache, + }); } - pub fn push_clip(&mut self, bounds: Option) { + pub fn push_clip(&mut self, bounds: Rectangle) { self.previous.push(self.current); - self.order.push(Kind::Live); - self.current = self.live_count; - self.live_count += 1; + self.current = self.active_count; + self.active_count += 1; - let bounds = bounds.map(|bounds| bounds * self.transformation()); + let bounds = bounds * self.transformation(); - if self.current == self.live.len() { - self.live.push(Live { + if self.current == self.layers.len() { + self.layers.push(Layer { bounds, - ..Live::default() + ..Layer::default() }); + self.pending_meshes.push(Vec::new()); + self.pending_text.push(Vec::new()); } else { - self.live[self.current].bounds = bounds; + self.layers[self.current].bounds = bounds; } } pub fn pop_clip(&mut self) { + self.flush_pending(); + self.current = self.previous.pop().unwrap(); } pub fn push_transformation(&mut self, transformation: Transformation) { + self.flush_pending(); + self.transformations .push(self.transformation() * transformation); } @@ -218,109 +261,62 @@ impl Stack { self.transformations.last().copied().unwrap() } - pub fn iter_mut(&mut self) -> impl Iterator> { - let mut live = self.live.iter_mut(); - let mut cached = self.cached.iter_mut(); - - self.order.iter().map(move |kind| match kind { - Kind::Live => LayerMut::Live(live.next().unwrap()), - Kind::Cache => { - let (transformation, layer) = cached.next().unwrap(); - let layer = layer.borrow_mut(); + pub fn iter_mut(&mut self) -> impl Iterator { + self.flush_pending(); - LayerMut::Cached(*transformation * layer.transformation, layer) - } - }) + self.layers[..self.active_count].iter_mut() } - pub fn iter(&self) -> impl Iterator> { - let mut live = self.live.iter(); - let mut cached = self.cached.iter(); - - self.order.iter().map(move |kind| match kind { - Kind::Live => Layer::Live(live.next().unwrap()), - Kind::Cache => { - let (transformation, layer) = cached.next().unwrap(); - let layer = layer.borrow(); - - Layer::Cached(*transformation * layer.transformation, layer) - } - }) + pub fn iter(&self) -> impl Iterator { + self.layers[..self.active_count].iter() } pub fn clear(&mut self) { - for live in &mut self.live[..self.live_count] { - live.bounds = None; - live.transformation = Transformation::IDENTITY; + for (live, pending_meshes) in self.layers[..self.active_count] + .iter_mut() + .zip(self.pending_meshes.iter_mut()) + { + live.bounds = Rectangle::INFINITE; live.quads.clear(); - live.meshes.clear(); + live.triangles.clear(); live.text.clear(); live.images.clear(); + pending_meshes.clear(); } self.current = 0; - self.live_count = 1; - - self.order.clear(); - self.order.push(Kind::Live); - - self.cached.clear(); + self.active_count = 1; self.previous.clear(); } -} -impl Default for Stack { - fn default() -> Self { - Self::new() - } -} + // We want to keep the allocated memory + #[allow(clippy::drain_collect)] + fn flush_pending(&mut self) { + let transformation = self.transformation(); -#[derive(Default)] -pub struct Live { - pub bounds: Option, - pub transformation: Transformation, - pub quads: quad::Batch, - pub meshes: triangle::Batch, - pub text: text::Batch, - pub images: image::Batch, -} + let pending_meshes = &mut self.pending_meshes[self.current]; + if !pending_meshes.is_empty() { + self.layers[self.current] + .triangles + .push(triangle::Item::Group { + transformation, + meshes: pending_meshes.drain(..).collect(), + }); + } -impl Live { - pub fn into_cached(self) -> Cached { - Cached { - bounds: self.bounds, - transformation: self.transformation, - quads: quad::Cache::Staged(self.quads), - meshes: triangle::Cache::Staged(self.meshes), - text: text::Cache::Staged(self.text), - images: self.images, + let pending_text = &mut self.pending_text[self.current]; + if !pending_text.is_empty() { + self.layers[self.current].text.push(text::Item::Group { + transformation, + text: pending_text.drain(..).collect(), + }); } } } -#[derive(Default)] -pub struct Cached { - pub bounds: Option, - pub transformation: Transformation, - pub quads: quad::Cache, - pub meshes: triangle::Cache, - pub text: text::Cache, - pub images: image::Batch, -} - -impl Cached { - pub fn update(&mut self, live: Live) { - self.bounds = live.bounds; - - self.quads.update(live.quads); - self.meshes.update(live.meshes); - self.text.update(live.text); - self.images = live.images; +impl Default for Stack { + fn default() -> Self { + Self::new() } } - -enum Kind { - Live, - Cache, -} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 4705cfa0..d632919f 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -60,7 +60,7 @@ pub use iced_graphics::core; pub use wgpu; pub use engine::Engine; -pub use layer::{Layer, LayerMut}; +pub use layer::Layer; pub use primitive::Primitive; pub use settings::Settings; @@ -85,6 +85,9 @@ pub struct Renderer { default_text_size: Pixels, layers: layer::Stack, + triangle_storage: triangle::Storage, + text_storage: text::Storage, + // TODO: Centralize all the image feature handling #[cfg(any(feature = "svg", feature = "image"))] image_cache: image::cache::Shared, @@ -97,6 +100,9 @@ impl Renderer { default_text_size: settings.default_text_size, layers: layer::Stack::new(), + triangle_storage: triangle::Storage::new(), + text_storage: text::Storage::new(), + #[cfg(any(feature = "svg", feature = "image"))] image_cache: _engine.image_cache().clone(), } @@ -117,9 +123,11 @@ impl Renderer { overlay: &[T], ) { self.draw_overlay(overlay, viewport); - self.prepare(engine, device, queue, format, encoder, viewport); self.render(engine, device, encoder, frame, clear_color, viewport); + + self.triangle_storage.trim(); + self.text_storage.trim(); } fn prepare( @@ -134,116 +142,51 @@ impl Renderer { let scale_factor = viewport.scale_factor() as f32; for layer in self.layers.iter_mut() { - match layer { - LayerMut::Live(live) => { - if !live.quads.is_empty() { - engine.quad_pipeline.prepare_batch( - device, - encoder, - &mut engine.staging_belt, - &live.quads, - viewport.projection(), - scale_factor, - ); - } - - if !live.meshes.is_empty() { - engine.triangle_pipeline.prepare_batch( - device, - encoder, - &mut engine.staging_belt, - &live.meshes, - viewport.projection() - * Transformation::scale(scale_factor), - ); - } - - if !live.text.is_empty() { - engine.text_pipeline.prepare_batch( - device, - queue, - encoder, - &live.text, - live.bounds.unwrap_or(Rectangle::with_size( - viewport.logical_size(), - )), - live.transformation - * Transformation::scale(scale_factor), - viewport.physical_size(), - ); - } - - #[cfg(any(feature = "svg", feature = "image"))] - if !live.images.is_empty() { - engine.image_pipeline.prepare( - device, - encoder, - &mut engine.staging_belt, - &live.images, - viewport.projection(), - scale_factor, - ); - } - } - LayerMut::Cached(layer_transformation, mut cached) => { - if !cached.quads.is_empty() { - engine.quad_pipeline.prepare_cache( - device, - encoder, - &mut engine.staging_belt, - &mut cached.quads, - viewport.projection(), - scale_factor, - ); - } - - if !cached.meshes.is_empty() { - let transformation = - Transformation::scale(scale_factor) - * layer_transformation; - - engine.triangle_pipeline.prepare_cache( - device, - encoder, - &mut engine.staging_belt, - &mut cached.meshes, - viewport.projection(), - transformation, - ); - } - - if !cached.text.is_empty() { - let bounds = cached.bounds.unwrap_or( - Rectangle::with_size(viewport.logical_size()), - ); - - let transformation = - Transformation::scale(scale_factor) - * layer_transformation; - - engine.text_pipeline.prepare_cache( - device, - queue, - encoder, - &mut cached.text, - bounds, - transformation, - viewport.physical_size(), - ); - } - - #[cfg(any(feature = "svg", feature = "image"))] - if !cached.images.is_empty() { - engine.image_pipeline.prepare( - device, - encoder, - &mut engine.staging_belt, - &cached.images, - viewport.projection(), - scale_factor, - ); - } - } + if !layer.quads.is_empty() { + engine.quad_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &layer.quads, + viewport.projection(), + scale_factor, + ); + } + + if !layer.triangles.is_empty() { + engine.triangle_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &mut self.triangle_storage, + &layer.triangles, + viewport.projection() * Transformation::scale(scale_factor), + ); + } + + if !layer.text.is_empty() { + engine.text_pipeline.prepare( + device, + queue, + encoder, + &mut self.text_storage, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + viewport.physical_size(), + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &layer.images, + viewport.projection(), + scale_factor, + ); } } } @@ -297,208 +240,87 @@ impl Renderer { #[cfg(any(feature = "svg", feature = "image"))] let mut image_layer = 0; - // TODO: Can we avoid collecting here? let scale_factor = viewport.scale_factor() as f32; - let screen_bounds = Rectangle::with_size(viewport.logical_size()); let physical_bounds = Rectangle::::from(Rectangle::with_size( viewport.physical_size(), )); - let layers: Vec<_> = self.layers.iter().collect(); - let mut i = 0; + let scale = Transformation::scale(scale_factor); - // println!("RENDER"); + for layer in self.layers.iter() { + let Some(physical_bounds) = + physical_bounds.intersection(&(layer.bounds * scale)) + else { + continue; + }; - while i < layers.len() { - match layers[i] { - Layer::Live(live) => { - let layer_transformation = - Transformation::scale(scale_factor) - * live.transformation; + let scissor_rect = physical_bounds.snap(); - let layer_bounds = live.bounds.unwrap_or(screen_bounds); + if !layer.quads.is_empty() { + engine.quad_pipeline.render( + quad_layer, + scissor_rect, + &layer.quads, + &mut render_pass, + ); - let Some(physical_bounds) = physical_bounds - .intersection(&(layer_bounds * layer_transformation)) - .map(Rectangle::snap) - else { - continue; - }; + quad_layer += 1; + } - if !live.quads.is_empty() { - engine.quad_pipeline.render_batch( - quad_layer, - physical_bounds, - &live.quads, - &mut render_pass, - ); - - quad_layer += 1; - } - - if !live.meshes.is_empty() { - // println!("LIVE PASS"); - let _ = ManuallyDrop::into_inner(render_pass); - - engine.triangle_pipeline.render_batch( - device, - encoder, - frame, - mesh_layer, - viewport.physical_size(), - &live.meshes, - physical_bounds, - &layer_transformation, - ); - - mesh_layer += 1; - - render_pass = - ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: frame, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - - if !live.text.is_empty() { - engine.text_pipeline.render_batch( - text_layer, - physical_bounds, - &mut render_pass, - ); - - text_layer += 1; - } - - #[cfg(any(feature = "svg", feature = "image"))] - if !live.images.is_empty() { - engine.image_pipeline.render( - image_layer, - physical_bounds, - &mut render_pass, - ); - - image_layer += 1; - } - - i += 1; - } - Layer::Cached(_, _) => { - let group_len = layers[i..] - .iter() - .position(|layer| matches!(layer, Layer::Live(_))) - .unwrap_or(layers.len() - i); - - let group = - layers[i..i + group_len].iter().filter_map(|layer| { - let Layer::Cached(transformation, cached) = layer - else { - unreachable!() - }; - - let physical_bounds = cached - .bounds - .and_then(|bounds| { - physical_bounds.intersection( - &(bounds - * *transformation - * Transformation::scale( - scale_factor, - )), - ) - }) - .unwrap_or(physical_bounds) - .snap(); - - Some((cached, physical_bounds)) - }); - - for (cached, bounds) in group.clone() { - if !cached.quads.is_empty() { - engine.quad_pipeline.render_cache( - &cached.quads, - bounds, - &mut render_pass, - ); - } - } - - let group_has_meshes = group - .clone() - .any(|(cached, _)| !cached.meshes.is_empty()); - - if group_has_meshes { - // println!("CACHE PASS"); - let _ = ManuallyDrop::into_inner(render_pass); - - engine.triangle_pipeline.render_cache_group( - device, - encoder, - frame, - viewport.physical_size(), - group.clone().map(|(cached, bounds)| { - (&cached.meshes, bounds) - }), - ); - - render_pass = - ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: frame, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, + if !layer.triangles.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + mesh_layer += engine.triangle_pipeline.render( + device, + encoder, + frame, + &self.triangle_storage, + mesh_layer, + &layer.triangles, + viewport.physical_size(), + physical_bounds, + scale, + ); + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, }, - )); - } - - for (cached, bounds) in group { - if !cached.text.is_empty() { - engine.text_pipeline.render_cache( - &cached.text, - bounds, - &mut render_pass, - ); - } - - #[cfg(any(feature = "svg", feature = "image"))] - if !cached.images.is_empty() { - engine.image_pipeline.render( - image_layer, - bounds, - &mut render_pass, - ); - - image_layer += 1; - } - } - - i += group_len; - } + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !layer.text.is_empty() { + text_layer += engine.text_pipeline.render( + &self.text_storage, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.render( + image_layer, + scissor_rect, + &mut render_pass, + ); + + image_layer += 1; } } @@ -552,7 +374,7 @@ impl Renderer { impl core::Renderer for Renderer { fn start_layer(&mut self, bounds: Rectangle) { - self.layers.push_clip(Some(bounds)); + self.layers.push_clip(bounds); } fn end_layer(&mut self, _bounds: Rectangle) { @@ -690,15 +512,13 @@ impl graphics::geometry::Renderer for Renderer { fn draw_geometry(&mut self, geometry: Self::Geometry) { match geometry { - Geometry::Live(layers) => { - for layer in layers { - self.layers.draw_layer(layer); - } + Geometry::Live { meshes, text } => { + self.layers.draw_mesh_group(meshes); + self.layers.draw_text_group(text); } - Geometry::Cached(layers) => { - for layer in layers.as_ref() { - self.layers.draw_cached_layer(layer); - } + Geometry::Cached(cache) => { + self.layers.draw_mesh_cache(cache.meshes); + self.layers.draw_text_cache(cache.text); } } } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 16d50b04..de432d2f 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -80,7 +80,7 @@ impl Pipeline { } } - pub fn prepare_batch( + pub fn prepare( &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, @@ -99,64 +99,7 @@ impl Pipeline { self.prepare_layer += 1; } - pub fn prepare_cache( - &self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - cache: &mut Cache, - transformation: Transformation, - scale: f32, - ) { - match cache { - Cache::Staged(_) => { - let Cache::Staged(batch) = - std::mem::replace(cache, Cache::Staged(Batch::default())) - else { - unreachable!() - }; - - let mut layer = Layer::new(device, &self.constant_layout); - layer.prepare( - device, - encoder, - belt, - &batch, - transformation, - scale, - ); - - *cache = Cache::Uploaded { - layer, - batch, - needs_reupload: false, - } - } - - Cache::Uploaded { - batch, - layer, - needs_reupload, - } => { - if *needs_reupload { - layer.prepare( - device, - encoder, - belt, - batch, - transformation, - scale, - ); - - *needs_reupload = false; - } else { - layer.update(device, encoder, belt, transformation, scale); - } - } - } - } - - pub fn render_batch<'a>( + pub fn render<'a>( &'a self, layer: usize, bounds: Rectangle, @@ -164,59 +107,38 @@ impl Pipeline { render_pass: &mut wgpu::RenderPass<'a>, ) { if let Some(layer) = self.layers.get(layer) { - self.render(bounds, layer, &quads.order, render_pass); - } - } - - pub fn render_cache<'a>( - &'a self, - cache: &'a Cache, - bounds: Rectangle, - render_pass: &mut wgpu::RenderPass<'a>, - ) { - if let Cache::Uploaded { layer, batch, .. } = cache { - self.render(bounds, layer, &batch.order, render_pass); - } - } - - fn render<'a>( - &'a self, - bounds: Rectangle, - layer: &'a Layer, - order: &Order, - render_pass: &mut wgpu::RenderPass<'a>, - ) { - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - - let mut solid_offset = 0; - let mut gradient_offset = 0; - - for (kind, count) in order { - match kind { - Kind::Solid => { - self.solid.render( - render_pass, - &layer.constants, - &layer.solid, - solid_offset..(solid_offset + count), - ); - - solid_offset += count; - } - Kind::Gradient => { - self.gradient.render( - render_pass, - &layer.constants, - &layer.gradient, - gradient_offset..(gradient_offset + count), - ); - - gradient_offset += count; + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + let mut solid_offset = 0; + let mut gradient_offset = 0; + + for (kind, count) in &quads.order { + match kind { + Kind::Solid => { + self.solid.render( + render_pass, + &layer.constants, + &layer.solid, + solid_offset..(solid_offset + count), + ); + + solid_offset += count; + } + Kind::Gradient => { + self.gradient.render( + render_pass, + &layer.constants, + &layer.gradient, + gradient_offset..(gradient_offset + count), + ); + + gradient_offset += count; + } } } } @@ -227,48 +149,6 @@ impl Pipeline { } } -#[derive(Debug)] -pub enum Cache { - Staged(Batch), - Uploaded { - batch: Batch, - layer: Layer, - needs_reupload: bool, - }, -} - -impl Cache { - pub fn is_empty(&self) -> bool { - match self { - Cache::Staged(batch) | Cache::Uploaded { batch, .. } => { - batch.is_empty() - } - } - } - - pub fn update(&mut self, new_batch: Batch) { - match self { - Self::Staged(batch) => { - *batch = new_batch; - } - Self::Uploaded { - batch, - needs_reupload, - .. - } => { - *batch = new_batch; - *needs_reupload = true; - } - } - } -} - -impl Default for Cache { - fn default() -> Self { - Self::Staged(Batch::default()) - } -} - #[derive(Debug)] pub struct Layer { constants: wgpu::BindGroup, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index a7695b74..e84e675d 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -4,245 +4,273 @@ use crate::graphics::color; use crate::graphics::text::cache::{self, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::hash_map; +use std::rc::Rc; +use std::sync::atomic::{self, AtomicU64}; use std::sync::Arc; pub use crate::graphics::Text; -pub type Batch = Vec; +const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION { + glyphon::ColorMode::Accurate +} else { + glyphon::ColorMode::Web +}; -#[allow(missing_debug_implementations)] -pub struct Pipeline { - format: wgpu::TextureFormat, - atlas: glyphon::TextAtlas, - renderers: Vec, - prepare_layer: usize, - cache: BufferCache, -} +pub type Batch = Vec; -pub enum Cache { - Staged(Batch), - Uploaded { - batch: Batch, - renderer: glyphon::TextRenderer, - atlas: Option, - buffer_cache: Option, +#[derive(Debug)] +pub enum Item { + Group { transformation: Transformation, - target_size: Size, - needs_reupload: bool, + text: Vec, }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + text: Rc<[Text]>, + version: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + impl Cache { - pub fn is_empty(&self) -> bool { - match self { - Cache::Staged(batch) | Cache::Uploaded { batch, .. } => { - batch.is_empty() - } + pub fn new(text: Vec) -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + text: Rc::from(text), + version: 0, } } - pub fn update(&mut self, new_batch: Batch) { - match self { - Self::Staged(batch) => { - *batch = new_batch; - } - Self::Uploaded { - batch, - needs_reupload, - .. - } => { - *batch = new_batch; - *needs_reupload = true; - } - } + pub fn update(&mut self, text: Vec) { + self.text = Rc::from(text); + self.version += 1; } } -impl Default for Cache { - fn default() -> Self { - Self::Staged(Batch::default()) - } +struct Upload { + renderer: glyphon::TextRenderer, + atlas: glyphon::TextAtlas, + buffer_cache: BufferCache, + transformation: Transformation, + version: usize, } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - ) -> Self { - Pipeline { - format, - renderers: Vec::new(), - atlas: glyphon::TextAtlas::with_color_mode( - device, - queue, - format, - if color::GAMMA_CORRECTION { - glyphon::ColorMode::Accurate - } else { - glyphon::ColorMode::Web - }, - ), - prepare_layer: 0, - cache: BufferCache::new(), - } +#[derive(Default)] +pub struct Storage { + uploads: FxHashMap, + recently_used: FxHashSet, +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, id: Id) -> Option<&Upload> { + self.uploads.get(&id) } - pub fn prepare_batch( + fn prepare( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, - sections: &Batch, - layer_bounds: Rectangle, - layer_transformation: Transformation, + format: wgpu::TextureFormat, + cache: &Cache, + new_transformation: Transformation, + bounds: Rectangle, target_size: Size, ) { - if self.renderers.len() <= self.prepare_layer { - self.renderers.push(glyphon::TextRenderer::new( - &mut self.atlas, - device, - wgpu::MultisampleState::default(), - None, - )); - } + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); - let renderer = &mut self.renderers[self.prepare_layer]; - let result = prepare( - device, - queue, - encoder, - renderer, - &mut self.atlas, - &mut self.cache, - sections, - layer_bounds, - layer_transformation, - target_size, - ); + if upload.version != cache.version + || upload.transformation != new_transformation + { + let _ = prepare( + device, + queue, + encoder, + &mut upload.renderer, + &mut upload.atlas, + &mut upload.buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); - match result { - Ok(()) => { - self.prepare_layer += 1; - } - Err(glyphon::PrepareError::AtlasFull) => { - // If the atlas cannot grow, then all bets are off. - // Instead of panicking, we will just pray that the result - // will be somewhat readable... - } - } - } + upload.version = cache.version; + upload.transformation = new_transformation; - pub fn prepare_cache( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - cache: &mut Cache, - layer_bounds: Rectangle, - new_transformation: Transformation, - new_target_size: Size, - ) { - match cache { - Cache::Staged(_) => { - let Cache::Staged(batch) = - std::mem::replace(cache, Cache::Staged(Batch::default())) - else { - unreachable!() - }; - - // TODO: Find a better heuristic (?) - let (mut atlas, mut buffer_cache) = if batch.len() > 10 { - ( - Some(glyphon::TextAtlas::with_color_mode( - device, - queue, - self.format, - if color::GAMMA_CORRECTION { - glyphon::ColorMode::Accurate - } else { - glyphon::ColorMode::Web - }, - )), - Some(BufferCache::new()), - ) - } else { - (None, None) - }; + upload.buffer_cache.trim(); + upload.atlas.trim(); + } + } + hash_map::Entry::Vacant(entry) => { + let mut atlas = glyphon::TextAtlas::with_color_mode( + device, queue, format, COLOR_MODE, + ); let mut renderer = glyphon::TextRenderer::new( - atlas.as_mut().unwrap_or(&mut self.atlas), + &mut atlas, device, wgpu::MultisampleState::default(), None, ); + let mut buffer_cache = BufferCache::new(); + let _ = prepare( device, queue, encoder, &mut renderer, - atlas.as_mut().unwrap_or(&mut self.atlas), - buffer_cache.as_mut().unwrap_or(&mut self.cache), - &batch, - layer_bounds, + &mut atlas, + &mut buffer_cache, + &cache.text, + bounds, new_transformation, - new_target_size, + target_size, ); - *cache = Cache::Uploaded { - batch, - needs_reupload: false, + let _ = entry.insert(Upload { renderer, atlas, buffer_cache, transformation: new_transformation, - target_size: new_target_size, - } + version: 0, + }); } - Cache::Uploaded { - batch, - needs_reupload, - renderer, - atlas, - buffer_cache, - transformation, - target_size, - } => { - if *needs_reupload - || atlas.is_none() - || buffer_cache.is_none() - || new_transformation != *transformation - || new_target_size != *target_size - { - let _ = prepare( + } + + let _ = self.recently_used.insert(cache.id); + } + + pub fn trim(&mut self) { + self.uploads.retain(|id, _| self.recently_used.contains(id)); + self.recently_used.clear(); + } +} + +#[allow(missing_debug_implementations)] +pub struct Pipeline { + format: wgpu::TextureFormat, + atlas: glyphon::TextAtlas, + renderers: Vec, + prepare_layer: usize, + cache: BufferCache, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + ) -> Self { + Pipeline { + format, + renderers: Vec::new(), + atlas: glyphon::TextAtlas::with_color_mode( + device, queue, format, COLOR_MODE, + ), + prepare_layer: 0, + cache: BufferCache::new(), + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + storage: &mut Storage, + batch: &Batch, + layer_bounds: Rectangle, + layer_transformation: Transformation, + target_size: Size, + ) { + for item in batch { + match item { + Item::Group { + transformation, + text, + } => { + if self.renderers.len() <= self.prepare_layer { + self.renderers.push(glyphon::TextRenderer::new( + &mut self.atlas, + device, + wgpu::MultisampleState::default(), + None, + )); + } + + let renderer = &mut self.renderers[self.prepare_layer]; + let result = prepare( device, queue, encoder, renderer, - atlas.as_mut().unwrap_or(&mut self.atlas), - buffer_cache.as_mut().unwrap_or(&mut self.cache), - batch, + &mut self.atlas, + &mut self.cache, + text, layer_bounds, - new_transformation, - new_target_size, + layer_transformation * *transformation, + target_size, ); - *transformation = new_transformation; - *target_size = new_target_size; - *needs_reupload = false; + match result { + Ok(()) => { + self.prepare_layer += 1; + } + Err(glyphon::PrepareError::AtlasFull) => { + // If the atlas cannot grow, then all bets are off. + // Instead of panicking, we will just pray that the result + // will be somewhat readable... + } + } + } + Item::Cached { + transformation, + cache, + } => { + storage.prepare( + device, + queue, + encoder, + self.format, + cache, + layer_transformation * *transformation, + layer_bounds, + target_size, + ); } } } } - pub fn render_batch<'a>( + pub fn render<'a>( &'a self, - layer: usize, + storage: &'a Storage, + start: usize, + batch: &'a Batch, bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, - ) { - let renderer = &self.renderers[layer]; + ) -> usize { + let mut layer_count = 0; render_pass.set_scissor_rect( bounds.x, @@ -251,34 +279,29 @@ impl Pipeline { bounds.height, ); - renderer - .render(&self.atlas, render_pass) - .expect("Render text"); - } + for item in batch { + match item { + Item::Group { .. } => { + let renderer = &self.renderers[start + layer_count]; - pub fn render_cache<'a>( - &'a self, - cache: &'a Cache, - bounds: Rectangle, - render_pass: &mut wgpu::RenderPass<'a>, - ) { - let Cache::Uploaded { - renderer, atlas, .. - } = cache - else { - return; - }; + renderer + .render(&self.atlas, render_pass) + .expect("Render text"); - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); + layer_count += 1; + } + Item::Cached { cache, .. } => { + if let Some(upload) = storage.get(cache.id) { + upload + .renderer + .render(&upload.atlas, render_pass) + .expect("Render cached text"); + } + } + } + } - renderer - .render(atlas.as_ref().unwrap_or(&self.atlas), render_pass) - .expect("Render text"); + layer_count } pub fn end_frame(&mut self) { @@ -296,7 +319,7 @@ fn prepare( renderer: &mut glyphon::TextRenderer, atlas: &mut glyphon::TextAtlas, buffer_cache: &mut BufferCache, - sections: &Batch, + sections: &[Text], layer_bounds: Rectangle, layer_transformation: Transformation, target_size: Size, @@ -333,8 +356,8 @@ fn prepare( font_system, cache::Key { content, - size: (*size).into(), - line_height: f32::from(line_height.to_absolute(*size)), + size: f32::from(*size), + line_height: f32::from(*line_height), font: *font, bounds: Size { width: bounds.width, diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 3a184da2..a08b6987 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -6,10 +6,136 @@ use crate::graphics::mesh::{self, Mesh}; use crate::graphics::Antialiasing; use crate::Buffer; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::hash_map; +use std::rc::Rc; +use std::sync::atomic::{self, AtomicU64}; + const INITIAL_INDEX_COUNT: usize = 1_000; const INITIAL_VERTEX_COUNT: usize = 1_000; -pub type Batch = Vec; +pub type Batch = Vec; + +pub enum Item { + Group { + transformation: Transformation, + meshes: Vec, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + batch: Rc<[Mesh]>, + version: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + pub fn new(meshes: Vec) -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + batch: Rc::from(meshes), + version: 0, + } + } + + pub fn update(&mut self, meshes: Vec) { + self.batch = Rc::from(meshes); + self.version += 1; + } +} + +#[derive(Debug)] +struct Upload { + layer: Layer, + transformation: Transformation, + version: usize, +} + +#[derive(Debug, Default)] +pub struct Storage { + uploads: FxHashMap, + recently_used: FxHashSet, +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, id: Id) -> Option<&Upload> { + self.uploads.get(&id) + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + cache: &Cache, + new_transformation: Transformation, + ) { + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if upload.version != cache.version + || upload.transformation != new_transformation + { + upload.layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + upload.version = cache.version; + upload.transformation = new_transformation; + } + } + hash_map::Entry::Vacant(entry) => { + let mut layer = Layer::new(device, solid, gradient); + + layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + let _ = entry.insert(Upload { + layer, + transformation: new_transformation, + version: 0, + }); + } + } + + let _ = self.recently_used.insert(cache.id); + } + + pub fn trim(&mut self) { + self.uploads.retain(|id, _| self.recently_used.contains(id)); + self.recently_used.clear(); + } +} #[derive(Debug)] pub struct Pipeline { @@ -35,180 +161,103 @@ impl Pipeline { } } - pub fn prepare_batch( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - meshes: &Batch, - transformation: Transformation, - ) { - if self.layers.len() <= self.prepare_layer { - self.layers - .push(Layer::new(device, &self.solid, &self.gradient)); - } - - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare( - device, - encoder, - belt, - &self.solid, - &self.gradient, - meshes, - transformation, - ); - - self.prepare_layer += 1; - } - - pub fn prepare_cache( + pub fn prepare( &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, - cache: &mut Cache, - new_projection: Transformation, - new_transformation: Transformation, + storage: &mut Storage, + items: &[Item], + projection: Transformation, ) { - let new_projection = new_projection * new_transformation; - - match cache { - Cache::Staged(_) => { - let Cache::Staged(batch) = - std::mem::replace(cache, Cache::Staged(Batch::default())) - else { - unreachable!() - }; - - let mut layer = Layer::new(device, &self.solid, &self.gradient); - layer.prepare( - device, - encoder, - belt, - &self.solid, - &self.gradient, - &batch, - new_projection, - ); + for item in items { + match item { + Item::Group { + transformation, + meshes, + } => { + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &self.solid, + &self.gradient, + )); + } - *cache = Cache::Uploaded { - layer, - batch, - transformation: new_transformation, - projection: new_projection, - needs_reupload: false, - } - } - Cache::Uploaded { - batch, - layer, - transformation, - projection, - needs_reupload, - } => { - if *needs_reupload || new_projection != *projection { + let layer = &mut self.layers[self.prepare_layer]; layer.prepare( device, encoder, belt, &self.solid, &self.gradient, - batch, - new_projection, + meshes, + projection * *transformation, ); - *transformation = new_transformation; - *projection = new_projection; - *needs_reupload = false; + self.prepare_layer += 1; + } + Item::Cached { + transformation, + cache, + } => { + storage.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + cache, + projection * *transformation, + ); } } } } - pub fn render_batch( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - layer: usize, - target_size: Size, - meshes: &Batch, - bounds: Rectangle, - transformation: &Transformation, - ) { - Self::render( - device, - encoder, - target, - self.blit.as_mut(), - &self.solid, - &self.gradient, - target_size, - std::iter::once(( - &self.layers[layer], - meshes, - transformation, - bounds, - )), - ); - } - - #[allow(dead_code)] - pub fn render_cache( + pub fn render( &mut self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, + storage: &Storage, + start: usize, + batch: &Batch, target_size: Size, - cache: &Cache, - bounds: Rectangle, - ) { - let Cache::Uploaded { - batch, - layer, - transformation, - .. - } = cache - else { - return; - }; + bounds: Rectangle, + screen_transformation: Transformation, + ) -> usize { + let mut layer_count = 0; - Self::render( - device, - encoder, - target, - self.blit.as_mut(), - &self.solid, - &self.gradient, - target_size, - std::iter::once((layer, batch, transformation, bounds)), - ); - } + let items = batch.iter().filter_map(|item| match item { + Item::Group { + transformation, + meshes, + } => { + let layer = &self.layers[start + layer_count]; + layer_count += 1; - pub fn render_cache_group<'a>( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - target_size: Size, - group: impl Iterator)>, - ) { - let group = group.filter_map(|(cache, bounds)| { - if let Cache::Uploaded { - batch, - layer, + Some(( + layer, + meshes.as_slice(), + screen_transformation * *transformation, + )) + } + Item::Cached { transformation, - .. - } = cache - { - Some((layer, batch, transformation, bounds)) - } else { - None + cache, + } => { + let upload = storage.get(cache.id)?; + + Some(( + &upload.layer, + &cache.batch, + screen_transformation * *transformation, + )) } }); - Self::render( + render( device, encoder, target, @@ -216,71 +265,11 @@ impl Pipeline { &self.solid, &self.gradient, target_size, - group, + bounds, + items, ); - } - - fn render<'a>( - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - mut blit: Option<&mut msaa::Blit>, - solid: &solid::Pipeline, - gradient: &gradient::Pipeline, - target_size: Size, - group: impl Iterator< - Item = (&'a Layer, &'a Batch, &'a Transformation, Rectangle), - >, - ) { - { - let (attachment, resolve_target, load) = if let Some(blit) = - &mut blit - { - let (attachment, resolve_target) = - blit.targets(device, target_size.width, target_size.height); - - ( - attachment, - Some(resolve_target), - wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - ) - } else { - (target, None, wgpu::LoadOp::Load) - }; - - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu.triangle.render_pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: attachment, - resolve_target, - ops: wgpu::Operations { - load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - for (layer, meshes, transformation, bounds) in group { - layer.render( - solid, - gradient, - meshes, - bounds, - *transformation, - &mut render_pass, - ); - } - } - if let Some(blit) = blit { - blit.draw(encoder, target); - } + layer_count } pub fn end_frame(&mut self) { @@ -288,47 +277,61 @@ impl Pipeline { } } -#[derive(Debug)] -pub enum Cache { - Staged(Batch), - Uploaded { - batch: Batch, - layer: Layer, - transformation: Transformation, - projection: Transformation, - needs_reupload: bool, - }, -} - -impl Cache { - pub fn is_empty(&self) -> bool { - match self { - Cache::Staged(batch) | Cache::Uploaded { batch, .. } => { - batch.is_empty() - } - } - } +fn render<'a>( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + mut blit: Option<&mut msaa::Blit>, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + target_size: Size, + bounds: Rectangle, + group: impl Iterator, +) { + { + let (attachment, resolve_target, load) = if let Some(blit) = &mut blit { + let (attachment, resolve_target) = + blit.targets(device, target_size.width, target_size.height); + + ( + attachment, + Some(resolve_target), + wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + ) + } else { + (target, None, wgpu::LoadOp::Load) + }; - pub fn update(&mut self, new_batch: Batch) { - match self { - Self::Staged(batch) => { - *batch = new_batch; - } - Self::Uploaded { - batch, - needs_reupload, - .. - } => { - *batch = new_batch; - *needs_reupload = true; - } + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + for (layer, meshes, transformation) in group { + layer.render( + solid, + gradient, + meshes, + bounds, + transformation, + &mut render_pass, + ); } } -} -impl Default for Cache { - fn default() -> Self { - Self::Staged(Batch::default()) + if let Some(blit) = blit { + blit.draw(encoder, target); } } @@ -366,7 +369,7 @@ impl Layer { belt: &mut wgpu::util::StagingBelt, solid: &solid::Pipeline, gradient: &gradient::Pipeline, - meshes: &Batch, + meshes: &[Mesh], transformation: Transformation, ) { // Count the total amount of vertices & indices we need to handle @@ -471,8 +474,8 @@ impl Layer { &'a self, solid: &'a solid::Pipeline, gradient: &'a gradient::Pipeline, - meshes: &Batch, - layer_bounds: Rectangle, + meshes: &[Mesh], + bounds: Rectangle, transformation: Transformation, render_pass: &mut wgpu::RenderPass<'a>, ) { @@ -481,8 +484,8 @@ impl Layer { let mut last_is_solid = None; for (index, mesh) in meshes.iter().enumerate() { - let Some(clip_bounds) = Rectangle::::from(layer_bounds) - .intersection(&(mesh.clip_bounds() * transformation)) + let Some(clip_bounds) = + bounds.intersection(&(mesh.clip_bounds() * transformation)) else { continue; }; diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 8cfb0408..05dd87b1 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -368,7 +368,7 @@ where let text = value.to_string(); - let (cursor, offset) = if let Some(focus) = state + let (cursor, offset, is_selecting) = if let Some(focus) = state .is_focused .as_ref() .filter(|focus| focus.is_window_focused) @@ -406,7 +406,7 @@ where None }; - (cursor, offset) + (cursor, offset, false) } cursor::State::Selection { start, end } => { let left = start.min(end); @@ -446,11 +446,12 @@ where } else { left_offset }, + true, ) } } } else { - (None, 0.0) + (None, 0.0, false) }; let draw = |renderer: &mut Renderer, viewport| { @@ -482,7 +483,7 @@ where ); }; - if cursor.is_some() { + if is_selecting { renderer .with_layer(text_bounds, |renderer| draw(renderer, *viewport)); } else { -- cgit From 7eb16452f340fe228e6928b496f8df6e9e86e554 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 6 Apr 2024 00:57:59 +0200 Subject: Avoid generating empty `Frame` geometry in `iced_wgpu` --- wgpu/src/geometry.rs | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index c8c350c5..b689d2a7 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -422,24 +422,31 @@ impl BufferStack { } fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator { - self.stack.into_iter().map(move |buffer| match buffer { - Buffer::Solid(buffer) => Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - clip_bounds, - transformation: Transformation::IDENTITY, - }, - Buffer::Gradient(buffer) => Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - clip_bounds, - transformation: Transformation::IDENTITY, - }, - }) + self.stack + .into_iter() + .filter_map(move |buffer| match buffer { + Buffer::Solid(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + Buffer::Gradient(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + _ => None, + }) } } -- cgit From 441aac25995290a83162a4728f22492ff69a5f4d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 6 Apr 2024 03:06:40 +0200 Subject: Avoid generating empty caches in `iced_wgpu` --- wgpu/src/geometry.rs | 17 +++++++++++++---- wgpu/src/lib.rs | 9 +++++++-- wgpu/src/text.rs | 31 +++++++++++++++++++++++-------- wgpu/src/triangle.rs | 31 +++++++++++++++++++++++-------- 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index b689d2a7..c36ff38e 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -38,8 +38,8 @@ pub enum Geometry { #[derive(Clone)] pub struct Cache { - pub meshes: triangle::Cache, - pub text: text::Cache, + pub meshes: Option, + pub text: Option, } impl Cached for Geometry { @@ -53,8 +53,17 @@ impl Cached for Geometry { match self { Self::Live { meshes, text } => { if let Some(mut previous) = previous { - previous.meshes.update(meshes); - previous.text.update(text); + if let Some(cache) = &mut previous.meshes { + cache.update(meshes); + } else { + previous.meshes = triangle::Cache::new(meshes); + } + + if let Some(cache) = &mut previous.text { + cache.update(text); + } else { + previous.text = text::Cache::new(text); + } previous } else { diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index d632919f..dfc1aad4 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -517,8 +517,13 @@ impl graphics::geometry::Renderer for Renderer { self.layers.draw_text_group(text); } Geometry::Cached(cache) => { - self.layers.draw_mesh_cache(cache.meshes); - self.layers.draw_text_cache(cache.text); + if let Some(meshes) = cache.meshes { + self.layers.draw_mesh_cache(meshes); + } + + if let Some(text) = cache.text { + self.layers.draw_text_cache(text); + } } } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e84e675d..1b21bb1c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -43,14 +43,18 @@ pub struct Cache { pub struct Id(u64); impl Cache { - pub fn new(text: Vec) -> Self { + pub fn new(text: Vec) -> Option { static NEXT_ID: AtomicU64 = AtomicU64::new(0); - Self { + if text.is_empty() { + return None; + } + + Some(Self { id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), text: Rc::from(text), version: 0, - } + }) } pub fn update(&mut self, text: Vec) { @@ -78,8 +82,12 @@ impl Storage { Self::default() } - fn get(&self, id: Id) -> Option<&Upload> { - self.uploads.get(&id) + fn get(&self, cache: &Cache) -> Option<&Upload> { + if cache.text.is_empty() { + return None; + } + + self.uploads.get(&cache.id) } fn prepare( @@ -97,8 +105,9 @@ impl Storage { hash_map::Entry::Occupied(entry) => { let upload = entry.into_mut(); - if upload.version != cache.version - || upload.transformation != new_transformation + if !cache.text.is_empty() + && (upload.version != cache.version + || upload.transformation != new_transformation) { let _ = prepare( device, @@ -154,6 +163,12 @@ impl Storage { transformation: new_transformation, version: 0, }); + + log::info!( + "New text upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); } } @@ -291,7 +306,7 @@ impl Pipeline { layer_count += 1; } Item::Cached { cache, .. } => { - if let Some(upload) = storage.get(cache.id) { + if let Some(upload) = storage.get(cache) { upload .renderer .render(&upload.atlas, render_pass) diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index a08b6987..de6c026a 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -38,14 +38,18 @@ pub struct Cache { pub struct Id(u64); impl Cache { - pub fn new(meshes: Vec) -> Self { + pub fn new(meshes: Vec) -> Option { static NEXT_ID: AtomicU64 = AtomicU64::new(0); - Self { + if meshes.is_empty() { + return None; + } + + Some(Self { id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), batch: Rc::from(meshes), version: 0, - } + }) } pub fn update(&mut self, meshes: Vec) { @@ -72,8 +76,12 @@ impl Storage { Self::default() } - fn get(&self, id: Id) -> Option<&Upload> { - self.uploads.get(&id) + fn get(&self, cache: &Cache) -> Option<&Upload> { + if cache.batch.is_empty() { + return None; + } + + self.uploads.get(&cache.id) } fn prepare( @@ -90,8 +98,9 @@ impl Storage { hash_map::Entry::Occupied(entry) => { let upload = entry.into_mut(); - if upload.version != cache.version - || upload.transformation != new_transformation + if !cache.batch.is_empty() + && (upload.version != cache.version + || upload.transformation != new_transformation) { upload.layer.prepare( device, @@ -125,6 +134,12 @@ impl Storage { transformation: new_transformation, version: 0, }); + + log::info!( + "New mesh upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); } } @@ -247,7 +262,7 @@ impl Pipeline { transformation, cache, } => { - let upload = storage.get(cache.id)?; + let upload = storage.get(cache)?; Some(( &upload.layer, -- cgit From a699348bf3c7af5622b7cbb0b8f8b6bb9b16a22a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 6 Apr 2024 06:23:29 +0200 Subject: Reenable proper `present_mode` in `iced_wgpu` --- wgpu/src/window/compositor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index ab441fa0..d546a6dc 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -321,12 +321,12 @@ impl graphics::Compositor for Compositor { &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: self.format, - present_mode: wgpu::PresentMode::Immediate, + present_mode: self.settings.present_mode, width, height, alpha_mode: self.alpha_mode, view_formats: vec![], - desired_maximum_frame_latency: 0, + desired_maximum_frame_latency: 1, }, ); } -- cgit From 5ffea8ddef1b966286c14a3294f501201d9edc70 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 7 Apr 2024 08:11:42 +0200 Subject: Add a simple `wgpu` benchmark using `criterion` --- Cargo.toml | 9 +++ benches/wgpu.rs | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 benches/wgpu.rs diff --git a/Cargo.toml b/Cargo.toml index b033cf33..def40bd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,3 +164,12 @@ wgpu = "0.19" winapi = "0.3" window_clipboard = "0.4.1" winit = { git = "https://github.com/iced-rs/winit.git", rev = "592bd152f6d5786fae7d918532d7db752c0d164f" } + +[dev-dependencies] +criterion = "0.5" +iced_wgpu.workspace = true + +[[bench]] +name = "wgpu" +harness = false +required-features = ["canvas"] diff --git a/benches/wgpu.rs b/benches/wgpu.rs new file mode 100644 index 00000000..c8d38dc9 --- /dev/null +++ b/benches/wgpu.rs @@ -0,0 +1,182 @@ +use criterion::{criterion_group, criterion_main, Bencher, Criterion}; + +use iced::alignment; +use iced::mouse; +use iced::widget::{canvas, text}; +use iced::{ + Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme, +}; +use iced_wgpu::Renderer; + +criterion_main!(benches); +criterion_group!(benches, wgpu_benchmark); + +pub fn wgpu_benchmark(c: &mut Criterion) { + c.bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10))); + c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000))); +} + +fn benchmark<'a>( + bencher: &mut Bencher<'_>, + widget: Element<'a, (), Theme, Renderer>, +) { + use iced_futures::futures::executor; + use iced_wgpu::graphics; + use iced_wgpu::graphics::Antialiasing; + use iced_wgpu::wgpu; + use iced_winit::core; + use iced_winit::runtime; + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + + let adapter = executor::block_on(instance.request_adapter( + &wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + force_fallback_adapter: false, + }, + )) + .expect("request adapter"); + + let (device, queue) = executor::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: None, + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + }, + None, + )) + .expect("request device"); + + let format = wgpu::TextureFormat::Bgra8UnormSrgb; + + let mut engine = iced_wgpu::Engine::new( + &adapter, + &device, + &queue, + format, + Some(Antialiasing::MSAAx4), + ); + + let mut renderer = Renderer::new( + iced_wgpu::Settings { + present_mode: wgpu::PresentMode::Immediate, + backends: wgpu::Backends::all(), + default_font: Font::DEFAULT, + default_text_size: Pixels::from(16), + antialiasing: Some(Antialiasing::MSAAx4), + }, + &engine, + ); + + let viewport = + graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: 3840, + height: 2160, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + let texture_view = + texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let mut user_interface = runtime::UserInterface::build( + widget, + viewport.logical_size(), + runtime::user_interface::Cache::default(), + &mut renderer, + ); + + bencher.iter(|| { + user_interface.draw( + &mut renderer, + &Theme::Dark, + &core::renderer::Style { + text_color: Color::WHITE, + }, + mouse::Cursor::Unavailable, + ); + + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: None, + }); + + renderer.present::<&str>( + &mut engine, + &device, + &queue, + &mut encoder, + Some(Color::BLACK), + format, + &texture_view, + &viewport, + &[], + ); + + engine.submit(&queue, encoder); + }); +} + +fn scene<'a, Message: 'a, Theme: 'a>( + n: usize, +) -> Element<'a, Message, Theme, Renderer> { + canvas(Scene { n }) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +struct Scene { + n: usize, +} + +impl canvas::Program for Scene { + type State = canvas::Cache; + + fn draw( + &self, + cache: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec> { + vec![cache.draw(renderer, bounds.size(), |frame| { + for i in 0..self.n { + frame.fill_rectangle( + Point::new(0.0, i as f32), + Size::new(10.0, 10.0), + Color::WHITE, + ); + } + + for i in 0..self.n { + frame.fill_text(canvas::Text { + content: i.to_string(), + position: Point::new(0.0, i as f32), + color: Color::BLACK, + size: Pixels::from(16), + line_height: text::LineHeight::default(), + font: Font::DEFAULT, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Basic, + }); + } + })] + } +} -- cgit From 67716720875368ac4ab2354bcea026a4fadbb469 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 7 Apr 2024 08:48:27 +0200 Subject: Wait for submission in `wgpu` benchmark --- benches/wgpu.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/benches/wgpu.rs b/benches/wgpu.rs index c8d38dc9..47e3f7cc 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -127,7 +127,9 @@ fn benchmark<'a>( &[], ); - engine.submit(&queue, encoder); + let submission = engine.submit(&queue, encoder); + + let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); }); } -- cgit From 68056f8ca401e2d0b5c96e18c78e19a771b3df07 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 7 Apr 2024 08:52:28 +0200 Subject: Test benchmarks in GitHub CI --- .cargo/config.toml | 4 ++-- .github/workflows/test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 85a46cda..20f9895b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [alias] lint = """ -clippy --workspace --no-deps -- \ +clippy --workspace --benches --no-deps -- \ -D warnings \ -A clippy::type_complexity \ -D clippy::semicolon_if_nothing_returned \ @@ -18,7 +18,7 @@ clippy --workspace --no-deps -- \ """ nitpick = """ -clippy --workspace --no-deps -- \ +clippy --workspace --benches --no-deps -- \ -D warnings \ -D clippy::pedantic \ -A clippy::type_complexity \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47c61f5e..c48b8b5b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,5 +22,5 @@ jobs: sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - name: Run tests run: | - cargo test --verbose --workspace - cargo test --verbose --workspace --all-features + cargo test --verbose --workspace --benches + cargo test --verbose --workspace --benches --all-features -- cgit From bcd6873b37ed63ade9743900cdae8e8619299eb2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 7 Apr 2024 09:33:33 +0200 Subject: Fix lint in `wgpu` benchmark --- benches/wgpu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 47e3f7cc..8489f61a 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -16,9 +16,9 @@ pub fn wgpu_benchmark(c: &mut Criterion) { c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000))); } -fn benchmark<'a>( +fn benchmark( bencher: &mut Bencher<'_>, - widget: Element<'a, (), Theme, Renderer>, + widget: Element<'_, (), Theme, Renderer>, ) { use iced_futures::futures::executor; use iced_wgpu::graphics; -- cgit From 5e01d767c5ef8a35937675d4e8f02b4c678a5da2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 7 Apr 2024 13:48:23 +0200 Subject: Check benchmarks only instead of testing them in CI --- .github/workflows/check.yml | 14 +++++++++++--- .github/workflows/test.yml | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4107e618..79fa7be6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,13 +1,13 @@ name: Check on: [push, pull_request] jobs: - widget: + benchmarks: runs-on: ubuntu-latest steps: - uses: hecrj/setup-rust-action@v2 - uses: actions/checkout@master - - name: Check standalone `iced_widget` crate - run: cargo check --package iced_widget --features image,svg,canvas + - name: Check benchmarks + run: cargo check --benches --all-features wasm: runs-on: ubuntu-latest @@ -27,3 +27,11 @@ jobs: run: cargo build --package todos --target wasm32-unknown-unknown - name: Check compilation of `integration` example run: cargo build --package integration --target wasm32-unknown-unknown + + widget: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v2 + - uses: actions/checkout@master + - name: Check standalone `iced_widget` crate + run: cargo check --package iced_widget --features image,svg,canvas diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c48b8b5b..47c61f5e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,5 +22,5 @@ jobs: sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - name: Run tests run: | - cargo test --verbose --workspace --benches - cargo test --verbose --workspace --benches --all-features + cargo test --verbose --workspace + cargo test --verbose --workspace --all-features -- cgit From 288f62bfb691a91e01b9ddbce9dbdc560ee9036a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 7 Apr 2024 18:45:30 +0200 Subject: Share `msaa::Blit` texture between multiple windows --- wgpu/src/lib.rs | 8 ++- wgpu/src/shader/blit.wgsl | 20 +++---- wgpu/src/triangle.rs | 19 +++---- wgpu/src/triangle/msaa.rs | 134 +++++++++++++++++++++++++++++++--------------- 4 files changed, 112 insertions(+), 69 deletions(-) diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index ccad08d5..030bcade 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -117,7 +117,7 @@ impl Renderer { ) { self.draw_overlay(overlay, viewport); self.prepare(engine, device, queue, format, encoder, viewport); - self.render(engine, device, encoder, frame, clear_color, viewport); + self.render(engine, encoder, frame, clear_color, viewport); self.triangle_storage.trim(); self.text_storage.trim(); @@ -153,7 +153,8 @@ impl Renderer { &mut engine.staging_belt, &mut self.triangle_storage, &layer.triangles, - viewport.projection() * Transformation::scale(scale_factor), + Transformation::scale(scale_factor), + viewport.physical_size(), ); } @@ -187,7 +188,6 @@ impl Renderer { fn render( &mut self, engine: &mut Engine, - device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, frame: &wgpu::TextureView, clear_color: Option, @@ -264,13 +264,11 @@ impl Renderer { let _ = ManuallyDrop::into_inner(render_pass); mesh_layer += engine.triangle_pipeline.render( - device, encoder, frame, &self.triangle_storage, mesh_layer, &layer.triangles, - viewport.physical_size(), physical_bounds, scale, ); diff --git a/wgpu/src/shader/blit.wgsl b/wgpu/src/shader/blit.wgsl index c2ea223f..d7633808 100644 --- a/wgpu/src/shader/blit.wgsl +++ b/wgpu/src/shader/blit.wgsl @@ -1,22 +1,14 @@ -var positions: array, 6> = array, 6>( - vec2(-1.0, 1.0), - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(1.0, 1.0), - vec2(1.0, -1.0) -); - var uvs: array, 6> = array, 6>( vec2(0.0, 0.0), - vec2(0.0, 1.0), + vec2(1.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 0.0), - vec2(1.0, 0.0), + vec2(0.0, 1.0), vec2(1.0, 1.0) ); @group(0) @binding(0) var u_sampler: sampler; +@group(0) @binding(1) var u_ratio: vec2; @group(1) @binding(0) var u_texture: texture_2d; struct VertexInput { @@ -30,9 +22,11 @@ struct VertexOutput { @vertex fn vs_main(input: VertexInput) -> VertexOutput { + let uv = uvs[input.vertex_index]; + var out: VertexOutput; - out.uv = uvs[input.vertex_index]; - out.position = vec4(positions[input.vertex_index], 0.0, 1.0); + out.uv = uv * u_ratio; + out.position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); return out; } diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 06c0ef02..98ba41b3 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -184,8 +184,16 @@ impl Pipeline { belt: &mut wgpu::util::StagingBelt, storage: &mut Storage, items: &[Item], - projection: Transformation, + scale: Transformation, + target_size: Size, ) { + let projection = if let Some(blit) = &mut self.blit { + blit.prepare(device, encoder, belt, target_size) * scale + } else { + Transformation::orthographic(target_size.width, target_size.height) + * scale + }; + for item in items { match item { Item::Group { @@ -233,13 +241,11 @@ impl Pipeline { pub fn render( &mut self, - device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, storage: &Storage, start: usize, batch: &Batch, - target_size: Size, bounds: Rectangle, screen_transformation: Transformation, ) -> usize { @@ -274,13 +280,11 @@ impl Pipeline { }); render( - device, encoder, target, self.blit.as_mut(), &self.solid, &self.gradient, - target_size, bounds, items, ); @@ -294,20 +298,17 @@ impl Pipeline { } fn render<'a>( - device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, mut blit: Option<&mut msaa::Blit>, solid: &solid::Pipeline, gradient: &gradient::Pipeline, - target_size: Size, bounds: Rectangle, group: impl Iterator, ) { { let (attachment, resolve_target, load) = if let Some(blit) = &mut blit { - let (attachment, resolve_target) = - blit.targets(device, target_size.width, target_size.height); + let (attachment, resolve_target) = blit.targets(); ( attachment, diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 14abd20b..fcee6b3e 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -1,13 +1,18 @@ +use crate::core::{Size, Transformation}; use crate::graphics; +use std::num::NonZeroU64; + #[derive(Debug)] pub struct Blit { format: wgpu::TextureFormat, pipeline: wgpu::RenderPipeline, constants: wgpu::BindGroup, + ratio: wgpu::Buffer, texture_layout: wgpu::BindGroupLayout, sample_count: u32, targets: Option, + last_region: Option>, } impl Blit { @@ -19,27 +24,52 @@ impl Blit { let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let ratio = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced-wgpu::triangle::msaa ratio"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + mapped_at_creation: false, + }); + let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu::triangle:msaa uniforms layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::NonFiltering, - ), - count: None, - }], + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], }); let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::triangle::msaa uniforms bind group"), layout: &constant_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&sampler), - }], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], }); let texture_layout = @@ -88,9 +118,7 @@ impl Blit { entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { format, - blend: Some( - wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, - ), + blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], }), @@ -112,43 +140,61 @@ impl Blit { format, pipeline, constants: constant_bind_group, + ratio, texture_layout, sample_count: antialiasing.sample_count(), targets: None, + last_region: None, } } - pub fn targets( + pub fn prepare( &mut self, device: &wgpu::Device, - width: u32, - height: u32, - ) -> (&wgpu::TextureView, &wgpu::TextureView) { + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + region_size: Size, + ) -> Transformation { match &mut self.targets { - None => { + Some(targets) + if region_size.width <= targets.size.width + && region_size.height <= targets.size.height => {} + _ => { self.targets = Some(Targets::new( device, self.format, &self.texture_layout, self.sample_count, - width, - height, + region_size, )); } - Some(targets) => { - if targets.width != width || targets.height != height { - self.targets = Some(Targets::new( - device, - self.format, - &self.texture_layout, - self.sample_count, - width, - height, - )); - } - } } + let targets = self.targets.as_mut().unwrap(); + + if Some(region_size) != self.last_region { + let ratio = Ratio { + u: region_size.width as f32 / targets.size.width as f32, + v: region_size.height as f32 / targets.size.height as f32, + }; + + belt.write_buffer( + encoder, + &self.ratio, + 0, + NonZeroU64::new(std::mem::size_of::() as u64) + .expect("non-empty ratio"), + device, + ) + .copy_from_slice(bytemuck::bytes_of(&ratio)); + + self.last_region = Some(region_size); + } + + Transformation::orthographic(targets.size.width, targets.size.height) + } + + pub fn targets(&self) -> (&wgpu::TextureView, &wgpu::TextureView) { let targets = self.targets.as_ref().unwrap(); (&targets.attachment, &targets.resolve) @@ -191,8 +237,7 @@ struct Targets { attachment: wgpu::TextureView, resolve: wgpu::TextureView, bind_group: wgpu::BindGroup, - width: u32, - height: u32, + size: Size, } impl Targets { @@ -201,12 +246,11 @@ impl Targets { format: wgpu::TextureFormat, texture_layout: &wgpu::BindGroupLayout, sample_count: u32, - width: u32, - height: u32, + size: Size, ) -> Targets { let extent = wgpu::Extent3d { - width, - height, + width: size.width, + height: size.height, depth_or_array_layers: 1, }; @@ -252,8 +296,14 @@ impl Targets { attachment, resolve, bind_group, - width, - height, + size, } } } + +#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Ratio { + u: f32, + v: f32, +} -- cgit From d922b478156488a7bc03c6e791e05c040d702634 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 8 Apr 2024 15:04:35 +0200 Subject: Reintroduce support for custom primitives in `iced_wgpu` --- core/src/rectangle.rs | 17 ++++-- examples/custom_shader/src/scene.rs | 33 ++++++----- renderer/src/fallback.rs | 10 ++-- wgpu/src/engine.rs | 13 ++-- wgpu/src/layer.rs | 16 +++++ wgpu/src/lib.rs | 62 ++++++++++++++++++- wgpu/src/primitive.rs | 103 +++++++++++++++++++++++++++----- wgpu/src/primitive/pipeline.rs | 115 ------------------------------------ wgpu/src/triangle.rs | 7 +-- widget/src/shader.rs | 11 ++-- widget/src/shader/program.rs | 4 +- 11 files changed, 219 insertions(+), 172 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 446d3769..2ab50137 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -147,13 +147,20 @@ impl Rectangle { } /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates. - pub fn snap(self) -> Rectangle { - Rectangle { + pub fn snap(self) -> Option> { + let width = self.width as u32; + let height = self.height as u32; + + if width < 1 || height < 1 { + return None; + } + + Some(Rectangle { x: self.x as u32, y: self.y as u32, - width: self.width as u32, - height: self.height as u32, - } + width, + height, + }) } /// Expands the [`Rectangle`] a given amount. diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs index a35efdd9..5fa42188 100644 --- a/examples/custom_shader/src/scene.rs +++ b/examples/custom_shader/src/scene.rs @@ -9,8 +9,8 @@ use pipeline::cube::{self, Cube}; use iced::mouse; use iced::time::Duration; -use iced::widget::shader; -use iced::{Color, Rectangle, Size}; +use iced::widget::shader::{self, Viewport}; +use iced::{Color, Rectangle}; use glam::Vec3; use rand::Rng; @@ -130,25 +130,29 @@ impl Primitive { impl shader::Primitive for Primitive { fn prepare( &self, - format: wgpu::TextureFormat, device: &wgpu::Device, queue: &wgpu::Queue, - _bounds: Rectangle, - target_size: Size, - _scale_factor: f32, + format: wgpu::TextureFormat, storage: &mut shader::Storage, + _bounds: &Rectangle, + viewport: &Viewport, ) { if !storage.has::() { - storage.store(Pipeline::new(device, queue, format, target_size)); + storage.store(Pipeline::new( + device, + queue, + format, + viewport.physical_size(), + )); } let pipeline = storage.get_mut::().unwrap(); - //upload data to GPU + // Upload data to GPU pipeline.update( device, queue, - target_size, + viewport.physical_size(), &self.uniforms, self.cubes.len(), &self.cubes, @@ -157,20 +161,19 @@ impl shader::Primitive for Primitive { fn render( &self, + encoder: &mut wgpu::CommandEncoder, storage: &shader::Storage, target: &wgpu::TextureView, - _target_size: Size, - viewport: Rectangle, - encoder: &mut wgpu::CommandEncoder, + clip_bounds: &Rectangle, ) { - //at this point our pipeline should always be initialized + // At this point our pipeline should always be initialized let pipeline = storage.get::().unwrap(); - //render primitive + // Render primitive pipeline.render( target, encoder, - viewport, + *clip_bounds, self.cubes.len() as u32, self.show_depth_buffer, ); diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 2676ba87..975f4866 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -399,19 +399,19 @@ where } #[cfg(feature = "wgpu")] -impl iced_wgpu::primitive::pipeline::Renderer for Renderer +impl iced_wgpu::primitive::Renderer for Renderer where - A: iced_wgpu::primitive::pipeline::Renderer, + A: iced_wgpu::primitive::Renderer, B: core::Renderer, { - fn draw_pipeline_primitive( + fn draw_primitive( &mut self, bounds: Rectangle, - primitive: impl iced_wgpu::primitive::pipeline::Primitive, + primitive: impl iced_wgpu::Primitive, ) { match self { Self::Primary(renderer) => { - renderer.draw_pipeline_primitive(bounds, primitive); + renderer.draw_primitive(bounds, primitive); } Self::Secondary(_) => { log::warn!( diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs index e45b62b2..96cd6db8 100644 --- a/wgpu/src/engine.rs +++ b/wgpu/src/engine.rs @@ -1,19 +1,21 @@ use crate::buffer; use crate::graphics::Antialiasing; -use crate::primitive::pipeline; +use crate::primitive; use crate::quad; use crate::text; use crate::triangle; #[allow(missing_debug_implementations)] pub struct Engine { + pub(crate) staging_belt: wgpu::util::StagingBelt, + pub(crate) format: wgpu::TextureFormat, + pub(crate) quad_pipeline: quad::Pipeline, pub(crate) text_pipeline: text::Pipeline, pub(crate) triangle_pipeline: triangle::Pipeline, - pub(crate) _pipeline_storage: pipeline::Storage, #[cfg(any(feature = "image", feature = "svg"))] pub(crate) image_pipeline: crate::image::Pipeline, - pub(crate) staging_belt: wgpu::util::StagingBelt, + pub(crate) primitive_storage: primitive::Storage, } impl Engine { @@ -43,13 +45,16 @@ impl Engine { staging_belt: wgpu::util::StagingBelt::new( buffer::MAX_WRITE_SIZE as u64, ), + format, + quad_pipeline, text_pipeline, triangle_pipeline, - _pipeline_storage: pipeline::Storage::default(), #[cfg(any(feature = "image", feature = "svg"))] image_pipeline, + + primitive_storage: primitive::Storage::default(), } } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index c8c27c61..7a18e322 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -4,6 +4,7 @@ use crate::graphics::color; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Mesh; use crate::image::{self, Image}; +use crate::primitive::{self, Primitive}; use crate::quad::{self, Quad}; use crate::text::{self, Text}; use crate::triangle; @@ -13,6 +14,7 @@ pub struct Layer { pub bounds: Rectangle, pub quads: quad::Batch, pub triangles: triangle::Batch, + pub primitives: primitive::Batch, pub text: text::Batch, pub images: image::Batch, } @@ -23,6 +25,7 @@ impl Default for Layer { bounds: Rectangle::INFINITE, quads: quad::Batch::default(), triangles: triangle::Batch::default(), + primitives: primitive::Batch::default(), text: text::Batch::default(), images: image::Batch::default(), } @@ -222,6 +225,18 @@ impl Stack { }); } + pub fn draw_primitive( + &mut self, + bounds: Rectangle, + primitive: Box, + ) { + let bounds = bounds * self.transformation(); + + self.layers[self.current] + .primitives + .push(primitive::Instance { bounds, primitive }); + } + pub fn push_clip(&mut self, bounds: Rectangle) { self.previous.push(self.current); @@ -282,6 +297,7 @@ impl Stack { live.quads.clear(); live.triangles.clear(); + live.primitives.clear(); live.text.clear(); live.images.clear(); pending_meshes.clear(); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 030bcade..0580399d 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -101,8 +101,6 @@ impl Renderer { } } - pub fn draw_primitive(&mut self, _primitive: Primitive) {} - pub fn present>( &mut self, engine: &mut Engine, @@ -158,6 +156,19 @@ impl Renderer { ); } + if !layer.primitives.is_empty() { + for instance in &layer.primitives { + instance.primitive.prepare( + device, + queue, + engine.format, + &mut engine.primitive_storage, + &instance.bounds, + viewport, + ); + } + } + if !layer.text.is_empty() { engine.text_pipeline.prepare( device, @@ -247,7 +258,9 @@ impl Renderer { continue; }; - let scissor_rect = physical_bounds.snap(); + let Some(scissor_rect) = physical_bounds.snap() else { + continue; + }; if !layer.quads.is_empty() { engine.quad_pipeline.render( @@ -293,6 +306,43 @@ impl Renderer { )); } + if !layer.primitives.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + for instance in &layer.primitives { + if let Some(clip_bounds) = (instance.bounds * scale) + .intersection(&physical_bounds) + .and_then(Rectangle::snap) + { + instance.primitive.render( + encoder, + &engine.primitive_storage, + frame, + &clip_bounds, + ); + } + } + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + if !layer.text.is_empty() { text_layer += engine.text_pipeline.render( &self.text_storage, @@ -520,6 +570,12 @@ impl graphics::geometry::Renderer for Renderer { } } +impl primitive::Renderer for Renderer { + fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) { + self.layers.draw_primitive(bounds, Box::new(primitive)); + } +} + impl graphics::compositor::Default for crate::Renderer { type Compositor = window::Compositor; } diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 8e311d2b..4ba1ed9a 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,20 +1,95 @@ -//! Draw using different graphical primitives. -pub mod pipeline; +//! Draw custom primitives. +use crate::core::{self, Rectangle}; +use crate::graphics::Viewport; -pub use pipeline::Pipeline; +use rustc_hash::FxHashMap; +use std::any::{Any, TypeId}; +use std::fmt::Debug; -use crate::graphics::Mesh; +/// A batch of primitives. +pub type Batch = Vec; -use std::fmt::Debug; +/// A set of methods which allows a [`Primitive`] to be rendered. +pub trait Primitive: Debug + Send + Sync + 'static { + /// Processes the [`Primitive`], allowing for GPU buffer allocation. + fn prepare( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + storage: &mut Storage, + bounds: &Rectangle, + viewport: &Viewport, + ); + + /// Renders the [`Primitive`]. + fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + storage: &Storage, + target: &wgpu::TextureView, + clip_bounds: &Rectangle, + ); +} + +#[derive(Debug)] +/// An instance of a specific [`Primitive`]. +pub struct Instance { + /// The bounds of the [`Instance`]. + pub bounds: Rectangle, + + /// The [`Primitive`] to render. + pub primitive: Box, +} + +impl Instance { + /// Creates a new [`Pipeline`] with the given [`Primitive`]. + pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { + Instance { + bounds, + primitive: Box::new(primitive), + } + } +} + +/// A renderer than can draw custom primitives. +pub trait Renderer: core::Renderer { + /// Draws a custom primitive. + fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive); +} + +/// Stores custom, user-provided types. +#[derive(Default, Debug)] +pub struct Storage { + pipelines: FxHashMap>, +} + +impl Storage { + /// Returns `true` if `Storage` contains a type `T`. + pub fn has(&self) -> bool { + self.pipelines.get(&TypeId::of::()).is_some() + } + + /// Inserts the data `T` in to [`Storage`]. + pub fn store(&mut self, data: T) { + let _ = self.pipelines.insert(TypeId::of::(), Box::new(data)); + } -/// The graphical primitives supported by `iced_wgpu`. -pub type Primitive = crate::graphics::Primitive; + /// Returns a reference to the data with type `T` if it exists in [`Storage`]. + pub fn get(&self) -> Option<&T> { + self.pipelines.get(&TypeId::of::()).map(|pipeline| { + pipeline + .downcast_ref::() + .expect("Pipeline with this type does not exist in Storage.") + }) + } -/// The custom primitives supported by `iced_wgpu`. -#[derive(Debug, Clone, PartialEq)] -pub enum Custom { - /// A mesh primitive. - Mesh(Mesh), - /// A custom pipeline primitive. - Pipeline(Pipeline), + /// Returns a mutable reference to the data with type `T` if it exists in [`Storage`]. + pub fn get_mut(&mut self) -> Option<&mut T> { + self.pipelines.get_mut(&TypeId::of::()).map(|pipeline| { + pipeline + .downcast_mut::() + .expect("Pipeline with this type does not exist in Storage.") + }) + } } diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs index 59c54db9..8b137891 100644 --- a/wgpu/src/primitive/pipeline.rs +++ b/wgpu/src/primitive/pipeline.rs @@ -1,116 +1 @@ -//! Draw primitives using custom pipelines. -use crate::core::{self, Rectangle, Size}; -use rustc_hash::FxHashMap; -use std::any::{Any, TypeId}; -use std::fmt::Debug; -use std::sync::Arc; - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Pipeline { - /// The bounds of the [`Pipeline`]. - pub bounds: Rectangle, - - /// The [`Primitive`] to render. - pub primitive: Arc, -} - -impl Pipeline { - /// Creates a new [`Pipeline`] with the given [`Primitive`]. - pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { - Pipeline { - bounds, - primitive: Arc::new(primitive), - } - } -} - -impl PartialEq for Pipeline { - fn eq(&self, other: &Self) -> bool { - self.primitive.type_id() == other.primitive.type_id() - } -} - -/// A set of methods which allows a [`Primitive`] to be rendered. -pub trait Primitive: Debug + Send + Sync + 'static { - /// Processes the [`Primitive`], allowing for GPU buffer allocation. - fn prepare( - &self, - format: wgpu::TextureFormat, - device: &wgpu::Device, - queue: &wgpu::Queue, - bounds: Rectangle, - target_size: Size, - scale_factor: f32, - storage: &mut Storage, - ); - - /// Renders the [`Primitive`]. - fn render( - &self, - storage: &Storage, - target: &wgpu::TextureView, - target_size: Size, - viewport: Rectangle, - encoder: &mut wgpu::CommandEncoder, - ); -} - -/// A renderer than can draw custom pipeline primitives. -pub trait Renderer: core::Renderer { - /// Draws a custom pipeline primitive. - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl Primitive, - ); -} - -impl Renderer for crate::Renderer { - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl Primitive, - ) { - self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline( - Pipeline::new(bounds, primitive), - ))); - } -} - -/// Stores custom, user-provided pipelines. -#[derive(Default, Debug)] -pub struct Storage { - pipelines: FxHashMap>, -} - -impl Storage { - /// Returns `true` if `Storage` contains a pipeline with type `T`. - pub fn has(&self) -> bool { - self.pipelines.get(&TypeId::of::()).is_some() - } - - /// Inserts the pipeline `T` in to [`Storage`]. - pub fn store(&mut self, pipeline: T) { - let _ = self.pipelines.insert(TypeId::of::(), Box::new(pipeline)); - } - - /// Returns a reference to pipeline with type `T` if it exists in [`Storage`]. - pub fn get(&self) -> Option<&T> { - self.pipelines.get(&TypeId::of::()).map(|pipeline| { - pipeline - .downcast_ref::() - .expect("Pipeline with this type does not exist in Storage.") - }) - } - - /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`]. - pub fn get_mut(&mut self) -> Option<&mut T> { - self.pipelines.get_mut(&TypeId::of::()).map(|pipeline| { - pipeline - .downcast_mut::() - .expect("Pipeline with this type does not exist in Storage.") - }) - } -} diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 98ba41b3..8470ea39 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -501,14 +501,13 @@ impl Layer { let mut last_is_solid = None; for (index, mesh) in meshes.iter().enumerate() { - let Some(clip_bounds) = - bounds.intersection(&(mesh.clip_bounds() * transformation)) + let Some(clip_bounds) = bounds + .intersection(&(mesh.clip_bounds() * transformation)) + .and_then(Rectangle::snap) else { continue; }; - let clip_bounds = clip_bounds.snap(); - render_pass.set_scissor_rect( clip_bounds.x, clip_bounds.y, diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 68112f83..fad2f4eb 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -13,12 +13,13 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::window; use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size}; -use crate::renderer::wgpu::primitive::pipeline; +use crate::renderer::wgpu::primitive; use std::marker::PhantomData; +pub use crate::graphics::Viewport; pub use crate::renderer::wgpu::wgpu; -pub use pipeline::{Primitive, Storage}; +pub use primitive::{Primitive, Storage}; /// A widget which can render custom shaders with Iced's `wgpu` backend. /// @@ -60,7 +61,7 @@ impl Widget for Shader where P: Program, - Renderer: pipeline::Renderer, + Renderer: primitive::Renderer, { fn tag(&self) -> tree::Tag { struct Tag(T); @@ -160,7 +161,7 @@ where let bounds = layout.bounds(); let state = tree.state.downcast_ref::(); - renderer.draw_pipeline_primitive( + renderer.draw_primitive( bounds, self.program.draw(state, cursor_position, bounds), ); @@ -171,7 +172,7 @@ impl<'a, Message, Theme, Renderer, P> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Renderer: pipeline::Renderer, + Renderer: primitive::Renderer, P: Program + 'a, { fn from( diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs index 6dd50404..902c7c3b 100644 --- a/widget/src/shader/program.rs +++ b/widget/src/shader/program.rs @@ -1,7 +1,7 @@ use crate::core::event; use crate::core::mouse; use crate::core::{Rectangle, Shell}; -use crate::renderer::wgpu::primitive::pipeline; +use crate::renderer::wgpu::Primitive; use crate::shader; /// The state and logic of a [`Shader`] widget. @@ -15,7 +15,7 @@ pub trait Program { type State: Default + 'static; /// The type of primitive this [`Program`] can draw. - type Primitive: pipeline::Primitive + 'static; + type Primitive: Primitive + 'static; /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a -- cgit From f88488543f0b2d0ca95753d1e6527ba06981290a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 8 Apr 2024 15:05:12 +0200 Subject: Remove leftover `primitive::pipeline` module --- wgpu/src/primitive/pipeline.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 wgpu/src/primitive/pipeline.rs diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs deleted file mode 100644 index 8b137891..00000000 --- a/wgpu/src/primitive/pipeline.rs +++ /dev/null @@ -1 +0,0 @@ - -- cgit From 2c6fd9ac14c5d270e05b97b7a7fab811d25834c4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 8 Apr 2024 15:35:54 +0200 Subject: Make arguments of `Renderer::new` explicit in `iced_wgpu` --- benches/wgpu.rs | 11 +---------- examples/integration/src/main.rs | 7 ++++--- wgpu/src/lib.rs | 10 +++++++--- wgpu/src/window/compositor.rs | 6 +++++- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 780c6bc2..61b4eb6c 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -63,16 +63,7 @@ fn benchmark( Some(Antialiasing::MSAAx4), ); - let mut renderer = Renderer::new( - iced_wgpu::Settings { - present_mode: wgpu::PresentMode::Immediate, - backends: wgpu::Backends::all(), - default_font: Font::DEFAULT, - default_text_size: Pixels::from(16), - antialiasing: Some(Antialiasing::MSAAx4), - }, - &engine, - ); + let mut renderer = Renderer::new(&engine, Font::DEFAULT, Pixels::from(16)); let viewport = graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0); diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index c292709f..c4b57ecf 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -5,12 +5,12 @@ use controls::Controls; use scene::Scene; use iced_wgpu::graphics::Viewport; -use iced_wgpu::{wgpu, Engine, Renderer, Settings}; +use iced_wgpu::{wgpu, Engine, Renderer}; use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; use iced_winit::core::window; -use iced_winit::core::{Color, Size, Theme}; +use iced_winit::core::{Color, Font, Pixels, Size, Theme}; use iced_winit::futures; use iced_winit::runtime::program; use iced_winit::runtime::Debug; @@ -156,7 +156,8 @@ pub fn main() -> Result<(), Box> { // Initialize iced let mut debug = Debug::new(); let mut engine = Engine::new(&adapter, &device, &queue, format, None); - let mut renderer = Renderer::new(Settings::default(), &engine); + let mut renderer = + Renderer::new(&engine, Font::default(), Pixels::from(16)); let mut state = program::State::new( controls, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 0580399d..b1ae4e89 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -87,10 +87,14 @@ pub struct Renderer { } impl Renderer { - pub fn new(settings: Settings, _engine: &Engine) -> Self { + pub fn new( + _engine: &Engine, + default_font: Font, + default_text_size: Pixels, + ) -> Self { Self { - default_font: settings.default_font, - default_text_size: settings.default_text_size, + default_font, + default_text_size, layers: layer::Stack::new(), triangle_storage: triangle::Storage::new(), diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index d546a6dc..095afd48 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -289,7 +289,11 @@ impl graphics::Compositor for Compositor { } fn create_renderer(&self) -> Self::Renderer { - Renderer::new(self.settings, &self.engine) + Renderer::new( + &self.engine, + self.settings.default_font, + self.settings.default_text_size, + ) } fn create_surface( -- cgit From 6ad5bb3597f640ac329801adf735d633bf0a512f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 9 Apr 2024 22:25:16 +0200 Subject: Port `iced_tiny_skia` to new layering architecture --- core/src/renderer.rs | 8 +- core/src/renderer/null.rs | 8 +- core/src/text.rs | 4 - graphics/src/backend.rs | 36 -- graphics/src/cached.rs | 18 - graphics/src/compositor.rs | 14 +- graphics/src/damage.rs | 257 --------- graphics/src/layer.rs | 139 +++++ graphics/src/lib.rs | 10 +- graphics/src/primitive.rs | 160 ------ graphics/src/renderer.rs | 2 +- graphics/src/text.rs | 2 +- renderer/src/fallback.rs | 18 +- tiny_skia/src/backend.rs | 1033 ------------------------------------ tiny_skia/src/engine.rs | 831 +++++++++++++++++++++++++++++ tiny_skia/src/geometry.rs | 143 +++-- tiny_skia/src/layer.rs | 243 +++++++++ tiny_skia/src/lib.rs | 383 ++++++++++++- tiny_skia/src/primitive.rs | 33 +- tiny_skia/src/settings.rs | 4 +- tiny_skia/src/text.rs | 29 +- tiny_skia/src/window/compositor.rs | 72 +-- wgpu/src/geometry.rs | 24 +- wgpu/src/layer.rs | 301 +++++------ wgpu/src/lib.rs | 58 +- wgpu/src/primitive.rs | 6 +- winit/src/application.rs | 12 +- winit/src/multi_window.rs | 7 +- 28 files changed, 1934 insertions(+), 1921 deletions(-) delete mode 100644 graphics/src/backend.rs delete mode 100644 graphics/src/damage.rs create mode 100644 graphics/src/layer.rs delete mode 100644 graphics/src/primitive.rs delete mode 100644 tiny_skia/src/backend.rs create mode 100644 tiny_skia/src/engine.rs create mode 100644 tiny_skia/src/layer.rs diff --git a/core/src/renderer.rs b/core/src/renderer.rs index f5ef8f68..a2785ae8 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -14,7 +14,7 @@ pub trait Renderer { /// Ends recording a new layer. /// /// The new layer will clip its contents to the provided `bounds`. - fn end_layer(&mut self, bounds: Rectangle); + fn end_layer(&mut self); /// Draws the primitives recorded in the given closure in a new layer. /// @@ -22,7 +22,7 @@ pub trait Renderer { fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { self.start_layer(bounds); f(self); - self.end_layer(bounds); + self.end_layer(); } /// Starts recording with a new [`Transformation`]. @@ -31,7 +31,7 @@ pub trait Renderer { /// Ends recording a new layer. /// /// The new layer will clip its contents to the provided `bounds`. - fn end_transformation(&mut self, transformation: Transformation); + fn end_transformation(&mut self); /// Applies a [`Transformation`] to the primitives recorded in the given closure. fn with_transformation( @@ -41,7 +41,7 @@ pub trait Renderer { ) { self.start_transformation(transformation); f(self); - self.end_transformation(transformation); + self.end_transformation(); } /// Applies a translation to the primitives recorded in the given closure. diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index f36d19aa..fe38ec87 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -7,16 +7,14 @@ use crate::{ Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; -use std::borrow::Cow; - impl Renderer for () { fn start_layer(&mut self, _bounds: Rectangle) {} - fn end_layer(&mut self, _bounds: Rectangle) {} + fn end_layer(&mut self) {} fn start_transformation(&mut self, _transformation: Transformation) {} - fn end_transformation(&mut self, _transformation: Transformation) {} + fn end_transformation(&mut self) {} fn clear(&mut self) {} @@ -45,8 +43,6 @@ impl text::Renderer for () { Pixels(16.0) } - fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - fn fill_paragraph( &mut self, _paragraph: &Self::Paragraph, diff --git a/core/src/text.rs b/core/src/text.rs index 3f1d2c77..b30feae0 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -11,7 +11,6 @@ pub use paragraph::Paragraph; use crate::alignment; use crate::{Color, Pixels, Point, Rectangle, Size}; -use std::borrow::Cow; use std::hash::{Hash, Hasher}; /// A paragraph. @@ -192,9 +191,6 @@ pub trait Renderer: crate::Renderer { /// Returns the default size of [`Text`]. fn default_size(&self) -> Pixels; - /// Loads a [`Self::Font`] from its bytes. - fn load_font(&mut self, font: Cow<'static, [u8]>); - /// Draws the given [`Paragraph`] at the given position and with the given /// [`Color`]. fn fill_paragraph( diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs deleted file mode 100644 index 7abc42c5..00000000 --- a/graphics/src/backend.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Write a graphics backend. -use crate::core::image; -use crate::core::svg; -use crate::core::Size; -use crate::{Compositor, Mesh, Renderer}; - -use std::borrow::Cow; - -/// The graphics backend of a [`Renderer`]. -/// -/// [`Renderer`]: crate::Renderer -pub trait Backend: Sized { - /// The custom kind of primitives this [`Backend`] supports. - type Primitive: TryFrom; - - /// The default compositor of this [`Backend`]. - type Compositor: Compositor>; -} - -/// A graphics backend that supports text rendering. -pub trait Text { - /// Loads a font from its bytes. - fn load_font(&mut self, font: Cow<'static, [u8]>); -} - -/// A graphics backend that supports image rendering. -pub trait Image { - /// Returns the dimensions of the provided image. - fn dimensions(&self, handle: &image::Handle) -> Size; -} - -/// A graphics backend that supports SVG rendering. -pub trait Svg { - /// Returns the viewport dimensions of the provided SVG. - fn viewport_dimensions(&self, handle: &svg::Handle) -> Size; -} diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs index 1ba82f9f..c0e4e029 100644 --- a/graphics/src/cached.rs +++ b/graphics/src/cached.rs @@ -1,7 +1,3 @@ -use crate::Primitive; - -use std::sync::Arc; - /// A piece of data that can be cached. pub trait Cached: Sized { /// The type of cache produced. @@ -18,20 +14,6 @@ pub trait Cached: Sized { fn cache(self, previous: Option) -> Self::Cache; } -impl Cached for Primitive { - type Cache = Arc; - - fn load(cache: &Arc) -> Self { - Self::Cache { - content: cache.clone(), - } - } - - fn cache(self, _previous: Option>) -> Arc { - Arc::new(self) - } -} - #[cfg(debug_assertions)] impl Cached for () { type Cache = (); diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 86472a58..47521eb0 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -5,9 +5,11 @@ use crate::futures::{MaybeSend, MaybeSync}; use crate::{Error, Settings, Viewport}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use std::future::Future; use thiserror::Error; +use std::borrow::Cow; +use std::future::Future; + /// A graphics compositor that can draw to windows. pub trait Compositor: Sized { /// The iced renderer of the backend. @@ -60,6 +62,14 @@ pub trait Compositor: Sized { /// Returns [`Information`] used by this [`Compositor`]. fn fetch_information(&self) -> Information; + /// Loads a font from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>) { + crate::text::font_system() + .write() + .expect("Write to font system") + .load_font(font); + } + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. /// /// [`Renderer`]: Self::Renderer @@ -168,6 +178,8 @@ impl Compositor for () { ) { } + fn load_font(&mut self, _font: Cow<'static, [u8]>) {} + fn fetch_information(&self) -> Information { Information { adapter: String::from("Null Renderer"), diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs deleted file mode 100644 index 8edf69d7..00000000 --- a/graphics/src/damage.rs +++ /dev/null @@ -1,257 +0,0 @@ -//! Track and compute the damage of graphical primitives. -use crate::core::alignment; -use crate::core::{Rectangle, Size}; -use crate::Primitive; - -use std::sync::Arc; - -/// A type that has some damage bounds. -pub trait Damage: PartialEq { - /// Returns the bounds of the [`Damage`]. - fn bounds(&self) -> Rectangle; -} - -impl Damage for Primitive { - fn bounds(&self) -> Rectangle { - match self { - Self::Text { - bounds, - horizontal_alignment, - vertical_alignment, - .. - } => { - let mut bounds = *bounds; - - bounds.x = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - bounds.y = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - bounds.expand(1.5) - } - Self::Paragraph { - paragraph, - position, - .. - } => { - let mut bounds = - Rectangle::new(*position, paragraph.min_bounds); - - bounds.x = match paragraph.horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; - - bounds.y = match paragraph.vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - bounds.expand(1.5) - } - Self::Editor { - editor, position, .. - } => { - let bounds = Rectangle::new(*position, editor.bounds); - - bounds.expand(1.5) - } - Self::RawText(raw) => { - // TODO: Add `size` field to `raw` to compute more accurate - // damage bounds (?) - raw.clip_bounds.expand(1.5) - } - Self::Quad { bounds, shadow, .. } if shadow.color.a > 0.0 => { - let bounds_with_shadow = Rectangle { - x: bounds.x + shadow.offset.x.min(0.0) - shadow.blur_radius, - y: bounds.y + shadow.offset.y.min(0.0) - shadow.blur_radius, - width: bounds.width - + shadow.offset.x.abs() - + shadow.blur_radius * 2.0, - height: bounds.height - + shadow.offset.y.abs() - + shadow.blur_radius * 2.0, - }; - - bounds_with_shadow.expand(1.0) - } - Self::Quad { bounds, .. } - | Self::Image { bounds, .. } - | Self::Svg { bounds, .. } => bounds.expand(1.0), - Self::Clip { bounds, .. } => bounds.expand(1.0), - Self::Group { primitives } => primitives - .iter() - .map(Self::bounds) - .fold(Rectangle::with_size(Size::ZERO), |a, b| { - Rectangle::union(&a, &b) - }), - Self::Transform { - transformation, - content, - } => content.bounds() * *transformation, - Self::Cache { content } => content.bounds(), - Self::Custom(custom) => custom.bounds(), - } - } -} - -fn regions(a: &Primitive, b: &Primitive) -> Vec { - match (a, b) { - ( - Primitive::Group { - primitives: primitives_a, - }, - Primitive::Group { - primitives: primitives_b, - }, - ) => return list(primitives_a, primitives_b), - ( - Primitive::Clip { - bounds: bounds_a, - content: content_a, - .. - }, - Primitive::Clip { - bounds: bounds_b, - content: content_b, - .. - }, - ) => { - if bounds_a == bounds_b { - return regions(content_a, content_b) - .into_iter() - .filter_map(|r| r.intersection(&bounds_a.expand(1.0))) - .collect(); - } else { - return vec![bounds_a.expand(1.0), bounds_b.expand(1.0)]; - } - } - ( - Primitive::Transform { - transformation: transformation_a, - content: content_a, - }, - Primitive::Transform { - transformation: transformation_b, - content: content_b, - }, - ) => { - if transformation_a == transformation_b { - return regions(content_a, content_b) - .into_iter() - .map(|r| r * *transformation_a) - .collect(); - } - } - ( - Primitive::Cache { content: content_a }, - Primitive::Cache { content: content_b }, - ) => { - if Arc::ptr_eq(content_a, content_b) { - return vec![]; - } - } - _ if a == b => return vec![], - _ => {} - } - - let bounds_a = a.bounds(); - let bounds_b = b.bounds(); - - if bounds_a == bounds_b { - vec![bounds_a] - } else { - vec![bounds_a, bounds_b] - } -} - -/// Computes the damage regions between the two given lists of primitives. -pub fn list( - previous: &[Primitive], - current: &[Primitive], -) -> Vec { - let damage = previous - .iter() - .zip(current) - .flat_map(|(a, b)| regions(a, b)); - - if previous.len() == current.len() { - damage.collect() - } else { - let (smaller, bigger) = if previous.len() < current.len() { - (previous, current) - } else { - (current, previous) - }; - - // Extend damage by the added/removed primitives - damage - .chain(bigger[smaller.len()..].iter().map(Damage::bounds)) - .collect() - } -} - -/// Groups the given damage regions that are close together inside the given -/// bounds. -pub fn group( - mut damage: Vec, - scale_factor: f32, - bounds: Size, -) -> Vec { - use std::cmp::Ordering; - - const AREA_THRESHOLD: f32 = 20_000.0; - - let bounds = Rectangle { - x: 0.0, - y: 0.0, - width: bounds.width as f32, - height: bounds.height as f32, - }; - - damage.sort_by(|a, b| { - a.x.partial_cmp(&b.x) - .unwrap_or(Ordering::Equal) - .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal)) - }); - - let mut output = Vec::new(); - let mut scaled = damage - .into_iter() - .filter_map(|region| (region * scale_factor).intersection(&bounds)) - .filter(|region| region.width >= 1.0 && region.height >= 1.0); - - if let Some(mut current) = scaled.next() { - for region in scaled { - let union = current.union(®ion); - - if union.area() - current.area() - region.area() <= AREA_THRESHOLD { - current = union; - } else { - output.push(current); - current = region; - } - } - - output.push(current); - } - - output -} diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs new file mode 100644 index 00000000..0187cc59 --- /dev/null +++ b/graphics/src/layer.rs @@ -0,0 +1,139 @@ +//! Draw and stack layers of graphical primitives. +use crate::core::{Rectangle, Transformation}; + +/// A layer of graphical primitives. +/// +/// Layers normally dictate a set of primitives that are +/// rendered in a specific order. +pub trait Layer: Default { + /// Creates a new [`Layer`] with the given bounds. + fn with_bounds(bounds: Rectangle) -> Self; + + /// Flushes and settles any pending group of primitives in the [`Layer`]. + /// + /// This will be called when a [`Layer`] is finished. It allows layers to efficiently + /// record primitives together and defer grouping until the end. + fn flush(&mut self); + + /// Resizes the [`Layer`] to the given bounds. + fn resize(&mut self, bounds: Rectangle); + + /// Clears all the layers contents and resets its bounds. + fn reset(&mut self); +} + +/// A stack of layers used for drawing. +#[derive(Debug)] +pub struct Stack { + layers: Vec, + transformations: Vec, + previous: Vec, + current: usize, + active_count: usize, +} + +impl Stack { + /// Creates a new empty [`Stack`]. + pub fn new() -> Self { + Self { + layers: vec![T::default()], + transformations: vec![Transformation::IDENTITY], + previous: vec![], + current: 0, + active_count: 1, + } + } + + /// Returns a mutable reference to the current [`Layer`] of the [`Stack`], together with + /// the current [`Transformation`]. + #[inline] + pub fn current_mut(&mut self) -> (&mut T, Transformation) { + let transformation = self.transformation(); + + (&mut self.layers[self.current], transformation) + } + + /// Returns the current [`Transformation`] of the [`Stack`]. + #[inline] + pub fn transformation(&self) -> Transformation { + self.transformations.last().copied().unwrap() + } + + /// Pushes a new clipping region in the [`Stack`]; creating a new layer in the + /// process. + pub fn push_clip(&mut self, bounds: Rectangle) { + self.previous.push(self.current); + + self.current = self.active_count; + self.active_count += 1; + + let bounds = bounds * self.transformation(); + + if self.current == self.layers.len() { + self.layers.push(T::with_bounds(bounds)); + } else { + self.layers[self.current].resize(bounds); + } + } + + /// Pops the current clipping region from the [`Stack`] and restores the previous one. + /// + /// The current layer will be recorded for drawing. + pub fn pop_clip(&mut self) { + self.flush(); + + self.current = self.previous.pop().unwrap(); + } + + /// Pushes a new [`Transformation`] in the [`Stack`]. + /// + /// Future drawing operations will be affected by this new [`Transformation`] until + /// it is popped using [`pop_transformation`]. + /// + /// [`pop_transformation`]: Self::pop_transformation + pub fn push_transformation(&mut self, transformation: Transformation) { + self.transformations + .push(self.transformation() * transformation); + } + + /// Pops the current [`Transformation`] in the [`Stack`]. + pub fn pop_transformation(&mut self) { + let _ = self.transformations.pop(); + } + + /// Returns an iterator over mutable references to the layers in the [`Stack`]. + pub fn iter_mut(&mut self) -> impl Iterator { + self.flush(); + + self.layers[..self.active_count].iter_mut() + } + + /// Returns an iterator over immutable references to the layers in the [`Stack`]. + pub fn iter(&self) -> impl Iterator { + self.layers[..self.active_count].iter() + } + + /// Flushes and settles any primitives in the current layer of the [`Stack`]. + pub fn flush(&mut self) { + self.layers[self.current].flush(); + } + + /// Clears the layers of the [`Stack`], allowing reuse. + /// + /// This will normally keep layer allocations for future drawing operations. + pub fn clear(&mut self) { + for layer in self.layers[..self.active_count].iter_mut() { + layer.reset(); + } + + self.current = 0; + self.active_count = 1; + self.previous.clear(); + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index b79ef70d..a9649c6e 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -10,35 +10,29 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod antialiasing; mod cached; -mod primitive; mod settings; mod viewport; -pub mod backend; pub mod color; pub mod compositor; -pub mod damage; pub mod error; pub mod gradient; pub mod image; +pub mod layer; pub mod mesh; -pub mod renderer; pub mod text; #[cfg(feature = "geometry")] pub mod geometry; pub use antialiasing::Antialiasing; -pub use backend::Backend; pub use cached::Cached; pub use compositor::Compositor; -pub use damage::Damage; pub use error::Error; pub use gradient::Gradient; pub use image::Image; +pub use layer::Layer; pub use mesh::Mesh; -pub use primitive::Primitive; -pub use renderer::Renderer; pub use settings::Settings; pub use text::Text; pub use viewport::Viewport; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs deleted file mode 100644 index 6929b0a1..00000000 --- a/graphics/src/primitive.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Draw using different graphical primitives. -use crate::core::alignment; -use crate::core::image; -use crate::core::svg; -use crate::core::text; -use crate::core::{ - Background, Border, Color, Font, Pixels, Point, Rectangle, Shadow, - Transformation, Vector, -}; -use crate::text::editor; -use crate::text::paragraph; - -use std::sync::Arc; - -/// A rendering primitive. -#[derive(Debug, Clone, PartialEq)] -pub enum Primitive { - /// A text primitive - Text { - /// The contents of the text. - content: String, - /// The bounds of the text. - bounds: Rectangle, - /// The color of the text. - color: Color, - /// The size of the text in logical pixels. - size: Pixels, - /// The line height of the text. - line_height: text::LineHeight, - /// The font of the text. - font: Font, - /// The horizontal alignment of the text. - horizontal_alignment: alignment::Horizontal, - /// The vertical alignment of the text. - vertical_alignment: alignment::Vertical, - /// The shaping strategy of the text. - shaping: text::Shaping, - /// The clip bounds of the text. - clip_bounds: Rectangle, - }, - /// A paragraph primitive - Paragraph { - /// The [`paragraph::Weak`] reference. - paragraph: paragraph::Weak, - /// The position of the paragraph. - position: Point, - /// The color of the paragraph. - color: Color, - /// The clip bounds of the paragraph. - clip_bounds: Rectangle, - }, - /// An editor primitive - Editor { - /// The [`editor::Weak`] reference. - editor: editor::Weak, - /// The position of the editor. - position: Point, - /// The color of the editor. - color: Color, - /// The clip bounds of the editor. - clip_bounds: Rectangle, - }, - /// A raw `cosmic-text` primitive - RawText(crate::text::Raw), - /// A quad primitive - Quad { - /// The bounds of the quad - bounds: Rectangle, - /// The background of the quad - background: Background, - /// The [`Border`] of the quad - border: Border, - /// The [`Shadow`] of the quad - shadow: Shadow, - }, - /// An image primitive - Image { - /// The handle of the image - handle: image::Handle, - /// The filter method of the image - filter_method: image::FilterMethod, - /// The bounds of the image - bounds: Rectangle, - }, - /// An SVG primitive - Svg { - /// The path of the SVG file - handle: svg::Handle, - - /// The [`Color`] filter - color: Option, - - /// The bounds of the viewport - bounds: Rectangle, - }, - /// A group of primitives - Group { - /// The primitives of the group - primitives: Vec>, - }, - /// A clip primitive - Clip { - /// The bounds of the clip - bounds: Rectangle, - /// The content of the clip - content: Box>, - }, - /// A primitive that applies a [`Transformation`] - Transform { - /// The [`Transformation`] - transformation: Transformation, - - /// The primitive to transform - content: Box>, - }, - /// A cached primitive. - /// - /// This can be useful if you are implementing a widget where primitive - /// generation is expensive. - Cache { - /// The cached primitive - content: Arc>, - }, - /// A backend-specific primitive. - Custom(T), -} - -impl Primitive { - /// Groups the current [`Primitive`]. - pub fn group(primitives: Vec) -> Self { - Self::Group { primitives } - } - - /// Clips the current [`Primitive`]. - pub fn clip(self, bounds: Rectangle) -> Self { - Self::Clip { - bounds, - content: Box::new(self), - } - } - - /// Translates the current [`Primitive`]. - pub fn translate(self, translation: Vector) -> Self { - Self::Transform { - transformation: Transformation::translate( - translation.x, - translation.y, - ), - content: Box::new(self), - } - } - - /// Transforms the current [`Primitive`]. - pub fn transform(self, transformation: Transformation) -> Self { - Self::Transform { - transformation, - content: Box::new(self), - } - } -} diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index d4f91dab..695759a4 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -66,7 +66,7 @@ impl iced_core::Renderer for Renderer { self.stack.push(std::mem::take(&mut self.primitives)); } - fn end_layer(&mut self, bounds: Rectangle) { + fn end_layer(&mut self) { let layer = std::mem::replace( &mut self.primitives, self.stack.pop().expect("a layer should be recording"), diff --git a/graphics/src/text.rs b/graphics/src/text.rs index f9fc1fec..c204c850 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -19,7 +19,7 @@ use std::borrow::Cow; use std::sync::{Arc, RwLock, Weak}; /// A text primitive. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Text { /// A paragraph. #[allow(missing_docs)] diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 975f4866..c932de00 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -9,6 +9,8 @@ use crate::graphics; use crate::graphics::compositor; use crate::graphics::mesh; +use std::borrow::Cow; + /// A renderer `A` with a fallback strategy `B`. /// /// This type can be used to easily compose existing renderers and @@ -51,8 +53,8 @@ where delegate!(self, renderer, renderer.start_layer(bounds)); } - fn end_layer(&mut self, bounds: Rectangle) { - delegate!(self, renderer, renderer.end_layer(bounds)); + fn end_layer(&mut self) { + delegate!(self, renderer, renderer.end_layer()); } fn start_transformation(&mut self, transformation: Transformation) { @@ -63,8 +65,8 @@ where ); } - fn end_transformation(&mut self, transformation: Transformation) { - delegate!(self, renderer, renderer.end_transformation(transformation)); + fn end_transformation(&mut self) { + delegate!(self, renderer, renderer.end_transformation()); } } @@ -93,10 +95,6 @@ where delegate!(self, renderer, renderer.default_size()) } - fn load_font(&mut self, font: std::borrow::Cow<'static, [u8]>) { - delegate!(self, renderer, renderer.load_font(font)); - } - fn fill_paragraph( &mut self, text: &Self::Paragraph, @@ -323,6 +321,10 @@ where } } + fn load_font(&mut self, font: Cow<'static, [u8]>) { + delegate!(self, compositor, compositor.load_font(font)); + } + fn fetch_information(&self) -> compositor::Information { delegate!(self, compositor, compositor.fetch_information()) } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs deleted file mode 100644 index d0f28876..00000000 --- a/tiny_skia/src/backend.rs +++ /dev/null @@ -1,1033 +0,0 @@ -use crate::core::{ - Background, Color, Gradient, Rectangle, Size, Transformation, Vector, -}; -use crate::graphics::backend; -use crate::graphics::text; -use crate::graphics::{Damage, Viewport}; -use crate::primitive::{self, Primitive}; -use crate::window; - -use std::borrow::Cow; - -#[derive(Debug)] -pub struct Backend { - text_pipeline: crate::text::Pipeline, - - #[cfg(feature = "image")] - raster_pipeline: crate::raster::Pipeline, - - #[cfg(feature = "svg")] - vector_pipeline: crate::vector::Pipeline, -} - -impl Backend { - pub fn new() -> Self { - Self { - text_pipeline: crate::text::Pipeline::new(), - - #[cfg(feature = "image")] - raster_pipeline: crate::raster::Pipeline::new(), - - #[cfg(feature = "svg")] - vector_pipeline: crate::vector::Pipeline::new(), - } - } - - pub fn draw>( - &mut self, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - primitives: &[Primitive], - viewport: &Viewport, - damage: &[Rectangle], - background_color: Color, - overlay: &[T], - ) { - let physical_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; - - if !overlay.is_empty() { - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - 0.0, - 0.0, - physical_size.width as f32, - physical_size.height as f32, - ) - .expect("Create damage rectangle"), - ); - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color(Color { - a: 0.1, - ..background_color - })), - anti_alias: false, - ..Default::default() - }, - tiny_skia::FillRule::default(), - tiny_skia::Transform::identity(), - None, - ); - } - - for ®ion in damage { - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - region.x, - region.y, - region.width, - region.height, - ) - .expect("Create damage rectangle"), - ); - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color( - background_color, - )), - anti_alias: false, - blend_mode: tiny_skia::BlendMode::Source, - ..Default::default() - }, - tiny_skia::FillRule::default(), - tiny_skia::Transform::identity(), - None, - ); - - adjust_clip_mask(clip_mask, region); - - for primitive in primitives { - self.draw_primitive( - primitive, - pixels, - clip_mask, - region, - scale_factor, - Transformation::IDENTITY, - ); - } - - if !overlay.is_empty() { - pixels.stroke_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color( - Color::from_rgb(1.0, 0.0, 0.0), - )), - anti_alias: false, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: 1.0, - ..tiny_skia::Stroke::default() - }, - tiny_skia::Transform::identity(), - None, - ); - } - } - - self.text_pipeline.trim_cache(); - - #[cfg(feature = "image")] - self.raster_pipeline.trim_cache(); - - #[cfg(feature = "svg")] - self.vector_pipeline.trim_cache(); - } - - fn draw_primitive( - &mut self, - primitive: &Primitive, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - clip_bounds: Rectangle, - scale_factor: f32, - transformation: Transformation, - ) { - match primitive { - Primitive::Quad { - bounds, - background, - border, - shadow, - } => { - debug_assert!( - bounds.width.is_normal(), - "Quad with non-normal width!" - ); - debug_assert!( - bounds.height.is_normal(), - "Quad with non-normal height!" - ); - - let physical_bounds = (*bounds * transformation) * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - let transform = into_transform(transformation) - .post_scale(scale_factor, scale_factor); - - // Make sure the border radius is not larger than the bounds - let border_width = border - .width - .min(bounds.width / 2.0) - .min(bounds.height / 2.0); - - let mut fill_border_radius = <[f32; 4]>::from(border.radius); - for radius in &mut fill_border_radius { - *radius = (*radius) - .min(bounds.width / 2.0) - .min(bounds.height / 2.0); - } - let path = rounded_rectangle(*bounds, fill_border_radius); - - if shadow.color.a > 0.0 { - let shadow_bounds = (Rectangle { - x: bounds.x + shadow.offset.x - shadow.blur_radius, - y: bounds.y + shadow.offset.y - shadow.blur_radius, - width: bounds.width + shadow.blur_radius * 2.0, - height: bounds.height + shadow.blur_radius * 2.0, - } * transformation) - * scale_factor; - - let radii = fill_border_radius - .into_iter() - .map(|radius| radius * scale_factor) - .collect::>(); - let (x, y, width, height) = ( - shadow_bounds.x as u32, - shadow_bounds.y as u32, - shadow_bounds.width as u32, - shadow_bounds.height as u32, - ); - let half_width = physical_bounds.width / 2.0; - let half_height = physical_bounds.height / 2.0; - - let colors = (y..y + height) - .flat_map(|y| { - (x..x + width).map(move |x| (x as f32, y as f32)) - }) - .filter_map(|(x, y)| { - tiny_skia::Size::from_wh(half_width, half_height) - .map(|size| { - let shadow_distance = rounded_box_sdf( - Vector::new( - x - physical_bounds.position().x - - (shadow.offset.x - * scale_factor) - - half_width, - y - physical_bounds.position().y - - (shadow.offset.y - * scale_factor) - - half_height, - ), - size, - &radii, - ) - .max(0.0); - let shadow_alpha = 1.0 - - smoothstep( - -shadow.blur_radius * scale_factor, - shadow.blur_radius * scale_factor, - shadow_distance, - ); - - let mut color = into_color(shadow.color); - color.apply_opacity(shadow_alpha); - - color.to_color_u8().premultiply() - }) - }) - .collect(); - - if let Some(pixmap) = tiny_skia::IntSize::from_wh( - width, height, - ) - .and_then(|size| { - tiny_skia::Pixmap::from_vec( - bytemuck::cast_vec(colors), - size, - ) - }) { - pixels.draw_pixmap( - x as i32, - y as i32, - pixmap.as_ref(), - &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::default(), - None, - ); - } - } - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: match background { - Background::Color(color) => { - tiny_skia::Shader::SolidColor(into_color( - *color, - )) - } - Background::Gradient(Gradient::Linear(linear)) => { - let (start, end) = - linear.angle.to_distance(bounds); - - let stops: Vec = - linear - .stops - .into_iter() - .flatten() - .map(|stop| { - tiny_skia::GradientStop::new( - stop.offset, - tiny_skia::Color::from_rgba( - stop.color.b, - stop.color.g, - stop.color.r, - stop.color.a, - ) - .expect("Create color"), - ) - }) - .collect(); - - tiny_skia::LinearGradient::new( - tiny_skia::Point { - x: start.x, - y: start.y, - }, - tiny_skia::Point { x: end.x, y: end.y }, - if stops.is_empty() { - vec![tiny_skia::GradientStop::new( - 0.0, - tiny_skia::Color::BLACK, - )] - } else { - stops - }, - tiny_skia::SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .expect("Create linear gradient") - } - }, - anti_alias: true, - ..tiny_skia::Paint::default() - }, - tiny_skia::FillRule::EvenOdd, - transform, - clip_mask, - ); - - if border_width > 0.0 { - // Border path is offset by half the border width - let border_bounds = Rectangle { - x: bounds.x + border_width / 2.0, - y: bounds.y + border_width / 2.0, - width: bounds.width - border_width, - height: bounds.height - border_width, - }; - - // Make sure the border radius is correct - let mut border_radius = <[f32; 4]>::from(border.radius); - let mut is_simple_border = true; - - for radius in &mut border_radius { - *radius = if *radius == 0.0 { - // Path should handle this fine - 0.0 - } else if *radius > border_width / 2.0 { - *radius - border_width / 2.0 - } else { - is_simple_border = false; - 0.0 - } - .min(border_bounds.width / 2.0) - .min(border_bounds.height / 2.0); - } - - // Stroking a path works well in this case - if is_simple_border { - let border_path = - rounded_rectangle(border_bounds, border_radius); - - pixels.stroke_path( - &border_path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor( - into_color(border.color), - ), - anti_alias: true, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: border_width, - ..tiny_skia::Stroke::default() - }, - transform, - clip_mask, - ); - } else { - // Draw corners that have too small border radii as having no border radius, - // but mask them with the rounded rectangle with the correct border radius. - let mut temp_pixmap = tiny_skia::Pixmap::new( - bounds.width as u32, - bounds.height as u32, - ) - .unwrap(); - - let mut quad_mask = tiny_skia::Mask::new( - bounds.width as u32, - bounds.height as u32, - ) - .unwrap(); - - let zero_bounds = Rectangle { - x: 0.0, - y: 0.0, - width: bounds.width, - height: bounds.height, - }; - let path = - rounded_rectangle(zero_bounds, fill_border_radius); - - quad_mask.fill_path( - &path, - tiny_skia::FillRule::EvenOdd, - true, - transform, - ); - let path_bounds = Rectangle { - x: border_width / 2.0, - y: border_width / 2.0, - width: bounds.width - border_width, - height: bounds.height - border_width, - }; - - let border_radius_path = - rounded_rectangle(path_bounds, border_radius); - - temp_pixmap.stroke_path( - &border_radius_path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor( - into_color(border.color), - ), - anti_alias: true, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: border_width, - ..tiny_skia::Stroke::default() - }, - transform, - Some(&quad_mask), - ); - - pixels.draw_pixmap( - bounds.x as i32, - bounds.y as i32, - temp_pixmap.as_ref(), - &tiny_skia::PixmapPaint::default(), - transform, - clip_mask, - ); - } - } - } - Primitive::Paragraph { - paragraph, - position, - color, - clip_bounds: _, // TODO: Support text clip bounds - } => { - let physical_bounds = - Rectangle::new(*position, paragraph.min_bounds) - * transformation - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_paragraph( - paragraph, - *position, - *color, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - Primitive::Editor { - editor, - position, - color, - clip_bounds: _, // TODO: Support text clip bounds - } => { - let physical_bounds = Rectangle::new(*position, editor.bounds) - * transformation - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_editor( - editor, - *position, - *color, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - Primitive::Text { - content, - bounds, - color, - size, - line_height, - font, - horizontal_alignment, - vertical_alignment, - shaping, - clip_bounds: _, // TODO: Support text clip bounds - } => { - let physical_bounds = - primitive.bounds() * transformation * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_cached( - content, - *bounds, - *color, - *size, - *line_height, - *font, - *horizontal_alignment, - *vertical_alignment, - *shaping, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - Primitive::RawText(text::Raw { - buffer, - position, - color, - clip_bounds: _, // TODO: Support text clip bounds - }) => { - let Some(buffer) = buffer.upgrade() else { - return; - }; - - let (width, height) = buffer.size(); - - let physical_bounds = - Rectangle::new(*position, Size::new(width, height)) - * transformation - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.text_pipeline.draw_raw( - &buffer, - *position, - *color, - scale_factor, - pixels, - clip_mask, - transformation, - ); - } - #[cfg(feature = "image")] - Primitive::Image { - handle, - filter_method, - bounds, - } => { - let physical_bounds = (*bounds * transformation) * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - let transform = into_transform(transformation) - .post_scale(scale_factor, scale_factor); - - self.raster_pipeline.draw( - handle, - *filter_method, - *bounds, - pixels, - transform, - clip_mask, - ); - } - #[cfg(not(feature = "image"))] - Primitive::Image { .. } => { - log::warn!( - "Unsupported primitive in `iced_tiny_skia`: {primitive:?}", - ); - } - #[cfg(feature = "svg")] - Primitive::Svg { - handle, - bounds, - color, - } => { - let physical_bounds = (*bounds * transformation) * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - self.vector_pipeline.draw( - handle, - *color, - (*bounds * transformation) * scale_factor, - pixels, - clip_mask, - ); - } - #[cfg(not(feature = "svg"))] - Primitive::Svg { .. } => { - log::warn!( - "Unsupported primitive in `iced_tiny_skia`: {primitive:?}", - ); - } - Primitive::Custom(primitive::Custom::Fill { - path, - paint, - rule, - }) => { - let bounds = path.bounds(); - - let physical_bounds = (Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width(), - height: bounds.height(), - } * transformation) - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - pixels.fill_path( - path, - paint, - *rule, - into_transform(transformation) - .post_scale(scale_factor, scale_factor), - clip_mask, - ); - } - Primitive::Custom(primitive::Custom::Stroke { - path, - paint, - stroke, - }) => { - let bounds = path.bounds(); - - let physical_bounds = (Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width().max(1.0), - height: bounds.height().max(1.0), - } * transformation) - * scale_factor; - - if !clip_bounds.intersects(&physical_bounds) { - return; - } - - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); - - pixels.stroke_path( - path, - paint, - stroke, - into_transform(transformation) - .post_scale(scale_factor, scale_factor), - clip_mask, - ); - } - Primitive::Group { primitives } => { - for primitive in primitives { - self.draw_primitive( - primitive, - pixels, - clip_mask, - clip_bounds, - scale_factor, - transformation, - ); - } - } - Primitive::Transform { - transformation: new_transformation, - content, - } => { - self.draw_primitive( - content, - pixels, - clip_mask, - clip_bounds, - scale_factor, - transformation * *new_transformation, - ); - } - Primitive::Clip { bounds, content } => { - let bounds = (*bounds * transformation) * scale_factor; - - if bounds == clip_bounds { - self.draw_primitive( - content, - pixels, - clip_mask, - bounds, - scale_factor, - transformation, - ); - } else if let Some(bounds) = clip_bounds.intersection(&bounds) { - if bounds.x + bounds.width <= 0.0 - || bounds.y + bounds.height <= 0.0 - || bounds.x as u32 >= pixels.width() - || bounds.y as u32 >= pixels.height() - || bounds.width <= 1.0 - || bounds.height <= 1.0 - { - return; - } - - adjust_clip_mask(clip_mask, bounds); - - self.draw_primitive( - content, - pixels, - clip_mask, - bounds, - scale_factor, - transformation, - ); - - adjust_clip_mask(clip_mask, clip_bounds); - } - } - Primitive::Cache { content } => { - self.draw_primitive( - content, - pixels, - clip_mask, - clip_bounds, - scale_factor, - transformation, - ); - } - } - } -} - -impl Default for Backend { - fn default() -> Self { - Self::new() - } -} - -fn into_color(color: Color) -> tiny_skia::Color { - tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) - .expect("Convert color from iced to tiny_skia") -} - -fn into_transform(transformation: Transformation) -> tiny_skia::Transform { - let translation = transformation.translation(); - - tiny_skia::Transform { - sx: transformation.scale_factor(), - kx: 0.0, - ky: 0.0, - sy: transformation.scale_factor(), - tx: translation.x, - ty: translation.y, - } -} - -fn rounded_rectangle( - bounds: Rectangle, - border_radius: [f32; 4], -) -> tiny_skia::Path { - let [top_left, top_right, bottom_right, bottom_left] = border_radius; - - if top_left == 0.0 - && top_right == 0.0 - && bottom_right == 0.0 - && bottom_left == 0.0 - { - return tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ) - .expect("Build quad rectangle"), - ); - } - - if top_left == top_right - && top_left == bottom_right - && top_left == bottom_left - && top_left == bounds.width / 2.0 - && top_left == bounds.height / 2.0 - { - return tiny_skia::PathBuilder::from_circle( - bounds.x + bounds.width / 2.0, - bounds.y + bounds.height / 2.0, - top_left, - ) - .expect("Build circle path"); - } - - let mut builder = tiny_skia::PathBuilder::new(); - - builder.move_to(bounds.x + top_left, bounds.y); - builder.line_to(bounds.x + bounds.width - top_right, bounds.y); - - if top_right > 0.0 { - arc_to( - &mut builder, - bounds.x + bounds.width - top_right, - bounds.y, - bounds.x + bounds.width, - bounds.y + top_right, - top_right, - ); - } - - maybe_line_to( - &mut builder, - bounds.x + bounds.width, - bounds.y + bounds.height - bottom_right, - ); - - if bottom_right > 0.0 { - arc_to( - &mut builder, - bounds.x + bounds.width, - bounds.y + bounds.height - bottom_right, - bounds.x + bounds.width - bottom_right, - bounds.y + bounds.height, - bottom_right, - ); - } - - maybe_line_to( - &mut builder, - bounds.x + bottom_left, - bounds.y + bounds.height, - ); - - if bottom_left > 0.0 { - arc_to( - &mut builder, - bounds.x + bottom_left, - bounds.y + bounds.height, - bounds.x, - bounds.y + bounds.height - bottom_left, - bottom_left, - ); - } - - maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); - - if top_left > 0.0 { - arc_to( - &mut builder, - bounds.x, - bounds.y + top_left, - bounds.x + top_left, - bounds.y, - top_left, - ); - } - - builder.finish().expect("Build rounded rectangle path") -} - -fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { - if path.last_point() != Some(tiny_skia::Point { x, y }) { - path.line_to(x, y); - } -} - -fn arc_to( - path: &mut tiny_skia::PathBuilder, - x_from: f32, - y_from: f32, - x_to: f32, - y_to: f32, - radius: f32, -) { - let svg_arc = kurbo::SvgArc { - from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), - to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), - radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)), - x_rotation: 0.0, - large_arc: false, - sweep: true, - }; - - match kurbo::Arc::from_svg_arc(&svg_arc) { - Some(arc) => { - arc.to_cubic_beziers(0.1, |p1, p2, p| { - path.cubic_to( - p1.x as f32, - p1.y as f32, - p2.x as f32, - p2.y as f32, - p.x as f32, - p.y as f32, - ); - }); - } - None => { - path.line_to(x_to, y_to); - } - } -} - -fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { - clip_mask.clear(); - - let path = { - let mut builder = tiny_skia::PathBuilder::new(); - builder.push_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ) - .unwrap(), - ); - - builder.finish().unwrap() - }; - - clip_mask.fill_path( - &path, - tiny_skia::FillRule::EvenOdd, - false, - tiny_skia::Transform::default(), - ); -} - -fn smoothstep(a: f32, b: f32, x: f32) -> f32 { - let x = ((x - a) / (b - a)).clamp(0.0, 1.0); - - x * x * (3.0 - 2.0 * x) -} - -fn rounded_box_sdf( - to_center: Vector, - size: tiny_skia::Size, - radii: &[f32], -) -> f32 { - let radius = match (to_center.x > 0.0, to_center.y > 0.0) { - (true, true) => radii[2], - (true, false) => radii[1], - (false, true) => radii[3], - (false, false) => radii[0], - }; - - let x = (to_center.x.abs() - size.width() + radius).max(0.0); - let y = (to_center.y.abs() - size.height() + radius).max(0.0); - - (x.powf(2.0) + y.powf(2.0)).sqrt() - radius -} - -impl backend::Backend for Backend { - type Primitive = primitive::Custom; - type Compositor = window::Compositor; -} - -impl backend::Text for Backend { - fn load_font(&mut self, font: Cow<'static, [u8]>) { - self.text_pipeline.load_font(font); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions( - &self, - handle: &crate::core::image::Handle, - ) -> crate::core::Size { - self.raster_pipeline.dimensions(handle) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> crate::core::Size { - self.vector_pipeline.viewport_dimensions(handle) - } -} - -#[cfg(feature = "geometry")] -impl crate::graphics::geometry::Backend for Backend { - type Frame = crate::geometry::Frame; - - fn new_frame(&self, size: Size) -> Self::Frame { - crate::geometry::Frame::new(size) - } -} diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs new file mode 100644 index 00000000..3930f46a --- /dev/null +++ b/tiny_skia/src/engine.rs @@ -0,0 +1,831 @@ +use crate::core::renderer::Quad; +use crate::core::{ + Background, Color, Gradient, Rectangle, Size, Transformation, Vector, +}; +use crate::graphics::{Image, Text}; +use crate::text; +use crate::Primitive; + +#[derive(Debug)] +pub struct Engine { + text_pipeline: text::Pipeline, + + #[cfg(feature = "image")] + pub(crate) raster_pipeline: crate::raster::Pipeline, + #[cfg(feature = "svg")] + pub(crate) vector_pipeline: crate::vector::Pipeline, +} + +impl Engine { + pub fn new() -> Self { + Self { + text_pipeline: text::Pipeline::new(), + #[cfg(feature = "image")] + raster_pipeline: crate::raster::Pipeline::new(), + #[cfg(feature = "svg")] + vector_pipeline: crate::vector::Pipeline::new(), + } + } + + pub fn draw_quad( + &mut self, + quad: &Quad, + background: &Background, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + debug_assert!( + quad.bounds.width.is_normal(), + "Quad with non-normal width!" + ); + debug_assert!( + quad.bounds.height.is_normal(), + "Quad with non-normal height!" + ); + + let physical_bounds = quad.bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let transform = into_transform(transformation); + + // Make sure the border radius is not larger than the bounds + let border_width = quad + .border + .width + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + + let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius); + + for radius in &mut fill_border_radius { + *radius = (*radius) + .min(quad.bounds.width / 2.0) + .min(quad.bounds.height / 2.0); + } + + let path = rounded_rectangle(quad.bounds, fill_border_radius); + + let shadow = quad.shadow; + + if shadow.color.a > 0.0 { + let shadow_bounds = Rectangle { + x: quad.bounds.x + shadow.offset.x - shadow.blur_radius, + y: quad.bounds.y + shadow.offset.y - shadow.blur_radius, + width: quad.bounds.width + shadow.blur_radius * 2.0, + height: quad.bounds.height + shadow.blur_radius * 2.0, + } * transformation; + + let radii = fill_border_radius + .into_iter() + .map(|radius| radius * transformation.scale_factor()) + .collect::>(); + let (x, y, width, height) = ( + shadow_bounds.x as u32, + shadow_bounds.y as u32, + shadow_bounds.width as u32, + shadow_bounds.height as u32, + ); + let half_width = physical_bounds.width / 2.0; + let half_height = physical_bounds.height / 2.0; + + let colors = (y..y + height) + .flat_map(|y| (x..x + width).map(move |x| (x as f32, y as f32))) + .filter_map(|(x, y)| { + tiny_skia::Size::from_wh(half_width, half_height).map( + |size| { + let shadow_distance = rounded_box_sdf( + Vector::new( + x - physical_bounds.position().x + - (shadow.offset.x + * transformation.scale_factor()) + - half_width, + y - physical_bounds.position().y + - (shadow.offset.y + * transformation.scale_factor()) + - half_height, + ), + size, + &radii, + ) + .max(0.0); + let shadow_alpha = 1.0 + - smoothstep( + -shadow.blur_radius + * transformation.scale_factor(), + shadow.blur_radius + * transformation.scale_factor(), + shadow_distance, + ); + + let mut color = into_color(shadow.color); + color.apply_opacity(shadow_alpha); + + color.to_color_u8().premultiply() + }, + ) + }) + .collect(); + + if let Some(pixmap) = tiny_skia::IntSize::from_wh(width, height) + .and_then(|size| { + tiny_skia::Pixmap::from_vec( + bytemuck::cast_vec(colors), + size, + ) + }) + { + pixels.draw_pixmap( + x as i32, + y as i32, + pixmap.as_ref(), + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::default(), + None, + ); + } + } + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: match background { + Background::Color(color) => { + tiny_skia::Shader::SolidColor(into_color(*color)) + } + Background::Gradient(Gradient::Linear(linear)) => { + let (start, end) = + linear.angle.to_distance(&quad.bounds); + + let stops: Vec = linear + .stops + .into_iter() + .flatten() + .map(|stop| { + tiny_skia::GradientStop::new( + stop.offset, + tiny_skia::Color::from_rgba( + stop.color.b, + stop.color.g, + stop.color.r, + stop.color.a, + ) + .expect("Create color"), + ) + }) + .collect(); + + tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: start.x, + y: start.y, + }, + tiny_skia::Point { x: end.x, y: end.y }, + if stops.is_empty() { + vec![tiny_skia::GradientStop::new( + 0.0, + tiny_skia::Color::BLACK, + )] + } else { + stops + }, + tiny_skia::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient") + } + }, + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::EvenOdd, + transform, + clip_mask, + ); + + if border_width > 0.0 { + // Border path is offset by half the border width + let border_bounds = Rectangle { + x: quad.bounds.x + border_width / 2.0, + y: quad.bounds.y + border_width / 2.0, + width: quad.bounds.width - border_width, + height: quad.bounds.height - border_width, + }; + + // Make sure the border radius is correct + let mut border_radius = <[f32; 4]>::from(quad.border.radius); + let mut is_simple_border = true; + + for radius in &mut border_radius { + *radius = if *radius == 0.0 { + // Path should handle this fine + 0.0 + } else if *radius > border_width / 2.0 { + *radius - border_width / 2.0 + } else { + is_simple_border = false; + 0.0 + } + .min(border_bounds.width / 2.0) + .min(border_bounds.height / 2.0); + } + + // Stroking a path works well in this case + if is_simple_border { + let border_path = + rounded_rectangle(border_bounds, border_radius); + + pixels.stroke_path( + &border_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + quad.border.color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + clip_mask, + ); + } else { + // Draw corners that have too small border radii as having no border radius, + // but mask them with the rounded rectangle with the correct border radius. + let mut temp_pixmap = tiny_skia::Pixmap::new( + quad.bounds.width as u32, + quad.bounds.height as u32, + ) + .unwrap(); + + let mut quad_mask = tiny_skia::Mask::new( + quad.bounds.width as u32, + quad.bounds.height as u32, + ) + .unwrap(); + + let zero_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: quad.bounds.width, + height: quad.bounds.height, + }; + let path = rounded_rectangle(zero_bounds, fill_border_radius); + + quad_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + true, + transform, + ); + let path_bounds = Rectangle { + x: border_width / 2.0, + y: border_width / 2.0, + width: quad.bounds.width - border_width, + height: quad.bounds.height - border_width, + }; + + let border_radius_path = + rounded_rectangle(path_bounds, border_radius); + + temp_pixmap.stroke_path( + &border_radius_path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(into_color( + quad.border.color, + )), + anti_alias: true, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: border_width, + ..tiny_skia::Stroke::default() + }, + transform, + Some(&quad_mask), + ); + + pixels.draw_pixmap( + quad.bounds.x as i32, + quad.bounds.y as i32, + temp_pixmap.as_ref(), + &tiny_skia::PixmapPaint::default(), + transform, + clip_mask, + ); + } + } + } + + pub fn draw_text( + &mut self, + text: &Text, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + match text { + Text::Paragraph { + paragraph, + position, + color, + clip_bounds: _, // TODO + transformation: local_transformation, + } => { + let transformation = transformation * *local_transformation; + + let physical_bounds = + Rectangle::new(*position, paragraph.min_bounds) + * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_paragraph( + paragraph, + *position, + *color, + pixels, + clip_mask, + transformation, + ); + } + Text::Editor { + editor, + position, + color, + clip_bounds: _, // TODO + transformation: local_transformation, + } => { + let transformation = transformation * *local_transformation; + + let physical_bounds = + Rectangle::new(*position, editor.bounds) * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_editor( + editor, + *position, + *color, + pixels, + clip_mask, + transformation, + ); + } + Text::Cached { + content, + bounds, + color, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping, + clip_bounds: _, // TODO + } => { + let physical_bounds = *bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_cached( + content, + *bounds, + *color, + *size, + *line_height, + *font, + *horizontal_alignment, + *vertical_alignment, + *shaping, + pixels, + clip_mask, + transformation, + ); + } + Text::Raw { + raw, + transformation: local_transformation, + } => { + let Some(buffer) = raw.buffer.upgrade() else { + return; + }; + + let transformation = transformation * *local_transformation; + let (width, height) = buffer.size(); + + let physical_bounds = + Rectangle::new(raw.position, Size::new(width, height)) + * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_raw( + &buffer, + raw.position, + raw.color, + pixels, + clip_mask, + transformation, + ); + } + } + } + + pub fn draw_primitive( + &mut self, + primitive: &Primitive, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + layer_bounds: Rectangle, + ) { + match primitive { + Primitive::Fill { path, paint, rule } => { + let physical_bounds = { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } * transformation + }; + + let Some(clip_bounds) = + layer_bounds.intersection(&physical_bounds) + else { + return; + }; + + let clip_mask = + (physical_bounds != clip_bounds).then_some(clip_mask as &_); + + pixels.fill_path( + path, + paint, + *rule, + into_transform(transformation), + clip_mask, + ); + } + Primitive::Stroke { + path, + paint, + stroke, + } => { + let physical_bounds = { + let bounds = path.bounds(); + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } * transformation + }; + + let Some(clip_bounds) = + layer_bounds.intersection(&physical_bounds) + else { + return; + }; + + let clip_mask = + (physical_bounds != clip_bounds).then_some(clip_mask as &_); + + pixels.stroke_path( + path, + paint, + stroke, + into_transform(transformation), + clip_mask, + ); + } + } + } + + pub fn draw_image( + &mut self, + image: &Image, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, + ) { + match image { + #[cfg(feature = "image")] + Image::Raster { + handle, + filter_method, + bounds, + } => { + let physical_bounds = *bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.raster_pipeline.draw( + handle, + *filter_method, + *bounds, + pixels, + into_transform(transformation), + clip_mask, + ); + } + #[cfg(feature = "svg")] + Image::Vector { + handle, + color, + bounds, + } => { + let physical_bounds = *bounds * transformation; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.vector_pipeline.draw( + handle, + *color, + physical_bounds, + pixels, + clip_mask, + ); + } + #[cfg(not(feature = "image"))] + Image::Raster { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {image:?}", + ); + } + #[cfg(not(feature = "svg"))] + Image::Vector { .. } => { + log::warn!( + "Unsupported primitive in `iced_tiny_skia`: {image:?}", + ); + } + } + } + + pub fn trim(&mut self) { + self.text_pipeline.trim_cache(); + + #[cfg(feature = "image")] + self.raster_pipeline.trim_cache(); + + #[cfg(feature = "svg")] + self.vector_pipeline.trim_cache(); + } +} + +pub fn into_color(color: Color) -> tiny_skia::Color { + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Convert color from iced to tiny_skia") +} + +fn into_transform(transformation: Transformation) -> tiny_skia::Transform { + let translation = transformation.translation(); + + tiny_skia::Transform { + sx: transformation.scale_factor(), + kx: 0.0, + ky: 0.0, + sy: transformation.scale_factor(), + tx: translation.x, + ty: translation.y, + } +} + +fn rounded_rectangle( + bounds: Rectangle, + border_radius: [f32; 4], +) -> tiny_skia::Path { + let [top_left, top_right, bottom_right, bottom_left] = border_radius; + + if top_left == 0.0 + && top_right == 0.0 + && bottom_right == 0.0 + && bottom_left == 0.0 + { + return tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Build quad rectangle"), + ); + } + + if top_left == top_right + && top_left == bottom_right + && top_left == bottom_left + && top_left == bounds.width / 2.0 + && top_left == bounds.height / 2.0 + { + return tiny_skia::PathBuilder::from_circle( + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, + top_left, + ) + .expect("Build circle path"); + } + + let mut builder = tiny_skia::PathBuilder::new(); + + builder.move_to(bounds.x + top_left, bounds.y); + builder.line_to(bounds.x + bounds.width - top_right, bounds.y); + + if top_right > 0.0 { + arc_to( + &mut builder, + bounds.x + bounds.width - top_right, + bounds.y, + bounds.x + bounds.width, + bounds.y + top_right, + top_right, + ); + } + + maybe_line_to( + &mut builder, + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + ); + + if bottom_right > 0.0 { + arc_to( + &mut builder, + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + bounds.x + bounds.width - bottom_right, + bounds.y + bounds.height, + bottom_right, + ); + } + + maybe_line_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + ); + + if bottom_left > 0.0 { + arc_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + bounds.x, + bounds.y + bounds.height - bottom_left, + bottom_left, + ); + } + + maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); + + if top_left > 0.0 { + arc_to( + &mut builder, + bounds.x, + bounds.y + top_left, + bounds.x + top_left, + bounds.y, + top_left, + ); + } + + builder.finish().expect("Build rounded rectangle path") +} + +fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) { + if path.last_point() != Some(tiny_skia::Point { x, y }) { + path.line_to(x, y); + } +} + +fn arc_to( + path: &mut tiny_skia::PathBuilder, + x_from: f32, + y_from: f32, + x_to: f32, + y_to: f32, + radius: f32, +) { + let svg_arc = kurbo::SvgArc { + from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)), + to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)), + radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)), + x_rotation: 0.0, + large_arc: false, + sweep: true, + }; + + match kurbo::Arc::from_svg_arc(&svg_arc) { + Some(arc) => { + arc.to_cubic_beziers(0.1, |p1, p2, p| { + path.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p.x as f32, + p.y as f32, + ); + }); + } + None => { + path.line_to(x_to, y_to); + } + } +} + +fn smoothstep(a: f32, b: f32, x: f32) -> f32 { + let x = ((x - a) / (b - a)).clamp(0.0, 1.0); + + x * x * (3.0 - 2.0 * x) +} + +fn rounded_box_sdf( + to_center: Vector, + size: tiny_skia::Size, + radii: &[f32], +) -> f32 { + let radius = match (to_center.x > 0.0, to_center.y > 0.0) { + (true, true) => radii[2], + (true, false) => radii[1], + (false, true) => radii[3], + (false, false) => radii[0], + }; + + let x = (to_center.x.abs() - size.width() + radius).max(0.0); + let y = (to_center.y.abs() - size.height() + radius).max(0.0); + + (x.powf(2.0) + y.powf(2.0)).sqrt() - radius +} + +pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) { + clip_mask.clear(); + + let path = { + let mut builder = tiny_skia::PathBuilder::new(); + builder.push_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .unwrap(), + ); + + builder.finish().unwrap() + }; + + clip_mask.fill_path( + &path, + tiny_skia::FillRule::EvenOdd, + false, + tiny_skia::Transform::default(), + ); +} diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index a9dcb610..04aabf0d 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,58 +1,98 @@ use crate::core::text::LineHeight; -use crate::core::{ - Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, -}; +use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; -use crate::graphics::geometry::{self, Path, Style, Text}; -use crate::graphics::Gradient; -use crate::primitive::{self, Primitive}; +use crate::graphics::geometry::{self, Path, Style}; +use crate::graphics::{Cached, Gradient, Text}; +use crate::Primitive; + +use std::rc::Rc; + +#[derive(Debug)] +pub enum Geometry { + Live { + text: Vec, + primitives: Vec, + clip_bounds: Rectangle, + }, + Cache(Cache), +} + +#[derive(Debug, Clone)] +pub struct Cache { + pub text: Rc<[Text]>, + pub primitives: Rc<[Primitive]>, + pub clip_bounds: Rectangle, +} + +impl Cached for Geometry { + type Cache = Cache; + + fn load(cache: &Cache) -> Self { + Self::Cache(cache.clone()) + } + + fn cache(self, _previous: Option) -> Cache { + match self { + Self::Live { + primitives, + text, + clip_bounds, + } => Cache { + primitives: Rc::from(primitives), + text: Rc::from(text), + clip_bounds, + }, + Self::Cache(cache) => cache, + } + } +} #[derive(Debug)] pub struct Frame { - size: Size, + clip_bounds: Rectangle, transform: tiny_skia::Transform, stack: Vec, primitives: Vec, + text: Vec, } impl Frame { pub fn new(size: Size) -> Self { + Self::with_clip(Rectangle::with_size(size)) + } + + pub fn with_clip(clip_bounds: Rectangle) -> Self { Self { - size, - transform: tiny_skia::Transform::identity(), + clip_bounds, stack: Vec::new(), primitives: Vec::new(), - } - } - - pub fn into_primitive(self) -> Primitive { - Primitive::Clip { - bounds: Rectangle::new(Point::ORIGIN, self.size), - content: Box::new(Primitive::Group { - primitives: self.primitives, - }), + text: Vec::new(), + transform: tiny_skia::Transform::from_translate( + clip_bounds.x, + clip_bounds.y, + ), } } } impl geometry::frame::Backend for Frame { - type Geometry = Primitive; + type Geometry = Geometry; fn width(&self) -> f32 { - self.size.width + self.clip_bounds.width } fn height(&self) -> f32 { - self.size.height + self.clip_bounds.height } fn size(&self) -> Size { - self.size + self.clip_bounds.size() } fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) } fn fill(&mut self, path: &Path, fill: impl Into) { @@ -67,12 +107,11 @@ impl geometry::frame::Backend for Frame { let mut paint = into_paint(fill.style); paint.shader.transform(self.transform); - self.primitives - .push(Primitive::Custom(primitive::Custom::Fill { - path, - paint, - rule: into_fill_rule(fill.rule), - })); + self.primitives.push(Primitive::Fill { + path, + paint, + rule: into_fill_rule(fill.rule), + }); } fn fill_rectangle( @@ -95,12 +134,11 @@ impl geometry::frame::Backend for Frame { }; paint.shader.transform(self.transform); - self.primitives - .push(Primitive::Custom(primitive::Custom::Fill { - path, - paint, - rule: into_fill_rule(fill.rule), - })); + self.primitives.push(Primitive::Fill { + path, + paint, + rule: into_fill_rule(fill.rule), + }); } fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { @@ -116,15 +154,14 @@ impl geometry::frame::Backend for Frame { let mut paint = into_paint(stroke.style); paint.shader.transform(self.transform); - self.primitives - .push(Primitive::Custom(primitive::Custom::Stroke { - path, - paint, - stroke: skia_stroke, - })); + self.primitives.push(Primitive::Stroke { + path, + paint, + stroke: skia_stroke, + }); } - fn fill_text(&mut self, text: impl Into) { + fn fill_text(&mut self, text: impl Into) { let text = text.into(); let (scale_x, scale_y) = self.transform.get_scale(); @@ -171,12 +208,12 @@ impl geometry::frame::Backend for Frame { }; // TODO: Honor layering! - self.primitives.push(Primitive::Text { + self.text.push(Text::Cached { content: text.content, bounds, color: text.color, size, - line_height, + line_height: line_height.to_absolute(size), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, @@ -197,14 +234,12 @@ impl geometry::frame::Backend for Frame { } fn draft(&mut self, clip_bounds: Rectangle) -> Self { - Self::new(clip_bounds.size()) + Self::with_clip(clip_bounds) } - fn paste(&mut self, frame: Self, at: Point) { - self.primitives.push(Primitive::Transform { - transformation: Transformation::translate(at.x, at.y), - content: Box::new(frame.into_primitive()), - }); + fn paste(&mut self, frame: Self, _at: Point) { + self.primitives.extend(frame.primitives); + self.text.extend(frame.text); } fn translate(&mut self, translation: Vector) { @@ -230,8 +265,12 @@ impl geometry::frame::Backend for Frame { self.transform = self.transform.pre_scale(scale.x, scale.y); } - fn into_geometry(self) -> Self::Geometry { - self.into_primitive() + fn into_geometry(self) -> Geometry { + Geometry::Live { + primitives: self.primitives, + text: self.text, + clip_bounds: self.clip_bounds, + } } } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs new file mode 100644 index 00000000..1f5c8b46 --- /dev/null +++ b/tiny_skia/src/layer.rs @@ -0,0 +1,243 @@ +use crate::core::image; +use crate::core::renderer::Quad; +use crate::core::svg; +use crate::core::{Background, Color, Point, Rectangle, Transformation}; +use crate::graphics::layer; +use crate::graphics::text::{Editor, Paragraph, Text}; +use crate::graphics::{self, Image}; +use crate::Primitive; + +use std::rc::Rc; + +pub type Stack = layer::Stack; + +#[derive(Debug, Clone)] +pub struct Layer { + pub bounds: Rectangle, + pub quads: Vec<(Quad, Background)>, + pub primitives: Vec>, + pub text: Vec>, + pub images: Vec, +} + +#[derive(Debug, Clone)] +pub enum Item { + Live(T), + Group(Vec, Rectangle, Transformation), + Cached(Rc<[T]>, Rectangle, Transformation), +} + +impl Item { + pub fn transformation(&self) -> Transformation { + match self { + Item::Live(_) => Transformation::IDENTITY, + Item::Group(_, _, transformation) + | Item::Cached(_, _, transformation) => *transformation, + } + } + + pub fn clip_bounds(&self) -> Rectangle { + match self { + Item::Live(_) => Rectangle::INFINITE, + Item::Group(_, clip_bounds, _) + | Item::Cached(_, clip_bounds, _) => *clip_bounds, + } + } + + pub fn as_slice(&self) -> &[T] { + match self { + Item::Live(item) => std::slice::from_ref(item), + Item::Group(group, _, _) => group.as_slice(), + Item::Cached(cache, _, _) => cache, + } + } +} + +impl Layer { + pub fn draw_quad( + &mut self, + mut quad: Quad, + background: Background, + transformation: Transformation, + ) { + quad.bounds = quad.bounds * transformation; + self.quads.push((quad, background)); + } + + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.text.push(Item::Live(paragraph)); + } + + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let editor = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.text.push(Item::Live(editor)); + } + + pub fn draw_text( + &mut self, + text: crate::core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let text = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.text.push(Item::Live(text)); + } + + pub fn draw_text_group( + &mut self, + text: Vec, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.text + .push(Item::Group(text, clip_bounds, transformation)); + } + + pub fn draw_text_cache( + &mut self, + text: Rc<[Text]>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.text + .push(Item::Cached(text, clip_bounds, transformation)); + } + + pub fn draw_image( + &mut self, + handle: image::Handle, + filter_method: image::FilterMethod, + bounds: Rectangle, + transformation: Transformation, + ) { + let image = Image::Raster { + handle, + filter_method, + bounds: bounds * transformation, + }; + + self.images.push(image); + } + + pub fn draw_svg( + &mut self, + handle: svg::Handle, + color: Option, + bounds: Rectangle, + transformation: Transformation, + ) { + let svg = Image::Vector { + handle, + color, + bounds: bounds * transformation, + }; + + self.images.push(svg); + } + + pub fn draw_primitive_group( + &mut self, + primitives: Vec, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.primitives.push(Item::Group( + primitives, + clip_bounds, + transformation, + )); + } + + pub fn draw_primitive_cache( + &mut self, + primitives: Rc<[Primitive]>, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + self.primitives.push(Item::Cached( + primitives, + clip_bounds, + transformation, + )); + } +} + +impl Default for Layer { + fn default() -> Self { + Self { + bounds: Rectangle::INFINITE, + quads: Vec::new(), + primitives: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } +} + +impl graphics::Layer for Layer { + fn with_bounds(bounds: Rectangle) -> Self { + Self { + bounds, + ..Self::default() + } + } + + fn flush(&mut self) {} + + fn resize(&mut self, bounds: graphics::core::Rectangle) { + self.bounds = bounds; + } + + fn reset(&mut self) { + self.bounds = Rectangle::INFINITE; + + self.quads.clear(); + self.text.clear(); + self.primitives.clear(); + self.images.clear(); + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index d1f68daa..fed2f32c 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -2,7 +2,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod window; -mod backend; +mod engine; +mod layer; mod primitive; mod settings; mod text; @@ -19,12 +20,388 @@ pub mod geometry; pub use iced_graphics as graphics; pub use iced_graphics::core; -pub use backend::Backend; +pub use layer::Layer; pub use primitive::Primitive; pub use settings::Settings; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::renderer; +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, +}; +use crate::engine::Engine; +use crate::graphics::compositor; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Viewport; + /// A [`tiny-skia`] graphics renderer for [`iced`]. /// /// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = iced_graphics::Renderer; +#[derive(Debug)] +pub struct Renderer { + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + engine: Engine, // TODO: Shared engine +} + +impl Renderer { + pub fn new(default_font: Font, default_text_size: Pixels) -> Self { + Self { + default_font, + default_text_size, + layers: layer::Stack::new(), + engine: Engine::new(), + } + } + + pub fn layers(&mut self) -> impl Iterator { + self.layers.flush(); + self.layers.iter() + } + + pub fn draw>( + &mut self, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + viewport: &Viewport, + damage: &[Rectangle], + background_color: Color, + overlay: &[T], + ) { + let physical_size = viewport.physical_size(); + let scale_factor = viewport.scale_factor() as f32; + + if !overlay.is_empty() { + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + 0.0, + 0.0, + physical_size.width as f32, + physical_size.height as f32, + ) + .expect("Create damage rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(engine::into_color( + Color { + a: 0.1, + ..background_color + }, + )), + anti_alias: false, + ..Default::default() + }, + tiny_skia::FillRule::default(), + tiny_skia::Transform::identity(), + None, + ); + } + + self.layers.flush(); + + for ®ion in damage { + let region = region * scale_factor; + + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + region.x, + region.y, + region.width, + region.height, + ) + .expect("Create damage rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor(engine::into_color( + background_color, + )), + anti_alias: false, + blend_mode: tiny_skia::BlendMode::Source, + ..Default::default() + }, + tiny_skia::FillRule::default(), + tiny_skia::Transform::identity(), + None, + ); + + for layer in self.layers.iter() { + let Some(clip_bounds) = region.intersection(&layer.bounds) + else { + continue; + }; + + let clip_bounds = clip_bounds * scale_factor; + engine::adjust_clip_mask(clip_mask, clip_bounds); + + for (quad, background) in &layer.quads { + self.engine.draw_quad( + quad, + background, + Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + + for group in &layer.text { + for text in group.as_slice() { + self.engine.draw_text( + text, + group.transformation() + * Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + } + + for group in &layer.primitives { + let Some(new_clip_bounds) = + group.clip_bounds().intersection(&layer.bounds) + else { + continue; + }; + + engine::adjust_clip_mask( + clip_mask, + new_clip_bounds * scale_factor, + ); + + for primitive in group.as_slice() { + self.engine.draw_primitive( + primitive, + group.transformation() + * Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + + engine::adjust_clip_mask(clip_mask, clip_bounds); + } + + for image in &layer.images { + self.engine.draw_image( + image, + Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + } + + if !overlay.is_empty() { + pixels.stroke_path( + &path, + &tiny_skia::Paint { + shader: tiny_skia::Shader::SolidColor( + engine::into_color(Color::from_rgb(1.0, 0.0, 0.0)), + ), + anti_alias: false, + ..tiny_skia::Paint::default() + }, + &tiny_skia::Stroke { + width: 1.0, + ..tiny_skia::Stroke::default() + }, + tiny_skia::Transform::identity(), + None, + ); + } + } + + self.engine.trim(); + } +} + +impl core::Renderer for Renderer { + fn start_layer(&mut self, bounds: Rectangle) { + self.layers.push_clip(bounds); + } + + fn end_layer(&mut self) { + self.layers.pop_clip(); + } + + fn start_transformation(&mut self, transformation: Transformation) { + self.layers.push_transformation(transformation); + } + + fn end_transformation(&mut self) { + self.layers.pop_transformation(); + } + + fn fill_quad( + &mut self, + quad: renderer::Quad, + background: impl Into, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_quad(quad, background.into(), transformation); + } + + fn clear(&mut self) { + self.layers.clear(); + } +} + +impl core::text::Renderer for Renderer { + type Font = Font; + type Paragraph = Paragraph; + type Editor = Editor; + + const ICON_FONT: Font = Font::with_name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Self::Font { + self.default_font + } + + fn default_size(&self) -> Pixels { + self.default_text_size + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + + layer.draw_paragraph( + text, + position, + color, + clip_bounds, + transformation, + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_editor(editor, position, color, clip_bounds, transformation); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text(text, position, color, clip_bounds, transformation); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, size: core::Size) -> Self::Frame { + geometry::Frame::new(size) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + let (layer, transformation) = self.layers.current_mut(); + + match geometry { + Geometry::Live { + primitives, + text, + clip_bounds, + } => { + layer.draw_primitive_group( + primitives, + clip_bounds, + transformation, + ); + + layer.draw_text_group(text, clip_bounds, transformation); + } + Geometry::Cache(cache) => { + layer.draw_primitive_cache( + cache.primitives, + cache.clip_bounds, + transformation, + ); + + layer.draw_text_cache( + cache.text, + cache.clip_bounds, + transformation, + ); + } + } + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, _mesh: graphics::Mesh) { + log::warn!("iced_tiny_skia does not support drawing meshes"); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> Size { + self.engine.raster_pipeline.dimensions(handle) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: core::image::FilterMethod, + bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_image(handle, filter_method, bounds, transformation); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg(&self, handle: &core::svg::Handle) -> Size { + self.engine.vector_pipeline.viewport_dimensions(handle) + } + + fn draw_svg( + &mut self, + handle: core::svg::Handle, + color: Option, + bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_svg(handle, color, bounds, transformation); + } +} + +impl compositor::Default for Renderer { + type Compositor = window::Compositor; +} diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs index b7c428e4..5305788d 100644 --- a/tiny_skia/src/primitive.rs +++ b/tiny_skia/src/primitive.rs @@ -1,10 +1,5 @@ -use crate::core::Rectangle; -use crate::graphics::{Damage, Mesh}; - -pub type Primitive = crate::graphics::Primitive; - #[derive(Debug, Clone, PartialEq)] -pub enum Custom { +pub enum Primitive { /// A path filled with some paint. Fill { /// The path to fill. @@ -24,29 +19,3 @@ pub enum Custom { stroke: tiny_skia::Stroke, }, } - -impl Damage for Custom { - fn bounds(&self) -> Rectangle { - match self { - Self::Fill { path, .. } | Self::Stroke { path, .. } => { - let bounds = path.bounds(); - - Rectangle { - x: bounds.x(), - y: bounds.y(), - width: bounds.width(), - height: bounds.height(), - } - .expand(1.0) - } - } - } -} - -impl TryFrom for Custom { - type Error = &'static str; - - fn try_from(_mesh: Mesh) -> Result { - Err("unsupported") - } -} diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs index 01d015b4..672c49f3 100644 --- a/tiny_skia/src/settings.rs +++ b/tiny_skia/src/settings.rs @@ -1,9 +1,9 @@ use crate::core::{Font, Pixels}; use crate::graphics; -/// The settings of a [`Backend`]. +/// The settings of a [`Compositor`]. /// -/// [`Backend`]: crate::Backend +/// [`Compositor`]: crate::window::Compositor #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { /// The default [`Font`] to use. diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 66ee88da..c71deb10 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,5 +1,5 @@ use crate::core::alignment; -use crate::core::text::{LineHeight, Shaping}; +use crate::core::text::Shaping; use crate::core::{ Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; @@ -27,6 +27,8 @@ impl Pipeline { } } + // TODO: Shared engine + #[allow(dead_code)] pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { font_system() .write() @@ -41,7 +43,6 @@ impl Pipeline { paragraph: ¶graph::Weak, position: Point, color: Color, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, @@ -62,7 +63,6 @@ impl Pipeline { color, paragraph.horizontal_alignment(), paragraph.vertical_alignment(), - scale_factor, pixels, clip_mask, transformation, @@ -74,7 +74,6 @@ impl Pipeline { editor: &editor::Weak, position: Point, color: Color, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, @@ -95,7 +94,6 @@ impl Pipeline { color, alignment::Horizontal::Left, alignment::Vertical::Top, - scale_factor, pixels, clip_mask, transformation, @@ -108,17 +106,16 @@ impl Pipeline { bounds: Rectangle, color: Color, size: Pixels, - line_height: LineHeight, + line_height: Pixels, font: Font, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, shaping: Shaping, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { - let line_height = f32::from(line_height.to_absolute(size)); + let line_height = f32::from(line_height); let mut font_system = font_system().write().expect("Write font system"); let font_system = font_system.raw(); @@ -149,7 +146,6 @@ impl Pipeline { color, horizontal_alignment, vertical_alignment, - scale_factor, pixels, clip_mask, transformation, @@ -161,7 +157,6 @@ impl Pipeline { buffer: &cosmic_text::Buffer, position: Point, color: Color, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, @@ -178,7 +173,6 @@ impl Pipeline { color, alignment::Horizontal::Left, alignment::Vertical::Top, - scale_factor, pixels, clip_mask, transformation, @@ -199,12 +193,11 @@ fn draw( color: Color, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - scale_factor: f32, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::Mask>, transformation: Transformation, ) { - let bounds = bounds * transformation * scale_factor; + let bounds = bounds * transformation; let x = match horizontal_alignment { alignment::Horizontal::Left => bounds.x, @@ -222,8 +215,8 @@ fn draw( for run in buffer.layout_runs() { for glyph in run.glyphs { - let physical_glyph = glyph - .physical((x, y), scale_factor * transformation.scale_factor()); + let physical_glyph = + glyph.physical((x, y), transformation.scale_factor()); if let Some((buffer, placement)) = glyph_cache.allocate( physical_glyph.cache_key, @@ -247,10 +240,8 @@ fn draw( pixels.draw_pixmap( physical_glyph.x + placement.left, physical_glyph.y - placement.top - + (run.line_y - * scale_factor - * transformation.scale_factor()) - .round() as i32, + + (run.line_y * transformation.scale_factor()).round() + as i32, pixmap, &tiny_skia::PixmapPaint { opacity, diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 2350adb9..e1a9dfec 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,9 +1,8 @@ use crate::core::{Color, Rectangle, Size}; use crate::graphics::compositor::{self, Information}; -use crate::graphics::damage; use crate::graphics::error::{self, Error}; use crate::graphics::{self, Viewport}; -use crate::{Backend, Primitive, Renderer, Settings}; +use crate::{Layer, Renderer, Settings}; use std::collections::VecDeque; use std::num::NonZeroU32; @@ -21,7 +20,7 @@ pub struct Surface { Box, >, clip_mask: tiny_skia::Mask, - primitive_stack: VecDeque>, + layer_stack: VecDeque>, background_color: Color, max_age: u8, } @@ -50,7 +49,6 @@ impl crate::graphics::Compositor for Compositor { fn create_renderer(&self) -> Self::Renderer { Renderer::new( - Backend::new(), self.settings.default_font, self.settings.default_text_size, ) @@ -72,7 +70,7 @@ impl crate::graphics::Compositor for Compositor { window, clip_mask: tiny_skia::Mask::new(width, height) .expect("Create clip mask"), - primitive_stack: VecDeque::new(), + layer_stack: VecDeque::new(), background_color: Color::BLACK, max_age: 0, }; @@ -98,7 +96,7 @@ impl crate::graphics::Compositor for Compositor { surface.clip_mask = tiny_skia::Mask::new(width, height).expect("Create clip mask"); - surface.primitive_stack.clear(); + surface.layer_stack.clear(); } fn fetch_information(&self) -> Information { @@ -116,16 +114,7 @@ impl crate::graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { - renderer.with_primitives(|backend, primitives| { - present( - backend, - surface, - primitives, - viewport, - background_color, - overlay, - ) - }) + present(renderer, surface, viewport, background_color, overlay) } fn screenshot>( @@ -136,16 +125,7 @@ impl crate::graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Vec { - renderer.with_primitives(|backend, primitives| { - screenshot( - surface, - backend, - primitives, - viewport, - background_color, - overlay, - ) - }) + screenshot(renderer, surface, viewport, background_color, overlay) } } @@ -161,49 +141,52 @@ pub fn new( } pub fn present>( - backend: &mut Backend, + renderer: &mut Renderer, surface: &mut Surface, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { let physical_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; let mut buffer = surface .window .buffer_mut() .map_err(|_| compositor::SurfaceError::Lost)?; - let last_primitives = { + let _last_layers = { let age = buffer.age(); surface.max_age = surface.max_age.max(age); - surface.primitive_stack.truncate(surface.max_age as usize); + surface.layer_stack.truncate(surface.max_age as usize); if age > 0 { - surface.primitive_stack.get(age as usize - 1) + surface.layer_stack.get(age as usize - 1) } else { None } }; - let damage = last_primitives - .and_then(|last_primitives| { - (surface.background_color == background_color) - .then(|| damage::list(last_primitives, primitives)) - }) - .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); + // TODO + // let damage = last_layers + // .and_then(|last_layers| { + // (surface.background_color == background_color) + // .then(|| damage::layers(last_layers, renderer.layers())) + // }) + // .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); + + let damage = vec![Rectangle::with_size(viewport.logical_size())]; if damage.is_empty() { return Ok(()); } - surface.primitive_stack.push_front(primitives.to_vec()); + surface + .layer_stack + .push_front(renderer.layers().cloned().collect()); surface.background_color = background_color; - let damage = damage::group(damage, scale_factor, physical_size); + // let damage = damage::group(damage, viewport.logical_size()); let mut pixels = tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut buffer), @@ -212,10 +195,9 @@ pub fn present>( ) .expect("Create pixel map"); - backend.draw( + renderer.draw( &mut pixels, &mut surface.clip_mask, - primitives, viewport, &damage, background_color, @@ -226,9 +208,8 @@ pub fn present>( } pub fn screenshot>( + renderer: &mut Renderer, surface: &mut Surface, - backend: &mut Backend, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, overlay: &[T], @@ -238,7 +219,7 @@ pub fn screenshot>( let mut offscreen_buffer: Vec = vec![0; size.width as usize * size.height as usize]; - backend.draw( + renderer.draw( &mut tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut offscreen_buffer), size.width, @@ -246,7 +227,6 @@ pub fn screenshot>( ) .expect("Create offscreen pixel map"), &mut surface.clip_mask, - primitives, viewport, &[Rectangle::with_size(Size::new( size.width as f32, diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 985650e2..60967082 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -19,18 +19,6 @@ use lyon::tessellation; use std::borrow::Cow; -/// A frame for drawing some geometry. -#[allow(missing_debug_implementations)] -pub struct Frame { - clip_bounds: Rectangle, - buffers: BufferStack, - meshes: Vec, - text: Vec, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - #[derive(Debug)] pub enum Geometry { Live { meshes: Vec, text: Vec }, @@ -79,6 +67,18 @@ impl Cached for Geometry { } } +/// A frame for drawing some geometry. +#[allow(missing_debug_implementations)] +pub struct Frame { + clip_bounds: Rectangle, + buffers: BufferStack, + meshes: Vec, + text: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + impl Frame { /// Creates a new [`Frame`] with the given [`Size`]. pub fn new(size: Size) -> Frame { diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 7a18e322..9526c5a8 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,6 +1,8 @@ use crate::core::renderer; use crate::core::{Background, Color, Point, Rectangle, Transformation}; +use crate::graphics; use crate::graphics::color; +use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Mesh; use crate::image::{self, Image}; @@ -9,6 +11,8 @@ use crate::quad::{self, Quad}; use crate::text::{self, Text}; use crate::triangle; +pub type Stack = layer::Stack; + #[derive(Debug)] pub struct Layer { pub bounds: Rectangle, @@ -17,48 +21,18 @@ pub struct Layer { pub primitives: primitive::Batch, pub text: text::Batch, pub images: image::Batch, + pending_meshes: Vec, + pending_text: Vec, } -impl Default for Layer { - fn default() -> Self { - Self { - bounds: Rectangle::INFINITE, - quads: quad::Batch::default(), - triangles: triangle::Batch::default(), - primitives: primitive::Batch::default(), - text: text::Batch::default(), - images: image::Batch::default(), - } - } -} - -#[derive(Debug)] -pub struct Stack { - layers: Vec, - transformations: Vec, - previous: Vec, - pending_meshes: Vec>, - pending_text: Vec>, - current: usize, - active_count: usize, -} - -impl Stack { - pub fn new() -> Self { - Self { - layers: vec![Layer::default()], - transformations: vec![Transformation::IDENTITY], - previous: vec![], - pending_meshes: vec![Vec::new()], - pending_text: vec![Vec::new()], - current: 0, - active_count: 1, - } - } - - pub fn draw_quad(&mut self, quad: renderer::Quad, background: Background) { - let transformation = self.transformations.last().unwrap(); - let bounds = quad.bounds * *transformation; +impl Layer { + pub fn draw_quad( + &mut self, + quad: renderer::Quad, + background: Background, + transformation: Transformation, + ) { + let bounds = quad.bounds * transformation; let quad = Quad { position: [bounds.x, bounds.y], @@ -71,7 +45,7 @@ impl Stack { shadow_blur_radius: quad.shadow.blur_radius, }; - self.layers[self.current].quads.add(quad, &background); + self.quads.add(quad, &background); } pub fn draw_paragraph( @@ -80,16 +54,17 @@ impl Stack { position: Point, color: Color, clip_bounds: Rectangle, + transformation: Transformation, ) { let paragraph = Text::Paragraph { paragraph: paragraph.downgrade(), position, color, clip_bounds, - transformation: self.transformations.last().copied().unwrap(), + transformation, }; - self.pending_text[self.current].push(paragraph); + self.pending_text.push(paragraph); } pub fn draw_editor( @@ -98,16 +73,17 @@ impl Stack { position: Point, color: Color, clip_bounds: Rectangle, + transformation: Transformation, ) { let editor = Text::Editor { editor: editor.downgrade(), position, color, clip_bounds, - transformation: self.transformation(), + transformation, }; - self.pending_text[self.current].push(editor); + self.pending_text.push(editor); } pub fn draw_text( @@ -116,9 +92,8 @@ impl Stack { position: Point, color: Color, clip_bounds: Rectangle, + transformation: Transformation, ) { - let transformation = self.transformation(); - let text = Text::Cached { content: text.content, bounds: Rectangle::new(position, text.bounds) * transformation, @@ -133,7 +108,7 @@ impl Stack { clip_bounds: clip_bounds * transformation, }; - self.pending_text[self.current].push(text); + self.pending_text.push(text); } pub fn draw_image( @@ -141,14 +116,15 @@ impl Stack { handle: crate::core::image::Handle, filter_method: crate::core::image::FilterMethod, bounds: Rectangle, + transformation: Transformation, ) { let image = Image::Raster { handle, filter_method, - bounds: bounds * self.transformation(), + bounds: bounds * transformation, }; - self.layers[self.current].images.push(image); + self.images.push(image); } pub fn draw_svg( @@ -156,72 +132,87 @@ impl Stack { handle: crate::core::svg::Handle, color: Option, bounds: Rectangle, + transformation: Transformation, ) { let svg = Image::Vector { handle, color, - bounds: bounds * self.transformation(), + bounds: bounds * transformation, }; - self.layers[self.current].images.push(svg); + self.images.push(svg); } - pub fn draw_mesh(&mut self, mut mesh: Mesh) { + pub fn draw_mesh( + &mut self, + mut mesh: Mesh, + transformation: Transformation, + ) { match &mut mesh { - Mesh::Solid { transformation, .. } - | Mesh::Gradient { transformation, .. } => { - *transformation = *transformation * self.transformation(); + Mesh::Solid { + transformation: local_transformation, + .. + } + | Mesh::Gradient { + transformation: local_transformation, + .. + } => { + *local_transformation = *local_transformation * transformation; } } - self.pending_meshes[self.current].push(mesh); + self.pending_meshes.push(mesh); } - pub fn draw_mesh_group(&mut self, meshes: Vec) { - self.flush_pending(); - - let transformation = self.transformation(); + pub fn draw_mesh_group( + &mut self, + meshes: Vec, + transformation: Transformation, + ) { + self.flush_meshes(); - self.layers[self.current] - .triangles - .push(triangle::Item::Group { - transformation, - meshes, - }); + self.triangles.push(triangle::Item::Group { + meshes, + transformation, + }); } - pub fn draw_mesh_cache(&mut self, cache: triangle::Cache) { - self.flush_pending(); - - let transformation = self.transformation(); + pub fn draw_mesh_cache( + &mut self, + cache: triangle::Cache, + transformation: Transformation, + ) { + self.flush_meshes(); - self.layers[self.current] - .triangles - .push(triangle::Item::Cached { - transformation, - cache, - }); + self.triangles.push(triangle::Item::Cached { + cache, + transformation, + }); } - pub fn draw_text_group(&mut self, text: Vec) { - self.flush_pending(); - - let transformation = self.transformation(); + pub fn draw_text_group( + &mut self, + text: Vec, + transformation: Transformation, + ) { + self.flush_text(); - self.layers[self.current].text.push(text::Item::Group { - transformation, + self.text.push(text::Item::Group { text, + transformation, }); } - pub fn draw_text_cache(&mut self, cache: text::Cache) { - self.flush_pending(); - - let transformation = self.transformation(); + pub fn draw_text_cache( + &mut self, + cache: text::Cache, + transformation: Transformation, + ) { + self.flush_text(); - self.layers[self.current].text.push(text::Item::Cached { - transformation, + self.text.push(text::Item::Cached { cache, + transformation, }); } @@ -229,112 +220,74 @@ impl Stack { &mut self, bounds: Rectangle, primitive: Box, + transformation: Transformation, ) { - let bounds = bounds * self.transformation(); + let bounds = bounds * transformation; - self.layers[self.current] - .primitives + self.primitives .push(primitive::Instance { bounds, primitive }); } - pub fn push_clip(&mut self, bounds: Rectangle) { - self.previous.push(self.current); - - self.current = self.active_count; - self.active_count += 1; - - let bounds = bounds * self.transformation(); - - if self.current == self.layers.len() { - self.layers.push(Layer { - bounds, - ..Layer::default() + fn flush_meshes(&mut self) { + if !self.pending_meshes.is_empty() { + self.triangles.push(triangle::Item::Group { + transformation: Transformation::IDENTITY, + meshes: self.pending_meshes.drain(..).collect(), }); - self.pending_meshes.push(Vec::new()); - self.pending_text.push(Vec::new()); - } else { - self.layers[self.current].bounds = bounds; } } - pub fn pop_clip(&mut self) { - self.flush_pending(); - - self.current = self.previous.pop().unwrap(); - } - - pub fn push_transformation(&mut self, transformation: Transformation) { - self.flush_pending(); - - self.transformations - .push(self.transformation() * transformation); - } - - pub fn pop_transformation(&mut self) { - let _ = self.transformations.pop(); - } - - fn transformation(&self) -> Transformation { - self.transformations.last().copied().unwrap() + fn flush_text(&mut self) { + if !self.pending_text.is_empty() { + self.text.push(text::Item::Group { + transformation: Transformation::IDENTITY, + text: self.pending_text.drain(..).collect(), + }); + } } +} - pub fn iter_mut(&mut self) -> impl Iterator { - self.flush_pending(); - - self.layers[..self.active_count].iter_mut() +impl graphics::Layer for Layer { + fn with_bounds(bounds: Rectangle) -> Self { + Self { + bounds, + ..Self::default() + } } - pub fn iter(&self) -> impl Iterator { - self.layers[..self.active_count].iter() + fn flush(&mut self) { + self.flush_meshes(); + self.flush_text(); } - pub fn clear(&mut self) { - for (live, pending_meshes) in self.layers[..self.active_count] - .iter_mut() - .zip(self.pending_meshes.iter_mut()) - { - live.bounds = Rectangle::INFINITE; - - live.quads.clear(); - live.triangles.clear(); - live.primitives.clear(); - live.text.clear(); - live.images.clear(); - pending_meshes.clear(); - } - - self.current = 0; - self.active_count = 1; - self.previous.clear(); + fn resize(&mut self, bounds: Rectangle) { + self.bounds = bounds; } - // We want to keep the allocated memory - #[allow(clippy::drain_collect)] - fn flush_pending(&mut self) { - let transformation = self.transformation(); - - let pending_meshes = &mut self.pending_meshes[self.current]; - if !pending_meshes.is_empty() { - self.layers[self.current] - .triangles - .push(triangle::Item::Group { - transformation, - meshes: pending_meshes.drain(..).collect(), - }); - } + fn reset(&mut self) { + self.bounds = Rectangle::INFINITE; - let pending_text = &mut self.pending_text[self.current]; - if !pending_text.is_empty() { - self.layers[self.current].text.push(text::Item::Group { - transformation, - text: pending_text.drain(..).collect(), - }); - } + self.quads.clear(); + self.triangles.clear(); + self.primitives.clear(); + self.text.clear(); + self.images.clear(); + self.pending_meshes.clear(); + self.pending_text.clear(); } } -impl Default for Stack { +impl Default for Layer { fn default() -> Self { - Self::new() + Self { + bounds: Rectangle::INFINITE, + quads: quad::Batch::default(), + triangles: triangle::Batch::default(), + primitives: primitive::Batch::default(), + text: text::Batch::default(), + images: image::Batch::default(), + pending_meshes: Vec::new(), + pending_text: Vec::new(), + } } } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b1ae4e89..178522de 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -66,8 +66,6 @@ use crate::core::{ use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; -use std::borrow::Cow; - /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs @@ -422,7 +420,7 @@ impl core::Renderer for Renderer { self.layers.push_clip(bounds); } - fn end_layer(&mut self, _bounds: Rectangle) { + fn end_layer(&mut self) { self.layers.pop_clip(); } @@ -430,7 +428,7 @@ impl core::Renderer for Renderer { self.layers.push_transformation(transformation); } - fn end_transformation(&mut self, _transformation: Transformation) { + fn end_transformation(&mut self) { self.layers.pop_transformation(); } @@ -439,7 +437,8 @@ impl core::Renderer for Renderer { quad: core::renderer::Quad, background: impl Into, ) { - self.layers.draw_quad(quad, background.into()); + let (layer, transformation) = self.layers.current_mut(); + layer.draw_quad(quad, background.into(), transformation); } fn clear(&mut self) { @@ -464,15 +463,6 @@ impl core::text::Renderer for Renderer { self.default_text_size } - fn load_font(&mut self, font: Cow<'static, [u8]>) { - graphics::text::font_system() - .write() - .expect("Write font system") - .load_font(font); - - // TODO: Invalidate buffer cache - } - fn fill_paragraph( &mut self, text: &Self::Paragraph, @@ -480,8 +470,15 @@ impl core::text::Renderer for Renderer { color: Color, clip_bounds: Rectangle, ) { - self.layers - .draw_paragraph(text, position, color, clip_bounds); + let (layer, transformation) = self.layers.current_mut(); + + layer.draw_paragraph( + text, + position, + color, + clip_bounds, + transformation, + ); } fn fill_editor( @@ -491,8 +488,8 @@ impl core::text::Renderer for Renderer { color: Color, clip_bounds: Rectangle, ) { - self.layers - .draw_editor(editor, position, color, clip_bounds); + let (layer, transformation) = self.layers.current_mut(); + layer.draw_editor(editor, position, color, clip_bounds, transformation); } fn fill_text( @@ -502,7 +499,8 @@ impl core::text::Renderer for Renderer { color: Color, clip_bounds: Rectangle, ) { - self.layers.draw_text(text, position, color, clip_bounds); + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text(text, position, color, clip_bounds, transformation); } } @@ -520,7 +518,8 @@ impl core::image::Renderer for Renderer { filter_method: core::image::FilterMethod, bounds: Rectangle, ) { - self.layers.draw_image(handle, filter_method, bounds); + let (layer, transformation) = self.layers.current_mut(); + layer.draw_image(handle, filter_method, bounds, transformation); } } @@ -536,13 +535,15 @@ impl core::svg::Renderer for Renderer { color_filter: Option, bounds: Rectangle, ) { - self.layers.draw_svg(handle, color_filter, bounds); + let (layer, transformation) = self.layers.current_mut(); + layer.draw_svg(handle, color_filter, bounds, transformation); } } impl graphics::mesh::Renderer for Renderer { fn draw_mesh(&mut self, mesh: graphics::Mesh) { - self.layers.draw_mesh(mesh); + let (layer, transformation) = self.layers.current_mut(); + layer.draw_mesh(mesh, transformation); } } @@ -556,18 +557,20 @@ impl graphics::geometry::Renderer for Renderer { } fn draw_geometry(&mut self, geometry: Self::Geometry) { + let (layer, transformation) = self.layers.current_mut(); + match geometry { Geometry::Live { meshes, text } => { - self.layers.draw_mesh_group(meshes); - self.layers.draw_text_group(text); + layer.draw_mesh_group(meshes, transformation); + layer.draw_text_group(text, transformation); } Geometry::Cached(cache) => { if let Some(meshes) = cache.meshes { - self.layers.draw_mesh_cache(meshes); + layer.draw_mesh_cache(meshes, transformation); } if let Some(text) = cache.text { - self.layers.draw_text_cache(text); + layer.draw_text_cache(text, transformation); } } } @@ -576,7 +579,8 @@ impl graphics::geometry::Renderer for Renderer { impl primitive::Renderer for Renderer { fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) { - self.layers.draw_primitive(bounds, Box::new(primitive)); + let (layer, transformation) = self.layers.current_mut(); + layer.draw_primitive(bounds, Box::new(primitive), transformation); } } diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 4ba1ed9a..1313e752 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -43,7 +43,7 @@ pub struct Instance { } impl Instance { - /// Creates a new [`Pipeline`] with the given [`Primitive`]. + /// Creates a new [`Instance`] with the given [`Primitive`]. pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { Instance { bounds, @@ -80,7 +80,7 @@ impl Storage { self.pipelines.get(&TypeId::of::()).map(|pipeline| { pipeline .downcast_ref::() - .expect("Pipeline with this type does not exist in Storage.") + .expect("Value with this type does not exist in Storage.") }) } @@ -89,7 +89,7 @@ impl Storage { self.pipelines.get_mut(&TypeId::of::()).map(|pipeline| { pipeline .downcast_mut::() - .expect("Pipeline with this type does not exist in Storage.") + .expect("Value with this type does not exist in Storage.") }) } } diff --git a/winit/src/application.rs b/winit/src/application.rs index d68523fa..1ca80609 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -220,13 +220,11 @@ where }; } - let compositor = C::new(graphics_settings, window.clone()).await?; - let mut renderer = compositor.create_renderer(); + let mut compositor = C::new(graphics_settings, window.clone()).await?; + let renderer = compositor.create_renderer(); for font in settings.fonts { - use crate::core::text::Renderer; - - renderer.load_font(font); + compositor.load_font(font); } let (mut event_sender, event_receiver) = mpsc::unbounded(); @@ -950,10 +948,8 @@ pub fn run_command( *cache = current_cache; } command::Action::LoadFont { bytes, tagger } => { - use crate::core::text::Renderer; - // TODO: Error handling (?) - renderer.load_font(bytes); + compositor.load_font(bytes); proxy .send_event(tagger(Ok(()))) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index e17cc180..3537ac18 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1194,13 +1194,8 @@ fn run_command( uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); } command::Action::LoadFont { bytes, tagger } => { - use crate::core::text::Renderer; - - // TODO change this once we change each renderer to having a single backend reference.. :pain: // TODO: Error handling (?) - for (_, window) in window_manager.iter_mut() { - window.renderer.load_font(bytes.clone()); - } + compositor.load_font(bytes.clone()); proxy .send_event(tagger(Ok(()))) -- cgit From 14b9708f723f9fc730634e7ded3dec7dc912ce34 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 9 Apr 2024 22:29:03 +0200 Subject: Remove leftover `renderer` module in `iced_graphics` --- graphics/src/geometry.rs | 9 -- graphics/src/renderer.rs | 269 ----------------------------------------------- graphics/src/settings.rs | 2 +- 3 files changed, 1 insertion(+), 279 deletions(-) delete mode 100644 graphics/src/renderer.rs diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index d251efb8..dd07097f 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -36,15 +36,6 @@ pub trait Renderer: core::Renderer { fn draw_geometry(&mut self, geometry: Self::Geometry); } -/// The graphics backend of a geometry renderer. -pub trait Backend { - /// The kind of [`Frame`] this backend supports. - type Frame: frame::Backend; - - /// Creates a new [`Self::Frame`]. - fn new_frame(&self, size: Size) -> Self::Frame; -} - #[cfg(debug_assertions)] impl Renderer for () { type Geometry = (); diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs deleted file mode 100644 index 695759a4..00000000 --- a/graphics/src/renderer.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! Create a renderer from a [`Backend`]. -use crate::backend::{self, Backend}; -use crate::compositor; -use crate::core; -use crate::core::image; -use crate::core::renderer; -use crate::core::svg; -use crate::core::text::Text; -use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, -}; -use crate::mesh; -use crate::text; -use crate::{Mesh, Primitive}; - -use std::borrow::Cow; - -/// A backend-agnostic renderer that supports all the built-in widgets. -#[derive(Debug)] -pub struct Renderer { - backend: B, - default_font: Font, - default_text_size: Pixels, - primitives: Vec>, - stack: Vec>>, -} - -impl Renderer { - /// Creates a new [`Renderer`] from the given [`Backend`]. - pub fn new( - backend: B, - default_font: Font, - default_text_size: Pixels, - ) -> Self { - Self { - backend, - default_font, - default_text_size, - primitives: Vec::new(), - stack: Vec::new(), - } - } - - /// Returns a reference to the [`Backend`] of the [`Renderer`]. - pub fn backend(&self) -> &B { - &self.backend - } - - /// Enqueues the given [`Primitive`] in the [`Renderer`] for drawing. - pub fn draw_primitive(&mut self, primitive: Primitive) { - self.primitives.push(primitive); - } - - /// Runs the given closure with the [`Backend`] and the recorded primitives - /// of the [`Renderer`]. - pub fn with_primitives( - &mut self, - f: impl FnOnce(&mut B, &[Primitive]) -> O, - ) -> O { - f(&mut self.backend, &self.primitives) - } -} - -impl iced_core::Renderer for Renderer { - fn start_layer(&mut self, _bounds: Rectangle) { - self.stack.push(std::mem::take(&mut self.primitives)); - } - - fn end_layer(&mut self) { - let layer = std::mem::replace( - &mut self.primitives, - self.stack.pop().expect("a layer should be recording"), - ); - - self.primitives.push(Primitive::group(layer).clip(bounds)); - } - - fn start_transformation(&mut self, _transformation: Transformation) { - self.stack.push(std::mem::take(&mut self.primitives)); - } - - fn end_transformation(&mut self, transformation: Transformation) { - let layer = std::mem::replace( - &mut self.primitives, - self.stack.pop().expect("a layer should be recording"), - ); - - self.primitives - .push(Primitive::group(layer).transform(transformation)); - } - - fn fill_quad( - &mut self, - quad: renderer::Quad, - background: impl Into, - ) { - self.primitives.push(Primitive::Quad { - bounds: quad.bounds, - background: background.into(), - border: quad.border, - shadow: quad.shadow, - }); - } - - fn clear(&mut self) { - self.primitives.clear(); - } -} - -impl core::text::Renderer for Renderer -where - B: Backend + backend::Text, -{ - type Font = Font; - type Paragraph = text::Paragraph; - type Editor = text::Editor; - - const ICON_FONT: Font = Font::with_name("Iced-Icons"); - const CHECKMARK_ICON: char = '\u{f00c}'; - const ARROW_DOWN_ICON: char = '\u{e800}'; - - fn default_font(&self) -> Self::Font { - self.default_font - } - - fn default_size(&self) -> Pixels { - self.default_text_size - } - - fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.backend.load_font(bytes); - } - - fn fill_paragraph( - &mut self, - paragraph: &Self::Paragraph, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - self.primitives.push(Primitive::Paragraph { - paragraph: paragraph.downgrade(), - position, - color, - clip_bounds, - }); - } - - fn fill_editor( - &mut self, - editor: &Self::Editor, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - self.primitives.push(Primitive::Editor { - editor: editor.downgrade(), - position, - color, - clip_bounds, - }); - } - - fn fill_text( - &mut self, - text: Text, - position: Point, - color: Color, - clip_bounds: Rectangle, - ) { - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle::new(position, text.bounds), - size: text.size, - line_height: text.line_height, - color, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - clip_bounds, - }); - } -} - -impl image::Renderer for Renderer -where - B: Backend + backend::Image, -{ - type Handle = image::Handle; - - fn measure_image(&self, handle: &image::Handle) -> Size { - self.backend().dimensions(handle) - } - - fn draw_image( - &mut self, - handle: image::Handle, - filter_method: image::FilterMethod, - bounds: Rectangle, - ) { - self.primitives.push(Primitive::Image { - handle, - filter_method, - bounds, - }); - } -} - -impl svg::Renderer for Renderer -where - B: Backend + backend::Svg, -{ - fn measure_svg(&self, handle: &svg::Handle) -> Size { - self.backend().viewport_dimensions(handle) - } - - fn draw_svg( - &mut self, - handle: svg::Handle, - color: Option, - bounds: Rectangle, - ) { - self.primitives.push(Primitive::Svg { - handle, - color, - bounds, - }); - } -} - -impl mesh::Renderer for Renderer { - fn draw_mesh(&mut self, mesh: Mesh) { - match B::Primitive::try_from(mesh) { - Ok(primitive) => { - self.draw_primitive(Primitive::Custom(primitive)); - } - Err(error) => { - log::warn!("mesh primitive could not be drawn: {error:?}"); - } - } - } -} - -#[cfg(feature = "geometry")] -impl crate::geometry::Renderer for Renderer -where - B: Backend + crate::geometry::Backend, - B::Frame: - crate::geometry::frame::Backend>, -{ - type Frame = B::Frame; - type Geometry = Primitive; - - fn new_frame(&self, size: Size) -> Self::Frame { - self.backend.new_frame(size) - } - - fn draw_geometry(&mut self, geometry: Self::Geometry) { - self.draw_primitive(geometry); - } -} - -impl compositor::Default for Renderer -where - B: Backend, -{ - type Compositor = B::Compositor; -} diff --git a/graphics/src/settings.rs b/graphics/src/settings.rs index 68673536..2e8275c6 100644 --- a/graphics/src/settings.rs +++ b/graphics/src/settings.rs @@ -1,7 +1,7 @@ use crate::core::{Font, Pixels}; use crate::Antialiasing; -/// The settings of a Backend. +/// The settings of a renderer. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { /// The default [`Font`] to use. -- cgit From 1e802e776cb591f3860d1bfbaa1423d356fc8456 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 10 Apr 2024 15:21:42 +0200 Subject: Reintroduce damage tracking for `iced_tiny_skia` --- graphics/src/damage.rs | 80 +++++++++++++++++++++ graphics/src/layer.rs | 5 ++ graphics/src/lib.rs | 1 + graphics/src/text.rs | 70 ++++++++++++++++++ tiny_skia/src/engine.rs | 34 ++++----- tiny_skia/src/geometry.rs | 2 +- tiny_skia/src/layer.rs | 144 ++++++++++++++++++++++++++++--------- tiny_skia/src/lib.rs | 53 +++++++------- tiny_skia/src/primitive.rs | 19 +++++ tiny_skia/src/window/compositor.rs | 31 ++++---- 10 files changed, 347 insertions(+), 92 deletions(-) create mode 100644 graphics/src/damage.rs diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs new file mode 100644 index 00000000..ff8edaf5 --- /dev/null +++ b/graphics/src/damage.rs @@ -0,0 +1,80 @@ +//! Compute the damage between frames. +use crate::core::Rectangle; + +/// Diffs the damage regions given some previous and current primitives. +pub fn diff( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + diff: impl Fn(&T, &T) -> Vec, +) -> Vec { + let damage = previous.iter().zip(current).flat_map(|(a, b)| diff(a, b)); + + if previous.len() == current.len() { + damage.collect() + } else { + let (smaller, bigger) = if previous.len() < current.len() { + (previous, current) + } else { + (current, previous) + }; + + // Extend damage by the added/removed primitives + damage + .chain(bigger[smaller.len()..].iter().flat_map(bounds)) + .collect() + } +} + +/// Computes the damage regions given some previous and current primitives. +pub fn list( + previous: &[T], + current: &[T], + bounds: impl Fn(&T) -> Vec, + are_equal: impl Fn(&T, &T) -> bool, +) -> Vec { + diff(previous, current, &bounds, |a, b| { + if are_equal(a, b) { + vec![] + } else { + bounds(a).into_iter().chain(bounds(b)).collect() + } + }) +} + +/// Groups the given damage regions that are close together inside the given +/// bounds. +pub fn group(mut damage: Vec, bounds: Rectangle) -> Vec { + use std::cmp::Ordering; + + const AREA_THRESHOLD: f32 = 20_000.0; + + damage.sort_by(|a, b| { + a.x.partial_cmp(&b.x) + .unwrap_or(Ordering::Equal) + .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal)) + }); + + let mut output = Vec::new(); + let mut scaled = damage + .into_iter() + .filter_map(|region| region.intersection(&bounds)) + .filter(|region| region.width >= 1.0 && region.height >= 1.0); + + if let Some(mut current) = scaled.next() { + for region in scaled { + let union = current.union(®ion); + + if union.area() - current.area() - region.area() <= AREA_THRESHOLD { + current = union; + } else { + output.push(current); + current = region; + } + } + + output.push(current); + } + + output +} diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index 0187cc59..c9a818fb 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -113,6 +113,11 @@ impl Stack { self.layers[..self.active_count].iter() } + /// Returns the slice of layers in the [`Stack`]. + pub fn as_slice(&self) -> &[T] { + &self.layers[..self.active_count] + } + /// Flushes and settles any primitives in the current layer of the [`Stack`]. pub fn flush(&mut self) { self.layers[self.current].flush(); diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index a9649c6e..865ebd97 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -15,6 +15,7 @@ mod viewport; pub mod color; pub mod compositor; +pub mod damage; pub mod error; pub mod gradient; pub mod image; diff --git a/graphics/src/text.rs b/graphics/src/text.rs index c204c850..31b6de28 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -70,6 +70,76 @@ pub enum Text { }, } +impl Text { + /// Returns the visible bounds of the [`Text`]. + pub fn visible_bounds(&self) -> Option { + let (bounds, horizontal_alignment, vertical_alignment) = match self { + Text::Paragraph { + position, + paragraph, + clip_bounds, + .. + } => ( + Rectangle::new(*position, paragraph.min_bounds) + .intersection(clip_bounds), + Some(paragraph.horizontal_alignment), + Some(paragraph.vertical_alignment), + ), + Text::Editor { + editor, + position, + clip_bounds, + .. + } => ( + Rectangle::new(*position, editor.bounds) + .intersection(clip_bounds), + None, + None, + ), + Text::Cached { + bounds, + clip_bounds, + horizontal_alignment, + vertical_alignment, + .. + } => ( + bounds.intersection(clip_bounds), + Some(*horizontal_alignment), + Some(*vertical_alignment), + ), + Text::Raw { raw, .. } => (Some(raw.clip_bounds), None, None), + }; + + let mut bounds = bounds?; + + if let Some(alignment) = horizontal_alignment { + match alignment { + alignment::Horizontal::Left => {} + alignment::Horizontal::Center => { + bounds.x -= bounds.width / 2.0; + } + alignment::Horizontal::Right => { + bounds.x -= bounds.width; + } + } + } + + if let Some(alignment) = vertical_alignment { + match alignment { + alignment::Vertical::Top => {} + alignment::Vertical::Center => { + bounds.y -= bounds.height / 2.0; + } + alignment::Vertical::Bottom => { + bounds.y -= bounds.height; + } + } + } + + Some(bounds) + } +} + /// The regular variant of the [Fira Sans] font. /// /// It is loaded as part of the default fonts in Wasm builds. diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 3930f46a..fbca1274 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -402,9 +402,9 @@ impl Engine { horizontal_alignment, vertical_alignment, shaping, - clip_bounds: _, // TODO + clip_bounds: text_bounds, // TODO } => { - let physical_bounds = *bounds * transformation; + let physical_bounds = *text_bounds * transformation; if !clip_bounds.intersects(&physical_bounds) { return; @@ -539,10 +539,10 @@ impl Engine { pub fn draw_image( &mut self, image: &Image, - transformation: Transformation, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: &mut tiny_skia::Mask, - clip_bounds: Rectangle, + _transformation: Transformation, + _pixels: &mut tiny_skia::PixmapMut<'_>, + _clip_mask: &mut tiny_skia::Mask, + _clip_bounds: Rectangle, ) { match image { #[cfg(feature = "image")] @@ -551,21 +551,21 @@ impl Engine { filter_method, bounds, } => { - let physical_bounds = *bounds * transformation; + let physical_bounds = *bounds * _transformation; - if !clip_bounds.intersects(&physical_bounds) { + if !_clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) + .then_some(_clip_mask as &_); self.raster_pipeline.draw( handle, *filter_method, *bounds, - pixels, - into_transform(transformation), + _pixels, + into_transform(_transformation), clip_mask, ); } @@ -575,20 +575,20 @@ impl Engine { color, bounds, } => { - let physical_bounds = *bounds * transformation; + let physical_bounds = *bounds * _transformation; - if !clip_bounds.intersects(&physical_bounds) { + if !_clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) + .then_some(_clip_mask as &_); self.vector_pipeline.draw( handle, *color, physical_bounds, - pixels, + _pixels, clip_mask, ); } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 04aabf0d..117daf41 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -166,7 +166,7 @@ impl geometry::frame::Backend for Frame { let (scale_x, scale_y) = self.transform.get_scale(); - if self.transform.is_scale_translate() + if !self.transform.has_skew() && scale_x == scale_y && scale_x > 0.0 && scale_y > 0.0 diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 1f5c8b46..0d770d81 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -2,6 +2,7 @@ use crate::core::image; use crate::core::renderer::Quad; use crate::core::svg; use crate::core::{Background, Color, Point, Rectangle, Transformation}; +use crate::graphics::damage; use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph, Text}; use crate::graphics::{self, Image}; @@ -20,39 +21,6 @@ pub struct Layer { pub images: Vec, } -#[derive(Debug, Clone)] -pub enum Item { - Live(T), - Group(Vec, Rectangle, Transformation), - Cached(Rc<[T]>, Rectangle, Transformation), -} - -impl Item { - pub fn transformation(&self) -> Transformation { - match self { - Item::Live(_) => Transformation::IDENTITY, - Item::Group(_, _, transformation) - | Item::Cached(_, _, transformation) => *transformation, - } - } - - pub fn clip_bounds(&self) -> Rectangle { - match self { - Item::Live(_) => Rectangle::INFINITE, - Item::Group(_, clip_bounds, _) - | Item::Cached(_, clip_bounds, _) => *clip_bounds, - } - } - - pub fn as_slice(&self) -> &[T] { - match self { - Item::Live(item) => std::slice::from_ref(item), - Item::Group(group, _, _) => group.as_slice(), - Item::Cached(cache, _, _) => cache, - } - } -} - impl Layer { pub fn draw_quad( &mut self, @@ -204,6 +172,81 @@ impl Layer { transformation, )); } + + pub fn damage(previous: &Self, current: &Self) -> Vec { + let mut damage = damage::list( + &previous.quads, + ¤t.quads, + |(quad, _)| vec![quad.bounds.expand(1.0)], + |(quad_a, background_a), (quad_b, background_b)| { + quad_a == quad_b && background_a == background_b + }, + ); + + let text = damage::diff( + &previous.text, + ¤t.text, + |item| { + item.as_slice() + .iter() + .filter_map(Text::visible_bounds) + .map(|bounds| bounds * item.transformation()) + .collect() + }, + |text_a, text_b| { + damage::list( + text_a.as_slice(), + text_b.as_slice(), + |text| { + text.visible_bounds() + .into_iter() + .filter_map(|bounds| { + bounds.intersection(&text_a.clip_bounds()) + }) + .collect() + }, + |text_a, text_b| text_a == text_b, + ) + }, + ); + + let primitives = damage::list( + &previous.primitives, + ¤t.primitives, + |item| match item { + Item::Live(primitive) => vec![primitive.visible_bounds()], + Item::Group(primitives, bounds, transformation) => { + damage::group( + primitives + .as_slice() + .iter() + .map(Primitive::visible_bounds) + .map(|bounds| bounds * *transformation) + .collect(), + *bounds, + ) + } + Item::Cached(_, bounds, _) => { + vec![*bounds] + } + }, + |primitive_a, primitive_b| match (primitive_a, primitive_b) { + ( + Item::Cached(cache_a, bounds_a, transformation_a), + Item::Cached(cache_b, bounds_b, transformation_b), + ) => { + Rc::ptr_eq(cache_a, cache_b) + && bounds_a == bounds_b + && transformation_a == transformation_b + } + _ => false, + }, + ); + + damage.extend(text); + damage.extend(primitives); + damage + } } impl Default for Layer { @@ -236,8 +279,41 @@ impl graphics::Layer for Layer { self.bounds = Rectangle::INFINITE; self.quads.clear(); - self.text.clear(); self.primitives.clear(); + self.text.clear(); self.images.clear(); } } + +#[derive(Debug, Clone)] +pub enum Item { + Live(T), + Group(Vec, Rectangle, Transformation), + Cached(Rc<[T]>, Rectangle, Transformation), +} + +impl Item { + pub fn transformation(&self) -> Transformation { + match self { + Item::Live(_) => Transformation::IDENTITY, + Item::Group(_, _, transformation) + | Item::Cached(_, _, transformation) => *transformation, + } + } + + pub fn clip_bounds(&self) -> Rectangle { + match self { + Item::Live(_) => Rectangle::INFINITE, + Item::Group(_, clip_bounds, _) + | Item::Cached(_, clip_bounds, _) => *clip_bounds, + } + } + + pub fn as_slice(&self) -> &[T] { + match self { + Item::Live(item) => std::slice::from_ref(item), + Item::Group(group, _, _) => group.as_slice(), + Item::Cached(cache, _, _) => cache, + } + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index fed2f32c..4c2c9430 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -58,9 +58,9 @@ impl Renderer { } } - pub fn layers(&mut self) -> impl Iterator { + pub fn layers(&mut self) -> &[Layer] { self.layers.flush(); - self.layers.iter() + self.layers.as_slice() } pub fn draw>( @@ -135,12 +135,12 @@ impl Renderer { ); for layer in self.layers.iter() { - let Some(clip_bounds) = region.intersection(&layer.bounds) + let Some(clip_bounds) = + region.intersection(&(layer.bounds * scale_factor)) else { continue; }; - let clip_bounds = clip_bounds * scale_factor; engine::adjust_clip_mask(clip_mask, clip_bounds); for (quad, background) in &layer.quads { @@ -154,30 +154,15 @@ impl Renderer { ); } - for group in &layer.text { - for text in group.as_slice() { - self.engine.draw_text( - text, - group.transformation() - * Transformation::scale(scale_factor), - pixels, - clip_mask, - clip_bounds, - ); - } - } - for group in &layer.primitives { - let Some(new_clip_bounds) = - group.clip_bounds().intersection(&layer.bounds) + let Some(new_clip_bounds) = (group.clip_bounds() + * scale_factor) + .intersection(&clip_bounds) else { continue; }; - engine::adjust_clip_mask( - clip_mask, - new_clip_bounds * scale_factor, - ); + engine::adjust_clip_mask(clip_mask, new_clip_bounds); for primitive in group.as_slice() { self.engine.draw_primitive( @@ -193,6 +178,19 @@ impl Renderer { engine::adjust_clip_mask(clip_mask, clip_bounds); } + for group in &layer.text { + for text in group.as_slice() { + self.engine.draw_text( + text, + group.transformation() + * Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + } + for image in &layer.images { self.engine.draw_image( image, @@ -370,7 +368,7 @@ impl graphics::mesh::Renderer for Renderer { impl core::image::Renderer for Renderer { type Handle = core::image::Handle; - fn measure_image(&self, handle: &Self::Handle) -> Size { + fn measure_image(&self, handle: &Self::Handle) -> crate::core::Size { self.engine.raster_pipeline.dimensions(handle) } @@ -387,7 +385,10 @@ impl core::image::Renderer for Renderer { #[cfg(feature = "svg")] impl core::svg::Renderer for Renderer { - fn measure_svg(&self, handle: &core::svg::Handle) -> Size { + fn measure_svg( + &self, + handle: &core::svg::Handle, + ) -> crate::core::Size { self.engine.vector_pipeline.viewport_dimensions(handle) } diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs index 5305788d..5de51047 100644 --- a/tiny_skia/src/primitive.rs +++ b/tiny_skia/src/primitive.rs @@ -1,3 +1,5 @@ +use crate::core::Rectangle; + #[derive(Debug, Clone, PartialEq)] pub enum Primitive { /// A path filled with some paint. @@ -19,3 +21,20 @@ pub enum Primitive { stroke: tiny_skia::Stroke, }, } + +impl Primitive { + /// Returns the visible bounds of the [`Primitive`]. + pub fn visible_bounds(&self) -> Rectangle { + let bounds = match self { + Primitive::Fill { path, .. } => path.bounds(), + Primitive::Stroke { path, .. } => path.bounds(), + }; + + Rectangle { + x: bounds.x(), + y: bounds.y(), + width: bounds.width(), + height: bounds.height(), + } + } +} diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index e1a9dfec..153af6d5 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,5 +1,6 @@ use crate::core::{Color, Rectangle, Size}; use crate::graphics::compositor::{self, Information}; +use crate::graphics::damage; use crate::graphics::error::{self, Error}; use crate::graphics::{self, Viewport}; use crate::{Layer, Renderer, Settings}; @@ -154,7 +155,7 @@ pub fn present>( .buffer_mut() .map_err(|_| compositor::SurfaceError::Lost)?; - let _last_layers = { + let last_layers = { let age = buffer.age(); surface.max_age = surface.max_age.max(age); @@ -167,26 +168,28 @@ pub fn present>( } }; - // TODO - // let damage = last_layers - // .and_then(|last_layers| { - // (surface.background_color == background_color) - // .then(|| damage::layers(last_layers, renderer.layers())) - // }) - // .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); - - let damage = vec![Rectangle::with_size(viewport.logical_size())]; + let damage = last_layers + .and_then(|last_layers| { + (surface.background_color == background_color).then(|| { + damage::diff( + last_layers, + renderer.layers(), + |layer| vec![layer.bounds], + Layer::damage, + ) + }) + }) + .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); if damage.is_empty() { return Ok(()); } - surface - .layer_stack - .push_front(renderer.layers().cloned().collect()); + surface.layer_stack.push_front(renderer.layers().to_vec()); surface.background_color = background_color; - // let damage = damage::group(damage, viewport.logical_size()); + let damage = + damage::group(damage, Rectangle::with_size(viewport.logical_size())); let mut pixels = tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut buffer), -- cgit From 32cd456fb936117307c178b4d47ae89124c8329a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 10 Apr 2024 16:26:55 +0200 Subject: Account for `transformation` in `Text::visible_bounds` --- graphics/src/text.rs | 8 ++++++-- tiny_skia/src/layer.rs | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 31b6de28..30269e69 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -78,10 +78,12 @@ impl Text { position, paragraph, clip_bounds, + transformation, .. } => ( Rectangle::new(*position, paragraph.min_bounds) - .intersection(clip_bounds), + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), Some(paragraph.horizontal_alignment), Some(paragraph.vertical_alignment), ), @@ -89,10 +91,12 @@ impl Text { editor, position, clip_bounds, + transformation, .. } => ( Rectangle::new(*position, editor.bounds) - .intersection(clip_bounds), + .intersection(clip_bounds) + .map(|bounds| bounds * *transformation), None, None, ), diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 0d770d81..17e6ac13 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -200,9 +200,7 @@ impl Layer { |text| { text.visible_bounds() .into_iter() - .filter_map(|bounds| { - bounds.intersection(&text_a.clip_bounds()) - }) + .map(|bounds| bounds * text_a.transformation()) .collect() }, |text_a, text_b| text_a == text_b, -- cgit From fdd9896dc5f727f4c659ad6252f1ab36cca77762 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 10 Apr 2024 19:55:27 +0200 Subject: Track image damage in `iced_tiny_skia` --- graphics/src/image.rs | 13 ++++++++++++- tiny_skia/src/layer.rs | 8 ++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 47a7b30e..c6135e9e 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -7,7 +7,7 @@ use crate::core::svg; use crate::core::{Color, Rectangle}; /// A raster or vector image. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Image { /// A raster image. Raster { @@ -33,6 +33,17 @@ pub enum Image { }, } +impl Image { + /// Returns the bounds of the [`Image`]. + pub fn bounds(&self) -> Rectangle { + match self { + Image::Raster { bounds, .. } | Image::Vector { bounds, .. } => { + *bounds + } + } + } +} + #[cfg(feature = "image")] /// Tries to load an image by its [`Handle`]. /// diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 17e6ac13..d3d8b988 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -241,8 +241,16 @@ impl Layer { }, ); + let images = damage::list( + &previous.images, + ¤t.images, + |image| vec![image.bounds().expand(1.0)], + Image::eq, + ); + damage.extend(text); damage.extend(primitives); + damage.extend(images); damage } } -- cgit From 1e8554bf02f366b18b31b9ea1dfb150f7935ca06 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 10 Apr 2024 20:23:07 +0200 Subject: Sort damage by distance from origin in `iced_graphics::damage` --- graphics/src/damage.rs | 7 ++++--- tiny_skia/src/layer.rs | 18 ++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index ff8edaf5..17d60451 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -1,5 +1,5 @@ //! Compute the damage between frames. -use crate::core::Rectangle; +use crate::core::{Point, Rectangle}; /// Diffs the damage regions given some previous and current primitives. pub fn diff( @@ -50,9 +50,10 @@ pub fn group(mut damage: Vec, bounds: Rectangle) -> Vec { const AREA_THRESHOLD: f32 = 20_000.0; damage.sort_by(|a, b| { - a.x.partial_cmp(&b.x) + a.center() + .distance(Point::ORIGIN) + .partial_cmp(&b.center().distance(Point::ORIGIN)) .unwrap_or(Ordering::Equal) - .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal)) }); let mut output = Vec::new(); diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index d3d8b988..ec87c2bf 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -213,16 +213,14 @@ impl Layer { ¤t.primitives, |item| match item { Item::Live(primitive) => vec![primitive.visible_bounds()], - Item::Group(primitives, bounds, transformation) => { - damage::group( - primitives - .as_slice() - .iter() - .map(Primitive::visible_bounds) - .map(|bounds| bounds * *transformation) - .collect(), - *bounds, - ) + Item::Group(primitives, group_bounds, transformation) => { + primitives + .as_slice() + .iter() + .map(Primitive::visible_bounds) + .map(|bounds| bounds * *transformation) + .filter_map(|bounds| bounds.intersection(group_bounds)) + .collect() } Item::Cached(_, bounds, _) => { vec![*bounds] -- cgit From 43aafb7b79d51106c05f2494e6adc4a7a51d947e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 10 Apr 2024 20:31:44 +0200 Subject: Clip quad damage with layer bounds in `iced_tiny_skia` --- tiny_skia/src/layer.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index ec87c2bf..3e42e4aa 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -174,10 +174,20 @@ impl Layer { } pub fn damage(previous: &Self, current: &Self) -> Vec { + if previous.bounds != current.bounds { + return vec![previous.bounds, current.bounds]; + } + let mut damage = damage::list( &previous.quads, ¤t.quads, - |(quad, _)| vec![quad.bounds.expand(1.0)], + |(quad, _)| { + quad.bounds + .expand(1.0) + .intersection(¤t.bounds) + .into_iter() + .collect() + }, |(quad_a, background_a), (quad_b, background_b)| { quad_a == quad_b && background_a == background_b }, -- cgit From d0233da8a25c48a17942a3652cf3775f37c1420f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 12 Apr 2024 18:37:38 +0200 Subject: Fix applying local transformation to `layer_bounds` in `iced_wgpu::text` --- wgpu/src/text.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 1b21bb1c..0d01faca 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -242,7 +242,7 @@ impl Pipeline { &mut self.atlas, &mut self.cache, text, - layer_bounds, + layer_bounds * layer_transformation, layer_transformation * *transformation, target_size, ); @@ -269,7 +269,7 @@ impl Pipeline { self.format, cache, layer_transformation * *transformation, - layer_bounds, + layer_bounds * layer_transformation, target_size, ); } @@ -388,8 +388,6 @@ fn prepare( }) .collect(); - let layer_bounds = layer_bounds * layer_transformation; - let text_areas = sections.iter().zip(allocations.iter()).filter_map( |(section, allocation)| { let ( -- cgit From dbbbadfc950dfdfd02c7abbbf993e0685ca0f64a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 12 Apr 2024 18:46:48 +0200 Subject: Restore `PREMULTIPLIED_ALPHA_BLENDING` in `triangle::msaa` pipeline --- wgpu/src/triangle/msaa.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index fcee6b3e..71c16925 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -118,7 +118,9 @@ impl Blit { entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { format, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), + blend: Some( + wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, + ), write_mask: wgpu::ColorWrites::ALL, })], }), -- cgit