diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f087f069..4f35556f 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_TEST_BACKEND: tiny-skia strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] diff --git a/benches/wgpu.rs b/benches/wgpu.rs index ba3aa60c..24d8212c 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,23 +133,13 @@ fn benchmark<'a>( cache = Some(user_interface.into_cache()); - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: None, - }); - - renderer.present( - &mut engine, - device, - queue, - &mut encoder, + let submission = renderer.present( Some(Color::BLACK), format, &texture_view, &viewport, ); - 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..199ca09b 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -106,7 +106,19 @@ 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, + 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. diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index cf9bd5e6..732bb21d 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -36,11 +36,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, controls: Controls, @@ -146,25 +144,26 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { let controls = Controls::new(); // Initialize iced - let engine = - Engine::new(&adapter, &device, &queue, format, None); - let renderer = Renderer::new( - &device, - &engine, - Font::default(), - Pixels::from(16), - ); + let renderer = { + let engine = Engine::new( + &adapter, + device.clone(), + queue, + format, + None, + ); + + Renderer::new(engine, Font::default(), Pixels::from(16)) + }; // You should change this if you want to render continuously event_loop.set_control_flow(ControlFlow::Wait); *self = Self::Ready { window, - device, - queue, surface, format, - engine, + device, renderer, scene, controls, @@ -188,10 +187,8 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { let Self::Ready { window, device, - queue, surface, format, - engine, renderer, scene, controls, @@ -285,10 +282,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { *cache = interface.into_cache(); renderer.present( - engine, - device, - queue, - &mut encoder, None, frame.texture.format(), &view, @@ -296,7 +289,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/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 3f9efc31..4470c834 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::{ @@ -189,13 +189,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() } } diff --git a/examples/todos/snapshots/creates_a_new_task.sha256 b/examples/todos/snapshots/creates_a_new_task-tiny-skia.sha256 similarity index 100% rename from examples/todos/snapshots/creates_a_new_task.sha256 rename to examples/todos/snapshots/creates_a_new_task-tiny-skia.sha256 diff --git a/examples/todos/snapshots/creates_a_new_task-wgpu.sha256 b/examples/todos/snapshots/creates_a_new_task-wgpu.sha256 new file mode 100644 index 00000000..6dd6ccbe --- /dev/null +++ b/examples/todos/snapshots/creates_a_new_task-wgpu.sha256 @@ -0,0 +1 @@ +804a1bb6d49e3b3158463202960447d9e7820b967280f41dd0c34c00d3edf2c3 \ No newline at end of file diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 8bafe973..338d648f 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -79,6 +79,7 @@ pub trait Compositor: Sized { surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, + on_pre_present: impl FnOnce(), ) -> Result<(), SurfaceError>; /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of @@ -190,6 +191,7 @@ impl Compositor for () { _surface: &mut Self::Surface, _viewport: &Viewport, _background_color: Color, + _on_pre_present: impl FnOnce(), ) -> Result<(), SurfaceError> { Ok(()) } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index c6fbcff6..4cea1a15 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; @@ -321,6 +322,7 @@ where surface: &mut Self::Surface, viewport: &graphics::Viewport, background_color: Color, + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { match (self, renderer, surface) { ( @@ -332,6 +334,7 @@ where surface, viewport, background_color, + on_pre_present, ), ( Self::Secondary(compositor), @@ -342,6 +345,7 @@ where surface, viewport, background_color, + on_pre_present, ), _ => unreachable!(), } @@ -600,6 +604,48 @@ mod geometry { } } +impl renderer::Headless for Renderer +where + A: renderer::Headless, + B: renderer::Headless, +{ + 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, backend).await + { + return Some(Self::Primary(renderer)); + } + + 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, + 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..6fff4c74 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,8 +187,16 @@ where load_font(font).expect("Font must be valid"); } - let mut renderer = - Renderer::new(default_font, settings.default_text_size); + let mut renderer = { + let backend = env::var("ICED_TEST_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, @@ -455,6 +464,7 @@ where physical_size, f64::from(scale_factor), ), + renderer: self.renderer.name(), }) } @@ -470,6 +480,7 @@ where #[derive(Debug, Clone)] pub struct Snapshot { screenshot: window::Screenshot, + renderer: String, } impl Snapshot { @@ -479,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)?; @@ -520,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(); @@ -541,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. @@ -633,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/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 { diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index f34f7e76..5ea3cbe9 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -357,8 +357,22 @@ 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, + 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( diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index f14ad34e..321a003f 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -113,8 +113,15 @@ impl crate::graphics::Compositor for Compositor { surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { - present(renderer, surface, viewport, background_color) + present( + renderer, + surface, + viewport, + background_color, + on_pre_present, + ) } fn screenshot( @@ -143,6 +150,7 @@ pub fn present( surface: &mut Surface, viewport: &Viewport, background_color: Color, + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { let physical_size = viewport.physical_size(); @@ -202,6 +210,7 @@ pub fn present( background_color, ); + on_pre_present(); buffer.present().map_err(|_| compositor::SurfaceError::Lost) } 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..51d2acef 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, @@ -329,7 +339,7 @@ impl Pipeline { } } - 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 9e48a0f4..cfb6a64f 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -60,8 +60,9 @@ pub use settings::Settings; #[cfg(feature = "geometry")] pub use geometry::Geometry; +use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; use crate::graphics::Viewport; use crate::graphics::text::{Editor, Paragraph}; @@ -72,23 +73,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 { @@ -97,50 +105,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.trim(); + self.triangle.trim(); + self.text.trim(); + + // TODO: Move to runtime! + self.engine.text_pipeline.trim(); + + #[cfg(any(feature = "svg", feature = "image"))] + { + self.image.trim(); + 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, - ) { - self.prepare(engine, device, queue, format, encoder, viewport); - self.render(engine, encoder, frame, clear_color, viewport); + ) -> wgpu::SubmissionIndex { + let encoder = self.draw(clear_color, frame, viewport); - self.triangle_storage.trim(); - self.text_storage.trim(); + self.staging_belt.finish(); + let submission = self.engine.queue.submit([encoder.finish()]); + self.staging_belt.recall(); + submission + } - #[cfg(any(feature = "svg", feature = "image"))] - self.image_cache.borrow_mut().trim(); + /// 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(), @@ -156,10 +320,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, @@ -167,11 +332,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(), @@ -179,12 +344,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, ); @@ -193,10 +364,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(), @@ -205,12 +377,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), @@ -221,7 +393,6 @@ impl Renderer { fn render( &mut self, - engine: &mut Engine, encoder: &mut wgpu::CommandEncoder, frame: &wgpu::TextureView, clear_color: Option, @@ -288,7 +459,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, @@ -301,10 +473,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, @@ -334,6 +506,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) @@ -341,7 +519,7 @@ impl Renderer { { instance.primitive.render( encoder, - &engine.primitive_storage, + &primitive_storage, frame, &clip_bounds, ); @@ -370,7 +548,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, @@ -381,9 +560,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, @@ -583,3 +762,76 @@ 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, + 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), + 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 name(&self) -> String { + "wgpu".to_owned() + } + + 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/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)); } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index de432d2f..4c00945c 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 trim(&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..2a4c6161 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,14 +268,12 @@ impl Viewport { } } +#[derive(Clone)] #[allow(missing_debug_implementations)] pub struct Pipeline { - state: cryoglyph::Cache, format: wgpu::TextureFormat, - atlas: cryoglyph::TextAtlas, - renderers: Vec, - prepare_layer: usize, - cache: BufferCache, + cache: cryoglyph::Cache, + atlas: Arc>, } impl Pipeline { @@ -288,32 +282,53 @@ impl Pipeline { queue: &wgpu::Queue, format: wgpu::TextureFormat, ) -> Self { - let state = cryoglyph::Cache::new(device); + let cache = cryoglyph::Cache::new(device); let atlas = cryoglyph::TextAtlas::with_color_mode( - device, queue, &state, format, COLOR_MODE, + device, queue, &cache, format, COLOR_MODE, ); Pipeline { - state, format, - renderers: Vec::new(), - atlas, - prepare_layer: 0, - cache: BufferCache::new(), + 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, + prepare_layer: usize, + cache: BufferCache, + storage: Storage, +} + +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 +337,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 +351,7 @@ impl Pipeline { &viewport.0, encoder, renderer, - &mut self.atlas, + &mut atlas, &mut self.cache, text, layer_bounds * layer_transformation, @@ -358,13 +373,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 +391,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 +414,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,13 +433,9 @@ 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(); + pub fn trim(&mut self) { self.cache.trim(); + self.storage.trim(); self.prepare_layer = 0; } diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index a2b976ea..6d0b0322 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, ); @@ -292,48 +297,55 @@ impl Pipeline { layer_count } - pub fn end_frame(&mut self) { + pub fn trim(&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..8172575d 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::Clear(wgpu::Color::TRANSPARENT), + 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 3043ab4c..a29c1ce1 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,38 +211,27 @@ 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, background_color: Color, + on_pre_present: impl FnOnce(), ) -> 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, viewport, ); - let _ = compositor.engine.submit(&compositor.queue, encoder); - // Present the frame + on_pre_present(); frame.present(); Ok(()) @@ -301,8 +286,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, ) @@ -333,7 +317,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, @@ -362,8 +346,15 @@ impl graphics::Compositor for Compositor { surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, + on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { - present(self, renderer, surface, viewport, background_color) + present( + renderer, + surface, + viewport, + background_color, + on_pre_present, + ) } fn screenshot( @@ -372,134 +363,6 @@ impl graphics::Compositor for Compositor { viewport: &Viewport, background_color: Color, ) -> Vec { - screenshot(self, renderer, viewport, background_color) - } -} - -/// 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, -) -> 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, - ); - - 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/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; 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(), 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; diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 33a58c63..e260dd20 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -819,6 +819,7 @@ async fn run_instance

( &mut window.surface, window.state.viewport(), window.state.background_color(), + || window.raw.pre_present_notify(), ) { Ok(()) => { present_span.finish();