From 836507eca0cbd12a186a2d39bde6988db1b2b299 Mon Sep 17 00:00:00 2001 From: edwloef Date: Sun, 16 Mar 2025 17:18:59 +0100 Subject: [PATCH 01/15] request redraw on mouse area hover change --- widget/src/mouse_area.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 10976c76..2ea0b059 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -343,6 +343,10 @@ fn update( state.cursor_position = cursor_position; state.bounds = bounds; + if widget.interaction.is_some() && state.is_hovered != was_hovered { + shell.request_redraw(); + } + match ( widget.on_enter.as_ref(), widget.on_move.as_ref(), From 363b0e903acf1dc377228b60c8977b2f33a176a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 18 Mar 2025 00:03:34 +0100 Subject: [PATCH 02/15] Fix `markdown` code blocks when `highlighter` is disabled --- widget/src/markdown.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index d4de2a8c..afcb5da2 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -474,6 +474,7 @@ fn parse_with<'a>( let mut strikethrough = false; let mut metadata = false; let mut table = false; + let mut code_block = false; let mut link = None; let mut image = None; let mut stack = Vec::new(); @@ -627,6 +628,7 @@ fn parse_with<'a>( }); } + code_block = true; code_language = (!language.is_empty()).then(|| language.into_string()); @@ -732,6 +734,8 @@ fn parse_with<'a>( ) } pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => { + code_block = false; + #[cfg(feature = "highlighter")] { state.borrow_mut().highlighter = highlighter.take(); @@ -759,14 +763,28 @@ fn parse_with<'a>( _ => None, }, pulldown_cmark::Event::Text(text) if !metadata && !table => { - #[cfg(feature = "highlighter")] - if let Some(highlighter) = &mut highlighter { + if code_block { code.push_str(&text); + #[cfg(feature = "highlighter")] + if let Some(highlighter) = &mut highlighter { + for line in text.lines() { + code_lines.push(Text::new( + highlighter.highlight_line(line).to_vec(), + )); + } + } + + #[cfg(not(feature = "highlighter"))] for line in text.lines() { - code_lines.push(Text::new( - highlighter.highlight_line(line).to_vec(), - )); + code_lines.push(Text::new(vec![Span::Standard { + text: line.to_owned(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: false, + }])); } return None; From 31b98ee3eb2449cb520853da62f64a8894a1d753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 18 Mar 2025 18:22:39 +0100 Subject: [PATCH 03/15] Notify `window` before presentation --- graphics/src/compositor.rs | 2 ++ renderer/src/fallback.rs | 3 +++ tiny_skia/src/window/compositor.rs | 12 +++++++++++- wgpu/src/window/compositor.rs | 13 ++++++++++++- winit/src/program.rs | 1 + 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 363d651d..847578cf 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -80,6 +80,7 @@ pub trait Compositor: Sized { viewport: &Viewport, background_color: Color, overlay: &[T], + on_pre_present: impl FnOnce(), ) -> Result<(), SurfaceError>; /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of @@ -193,6 +194,7 @@ impl Compositor for () { _viewport: &Viewport, _background_color: Color, _overlay: &[T], + _on_pre_present: impl FnOnce(), ) -> Result<(), SurfaceError> { Ok(()) } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 82fc74ed..e8eb3327 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -322,6 +322,7 @@ where viewport: &graphics::Viewport, background_color: Color, overlay: &[T], + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { match (self, renderer, surface) { ( @@ -334,6 +335,7 @@ where viewport, background_color, overlay, + on_pre_present, ), ( Self::Secondary(compositor), @@ -345,6 +347,7 @@ where viewport, background_color, overlay, + on_pre_present, ), _ => unreachable!(), } diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 6c144be0..b0c369fb 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -114,8 +114,16 @@ impl crate::graphics::Compositor for Compositor { viewport: &Viewport, background_color: Color, overlay: &[T], + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { - present(renderer, surface, viewport, background_color, overlay) + present( + renderer, + surface, + viewport, + background_color, + overlay, + on_pre_present, + ) } fn screenshot>( @@ -146,6 +154,7 @@ pub fn present>( viewport: &Viewport, background_color: Color, overlay: &[T], + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { let physical_size = viewport.physical_size(); @@ -206,6 +215,7 @@ pub fn present>( overlay, ); + on_pre_present(); buffer.present().map_err(|_| compositor::SurfaceError::Lost) } diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index bc932b44..107aa1f5 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -221,6 +221,7 @@ pub fn present>( viewport: &Viewport, background_color: Color, overlay: &[T], + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { match surface.get_current_texture() { Ok(frame) => { @@ -249,6 +250,7 @@ pub fn present>( let _ = compositor.engine.submit(&compositor.queue, encoder); // Present the frame + on_pre_present(); frame.present(); Ok(()) @@ -365,8 +367,17 @@ impl graphics::Compositor for Compositor { viewport: &Viewport, background_color: Color, overlay: &[T], + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { - present(self, renderer, surface, viewport, background_color, overlay) + present( + self, + renderer, + surface, + viewport, + background_color, + overlay, + on_pre_present, + ) } fn screenshot>( diff --git a/winit/src/program.rs b/winit/src/program.rs index a1b8b579..e541b79b 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -892,6 +892,7 @@ async fn run_instance( window.state.viewport(), window.state.background_color(), &debug.overlay(), + || window.raw.pre_present_notify(), ) { Ok(()) => { debug.render_finished(); From 9c1edc3d782772e4d053ba56f70160e2b13aa61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 19 Mar 2025 19:23:35 +0100 Subject: [PATCH 04/15] Downgrade presentation `error!` to `warn!` in `iced_winit` --- winit/src/program.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/winit/src/program.rs b/winit/src/program.rs index e541b79b..5a0ebcb1 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -900,14 +900,13 @@ async fn run_instance( Err(error) => match error { // This is an unrecoverable error. compositor::SurfaceError::OutOfMemory => { - panic!("{:?}", error); + panic!("{error}"); } _ => { debug.render_finished(); - log::error!( - "Error {error:?} when \ - presenting surface." + log::warn!( + "Error {error:?} when presenting surface." ); // Try rendering all windows again next frame. From 65e8fed37d22824e9c45fff6bf580fe040b2a197 Mon Sep 17 00:00:00 2001 From: mtkennerly Date: Thu, 20 Mar 2025 18:31:24 -0400 Subject: [PATCH 05/15] Allow custom vertical spacing for wrapped rows --- widget/src/row.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/widget/src/row.rs b/widget/src/row.rs index 5ffeab49..b9fd2569 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -167,7 +167,10 @@ where /// /// The original alignment of the [`Row`] is preserved per row wrapped. pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> { - Wrapping { row: self } + Wrapping { + row: self, + vertical_spacing: None, + } } } @@ -372,6 +375,15 @@ pub struct Wrapping< Renderer = crate::Renderer, > { row: Row<'a, Message, Theme, Renderer>, + vertical_spacing: Option, +} + +impl Wrapping<'_, Message, Theme, Renderer> { + /// Sets the vertical spacing _between_ lines. + pub fn vertical_spacing(mut self, amount: impl Into) -> Self { + self.vertical_spacing = Some(amount.into().0); + self + } } impl Widget @@ -403,6 +415,7 @@ where .shrink(self.row.padding); let spacing = self.row.spacing; + let vertical_spacing = self.vertical_spacing.unwrap_or(spacing); let max_width = limits.max().width; let mut children: Vec = Vec::new(); @@ -447,7 +460,7 @@ where align(row_start..i, row_height, &mut children); - y += row_height + spacing; + y += row_height + vertical_spacing; x = 0.0; row_start = i; row_height = 0.0; From 576dd22733f29c85086f2cd0a223bea655ebd4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 24 Mar 2025 01:28:39 +0100 Subject: [PATCH 06/15] Split local state from `Engine` in `iced_wgpu` --- benches/wgpu.rs | 21 +- core/src/renderer.rs | 7 +- examples/integration/src/main.rs | 25 +- .../todos/snapshots/creates_a_new_task.sha256 | 2 +- graphics/src/compositor.rs | 6 +- renderer/src/compositor.rs | 1 - renderer/src/fallback.rs | 55 ++- renderer/src/lib.rs | 28 -- test/src/lib.rs | 6 +- tiny_skia/src/lib.rs | 14 +- tiny_skia/src/window/compositor.rs | 12 +- wgpu/src/engine.rs | 73 ++-- wgpu/src/image/mod.rs | 34 +- wgpu/src/lib.rs | 343 +++++++++++++++--- wgpu/src/quad.rs | 153 ++++---- wgpu/src/quad/gradient.rs | 2 +- wgpu/src/quad/solid.rs | 2 +- wgpu/src/text.rs | 88 +++-- wgpu/src/triangle.rs | 114 +++--- wgpu/src/triangle/msaa.rs | 253 +++++++------ wgpu/src/window/compositor.rs | 166 +-------- winit/src/program.rs | 4 - 22 files changed, 768 insertions(+), 641 deletions(-) delete mode 100644 renderer/src/compositor.rs diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 8d61137e..b400bfa1 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -80,16 +80,15 @@ fn benchmark<'a>( let format = wgpu::TextureFormat::Bgra8UnormSrgb; - let mut engine = iced_wgpu::Engine::new( + let engine = iced_wgpu::Engine::new( adapter, - device, - queue, + device.clone(), + queue.clone(), format, Some(Antialiasing::MSAAx4), ); - let mut renderer = - Renderer::new(device, &engine, Font::DEFAULT, Pixels::from(16)); + let mut renderer = Renderer::new(engine, Font::DEFAULT, Pixels::from(16)); let viewport = graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0); @@ -134,16 +133,7 @@ fn benchmark<'a>( cache = Some(user_interface.into_cache()); - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: None, - }); - - renderer.present::<&str>( - &mut engine, - device, - queue, - &mut encoder, + let submission = renderer.present::<&str>( Some(Color::BLACK), format, &texture_view, @@ -151,7 +141,6 @@ fn benchmark<'a>( &[], ); - let submission = engine.submit(queue, encoder); let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); i += 1; diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 68e070e8..c9224970 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -106,7 +106,12 @@ impl Default for Style { /// a window nor a compositor. pub trait Headless { /// Creates a new [`Headless`] renderer; - fn new(default_font: Font, default_text_size: Pixels) -> Self; + fn new( + default_font: Font, + default_text_size: Pixels, + ) -> impl Future> + where + Self: Sized; /// Draws offscreen into a screenshot, returning a collection of /// bytes representing the rendered pixels in RGBA order. diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 7c5388ab..6c6cb120 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -35,11 +35,9 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { Loading, Ready { window: Arc, - device: wgpu::Device, - queue: wgpu::Queue, surface: wgpu::Surface<'static>, format: wgpu::TextureFormat, - engine: Engine, + device: wgpu::Device, renderer: Renderer, scene: Scene, state: program::State, @@ -146,13 +144,9 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { // Initialize iced let mut debug = Debug::new(); let engine = - Engine::new(&adapter, &device, &queue, format, None); - let mut renderer = Renderer::new( - &device, - &engine, - Font::default(), - Pixels::from(16), - ); + Engine::new(&adapter, device.clone(), queue, format, None); + let mut renderer = + Renderer::new(engine, Font::default(), Pixels::from(16)); let state = program::State::new( controls, @@ -166,11 +160,9 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { *self = Self::Ready { window, - device, - queue, surface, format, - engine, + device, renderer, scene, state, @@ -193,10 +185,8 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { let Self::Ready { window, device, - queue, surface, format, - engine, renderer, scene, state, @@ -264,10 +254,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { // And then iced on top renderer.present( - engine, - device, - queue, - &mut encoder, None, frame.texture.format(), &view, @@ -276,7 +262,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { ); // Then we submit the work - engine.submit(queue, encoder); frame.present(); // Update the mouse cursor diff --git a/examples/todos/snapshots/creates_a_new_task.sha256 b/examples/todos/snapshots/creates_a_new_task.sha256 index f62b2f69..6dd6ccbe 100644 --- a/examples/todos/snapshots/creates_a_new_task.sha256 +++ b/examples/todos/snapshots/creates_a_new_task.sha256 @@ -1 +1 @@ -0e355b080ad33905145e9f70a3b29e2481197c8fc8f42491acd5358238ebbd5f \ No newline at end of file +804a1bb6d49e3b3158463202960447d9e7820b967280f41dd0c34c00d3edf2c3 \ No newline at end of file diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 847578cf..e9063678 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -87,12 +87,11 @@ pub trait Compositor: Sized { /// the texture ordered as `RGBA` in the `sRGB` color space. /// /// [`Renderer`]: Self::Renderer - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec; } @@ -199,12 +198,11 @@ impl Compositor for () { Ok(()) } - fn screenshot>( + fn screenshot( &mut self, _renderer: &mut Self::Renderer, _viewport: &Viewport, _background_color: Color, - _overlay: &[T], ) -> Vec { vec![] } diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs deleted file mode 100644 index 8b137891..00000000 --- a/renderer/src/compositor.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index e8eb3327..8ec49212 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -3,7 +3,8 @@ use crate::core::image; use crate::core::renderer; use crate::core::svg; use crate::core::{ - self, Background, Color, Image, Point, Rectangle, Size, Svg, Transformation, + self, Background, Color, Font, Image, Pixels, Point, Rectangle, Size, Svg, + Transformation, }; use crate::graphics; use crate::graphics::compositor; @@ -353,29 +354,18 @@ where } } - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, viewport: &graphics::Viewport, background_color: Color, - overlay: &[T], ) -> Vec { match (self, renderer) { (Self::Primary(compositor), Renderer::Primary(renderer)) => { - compositor.screenshot( - renderer, - viewport, - background_color, - overlay, - ) + compositor.screenshot(renderer, viewport, background_color) } (Self::Secondary(compositor), Renderer::Secondary(renderer)) => { - compositor.screenshot( - renderer, - viewport, - background_color, - overlay, - ) + compositor.screenshot(renderer, viewport, background_color) } _ => unreachable!(), } @@ -617,6 +607,41 @@ mod geometry { } } +impl renderer::Headless for Renderer +where + A: renderer::Headless, + B: renderer::Headless, +{ + async fn new( + default_font: Font, + default_text_size: Pixels, + ) -> Option { + if let Some(renderer) = A::new(default_font, default_text_size).await { + return Some(Self::Primary(renderer)); + } + + B::new(default_font, default_text_size) + .await + .map(Self::Secondary) + } + + fn screenshot( + &mut self, + size: Size, + scale_factor: f32, + background_color: Color, + ) -> Vec { + match self { + crate::fallback::Renderer::Primary(renderer) => { + renderer.screenshot(size, scale_factor, background_color) + } + crate::fallback::Renderer::Secondary(renderer) => { + renderer.screenshot(size, scale_factor, background_color) + } + } + } +} + impl compositor::Default for Renderer where A: compositor::Default, diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index ee20a458..220542e1 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -23,9 +23,6 @@ pub type Compositor = renderer::Compositor; #[cfg(all(feature = "wgpu", feature = "tiny-skia"))] mod renderer { - use crate::core::renderer; - use crate::core::{Color, Font, Pixels, Size}; - pub type Renderer = crate::fallback::Renderer< iced_wgpu::Renderer, iced_tiny_skia::Renderer, @@ -35,31 +32,6 @@ mod renderer { iced_wgpu::window::Compositor, iced_tiny_skia::window::Compositor, >; - - impl renderer::Headless for Renderer { - fn new(default_font: Font, default_text_size: Pixels) -> Self { - Self::Secondary(iced_tiny_skia::Renderer::new( - default_font, - default_text_size, - )) - } - - fn screenshot( - &mut self, - size: Size, - scale_factor: f32, - background_color: Color, - ) -> Vec { - match self { - crate::fallback::Renderer::Primary(_) => unreachable!( - "iced_wgpu does not support headless mode yet!" - ), - crate::fallback::Renderer::Secondary(renderer) => { - renderer.screenshot(size, scale_factor, background_color) - } - } - } - } } #[cfg(all(feature = "wgpu", not(feature = "tiny-skia")))] diff --git a/test/src/lib.rs b/test/src/lib.rs index 982cc2c1..c2033f81 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -186,8 +186,10 @@ where load_font(font).expect("Font must be valid"); } - let mut renderer = - Renderer::new(default_font, settings.default_text_size); + let mut renderer = iced_runtime::futures::futures::executor::block_on( + Renderer::new(default_font, settings.default_text_size), + ) + .expect("Create new headless renderer"); let raw = UserInterface::build( element, diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index b73eb842..2442f8fd 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -407,8 +407,11 @@ impl compositor::Default for Renderer { } impl renderer::Headless for Renderer { - fn new(default_font: Font, default_text_size: Pixels) -> Self { - Self::new(default_font, default_text_size) + async fn new( + default_font: Font, + default_text_size: Pixels, + ) -> Option { + Some(Self::new(default_font, default_text_size)) } fn screenshot( @@ -420,11 +423,6 @@ impl renderer::Headless for Renderer { let viewport = Viewport::with_physical_size(size, f64::from(scale_factor)); - window::compositor::screenshot::<&str>( - self, - &viewport, - background_color, - &[], - ) + window::compositor::screenshot(self, &viewport, background_color) } } diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index b0c369fb..a36be106 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -126,14 +126,13 @@ impl crate::graphics::Compositor for Compositor { ) } - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { - screenshot(renderer, viewport, background_color, overlay) + screenshot(renderer, viewport, background_color) } } @@ -219,11 +218,10 @@ pub fn present>( buffer.present().map_err(|_| compositor::SurfaceError::Lost) } -pub fn screenshot>( +pub fn screenshot( renderer: &mut Renderer, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { let size = viewport.physical_size(); @@ -233,7 +231,7 @@ pub fn screenshot>( let mut clip_mask = tiny_skia::Mask::new(size.width, size.height) .expect("Create clip mask"); - renderer.draw( + renderer.draw::<&str>( &mut tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut offscreen_buffer), size.width, @@ -247,7 +245,7 @@ pub fn screenshot>( size.height as f32, ))], background_color, - overlay, + &[], ); offscreen_buffer.iter().fold( diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs index 782fd58c..5125562c 100644 --- a/wgpu/src/engine.rs +++ b/wgpu/src/engine.rs @@ -1,13 +1,16 @@ -use crate::buffer; use crate::graphics::Antialiasing; use crate::primitive; use crate::quad; use crate::text; use crate::triangle; +use std::sync::{Arc, RwLock}; + +#[derive(Clone)] #[allow(missing_debug_implementations)] pub struct Engine { - pub(crate) staging_belt: wgpu::util::StagingBelt, + pub(crate) device: wgpu::Device, + pub(crate) queue: wgpu::Queue, pub(crate) format: wgpu::TextureFormat, pub(crate) quad_pipeline: quad::Pipeline, @@ -15,46 +18,41 @@ pub struct Engine { pub(crate) triangle_pipeline: triangle::Pipeline, #[cfg(any(feature = "image", feature = "svg"))] pub(crate) image_pipeline: crate::image::Pipeline, - pub(crate) primitive_storage: primitive::Storage, + pub(crate) primitive_storage: Arc>, } impl Engine { pub fn new( _adapter: &wgpu::Adapter, - device: &wgpu::Device, - queue: &wgpu::Queue, + 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, - ), format, - quad_pipeline, - text_pipeline, - triangle_pipeline, + quad_pipeline: quad::Pipeline::new(&device, format), + text_pipeline: text::Pipeline::new(&device, &queue, format), + triangle_pipeline: triangle::Pipeline::new( + &device, + format, + antialiasing, + ), #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, + image_pipeline: { + let backend = _adapter.get_info().backend; - primitive_storage: primitive::Storage::default(), + crate::image::Pipeline::new(&device, format, backend) + }, + + primitive_storage: Arc::new(RwLock::new( + primitive::Storage::default(), + )), + + device, + queue, } } @@ -65,23 +63,4 @@ impl Engine { ) -> crate::image::Cache { self.image_pipeline.create_cache(device) } - - 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/image/mod.rs b/wgpu/src/image/mod.rs index 6cb18a07..08cf4ed5 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -21,16 +21,14 @@ pub use crate::graphics::Image; pub type Batch = Vec; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Pipeline { - pipeline: wgpu::RenderPipeline, + raw: wgpu::RenderPipeline, backend: wgpu::Backend, nearest_sampler: wgpu::Sampler, linear_sampler: wgpu::Sampler, texture_layout: Arc, constant_layout: wgpu::BindGroupLayout, - layers: Vec, - prepare_layer: usize, } impl Pipeline { @@ -194,26 +192,37 @@ impl Pipeline { }); Pipeline { - pipeline, + raw: pipeline, backend, nearest_sampler, linear_sampler, texture_layout: Arc::new(texture_layout), constant_layout, - layers: Vec::new(), - prepare_layer: 0, } } pub fn create_cache(&self, device: &wgpu::Device) -> Cache { Cache::new(device, self.backend, self.texture_layout.clone()) } +} + +#[derive(Default)] +pub struct State { + layers: Vec, + prepare_layer: usize, +} + +impl State { + pub fn new() -> Self { + Self::default() + } pub fn prepare( &mut self, + pipeline: &Pipeline, device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, + encoder: &mut wgpu::CommandEncoder, cache: &mut Cache, images: &Batch, transformation: Transformation, @@ -285,9 +294,9 @@ impl Pipeline { if self.layers.len() <= self.prepare_layer { self.layers.push(Layer::new( device, - &self.constant_layout, - &self.nearest_sampler, - &self.linear_sampler, + &pipeline.constant_layout, + &pipeline.nearest_sampler, + &pipeline.linear_sampler, )); } @@ -308,13 +317,14 @@ impl Pipeline { pub fn render<'a>( &'a self, + pipeline: &'a Pipeline, cache: &'a Cache, 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_pipeline(&pipeline.raw); render_pass.set_scissor_rect( bounds.x, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index ac3210e2..8c5a759a 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -60,6 +60,7 @@ 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, Vector, @@ -73,23 +74,30 @@ use crate::graphics::text::{Editor, Paragraph}; /// [`iced`]: https://github.com/iced-rs/iced #[allow(missing_debug_implementations)] pub struct Renderer { + engine: Engine, + default_font: Font, default_text_size: Pixels, layers: layer::Stack, - triangle_storage: triangle::Storage, - text_storage: text::Storage, + quad: quad::State, + triangle: triangle::State, + text: text::State, text_viewport: text::Viewport, + #[cfg(any(feature = "svg", feature = "image"))] + image: image::State, + // TODO: Centralize all the image feature handling #[cfg(any(feature = "svg", feature = "image"))] image_cache: std::cell::RefCell, + + staging_belt: wgpu::util::StagingBelt, } impl Renderer { pub fn new( - device: &wgpu::Device, - engine: &Engine, + engine: Engine, default_font: Font, default_text_size: Pixels, ) -> Self { @@ -98,52 +106,206 @@ impl Renderer { default_text_size, layers: layer::Stack::new(), - triangle_storage: triangle::Storage::new(), - text_storage: text::Storage::new(), - text_viewport: engine.text_pipeline.create_viewport(device), + quad: quad::State::new(), + triangle: triangle::State::new( + &engine.device, + &engine.triangle_pipeline, + ), + text: text::State::new(), + text_viewport: engine.text_pipeline.create_viewport(&engine.device), + + #[cfg(any(feature = "svg", feature = "image"))] + image: image::State::new(), #[cfg(any(feature = "svg", feature = "image"))] image_cache: std::cell::RefCell::new( - engine.create_image_cache(device), + engine.create_image_cache(&engine.device), ), + + // 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, + ), + + engine, } } + fn draw( + &mut self, + clear_color: Option, + target: &wgpu::TextureView, + viewport: &Viewport, + ) -> wgpu::CommandEncoder { + let mut encoder = self.engine.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu encoder"), + }, + ); + + self.prepare(&mut encoder, viewport); + self.render(&mut encoder, target, clear_color, viewport); + + self.quad.end_frame(); + self.triangle.end_frame(); + self.text.end_frame(); + + #[cfg(any(feature = "svg", feature = "image"))] + { + self.image.end_frame(); + self.image_cache.borrow_mut().trim(); + } + + encoder + } + pub fn present>( &mut self, - engine: &mut Engine, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, clear_color: Option, - format: wgpu::TextureFormat, + _format: wgpu::TextureFormat, frame: &wgpu::TextureView, viewport: &Viewport, overlay: &[T], - ) { + ) -> wgpu::SubmissionIndex { self.draw_overlay(overlay, viewport); - self.prepare(engine, device, queue, format, encoder, viewport); - self.render(engine, encoder, frame, clear_color, viewport); - self.triangle_storage.trim(); - self.text_storage.trim(); + let encoder = self.draw(clear_color, frame, viewport); - #[cfg(any(feature = "svg", feature = "image"))] - self.image_cache.borrow_mut().trim(); + self.staging_belt.finish(); + let submission = self.engine.queue.submit([encoder.finish()]); + self.staging_belt.recall(); + submission + } + + /// Renders the current surface to an offscreen buffer. + /// + /// Returns RGBA bytes of the texture data. + pub fn screenshot( + &mut self, + viewport: &Viewport, + background_color: Color, + ) -> Vec { + #[derive(Clone, Copy, Debug)] + struct BufferDimensions { + width: u32, + height: u32, + unpadded_bytes_per_row: usize, + padded_bytes_per_row: usize, + } + + impl BufferDimensions { + fn new(size: Size) -> Self { + let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA + let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256 + let padded_bytes_per_row_padding = (alignment + - unpadded_bytes_per_row % alignment) + % alignment; + let padded_bytes_per_row = + unpadded_bytes_per_row + padded_bytes_per_row_padding; + + Self { + width: size.width, + height: size.height, + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } + } + + let dimensions = BufferDimensions::new(viewport.physical_size()); + + let texture_extent = wgpu::Extent3d { + width: dimensions.width, + height: dimensions.height, + depth_or_array_layers: 1, + }; + + let texture = + self.engine.device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu.offscreen.source_texture"), + size: texture_extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: self.engine.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self.draw(Some(background_color), &view, &viewport); + + let texture = crate::color::convert( + &self.engine.device, + &mut encoder, + texture, + if graphics::color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + ); + + let output_buffer = + self.engine.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu.offscreen.output_texture_buffer"), + size: (dimensions.padded_bytes_per_row + * dimensions.height as usize) as u64, + usage: wgpu::BufferUsages::MAP_READ + | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + wgpu::TexelCopyBufferInfo { + buffer: &output_buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), + rows_per_image: None, + }, + }, + texture_extent, + ); + + self.staging_belt.finish(); + let index = self.engine.queue.submit([encoder.finish()]); + self.staging_belt.recall(); + + let slice = output_buffer.slice(..); + slice.map_async(wgpu::MapMode::Read, |_| {}); + + let _ = self + .engine + .device + .poll(wgpu::Maintain::WaitForSubmissionIndex(index)); + + let mapped_buffer = slice.get_mapped_range(); + + mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold( + vec![], + |mut acc, row| { + acc.extend(&row[..dimensions.unpadded_bytes_per_row]); + acc + }, + ) } fn prepare( &mut self, - engine: &mut Engine, - device: &wgpu::Device, - queue: &wgpu::Queue, - _format: wgpu::TextureFormat, encoder: &mut wgpu::CommandEncoder, viewport: &Viewport, ) { let scale_factor = viewport.scale_factor() as f32; - self.text_viewport.update(queue, viewport.physical_size()); + self.text_viewport + .update(&self.engine.queue, viewport.physical_size()); let physical_bounds = Rectangle::::from(Rectangle::with_size( viewport.physical_size(), @@ -159,10 +321,11 @@ impl Renderer { } if !layer.quads.is_empty() { - engine.quad_pipeline.prepare( - device, + self.quad.prepare( + &self.engine.quad_pipeline, + &self.engine.device, + &mut self.staging_belt, encoder, - &mut engine.staging_belt, &layer.quads, viewport.projection(), scale_factor, @@ -170,11 +333,11 @@ impl Renderer { } if !layer.triangles.is_empty() { - engine.triangle_pipeline.prepare( - device, + self.triangle.prepare( + &self.engine.triangle_pipeline, + &self.engine.device, + &mut self.staging_belt, encoder, - &mut engine.staging_belt, - &mut self.triangle_storage, &layer.triangles, Transformation::scale(scale_factor), viewport.physical_size(), @@ -182,12 +345,18 @@ impl Renderer { } if !layer.primitives.is_empty() { + let mut primitive_storage = self + .engine + .primitive_storage + .write() + .expect("Write primitive storage"); + for instance in &layer.primitives { instance.primitive.prepare( - device, - queue, - engine.format, - &mut engine.primitive_storage, + &self.engine.device, + &self.engine.queue, + self.engine.format, + &mut primitive_storage, &instance.bounds, viewport, ); @@ -196,10 +365,11 @@ impl Renderer { #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { - engine.image_pipeline.prepare( - device, + self.image.prepare( + &self.engine.image_pipeline, + &self.engine.device, + &mut self.staging_belt, encoder, - &mut engine.staging_belt, &mut self.image_cache.borrow_mut(), &layer.images, viewport.projection(), @@ -208,12 +378,12 @@ impl Renderer { } if !layer.text.is_empty() { - engine.text_pipeline.prepare( - device, - queue, + self.text.prepare( + &self.engine.text_pipeline, + &self.engine.device, + &self.engine.queue, &self.text_viewport, encoder, - &mut self.text_storage, &layer.text, layer.bounds, Transformation::scale(scale_factor), @@ -224,7 +394,6 @@ impl Renderer { fn render( &mut self, - engine: &mut Engine, encoder: &mut wgpu::CommandEncoder, frame: &wgpu::TextureView, clear_color: Option, @@ -291,7 +460,8 @@ impl Renderer { }; if !layer.quads.is_empty() { - engine.quad_pipeline.render( + self.quad.render( + &self.engine.quad_pipeline, quad_layer, scissor_rect, &layer.quads, @@ -304,10 +474,10 @@ impl Renderer { if !layer.triangles.is_empty() { let _ = ManuallyDrop::into_inner(render_pass); - mesh_layer += engine.triangle_pipeline.render( + mesh_layer += self.triangle.render( + &self.engine.triangle_pipeline, encoder, frame, - &self.triangle_storage, mesh_layer, &layer.triangles, physical_bounds, @@ -337,6 +507,12 @@ impl Renderer { if !layer.primitives.is_empty() { let _ = ManuallyDrop::into_inner(render_pass); + let primitive_storage = self + .engine + .primitive_storage + .read() + .expect("Read primitive storage"); + for instance in &layer.primitives { if let Some(clip_bounds) = (instance.bounds * scale) .intersection(&physical_bounds) @@ -344,7 +520,7 @@ impl Renderer { { instance.primitive.render( encoder, - &engine.primitive_storage, + &primitive_storage, frame, &clip_bounds, ); @@ -373,7 +549,8 @@ impl Renderer { #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { - engine.image_pipeline.render( + self.image.render( + &self.engine.image_pipeline, &image_cache, image_layer, scissor_rect, @@ -384,9 +561,9 @@ impl Renderer { } if !layer.text.is_empty() { - text_layer += engine.text_pipeline.render( + text_layer += self.text.render( + &self.engine.text_pipeline, &self.text_viewport, - &self.text_storage, text_layer, &layer.text, scissor_rect, @@ -630,3 +807,67 @@ impl primitive::Renderer for Renderer { impl graphics::compositor::Default for crate::Renderer { type Compositor = window::Compositor; } + +impl renderer::Headless for Renderer { + async fn new( + default_font: Font, + default_text_size: Pixels, + ) -> Option { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::from_env() + .unwrap_or(wgpu::Backends::PRIMARY), + flags: wgpu::InstanceFlags::empty(), + ..wgpu::InstanceDescriptor::default() + }); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + force_fallback_adapter: false, + compatible_surface: None, + }) + .await?; + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: Some("iced_wgpu [headless]"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits { + max_bind_groups: 2, + ..wgpu::Limits::default() + }, + memory_hints: wgpu::MemoryHints::MemoryUsage, + }, + None, + ) + .await + .ok()?; + + let engine = Engine::new( + &adapter, + device, + queue, + if graphics::color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + Some(graphics::Antialiasing::MSAAx4), + ); + + Some(Self::new(engine, default_font, default_text_size)) + } + + fn screenshot( + &mut self, + size: Size, + scale_factor: f32, + background_color: Color, + ) -> Vec { + self.screenshot( + &Viewport::with_physical_size(size, f64::from(scale_factor)), + background_color, + ) + } +} diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index de432d2f..24a538a2 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -43,15 +43,96 @@ pub struct Quad { pub shadow_blur_radius: f32, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Pipeline { solid: solid::Pipeline, gradient: gradient::Pipeline, constant_layout: wgpu::BindGroupLayout, +} + +#[derive(Default)] +pub struct State { layers: Vec, prepare_layer: usize, } +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn prepare( + &mut self, + pipeline: &Pipeline, + device: &wgpu::Device, + belt: &mut wgpu::util::StagingBelt, + encoder: &mut wgpu::CommandEncoder, + quads: &Batch, + transformation: Transformation, + scale: f32, + ) { + if self.layers.len() <= self.prepare_layer { + self.layers + .push(Layer::new(device, &pipeline.constant_layout)); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare(device, encoder, belt, quads, transformation, scale); + + self.prepare_layer += 1; + } + + pub fn render<'a>( + &'a self, + pipeline: &'a Pipeline, + layer: usize, + bounds: Rectangle, + quads: &Batch, + 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 => { + pipeline.solid.render( + render_pass, + &layer.constants, + &layer.solid, + solid_offset..(solid_offset + count), + ); + + solid_offset += count; + } + Kind::Gradient => { + pipeline.gradient.render( + render_pass, + &layer.constants, + &layer.gradient, + gradient_offset..(gradient_offset + count), + ); + + gradient_offset += count; + } + } + } + } + } + + pub fn end_frame(&mut self) { + self.prepare_layer = 0; + } +} + impl Pipeline { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Pipeline { let constant_layout = @@ -74,79 +155,9 @@ impl Pipeline { Self { solid: solid::Pipeline::new(device, format, &constant_layout), gradient: gradient::Pipeline::new(device, format, &constant_layout), - layers: Vec::new(), - prepare_layer: 0, constant_layout, } } - - pub fn prepare( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - quads: &Batch, - transformation: Transformation, - scale: f32, - ) { - if self.layers.len() <= self.prepare_layer { - self.layers.push(Layer::new(device, &self.constant_layout)); - } - - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, encoder, belt, quads, transformation, scale); - - self.prepare_layer += 1; - } - - pub fn render<'a>( - &'a self, - layer: usize, - bounds: Rectangle, - quads: &Batch, - 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; - } - } - } - } - } - - pub fn end_frame(&mut self) { - self.prepare_layer = 0; - } } #[derive(Debug)] diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 68c11157..3c5fc33f 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -57,7 +57,7 @@ impl Layer { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Pipeline { #[cfg(not(target_arch = "wasm32"))] pipeline: wgpu::RenderPipeline, diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index b6d88486..317a248c 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -51,7 +51,7 @@ impl Layer { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Pipeline { pipeline: wgpu::RenderPipeline, } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 9a8f5c0d..7f88cac4 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -9,7 +9,7 @@ use crate::graphics::text::{Editor, Paragraph, font_system, to_color}; use rustc_hash::FxHashMap; use std::collections::hash_map; use std::sync::atomic::{self, AtomicU64}; -use std::sync::{self, Arc}; +use std::sync::{self, Arc, RwLock}; pub use crate::graphics::Text; @@ -94,10 +94,6 @@ struct Group { } impl Storage { - pub fn new() -> Self { - Self::default() - } - fn get(&self, cache: &Cache) -> Option<(&cryoglyph::TextAtlas, &Upload)> { if cache.text.is_empty() { return None; @@ -272,48 +268,40 @@ impl Viewport { } } +#[derive(Clone)] #[allow(missing_debug_implementations)] pub struct Pipeline { - state: cryoglyph::Cache, format: wgpu::TextureFormat, - atlas: cryoglyph::TextAtlas, + cache: cryoglyph::Cache, + atlas: Arc>, +} + +#[derive(Default)] +pub struct State { renderers: Vec, prepare_layer: usize, cache: BufferCache, + storage: Storage, } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - ) -> Self { - let state = cryoglyph::Cache::new(device); - let atlas = cryoglyph::TextAtlas::with_color_mode( - device, queue, &state, format, COLOR_MODE, - ); - - Pipeline { - state, - format, - renderers: Vec::new(), - atlas, - prepare_layer: 0, - cache: BufferCache::new(), - } +impl State { + pub fn new() -> Self { + Self::default() } pub fn prepare( &mut self, + pipeline: &Pipeline, device: &wgpu::Device, queue: &wgpu::Queue, viewport: &Viewport, encoder: &mut wgpu::CommandEncoder, - storage: &mut Storage, batch: &Batch, layer_bounds: Rectangle, layer_transformation: Transformation, ) { + let mut atlas = pipeline.atlas.write().expect("Write to text atlas"); + for item in batch { match item { Item::Group { @@ -322,7 +310,7 @@ impl Pipeline { } => { if self.renderers.len() <= self.prepare_layer { self.renderers.push(cryoglyph::TextRenderer::new( - &mut self.atlas, + &mut atlas, device, wgpu::MultisampleState::default(), None, @@ -336,7 +324,7 @@ impl Pipeline { &viewport.0, encoder, renderer, - &mut self.atlas, + &mut atlas, &mut self.cache, text, layer_bounds * layer_transformation, @@ -358,13 +346,13 @@ impl Pipeline { transformation, cache, } => { - storage.prepare( + self.storage.prepare( device, queue, &viewport.0, encoder, - self.format, - &self.state, + pipeline.format, + &pipeline.cache, cache, layer_transformation * *transformation, layer_bounds * layer_transformation, @@ -376,13 +364,14 @@ impl Pipeline { pub fn render<'a>( &'a self, + pipeline: &'a Pipeline, viewport: &'a Viewport, - storage: &'a Storage, start: usize, batch: &'a Batch, bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, ) -> usize { + let atlas = pipeline.atlas.read().expect("Read text atlas"); let mut layer_count = 0; render_pass.set_scissor_rect( @@ -398,13 +387,13 @@ impl Pipeline { let renderer = &self.renderers[start + layer_count]; renderer - .render(&self.atlas, &viewport.0, render_pass) + .render(&atlas, &viewport.0, render_pass) .expect("Render text"); layer_count += 1; } Item::Cached { cache, .. } => { - if let Some((atlas, upload)) = storage.get(cache) { + if let Some((atlas, upload)) = self.storage.get(cache) { upload .renderer .render(atlas, &viewport.0, render_pass) @@ -417,18 +406,37 @@ impl Pipeline { layer_count } - pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { - Viewport(cryoglyph::Viewport::new(device, &self.state)) - } - pub fn end_frame(&mut self) { - self.atlas.trim(); self.cache.trim(); + self.storage.trim(); self.prepare_layer = 0; } } +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + ) -> Self { + let cache = cryoglyph::Cache::new(device); + let atlas = cryoglyph::TextAtlas::with_color_mode( + device, queue, &cache, format, COLOR_MODE, + ); + + Pipeline { + format, + cache, + atlas: Arc::new(RwLock::new(atlas)), + } + } + + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(cryoglyph::Viewport::new(device, &self.cache)) + } +} + fn prepare( device: &wgpu::Device, queue: &wgpu::Queue, diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index a2b976ea..ec3ee309 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -153,42 +153,47 @@ impl Storage { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Pipeline { - blit: Option, + msaa: Option, solid: solid::Pipeline, gradient: gradient::Pipeline, - layers: Vec, - 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), +pub struct State { + msaa: Option, + layers: Vec, + prepare_layer: usize, + storage: Storage, +} + +impl State { + pub fn new(device: &wgpu::Device, pipeline: &Pipeline) -> Self { + Self { + msaa: pipeline + .msaa + .as_ref() + .map(|pipeline| msaa::State::new(device, pipeline)), layers: Vec::new(), prepare_layer: 0, + storage: Storage::new(), } } pub fn prepare( &mut self, + pipeline: &Pipeline, device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, - storage: &mut Storage, + encoder: &mut wgpu::CommandEncoder, items: &[Item], scale: Transformation, target_size: Size, ) { - let projection = if let Some(blit) = &mut self.blit { - blit.prepare(device, encoder, belt, target_size) * scale + let projection = if let Some((state, pipeline)) = + self.msaa.as_mut().zip(pipeline.msaa.as_ref()) + { + state.prepare(device, encoder, belt, pipeline, target_size) * scale } else { Transformation::orthographic(target_size.width, target_size.height) * scale @@ -203,8 +208,8 @@ impl Pipeline { if self.layers.len() <= self.prepare_layer { self.layers.push(Layer::new( device, - &self.solid, - &self.gradient, + &pipeline.solid, + &pipeline.gradient, )); } @@ -213,8 +218,8 @@ impl Pipeline { device, encoder, belt, - &self.solid, - &self.gradient, + &pipeline.solid, + &pipeline.gradient, meshes, projection * *transformation, ); @@ -225,12 +230,12 @@ impl Pipeline { transformation, cache, } => { - storage.prepare( + self.storage.prepare( device, encoder, belt, - &self.solid, - &self.gradient, + &pipeline.solid, + &pipeline.gradient, cache, projection * *transformation, ); @@ -241,9 +246,9 @@ impl Pipeline { pub fn render( &mut self, + pipeline: &Pipeline, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, - storage: &Storage, start: usize, batch: &Batch, bounds: Rectangle, @@ -269,7 +274,7 @@ impl Pipeline { transformation, cache, } => { - let upload = storage.get(cache)?; + let upload = self.storage.get(cache)?; Some(( &upload.layer, @@ -282,9 +287,9 @@ impl Pipeline { render( encoder, target, - self.blit.as_mut(), - &self.solid, - &self.gradient, + self.msaa.as_ref().zip(pipeline.msaa.as_ref()), + &pipeline.solid, + &pipeline.gradient, bounds, items, ); @@ -293,47 +298,54 @@ impl Pipeline { } pub fn end_frame(&mut self) { + self.storage.trim(); + self.prepare_layer = 0; } } +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option, + ) -> Pipeline { + Pipeline { + msaa: antialiasing.map(|a| msaa::Pipeline::new(device, format, a)), + solid: solid::Pipeline::new(device, format, antialiasing), + gradient: gradient::Pipeline::new(device, format, antialiasing), + } + } +} + fn render<'a>( encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, - mut blit: Option<&mut msaa::Blit>, + mut msaa: Option<(&msaa::State, &msaa::Pipeline)>, solid: &solid::Pipeline, gradient: &gradient::Pipeline, bounds: Rectangle, group: impl Iterator, ) { { - let (attachment, resolve_target, load) = if let Some(blit) = &mut blit { - let (attachment, resolve_target) = blit.targets(); - - ( - attachment, - Some(resolve_target), - wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - ) + let mut render_pass = if let Some((_state, pipeline)) = &mut msaa { + pipeline.render_pass(encoder) } 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, + view: target, + resolve_target: None, ops: wgpu::Operations { - load, + load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, - }); + }) + }; for (layer, meshes, transformation) in group { layer.render( @@ -347,8 +359,8 @@ fn render<'a>( } } - if let Some(blit) = blit { - blit.draw(encoder, target); + if let Some((state, pipeline)) = msaa { + state.render(pipeline, encoder, target); } } @@ -649,7 +661,7 @@ mod solid { use crate::graphics::mesh; use crate::triangle; - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct Pipeline { pub pipeline: wgpu::RenderPipeline, pub constants_layout: wgpu::BindGroupLayout, @@ -801,7 +813,7 @@ mod gradient { use crate::graphics::mesh; use crate::triangle; - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct Pipeline { pub pipeline: wgpu::RenderPipeline, pub constants_layout: wgpu::BindGroupLayout, diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 0a5b134f..86dddb0b 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -2,35 +2,28 @@ use crate::core::{Size, Transformation}; use crate::graphics; use std::num::NonZeroU64; +use std::sync::{Arc, RwLock}; -#[derive(Debug)] -pub struct Blit { +#[derive(Debug, Clone)] +pub struct Pipeline { format: wgpu::TextureFormat, - pipeline: wgpu::RenderPipeline, - constants: wgpu::BindGroup, - ratio: wgpu::Buffer, + sampler: wgpu::Sampler, + raw: wgpu::RenderPipeline, + constant_layout: wgpu::BindGroupLayout, texture_layout: wgpu::BindGroupLayout, sample_count: u32, - targets: Option, - last_region: Option>, + targets: Arc>>, } -impl Blit { +impl Pipeline { pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, antialiasing: graphics::Antialiasing, - ) -> Blit { + ) -> Pipeline { 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"), @@ -56,22 +49,6 @@ impl Blit { ], }); - 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), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: ratio.as_entire_binding(), - }, - ], - }); - let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu::triangle::msaa texture layout"), @@ -143,31 +120,30 @@ impl Blit { cache: None, }); - Blit { + Self { format, - pipeline, - constants: constant_bind_group, - ratio, + sampler, + raw: pipeline, + constant_layout, texture_layout, sample_count: antialiasing.sample_count(), - targets: None, - last_region: None, + targets: Arc::new(RwLock::new(None)), } } - pub fn prepare( - &mut self, + fn targets( + &self, device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, region_size: Size, - ) -> Transformation { - match &mut self.targets { + ) -> Targets { + let mut targets = self.targets.write().expect("Write MSAA targets"); + + match targets.as_mut() { Some(targets) if region_size.width <= targets.size.width && region_size.height <= targets.size.height => {} _ => { - self.targets = Some(Targets::new( + *targets = Some(Targets::new( device, self.format, &self.texture_layout, @@ -177,69 +153,34 @@ impl Blit { } } - 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) + targets.as_ref().unwrap().clone() } - pub fn targets(&self) -> (&wgpu::TextureView, &wgpu::TextureView) { - let targets = self.targets.as_ref().unwrap(); - - (&targets.attachment, &targets.resolve) - } - - pub fn draw( + pub fn render_pass<'a>( &self, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - ) { - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::triangle::msaa 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, - }); + encoder: &'a mut wgpu::CommandEncoder, + ) -> wgpu::RenderPass<'a> { + let targets = self.targets.read().expect("Read MSAA targets"); + let targets = targets.as_ref().unwrap(); - render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_bind_group( - 1, - &self.targets.as_ref().unwrap().bind_group, - &[], - ); - render_pass.draw(0..6, 0..1); + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &targets.attachment, + resolve_target: Some(&targets.resolve), + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }) } } -#[derive(Debug)] +#[derive(Debug, Clone)] struct Targets { attachment: wgpu::TextureView, resolve: wgpu::TextureView, @@ -308,9 +249,117 @@ impl Targets { } } -#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] struct Ratio { u: f32, v: f32, } + +pub struct State { + ratio: wgpu::Buffer, + constants: wgpu::BindGroup, + last_ratio: Option, +} + +impl State { + pub fn new(device: &wgpu::Device, pipeline: &Pipeline) -> Self { + 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 constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::triangle::msaa uniforms bind group"), + layout: &pipeline.constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&pipeline.sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], + }); + + Self { + ratio, + constants, + last_ratio: None, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + pipeline: &Pipeline, + region_size: Size, + ) -> Transformation { + let targets = pipeline.targets(device, region_size); + + 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, + }; + + if Some(ratio) != self.last_ratio { + 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_ratio = Some(ratio); + } + + Transformation::orthographic(targets.size.width, targets.size.height) + } + + pub fn render( + &self, + pipeline: &Pipeline, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + ) { + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu::triangle::msaa 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, + }); + + render_pass.set_pipeline(&pipeline.raw); + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_bind_group( + 1, + &pipeline + .targets + .read() + .expect("Read MSAA targets") + .as_ref() + .unwrap() + .bind_group, + &[], + ); + render_pass.draw(0..6, 0..1); + } +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 107aa1f5..053d3a19 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,5 +1,5 @@ //! Connect a window with a renderer. -use crate::core::{Color, Size}; +use crate::core::Color; use crate::graphics::color; use crate::graphics::compositor; use crate::graphics::error; @@ -12,8 +12,6 @@ use crate::{Engine, Renderer}; pub struct Compositor { instance: wgpu::Instance, adapter: wgpu::Adapter, - device: wgpu::Device, - queue: wgpu::Queue, format: wgpu::TextureFormat, alpha_mode: wgpu::CompositeAlphaMode, engine: Engine, @@ -178,8 +176,8 @@ impl Compositor { Ok((device, queue)) => { let engine = Engine::new( &adapter, - &device, - &queue, + device, + queue, format, settings.antialiasing, ); @@ -187,8 +185,6 @@ impl Compositor { return Ok(Compositor { instance, adapter, - device, - queue, format, alpha_mode, engine, @@ -215,7 +211,6 @@ pub async fn new( /// Presents the given primitives with the given [`Compositor`]. pub fn present>( - compositor: &mut Compositor, renderer: &mut Renderer, surface: &mut wgpu::Surface<'static>, viewport: &Viewport, @@ -225,21 +220,11 @@ pub fn present>( ) -> Result<(), compositor::SurfaceError> { match surface.get_current_texture() { Ok(frame) => { - let mut encoder = compositor.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu encoder"), - }, - ); - let view = &frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); - renderer.present( - &mut compositor.engine, - &compositor.device, - &compositor.queue, - &mut encoder, + let _submission = renderer.present( Some(background_color), frame.texture.format(), view, @@ -247,8 +232,6 @@ pub fn present>( overlay, ); - let _ = compositor.engine.submit(&compositor.queue, encoder); - // Present the frame on_pre_present(); frame.present(); @@ -305,8 +288,7 @@ impl graphics::Compositor for Compositor { fn create_renderer(&self) -> Self::Renderer { Renderer::new( - &self.device, - &self.engine, + self.engine.clone(), self.settings.default_font, self.settings.default_text_size, ) @@ -337,7 +319,7 @@ impl graphics::Compositor for Compositor { height: u32, ) { surface.configure( - &self.device, + &self.engine.device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: self.format, @@ -370,7 +352,6 @@ impl graphics::Compositor for Compositor { on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { present( - self, renderer, surface, viewport, @@ -380,143 +361,12 @@ impl graphics::Compositor for Compositor { ) } - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { - screenshot(self, renderer, viewport, background_color, overlay) - } -} - -/// Renders the current surface to an offscreen buffer. -/// -/// Returns RGBA bytes of the texture data. -pub fn screenshot>( - compositor: &mut Compositor, - renderer: &mut Renderer, - viewport: &Viewport, - background_color: Color, - overlay: &[T], -) -> Vec { - let dimensions = BufferDimensions::new(viewport.physical_size()); - - let texture_extent = wgpu::Extent3d { - width: dimensions.width, - height: dimensions.height, - depth_or_array_layers: 1, - }; - - let texture = compositor.device.create_texture(&wgpu::TextureDescriptor { - label: Some("iced_wgpu.offscreen.source_texture"), - size: texture_extent, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: compositor.format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - 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, - viewport, - overlay, - ); - - let texture = crate::color::convert( - &compositor.device, - &mut encoder, - texture, - if color::GAMMA_CORRECTION { - wgpu::TextureFormat::Rgba8UnormSrgb - } else { - wgpu::TextureFormat::Rgba8Unorm - }, - ); - - let output_buffer = - compositor.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu.offscreen.output_texture_buffer"), - size: (dimensions.padded_bytes_per_row * dimensions.height as usize) - as u64, - usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - encoder.copy_texture_to_buffer( - texture.as_image_copy(), - wgpu::TexelCopyBufferInfo { - buffer: &output_buffer, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), - rows_per_image: None, - }, - }, - texture_extent, - ); - - let index = compositor.engine.submit(&compositor.queue, encoder); - - let slice = output_buffer.slice(..); - slice.map_async(wgpu::MapMode::Read, |_| {}); - - let _ = compositor - .device - .poll(wgpu::Maintain::WaitForSubmissionIndex(index)); - - let mapped_buffer = slice.get_mapped_range(); - - mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold( - vec![], - |mut acc, row| { - acc.extend(&row[..dimensions.unpadded_bytes_per_row]); - acc - }, - ) -} - -#[derive(Clone, Copy, Debug)] -struct BufferDimensions { - width: u32, - height: u32, - unpadded_bytes_per_row: usize, - padded_bytes_per_row: usize, -} - -impl BufferDimensions { - fn new(size: Size) -> Self { - let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA - let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256 - let padded_bytes_per_row_padding = - (alignment - unpadded_bytes_per_row % alignment) % alignment; - let padded_bytes_per_row = - unpadded_bytes_per_row + padded_bytes_per_row_padding; - - Self { - width: size.width, - height: size.height, - unpadded_bytes_per_row, - padded_bytes_per_row, - } + renderer.screenshot(viewport, background_color) } } diff --git a/winit/src/program.rs b/winit/src/program.rs index 5a0ebcb1..6904c02a 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -773,7 +773,6 @@ async fn run_instance( &mut messages, &mut clipboard, &mut control_sender, - &mut debug, &mut user_interfaces, &mut window_manager, &mut ui_caches, @@ -966,7 +965,6 @@ async fn run_instance( &mut messages, &mut clipboard, &mut control_sender, - &mut debug, &mut user_interfaces, &mut window_manager, &mut ui_caches, @@ -1187,7 +1185,6 @@ fn run_action( messages: &mut Vec, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, - debug: &mut Debug, interfaces: &mut FxHashMap< window::Id, UserInterface<'_, P::Message, P::Theme, P::Renderer>, @@ -1477,7 +1474,6 @@ fn run_action( &mut window.renderer, window.state.viewport(), window.state.background_color(), - &debug.overlay(), ); let _ = channel.send(core::window::Screenshot::new( From af6b8155c6aa83dff706463ead974eb80a72ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 24 Mar 2025 19:36:02 +0100 Subject: [PATCH 07/15] Fix `cargo lint` issues --- wgpu/src/lib.rs | 2 +- wgpu/src/primitive.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 8c5a759a..b39defa8 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -238,7 +238,7 @@ impl Renderer { let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = self.draw(Some(background_color), &view, &viewport); + let mut encoder = self.draw(Some(background_color), &view, viewport); let texture = crate::color::convert( &self.engine.device, diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 8641f27a..c8bcb65d 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -61,7 +61,7 @@ pub trait Renderer: core::Renderer { /// Stores custom, user-provided types. #[derive(Default, Debug)] pub struct Storage { - pipelines: FxHashMap>, + pipelines: FxHashMap>, } impl Storage { @@ -71,7 +71,7 @@ impl Storage { } /// Inserts the data `T` in to [`Storage`]. - pub fn store(&mut self, data: T) { + pub fn store(&mut self, data: T) { let _ = self.pipelines.insert(TypeId::of::(), Box::new(data)); } From 76c5306581b3ca13624bf9e9de70808ff956b07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 24 Mar 2025 19:47:25 +0100 Subject: [PATCH 08/15] Trim text atlas in `iced_wgpu` after drawing This will need to change to only trim when all windows have finished drawing once we implement concurrent presentation, since there would be glyph fighting otherwise. --- wgpu/src/lib.rs | 3 +++ wgpu/src/text.rs | 50 ++++++++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b39defa8..a585782e 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -152,6 +152,9 @@ impl Renderer { self.triangle.end_frame(); self.text.end_frame(); + // TODO: Move to runtime! + self.engine.text_pipeline.trim(); + #[cfg(any(feature = "svg", feature = "image"))] { self.image.end_frame(); diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 7f88cac4..05e99396 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -276,6 +276,33 @@ pub struct Pipeline { atlas: Arc>, } +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + ) -> Self { + let cache = cryoglyph::Cache::new(device); + let atlas = cryoglyph::TextAtlas::with_color_mode( + device, queue, &cache, format, COLOR_MODE, + ); + + Pipeline { + format, + cache, + atlas: Arc::new(RwLock::new(atlas)), + } + } + + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(cryoglyph::Viewport::new(device, &self.cache)) + } + + pub fn trim(&self) { + self.atlas.write().expect("Write text atlas").trim(); + } +} + #[derive(Default)] pub struct State { renderers: Vec, @@ -414,29 +441,6 @@ impl State { } } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - ) -> Self { - let cache = cryoglyph::Cache::new(device); - let atlas = cryoglyph::TextAtlas::with_color_mode( - device, queue, &cache, format, COLOR_MODE, - ); - - Pipeline { - format, - cache, - atlas: Arc::new(RwLock::new(atlas)), - } - } - - pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { - Viewport(cryoglyph::Viewport::new(device, &self.cache)) - } -} - fn prepare( device: &wgpu::Device, queue: &wgpu::Queue, From 175a53bc86fbb6a645bb630ab1efeac0b2101ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 24 Mar 2025 19:51:18 +0100 Subject: [PATCH 09/15] Rename `end_frame` to `trim` in `iced_wgpu` --- wgpu/src/image/mod.rs | 2 +- wgpu/src/lib.rs | 8 ++++---- wgpu/src/quad.rs | 2 +- wgpu/src/text.rs | 2 +- wgpu/src/triangle.rs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 08cf4ed5..51d2acef 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -339,7 +339,7 @@ impl State { } } - pub fn end_frame(&mut self) { + pub fn trim(&mut self) { self.prepare_layer = 0; } } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index a585782e..fe583240 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -148,16 +148,16 @@ impl Renderer { self.prepare(&mut encoder, viewport); self.render(&mut encoder, target, clear_color, viewport); - self.quad.end_frame(); - self.triangle.end_frame(); - self.text.end_frame(); + self.quad.trim(); + self.triangle.trim(); + self.text.trim(); // TODO: Move to runtime! self.engine.text_pipeline.trim(); #[cfg(any(feature = "svg", feature = "image"))] { - self.image.end_frame(); + self.image.trim(); self.image_cache.borrow_mut().trim(); } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 24a538a2..4c00945c 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -128,7 +128,7 @@ impl State { } } - pub fn end_frame(&mut self) { + pub fn trim(&mut self) { self.prepare_layer = 0; } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 05e99396..2a4c6161 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -433,7 +433,7 @@ impl State { layer_count } - pub fn end_frame(&mut self) { + pub fn trim(&mut self) { self.cache.trim(); self.storage.trim(); diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index ec3ee309..6d0b0322 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -297,7 +297,7 @@ impl State { layer_count } - pub fn end_frame(&mut self) { + pub fn trim(&mut self) { self.storage.trim(); self.prepare_layer = 0; From 5e5c7c85adfa924ccb7a6832a25a6e8cdd8832e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 24 Mar 2025 20:18:24 +0100 Subject: [PATCH 10/15] Append renderer name to `iced_test` snapshots --- core/src/renderer.rs | 7 ++++ .../creates_a_new_task-tiny-skia.sha256 | 1 + ....sha256 => creates_a_new_task-wgpu.sha256} | 0 renderer/src/fallback.rs | 11 +++++- test/src/lib.rs | 39 ++++++++++++++----- tiny_skia/src/lib.rs | 11 ++++++ wgpu/src/lib.rs | 9 +++++ 7 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 examples/todos/snapshots/creates_a_new_task-tiny-skia.sha256 rename examples/todos/snapshots/{creates_a_new_task.sha256 => creates_a_new_task-wgpu.sha256} (100%) diff --git a/core/src/renderer.rs b/core/src/renderer.rs index c9224970..199ca09b 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -109,10 +109,17 @@ pub trait Headless { fn new( default_font: Font, default_text_size: Pixels, + backend: Option<&str>, ) -> impl Future> where Self: Sized; + /// Returns the unique name of the renderer. + /// + /// This name may be used by testing libraries to uniquely identify + /// snapshots. + fn name(&self) -> String; + /// Draws offscreen into a screenshot, returning a collection of /// bytes representing the rendered pixels in RGBA order. fn screenshot( diff --git a/examples/todos/snapshots/creates_a_new_task-tiny-skia.sha256 b/examples/todos/snapshots/creates_a_new_task-tiny-skia.sha256 new file mode 100644 index 00000000..f62b2f69 --- /dev/null +++ b/examples/todos/snapshots/creates_a_new_task-tiny-skia.sha256 @@ -0,0 +1 @@ +0e355b080ad33905145e9f70a3b29e2481197c8fc8f42491acd5358238ebbd5f \ No newline at end of file diff --git a/examples/todos/snapshots/creates_a_new_task.sha256 b/examples/todos/snapshots/creates_a_new_task-wgpu.sha256 similarity index 100% rename from examples/todos/snapshots/creates_a_new_task.sha256 rename to examples/todos/snapshots/creates_a_new_task-wgpu.sha256 diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 8ec49212..958870b3 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -615,16 +615,23 @@ where async fn new( default_font: Font, default_text_size: Pixels, + backend: Option<&str>, ) -> Option { - if let Some(renderer) = A::new(default_font, default_text_size).await { + if let Some(renderer) = + A::new(default_font, default_text_size, backend).await + { return Some(Self::Primary(renderer)); } - B::new(default_font, default_text_size) + B::new(default_font, default_text_size, backend) .await .map(Self::Secondary) } + fn name(&self) -> String { + delegate!(self, renderer, renderer.name()) + } + fn screenshot( &mut self, size: Size, diff --git a/test/src/lib.rs b/test/src/lib.rs index c2033f81..41ae66d6 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -109,6 +109,7 @@ use crate::runtime::UserInterface; use crate::runtime::user_interface; use std::borrow::Cow; +use std::env; use std::fs; use std::io; use std::path::{Path, PathBuf}; @@ -186,10 +187,16 @@ where load_font(font).expect("Font must be valid"); } - let mut renderer = iced_runtime::futures::futures::executor::block_on( - Renderer::new(default_font, settings.default_text_size), - ) - .expect("Create new headless renderer"); + let mut renderer = { + let backend = env::var("ICED_BACKEND").ok(); + + iced_runtime::futures::futures::executor::block_on(Renderer::new( + default_font, + settings.default_text_size, + backend.as_deref(), + )) + .expect("Create new headless renderer") + }; let raw = UserInterface::build( element, @@ -457,6 +464,7 @@ where physical_size, f64::from(scale_factor), ), + renderer: self.renderer.name(), }) } @@ -472,6 +480,7 @@ where #[derive(Debug, Clone)] pub struct Snapshot { screenshot: window::Screenshot, + renderer: String, } impl Snapshot { @@ -481,7 +490,7 @@ impl Snapshot { /// If the PNG image does not exist, it will be created by the [`Snapshot`] for future /// testing and `true` will be returned. pub fn matches_image(&self, path: impl AsRef) -> Result { - let path = snapshot_path(path, "png"); + let path = self.path(path, "png"); if path.exists() { let file = fs::File::open(&path)?; @@ -522,7 +531,7 @@ impl Snapshot { pub fn matches_hash(&self, path: impl AsRef) -> Result { use sha2::{Digest, Sha256}; - let path = snapshot_path(path, "sha256"); + let path = self.path(path, "sha256"); let hash = { let mut hasher = Sha256::new(); @@ -543,6 +552,20 @@ impl Snapshot { Ok(true) } } + + fn path(&self, path: impl AsRef, extension: &str) -> PathBuf { + let path = path.as_ref(); + + path.with_file_name(format!( + "{name}-{renderer}", + name = path + .file_stem() + .map(std::ffi::OsStr::to_string_lossy) + .unwrap_or_default(), + renderer = self.renderer + )) + .with_extension(extension) + } } /// Returns the sequence of events of a click. @@ -635,7 +658,3 @@ fn load_font(font: impl Into>) -> Result<(), Error> { Ok(()) } - -fn snapshot_path(path: impl AsRef, extension: &str) -> PathBuf { - path.as_ref().with_extension(extension) -} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 2442f8fd..65980e6a 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -410,10 +410,21 @@ impl renderer::Headless for Renderer { async fn new( default_font: Font, default_text_size: Pixels, + backend: Option<&str>, ) -> Option { + if backend.is_some_and(|backend| { + !["tiny-skia", "tiny_skia"].contains(&backend) + }) { + return None; + } + Some(Self::new(default_font, default_text_size)) } + fn name(&self) -> String { + "tiny-skia".to_owned() + } + fn screenshot( &mut self, size: Size, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index fe583240..e5885547 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -815,7 +815,12 @@ impl renderer::Headless for Renderer { async fn new( default_font: Font, default_text_size: Pixels, + backend: Option<&str>, ) -> Option { + if backend.is_some_and(|backend| backend != "wgpu") { + return None; + } + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::from_env() .unwrap_or(wgpu::Backends::PRIMARY), @@ -862,6 +867,10 @@ impl renderer::Headless for Renderer { Some(Self::new(engine, default_font, default_text_size)) } + fn name(&self) -> String { + "wgpu".to_owned() + } + fn screenshot( &mut self, size: Size, From 86311b8914b04c781446c45dc49b661f15110a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 24 Mar 2025 20:28:14 +0100 Subject: [PATCH 11/15] Fix `multi_window` example scrollable layout --- examples/multi_window/src/main.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 8cec9d4c..bc2a8908 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,6 +1,6 @@ use iced::widget::{ - button, center, center_x, column, horizontal_space, scrollable, text, - text_input, + button, center, center_x, column, container, horizontal_space, scrollable, + text, text_input, }; use iced::window; use iced::{ @@ -188,13 +188,12 @@ impl Window { let new_window_button = button(text("New Window")).on_press(Message::OpenWindow); - let content = scrollable( - column![scale_input, title_input, new_window_button] - .spacing(50) - .width(Fill) - .align_x(Center), - ); + let content = column![scale_input, title_input, new_window_button] + .spacing(50) + .width(Fill) + .align_x(Center) + .width(200); - center_x(content).width(200).into() + container(scrollable(center_x(content))).padding(10).into() } } From 5c196889f24e40a59f86d1b3e96f90720611ee26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 24 Mar 2025 20:35:10 +0100 Subject: [PATCH 12/15] Set `ICED_BACKEND` to `tiny-skia` in `test` workflow --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f087f069..dd91f184 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ jobs: runs-on: ${{ matrix.os }} env: RUSTFLAGS: --deny warnings + ICED_BACKEND: tiny-skia strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] From 788d9989ba10f07d01dd7419041810d6fcc76cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 27 Mar 2025 17:01:41 +0100 Subject: [PATCH 13/15] Fix load operation for `msaa` render pass in `iced_wgpu` --- wgpu/src/triangle/msaa.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 86dddb0b..8172575d 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -169,7 +169,7 @@ impl Pipeline { view: &targets.attachment, resolve_target: Some(&targets.resolve), ops: wgpu::Operations { - load: wgpu::LoadOp::Load, + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store, }, })], From f74ae737847e6f8d54c6482772d475715160d93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 28 Mar 2025 20:55:16 +0100 Subject: [PATCH 14/15] Use `ICED_TEST_BACKEND` env var in `iced_test` --- .github/workflows/test.yml | 2 +- test/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd91f184..4f35556f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ jobs: runs-on: ${{ matrix.os }} env: RUSTFLAGS: --deny warnings - ICED_BACKEND: tiny-skia + ICED_TEST_BACKEND: tiny-skia strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] diff --git a/test/src/lib.rs b/test/src/lib.rs index 41ae66d6..6fff4c74 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -188,7 +188,7 @@ where } let mut renderer = { - let backend = env::var("ICED_BACKEND").ok(); + let backend = env::var("ICED_TEST_BACKEND").ok(); iced_runtime::futures::futures::executor::block_on(Renderer::new( default_font, From ced4276a5e37b17a1ac7a7886ab4a10c179d3216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 29 Mar 2025 21:24:48 +0100 Subject: [PATCH 15/15] Replace `Rc` with `Arc` in `tiny_skia` renderer --- tiny_skia/src/geometry.rs | 14 +++++++------- tiny_skia/src/layer.rs | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 0c2e8b1d..e089bbb1 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -7,7 +7,7 @@ use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{self, Path, Style}; use crate::graphics::{self, Gradient, Image, Text}; -use std::rc::Rc; +use std::sync::Arc; #[derive(Debug)] pub enum Geometry { @@ -22,9 +22,9 @@ pub enum Geometry { #[derive(Debug, Clone)] pub struct Cache { - pub text: Rc<[Text]>, - pub images: Rc<[graphics::Image]>, - pub primitives: Rc<[Primitive]>, + pub text: Arc<[Text]>, + pub images: Arc<[graphics::Image]>, + pub primitives: Arc<[Primitive]>, pub clip_bounds: Rectangle, } @@ -43,9 +43,9 @@ impl Cached for Geometry { text, clip_bounds, } => Cache { - primitives: Rc::from(primitives), - images: Rc::from(images), - text: Rc::from(text), + primitives: Arc::from(primitives), + images: Arc::from(images), + text: Arc::from(text), clip_bounds, }, Self::Cache(cache) => cache, diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index a1ad1127..24e62ecb 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -8,7 +8,7 @@ use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph, Text}; use crate::graphics::{self, Image}; -use std::rc::Rc; +use std::sync::Arc; pub type Stack = layer::Stack; @@ -107,7 +107,7 @@ impl Layer { pub fn draw_text_cache( &mut self, - text: Rc<[Text]>, + text: Arc<[Text]>, clip_bounds: Rectangle, transformation: Transformation, ) { @@ -163,7 +163,7 @@ impl Layer { pub fn draw_primitive_cache( &mut self, - primitives: Rc<[Primitive]>, + primitives: Arc<[Primitive]>, clip_bounds: Rectangle, transformation: Transformation, ) { @@ -242,7 +242,7 @@ impl Layer { Item::Cached(cache_a, bounds_a, transformation_a), Item::Cached(cache_b, bounds_b, transformation_b), ) => { - Rc::ptr_eq(cache_a, cache_b) + Arc::ptr_eq(cache_a, cache_b) && bounds_a == bounds_b && transformation_a == transformation_b } @@ -304,7 +304,7 @@ impl graphics::Layer for Layer { pub enum Item { Live(T), Group(Vec, Rectangle, Transformation), - Cached(Rc<[T]>, Rectangle, Transformation), + Cached(Arc<[T]>, Rectangle, Transformation), } impl Item {