diff options
Diffstat (limited to 'wgpu')
33 files changed, 3149 insertions, 2307 deletions
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 4a0d89f0..30545fa2 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -10,6 +10,9 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[lints] +workspace = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true @@ -32,6 +35,8 @@ glyphon.workspace = true guillotiere.workspace = true log.workspace = true once_cell.workspace = true +rustc-hash.workspace = true +thiserror.workspace = true wgpu.workspace = true lyon.workspace = true @@ -39,6 +44,3 @@ lyon.optional = true resvg.workspace = true resvg.optional = true - -tracing.workspace = true -tracing.optional = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs deleted file mode 100644 index 09ddbe4d..00000000 --- a/wgpu/src/backend.rs +++ /dev/null @@ -1,399 +0,0 @@ -use crate::core::{Color, Size, Transformation}; -use crate::graphics::backend; -use crate::graphics::color; -use crate::graphics::Viewport; -use crate::primitive::pipeline; -use crate::primitive::{self, Primitive}; -use crate::quad; -use crate::text; -use crate::triangle; -use crate::{Layer, Settings}; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -#[cfg(any(feature = "image", feature = "svg"))] -use crate::image; - -use std::borrow::Cow; - -/// A [`wgpu`] graphics backend for [`iced`]. -/// -/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -/// [`iced`]: https://github.com/iced-rs/iced -#[allow(missing_debug_implementations)] -pub struct Backend { - quad_pipeline: quad::Pipeline, - text_pipeline: text::Pipeline, - triangle_pipeline: triangle::Pipeline, - pipeline_storage: pipeline::Storage, - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline: image::Pipeline, -} - -impl Backend { - /// Creates a new [`Backend`]. - pub fn new( - _adapter: &wgpu::Adapter, - device: &wgpu::Device, - queue: &wgpu::Queue, - settings: Settings, - format: wgpu::TextureFormat, - ) -> Self { - let text_pipeline = text::Pipeline::new(device, queue, format); - let quad_pipeline = quad::Pipeline::new(device, format); - let triangle_pipeline = - triangle::Pipeline::new(device, format, settings.antialiasing); - - #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = { - let backend = _adapter.get_info().backend; - - image::Pipeline::new(device, format, backend) - }; - - Self { - quad_pipeline, - text_pipeline, - triangle_pipeline, - pipeline_storage: pipeline::Storage::default(), - - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, - } - } - - /// Draws the provided primitives in the given `TextureView`. - /// - /// The text provided as overlay will be rendered on top of the primitives. - /// This is useful for rendering debug information. - pub fn present<T: AsRef<str>>( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - clear_color: Option<Color>, - format: wgpu::TextureFormat, - frame: &wgpu::TextureView, - primitives: &[Primitive], - viewport: &Viewport, - overlay_text: &[T], - ) { - log::debug!("Drawing"); - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Backend", "PRESENT").entered(); - - let target_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; - let transformation = viewport.projection(); - - let mut layers = Layer::generate(primitives, viewport); - - if !overlay_text.is_empty() { - layers.push(Layer::overlay(overlay_text, viewport)); - } - - self.prepare( - device, - queue, - format, - encoder, - scale_factor, - target_size, - transformation, - &layers, - ); - - self.render( - device, - encoder, - frame, - clear_color, - scale_factor, - target_size, - &layers, - ); - - self.quad_pipeline.end_frame(); - self.text_pipeline.end_frame(); - self.triangle_pipeline.end_frame(); - - #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.end_frame(); - } - - fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - _encoder: &mut wgpu::CommandEncoder, - scale_factor: f32, - target_size: Size<u32>, - transformation: Transformation, - layers: &[Layer<'_>], - ) { - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.prepare( - device, - queue, - &layer.quads, - transformation, - scale_factor, - ); - } - - if !layer.meshes.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.triangle_pipeline.prepare( - device, - queue, - &layer.meshes, - scaled, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.image_pipeline.prepare( - device, - queue, - _encoder, - &layer.images, - scaled, - scale_factor, - ); - } - } - - if !layer.text.is_empty() { - self.text_pipeline.prepare( - device, - queue, - &layer.text, - layer.bounds, - scale_factor, - target_size, - ); - } - - if !layer.pipelines.is_empty() { - for pipeline in &layer.pipelines { - pipeline.primitive.prepare( - format, - device, - queue, - pipeline.bounds, - target_size, - scale_factor, - &mut self.pipeline_storage, - ); - } - } - } - } - - fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - clear_color: Option<Color>, - scale_factor: f32, - target_size: Size<u32>, - layers: &[Layer<'_>], - ) { - use std::mem::ManuallyDrop; - - let mut quad_layer = 0; - let mut triangle_layer = 0; - #[cfg(any(feature = "image", feature = "svg"))] - let mut image_layer = 0; - let mut text_layer = 0; - - let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: match clear_color { - Some(background_color) => wgpu::LoadOp::Clear({ - let [r, g, b, a] = - color::pack(background_color).components(); - - wgpu::Color { - r: f64::from(r), - g: f64::from(g), - b: f64::from(b), - a: f64::from(a), - } - }), - None => wgpu::LoadOp::Load, - }, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.render( - quad_layer, - bounds, - &layer.quads, - &mut render_pass, - ); - - quad_layer += 1; - } - - if !layer.meshes.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - self.triangle_pipeline.render( - device, - encoder, - target, - triangle_layer, - target_size, - &layer.meshes, - scale_factor, - ); - - triangle_layer += 1; - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - self.image_pipeline.render( - image_layer, - bounds, - &mut render_pass, - ); - - image_layer += 1; - } - } - - if !layer.text.is_empty() { - self.text_pipeline - .render(text_layer, bounds, &mut render_pass); - - text_layer += 1; - } - - if !layer.pipelines.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - for pipeline in &layer.pipelines { - let viewport = (pipeline.viewport * scale_factor).snap(); - - if viewport.width < 1 || viewport.height < 1 { - continue; - } - - pipeline.primitive.render( - &self.pipeline_storage, - target, - target_size, - viewport, - encoder, - ); - } - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - } - - let _ = ManuallyDrop::into_inner(render_pass); - } -} - -impl crate::graphics::Backend for Backend { - type Primitive = primitive::Custom; -} - -impl backend::Text for Backend { - fn load_font(&mut self, font: Cow<'static, [u8]>) { - self.text_pipeline.load_font(font); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { - self.image_pipeline.dimensions(handle) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> Size<u32> { - self.image_pipeline.viewport_dimensions(handle) - } -} diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs index ef00c58f..463ea24a 100644 --- a/wgpu/src/buffer.rs +++ b/wgpu/src/buffer.rs @@ -1,6 +1,13 @@ use std::marker::PhantomData; +use std::num::NonZeroU64; use std::ops::RangeBounds; +pub const MAX_WRITE_SIZE: usize = 100 * 1024; + +#[allow(unsafe_code)] +const MAX_WRITE_SIZE_U64: NonZeroU64 = + unsafe { NonZeroU64::new_unchecked(MAX_WRITE_SIZE as u64) }; + #[derive(Debug)] pub struct Buffer<T> { label: &'static str, @@ -61,12 +68,46 @@ impl<T: bytemuck::Pod> Buffer<T> { /// Returns the size of the written bytes. pub fn write( &mut self, - queue: &wgpu::Queue, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, offset: usize, contents: &[T], ) -> usize { let bytes: &[u8] = bytemuck::cast_slice(contents); - queue.write_buffer(&self.raw, offset as u64, bytes); + let mut bytes_written = 0; + + // Split write into multiple chunks if necessary + while bytes_written + MAX_WRITE_SIZE < bytes.len() { + belt.write_buffer( + encoder, + &self.raw, + (offset + bytes_written) as u64, + MAX_WRITE_SIZE_U64, + device, + ) + .copy_from_slice( + &bytes[bytes_written..bytes_written + MAX_WRITE_SIZE], + ); + + bytes_written += MAX_WRITE_SIZE; + } + + // There will always be some bytes left, since the previous + // loop guarantees `bytes_written < bytes.len()` + let bytes_left = ((bytes.len() - bytes_written) as u64) + .try_into() + .expect("non-empty write"); + + // Write them + belt.write_buffer( + encoder, + &self.raw, + (offset + bytes_written) as u64, + bytes_left, + device, + ) + .copy_from_slice(&bytes[bytes_written..]); self.offsets.push(offset as u64); diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 890f3f89..9d593d9c 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -1,5 +1,7 @@ use std::borrow::Cow; +use wgpu::util::DeviceExt; + pub fn convert( device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, @@ -15,28 +17,58 @@ pub fn convert( ..wgpu::SamplerDescriptor::default() }); - //sampler in 0 - let sampler_layout = + #[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] + #[repr(C)] + struct Ratio { + u: f32, + v: f32, + } + + let ratio = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("iced-wgpu::triangle::msaa ratio"), + contents: bytemuck::bytes_of(&Ratio { u: 1.0, v: 1.0 }), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }); + + let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu.offscreen.blit.sampler_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::NonFiltering, - ), - count: None, - }], + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], }); - let sampler_bind_group = + let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu.offscreen.sampler.bind_group"), - layout: &sampler_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&sampler), - }], + 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 = @@ -59,7 +91,7 @@ pub fn convert( let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), - bind_group_layouts: &[&sampler_layout, &texture_layout], + bind_group_layouts: &[&constant_layout, &texture_layout], push_constant_ranges: &[], }); @@ -152,7 +184,7 @@ pub fn convert( }); pass.set_pipeline(&pipeline); - pass.set_bind_group(0, &sampler_bind_group, &[]); + pass.set_bind_group(0, &constant_bind_group, &[]); pass.set_bind_group(1, &texture_bind_group, &[]); pass.draw(0..6, 0..1); diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs new file mode 100644 index 00000000..782fd58c --- /dev/null +++ b/wgpu/src/engine.rs @@ -0,0 +1,87 @@ +use crate::buffer; +use crate::graphics::Antialiasing; +use crate::primitive; +use crate::quad; +use crate::text; +use crate::triangle; + +#[allow(missing_debug_implementations)] +pub struct Engine { + pub(crate) staging_belt: wgpu::util::StagingBelt, + pub(crate) format: wgpu::TextureFormat, + + pub(crate) quad_pipeline: quad::Pipeline, + pub(crate) text_pipeline: text::Pipeline, + pub(crate) triangle_pipeline: triangle::Pipeline, + #[cfg(any(feature = "image", feature = "svg"))] + pub(crate) image_pipeline: crate::image::Pipeline, + pub(crate) primitive_storage: primitive::Storage, +} + +impl Engine { + pub fn new( + _adapter: &wgpu::Adapter, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + antialiasing: Option<Antialiasing>, // 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, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, + + primitive_storage: primitive::Storage::default(), + } + } + + #[cfg(any(feature = "image", feature = "svg"))] + pub fn create_image_cache( + &self, + device: &wgpu::Device, + ) -> 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/geometry.rs b/wgpu/src/geometry.rs index f4e0fbda..f6213e1d 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -3,215 +3,136 @@ use crate::core::text::LineHeight; use crate::core::{ Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, }; +use crate::graphics::cache::{self, Cached}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ - LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, + self, LineCap, LineDash, LineJoin, Path, Stroke, Style, }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::primitive::{self, Primitive}; +use crate::graphics::{self, Text}; +use crate::text; +use crate::triangle; use lyon::geom::euclid; use lyon::tessellation; + use std::borrow::Cow; -/// A frame for drawing some geometry. -#[allow(missing_debug_implementations)] -pub struct Frame { - size: Size, - buffers: BufferStack, - primitives: Vec<Primitive>, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, +#[derive(Debug)] +pub enum Geometry { + Live { meshes: Vec<Mesh>, text: Vec<Text> }, + Cached(Cache), } -enum Buffer { - Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>), - Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>), +#[derive(Debug, Clone)] +pub struct Cache { + pub meshes: Option<triangle::Cache>, + pub text: Option<text::Cache>, } -struct BufferStack { - stack: Vec<Buffer>, -} +impl Cached for Geometry { + type Cache = Cache; -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } + fn load(cache: &Self::Cache) -> Self { + Geometry::Cached(cache.clone()) } - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(_) => match self.stack.last() { - Some(Buffer::Gradient(_)) => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - )); - } - }, - } - - self.stack.last_mut().unwrap() - } + fn cache( + self, + group: cache::Group, + previous: Option<Self::Cache>, + ) -> Self::Cache { + match self { + Self::Live { meshes, text } => { + if let Some(mut previous) = previous { + if let Some(cache) = &mut previous.meshes { + cache.update(meshes); + } else { + previous.meshes = triangle::Cache::new(meshes); + } - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box<dyn tessellation::FillGeometryBuilder + 'a> { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color::pack(*color)), - )) - } - (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - GradientVertex2DBuilder { - gradient: gradient.pack(), - }, - )) - } - _ => unreachable!(), - } - } + if let Some(cache) = &mut previous.text { + cache.update(text); + } else { + previous.text = text::Cache::new(group, text); + } - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color::pack(*color)), - )) - } - (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - GradientVertex2DBuilder { - gradient: gradient.pack(), - }, - )) + previous + } else { + Cache { + meshes: triangle::Cache::new(meshes), + text: text::Cache::new(group, text), + } + } } - _ => unreachable!(), + Self::Cached(cache) => cache, } } } -#[derive(Debug)] -struct Transforms { - previous: Vec<Transform>, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform(lyon::math::Transform); - -impl Transform { - fn is_identity(&self) -> bool { - self.0 == lyon::math::Transform::identity() - } - - fn is_scale_translation(&self) -> bool { - self.0.m12.abs() < 2.0 * f32::EPSILON - && self.0.m21.abs() < 2.0 * f32::EPSILON - } - - fn scale(&self) -> (f32, f32) { - (self.0.m11, self.0.m22) - } - - fn transform_point(&self, point: Point) -> Point { - let transformed = self - .0 - .transform_point(euclid::Point2D::new(point.x, point.y)); - - Point { - x: transformed.x, - y: transformed.y, - } - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - match &mut gradient { - Gradient::Linear(linear) => { - linear.start = self.transform_point(linear.start); - linear.end = self.transform_point(linear.end); - } - } - - gradient - } +/// A frame for drawing some geometry. +#[allow(missing_debug_implementations)] +pub struct Frame { + clip_bounds: Rectangle, + buffers: BufferStack, + meshes: Vec<Mesh>, + text: Vec<Text>, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, } impl Frame { - /// Creates a new empty [`Frame`] with the given dimensions. - /// - /// The default coordinate system of a [`Frame`] has its origin at the - /// top-left corner of its bounds. + /// Creates a new [`Frame`] with the given [`Size`]. pub fn new(size: Size) -> Frame { + Self::with_clip(Rectangle::with_size(size)) + } + + /// Creates a new [`Frame`] with the given clip bounds. + pub fn with_clip(bounds: Rectangle) -> Frame { Frame { - size, + clip_bounds: bounds, buffers: BufferStack::new(), - primitives: Vec::new(), + meshes: Vec::new(), + text: Vec::new(), transforms: Transforms { previous: Vec::new(), - current: Transform(lyon::math::Transform::identity()), + current: Transform(lyon::math::Transform::translation( + bounds.x, bounds.y, + )), }, fill_tessellator: tessellation::FillTessellator::new(), stroke_tessellator: tessellation::StrokeTessellator::new(), } } +} + +impl geometry::frame::Backend for Frame { + type Geometry = Geometry; - /// Returns the width of the [`Frame`]. #[inline] - pub fn width(&self) -> f32 { - self.size.width + fn width(&self) -> f32 { + self.clip_bounds.width } - /// Returns the height of the [`Frame`]. #[inline] - pub fn height(&self) -> f32 { - self.size.height + fn height(&self) -> f32 { + self.clip_bounds.height } - /// Returns the dimensions of the [`Frame`]. #[inline] - pub fn size(&self) -> Size { - self.size + fn size(&self) -> Size { + self.clip_bounds.size() } - /// Returns the coordinate of the center of the [`Frame`]. #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) + fn center(&self) -> Point { + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) } - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { let Fill { style, rule } = fill.into(); let mut buffer = self @@ -239,9 +160,7 @@ impl Frame { .expect("Tessellate path."); } - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( + fn fill_rectangle( &mut self, top_left: Point, size: Size, @@ -276,9 +195,7 @@ impl Frame { .expect("Fill rectangle"); } - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { let stroke = stroke.into(); let mut buffer = self @@ -315,20 +232,7 @@ impl Frame { .expect("Stroke path"); } - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a `Canvas`. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - pub fn fill_text(&mut self, text: impl Into<Text>) { + fn fill_text(&mut self, text: impl Into<geometry::Text>) { let text = text.into(); let (scale_x, scale_y) = self.transforms.current.scale(); @@ -366,105 +270,25 @@ impl Frame { height: f32::INFINITY, }; - // TODO: Honor layering! - self.primitives.push(Primitive::Text { + self.text.push(graphics::Text::Cached { content: text.content, bounds, color: text.color, size, - line_height, + line_height: line_height.to_absolute(size), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), + clip_bounds: self.clip_bounds, }); } else { text.draw_with(|path, color| self.fill(&path, color)); } } - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save<R>(&mut self, f: impl FnOnce(&mut Frame) -> R) -> R { - self.push_transform(); - - let result = f(self); - - self.pop_transform(); - - result - } - - /// Pushes the current transform in the transform stack. - pub fn push_transform(&mut self) { - self.transforms.previous.push(self.transforms.current); - } - - /// Pops a transform from the transform stack and sets it as the current transform. - pub fn pop_transform(&mut self) { - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. #[inline] - pub fn with_clip<R>( - &mut self, - region: Rectangle, - f: impl FnOnce(&mut Frame) -> R, - ) -> R { - let mut frame = Frame::new(region.size()); - - let result = f(&mut frame); - - let origin = Point::new(region.x, region.y); - - self.clip(frame, origin); - - result - } - - /// Draws the clipped contents of the given [`Frame`] with origin at the given [`Point`]. - pub fn clip(&mut self, frame: Frame, at: Point) { - let size = frame.size(); - let primitives = frame.into_primitives(); - let transformation = Transformation::translate(at.x, at.y); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Transform { - transformation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Transform { - transformation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { + fn translate(&mut self, translation: Vector) { self.transforms.current.0 = self.transforms .current @@ -475,9 +299,8 @@ impl Frame { )); } - /// Applies a rotation in radians to the current transform of the [`Frame`]. #[inline] - pub fn rotate(&mut self, angle: impl Into<Radians>) { + fn rotate(&mut self, angle: impl Into<Radians>) { self.transforms.current.0 = self .transforms .current @@ -485,66 +308,217 @@ impl Frame { .pre_rotate(lyon::math::Angle::radians(angle.into().0)); } - /// Applies a uniform scaling to the current transform of the [`Frame`]. #[inline] - pub fn scale(&mut self, scale: impl Into<f32>) { + fn scale(&mut self, scale: impl Into<f32>) { let scale = scale.into(); self.scale_nonuniform(Vector { x: scale, y: scale }); } - /// Applies a non-uniform scaling to the current transform of the [`Frame`]. #[inline] - pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { + fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { let scale = scale.into(); self.transforms.current.0 = self.transforms.current.0.pre_scale(scale.x, scale.y); } - /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. - pub fn into_primitive(self) -> Primitive { - Primitive::Group { - primitives: self.into_primitives(), + fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + fn draft(&mut self, clip_bounds: Rectangle) -> Frame { + Frame::with_clip(clip_bounds) + } + + fn paste(&mut self, frame: Frame, _at: Point) { + self.meshes + .extend(frame.buffers.into_meshes(frame.clip_bounds)); + + self.text.extend(frame.text); + } + + fn into_geometry(mut self) -> Self::Geometry { + self.meshes + .extend(self.buffers.into_meshes(self.clip_bounds)); + + Geometry::Live { + meshes: self.meshes, + text: self.text, } } +} - fn into_primitives(mut self) -> Vec<Primitive> { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } +enum Buffer { + Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>), + Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>), +} + +struct BufferStack { + stack: Vec<Buffer>, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); } - Buffer::Gradient(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } + }, + Style::Gradient(_) => match self.stack.last() { + Some(Buffer::Gradient(_)) => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + )); } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box<dyn tessellation::FillGeometryBuilder + 'a> { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color::pack(*color)), + )) + } + (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + GradientVertex2DBuilder { + gradient: gradient.pack(), + }, + )) } + _ => unreachable!(), } + } - self.primitives + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color::pack(*color)), + )) + } + (Style::Gradient(gradient), Buffer::Gradient(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + GradientVertex2DBuilder { + gradient: gradient.pack(), + }, + )) + } + _ => unreachable!(), + } + } + + fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator<Item = Mesh> { + self.stack + .into_iter() + .filter_map(move |buffer| match buffer { + Buffer::Solid(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + Buffer::Gradient(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + _ => None, + }) } } +#[derive(Debug)] +struct Transforms { + previous: Vec<Transform>, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform(lyon::math::Transform); + +impl Transform { + fn is_identity(&self) -> bool { + self.0 == lyon::math::Transform::identity() + } + + fn is_scale_translation(&self) -> bool { + self.0.m12.abs() < 2.0 * f32::EPSILON + && self.0.m21.abs() < 2.0 * f32::EPSILON + } + + fn scale(&self) -> (f32, f32) { + (self.0.m11, self.0.m22) + } + + fn transform_point(&self, point: Point) -> Point { + let transformed = self + .0 + .transform_point(euclid::Point2D::new(point.x, point.y)); + + Point { + x: transformed.x, + y: transformed.y, + } + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + match &mut gradient { + Gradient::Linear(linear) => { + linear.start = self.transform_point(linear.start); + linear.end = self.transform_point(linear.end); + } + } + + gradient + } +} struct GradientVertex2DBuilder { gradient: gradient::Packed, } @@ -651,7 +625,9 @@ pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { let mut draw_line = false; walk_along_path( - path.raw().iter().flattened(0.01), + path.raw().iter().flattened( + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + ), 0.0, lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, &mut RepeatedPattern { diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index ea36e06d..a1ec0d7b 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -15,15 +15,23 @@ pub const SIZE: u32 = 2048; use crate::core::Size; use crate::graphics::color; +use std::sync::Arc; + #[derive(Debug)] pub struct Atlas { texture: wgpu::Texture, texture_view: wgpu::TextureView, + texture_bind_group: wgpu::BindGroup, + texture_layout: Arc<wgpu::BindGroupLayout>, layers: Vec<Layer>, } impl Atlas { - pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self { + pub fn new( + device: &wgpu::Device, + backend: wgpu::Backend, + texture_layout: Arc<wgpu::BindGroupLayout>, + ) -> Self { let layers = match backend { // On the GL backend we start with 2 layers, to help wgpu figure // out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D` @@ -60,15 +68,27 @@ impl Atlas { ..Default::default() }); + let texture_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image texture atlas bind group"), + layout: &texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&texture_view), + }], + }); + Atlas { texture, texture_view, + texture_bind_group, + texture_layout, layers, } } - pub fn view(&self) -> &wgpu::TextureView { - &self.texture_view + pub fn bind_group(&self) -> &wgpu::BindGroup { + &self.texture_bind_group } pub fn layer_count(&self) -> usize { @@ -94,7 +114,7 @@ impl Atlas { entry }; - log::info!("Allocated atlas entry: {entry:?}"); + log::debug!("Allocated atlas entry: {entry:?}"); // It is a webgpu requirement that: // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 @@ -147,13 +167,20 @@ impl Atlas { } } - log::info!("Current atlas: {self:?}"); + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "Atlas layers: {} (busy: {}, allocations: {})", + self.layer_count(), + self.layers.iter().filter(|layer| !layer.is_empty()).count(), + self.layers.iter().map(Layer::allocations).sum::<usize>(), + ); + } Some(entry) } pub fn remove(&mut self, entry: &Entry) { - log::info!("Removing atlas entry: {entry:?}"); + log::debug!("Removing atlas entry: {entry:?}"); match entry { Entry::Contiguous(allocation) => { @@ -266,7 +293,7 @@ impl Atlas { } fn deallocate(&mut self, allocation: &Allocation) { - log::info!("Deallocating atlas: {allocation:?}"); + log::debug!("Deallocating atlas: {allocation:?}"); match allocation { Allocation::Full { layer } => { @@ -414,5 +441,17 @@ impl Atlas { dimension: Some(wgpu::TextureViewDimension::D2Array), ..Default::default() }); + + self.texture_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image texture atlas bind group"), + layout: &self.texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.texture_view, + ), + }], + }); } } diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs index 204a5c26..a51ac1f5 100644 --- a/wgpu/src/image/atlas/allocator.rs +++ b/wgpu/src/image/atlas/allocator.rs @@ -33,6 +33,10 @@ impl Allocator { pub fn is_empty(&self) -> bool { self.allocations == 0 } + + pub fn allocations(&self) -> usize { + self.allocations + } } pub struct Region { diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs index cf089601..fd6788d9 100644 --- a/wgpu/src/image/atlas/layer.rs +++ b/wgpu/src/image/atlas/layer.rs @@ -11,4 +11,12 @@ impl Layer { pub fn is_empty(&self) -> bool { matches!(self, Layer::Empty) } + + pub fn allocations(&self) -> usize { + match self { + Layer::Empty => 0, + Layer::Busy(allocator) => allocator.allocations(), + Layer::Full => 1, + } + } } diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs new file mode 100644 index 00000000..94f7071d --- /dev/null +++ b/wgpu/src/image/cache.rs @@ -0,0 +1,86 @@ +use crate::core::{self, Size}; +use crate::image::atlas::{self, Atlas}; + +use std::sync::Arc; + +#[derive(Debug)] +pub struct Cache { + atlas: Atlas, + #[cfg(feature = "image")] + raster: crate::image::raster::Cache, + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache, +} + +impl Cache { + pub fn new( + device: &wgpu::Device, + backend: wgpu::Backend, + layout: Arc<wgpu::BindGroupLayout>, + ) -> Self { + Self { + atlas: Atlas::new(device, backend, layout), + #[cfg(feature = "image")] + raster: crate::image::raster::Cache::default(), + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache::default(), + } + } + + pub fn bind_group(&self) -> &wgpu::BindGroup { + self.atlas.bind_group() + } + + pub fn layer_count(&self) -> usize { + self.atlas.layer_count() + } + + #[cfg(feature = "image")] + pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> { + self.raster.load(handle).dimensions() + } + + #[cfg(feature = "svg")] + pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> { + self.vector.load(handle).viewport_dimensions() + } + + #[cfg(feature = "image")] + pub fn upload_raster( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::image::Handle, + ) -> Option<&atlas::Entry> { + self.raster.upload(device, encoder, handle, &mut self.atlas) + } + + #[cfg(feature = "svg")] + pub fn upload_vector( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::svg::Handle, + color: Option<core::Color>, + size: [f32; 2], + scale: f32, + ) -> Option<&atlas::Entry> { + self.vector.upload( + device, + encoder, + handle, + color, + size, + scale, + &mut self.atlas, + ) + } + + pub fn trim(&mut self) { + #[cfg(feature = "image")] + self.raster.trim(&mut self.atlas); + + #[cfg(feature = "svg")] + self.vector.trim(&mut self.atlas); + } +} diff --git a/wgpu/src/image.rs b/wgpu/src/image/mod.rs index c8e4a4c2..daa2fe16 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image/mod.rs @@ -1,3 +1,6 @@ +pub(crate) mod cache; +pub(crate) use cache::Cache; + mod atlas; #[cfg(feature = "image")] @@ -6,175 +9,30 @@ mod raster; #[cfg(feature = "svg")] mod vector; -use atlas::Atlas; - use crate::core::{Rectangle, Size, Transformation}; -use crate::layer; use crate::Buffer; -use std::cell::RefCell; -use std::mem; - use bytemuck::{Pod, Zeroable}; -#[cfg(feature = "image")] -use crate::core::image; +use std::mem; +use std::sync::Arc; -#[cfg(feature = "svg")] -use crate::core::svg; +pub use crate::graphics::Image; -#[cfg(feature = "tracing")] -use tracing::info_span; +pub type Batch = Vec<Image>; #[derive(Debug)] pub struct Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell<raster::Cache>, - #[cfg(feature = "svg")] - vector_cache: RefCell<vector::Cache>, - pipeline: wgpu::RenderPipeline, + backend: wgpu::Backend, nearest_sampler: wgpu::Sampler, linear_sampler: wgpu::Sampler, - texture: wgpu::BindGroup, - texture_version: usize, - texture_atlas: Atlas, - texture_layout: wgpu::BindGroupLayout, + texture_layout: Arc<wgpu::BindGroupLayout>, constant_layout: wgpu::BindGroupLayout, - layers: Vec<Layer>, prepare_layer: usize, } -#[derive(Debug)] -struct Layer { - uniforms: wgpu::Buffer, - nearest: Data, - linear: Data, -} - -impl Layer { - fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - nearest_sampler: &wgpu::Sampler, - linear_sampler: &wgpu::Sampler, - ) -> Self { - let uniforms = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::image uniforms buffer"), - size: mem::size_of::<Uniforms>() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let nearest = - Data::new(device, constant_layout, nearest_sampler, &uniforms); - - let linear = - Data::new(device, constant_layout, linear_sampler, &uniforms); - - Self { - uniforms, - nearest, - linear, - } - } - - fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - nearest_instances: &[Instance], - linear_instances: &[Instance], - transformation: Transformation, - ) { - queue.write_buffer( - &self.uniforms, - 0, - bytemuck::bytes_of(&Uniforms { - transform: transformation.into(), - }), - ); - - self.nearest.upload(device, queue, nearest_instances); - self.linear.upload(device, queue, linear_instances); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - self.nearest.render(render_pass); - self.linear.render(render_pass); - } -} - -#[derive(Debug)] -struct Data { - constants: wgpu::BindGroup, - instances: Buffer<Instance>, - instance_count: usize, -} - -impl Data { - pub fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - sampler: &wgpu::Sampler, - uniforms: &wgpu::Buffer, - ) -> Self { - let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image constants bind group"), - layout: constant_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: uniforms, - offset: 0, - size: None, - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(sampler), - }, - ], - }); - - let instances = Buffer::new( - device, - "iced_wgpu::image instance buffer", - Instance::INITIAL, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - Self { - constants, - instances, - instance_count: 0, - } - } - - fn upload( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - instances: &[Instance], - ) { - let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(queue, 0, instances); - - self.instance_count = instances.len(); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_vertex_buffer(0, self.instances.slice(..)); - - render_pass.draw(0..6, 0..self.instance_count as u32); - } -} - impl Pipeline { pub fn new( device: &wgpu::Device, @@ -257,9 +115,9 @@ impl Pipeline { label: Some("iced_wgpu image shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( concat!( - include_str!("shader/vertex.wgsl"), + include_str!("../shader/vertex.wgsl"), "\n", - include_str!("shader/image.wgsl"), + include_str!("../shader/image.wgsl"), ), )), }); @@ -277,14 +135,20 @@ impl Pipeline { attributes: &wgpu::vertex_attr_array!( // Position 0 => Float32x2, - // Scale + // Center 1 => Float32x2, - // Atlas position + // Scale 2 => Float32x2, + // Rotation + 3 => Float32, + // Opacity + 4 => Float32, + // Atlas position + 5 => Float32x2, // Atlas scale - 3 => Float32x2, + 6 => Float32x2, // Layer - 4 => Sint32, + 7 => Sint32, ), }], }, @@ -322,137 +186,95 @@ impl Pipeline { multiview: None, }); - let texture_atlas = Atlas::new(device, backend); - - let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - texture_atlas.view(), - ), - }], - }); - Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell::new(raster::Cache::default()), - - #[cfg(feature = "svg")] - vector_cache: RefCell::new(vector::Cache::default()), - pipeline, + backend, nearest_sampler, linear_sampler, - texture, - texture_version: texture_atlas.layer_count(), - texture_atlas, - texture_layout, + texture_layout: Arc::new(texture_layout), constant_layout, - layers: Vec::new(), prepare_layer: 0, } } - #[cfg(feature = "image")] - pub fn dimensions(&self, handle: &image::Handle) -> Size<u32> { - let mut cache = self.raster_cache.borrow_mut(); - let memory = cache.load(handle); - - memory.dimensions() - } - - #[cfg(feature = "svg")] - pub fn viewport_dimensions(&self, handle: &svg::Handle) -> Size<u32> { - let mut cache = self.vector_cache.borrow_mut(); - let svg = cache.load(handle); - - svg.viewport_dimensions() + pub fn create_cache(&self, device: &wgpu::Device) -> Cache { + Cache::new(device, self.backend, self.texture_layout.clone()) } pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, - images: &[layer::Image], + belt: &mut wgpu::util::StagingBelt, + cache: &mut Cache, + images: &Batch, transformation: Transformation, - _scale: f32, + scale: f32, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "PREPARE").entered(); - - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "DRAW").entered(); + let transformation = transformation * Transformation::scale(scale); let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); let linear_instances: &mut Vec<Instance> = &mut Vec::new(); - #[cfg(feature = "image")] - let mut raster_cache = self.raster_cache.borrow_mut(); - - #[cfg(feature = "svg")] - let mut vector_cache = self.vector_cache.borrow_mut(); - for image in images { match &image { #[cfg(feature = "image")] - layer::Image::Raster { + Image::Raster { handle, filter_method, bounds, + rotation, + opacity, } => { - if let Some(atlas_entry) = raster_cache.upload( - device, - encoder, - handle, - &mut self.texture_atlas, - ) { + if let Some(atlas_entry) = + cache.upload_raster(device, encoder, handle) + { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], + f32::from(*rotation), + *opacity, atlas_entry, match filter_method { - image::FilterMethod::Nearest => { + crate::core::image::FilterMethod::Nearest => { nearest_instances } - image::FilterMethod::Linear => linear_instances, + crate::core::image::FilterMethod::Linear => { + linear_instances + } }, ); } } #[cfg(not(feature = "image"))] - layer::Image::Raster { .. } => {} + Image::Raster { .. } => {} #[cfg(feature = "svg")] - layer::Image::Vector { + Image::Vector { handle, color, bounds, + rotation, + opacity, } => { let size = [bounds.width, bounds.height]; - if let Some(atlas_entry) = vector_cache.upload( - device, - encoder, - handle, - *color, - size, - _scale, - &mut self.texture_atlas, + if let Some(atlas_entry) = cache.upload_vector( + device, encoder, handle, *color, size, scale, ) { add_instances( [bounds.x, bounds.y], size, + f32::from(*rotation), + *opacity, atlas_entry, nearest_instances, ); } } #[cfg(not(feature = "svg"))] - layer::Image::Vector { .. } => {} + Image::Vector { .. } => {} } } @@ -460,26 +282,6 @@ impl Pipeline { return; } - let texture_version = self.texture_atlas.layer_count(); - - if self.texture_version != texture_version { - log::info!("Atlas has grown. Recreating bind group..."); - - self.texture = - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &self.texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - self.texture_atlas.view(), - ), - }], - }); - - self.texture_version = texture_version; - } - if self.layers.len() <= self.prepare_layer { self.layers.push(Layer::new( device, @@ -493,7 +295,8 @@ impl Pipeline { layer.prepare( device, - queue, + encoder, + belt, nearest_instances, linear_instances, transformation, @@ -504,6 +307,7 @@ impl Pipeline { pub fn render<'a>( &'a self, + cache: &'a Cache, layer: usize, bounds: Rectangle<u32>, render_pass: &mut wgpu::RenderPass<'a>, @@ -518,20 +322,162 @@ impl Pipeline { bounds.height, ); - render_pass.set_bind_group(1, &self.texture, &[]); + render_pass.set_bind_group(1, cache.bind_group(), &[]); layer.render(render_pass); } } pub fn end_frame(&mut self) { - #[cfg(feature = "image")] - self.raster_cache.borrow_mut().trim(&mut self.texture_atlas); + self.prepare_layer = 0; + } +} + +#[derive(Debug)] +struct Layer { + uniforms: wgpu::Buffer, + nearest: Data, + linear: Data, +} - #[cfg(feature = "svg")] - self.vector_cache.borrow_mut().trim(&mut self.texture_atlas); +impl Layer { + fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + nearest_sampler: &wgpu::Sampler, + linear_sampler: &wgpu::Sampler, + ) -> Self { + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::image uniforms buffer"), + size: mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); - self.prepare_layer = 0; + let nearest = + Data::new(device, constant_layout, nearest_sampler, &uniforms); + + let linear = + Data::new(device, constant_layout, linear_sampler, &uniforms); + + Self { + uniforms, + nearest, + linear, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + nearest_instances: &[Instance], + linear_instances: &[Instance], + transformation: Transformation, + ) { + let uniforms = Uniforms { + transform: transformation.into(), + }; + + let bytes = bytemuck::bytes_of(&uniforms); + + belt.write_buffer( + encoder, + &self.uniforms, + 0, + (bytes.len() as u64).try_into().expect("Sized uniforms"), + device, + ) + .copy_from_slice(bytes); + + self.nearest + .upload(device, encoder, belt, nearest_instances); + + self.linear.upload(device, encoder, belt, linear_instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + self.nearest.render(render_pass); + self.linear.render(render_pass); + } +} + +#[derive(Debug)] +struct Data { + constants: wgpu::BindGroup, + instances: Buffer<Instance>, + instance_count: usize, +} + +impl Data { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + uniforms: &wgpu::Buffer, + ) -> Self { + let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image constants bind group"), + layout: constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: uniforms, + offset: 0, + size: None, + }, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(sampler), + }, + ], + }); + + let instances = Buffer::new( + device, + "iced_wgpu::image instance buffer", + Instance::INITIAL, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + constants, + instances, + instance_count: 0, + } + } + + fn upload( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + instances: &[Instance], + ) { + self.instance_count = instances.len(); + + if self.instance_count == 0 { + return; + } + + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(device, encoder, belt, 0, instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + if self.instance_count == 0 { + return; + } + + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_vertex_buffer(0, self.instances.slice(..)); + + render_pass.draw(0..6, 0..self.instance_count as u32); } } @@ -539,7 +485,10 @@ impl Pipeline { #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { _position: [f32; 2], + _center: [f32; 2], _size: [f32; 2], + _rotation: f32, + _opacity: f32, _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -558,12 +507,27 @@ struct Uniforms { fn add_instances( image_position: [f32; 2], image_size: [f32; 2], + rotation: f32, + opacity: f32, entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { + let center = [ + image_position[0] + image_size[0] / 2.0, + image_position[1] + image_size[1] / 2.0, + ]; + match entry { atlas::Entry::Contiguous(allocation) => { - add_instance(image_position, image_size, allocation, instances); + add_instance( + image_position, + center, + image_size, + rotation, + opacity, + allocation, + instances, + ); } atlas::Entry::Fragmented { fragments, size } => { let scaling_x = image_size[0] / size.width as f32; @@ -589,7 +553,10 @@ fn add_instances( fragment_height as f32 * scaling_y, ]; - add_instance(position, size, allocation, instances); + add_instance( + position, center, size, rotation, opacity, allocation, + instances, + ); } } } @@ -598,7 +565,10 @@ fn add_instances( #[inline] fn add_instance( position: [f32; 2], + center: [f32; 2], size: [f32; 2], + rotation: f32, + opacity: f32, allocation: &atlas::Allocation, instances: &mut Vec<Instance>, ) { @@ -608,7 +578,10 @@ fn add_instance( let instance = Instance { _position: position, + _center: center, _size: size, + _rotation: rotation, + _opacity: opacity, _position_in_atlas: [ (x as f32 + 0.5) / atlas::SIZE as f32, (y as f32 + 0.5) / atlas::SIZE as f32, diff --git a/wgpu/src/image/null.rs b/wgpu/src/image/null.rs new file mode 100644 index 00000000..c06d56be --- /dev/null +++ b/wgpu/src/image/null.rs @@ -0,0 +1,10 @@ +pub use crate::graphics::Image; + +#[derive(Debug, Default)] +pub struct Batch; + +impl Batch { + pub fn push(&mut self, _image: Image) {} + + pub fn clear(&mut self) {} +} diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index a6cba76a..4d3c3125 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -4,13 +4,13 @@ use crate::graphics; use crate::graphics::image::image_rs; use crate::image::atlas::{self, Atlas}; -use std::collections::{HashMap, HashSet}; +use rustc_hash::{FxHashMap, FxHashSet}; /// Entry in cache corresponding to an image handle #[derive(Debug)] pub enum Memory { /// Image data on host - Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, Vec<u8>>), + Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>), /// Storage entry Device(atlas::Entry), /// Image not found @@ -38,8 +38,9 @@ impl Memory { /// Caches image raster data #[derive(Debug, Default)] pub struct Cache { - map: HashMap<u64, Memory>, - hits: HashSet<u64>, + map: FxHashMap<image::Id, Memory>, + hits: FxHashSet<image::Id>, + should_trim: bool, } impl Cache { @@ -50,11 +51,13 @@ impl Cache { } let memory = match graphics::image::load(handle) { - Ok(image) => Memory::Host(image.to_rgba8()), + Ok(image) => Memory::Host(image), Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound, Err(_) => Memory::Invalid, }; + self.should_trim = true; + self.insert(handle, memory); self.get(handle).unwrap() } @@ -86,6 +89,11 @@ impl Cache { /// Trim cache misses from cache pub fn trim(&mut self, atlas: &mut Atlas) { + // Only trim if new entries have landed in the `Cache` + if !self.should_trim { + return; + } + let hits = &self.hits; self.map.retain(|k, memory| { @@ -101,6 +109,7 @@ impl Cache { }); self.hits.clear(); + self.should_trim = false; } fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index d9be50d7..c6d829af 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -5,7 +5,7 @@ use crate::image::atlas::{self, Atlas}; use resvg::tiny_skia; use resvg::usvg::{self, TreeTextToPath}; -use std::collections::{HashMap, HashSet}; +use rustc_hash::{FxHashMap, FxHashSet}; use std::fs; /// Entry in cache corresponding to an svg handle @@ -33,10 +33,11 @@ impl Svg { /// Caches svg vector and raster data #[derive(Debug, Default)] pub struct Cache { - svgs: HashMap<u64, Svg>, - rasterized: HashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, - svg_hits: HashSet<u64>, - rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>, + svgs: FxHashMap<u64, Svg>, + rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, + svg_hits: FxHashSet<u64>, + rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>, + should_trim: bool, } type ColorFilter = Option<[u8; 4]>; @@ -76,6 +77,8 @@ impl Cache { } } + self.should_trim = true; + let _ = self.svgs.insert(handle.id(), svg); self.svgs.get(&handle.id()).unwrap() } @@ -176,6 +179,10 @@ impl Cache { /// Load svg and upload raster data pub fn trim(&mut self, atlas: &mut Atlas) { + if !self.should_trim { + return; + } + let svg_hits = &self.svg_hits; let rasterized_hits = &self.rasterized_hits; @@ -191,6 +198,7 @@ impl Cache { }); self.svg_hits.clear(); self.rasterized_hits.clear(); + self.should_trim = false; } } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index cc767c25..9551311d 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,343 +1,302 @@ -//! Organize rendering primitives into a flattened list of layers. -mod image; -mod pipeline; -mod text; - -pub mod mesh; - -pub use image::Image; -pub use mesh::Mesh; -pub use pipeline::Pipeline; -pub use text::Text; - -use crate::core; -use crate::core::alignment; use crate::core::{ - Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector, + renderer, Background, Color, Point, Radians, Rectangle, Transformation, }; use crate::graphics; use crate::graphics::color; -use crate::graphics::Viewport; +use crate::graphics::layer; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Mesh; +use crate::image::{self, Image}; use crate::primitive::{self, Primitive}; use crate::quad::{self, Quad}; +use crate::text::{self, Text}; +use crate::triangle; + +pub type Stack = layer::Stack<Layer>; -/// A group of primitives that should be clipped together. #[derive(Debug)] -pub struct Layer<'a> { - /// The clipping bounds of the [`Layer`]. +pub struct Layer { pub bounds: Rectangle, - - /// The quads of the [`Layer`]. pub quads: quad::Batch, + pub triangles: triangle::Batch, + pub primitives: primitive::Batch, + pub text: text::Batch, + pub images: image::Batch, + pending_meshes: Vec<Mesh>, + pending_text: Vec<Text>, +} - /// The triangle meshes of the [`Layer`]. - pub meshes: Vec<Mesh<'a>>, +impl Layer { + pub fn draw_quad( + &mut self, + quad: renderer::Quad, + background: Background, + transformation: Transformation, + ) { + let bounds = quad.bounds * transformation; + + let quad = Quad { + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + border_color: color::pack(quad.border.color), + border_radius: quad.border.radius.into(), + border_width: quad.border.width, + shadow_color: color::pack(quad.shadow.color), + shadow_offset: quad.shadow.offset.into(), + shadow_blur_radius: quad.shadow.blur_radius, + }; + + self.quads.add(quad, &background); + } - /// The text of the [`Layer`]. - pub text: Vec<Text<'a>>, + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.pending_text.push(paragraph); + } - /// The images of the [`Layer`]. - pub images: Vec<Image>, + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let editor = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.pending_text.push(editor); + } - /// The custom pipelines of this [`Layer`]. - pub pipelines: Vec<Pipeline>, -} + pub fn draw_text( + &mut self, + text: crate::core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let text = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.pending_text.push(text); + } -impl<'a> Layer<'a> { - /// Creates a new [`Layer`] with the given clipping bounds. - pub fn new(bounds: Rectangle) -> Self { - Self { - bounds, - quads: quad::Batch::default(), - meshes: Vec::new(), - text: Vec::new(), - images: Vec::new(), - pipelines: Vec::new(), - } + pub fn draw_image( + &mut self, + handle: crate::core::image::Handle, + filter_method: crate::core::image::FilterMethod, + bounds: Rectangle, + transformation: Transformation, + rotation: Radians, + opacity: f32, + ) { + let image = Image::Raster { + handle, + filter_method, + bounds: bounds * transformation, + rotation, + opacity, + }; + + self.images.push(image); + } + + pub fn draw_svg( + &mut self, + handle: crate::core::svg::Handle, + color: Option<Color>, + bounds: Rectangle, + transformation: Transformation, + rotation: Radians, + opacity: f32, + ) { + let svg = Image::Vector { + handle, + color, + bounds: bounds * transformation, + rotation, + opacity, + }; + + self.images.push(svg); } - /// Creates a new [`Layer`] for the provided overlay text. - /// - /// This can be useful for displaying debug information. - pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self { - let mut overlay = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - for (i, line) in lines.iter().enumerate() { - let text = text::Cached { - content: line.as_ref(), - bounds: Rectangle::new( - Point::new(11.0, 11.0 + 25.0 * i as f32), - Size::INFINITY, - ), - color: Color::new(0.9, 0.9, 0.9, 1.0), - size: Pixels(20.0), - line_height: core::text::LineHeight::default(), - font: Font::MONOSPACE, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: core::text::Shaping::Basic, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }; - - overlay.text.push(Text::Cached(text.clone())); - - overlay.text.push(Text::Cached(text::Cached { - bounds: text.bounds + Vector::new(-1.0, -1.0), - color: Color::BLACK, - ..text - })); + pub fn draw_mesh( + &mut self, + mut mesh: Mesh, + transformation: Transformation, + ) { + match &mut mesh { + Mesh::Solid { + transformation: local_transformation, + .. + } + | Mesh::Gradient { + transformation: local_transformation, + .. + } => { + *local_transformation = *local_transformation * transformation; + } } - overlay + self.pending_meshes.push(mesh); } - /// Distributes the given [`Primitive`] and generates a list of layers based - /// on its contents. - pub fn generate( - primitives: &'a [Primitive], - viewport: &Viewport, - ) -> Vec<Self> { - let first_layer = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - let mut layers = vec![first_layer]; - - for primitive in primitives { - Self::process_primitive( - &mut layers, - Transformation::IDENTITY, - primitive, - 0, - ); - } + pub fn draw_mesh_group( + &mut self, + meshes: Vec<Mesh>, + transformation: Transformation, + ) { + self.flush_meshes(); - layers + self.triangles.push(triangle::Item::Group { + meshes, + transformation, + }); } - fn process_primitive( - layers: &mut Vec<Self>, + pub fn draw_mesh_cache( + &mut self, + cache: triangle::Cache, transformation: Transformation, - primitive: &'a Primitive, - current_layer: usize, ) { - match primitive { - Primitive::Paragraph { - paragraph, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Paragraph { - paragraph: paragraph.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Editor { - editor, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Editor { - editor: editor.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Text { - content, - bounds, - size, - line_height, - color, - font, - horizontal_alignment, - vertical_alignment, - shaping, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Cached(text::Cached { - content, - bounds: *bounds + transformation.translation(), - size: *size * transformation.scale_factor(), - line_height: *line_height, - color: *color, - font: *font, - horizontal_alignment: *horizontal_alignment, - vertical_alignment: *vertical_alignment, - shaping: *shaping, - clip_bounds: *clip_bounds * transformation, - })); - } - graphics::Primitive::RawText(raw) => { - let layer = &mut layers[current_layer]; + self.flush_meshes(); - layer.text.push(Text::Raw { - raw: raw.clone(), - transformation, - }); - } - Primitive::Quad { - bounds, - background, - border, - shadow, - } => { - let layer = &mut layers[current_layer]; - let bounds = *bounds * transformation; - - let quad = Quad { - position: [bounds.x, bounds.y], - size: [bounds.width, bounds.height], - border_color: color::pack(border.color), - border_radius: border.radius.into(), - border_width: border.width, - shadow_color: shadow.color.into_linear(), - shadow_offset: shadow.offset.into(), - shadow_blur_radius: shadow.blur_radius, - }; - - layer.quads.add(quad, background); - } - Primitive::Image { - handle, - filter_method, - bounds, - } => { - let layer = &mut layers[current_layer]; + self.triangles.push(triangle::Item::Cached { + cache, + transformation, + }); + } - layer.images.push(Image::Raster { - handle: handle.clone(), - filter_method: *filter_method, - bounds: *bounds * transformation, - }); - } - Primitive::Svg { - handle, - color, - bounds, - } => { - let layer = &mut layers[current_layer]; + pub fn draw_text_group( + &mut self, + text: Vec<Text>, + transformation: Transformation, + ) { + self.flush_text(); - layer.images.push(Image::Vector { - handle: handle.clone(), - color: *color, - bounds: *bounds * transformation, - }); - } - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - Self::process_primitive( - layers, - transformation, - primitive, - current_layer, - ); - } - } - Primitive::Clip { bounds, content } => { - let layer = &mut layers[current_layer]; - let translated_bounds = *bounds * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&translated_bounds) - { - let clip_layer = Layer::new(clip_bounds); - layers.push(clip_layer); - - Self::process_primitive( - layers, - transformation, - content, - layers.len() - 1, - ); - } - } - Primitive::Transform { - transformation: new_transformation, - content, - } => { - Self::process_primitive( - layers, - transformation * *new_transformation, - content, - current_layer, - ); - } - Primitive::Cache { content } => { - Self::process_primitive( - layers, - transformation, - content, - current_layer, - ); - } - Primitive::Custom(custom) => match custom { - primitive::Custom::Mesh(mesh) => match mesh { - graphics::Mesh::Solid { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Solid { - transformation, - buffers, - clip_bounds, - }); - } - } - graphics::Mesh::Gradient { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Gradient { - transformation, - buffers, - clip_bounds, - }); - } - } - }, - primitive::Custom::Pipeline(pipeline) => { - let layer = &mut layers[current_layer]; - let bounds = pipeline.bounds * transformation; - - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.pipelines.push(Pipeline { - bounds, - viewport: clip_bounds, - primitive: pipeline.primitive.clone(), - }); - } - } - }, + self.text.push(text::Item::Group { + text, + transformation, + }); + } + + pub fn draw_text_cache( + &mut self, + cache: text::Cache, + transformation: Transformation, + ) { + self.flush_text(); + + self.text.push(text::Item::Cached { + cache, + transformation, + }); + } + + pub fn draw_primitive( + &mut self, + bounds: Rectangle, + primitive: Box<dyn Primitive>, + transformation: Transformation, + ) { + let bounds = bounds * transformation; + + self.primitives + .push(primitive::Instance { bounds, primitive }); + } + + fn flush_meshes(&mut self) { + if !self.pending_meshes.is_empty() { + self.triangles.push(triangle::Item::Group { + transformation: Transformation::IDENTITY, + meshes: self.pending_meshes.drain(..).collect(), + }); + } + } + + fn flush_text(&mut self) { + if !self.pending_text.is_empty() { + self.text.push(text::Item::Group { + transformation: Transformation::IDENTITY, + text: self.pending_text.drain(..).collect(), + }); + } + } +} + +impl graphics::Layer for Layer { + fn with_bounds(bounds: Rectangle) -> Self { + Self { + bounds, + ..Self::default() + } + } + + fn flush(&mut self) { + self.flush_meshes(); + self.flush_text(); + } + + fn resize(&mut self, bounds: Rectangle) { + self.bounds = bounds; + } + + fn reset(&mut self) { + self.bounds = Rectangle::INFINITE; + + self.quads.clear(); + self.triangles.clear(); + self.primitives.clear(); + self.text.clear(); + self.images.clear(); + self.pending_meshes.clear(); + self.pending_text.clear(); + } +} + +impl Default for Layer { + fn default() -> Self { + Self { + bounds: Rectangle::INFINITE, + quads: quad::Batch::default(), + triangles: triangle::Batch::default(), + primitives: primitive::Batch::default(), + text: text::Batch::default(), + images: image::Batch::default(), + pending_meshes: Vec::new(), + pending_text: Vec::new(), } } } diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs deleted file mode 100644 index facbe192..00000000 --- a/wgpu/src/layer/image.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::core::image; -use crate::core::svg; -use crate::core::{Color, Rectangle}; - -/// A raster or vector image. -#[derive(Debug, Clone)] -pub enum Image { - /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, - - /// The filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - }, - /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option<Color>, - - /// The bounds of the image. - bounds: Rectangle, - }, -} diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs deleted file mode 100644 index 5ed7c654..00000000 --- a/wgpu/src/layer/mesh.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! A collection of triangle primitives. -use crate::core::{Rectangle, Transformation}; -use crate::graphics::mesh; - -/// A mesh of triangles. -#[derive(Debug, Clone, Copy)] -pub enum Mesh<'a> { - /// A mesh of triangles with a solid color. - Solid { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed<mesh::SolidVertex2D>, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle<f32>, - }, - /// A mesh of triangles with a gradient color. - Gradient { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed<mesh::GradientVertex2D>, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle<f32>, - }, -} - -impl Mesh<'_> { - /// Returns the origin of the [`Mesh`]. - pub fn transformation(&self) -> Transformation { - match self { - Self::Solid { transformation, .. } - | Self::Gradient { transformation, .. } => *transformation, - } - } - - /// Returns the indices of the [`Mesh`]. - pub fn indices(&self) -> &[u32] { - match self { - Self::Solid { buffers, .. } => &buffers.indices, - Self::Gradient { buffers, .. } => &buffers.indices, - } - } - - /// Returns the clip bounds of the [`Mesh`]. - pub fn clip_bounds(&self) -> Rectangle<f32> { - match self { - Self::Solid { clip_bounds, .. } - | Self::Gradient { clip_bounds, .. } => *clip_bounds, - } - } -} - -/// The result of counting the attributes of a set of meshes. -#[derive(Debug, Clone, Copy, Default)] -pub struct AttributeCount { - /// The total amount of solid vertices. - pub solid_vertices: usize, - - /// The total amount of solid meshes. - pub solids: usize, - - /// The total amount of gradient vertices. - pub gradient_vertices: usize, - - /// The total amount of gradient meshes. - pub gradients: usize, - - /// The total amount of indices. - pub indices: usize, -} - -/// Returns the number of total vertices & total indices of all [`Mesh`]es. -pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount { - meshes - .iter() - .fold(AttributeCount::default(), |mut count, mesh| { - match mesh { - Mesh::Solid { buffers, .. } => { - count.solids += 1; - count.solid_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - Mesh::Gradient { buffers, .. } => { - count.gradients += 1; - count.gradient_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - } - - count - }) -} diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs deleted file mode 100644 index 6dfe6750..00000000 --- a/wgpu/src/layer/pipeline.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::core::Rectangle; -use crate::primitive::pipeline::Primitive; - -use std::sync::Arc; - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Pipeline { - /// The bounds of the [`Pipeline`]. - pub bounds: Rectangle, - - /// The viewport of the [`Pipeline`]. - pub viewport: Rectangle, - - /// The [`Primitive`] to render. - pub primitive: Arc<dyn Primitive>, -} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs deleted file mode 100644 index b3a00130..00000000 --- a/wgpu/src/layer/text.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::core::alignment; -use crate::core::text; -use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation}; -use crate::graphics; -use crate::graphics::text::editor; -use crate::graphics::text::paragraph; - -/// A text primitive. -#[derive(Debug, Clone)] -pub enum Text<'a> { - /// A paragraph. - #[allow(missing_docs)] - Paragraph { - paragraph: paragraph::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// An editor. - #[allow(missing_docs)] - Editor { - editor: editor::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// Some cached text. - Cached(Cached<'a>), - /// Some raw text. - #[allow(missing_docs)] - Raw { - raw: graphics::text::Raw, - transformation: Transformation, - }, -} - -#[derive(Debug, Clone)] -pub struct Cached<'a> { - /// The content of the [`Text`]. - pub content: &'a str, - - /// The layout bounds of the [`Text`]. - pub bounds: Rectangle, - - /// The color of the [`Text`], in __linear RGB_. - pub color: Color, - - /// The size of the [`Text`] in logical pixels. - pub size: Pixels, - - /// The line height of the [`Text`]. - pub line_height: text::LineHeight, - - /// The font of the [`Text`]. - pub font: Font, - - /// The horizontal alignment of the [`Text`]. - pub horizontal_alignment: alignment::Horizontal, - - /// The vertical alignment of the [`Text`]. - pub vertical_alignment: alignment::Vertical, - - /// The shaping strategy of the text. - pub shaping: text::Shaping, - - /// The clip bounds of the text. - pub clip_bounds: Rectangle, -} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index b00e5c3c..ad88ce3e 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -20,15 +20,8 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(rust_2018_idioms)] -#![deny( - missing_debug_implementations, - missing_docs, - unsafe_code, - unused_results, - rustdoc::broken_intra_doc_links -)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(missing_docs)] pub mod layer; pub mod primitive; pub mod settings; @@ -37,13 +30,21 @@ pub mod window; #[cfg(feature = "geometry")] pub mod geometry; -mod backend; mod buffer; mod color; +mod engine; mod quad; mod text; mod triangle; +#[cfg(any(feature = "image", feature = "svg"))] +#[path = "image/mod.rs"] +mod image; + +#[cfg(not(any(feature = "image", feature = "svg")))] +#[path = "image/null.rs"] +mod image; + use buffer::Buffer; pub use iced_graphics as graphics; @@ -51,16 +52,571 @@ pub use iced_graphics::core; pub use wgpu; -pub use backend::Backend; +pub use engine::Engine; pub use layer::Layer; pub use primitive::Primitive; pub use settings::Settings; -#[cfg(any(feature = "image", feature = "svg"))] -mod image; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Vector, +}; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Viewport; /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = iced_graphics::Renderer<Backend>; +#[allow(missing_debug_implementations)] +pub struct Renderer { + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + + triangle_storage: triangle::Storage, + text_storage: text::Storage, + text_viewport: text::Viewport, + + // TODO: Centralize all the image feature handling + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: std::cell::RefCell<image::Cache>, +} + +impl Renderer { + pub fn new( + device: &wgpu::Device, + engine: &Engine, + default_font: Font, + default_text_size: Pixels, + ) -> Self { + Self { + default_font, + 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), + + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: std::cell::RefCell::new( + engine.create_image_cache(device), + ), + } + } + + pub fn present<T: AsRef<str>>( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + clear_color: Option<Color>, + format: wgpu::TextureFormat, + frame: &wgpu::TextureView, + viewport: &Viewport, + overlay: &[T], + ) { + 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(); + + #[cfg(any(feature = "svg", feature = "image"))] + self.image_cache.borrow_mut().trim(); + } + + 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()); + + for layer in self.layers.iter_mut() { + if !layer.quads.is_empty() { + engine.quad_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &layer.quads, + viewport.projection(), + scale_factor, + ); + } + + if !layer.triangles.is_empty() { + engine.triangle_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &mut self.triangle_storage, + &layer.triangles, + Transformation::scale(scale_factor), + viewport.physical_size(), + ); + } + + if !layer.primitives.is_empty() { + for instance in &layer.primitives { + instance.primitive.prepare( + device, + queue, + engine.format, + &mut engine.primitive_storage, + &instance.bounds, + viewport, + ); + } + } + + if !layer.text.is_empty() { + engine.text_pipeline.prepare( + device, + queue, + &self.text_viewport, + encoder, + &mut self.text_storage, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &mut self.image_cache.borrow_mut(), + &layer.images, + viewport.projection(), + scale_factor, + ); + } + } + } + + fn render( + &mut self, + engine: &mut Engine, + encoder: &mut wgpu::CommandEncoder, + frame: &wgpu::TextureView, + clear_color: Option<Color>, + viewport: &Viewport, + ) { + use std::mem::ManuallyDrop; + + let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: match clear_color { + Some(background_color) => wgpu::LoadOp::Clear({ + let [r, g, b, a] = + graphics::color::pack(background_color) + .components(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }), + None => wgpu::LoadOp::Load, + }, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + + let mut quad_layer = 0; + let mut mesh_layer = 0; + let mut text_layer = 0; + + #[cfg(any(feature = "svg", feature = "image"))] + let mut image_layer = 0; + #[cfg(any(feature = "svg", feature = "image"))] + let image_cache = self.image_cache.borrow(); + + let scale_factor = viewport.scale_factor() as f32; + let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size( + viewport.physical_size(), + )); + + let scale = Transformation::scale(scale_factor); + + for layer in self.layers.iter() { + let Some(physical_bounds) = + physical_bounds.intersection(&(layer.bounds * scale)) + else { + continue; + }; + + let Some(scissor_rect) = physical_bounds.snap() else { + continue; + }; + + if !layer.quads.is_empty() { + engine.quad_pipeline.render( + quad_layer, + scissor_rect, + &layer.quads, + &mut render_pass, + ); + + quad_layer += 1; + } + + if !layer.triangles.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + mesh_layer += engine.triangle_pipeline.render( + encoder, + frame, + &self.triangle_storage, + mesh_layer, + &layer.triangles, + physical_bounds, + scale, + ); + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !layer.primitives.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + for instance in &layer.primitives { + if let Some(clip_bounds) = (instance.bounds * scale) + .intersection(&physical_bounds) + .and_then(Rectangle::snap) + { + instance.primitive.render( + encoder, + &engine.primitive_storage, + frame, + &clip_bounds, + ); + } + } + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !layer.text.is_empty() { + text_layer += engine.text_pipeline.render( + &self.text_viewport, + &self.text_storage, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.render( + &image_cache, + image_layer, + scissor_rect, + &mut render_pass, + ); + + image_layer += 1; + } + } + + let _ = ManuallyDrop::into_inner(render_pass); + } + + fn draw_overlay( + &mut self, + overlay: &[impl AsRef<str>], + viewport: &Viewport, + ) { + use crate::core::alignment; + use crate::core::text::Renderer as _; + use crate::core::Renderer as _; + + self.with_layer( + Rectangle::with_size(viewport.logical_size()), + |renderer| { + for (i, line) in overlay.iter().enumerate() { + let text = crate::core::Text { + content: line.as_ref().to_owned(), + bounds: viewport.logical_size(), + size: Pixels(20.0), + line_height: core::text::LineHeight::default(), + font: Font::MONOSPACE, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: core::text::Shaping::Basic, + }; + + renderer.fill_text( + text.clone(), + Point::new(11.0, 11.0 + 25.0 * i as f32), + Color::new(0.9, 0.9, 0.9, 1.0), + Rectangle::with_size(Size::INFINITY), + ); + + renderer.fill_text( + text, + Point::new(11.0, 11.0 + 25.0 * i as f32) + + Vector::new(-1.0, -1.0), + Color::BLACK, + Rectangle::with_size(Size::INFINITY), + ); + } + }, + ); + } +} + +impl core::Renderer for Renderer { + fn start_layer(&mut self, bounds: Rectangle) { + self.layers.push_clip(bounds); + } + + fn end_layer(&mut self) { + self.layers.pop_clip(); + } + + fn start_transformation(&mut self, transformation: Transformation) { + self.layers.push_transformation(transformation); + } + + fn end_transformation(&mut self) { + self.layers.pop_transformation(); + } + + fn fill_quad( + &mut self, + quad: core::renderer::Quad, + background: impl Into<Background>, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_quad(quad, background.into(), transformation); + } + + fn clear(&mut self) { + self.layers.clear(); + } +} + +impl core::text::Renderer for Renderer { + type Font = Font; + type Paragraph = Paragraph; + type Editor = Editor; + + const ICON_FONT: Font = Font::with_name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Self::Font { + self.default_font + } + + fn default_size(&self) -> Pixels { + self.default_text_size + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + + layer.draw_paragraph( + text, + position, + color, + clip_bounds, + transformation, + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_editor(editor, position, color, clip_bounds, transformation); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text(text, position, color, clip_bounds, transformation); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> Size<u32> { + self.image_cache.borrow_mut().measure_image(handle) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: core::image::FilterMethod, + bounds: Rectangle, + rotation: core::Radians, + opacity: f32, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + opacity, + ); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg(&self, handle: &core::svg::Handle) -> Size<u32> { + self.image_cache.borrow_mut().measure_svg(handle) + } + + fn draw_svg( + &mut self, + handle: core::svg::Handle, + color_filter: Option<Color>, + bounds: Rectangle, + rotation: core::Radians, + opacity: f32, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_svg( + handle, + color_filter, + bounds, + transformation, + rotation, + opacity, + ); + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, mesh: graphics::Mesh) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_mesh(mesh, transformation); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, size: Size) -> Self::Frame { + geometry::Frame::new(size) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + let (layer, transformation) = self.layers.current_mut(); + + match geometry { + Geometry::Live { meshes, text } => { + layer.draw_mesh_group(meshes, transformation); + layer.draw_text_group(text, transformation); + } + Geometry::Cached(cache) => { + if let Some(meshes) = cache.meshes { + layer.draw_mesh_cache(meshes, transformation); + } + + if let Some(text) = cache.text { + layer.draw_text_cache(text, transformation); + } + } + } + } +} + +impl primitive::Renderer for Renderer { + fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_primitive(bounds, Box::new(primitive), transformation); + } +} + +impl graphics::compositor::Default for crate::Renderer { + type Compositor = window::Compositor; +} diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index fff927ea..8641f27a 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,30 +1,95 @@ -//! Draw using different graphical primitives. -pub mod pipeline; +//! Draw custom primitives. +use crate::core::{self, Rectangle}; +use crate::graphics::Viewport; -pub use pipeline::Pipeline; +use rustc_hash::FxHashMap; +use std::any::{Any, TypeId}; +use std::fmt::Debug; -use crate::core::Rectangle; -use crate::graphics::{Damage, Mesh}; +/// A batch of primitives. +pub type Batch = Vec<Instance>; -use std::fmt::Debug; +/// A set of methods which allows a [`Primitive`] to be rendered. +pub trait Primitive: Debug + Send + Sync + 'static { + /// Processes the [`Primitive`], allowing for GPU buffer allocation. + fn prepare( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + storage: &mut Storage, + bounds: &Rectangle, + viewport: &Viewport, + ); -/// The graphical primitives supported by `iced_wgpu`. -pub type Primitive = crate::graphics::Primitive<Custom>; + /// Renders the [`Primitive`]. + fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + storage: &Storage, + target: &wgpu::TextureView, + clip_bounds: &Rectangle<u32>, + ); +} -/// The custom primitives supported by `iced_wgpu`. -#[derive(Debug, Clone, PartialEq)] -pub enum Custom { - /// A mesh primitive. - Mesh(Mesh), - /// A custom pipeline primitive. - Pipeline(Pipeline), +#[derive(Debug)] +/// An instance of a specific [`Primitive`]. +pub struct Instance { + /// The bounds of the [`Instance`]. + pub bounds: Rectangle, + + /// The [`Primitive`] to render. + pub primitive: Box<dyn Primitive>, } -impl Damage for Custom { - fn bounds(&self) -> Rectangle { - match self { - Self::Mesh(mesh) => mesh.bounds(), - Self::Pipeline(pipeline) => pipeline.bounds, +impl Instance { + /// Creates a new [`Instance`] with the given [`Primitive`]. + pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { + Instance { + bounds, + primitive: Box::new(primitive), } } } + +/// A renderer than can draw custom primitives. +pub trait Renderer: core::Renderer { + /// Draws a custom primitive. + fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive); +} + +/// Stores custom, user-provided types. +#[derive(Default, Debug)] +pub struct Storage { + pipelines: FxHashMap<TypeId, Box<dyn Any + Send>>, +} + +impl Storage { + /// Returns `true` if `Storage` contains a type `T`. + pub fn has<T: 'static>(&self) -> bool { + self.pipelines.contains_key(&TypeId::of::<T>()) + } + + /// Inserts the data `T` in to [`Storage`]. + pub fn store<T: 'static + Send>(&mut self, data: T) { + let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(data)); + } + + /// Returns a reference to the data with type `T` if it exists in [`Storage`]. + pub fn get<T: 'static>(&self) -> Option<&T> { + self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_ref::<T>() + .expect("Value with this type does not exist in Storage.") + }) + } + + /// Returns a mutable reference to the data with type `T` if it exists in [`Storage`]. + pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { + self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_mut::<T>() + .expect("Value with this type does not exist in Storage.") + }) + } +} diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs deleted file mode 100644 index c6b7c5e2..00000000 --- a/wgpu/src/primitive/pipeline.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Draw primitives using custom pipelines. -use crate::core::{Rectangle, Size}; - -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Arc; - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Pipeline { - /// The bounds of the [`Pipeline`]. - pub bounds: Rectangle, - - /// The [`Primitive`] to render. - pub primitive: Arc<dyn Primitive>, -} - -impl Pipeline { - /// Creates a new [`Pipeline`] with the given [`Primitive`]. - pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { - Pipeline { - bounds, - primitive: Arc::new(primitive), - } - } -} - -impl PartialEq for Pipeline { - fn eq(&self, other: &Self) -> bool { - self.primitive.type_id() == other.primitive.type_id() - } -} - -/// A set of methods which allows a [`Primitive`] to be rendered. -pub trait Primitive: Debug + Send + Sync + 'static { - /// Processes the [`Primitive`], allowing for GPU buffer allocation. - fn prepare( - &self, - format: wgpu::TextureFormat, - device: &wgpu::Device, - queue: &wgpu::Queue, - bounds: Rectangle, - target_size: Size<u32>, - scale_factor: f32, - storage: &mut Storage, - ); - - /// Renders the [`Primitive`]. - fn render( - &self, - storage: &Storage, - target: &wgpu::TextureView, - target_size: Size<u32>, - viewport: Rectangle<u32>, - encoder: &mut wgpu::CommandEncoder, - ); -} - -/// A renderer than can draw custom pipeline primitives. -pub trait Renderer: crate::core::Renderer { - /// Draws a custom pipeline primitive. - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl Primitive, - ); -} - -impl Renderer for crate::Renderer { - fn draw_pipeline_primitive( - &mut self, - bounds: Rectangle, - primitive: impl Primitive, - ) { - self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline( - Pipeline::new(bounds, primitive), - ))); - } -} - -/// Stores custom, user-provided pipelines. -#[derive(Default, Debug)] -pub struct Storage { - pipelines: HashMap<TypeId, Box<dyn Any + Send>>, -} - -impl Storage { - /// Returns `true` if `Storage` contains a pipeline with type `T`. - pub fn has<T: 'static>(&self) -> bool { - self.pipelines.get(&TypeId::of::<T>()).is_some() - } - - /// Inserts the pipeline `T` in to [`Storage`]. - pub fn store<T: 'static + Send>(&mut self, pipeline: T) { - let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline)); - } - - /// Returns a reference to pipeline with type `T` if it exists in [`Storage`]. - pub fn get<T: 'static>(&self) -> Option<&T> { - self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { - pipeline - .downcast_ref::<T>() - .expect("Pipeline with this type does not exist in Storage.") - }) - } - - /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`]. - pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { - self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { - pipeline - .downcast_mut::<T>() - .expect("Pipeline with this type does not exist in Storage.") - }) - } -} diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index b932f54f..de432d2f 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -12,11 +12,37 @@ use bytemuck::{Pod, Zeroable}; use std::mem; -#[cfg(feature = "tracing")] -use tracing::info_span; - const INITIAL_INSTANCES: usize = 2_000; +/// The properties of a quad. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: color::Packed, + + /// The border radii of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, + + /// The shadow color of the [`Quad`]. + pub shadow_color: color::Packed, + + /// The shadow offset of the [`Quad`]. + pub shadow_offset: [f32; 2], + + /// The shadow blur radius of the [`Quad`]. + pub shadow_blur_radius: f32, +} + #[derive(Debug)] pub struct Pipeline { solid: solid::Pipeline, @@ -57,7 +83,8 @@ impl Pipeline { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, quads: &Batch, transformation: Transformation, scale: f32, @@ -67,7 +94,7 @@ impl Pipeline { } let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, queue, quads, transformation, scale); + layer.prepare(device, encoder, belt, quads, transformation, scale); self.prepare_layer += 1; } @@ -123,7 +150,7 @@ impl Pipeline { } #[derive(Debug)] -struct Layer { +pub struct Layer { constants: wgpu::BindGroup, constants_buffer: wgpu::Buffer, solid: solid::Layer, @@ -162,56 +189,46 @@ impl Layer { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, quads: &Batch, transformation: Transformation, scale: f32, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Quad", "PREPARE").entered(); + self.update(device, encoder, belt, transformation, scale); + if !quads.solids.is_empty() { + self.solid.prepare(device, encoder, belt, &quads.solids); + } + + if !quads.gradients.is_empty() { + self.gradient + .prepare(device, encoder, belt, &quads.gradients); + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + transformation: Transformation, + scale: f32, + ) { let uniforms = Uniforms::new(transformation, scale); + let bytes = bytemuck::bytes_of(&uniforms); - queue.write_buffer( + belt.write_buffer( + encoder, &self.constants_buffer, 0, - bytemuck::bytes_of(&uniforms), - ); - - self.solid.prepare(device, queue, &quads.solids); - self.gradient.prepare(device, queue, &quads.gradients); + (bytes.len() as u64).try_into().expect("Sized uniforms"), + device, + ) + .copy_from_slice(bytes); } } -/// The properties of a quad. -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -#[repr(C)] -pub struct Quad { - /// The position of the [`Quad`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: color::Packed, - - /// The border radii of the [`Quad`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Quad`]. - pub border_width: f32, - - /// The shadow color of the [`Quad`]. - pub shadow_color: [f32; 4], - - /// The shadow offset of the [`Quad`]. - pub shadow_offset: [f32; 2], - - /// The shadow blur radius of the [`Quad`]. - pub shadow_blur_radius: f32, -} - /// A group of [`Quad`]s rendered together. #[derive(Default, Debug)] pub struct Batch { @@ -221,10 +238,13 @@ pub struct Batch { /// The gradient quads of the [`Layer`]. gradients: Vec<Gradient>, - /// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count. - order: Vec<(Kind, usize)>, + /// The quad order of the [`Layer`]. + order: Order, } +/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count. +type Order = Vec<(Kind, usize)>; + impl Batch { /// Returns true if there are no quads of any type in [`Quads`]. pub fn is_empty(&self) -> bool { @@ -264,6 +284,12 @@ impl Batch { } } } + + pub fn clear(&mut self) { + self.solids.clear(); + self.gradients.clear(); + self.order.clear(); + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 560fcad2..5b32c52a 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -46,11 +46,12 @@ impl Layer { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, instances: &[Gradient], ) { let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(queue, 0, instances); + let _ = self.instances.write(device, encoder, belt, 0, instances); self.instance_count = instances.len(); } diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 771eee34..1cead367 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -40,11 +40,12 @@ impl Layer { pub fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, instances: &[Solid], ) { let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(queue, 0, instances); + let _ = self.instances.write(device, encoder, belt, 0, instances); self.instance_count = instances.len(); } diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index c9338fec..b3c3cf6a 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,19 +1,19 @@ //! Configure a renderer. use crate::core::{Font, Pixels}; -use crate::graphics::Antialiasing; +use crate::graphics::{self, Antialiasing}; -/// The settings of a [`Backend`]. +/// The settings of a [`Renderer`]. /// -/// [`Backend`]: crate::Backend +/// [`Renderer`]: crate::Renderer #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { - /// The present mode of the [`Backend`]. + /// The present mode of the [`Renderer`]. /// - /// [`Backend`]: crate::Backend + /// [`Renderer`]: crate::Renderer pub present_mode: wgpu::PresentMode, - /// The internal graphics backend to use. - pub internal_backend: wgpu::Backends, + /// The graphics backends to use. + pub backends: wgpu::Backends, /// The default [`Font`] to use. pub default_font: Font, @@ -29,38 +29,51 @@ pub struct Settings { pub antialiasing: Option<Antialiasing>, } -impl Settings { - /// Creates new [`Settings`] using environment configuration. - /// - /// Specifically: - /// - /// - The `internal_backend` can be configured using the `WGPU_BACKEND` - /// environment variable. If the variable is not set, the primary backend - /// will be used. The following values are allowed: - /// - `vulkan` - /// - `metal` - /// - `dx12` - /// - `dx11` - /// - `gl` - /// - `webgpu` - /// - `primary` - pub fn from_env() -> Self { - Settings { - internal_backend: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::all()), - ..Self::default() - } - } -} - impl Default for Settings { fn default() -> Settings { Settings { present_mode: wgpu::PresentMode::AutoVsync, - internal_backend: wgpu::Backends::all(), + backends: wgpu::Backends::all(), default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: None, } } } + +impl From<graphics::Settings> for Settings { + fn from(settings: graphics::Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + ..Settings::default() + } + } +} + +/// Obtains a [`wgpu::PresentMode`] from the current environment +/// configuration, if set. +/// +/// The value returned by this function can be changed by setting +/// the `ICED_PRESENT_MODE` env variable. The possible values are: +/// +/// - `vsync` → [`wgpu::PresentMode::AutoVsync`] +/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`] +/// - `immediate` → [`wgpu::PresentMode::Immediate`] +/// - `fifo` → [`wgpu::PresentMode::Fifo`] +/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`] +/// - `mailbox` → [`wgpu::PresentMode::Mailbox`] +pub fn present_mode_from_env() -> Option<wgpu::PresentMode> { + let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?; + + match present_mode.to_lowercase().as_str() { + "vsync" => Some(wgpu::PresentMode::AutoVsync), + "no_vsync" => Some(wgpu::PresentMode::AutoNoVsync), + "immediate" => Some(wgpu::PresentMode::Immediate), + "fifo" => Some(wgpu::PresentMode::Fifo), + "fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed), + "mailbox" => Some(wgpu::PresentMode::Mailbox), + _ => None, + } +} diff --git a/wgpu/src/shader/blit.wgsl b/wgpu/src/shader/blit.wgsl index c2ea223f..d7633808 100644 --- a/wgpu/src/shader/blit.wgsl +++ b/wgpu/src/shader/blit.wgsl @@ -1,22 +1,14 @@ -var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>( - vec2<f32>(-1.0, 1.0), - vec2<f32>(-1.0, -1.0), - vec2<f32>(1.0, -1.0), - vec2<f32>(-1.0, 1.0), - vec2<f32>(1.0, 1.0), - vec2<f32>(1.0, -1.0) -); - var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>( vec2<f32>(0.0, 0.0), - vec2<f32>(0.0, 1.0), + vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0), vec2<f32>(0.0, 0.0), - vec2<f32>(1.0, 0.0), + vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 1.0) ); @group(0) @binding(0) var u_sampler: sampler; +@group(0) @binding(1) var<uniform> u_ratio: vec2<f32>; @group(1) @binding(0) var u_texture: texture_2d<f32>; struct VertexInput { @@ -30,9 +22,11 @@ struct VertexOutput { @vertex fn vs_main(input: VertexInput) -> VertexOutput { + let uv = uvs[input.vertex_index]; + var out: VertexOutput; - out.uv = uvs[input.vertex_index]; - out.position = vec4<f32>(positions[input.vertex_index], 0.0, 1.0); + out.uv = uv * u_ratio; + out.position = vec4<f32>(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); return out; } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 7b2e5238..0eeb100f 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -9,40 +9,55 @@ struct Globals { struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) pos: vec2<f32>, - @location(1) scale: vec2<f32>, - @location(2) atlas_pos: vec2<f32>, - @location(3) atlas_scale: vec2<f32>, - @location(4) layer: i32, + @location(1) center: vec2<f32>, + @location(2) scale: vec2<f32>, + @location(3) rotation: f32, + @location(4) opacity: f32, + @location(5) atlas_pos: vec2<f32>, + @location(6) atlas_scale: vec2<f32>, + @location(7) layer: i32, } struct VertexOutput { @builtin(position) position: vec4<f32>, @location(0) uv: vec2<f32>, @location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation. + @location(2) opacity: f32, } @vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; - let v_pos = vertex_position(input.vertex_index); + // Generate a vertex position in the range [0, 1] from the vertex index. + var v_pos = vertex_position(input.vertex_index); + // Map the vertex position to the atlas texture. out.uv = vec2<f32>(v_pos * input.atlas_scale + input.atlas_pos); out.layer = f32(input.layer); + out.opacity = input.opacity; - var transform: mat4x4<f32> = mat4x4<f32>( - vec4<f32>(input.scale.x, 0.0, 0.0, 0.0), - vec4<f32>(0.0, input.scale.y, 0.0, 0.0), + // Calculate the vertex position and move the center to the origin + v_pos = round(input.pos) + v_pos * input.scale - input.center; + + // Apply the rotation around the center of the image + let cos_rot = cos(input.rotation); + let sin_rot = sin(input.rotation); + let rotate = mat4x4<f32>( + vec4<f32>(cos_rot, sin_rot, 0.0, 0.0), + vec4<f32>(-sin_rot, cos_rot, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(input.pos, 0.0, 1.0) + vec4<f32>(0.0, 0.0, 0.0, 1.0) ); - out.position = globals.transform * transform * vec4<f32>(v_pos, 0.0, 1.0); + // Calculate the final position of the vertex + out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { - return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); + // Sample the texture at the given UV coordinate and layer. + return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4<f32>(1.0, 1.0, 1.0, input.opacity); } diff --git a/wgpu/src/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl index 1274f814..d908afbc 100644 --- a/wgpu/src/shader/quad/solid.wgsl +++ b/wgpu/src/shader/quad/solid.wgsl @@ -107,13 +107,19 @@ fn solid_fs_main( let quad_color = vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha); if input.shadow_color.a > 0.0 { - let shadow_distance = rounded_box_sdf(input.position.xy - input.pos - input.shadow_offset - (input.scale / 2.0), input.scale / 2.0, border_radius); + let shadow_radius = select_border_radius( + input.border_radius, + input.position.xy - input.shadow_offset, + (input.pos + input.scale * 0.5).xy + ); + let shadow_distance = max(rounded_box_sdf(input.position.xy - input.pos - input.shadow_offset - (input.scale / 2.0), input.scale / 2.0, shadow_radius), 0.); + let shadow_alpha = 1.0 - smoothstep(-input.shadow_blur_radius, input.shadow_blur_radius, shadow_distance); let shadow_color = input.shadow_color; - let base_color = select( + let base_color = mix( vec4<f32>(shadow_color.x, shadow_color.y, shadow_color.z, 0.0), quad_color, - quad_color.a > 0.0 + quad_color.a ); return mix(base_color, shadow_color, (1.0 - radius_alpha) * shadow_alpha); diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6fa1922d..05db5f80 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,296 +1,389 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::cache; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; -use crate::layer::Text; -use std::borrow::Cow; -use std::cell::RefCell; +use rustc_hash::FxHashMap; +use std::collections::hash_map; +use std::rc::{self, Rc}; +use std::sync::atomic::{self, AtomicU64}; use std::sync::Arc; -#[allow(missing_debug_implementations)] -pub struct Pipeline { - renderers: Vec<glyphon::TextRenderer>, - atlas: glyphon::TextAtlas, - prepare_layer: usize, - cache: RefCell<Cache>, +pub use crate::graphics::Text; + +const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION { + glyphon::ColorMode::Accurate +} else { + glyphon::ColorMode::Web +}; + +pub type Batch = Vec<Item>; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + text: Vec<Text>, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - ) -> Self { - Pipeline { - renderers: Vec::new(), - atlas: glyphon::TextAtlas::with_color_mode( - device, - queue, - format, - if color::GAMMA_CORRECTION { - glyphon::ColorMode::Accurate - } else { - glyphon::ColorMode::Web - }, - ), - prepare_layer: 0, - cache: RefCell::new(Cache::new()), +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + group: cache::Group, + text: Rc<[Text]>, + version: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + if text.is_empty() { + return None; + } + + Some(Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + group, + text: Rc::from(text), + version: 0, + }) + } + + pub fn update(&mut self, text: Vec<Text>) { + if self.text.is_empty() && text.is_empty() { + return; } + + self.text = Rc::from(text); + self.version += 1; + } +} + +struct Upload { + renderer: glyphon::TextRenderer, + buffer_cache: BufferCache, + transformation: Transformation, + version: usize, + group_version: usize, + text: rc::Weak<[Text]>, + _atlas: rc::Weak<()>, +} + +#[derive(Default)] +pub struct Storage { + groups: FxHashMap<cache::Group, Group>, + uploads: FxHashMap<Id, Upload>, +} + +struct Group { + atlas: glyphon::TextAtlas, + version: usize, + should_trim: bool, + handle: Rc<()>, // Keeps track of active uploads +} + +impl Storage { + pub fn new() -> Self { + Self::default() } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - font_system() - .write() - .expect("Write font system") - .load_font(bytes); + fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> { + if cache.text.is_empty() { + return None; + } - self.cache = RefCell::new(Cache::new()); + self.groups + .get(&cache.group) + .map(|group| &group.atlas) + .zip(self.uploads.get(&cache.id)) } - pub fn prepare( + fn prepare( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, - sections: &[Text<'_>], - layer_bounds: Rectangle, - scale_factor: f32, - target_size: Size<u32>, + viewport: &glyphon::Viewport, + encoder: &mut wgpu::CommandEncoder, + format: wgpu::TextureFormat, + state: &glyphon::Cache, + cache: &Cache, + new_transformation: Transformation, + bounds: Rectangle, ) { - if self.renderers.len() <= self.prepare_layer { - self.renderers.push(glyphon::TextRenderer::new( - &mut self.atlas, - device, - wgpu::MultisampleState::default(), - None, - )); - } - - let mut font_system = font_system().write().expect("Write font system"); - let font_system = font_system.raw(); + let group_count = self.groups.len(); + + let group = self.groups.entry(cache.group).or_insert_with(|| { + log::debug!( + "New text atlas: {:?} (total: {})", + cache.group, + group_count + 1 + ); + + Group { + atlas: glyphon::TextAtlas::with_color_mode( + device, queue, state, format, COLOR_MODE, + ), + version: 0, + should_trim: false, + handle: Rc::new(()), + } + }); + + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if upload.version != cache.version + || upload.group_version != group.version + || upload.transformation != new_transformation + { + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + viewport, + encoder, + &mut upload.renderer, + &mut group.atlas, + &mut upload.buffer_cache, + &cache.text, + bounds, + new_transformation, + ); + } - let renderer = &mut self.renderers[self.prepare_layer]; - let cache = self.cache.get_mut(); + // Only trim if glyphs have changed + group.should_trim = + group.should_trim || upload.version != cache.version; - enum Allocation { - Paragraph(Paragraph), - Editor(Editor), - Cache(cache::KeyHash), - Raw(Arc<glyphon::Buffer>), - } + upload.text = Rc::downgrade(&cache.text); + upload.version = cache.version; + upload.group_version = group.version; + upload.transformation = new_transformation; - let allocations: Vec<_> = sections - .iter() - .map(|section| match section { - Text::Paragraph { paragraph, .. } => { - paragraph.upgrade().map(Allocation::Paragraph) + upload.buffer_cache.trim(); } - Text::Editor { editor, .. } => { - editor.upgrade().map(Allocation::Editor) - } - Text::Cached(text) => { - let (key, _) = cache.allocate( - font_system, - cache::Key { - content: text.content, - size: text.size.into(), - line_height: f32::from( - text.line_height.to_absolute(text.size), - ), - font: text.font, - bounds: Size { - width: text.bounds.width, - height: text.bounds.height, - }, - shaping: text.shaping, - }, + } + hash_map::Entry::Vacant(entry) => { + let mut renderer = glyphon::TextRenderer::new( + &mut group.atlas, + device, + wgpu::MultisampleState::default(), + None, + ); + + let mut buffer_cache = BufferCache::new(); + + if !cache.text.is_empty() { + let _ = prepare( + device, + queue, + viewport, + encoder, + &mut renderer, + &mut group.atlas, + &mut buffer_cache, + &cache.text, + bounds, + new_transformation, ); - - Some(Allocation::Cache(key)) - } - Text::Raw { raw, .. } => { - raw.buffer.upgrade().map(Allocation::Raw) } - }) - .collect(); - let layer_bounds = layer_bounds * scale_factor; + let _ = entry.insert(Upload { + renderer, + buffer_cache, + transformation: new_transformation, + version: 0, + group_version: group.version, + text: Rc::downgrade(&cache.text), + _atlas: Rc::downgrade(&group.handle), + }); + + group.should_trim = cache.group.is_singleton(); + + log::debug!( + "New text upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); + } + } + } - let text_areas = sections.iter().zip(allocations.iter()).filter_map( - |(section, allocation)| { - let ( - buffer, - bounds, - horizontal_alignment, - vertical_alignment, - color, - clip_bounds, - transformation, - ) = match section { - Text::Paragraph { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Paragraph as _; - - let Some(Allocation::Paragraph(paragraph)) = allocation - else { - return None; - }; - - ( - paragraph.buffer(), - Rectangle::new(*position, paragraph.min_bounds()), - paragraph.horizontal_alignment(), - paragraph.vertical_alignment(), - *color, - *clip_bounds, - *transformation, - ) - } - Text::Editor { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Editor as _; - - let Some(Allocation::Editor(editor)) = allocation - else { - return None; - }; - - ( - editor.buffer(), - Rectangle::new(*position, editor.bounds()), - alignment::Horizontal::Left, - alignment::Vertical::Top, - *color, - *clip_bounds, - *transformation, - ) - } - Text::Cached(text) => { - let Some(Allocation::Cache(key)) = allocation else { - return None; - }; - - let entry = cache.get(key).expect("Get cached buffer"); - - ( - &entry.buffer, - Rectangle::new( - text.bounds.position(), - entry.min_bounds, - ), - text.horizontal_alignment, - text.vertical_alignment, - text.color, - text.clip_bounds, - Transformation::IDENTITY, - ) - } - Text::Raw { - raw, - transformation, - } => { - let Some(Allocation::Raw(buffer)) = allocation else { - return None; - }; - - let (width, height) = buffer.size(); - - ( - buffer.as_ref(), - Rectangle::new( - raw.position, - Size::new(width, height), - ), - alignment::Horizontal::Left, - alignment::Vertical::Top, - raw.color, - raw.clip_bounds, - *transformation, - ) - } - }; + pub fn trim(&mut self) { + self.uploads + .retain(|_id, upload| upload.text.strong_count() > 0); - let bounds = bounds * transformation * scale_factor; + self.groups.retain(|id, group| { + let active_uploads = Rc::weak_count(&group.handle); - let left = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; + if active_uploads == 0 { + log::debug!("Dropping text atlas: {id:?}"); - let top = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 - } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - let clip_bounds = layer_bounds.intersection( - &(clip_bounds * transformation * scale_factor), - )?; - - Some(glyphon::TextArea { - buffer, - left, - top, - scale: scale_factor * transformation.scale_factor(), - bounds: glyphon::TextBounds { - left: clip_bounds.x as i32, - top: clip_bounds.y as i32, - right: (clip_bounds.x + clip_bounds.width) as i32, - bottom: (clip_bounds.y + clip_bounds.height) as i32, - }, - default_color: to_color(color), - }) - }, - ); + return false; + } + + if group.should_trim { + log::trace!("Trimming text atlas: {id:?}"); + + group.atlas.trim(); + group.should_trim = false; + + // We only need to worry about glyph fighting + // when the atlas may be shared by multiple + // uploads. + if !id.is_singleton() { + log::debug!( + "Invalidating text atlas: {id:?} \ + (uploads: {active_uploads})" + ); + + group.version += 1; + } + } + + true + }); + } +} + +pub struct Viewport(glyphon::Viewport); - let result = renderer.prepare( - device, +impl Viewport { + pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) { + self.0.update( queue, - font_system, - &mut self.atlas, glyphon::Resolution { - width: target_size.width, - height: target_size.height, + width: resolution.width, + height: resolution.height, }, - text_areas, - &mut glyphon::SwashCache::new(), ); + } +} - match result { - Ok(()) => { - self.prepare_layer += 1; - } - Err(glyphon::PrepareError::AtlasFull) => { - // If the atlas cannot grow, then all bets are off. - // Instead of panicking, we will just pray that the result - // will be somewhat readable... +#[allow(missing_debug_implementations)] +pub struct Pipeline { + state: glyphon::Cache, + format: wgpu::TextureFormat, + atlas: glyphon::TextAtlas, + renderers: Vec<glyphon::TextRenderer>, + prepare_layer: usize, + cache: BufferCache, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + ) -> Self { + let state = glyphon::Cache::new(device); + let atlas = glyphon::TextAtlas::with_color_mode( + device, queue, &state, format, COLOR_MODE, + ); + + Pipeline { + state, + format, + renderers: Vec::new(), + atlas, + prepare_layer: 0, + cache: BufferCache::new(), + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + viewport: &Viewport, + encoder: &mut wgpu::CommandEncoder, + storage: &mut Storage, + batch: &Batch, + layer_bounds: Rectangle, + layer_transformation: Transformation, + ) { + for item in batch { + match item { + Item::Group { + transformation, + text, + } => { + if self.renderers.len() <= self.prepare_layer { + self.renderers.push(glyphon::TextRenderer::new( + &mut self.atlas, + device, + wgpu::MultisampleState::default(), + None, + )); + } + + let renderer = &mut self.renderers[self.prepare_layer]; + let result = prepare( + device, + queue, + &viewport.0, + encoder, + renderer, + &mut self.atlas, + &mut self.cache, + text, + layer_bounds * layer_transformation, + layer_transformation * *transformation, + ); + + match result { + Ok(()) => { + self.prepare_layer += 1; + } + Err(glyphon::PrepareError::AtlasFull) => { + // If the atlas cannot grow, then all bets are off. + // Instead of panicking, we will just pray that the result + // will be somewhat readable... + } + } + } + Item::Cached { + transformation, + cache, + } => { + storage.prepare( + device, + queue, + &viewport.0, + encoder, + self.format, + &self.state, + cache, + layer_transformation * *transformation, + layer_bounds * layer_transformation, + ); + } } } } pub fn render<'a>( &'a self, - layer: usize, + viewport: &'a Viewport, + storage: &'a Storage, + start: usize, + batch: &'a Batch, bounds: Rectangle<u32>, render_pass: &mut wgpu::RenderPass<'a>, - ) { - let renderer = &self.renderers[layer]; + ) -> usize { + let mut layer_count = 0; render_pass.set_scissor_rect( bounds.x, @@ -299,15 +392,252 @@ impl Pipeline { bounds.height, ); - renderer - .render(&self.atlas, render_pass) - .expect("Render text"); + for item in batch { + match item { + Item::Group { .. } => { + let renderer = &self.renderers[start + layer_count]; + + renderer + .render(&self.atlas, &viewport.0, render_pass) + .expect("Render text"); + + layer_count += 1; + } + Item::Cached { cache, .. } => { + if let Some((atlas, upload)) = storage.get(cache) { + upload + .renderer + .render(atlas, &viewport.0, render_pass) + .expect("Render cached text"); + } + } + } + } + + layer_count + } + + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(glyphon::Viewport::new(device, &self.state)) } pub fn end_frame(&mut self) { self.atlas.trim(); - self.cache.get_mut().trim(); + self.cache.trim(); self.prepare_layer = 0; } } + +fn prepare( + device: &wgpu::Device, + queue: &wgpu::Queue, + viewport: &glyphon::Viewport, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut glyphon::TextRenderer, + atlas: &mut glyphon::TextAtlas, + buffer_cache: &mut BufferCache, + sections: &[Text], + layer_bounds: Rectangle, + layer_transformation: Transformation, +) -> Result<(), glyphon::PrepareError> { + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + + enum Allocation { + Paragraph(Paragraph), + Editor(Editor), + Cache(text_cache::KeyHash), + Raw(Arc<glyphon::Buffer>), + } + + let allocations: Vec<_> = sections + .iter() + .map(|section| match section { + Text::Paragraph { paragraph, .. } => { + paragraph.upgrade().map(Allocation::Paragraph) + } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } + Text::Cached { + content, + bounds, + size, + line_height, + font, + shaping, + .. + } => { + let (key, _) = buffer_cache.allocate( + font_system, + text_cache::Key { + content, + size: f32::from(*size), + line_height: f32::from(*line_height), + font: *font, + bounds: Size { + width: bounds.width, + height: bounds.height, + }, + shaping: *shaping, + }, + ); + + Some(Allocation::Cache(key)) + } + Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw), + }) + .collect(); + + let text_areas = sections.iter().zip(allocations.iter()).filter_map( + |(section, allocation)| { + let ( + buffer, + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + transformation, + ) = match section { + Text::Paragraph { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Paragraph as _; + + let Some(Allocation::Paragraph(paragraph)) = allocation + else { + return None; + }; + + ( + paragraph.buffer(), + Rectangle::new(*position, paragraph.min_bounds()), + paragraph.horizontal_alignment(), + paragraph.vertical_alignment(), + *color, + *clip_bounds, + *transformation, + ) + } + Text::Editor { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + *clip_bounds, + *transformation, + ) + } + Text::Cached { + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + .. + } => { + let Some(Allocation::Cache(key)) = allocation else { + return None; + }; + + let entry = + buffer_cache.get(key).expect("Get cached buffer"); + + ( + &entry.buffer, + Rectangle::new(bounds.position(), entry.min_bounds), + *horizontal_alignment, + *vertical_alignment, + *color, + *clip_bounds, + Transformation::IDENTITY, + ) + } + Text::Raw { + raw, + transformation, + } => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new(raw.position, Size::new(width, height)), + alignment::Horizontal::Left, + alignment::Vertical::Top, + raw.color, + raw.clip_bounds, + *transformation, + ) + } + }; + + let bounds = bounds * transformation * layer_transformation; + + let left = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - bounds.width / 2.0, + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let top = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - bounds.height / 2.0, + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let clip_bounds = layer_bounds.intersection( + &(clip_bounds * transformation * layer_transformation), + )?; + + Some(glyphon::TextArea { + buffer, + left, + top, + scale: transformation.scale_factor() + * layer_transformation.scale_factor(), + bounds: glyphon::TextBounds { + left: clip_bounds.x as i32, + top: clip_bounds.y as i32, + right: (clip_bounds.x + clip_bounds.width) as i32, + bottom: (clip_bounds.y + clip_bounds.height) as i32, + }, + default_color: to_color(color), + }) + }, + ); + + renderer.prepare( + device, + queue, + encoder, + font_system, + atlas, + viewport, + text_areas, + &mut glyphon::SwashCache::new(), + ) +} diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 2bb6f307..b0551f55 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,14 +1,158 @@ //! Draw meshes of triangles. mod msaa; -use crate::core::{Size, Transformation}; +use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::mesh::{self, Mesh}; use crate::graphics::Antialiasing; -use crate::layer::mesh::{self, Mesh}; use crate::Buffer; +use rustc_hash::FxHashMap; +use std::collections::hash_map; +use std::rc::{self, Rc}; +use std::sync::atomic::{self, AtomicU64}; + const INITIAL_INDEX_COUNT: usize = 1_000; const INITIAL_VERTEX_COUNT: usize = 1_000; +pub type Batch = Vec<Item>; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + meshes: Vec<Mesh>, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + batch: Rc<[Mesh]>, + version: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + pub fn new(meshes: Vec<Mesh>) -> Option<Self> { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + if meshes.is_empty() { + return None; + } + + Some(Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + batch: Rc::from(meshes), + version: 0, + }) + } + + pub fn update(&mut self, meshes: Vec<Mesh>) { + self.batch = Rc::from(meshes); + self.version += 1; + } +} + +#[derive(Debug)] +struct Upload { + layer: Layer, + transformation: Transformation, + version: usize, + batch: rc::Weak<[Mesh]>, +} + +#[derive(Debug, Default)] +pub struct Storage { + uploads: FxHashMap<Id, Upload>, +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, cache: &Cache) -> Option<&Upload> { + if cache.batch.is_empty() { + return None; + } + + self.uploads.get(&cache.id) + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + cache: &Cache, + new_transformation: Transformation, + ) { + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if !cache.batch.is_empty() + && (upload.version != cache.version + || upload.transformation != new_transformation) + { + upload.layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + upload.batch = Rc::downgrade(&cache.batch); + upload.version = cache.version; + upload.transformation = new_transformation; + } + } + hash_map::Entry::Vacant(entry) => { + let mut layer = Layer::new(device, solid, gradient); + + layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + let _ = entry.insert(Upload { + layer, + transformation: new_transformation, + version: 0, + batch: Rc::downgrade(&cache.batch), + }); + + log::debug!( + "New mesh upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); + } + } + } + + pub fn trim(&mut self) { + self.uploads + .retain(|_id, upload| upload.batch.strong_count() > 0); + } +} + #[derive(Debug)] pub struct Pipeline { blit: Option<msaa::Blit>, @@ -18,8 +162,198 @@ pub struct Pipeline { prepare_layer: usize, } +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option<Antialiasing>, + ) -> Pipeline { + Pipeline { + blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + solid: solid::Pipeline::new(device, format, antialiasing), + gradient: gradient::Pipeline::new(device, format, antialiasing), + layers: Vec::new(), + prepare_layer: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + storage: &mut Storage, + items: &[Item], + scale: Transformation, + target_size: Size<u32>, + ) { + let projection = if let Some(blit) = &mut self.blit { + blit.prepare(device, encoder, belt, target_size) * scale + } else { + Transformation::orthographic(target_size.width, target_size.height) + * scale + }; + + for item in items { + match item { + Item::Group { + transformation, + meshes, + } => { + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &self.solid, + &self.gradient, + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + meshes, + projection * *transformation, + ); + + self.prepare_layer += 1; + } + Item::Cached { + transformation, + cache, + } => { + storage.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + cache, + projection * *transformation, + ); + } + } + } + } + + pub fn render( + &mut self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + storage: &Storage, + start: usize, + batch: &Batch, + bounds: Rectangle, + screen_transformation: Transformation, + ) -> usize { + let mut layer_count = 0; + + let items = batch.iter().filter_map(|item| match item { + Item::Group { + transformation, + meshes, + } => { + let layer = &self.layers[start + layer_count]; + layer_count += 1; + + Some(( + layer, + meshes.as_slice(), + screen_transformation * *transformation, + )) + } + Item::Cached { + transformation, + cache, + } => { + let upload = storage.get(cache)?; + + Some(( + &upload.layer, + &cache.batch, + screen_transformation * *transformation, + )) + } + }); + + render( + encoder, + target, + self.blit.as_mut(), + &self.solid, + &self.gradient, + bounds, + items, + ); + + layer_count + } + + pub fn end_frame(&mut self) { + self.prepare_layer = 0; + } +} + +fn render<'a>( + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + mut blit: Option<&mut msaa::Blit>, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + bounds: Rectangle, + group: impl Iterator<Item = (&'a Layer, &'a [Mesh], Transformation)>, +) { + { + 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), + ) + } else { + (target, None, wgpu::LoadOp::Load) + }; + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + for (layer, meshes, transformation) in group { + layer.render( + solid, + gradient, + meshes, + bounds, + transformation, + &mut render_pass, + ); + } + } + + if let Some(blit) = blit { + blit.draw(encoder, target); + } +} + #[derive(Debug)] -struct Layer { +pub struct Layer { index_buffer: Buffer<u32>, index_strides: Vec<u32>, solid: solid::Layer, @@ -48,10 +382,11 @@ impl Layer { fn prepare( &mut self, device: &wgpu::Device, - queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, solid: &solid::Pipeline, gradient: &gradient::Pipeline, - meshes: &[Mesh<'_>], + meshes: &[Mesh], transformation: Transformation, ) { // Count the total amount of vertices & indices we need to handle @@ -103,33 +438,47 @@ impl Layer { let uniforms = Uniforms::new(transformation * mesh.transformation()); - index_offset += - self.index_buffer.write(queue, index_offset, indices); + index_offset += self.index_buffer.write( + device, + encoder, + belt, + index_offset, + indices, + ); + self.index_strides.push(indices.len() as u32); match mesh { Mesh::Solid { buffers, .. } => { solid_vertex_offset += self.solid.vertices.write( - queue, + device, + encoder, + belt, solid_vertex_offset, &buffers.vertices, ); solid_uniform_offset += self.solid.uniforms.write( - queue, + device, + encoder, + belt, solid_uniform_offset, &[uniforms], ); } Mesh::Gradient { buffers, .. } => { gradient_vertex_offset += self.gradient.vertices.write( - queue, + device, + encoder, + belt, gradient_vertex_offset, &buffers.vertices, ); gradient_uniform_offset += self.gradient.uniforms.write( - queue, + device, + encoder, + belt, gradient_uniform_offset, &[uniforms], ); @@ -142,8 +491,9 @@ impl Layer { &'a self, solid: &'a solid::Pipeline, gradient: &'a gradient::Pipeline, - meshes: &[Mesh<'_>], - scale_factor: f32, + meshes: &[Mesh], + bounds: Rectangle, + transformation: Transformation, render_pass: &mut wgpu::RenderPass<'a>, ) { let mut num_solids = 0; @@ -151,11 +501,12 @@ impl Layer { let mut last_is_solid = None; for (index, mesh) in meshes.iter().enumerate() { - let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); - - if clip_bounds.width < 1 || clip_bounds.height < 1 { + let Some(clip_bounds) = bounds + .intersection(&(mesh.clip_bounds() * transformation)) + .and_then(Rectangle::snap) + else { continue; - } + }; render_pass.set_scissor_rect( clip_bounds.x, @@ -219,117 +570,6 @@ impl Layer { } } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - antialiasing: Option<Antialiasing>, - ) -> Pipeline { - Pipeline { - blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), - solid: solid::Pipeline::new(device, format, antialiasing), - gradient: gradient::Pipeline::new(device, format, antialiasing), - layers: Vec::new(), - prepare_layer: 0, - } - } - - pub fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - meshes: &[Mesh<'_>], - transformation: Transformation, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered(); - - if self.layers.len() <= self.prepare_layer { - self.layers - .push(Layer::new(device, &self.solid, &self.gradient)); - } - - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare( - device, - queue, - &self.solid, - &self.gradient, - meshes, - transformation, - ); - - self.prepare_layer += 1; - } - - pub fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - layer: usize, - target_size: Size<u32>, - meshes: &[Mesh<'_>], - scale_factor: f32, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered(); - - { - let (attachment, resolve_target, load) = if let Some(blit) = - &mut self.blit - { - let (attachment, resolve_target) = - blit.targets(device, target_size.width, target_size.height); - - ( - attachment, - Some(resolve_target), - wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - ) - } else { - (target, None, wgpu::LoadOp::Load) - }; - - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu.triangle.render_pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: attachment, - resolve_target, - ops: wgpu::Operations { - load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - let layer = &mut self.layers[layer]; - - layer.render( - &self.solid, - &self.gradient, - meshes, - scale_factor, - &mut render_pass, - ); - } - - if let Some(blit) = &mut self.blit { - blit.draw(encoder, target); - } - } - - pub fn end_frame(&mut self) { - self.prepare_layer = 0; - } -} - fn fragment_target( texture_format: wgpu::TextureFormat, ) -> wgpu::ColorTargetState { diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 14abd20b..71c16925 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -1,13 +1,18 @@ +use crate::core::{Size, Transformation}; use crate::graphics; +use std::num::NonZeroU64; + #[derive(Debug)] pub struct Blit { format: wgpu::TextureFormat, pipeline: wgpu::RenderPipeline, constants: wgpu::BindGroup, + ratio: wgpu::Buffer, texture_layout: wgpu::BindGroupLayout, sample_count: u32, targets: Option<Targets>, + last_region: Option<Size<u32>>, } impl Blit { @@ -19,27 +24,52 @@ impl Blit { let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let ratio = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced-wgpu::triangle::msaa ratio"), + size: std::mem::size_of::<Ratio>() as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + mapped_at_creation: false, + }); + let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu::triangle:msaa uniforms layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::NonFiltering, - ), - count: None, - }], + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], }); let constant_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::triangle::msaa uniforms bind group"), layout: &constant_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&sampler), - }], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], }); let texture_layout = @@ -112,43 +142,61 @@ impl Blit { format, pipeline, constants: constant_bind_group, + ratio, texture_layout, sample_count: antialiasing.sample_count(), targets: None, + last_region: None, } } - pub fn targets( + pub fn prepare( &mut self, device: &wgpu::Device, - width: u32, - height: u32, - ) -> (&wgpu::TextureView, &wgpu::TextureView) { + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + region_size: Size<u32>, + ) -> Transformation { match &mut self.targets { - None => { + Some(targets) + if region_size.width <= targets.size.width + && region_size.height <= targets.size.height => {} + _ => { self.targets = Some(Targets::new( device, self.format, &self.texture_layout, self.sample_count, - width, - height, + region_size, )); } - Some(targets) => { - if targets.width != width || targets.height != height { - self.targets = Some(Targets::new( - device, - self.format, - &self.texture_layout, - self.sample_count, - width, - height, - )); - } - } } + let targets = self.targets.as_mut().unwrap(); + + if Some(region_size) != self.last_region { + let ratio = Ratio { + u: region_size.width as f32 / targets.size.width as f32, + v: region_size.height as f32 / targets.size.height as f32, + }; + + belt.write_buffer( + encoder, + &self.ratio, + 0, + NonZeroU64::new(std::mem::size_of::<Ratio>() as u64) + .expect("non-empty ratio"), + device, + ) + .copy_from_slice(bytemuck::bytes_of(&ratio)); + + self.last_region = Some(region_size); + } + + Transformation::orthographic(targets.size.width, targets.size.height) + } + + pub fn targets(&self) -> (&wgpu::TextureView, &wgpu::TextureView) { let targets = self.targets.as_ref().unwrap(); (&targets.attachment, &targets.resolve) @@ -191,8 +239,7 @@ struct Targets { attachment: wgpu::TextureView, resolve: wgpu::TextureView, bind_group: wgpu::BindGroup, - width: u32, - height: u32, + size: Size<u32>, } impl Targets { @@ -201,12 +248,11 @@ impl Targets { format: wgpu::TextureFormat, texture_layout: &wgpu::BindGroupLayout, sample_count: u32, - width: u32, - height: u32, + size: Size<u32>, ) -> Targets { let extent = wgpu::Extent3d { - width, - height, + width: size.width, + height: size.height, depth_or_array_layers: 1, }; @@ -252,8 +298,14 @@ impl Targets { attachment, resolve, bind_group, - width, - height, + size, } } } + +#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Ratio { + u: f32, + v: f32, +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 328ad781..2e938c77 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,23 +1,49 @@ //! Connect a window with a renderer. use crate::core::{Color, Size}; -use crate::graphics; use crate::graphics::color; use crate::graphics::compositor; -use crate::graphics::{Error, Viewport}; -use crate::{Backend, Primitive, Renderer, Settings}; - -use std::future::Future; +use crate::graphics::error; +use crate::graphics::{self, Viewport}; +use crate::settings::{self, Settings}; +use crate::{Engine, Renderer}; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { - settings: Settings, instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat, alpha_mode: wgpu::CompositeAlphaMode, + engine: Engine, + settings: Settings, +} + +/// A compositor error. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// The surface creation failed. + #[error("the surface creation failed: {0}")] + SurfaceCreationFailed(#[from] wgpu::CreateSurfaceError), + /// The surface is not compatible. + #[error("the surface is not compatible")] + IncompatibleSurface, + /// No adapter was found for the options requested. + #[error("no adapter was found for the options requested: {0:?}")] + NoAdapterFound(String), + /// No device request succeeded. + #[error("no device request succeeded: {0:?}")] + RequestDeviceFailed(Vec<(wgpu::Limits, wgpu::RequestDeviceError)>), +} + +impl From<Error> for graphics::Error { + fn from(error: Error) -> Self { + Self::GraphicsAdapterNotFound { + backend: "wgpu", + reason: error::Reason::RequestFailed(error.to_string()), + } + } } impl Compositor { @@ -27,9 +53,9 @@ impl Compositor { pub async fn request<W: compositor::Window>( settings: Settings, compatible_window: Option<W>, - ) -> Option<Self> { + ) -> Result<Self, Error> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: settings.internal_backend, + backends: settings.backends, ..Default::default() }); @@ -38,7 +64,7 @@ impl Compositor { #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance - .enumerate_adapters(settings.internal_backend) + .enumerate_adapters(settings.backends) .iter() .map(wgpu::Adapter::get_info) .collect(); @@ -49,23 +75,27 @@ impl Compositor { let compatible_surface = compatible_window .and_then(|window| instance.create_surface(window).ok()); + let adapter_options = wgpu::RequestAdapterOptions { + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(if settings.antialiasing.is_none() { + wgpu::PowerPreference::LowPower + } else { + wgpu::PowerPreference::HighPerformance + }), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }; + let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(if settings.antialiasing.is_none() { - wgpu::PowerPreference::LowPower - } else { - wgpu::PowerPreference::HighPerformance - }), - compatible_surface: compatible_surface.as_ref(), - force_fallback_adapter: false, - }) - .await?; + .request_adapter(&adapter_options) + .await + .ok_or(Error::NoAdapterFound(format!("{:?}", adapter_options)))?; log::info!("Selected: {:#?}", adapter.get_info()); - let (format, alpha_mode) = - compatible_surface.as_ref().and_then(|surface| { + let (format, alpha_mode) = compatible_surface + .as_ref() + .and_then(|surface| { let capabilities = surface.get_capabilities(&adapter); let mut formats = capabilities.formats.iter().copied(); @@ -92,12 +122,17 @@ impl Compositor { .contains(&wgpu::CompositeAlphaMode::PostMultiplied) { wgpu::CompositeAlphaMode::PostMultiplied + } else if alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied } else { wgpu::CompositeAlphaMode::Auto }; format.zip(Some(preferred_alpha)) - })?; + }) + .ok_or(Error::IncompatibleSurface)?; log::info!( "Selected format: {format:?} with alpha mode: {alpha_mode:?}" @@ -111,70 +146,71 @@ impl Compositor { let limits = [wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()]; - let mut limits = limits.into_iter().map(|limits| wgpu::Limits { + let limits = limits.into_iter().map(|limits| wgpu::Limits { max_bind_groups: 2, ..limits }); - let (device, queue) = - loop { - let required_limits = limits.next()?; - let device = adapter.request_device( + let mut errors = Vec::new(); + + for required_limits in limits { + let result = adapter + .request_device( &wgpu::DeviceDescriptor { label: Some( "iced_wgpu::window::compositor device descriptor", ), required_features: wgpu::Features::empty(), - required_limits, + required_limits: required_limits.clone(), }, None, - ).await.ok(); - - if let Some(device) = device { - break Some(device); + ) + .await; + + match result { + Ok((device, queue)) => { + let engine = Engine::new( + &adapter, + &device, + &queue, + format, + settings.antialiasing, + ); + + return Ok(Compositor { + instance, + adapter, + device, + queue, + format, + alpha_mode, + engine, + settings, + }); } - }?; - - Some(Compositor { - instance, - settings, - adapter, - device, - queue, - format, - alpha_mode, - }) - } + Err(error) => { + errors.push((required_limits, error)); + } + } + } - /// Creates a new rendering [`Backend`] for this [`Compositor`]. - pub fn create_backend(&self) -> Backend { - Backend::new( - &self.adapter, - &self.device, - &self.queue, - self.settings, - self.format, - ) + Err(Error::RequestDeviceFailed(errors)) } } -/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and -/// window. +/// Creates a [`Compositor`] with the given [`Settings`] and window. pub async fn new<W: compositor::Window>( settings: Settings, compatible_window: W, ) -> Result<Compositor, Error> { - Compositor::request(settings, Some(compatible_window)) - .await - .ok_or(Error::GraphicsAdapterNotFound) + Compositor::request(settings, Some(compatible_window)).await } -/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. +/// Presents the given primitives with the given [`Compositor`]. pub fn present<T: AsRef<str>>( compositor: &mut Compositor, - backend: &mut Backend, + renderer: &mut Renderer, surface: &mut wgpu::Surface<'static>, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, overlay: &[T], @@ -191,20 +227,21 @@ pub fn present<T: AsRef<str>>( .texture .create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), frame.texture.format(), view, - primitives, viewport, overlay, ); - // Submit work - let _submission = compositor.queue.submit(Some(encoder.finish())); + let _ = compositor.engine.submit(&compositor.queue, encoder); + + // Present the frame frame.present(); Ok(()) @@ -225,20 +262,41 @@ pub fn present<T: AsRef<str>>( } impl graphics::Compositor for Compositor { - type Settings = Settings; type Renderer = Renderer; type Surface = wgpu::Surface<'static>; - fn new<W: compositor::Window>( - settings: Self::Settings, + async fn with_backend<W: compositor::Window>( + settings: graphics::Settings, compatible_window: W, - ) -> impl Future<Output = Result<Self, Error>> { - new(settings, compatible_window) + backend: Option<&str>, + ) -> Result<Self, graphics::Error> { + match backend { + None | Some("wgpu") => { + let mut settings = Settings::from(settings); + + if let Some(backends) = wgpu::util::backend_bits_from_env() { + settings.backends = backends; + } + + if let Some(present_mode) = settings::present_mode_from_env() { + settings.present_mode = present_mode; + } + + Ok(new(settings, compatible_window).await?) + } + Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound { + backend: "wgpu", + reason: error::Reason::DidNotMatch { + preferred_backend: backend.to_owned(), + }, + }), + } } fn create_renderer(&self) -> Self::Renderer { Renderer::new( - self.create_backend(), + &self.device, + &self.engine, self.settings.default_font, self.settings.default_text_size, ) @@ -278,7 +336,7 @@ impl graphics::Compositor for Compositor { height, alpha_mode: self.alpha_mode, view_formats: vec![], - desired_maximum_frame_latency: 2, + desired_maximum_frame_latency: 1, }, ); } @@ -300,17 +358,7 @@ impl graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { - renderer.with_primitives(|backend, primitives| { - present( - self, - backend, - surface, - primitives, - viewport, - background_color, - overlay, - ) - }) + present(self, renderer, surface, viewport, background_color, overlay) } fn screenshot<T: AsRef<str>>( @@ -321,16 +369,7 @@ impl graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Vec<u8> { - renderer.with_primitives(|backend, primitives| { - screenshot( - self, - backend, - primitives, - viewport, - background_color, - overlay, - ) - }) + screenshot(self, renderer, viewport, background_color, overlay) } } @@ -338,19 +377,12 @@ impl graphics::Compositor for Compositor { /// /// Returns RGBA bytes of the texture data. pub fn screenshot<T: AsRef<str>>( - compositor: &Compositor, - backend: &mut Backend, - primitives: &[Primitive], + compositor: &mut Compositor, + renderer: &mut Renderer, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Vec<u8> { - let mut encoder = compositor.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu.offscreen.encoder"), - }, - ); - let dimensions = BufferDimensions::new(viewport.physical_size()); let texture_extent = wgpu::Extent3d { @@ -374,14 +406,20 @@ pub fn screenshot<T: AsRef<str>>( let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu.offscreen.encoder"), + }, + ); + + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), texture.format(), &view, - primitives, viewport, overlay, ); @@ -419,7 +457,7 @@ pub fn screenshot<T: AsRef<str>>( texture_extent, ); - let index = compositor.queue.submit(Some(encoder.finish())); + let index = compositor.engine.submit(&compositor.queue, encoder); let slice = output_buffer.slice(..); slice.map_async(wgpu::MapMode::Read, |_| {}); |