diff --git a/.gitignore b/.gitignore index 80e292024..0bdfeceb2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target # Trunk web build output (wasm/js bundle) dist +web/worker_pkg # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index d80235ada..1f5153720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,7 @@ name = "OpenCADStudio" version = "0.8.7" dependencies = [ "acadrust", + "bincode", "bytemuck", "clap", "console_error_panic_hook", @@ -74,7 +75,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc?rev=cd90c256a5e7d115f6e995275d6b3117d5775da9#cd90c256a5e7d115f6e995275d6b3117d5775da9" +source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=bee1a58#bee1a5857d444a32b67f98e6babc1ad6483f6865" dependencies = [ "ahash 0.8.12", "anyhow", @@ -2305,8 +2306,6 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff144a999b0ca0f8a10257934500060240825c42e950ec0ebee9c8ae30561c13" dependencies = [ "bitflags 2.13.1", "bytemuck", @@ -3667,6 +3666,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ocs_web_worker" +version = "0.1.0" +dependencies = [ + "acadrust", + "bincode", + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "once_cell" version = "1.21.4" diff --git a/Cargo.toml b/Cargo.toml index a22b95283..7ddbfb82a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ build = "build.rs" # verifies it. members = [ "crates/ocs_plugin_api", + "crates/ocs_web_worker", "crates/dwg-thumbnailer", "crates/dwg-thumbnailer-win", ] @@ -57,7 +58,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } # Opt-in logging via --log / RUST_LOG (surfaces wgpu / iced / winit diagnostics). env_logger = "0.11" -acadrust = "0.4" +acadrust = { version = "0.4", features = ["serde"] } # Shared DWG embedded-preview extraction (Start-page + file-manager thumbnails). dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" @@ -90,7 +91,11 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window [patch.crates-io] # Track the verified DWG round-trip, I/O, and unified PERF fixes. -acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", rev = "cd90c256a5e7d115f6e995275d6b3117d5775da9" } +acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "bee1a58" } +# iced_wgpu 0.14 requests WebGL2 limits on every wasm adapter, including +# BrowserWebGpu. Keep the release source local with the one adapter-aware limit +# fix so WebGPU can expose storage buffers while WebGL2 remains the fallback. +iced_wgpu = { path = "crates/iced_wgpu" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Native enables the plugin host runtime (out-of-process plugins). @@ -119,6 +124,7 @@ wasm-bindgen = "0.2" # web font loader (#141). wasm-bindgen-futures = "0.4" js-sys = "0.3" +bincode = "1.3" # window.open (external URLs) + Blob/anchor file downloads (Save) + fetch() # (Response) for the lazy per-script web font loader (#141). web-sys = { version = "0.3", features = [ @@ -130,6 +136,11 @@ web-sys = { version = "0.3", features = [ "Blob", "Url", "Response", + "Worker", + "WorkerOptions", + "WorkerType", + "MessageEvent", + "ErrorEvent", # Async clipboard read for paste into the text/MText editors (iced's own # clipboard read is a no-op on the web). "Clipboard", diff --git a/Trunk.toml b/Trunk.toml index 4c60188c6..86abdd79b 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -12,6 +12,11 @@ target = "index.html" # on the rust in index.html (truck-meshalgo → lzma-sys C deps cannot # cross-compile to wasm). +[[hooks]] +stage = "pre_build" +command = "sh" +command_arguments = ["scripts/build-web-worker.sh"] + [serve] # Cross-origin isolation headers enable SharedArrayBuffer, which wasm threads # (rayon) need. GitHub Pages cannot set these; a self-hosted server (e.g. the diff --git a/crates/iced_wgpu/Cargo.toml b/crates/iced_wgpu/Cargo.toml new file mode 100644 index 000000000..b80d3cf0e --- /dev/null +++ b/crates/iced_wgpu/Cargo.toml @@ -0,0 +1,146 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "iced_wgpu" +version = "0.14.0" +authors = ["Héctor Ramón Jiménez "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A renderer for iced on top of wgpu" +homepage = "https://iced.rs" +readme = "README.md" +keywords = [ + "gui", + "ui", + "graphics", + "interface", + "widgets", +] +categories = ["gui"] +license = "MIT" +repository = "https://github.com/iced-rs/iced" + +[package.metadata.docs.rs] +rustdoc-args = [ + "--cfg", + "docsrs", +] +all-features = true + +[features] +default = ["wgpu/default"] +geometry = [ + "iced_graphics/geometry", + "lyon", +] +image = ["iced_graphics/image"] +strict-assertions = [] +svg = [ + "iced_graphics/svg", + "resvg/text", +] +web-colors = ["iced_graphics/web-colors"] +webgl = ["wgpu/webgl"] + +[lib] +name = "iced_wgpu" +path = "src/lib.rs" + +[dependencies.bitflags] +version = "2.0" + +[dependencies.bytemuck] +version = "1.0" +features = ["derive"] + +[dependencies.cryoglyph] +version = "0.1" + +[dependencies.futures] +version = "0.3" +features = [ + "std", + "async-await", +] +default-features = false + +[dependencies.glam] +version = "0.25" + +[dependencies.guillotiere] +version = "0.6" + +[dependencies.iced_debug] +version = "0.14.0" + +[dependencies.iced_graphics] +version = "0.14.0" + +[dependencies.log] +version = "0.4" + +[dependencies.lyon] +version = "1.0" +optional = true + +[dependencies.resvg] +version = "0.45" +optional = true + +[dependencies.rustc-hash] +version = "2.0" + +[dependencies.thiserror] +version = "2" + +[dependencies.wgpu] +version = "27.0" +features = [ + "std", + "wgsl", +] +default-features = false + +[lints.clippy] +default_trait_access = "deny" +filter_map_next = "deny" +from_over_into = "deny" +large-enum-variant = "allow" +manual_let_else = "deny" +map-entry = "allow" +match-wildcard-for-single-variants = "deny" +needless_borrow = "deny" +new_without_default = "deny" +redundant-closure-for-method-calls = "deny" +result_large_err = "allow" +semicolon_if_nothing_returned = "deny" +trivially-copy-pass-by-ref = "deny" +type-complexity = "allow" +unused_async = "deny" +useless_conversion = "deny" + +[lints.rust] +missing_docs = "deny" +unsafe_code = "deny" +unused_results = "deny" + +[lints.rust.rust_2018_idioms] +level = "deny" +priority = -1 + +[lints.rustdoc] +broken_intra_doc_links = "forbid" diff --git a/crates/iced_wgpu/LICENSE b/crates/iced_wgpu/LICENSE new file mode 100644 index 000000000..2c16cff00 --- /dev/null +++ b/crates/iced_wgpu/LICENSE @@ -0,0 +1,19 @@ +Copyright 2019 Héctor Ramón, Iced contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/iced_wgpu/OCS_PATCH.md b/crates/iced_wgpu/OCS_PATCH.md new file mode 100644 index 000000000..40d42737a --- /dev/null +++ b/crates/iced_wgpu/OCS_PATCH.md @@ -0,0 +1,8 @@ +# OpenCADStudio patch + +This directory vendors `iced_wgpu` 0.14.0 under its MIT license. + +OpenCADStudio changes only the WASM device-limit selection in +`src/window/compositor.rs`: Browser WebGPU first requests normal WebGPU limits +so storage-buffer pipelines are available, then falls back to WebGL2 limits. +An actual WebGL adapter continues to request WebGL2 limits directly. diff --git a/crates/iced_wgpu/README.md b/crates/iced_wgpu/README.md new file mode 100644 index 000000000..8e9602ea6 --- /dev/null +++ b/crates/iced_wgpu/README.md @@ -0,0 +1,20 @@ +# `iced_wgpu` +[![Documentation](https://docs.rs/iced_wgpu/badge.svg)][documentation] +[![Crates.io](https://img.shields.io/crates/v/iced_wgpu.svg)](https://crates.io/crates/iced_wgpu) +[![License](https://img.shields.io/crates/l/iced_wgpu.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE) +[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) + +`iced_wgpu` is a [`wgpu`] renderer for [`iced_runtime`]. For now, it is the default renderer of Iced on [native platforms]. + +[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX12, OpenGL, and WebGPU. + +

+ The native target +

+ +[documentation]: https://docs.rs/iced_wgpu +[`iced_runtime`]: ../runtime +[`wgpu`]: https://github.com/gfx-rs/wgpu +[native platforms]: https://github.com/gfx-rs/wgpu#supported-platforms +[WebGPU API]: https://gpuweb.github.io/gpuweb/ +[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph diff --git a/crates/iced_wgpu/src/buffer.rs b/crates/iced_wgpu/src/buffer.rs new file mode 100644 index 000000000..172922da3 --- /dev/null +++ b/crates/iced_wgpu/src/buffer.rs @@ -0,0 +1,132 @@ +use std::marker::PhantomData; +use std::num::NonZeroU64; +use std::ops::RangeBounds; + +pub const MAX_WRITE_SIZE: usize = 100 * 1024; + +const MAX_WRITE_SIZE_U64: NonZeroU64 = NonZeroU64::new(MAX_WRITE_SIZE as u64) + .expect("MAX_WRITE_SIZE must be non-zero"); + +#[derive(Debug)] +pub struct Buffer { + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, + pub(crate) raw: wgpu::Buffer, + type_: PhantomData, +} + +impl Buffer { + pub fn new( + device: &wgpu::Device, + label: &'static str, + amount: usize, + usage: wgpu::BufferUsages, + ) -> Self { + let size = next_copy_size::(amount); + + let raw = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage, + mapped_at_creation: false, + }); + + Self { + label, + size, + usage, + raw, + type_: PhantomData, + } + } + + pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool { + let new_size = next_copy_size::(new_count); + + if self.size < new_size { + self.raw = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(self.label), + size: new_size, + usage: self.usage, + mapped_at_creation: false, + }); + + self.size = new_size; + + true + } else { + false + } + } + + /// Returns the size of the written bytes. + pub fn write( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + offset: usize, + contents: &[T], + ) -> usize { + let bytes: &[u8] = bytemuck::cast_slice(contents); + 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..]); + + bytes.len() + } + + pub fn slice( + &self, + bounds: impl RangeBounds, + ) -> wgpu::BufferSlice<'_> { + self.raw.slice(bounds) + } + + pub fn range(&self, start: usize, end: usize) -> wgpu::BufferSlice<'_> { + self.slice( + start as u64 * std::mem::size_of::() as u64 + ..end as u64 * std::mem::size_of::() as u64, + ) + } +} + +fn next_copy_size(amount: usize) -> u64 { + let align_mask = wgpu::COPY_BUFFER_ALIGNMENT - 1; + + (((std::mem::size_of::() * amount).next_power_of_two() as u64 + + align_mask) + & !align_mask) + .max(wgpu::COPY_BUFFER_ALIGNMENT) +} diff --git a/crates/iced_wgpu/src/color.rs b/crates/iced_wgpu/src/color.rs new file mode 100644 index 000000000..52fe84633 --- /dev/null +++ b/crates/iced_wgpu/src/color.rs @@ -0,0 +1,205 @@ +use std::borrow::Cow; + +use wgpu::util::DeviceExt; + +pub fn convert( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + source: wgpu::Texture, + format: wgpu::TextureFormat, +) -> wgpu::Texture { + if source.format() == format { + return source; + } + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("iced_wgpu.offscreen.sampler"), + ..wgpu::SamplerDescriptor::default() + }); + + #[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] + #[repr(C)] + struct Ratio { + u: f32, + v: f32, + // Padding field for 16-byte alignment. + // See https://docs.rs/wgpu/latest/wgpu/struct.DownlevelFlags.html#associatedconstant.BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED + _padding: [f32; 2], + } + + 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, + _padding: [0.0; 2], + }), + 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, + }, + 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.offscreen.sampler.bind_group"), + layout: &constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], + }); + + let texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.texture_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }], + }); + + let pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), + bind_group_layouts: &[&constant_layout, &texture_layout], + push_constant_ranges: &[], + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.offscreen.blit.shader"), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( + "shader/blit.wgsl" + ))), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default( + ), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default( + ), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu.offscreen.conversion.source_texture"), + size: source.size(), + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let view = &texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let texture_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.offscreen.blit.texture_bind_group"), + layout: &texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &source + .create_view(&wgpu::TextureViewDescriptor::default()), + ), + }], + }); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.offscreen.blit.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + depth_slice: None, + 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, + }); + + pass.set_pipeline(&pipeline); + pass.set_bind_group(0, &constant_bind_group, &[]); + pass.set_bind_group(1, &texture_bind_group, &[]); + pass.draw(0..6, 0..1); + + texture +} diff --git a/crates/iced_wgpu/src/engine.rs b/crates/iced_wgpu/src/engine.rs new file mode 100644 index 000000000..d86499eb6 --- /dev/null +++ b/crates/iced_wgpu/src/engine.rs @@ -0,0 +1,78 @@ +use crate::graphics::{Antialiasing, Shell}; +use crate::primitive; +use crate::quad; +use crate::text; +use crate::triangle; + +use std::sync::{Arc, RwLock}; + +#[derive(Clone)] +pub struct Engine { + pub(crate) device: wgpu::Device, + pub(crate) queue: wgpu::Queue, + 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: Arc>, + _shell: Shell, +} + +impl Engine { + pub fn new( + _adapter: &wgpu::Adapter, + device: wgpu::Device, + queue: wgpu::Queue, + format: wgpu::TextureFormat, + antialiasing: Option, // TODO: Initialize AA pipelines lazily + shell: Shell, + ) -> Self { + Self { + format, + + quad_pipeline: quad::Pipeline::new(&device, format), + text_pipeline: text::Pipeline::new(&device, &queue, format), + triangle_pipeline: triangle::Pipeline::new( + &device, + format, + antialiasing, + ), + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline: { + let backend = _adapter.get_info().backend; + + crate::image::Pipeline::new(&device, format, backend) + }, + + primitive_storage: Arc::new(RwLock::new( + primitive::Storage::default(), + )), + + device, + queue, + _shell: shell, + } + } + + #[cfg(any(feature = "image", feature = "svg"))] + pub fn create_image_cache(&self) -> crate::image::Cache { + self.image_pipeline.create_cache( + &self.device, + &self.queue, + &self._shell, + ) + } + + pub fn trim(&mut self) { + self.text_pipeline.trim(); + + self.primitive_storage + .write() + .expect("primitive storage should be writable") + .trim(); + } +} diff --git a/crates/iced_wgpu/src/geometry.rs b/crates/iced_wgpu/src/geometry.rs new file mode 100644 index 000000000..c1433c454 --- /dev/null +++ b/crates/iced_wgpu/src/geometry.rs @@ -0,0 +1,785 @@ +//! Build and draw geometry. +use crate::core::text::LineHeight; +use crate::core::{ + self, Pixels, Point, Radians, Rectangle, Size, Svg, Transformation, Vector, +}; +use crate::graphics::cache::{self, Cached}; +use crate::graphics::color; +use crate::graphics::geometry::fill::{self, Fill}; +use crate::graphics::geometry::{ + self, LineCap, LineDash, LineJoin, Path, Stroke, Style, +}; +use crate::graphics::gradient::{self, Gradient}; +use crate::graphics::mesh::{self, Mesh}; +use crate::graphics::{Image, Text}; +use crate::text; + +use lyon::geom::euclid; +use lyon::tessellation; + +use std::borrow::Cow; +use std::sync::Arc; + +#[derive(Debug)] +pub enum Geometry { + Live { + meshes: Vec, + images: Vec, + text: Vec, + }, + Cached(Cache), +} + +#[derive(Debug, Clone, Default)] +pub struct Cache { + pub meshes: Option, + pub images: Option>, + pub text: Option, +} + +impl Cached for Geometry { + type Cache = Cache; + + fn load(cache: &Self::Cache) -> Self { + Geometry::Cached(cache.clone()) + } + + fn cache( + self, + group: cache::Group, + previous: Option, + ) -> Self::Cache { + match self { + Self::Live { + meshes, + images, + text, + } => { + let images = if images.is_empty() { + None + } else { + Some(Arc::from(images)) + }; + + let meshes = Arc::from(meshes); + + if let Some(mut previous) = previous { + if let Some(cache) = &mut previous.meshes { + cache.update(meshes); + } else { + previous.meshes = if meshes.is_empty() { + None + } else { + Some(mesh::Cache::new(meshes)) + }; + } + + if let Some(cache) = &mut previous.text { + cache.update(text); + } else { + previous.text = text::Cache::new(group, text); + } + + previous.images = images; + + previous + } else { + Cache { + meshes: if meshes.is_empty() { + None + } else { + Some(mesh::Cache::new(meshes)) + }, + images, + text: text::Cache::new(group, text), + } + } + } + Self::Cached(cache) => cache, + } + } +} + +/// A frame for drawing some geometry. +pub struct Frame { + clip_bounds: Rectangle, + buffers: BufferStack, + meshes: Vec, + images: Vec, + text: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +impl Frame { + /// Creates a new [`Frame`] with the given clip bounds. + pub fn new(bounds: Rectangle) -> Frame { + Frame { + clip_bounds: bounds, + buffers: BufferStack::new(), + meshes: Vec::new(), + images: Vec::new(), + text: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform(lyon::math::Transform::identity()), + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } +} + +impl geometry::frame::Backend for Frame { + type Geometry = Geometry; + + #[inline] + fn width(&self) -> f32 { + self.clip_bounds.width + } + + #[inline] + fn height(&self) -> f32 { + self.clip_bounds.height + } + + #[inline] + fn size(&self) -> Size { + self.clip_bounds.size() + } + + #[inline] + fn center(&self) -> Point { + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) + } + + fn fill(&mut self, path: &Path, fill: impl Into) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity() { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.0); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); + + let size = + self.transforms.current.0.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity() { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.0); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); + + let size = + self.transforms.current.0.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + self.stroke_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Stroke rectangle"); + } + + fn stroke_text<'a>( + &mut self, + text: impl Into, + stroke: impl Into>, + ) { + let text = text.into(); + let stroke = stroke.into(); + + text.draw_with(|glyph, _color| self.stroke(&glyph, stroke)); + } + + fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let (scale_x, scale_y) = self.transforms.current.scale(); + + if self.transforms.current.is_scale_translation() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (bounds, size, line_height) = + if self.transforms.current.is_identity() { + ( + Rectangle::new( + text.position, + Size::new(text.max_width, f32::INFINITY), + ), + text.size, + text.line_height, + ) + } else { + let position = + self.transforms.current.transform_point(text.position); + + let size = Pixels(text.size.0 * scale_y); + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + ( + Rectangle::new( + position, + Size::new(text.max_width, f32::INFINITY), + ), + size, + line_height, + ) + }; + + self.text.push(Text::Cached { + content: text.content, + bounds, + color: text.color, + size, + line_height: line_height.to_absolute(size), + font: text.font, + align_x: text.align_x, + align_y: text.align_y, + shaping: text.shaping, + clip_bounds: self.clip_bounds, + }); + } else { + text.draw_with(|path, color| self.fill(&path, color)); + } + } + + #[inline] + fn translate(&mut self, translation: Vector) { + self.transforms.current.0 = + self.transforms + .current + .0 + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + } + + #[inline] + fn rotate(&mut self, angle: impl Into) { + self.transforms.current.0 = self + .transforms + .current + .0 + .pre_rotate(lyon::math::Angle::radians(angle.into().0)); + } + + #[inline] + fn scale(&mut self, scale: impl Into) { + let scale = scale.into(); + + self.scale_nonuniform(Vector { x: scale, y: scale }); + } + + #[inline] + fn scale_nonuniform(&mut self, scale: impl Into) { + let scale = scale.into(); + + self.transforms.current.0 = + self.transforms.current.0.pre_scale(scale.x, scale.y); + } + + 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::new(clip_bounds) + } + + fn paste(&mut self, frame: Frame) { + self.meshes.extend(frame.meshes); + self.meshes + .extend(frame.buffers.into_meshes(frame.clip_bounds)); + + self.images.extend(frame.images); + 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, + images: self.images, + text: self.text, + } + } + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into) { + let mut image = image.into(); + + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + image.rotation += external_rotation; + image.border_radius = + image.border_radius * self.transforms.current.scale().0; + + self.images.push(Image::Raster { + image, + bounds, + clip_bounds: self.clip_bounds, + }); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { + let mut svg = svg.into(); + + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + svg.rotation += external_rotation; + + self.images.push(Image::Vector { + svg, + bounds, + clip_bounds: self.clip_bounds, + }); + } +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient(tessellation::VertexBuffers), +} + +struct BufferStack { + stack: Vec, +} + +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(), + )); + } + }, + 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 { + 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 get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + 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 { + 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, + 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 + } + + fn transform_rectangle( + &self, + rectangle: Rectangle, + ) -> (Rectangle, Radians) { + let top_left = self.transform_point(rectangle.position()); + let top_right = self.transform_point( + rectangle.position() + Vector::new(rectangle.width, 0.0), + ); + let bottom_left = self.transform_point( + rectangle.position() + Vector::new(0.0, rectangle.height), + ); + + Rectangle::with_vertices(top_left, top_right, bottom_left) + } +} +struct GradientVertex2DBuilder { + gradient: gradient::Packed, +} + +impl tessellation::FillVertexConstructor + for GradientVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> mesh::GradientVertex2D { + let position = vertex.position(); + + mesh::GradientVertex2D { + position: [position.x, position.y], + gradient: self.gradient, + } + } +} + +impl tessellation::StrokeVertexConstructor + for GradientVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> mesh::GradientVertex2D { + let position = vertex.position(); + + mesh::GradientVertex2D { + position: [position.x, position.y], + gradient: self.gradient, + } + } +} + +struct TriangleVertex2DBuilder(color::Packed); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> mesh::SolidVertex2D { + let position = vertex.position(); + + mesh::SolidVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> mesh::SolidVertex2D { + let position = vertex.position(); + + mesh::SolidVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } +} + +fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } +} + +fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { + match rule { + fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, + fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } +} + +pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { + use lyon::algorithms::walk::{ + RepeatedPattern, WalkerEvent, walk_along_path, + }; + use lyon::path::iterator::PathIterator; + + Path::new(|builder| { + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); + + let mut draw_line = false; + + walk_along_path( + path.raw().iter().flattened( + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + ), + 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + &mut RepeatedPattern { + callback: |event: WalkerEvent<'_>| { + let point = Point { + x: event.position.x, + y: event.position.y, + }; + + if draw_line { + builder.line_to(point); + } else { + builder.move_to(point); + } + + draw_line = !draw_line; + + true + }, + index: line_dash.offset, + intervals: segments_odd + .as_deref() + .unwrap_or(line_dash.segments), + }, + ); + }) +} diff --git a/crates/iced_wgpu/src/image/atlas.rs b/crates/iced_wgpu/src/image/atlas.rs new file mode 100644 index 000000000..bbd71d4e7 --- /dev/null +++ b/crates/iced_wgpu/src/image/atlas.rs @@ -0,0 +1,542 @@ +pub mod entry; + +mod allocation; +mod allocator; +mod layer; + +pub use allocation::Allocation; +pub use entry::Entry; +pub use layer::Layer; + +use allocator::Allocator; + +pub const DEFAULT_SIZE: u32 = 2048; +pub const MAX_SIZE: u32 = 2048; + +use crate::core::Size; +use crate::graphics::color; + +use std::sync::Arc; + +#[derive(Debug)] +pub struct Atlas { + size: u32, + backend: wgpu::Backend, + texture: wgpu::Texture, + texture_view: wgpu::TextureView, + texture_bind_group: Arc, + texture_layout: wgpu::BindGroupLayout, + layers: Vec, +} + +impl Atlas { + pub fn new( + device: &wgpu::Device, + backend: wgpu::Backend, + texture_layout: wgpu::BindGroupLayout, + ) -> Self { + Self::with_size(device, backend, texture_layout, DEFAULT_SIZE) + } + + pub fn with_size( + device: &wgpu::Device, + backend: wgpu::Backend, + texture_layout: wgpu::BindGroupLayout, + size: u32, + ) -> Self { + let size = size.min(MAX_SIZE); + + 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` + // https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371 + wgpu::Backend::Gl => vec![Layer::Empty, Layer::Empty], + _ => vec![Layer::Empty], + }; + + let extent = wgpu::Extent3d { + width: size, + height: size, + depth_or_array_layers: layers.len() as u32, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu::image texture atlas"), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: if color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + usage: wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2Array), + ..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 { + size, + backend, + texture, + texture_view, + texture_bind_group: Arc::new(texture_bind_group), + texture_layout, + layers, + } + } + + pub fn bind_group(&self) -> &Arc { + &self.texture_bind_group + } + + pub fn upload( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + width: u32, + height: u32, + pixels: &[u8], + ) -> Option { + let entry = { + let current_size = self.layers.len(); + let entry = self.allocate(width, height)?; + + // We grow the internal texture after allocating if necessary + let new_layers = self.layers.len() - current_size; + self.grow(new_layers, device, encoder, self.backend); + + entry + }; + + log::debug!("Allocated atlas entry: {entry:?}"); + + match &entry { + Entry::Contiguous(allocation) => { + self.upload_allocation( + pixels, width, 0, allocation, device, encoder, belt, + ); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + let (x, y) = fragment.position; + let offset = 4 * (y * width + x) as usize; + + self.upload_allocation( + pixels, + width, + offset, + &fragment.allocation, + device, + encoder, + belt, + ); + } + } + } + + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "Atlas layers: {} (busy: {}, allocations: {})", + self.layers.len(), + self.layers.iter().filter(|layer| !layer.is_empty()).count(), + self.layers.iter().map(Layer::allocations).sum::(), + ); + } + + Some(entry) + } + + pub fn remove(&mut self, entry: &Entry) { + log::debug!("Removing atlas entry: {entry:?}"); + + match entry { + Entry::Contiguous(allocation) => { + self.deallocate(allocation); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + self.deallocate(&fragment.allocation); + } + } + } + } + + fn allocate(&mut self, width: u32, height: u32) -> Option { + // Allocate one layer if texture fits perfectly + if width == self.size && height == self.size { + let mut empty_layers = self + .layers + .iter_mut() + .enumerate() + .filter(|(_, layer)| layer.is_empty()); + + if let Some((i, layer)) = empty_layers.next() { + *layer = Layer::Full; + + return Some(Entry::Contiguous(Allocation::Full { + layer: i, + size: self.size, + })); + } + + self.layers.push(Layer::Full); + + return Some(Entry::Contiguous(Allocation::Full { + layer: self.layers.len() - 1, + size: self.size, + })); + } + + // Split big textures across multiple layers + if width > self.size || height > self.size { + let mut fragments = Vec::new(); + let mut y = 0; + + while y < height { + let height = std::cmp::min(height - y, self.size); + let mut x = 0; + + while x < width { + let width = std::cmp::min(width - x, self.size); + + let allocation = self.allocate(width, height)?; + + if let Entry::Contiguous(allocation) = allocation { + fragments.push(entry::Fragment { + position: (x, y), + allocation, + }); + } + + x += width; + } + + y += height; + } + + return Some(Entry::Fragmented { + size: Size::new(width, height), + fragments, + }); + } + + // Try allocating on an existing layer + for (i, layer) in self.layers.iter_mut().enumerate() { + match layer { + Layer::Empty => { + let mut allocator = Allocator::new(self.size); + + if let Some(region) = allocator.allocate(width, height) { + *layer = Layer::Busy(allocator); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + atlas_size: self.size, + })); + } + } + Layer::Busy(allocator) => { + if let Some(region) = allocator.allocate(width, height) { + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + atlas_size: self.size, + })); + } + } + Layer::Full => {} + } + } + + // Create new layer with atlas allocator + let mut allocator = Allocator::new(self.size); + + if let Some(region) = allocator.allocate(width, height) { + self.layers.push(Layer::Busy(allocator)); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: self.layers.len() - 1, + atlas_size: self.size, + })); + } + + // We ran out of memory (?) + None + } + + fn deallocate(&mut self, allocation: &Allocation) { + log::debug!("Deallocating atlas: {allocation:?}"); + + match allocation { + Allocation::Full { layer, .. } => { + self.layers[*layer] = Layer::Empty; + } + Allocation::Partial { layer, region, .. } => { + let layer = &mut self.layers[*layer]; + + if let Layer::Busy(allocator) = layer { + allocator.deallocate(region); + + if allocator.is_empty() { + *layer = Layer::Empty; + } + } + } + } + } + + fn upload_allocation( + &self, + pixels: &[u8], + image_width: u32, + offset: usize, + allocation: &Allocation, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + ) { + let (x, y) = allocation.position(); + let Size { width, height } = allocation.size(); + let layer = allocation.layer(); + let padding = allocation.padding(); + + // It is a webgpu requirement that: + // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 + // So we calculate bytes_per_row by rounding width up to the next + // multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT. + let bytes_per_row = (4 * (width + padding.width * 2)) + .next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) + as usize; + let total_bytes = + bytes_per_row * (height + padding.height * 2) as usize; + + let buffer_slice = belt.allocate( + wgpu::BufferSize::new(total_bytes as u64).unwrap(), + wgpu::BufferSize::new(8 * 4).unwrap(), + device, + ); + + const PIXEL: usize = 4; + + let mut fragment = buffer_slice.get_mapped_range_mut(); + let w = width as usize; + let h = height as usize; + let pad_w = padding.width as usize; + let pad_h = padding.height as usize; + let stride = PIXEL * w; + + // Copy image rows + for row in 0..h { + let src = offset + row * PIXEL * image_width as usize; + let dst = (row + pad_h) * bytes_per_row; + + fragment[dst + PIXEL * pad_w..dst + PIXEL * pad_w + stride] + .copy_from_slice(&pixels[src..src + stride]); + + // Add padding to the sides, if needed + for i in 0..pad_w { + fragment[dst + PIXEL * i..dst + PIXEL * (i + 1)] + .copy_from_slice(&pixels[src..src + PIXEL]); + + fragment[dst + stride + PIXEL * (pad_w + i) + ..dst + stride + PIXEL * (pad_w + i + 1)] + .copy_from_slice( + &pixels[src + stride - PIXEL..src + stride], + ); + } + } + + // Add padding on top and bottom + for row in 0..pad_h { + let dst_top = row * bytes_per_row; + let dst_bottom = (pad_h + h + row) * bytes_per_row; + let src_top = offset; + let src_bottom = offset + (h - 1) * PIXEL * image_width as usize; + + // Top + fragment[dst_top + PIXEL * pad_w..dst_top + PIXEL * (pad_w + w)] + .copy_from_slice(&pixels[src_top..src_top + PIXEL * w]); + + // Bottom + fragment + [dst_bottom + PIXEL * pad_w..dst_bottom + PIXEL * (pad_w + w)] + .copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL * w]); + + // Corners + for i in 0..pad_w { + // Top left + fragment[dst_top + PIXEL * i..dst_top + PIXEL * (i + 1)] + .copy_from_slice(&pixels[offset..offset + PIXEL]); + + // Top right + fragment[dst_top + PIXEL * (w + pad_w + i) + ..dst_top + PIXEL * (w + pad_w + i + 1)] + .copy_from_slice( + &pixels[offset + PIXEL * (w - 1)..offset + PIXEL * w], + ); + + // Bottom left + fragment[dst_bottom + PIXEL * i..dst_bottom + PIXEL * (i + 1)] + .copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL]); + + // Bottom right + fragment[dst_bottom + PIXEL * (w + pad_w + i) + ..dst_bottom + PIXEL * (w + pad_w + i + 1)] + .copy_from_slice( + &pixels[src_bottom + PIXEL * (w - 1) + ..src_bottom + PIXEL * w], + ); + } + } + + // Copy actual image + encoder.copy_buffer_to_texture( + wgpu::TexelCopyBufferInfo { + buffer: buffer_slice.buffer(), + layout: wgpu::TexelCopyBufferLayout { + offset: buffer_slice.offset(), + bytes_per_row: Some(bytes_per_row as u32), + rows_per_image: Some(height + padding.height * 2), + }, + }, + wgpu::TexelCopyTextureInfo { + texture: &self.texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: x - padding.width, + y: y - padding.height, + z: layer as u32, + }, + aspect: wgpu::TextureAspect::default(), + }, + wgpu::Extent3d { + width: width + padding.width * 2, + height: height + padding.height * 2, + depth_or_array_layers: 1, + }, + ); + } + + fn grow( + &mut self, + amount: usize, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + backend: wgpu::Backend, + ) { + if amount == 0 { + return; + } + + // On the GL backend if layers.len() is a multiple of 6 we need to help wgpu figure out that this texture + // is still a `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_CUBE_MAP` or `GL_TEXTURE_CUBE_ARRAY`. + // This will over-allocate some unused memory on GL, but it's better than not being able to + // grow the atlas past multiples of 6! + // https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371 + let depth_or_array_layers = match backend { + wgpu::Backend::Gl if self.layers.len().is_multiple_of(6) => { + self.layers.len() as u32 + 1 + } + _ => self.layers.len() as u32, + }; + + let new_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu::image texture atlas"), + size: wgpu::Extent3d { + width: self.size, + height: self.size, + depth_or_array_layers, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: if color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + usage: wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let amount_to_copy = self.layers.len() - amount; + + for (i, layer) in + self.layers.iter_mut().take(amount_to_copy).enumerate() + { + if layer.is_empty() { + continue; + } + + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + texture: &self.texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: i as u32, + }, + aspect: wgpu::TextureAspect::default(), + }, + wgpu::TexelCopyTextureInfo { + texture: &new_texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: i as u32, + }, + aspect: wgpu::TextureAspect::default(), + }, + wgpu::Extent3d { + width: self.size, + height: self.size, + depth_or_array_layers: 1, + }, + ); + } + + self.texture = new_texture; + self.texture_view = + self.texture.create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2Array), + ..Default::default() + }); + + self.texture_bind_group = + Arc::new(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/crates/iced_wgpu/src/image/atlas/allocation.rs b/crates/iced_wgpu/src/image/atlas/allocation.rs new file mode 100644 index 000000000..295a7a132 --- /dev/null +++ b/crates/iced_wgpu/src/image/atlas/allocation.rs @@ -0,0 +1,52 @@ +use crate::core::Size; +use crate::image::atlas::allocator; + +#[derive(Debug)] +pub enum Allocation { + Partial { + layer: usize, + region: allocator::Region, + atlas_size: u32, + }, + Full { + layer: usize, + size: u32, + }, +} + +impl Allocation { + pub fn position(&self) -> (u32, u32) { + match self { + Allocation::Partial { region, .. } => region.position(), + Allocation::Full { .. } => (0, 0), + } + } + + pub fn size(&self) -> Size { + match self { + Allocation::Partial { region, .. } => region.size(), + Allocation::Full { size, .. } => Size::new(*size, *size), + } + } + + pub fn padding(&self) -> Size { + match self { + Allocation::Partial { region, .. } => region.padding(), + Allocation::Full { .. } => Size::new(0, 0), + } + } + + pub fn layer(&self) -> usize { + match self { + Allocation::Partial { layer, .. } => *layer, + Allocation::Full { layer, .. } => *layer, + } + } + + pub fn atlas_size(&self) -> u32 { + match self { + Allocation::Partial { atlas_size, .. } => *atlas_size, + Allocation::Full { size, .. } => *size, + } + } +} diff --git a/crates/iced_wgpu/src/image/atlas/allocator.rs b/crates/iced_wgpu/src/image/atlas/allocator.rs new file mode 100644 index 000000000..263619f6e --- /dev/null +++ b/crates/iced_wgpu/src/image/atlas/allocator.rs @@ -0,0 +1,108 @@ +use crate::core; + +use guillotiere::{AtlasAllocator, Size}; + +pub struct Allocator { + raw: AtlasAllocator, + allocations: usize, +} + +impl Allocator { + const PADDING: u32 = 1; + + pub fn new(size: u32) -> Allocator { + let raw = AtlasAllocator::new(Size::new(size as i32, size as i32)); + + Allocator { + raw, + allocations: 0, + } + } + + pub fn allocate(&mut self, width: u32, height: u32) -> Option { + let size = self.raw.size(); + + let padded_width = width + Self::PADDING * 2; + let padded_height = height + Self::PADDING * 2; + + let pad_width = padded_width as i32 <= size.width; + let pad_height = padded_height as i32 <= size.height; + + let mut allocation = self.raw.allocate(Size::new( + if pad_width { padded_width } else { width } as i32, + if pad_height { padded_height } else { height } as i32, + ))?; + + if pad_width { + allocation.rectangle.min.x += Self::PADDING as i32; + allocation.rectangle.max.x -= Self::PADDING as i32; + } + + if pad_height { + allocation.rectangle.min.y += Self::PADDING as i32; + allocation.rectangle.max.y -= Self::PADDING as i32; + } + + self.allocations += 1; + + Some(Region { + allocation, + padding: core::Size::new( + if pad_width { Self::PADDING } else { 0 }, + if pad_height { Self::PADDING } else { 0 }, + ), + }) + } + + pub fn deallocate(&mut self, region: &Region) { + self.raw.deallocate(region.allocation.id); + + self.allocations = self.allocations.saturating_sub(1); + } + + pub fn is_empty(&self) -> bool { + self.allocations == 0 + } + + pub fn allocations(&self) -> usize { + self.allocations + } +} + +pub struct Region { + allocation: guillotiere::Allocation, + padding: core::Size, +} + +impl Region { + pub fn position(&self) -> (u32, u32) { + let rectangle = &self.allocation.rectangle; + + (rectangle.min.x as u32, rectangle.min.y as u32) + } + + pub fn size(&self) -> core::Size { + let size = self.allocation.rectangle.size(); + + core::Size::new(size.width as u32, size.height as u32) + } + + pub fn padding(&self) -> crate::core::Size { + self.padding + } +} + +impl std::fmt::Debug for Allocator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Allocator") + } +} + +impl std::fmt::Debug for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Region") + .field("id", &self.allocation.id) + .field("rectangle", &self.allocation.rectangle) + .finish() + } +} diff --git a/crates/iced_wgpu/src/image/atlas/entry.rs b/crates/iced_wgpu/src/image/atlas/entry.rs new file mode 100644 index 000000000..7e4c92a2c --- /dev/null +++ b/crates/iced_wgpu/src/image/atlas/entry.rs @@ -0,0 +1,27 @@ +use crate::core::Size; +use crate::image::atlas; + +#[derive(Debug)] +pub enum Entry { + Contiguous(atlas::Allocation), + Fragmented { + size: Size, + fragments: Vec, + }, +} + +impl Entry { + #[cfg(feature = "image")] + pub fn size(&self) -> Size { + match self { + Entry::Contiguous(allocation) => allocation.size(), + Entry::Fragmented { size, .. } => *size, + } + } +} + +#[derive(Debug)] +pub struct Fragment { + pub position: (u32, u32), + pub allocation: atlas::Allocation, +} diff --git a/crates/iced_wgpu/src/image/atlas/layer.rs b/crates/iced_wgpu/src/image/atlas/layer.rs new file mode 100644 index 000000000..fd6788d9c --- /dev/null +++ b/crates/iced_wgpu/src/image/atlas/layer.rs @@ -0,0 +1,22 @@ +use crate::image::atlas::Allocator; + +#[derive(Debug)] +pub enum Layer { + Empty, + Busy(Allocator), + Full, +} + +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/crates/iced_wgpu/src/image/cache.rs b/crates/iced_wgpu/src/image/cache.rs new file mode 100644 index 000000000..5e8059e8e --- /dev/null +++ b/crates/iced_wgpu/src/image/cache.rs @@ -0,0 +1,636 @@ +use crate::core::{self, Size}; +use crate::graphics::Shell; +use crate::image::atlas::{self, Atlas}; + +#[cfg(all(feature = "image", not(target_arch = "wasm32")))] +use worker::Worker; + +#[cfg(feature = "image")] +use std::collections::HashMap; + +use std::sync::Arc; + +pub struct Cache { + atlas: Atlas, + #[cfg(feature = "image")] + raster: Raster, + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache, + #[cfg(all(feature = "image", not(target_arch = "wasm32")))] + worker: Worker, +} + +impl Cache { + pub fn new( + device: &wgpu::Device, + _queue: &wgpu::Queue, + backend: wgpu::Backend, + layout: wgpu::BindGroupLayout, + _shell: &Shell, + ) -> Self { + #[cfg(all(feature = "image", not(target_arch = "wasm32")))] + let worker = + Worker::new(device, _queue, backend, layout.clone(), _shell); + + Self { + atlas: Atlas::new(device, backend, layout), + #[cfg(feature = "image")] + raster: Raster { + cache: crate::image::raster::Cache::default(), + pending: HashMap::new(), + belt: wgpu::util::StagingBelt::new(2 * 1024 * 1024), + }, + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache::default(), + #[cfg(all(feature = "image", not(target_arch = "wasm32")))] + worker, + } + } + + #[cfg(feature = "image")] + pub fn allocate_image( + &mut self, + handle: &core::image::Handle, + callback: impl FnOnce(Result) + + Send + + 'static, + ) { + use crate::image::raster::Memory; + + let callback = Box::new(callback); + + if let Some(callbacks) = self.raster.pending.get_mut(&handle.id()) { + callbacks.push(callback); + return; + } + + if let Some(Memory::Device { + allocation, entry, .. + }) = self.raster.cache.get_mut(handle) + { + if let Some(allocation) = allocation + .as_ref() + .and_then(core::image::Allocation::upgrade) + { + callback(Ok(allocation)); + return; + } + + #[allow(unsafe_code)] + let new = unsafe { core::image::allocate(handle, entry.size()) }; + *allocation = Some(new.downgrade()); + callback(Ok(new)); + + return; + } + + let _ = self.raster.pending.insert(handle.id(), vec![callback]); + + #[cfg(not(target_arch = "wasm32"))] + self.worker.load(handle); + } + + #[cfg(feature = "image")] + pub fn load_image( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + handle: &core::image::Handle, + ) -> Result { + use crate::image::raster::Memory; + + if !self.raster.cache.contains(handle) { + self.raster.cache.insert(handle, Memory::load(handle)); + } + + match self.raster.cache.get_mut(handle).unwrap() { + Memory::Host(image) => { + let mut encoder = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("raster image upload"), + }, + ); + + let entry = self.atlas.upload( + device, + &mut encoder, + &mut self.raster.belt, + image.width(), + image.height(), + image, + ); + + self.raster.belt.finish(); + let submission = queue.submit([encoder.finish()]); + self.raster.belt.recall(); + + let Some(entry) = entry else { + return Err(core::image::Error::OutOfMemory); + }; + + let _ = device.poll(wgpu::PollType::Wait { + submission_index: Some(submission), + timeout: None, + }); + + #[allow(unsafe_code)] + let allocation = unsafe { + core::image::allocate( + handle, + Size::new(image.width(), image.height()), + ) + }; + + self.raster.cache.insert( + handle, + Memory::Device { + entry, + bind_group: None, + allocation: Some(allocation.downgrade()), + }, + ); + + Ok(allocation) + } + Memory::Device { + entry, allocation, .. + } => { + if let Some(allocation) = allocation + .as_ref() + .and_then(core::image::Allocation::upgrade) + { + return Ok(allocation); + } + + #[allow(unsafe_code)] + let new = + unsafe { core::image::allocate(handle, entry.size()) }; + + *allocation = Some(new.downgrade()); + + Ok(new) + } + Memory::Error(error) => Err(error.clone()), + } + } + + #[cfg(feature = "image")] + pub fn measure_image( + &mut self, + handle: &core::image::Handle, + ) -> Option> { + self.receive(); + + let image = load_image( + &mut self.raster.cache, + &mut self.raster.pending, + #[cfg(not(target_arch = "wasm32"))] + &self.worker, + handle, + None, + )?; + + Some(image.dimensions()) + } + + #[cfg(feature = "svg")] + pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size { + // TODO: Concurrency + self.vector.load(handle).viewport_dimensions() + } + + #[cfg(feature = "image")] + pub fn upload_raster( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + handle: &core::image::Handle, + ) -> Option<(&atlas::Entry, &Arc)> { + use crate::image::raster::Memory; + + self.receive(); + + let memory = load_image( + &mut self.raster.cache, + &mut self.raster.pending, + #[cfg(not(target_arch = "wasm32"))] + &self.worker, + handle, + None, + )?; + + if let Memory::Device { + entry, bind_group, .. + } = memory + { + return Some(( + entry, + bind_group.as_ref().unwrap_or(self.atlas.bind_group()), + )); + } + + let image = memory.host()?; + + const MAX_SYNC_SIZE: usize = 2 * 1024 * 1024; + + // TODO: Concurrent Wasm support + if image.len() < MAX_SYNC_SIZE || cfg!(target_arch = "wasm32") { + let entry = self.atlas.upload( + device, + encoder, + belt, + image.width(), + image.height(), + &image, + )?; + + *memory = Memory::Device { + entry, + bind_group: None, + allocation: None, + }; + + if let Memory::Device { entry, .. } = memory { + return Some((entry, self.atlas.bind_group())); + } + } + + if !self.raster.pending.contains_key(&handle.id()) { + let _ = self.raster.pending.insert(handle.id(), Vec::new()); + + #[cfg(not(target_arch = "wasm32"))] + self.worker.upload(handle, image); + } + + None + } + + #[cfg(feature = "svg")] + pub fn upload_vector( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + handle: &core::svg::Handle, + color: Option, + size: Size, + scale: f32, + ) -> Option<(&atlas::Entry, &Arc)> { + // TODO: Concurrency + self.vector + .upload( + device, + encoder, + belt, + handle, + color, + size, + scale, + &mut self.atlas, + ) + .map(|entry| (entry, self.atlas.bind_group())) + } + + pub fn trim(&mut self) { + #[cfg(feature = "image")] + { + self.receive(); + self.raster.cache.trim(&mut self.atlas, |_bind_group| { + #[cfg(not(target_arch = "wasm32"))] + self.worker.drop(_bind_group); + }); + } + + #[cfg(feature = "svg")] + self.vector.trim(&mut self.atlas); // TODO: Concurrency + } + + #[cfg(feature = "image")] + fn receive(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + while let Ok(work) = self.worker.try_recv() { + use crate::image::raster::Memory; + + match work { + worker::Work::Upload { + handle, + entry, + bind_group, + } => { + let callbacks = self.raster.pending.remove(&handle.id()); + + let allocation = if let Some(callbacks) = callbacks { + #[allow(unsafe_code)] + let allocation = unsafe { + core::image::allocate(&handle, entry.size()) + }; + + let reference = allocation.downgrade(); + + for callback in callbacks { + callback(Ok(allocation.clone())); + } + + Some(reference) + } else { + None + }; + + self.raster.cache.insert( + &handle, + Memory::Device { + entry, + bind_group: Some(bind_group), + allocation, + }, + ); + } + worker::Work::Error { handle, error } => { + let callbacks = self.raster.pending.remove(&handle.id()); + + if let Some(callbacks) = callbacks { + for callback in callbacks { + callback(Err(error.clone())); + } + } + + self.raster.cache.insert(&handle, Memory::Error(error)); + } + } + } + } +} + +#[cfg(all(feature = "image", not(target_arch = "wasm32")))] +impl Drop for Cache { + fn drop(&mut self) { + self.worker.quit(); + } +} + +#[cfg(feature = "image")] +struct Raster { + cache: crate::image::raster::Cache, + pending: HashMap>, + belt: wgpu::util::StagingBelt, +} + +#[cfg(feature = "image")] +type Callback = + Box) + Send>; + +#[cfg(feature = "image")] +fn load_image<'a>( + cache: &'a mut crate::image::raster::Cache, + pending: &mut HashMap>, + #[cfg(not(target_arch = "wasm32"))] worker: &Worker, + handle: &core::image::Handle, + callback: Option, +) -> Option<&'a mut crate::image::raster::Memory> { + use crate::image::raster::Memory; + + if !cache.contains(handle) { + if cfg!(target_arch = "wasm32") { + // TODO: Concurrent support for Wasm + cache.insert(handle, Memory::load(handle)); + } else if let core::image::Handle::Rgba { .. } = handle { + // Load RGBA handles synchronously, since it's very cheap + cache.insert(handle, Memory::load(handle)); + } else if !pending.contains_key(&handle.id()) { + let _ = pending.insert(handle.id(), Vec::from_iter(callback)); + + #[cfg(not(target_arch = "wasm32"))] + worker.load(handle); + } + } + + cache.get_mut(handle) +} + +#[cfg(all(feature = "image", not(target_arch = "wasm32")))] +mod worker { + use crate::core::Bytes; + use crate::core::image; + use crate::graphics::Shell; + use crate::image::atlas::{self, Atlas}; + use crate::image::raster; + + use std::sync::Arc; + use std::sync::mpsc; + use std::thread; + + pub struct Worker { + jobs: mpsc::SyncSender, + quit: mpsc::SyncSender<()>, + work: mpsc::Receiver, + handle: Option>, + } + + impl Worker { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + backend: wgpu::Backend, + texture_layout: wgpu::BindGroupLayout, + shell: &Shell, + ) -> Self { + let (jobs_sender, jobs_receiver) = mpsc::sync_channel(1_000); + let (quit_sender, quit_receiver) = mpsc::sync_channel(1); + let (work_sender, work_receiver) = mpsc::sync_channel(1_000); + + let instance = Instance { + device: device.clone(), + queue: queue.clone(), + backend, + texture_layout, + shell: shell.clone(), + belt: wgpu::util::StagingBelt::new(4 * 1024 * 1024), + jobs: jobs_receiver, + output: work_sender, + quit: quit_receiver, + }; + + let handle = thread::spawn(move || instance.run()); + + Self { + jobs: jobs_sender, + quit: quit_sender, + work: work_receiver, + handle: Some(handle), + } + } + + pub fn load(&self, handle: &image::Handle) { + let _ = self.jobs.send(Job::Load(handle.clone())); + } + + pub fn upload(&self, handle: &image::Handle, image: raster::Image) { + let _ = self.jobs.send(Job::Upload { + handle: handle.clone(), + width: image.width(), + height: image.height(), + rgba: image.into_raw(), + }); + } + + pub fn drop(&self, bind_group: Arc) { + let _ = self.jobs.send(Job::Drop(bind_group)); + } + + pub fn try_recv(&self) -> Result { + self.work.try_recv() + } + + pub fn quit(&mut self) { + let _ = self.quit.try_send(()); + let _ = self.jobs.send(Job::Quit); + let _ = self.handle.take().map(thread::JoinHandle::join); + } + } + + pub struct Instance { + device: wgpu::Device, + queue: wgpu::Queue, + backend: wgpu::Backend, + texture_layout: wgpu::BindGroupLayout, + shell: Shell, + belt: wgpu::util::StagingBelt, + jobs: mpsc::Receiver, + output: mpsc::SyncSender, + quit: mpsc::Receiver<()>, + } + + #[derive(Debug)] + enum Job { + Load(image::Handle), + Upload { + handle: image::Handle, + rgba: Bytes, + width: u32, + height: u32, + }, + Drop(Arc), + Quit, + } + + pub enum Work { + Upload { + handle: image::Handle, + entry: atlas::Entry, + bind_group: Arc, + }, + Error { + handle: image::Handle, + error: image::Error, + }, + } + + impl Instance { + fn run(mut self) { + loop { + if self.quit.try_recv().is_ok() { + return; + } + + let Ok(job) = self.jobs.recv() else { + return; + }; + + match job { + Job::Load(handle) => { + match crate::graphics::image::load(&handle) { + Ok(image) => self.upload( + handle, + image.width(), + image.height(), + image.into_raw(), + Shell::invalidate_layout, + ), + Err(error) => { + let _ = self + .output + .send(Work::Error { handle, error }); + } + } + } + Job::Upload { + handle, + rgba, + width, + height, + } => { + self.upload( + handle, + width, + height, + rgba, + Shell::request_redraw, + ); + } + Job::Drop(bind_group) => { + drop(bind_group); + } + Job::Quit => return, + } + } + } + + fn upload( + &mut self, + handle: image::Handle, + width: u32, + height: u32, + rgba: Bytes, + callback: fn(&Shell), + ) { + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("raster image upload"), + }, + ); + + let mut atlas = Atlas::with_size( + &self.device, + self.backend, + self.texture_layout.clone(), + width.max(height), + ); + + let Some(entry) = atlas.upload( + &self.device, + &mut encoder, + &mut self.belt, + width, + height, + &rgba, + ) else { + return; + }; + + let output = self.output.clone(); + let shell = self.shell.clone(); + + self.belt.finish(); + let submission = self.queue.submit([encoder.finish()]); + self.belt.recall(); + + let bind_group = atlas.bind_group().clone(); + + self.queue.on_submitted_work_done(move || { + let _ = output.send(Work::Upload { + handle, + entry, + bind_group, + }); + + callback(&shell); + }); + + let _ = self.device.poll(wgpu::PollType::Wait { + submission_index: Some(submission), + timeout: None, + }); + } + } +} diff --git a/crates/iced_wgpu/src/image/mod.rs b/crates/iced_wgpu/src/image/mod.rs new file mode 100644 index 000000000..ba98f4432 --- /dev/null +++ b/crates/iced_wgpu/src/image/mod.rs @@ -0,0 +1,758 @@ +pub(crate) mod cache; +pub(crate) use cache::Cache; + +mod atlas; + +#[cfg(feature = "image")] +mod raster; + +#[cfg(feature = "svg")] +mod vector; + +use crate::Buffer; +use crate::core::border; +use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::Shell; + +use bytemuck::{Pod, Zeroable}; + +use std::mem; +use std::sync::Arc; + +pub use crate::graphics::Image; + +pub type Batch = Vec; + +#[derive(Debug, Clone)] +pub struct Pipeline { + raw: wgpu::RenderPipeline, + backend: wgpu::Backend, + nearest_sampler: wgpu::Sampler, + linear_sampler: wgpu::Sampler, + texture_layout: wgpu::BindGroupLayout, + constant_layout: wgpu::BindGroupLayout, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + backend: wgpu::Backend, + ) -> Self { + let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + min_filter: wgpu::FilterMode::Nearest, + mag_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + min_filter: wgpu::FilterMode::Linear, + mag_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let constant_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu::image constants layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new( + mem::size_of::() as u64, + ), + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::Filtering, + ), + count: None, + }, + ], + }); + + let texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu::image texture atlas layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::D2Array, + multisampled: false, + }, + count: None, + }], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu::image pipeline layout"), + push_constant_ranges: &[], + bind_group_layouts: &[&constant_layout, &texture_layout], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu image shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + concat!( + include_str!("../shader/vertex.wgsl"), + "\n", + include_str!("../shader/image.wgsl"), + ), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu::image pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Center + 0 => Float32x2, + // Clip bounds + 1 => Float32x4, + // Border radius + 2 => Float32x4, + // Tile + 3 => Float32x4, + // Rotation + 4 => Float32, + // Opacity + 5 => Float32, + // Atlas position + 6 => Float32x2, + // Atlas scale + 7 => Float32x2, + // Layer + 8 => Sint32, + // Snap + 9 => Uint32, + ), + }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + Pipeline { + raw: pipeline, + backend, + nearest_sampler, + linear_sampler, + texture_layout, + constant_layout, + } + } + + pub fn create_cache( + &self, + device: &wgpu::Device, + queue: &wgpu::Queue, + shell: &Shell, + ) -> Cache { + Cache::new( + device, + queue, + self.backend, + self.texture_layout.clone(), + shell, + ) + } +} + +#[derive(Default)] +pub struct State { + layers: Vec, + prepare_layer: usize, + nearest_instances: Vec, + linear_instances: Vec, +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn prepare( + &mut self, + pipeline: &Pipeline, + device: &wgpu::Device, + belt: &mut wgpu::util::StagingBelt, + encoder: &mut wgpu::CommandEncoder, + cache: &mut Cache, + images: &Batch, + transformation: Transformation, + scale: f32, + ) { + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &pipeline.constant_layout, + &pipeline.nearest_sampler, + &pipeline.linear_sampler, + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + + let mut atlas: Option> = None; + + for image in images { + match &image { + #[cfg(feature = "image")] + Image::Raster { + image, + bounds, + clip_bounds, + } => { + if let Some((atlas_entry, bind_group)) = cache + .upload_raster(device, encoder, belt, &image.handle) + { + match atlas.as_mut() { + None => { + atlas = Some(bind_group.clone()); + } + Some(atlas) if atlas != bind_group => { + layer.push( + atlas, + &self.nearest_instances, + &self.linear_instances, + ); + + *atlas = Arc::clone(bind_group); + } + _ => {} + } + + add_instances( + *bounds, + *clip_bounds, + image.border_radius, + f32::from(image.rotation), + image.opacity, + image.snap, + atlas_entry, + match image.filter_method { + crate::core::image::FilterMethod::Nearest => { + &mut self.nearest_instances + } + crate::core::image::FilterMethod::Linear => { + &mut self.linear_instances + } + }, + ); + } + } + #[cfg(not(feature = "image"))] + Image::Raster { .. } => continue, + + #[cfg(feature = "svg")] + Image::Vector { + svg, + bounds, + clip_bounds, + } => { + if let Some((atlas_entry, bind_group)) = cache + .upload_vector( + device, + encoder, + belt, + &svg.handle, + svg.color, + bounds.size(), + scale, + ) + { + match atlas.as_mut() { + None => { + atlas = Some(bind_group.clone()); + } + Some(atlas) if atlas != bind_group => { + layer.push( + atlas, + &self.nearest_instances, + &self.linear_instances, + ); + + *atlas = bind_group.clone(); + } + _ => {} + } + + add_instances( + *bounds, + *clip_bounds, + border::radius(0), + f32::from(svg.rotation), + svg.opacity, + true, + atlas_entry, + &mut self.nearest_instances, + ); + } + } + #[cfg(not(feature = "svg"))] + Image::Vector { .. } => continue, + } + } + + if let Some(atlas) = &atlas { + layer.push(atlas, &self.nearest_instances, &self.linear_instances); + } + + layer.prepare( + device, + encoder, + belt, + transformation, + scale, + &self.nearest_instances, + &self.linear_instances, + ); + + self.prepare_layer += 1; + self.nearest_instances.clear(); + self.linear_instances.clear(); + } + + pub fn render<'a>( + &'a self, + pipeline: &'a Pipeline, + layer: usize, + bounds: Rectangle, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + if let Some(layer) = self.layers.get(layer) { + render_pass.set_pipeline(&pipeline.raw); + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + layer.render(render_pass); + } + } + + pub fn trim(&mut self) { + for layer in &mut self.layers[..self.prepare_layer] { + layer.clear(); + } + + self.prepare_layer = 0; + } +} + +#[derive(Debug)] +struct Layer { + uniforms: wgpu::Buffer, + instances: Buffer, + nearest: Vec, + nearest_layout: wgpu::BindGroup, + nearest_total: usize, + linear: Vec, + linear_layout: wgpu::BindGroup, + linear_total: usize, +} + +#[derive(Debug)] +struct Group { + atlas: Arc, + instance_count: usize, +} + +impl Layer { + fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + nearest_sampler: &wgpu::Sampler, + linear_sampler: &wgpu::Sampler, + ) -> Self { + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::image uniforms buffer"), + size: mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let instances = Buffer::new( + device, + "iced_wgpu::image instance buffer", + Instance::INITIAL, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + let nearest_layout = + 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( + nearest_sampler, + ), + }, + ], + }); + + let linear_layout = + 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( + linear_sampler, + ), + }, + ], + }); + + Self { + uniforms, + instances, + nearest: Vec::new(), + nearest_layout, + nearest_total: 0, + linear: Vec::new(), + linear_layout, + linear_total: 0, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + transformation: Transformation, + scale_factor: f32, + nearest: &[Instance], + linear: &[Instance], + ) { + let uniforms = Uniforms { + transform: transformation.into(), + scale_factor, + _padding: [0.0; 3], + }; + + 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); + + let _ = self + .instances + .resize(device, self.nearest_total + self.linear_total); + + let mut offset = 0; + + if !nearest.is_empty() { + offset += self.instances.write(device, encoder, belt, 0, nearest); + } + + if !linear.is_empty() { + let _ = self.instances.write(device, encoder, belt, offset, linear); + } + } + + fn push( + &mut self, + atlas: &Arc, + nearest: &[Instance], + linear: &[Instance], + ) { + let new_nearest = nearest.len() - self.nearest_total; + + if new_nearest > 0 { + self.nearest.push(Group { + atlas: atlas.clone(), + instance_count: new_nearest, + }); + + self.nearest_total = nearest.len(); + } + + let new_linear = linear.len() - self.linear_total; + + if new_linear > 0 { + self.linear.push(Group { + atlas: atlas.clone(), + instance_count: new_linear, + }); + + self.linear_total = linear.len(); + } + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_vertex_buffer(0, self.instances.slice(..)); + + let mut offset = 0; + + if !self.nearest.is_empty() { + render_pass.set_bind_group(0, &self.nearest_layout, &[]); + + for group in &self.nearest { + render_pass.set_bind_group(1, group.atlas.as_ref(), &[]); + render_pass + .draw(0..6, offset..offset + group.instance_count as u32); + + offset += group.instance_count as u32; + } + } + + if !self.linear.is_empty() { + render_pass.set_bind_group(0, &self.linear_layout, &[]); + + for group in &self.linear { + render_pass.set_bind_group(1, group.atlas.as_ref(), &[]); + render_pass + .draw(0..6, offset..offset + group.instance_count as u32); + + offset += group.instance_count as u32; + } + } + } + + fn clear(&mut self) { + self.nearest.clear(); + self.nearest_total = 0; + + self.linear.clear(); + self.linear_total = 0; + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +struct Instance { + _center: [f32; 2], + _clip_bounds: [f32; 4], + _border_radius: [f32; 4], + _tile: [f32; 4], + _rotation: f32, + _opacity: f32, + _position_in_atlas: [f32; 2], + _size_in_atlas: [f32; 2], + _layer: u32, + _snap: u32, +} + +impl Instance { + pub const INITIAL: usize = 20; +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +struct Uniforms { + transform: [f32; 16], + scale_factor: f32, + // Uniforms must be aligned to their largest member, + // this uses a mat4x4 which aligns to 16, so align to that + _padding: [f32; 3], +} + +fn add_instances( + bounds: Rectangle, + clip_bounds: Rectangle, + border_radius: border::Radius, + rotation: f32, + opacity: f32, + snap: bool, + entry: &atlas::Entry, + instances: &mut Vec, +) { + let center = [ + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, + ]; + + let clip_bounds = [ + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, + ]; + + let border_radius = border_radius.into(); + + match entry { + atlas::Entry::Contiguous(allocation) => { + add_instance( + center, + clip_bounds, + border_radius, + [bounds.x, bounds.y, bounds.width, bounds.height], + rotation, + opacity, + snap, + allocation, + instances, + ); + } + atlas::Entry::Fragmented { fragments, size } => { + let scaling_x = bounds.width / size.width as f32; + let scaling_y = bounds.height / size.height as f32; + + for fragment in fragments { + let allocation = &fragment.allocation; + let (fragment_x, fragment_y) = fragment.position; + + let Size { + width: fragment_width, + height: fragment_height, + } = allocation.size(); + + let tile = [ + bounds.x + fragment_x as f32 * scaling_x, + bounds.y + fragment_y as f32 * scaling_y, + fragment_width as f32 * scaling_x, + fragment_height as f32 * scaling_y, + ]; + + add_instance( + center, + clip_bounds, + border_radius, + tile, + rotation, + opacity, + snap, + allocation, + instances, + ); + } + } + } +} + +#[inline] +fn add_instance( + center: [f32; 2], + clip_bounds: [f32; 4], + border_radius: [f32; 4], + tile: [f32; 4], + rotation: f32, + opacity: f32, + snap: bool, + allocation: &atlas::Allocation, + instances: &mut Vec, +) { + let (x, y) = allocation.position(); + let Size { width, height } = allocation.size(); + let layer = allocation.layer(); + let atlas_size = allocation.atlas_size(); + + let instance = Instance { + _center: center, + _clip_bounds: clip_bounds, + _border_radius: border_radius, + _tile: tile, + _rotation: rotation, + _opacity: opacity, + _position_in_atlas: [ + x as f32 / atlas_size as f32, + y as f32 / atlas_size as f32, + ], + _size_in_atlas: [ + width as f32 / atlas_size as f32, + height as f32 / atlas_size as f32, + ], + _layer: layer as u32, + _snap: snap as u32, + }; + + instances.push(instance); +} diff --git a/crates/iced_wgpu/src/image/null.rs b/crates/iced_wgpu/src/image/null.rs new file mode 100644 index 000000000..d0d74cfc1 --- /dev/null +++ b/crates/iced_wgpu/src/image/null.rs @@ -0,0 +1,16 @@ +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) {} + + pub fn is_empty(&self) -> bool { + true + } + + pub fn append(&mut self, _batch: &mut Self) {} +} diff --git a/crates/iced_wgpu/src/image/raster.rs b/crates/iced_wgpu/src/image/raster.rs new file mode 100644 index 000000000..881e75e3d --- /dev/null +++ b/crates/iced_wgpu/src/image/raster.rs @@ -0,0 +1,123 @@ +use crate::core::Size; +use crate::core::image; +use crate::graphics; +use crate::image::atlas::{self, Atlas}; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::sync::{Arc, Weak}; + +pub type Image = graphics::image::Buffer; + +/// Entry in cache corresponding to an image handle +#[derive(Debug)] +pub enum Memory { + /// Image data on host + Host(Image), + /// Storage entry + Device { + entry: atlas::Entry, + bind_group: Option>, + allocation: Option>, + }, + Error(image::Error), +} + +impl Memory { + pub fn load(handle: &image::Handle) -> Self { + match graphics::image::load(handle) { + Ok(image) => Self::Host(image), + Err(error) => Self::Error(error), + } + } + + pub fn dimensions(&self) -> Size { + match self { + Memory::Host(image) => { + let (width, height) = image.dimensions(); + + Size::new(width, height) + } + Memory::Device { entry, .. } => entry.size(), + Memory::Error(_) => Size::new(1, 1), + } + } + + pub fn host(&self) -> Option { + match self { + Memory::Host(image) => Some(image.clone()), + Memory::Device { .. } | Memory::Error(_) => None, + } + } +} + +#[derive(Debug, Default)] +pub struct Cache { + map: FxHashMap, + hits: FxHashSet, + should_trim: bool, +} + +impl Cache { + pub fn get_mut(&mut self, handle: &image::Handle) -> Option<&mut Memory> { + let _ = self.hits.insert(handle.id()); + + self.map.get_mut(&handle.id()) + } + + pub fn insert(&mut self, handle: &image::Handle, memory: Memory) { + let _ = self.map.insert(handle.id(), memory); + let _ = self.hits.insert(handle.id()); + + self.should_trim = true; + } + + pub fn contains(&self, handle: &image::Handle) -> bool { + self.map.contains_key(&handle.id()) + } + + pub fn trim( + &mut self, + atlas: &mut Atlas, + on_drop: impl Fn(Arc), + ) { + // Only trim if new entries have landed in the `Cache` + if !self.should_trim { + return; + } + + let hits = &self.hits; + + self.map.retain(|id, memory| { + // Retain active allocations + if let Memory::Device { allocation, .. } = memory + && allocation + .as_ref() + .is_some_and(|allocation| allocation.strong_count() > 0) + { + return true; + } + + let retain = hits.contains(id); + + if !retain { + log::debug!("Dropping image allocation: {id:?}"); + + if let Memory::Device { + entry, bind_group, .. + } = memory + { + if let Some(bind_group) = bind_group.take() { + on_drop(bind_group); + } else { + atlas.remove(entry); + } + } + } + + retain + }); + + self.hits.clear(); + self.should_trim = false; + } +} diff --git a/crates/iced_wgpu/src/image/vector.rs b/crates/iced_wgpu/src/image/vector.rs new file mode 100644 index 000000000..0704a966e --- /dev/null +++ b/crates/iced_wgpu/src/image/vector.rs @@ -0,0 +1,231 @@ +use crate::core::svg; +use crate::core::{Color, Size}; +use crate::image::atlas::{self, Atlas}; + +use resvg::tiny_skia; +use resvg::usvg; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::fs; +use std::panic; +use std::sync::Arc; + +/// Entry in cache corresponding to an svg handle +pub enum Svg { + /// Parsed svg + Loaded(usvg::Tree), + /// Svg not found or failed to parse + NotFound, +} + +impl Svg { + /// Viewport width and height + pub fn viewport_dimensions(&self) -> Size { + match self { + Svg::Loaded(tree) => { + let size = tree.size(); + + Size::new(size.width() as u32, size.height() as u32) + } + Svg::NotFound => Size::new(1, 1), + } + } +} + +/// Caches svg vector and raster data +#[derive(Debug, Default)] +pub struct Cache { + svgs: FxHashMap, + rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, + svg_hits: FxHashSet, + rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>, + should_trim: bool, + fontdb: Option>, +} + +type ColorFilter = Option<[u8; 4]>; + +impl Cache { + /// Load svg + pub fn load(&mut self, handle: &svg::Handle) -> &Svg { + if self.svgs.contains_key(&handle.id()) { + return self.svgs.get(&handle.id()).unwrap(); + } + + // TODO: Reuse `cosmic-text` font database + if self.fontdb.is_none() { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + + self.fontdb = Some(Arc::new(fontdb)); + } + + let options = usvg::Options { + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }; + + let svg = match handle.data() { + svg::Data::Path(path) => fs::read_to_string(path) + .ok() + .and_then(|contents| { + usvg::Tree::from_str(&contents, &options).ok() + }) + .map(Svg::Loaded) + .unwrap_or(Svg::NotFound), + svg::Data::Bytes(bytes) => { + match usvg::Tree::from_data(bytes, &options) { + Ok(tree) => Svg::Loaded(tree), + Err(_) => Svg::NotFound, + } + } + }; + + self.should_trim = true; + + let _ = self.svgs.insert(handle.id(), svg); + self.svgs.get(&handle.id()).unwrap() + } + + /// Load svg and upload raster data + pub fn upload( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + handle: &svg::Handle, + color: Option, + size: Size, + scale: f32, + atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let id = handle.id(); + + let (width, height) = ( + (scale * size.width).ceil() as u32, + (scale * size.height).ceil() as u32, + ); + + let color = color.map(Color::into_rgba8); + let key = (id, width, height, color); + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + if self.rasterized.contains_key(&key) { + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert(key); + + return self.rasterized.get(&key); + } + + match self.load(handle) { + Svg::Loaded(tree) => { + if width == 0 || height == 0 { + return None; + } + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + let mut img = tiny_skia::Pixmap::new(width, height)?; + + let tree_size = tree.size().to_int_size(); + + let target_size = if width > height { + tree_size.scale_to_width(width) + } else { + tree_size.scale_to_height(height) + }; + + let transform = if let Some(target_size) = target_size { + let tree_size = tree_size.to_size(); + let target_size = target_size.to_size(); + + tiny_skia::Transform::from_scale( + target_size.width() / tree_size.width(), + target_size.height() / tree_size.height(), + ) + } else { + tiny_skia::Transform::default() + }; + + // SVG rendering can panic on malformed or complex vectors. + // We catch panics to prevent crashes and continue gracefully. + let render = + panic::catch_unwind(panic::AssertUnwindSafe(|| { + resvg::render(tree, transform, &mut img.as_mut()); + })); + + if let Err(error) = render { + log::warn!( + "SVG rendering for {handle:?} panicked: {error:?}" + ); + } + + let mut rgba = img.take(); + + if let Some(color) = color { + rgba.chunks_exact_mut(4).for_each(|rgba| { + if rgba[3] > 0 { + rgba[0] = color[0]; + rgba[1] = color[1]; + rgba[2] = color[2]; + } + }); + } + + let allocation = atlas + .upload(device, encoder, belt, width, height, &rgba)?; + + log::debug!("allocating {id} {width}x{height}"); + + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert(key); + let _ = self.rasterized.insert(key, allocation); + self.should_trim = true; + + self.rasterized.get(&key) + } + Svg::NotFound => None, + } + } + + /// 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; + + self.svgs.retain(|k, _| svg_hits.contains(k)); + self.rasterized.retain(|k, entry| { + let retain = rasterized_hits.contains(k); + + if !retain { + atlas.remove(entry); + } + + retain + }); + self.svg_hits.clear(); + self.rasterized_hits.clear(); + self.should_trim = false; + } +} + +impl std::fmt::Debug for Svg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Svg::Loaded(_) => write!(f, "Svg::Loaded"), + Svg::NotFound => write!(f, "Svg::NotFound"), + } + } +} diff --git a/crates/iced_wgpu/src/layer.rs b/crates/iced_wgpu/src/layer.rs new file mode 100644 index 000000000..c61730b66 --- /dev/null +++ b/crates/iced_wgpu/src/layer.rs @@ -0,0 +1,405 @@ +use crate::core::{ + self, Background, Color, Point, Rectangle, Svg, Transformation, renderer, +}; +use crate::graphics; +use crate::graphics::Mesh; +use crate::graphics::color; +use crate::graphics::layer; +use crate::graphics::mesh; +use crate::graphics::text::{Editor, Paragraph}; +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; + +#[derive(Debug)] +pub struct Layer { + pub bounds: Rectangle, + pub quads: quad::Batch, + pub triangles: triangle::Batch, + pub primitives: primitive::Batch, + pub images: image::Batch, + pub text: text::Batch, + pending_meshes: Vec, + pending_text: Vec, +} + +impl Layer { + pub fn is_empty(&self) -> bool { + self.quads.is_empty() + && self.triangles.is_empty() + && self.primitives.is_empty() + && self.images.is_empty() + && self.text.is_empty() + && self.pending_meshes.is_empty() + && self.pending_text.is_empty() + } + + 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 * transformation.scale_factor()) + .into(), + border_width: quad.border.width * transformation.scale_factor(), + shadow_color: color::pack(quad.shadow.color), + shadow_offset: (quad.shadow.offset * transformation.scale_factor()) + .into(), + shadow_blur_radius: quad.shadow.blur_radius + * transformation.scale_factor(), + snap: quad.snap as u32, + }; + + self.quads.add(quad, &background); + } + + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.pending_text.push(paragraph); + } + + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let editor = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation, + }; + + self.pending_text.push(editor); + } + + pub fn draw_text( + &mut self, + text: crate::core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let text = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), + font: text.font, + align_x: text.align_x, + align_y: text.align_y, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.pending_text.push(text); + } + + pub fn draw_text_raw( + &mut self, + raw: graphics::text::Raw, + transformation: Transformation, + ) { + let raw = Text::Raw { + raw, + transformation, + }; + + self.pending_text.push(raw); + } + + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { + match image { + Image::Raster { + image, + bounds, + clip_bounds, + } => { + self.draw_raster(image, bounds, clip_bounds, transformation); + } + Image::Vector { + svg, + bounds, + clip_bounds, + } => { + self.draw_svg(svg, bounds, clip_bounds, transformation); + } + } + } + + pub fn draw_raster( + &mut self, + image: core::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let image = Image::Raster { + image: core::Image { + border_radius: image.border_radius + * transformation.scale_factor(), + ..image + }, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; + + self.images.push(image); + } + + pub fn draw_svg( + &mut self, + svg: Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + transformation: Transformation, + ) { + let svg = Image::Vector { + svg, + bounds: bounds * transformation, + clip_bounds: clip_bounds * transformation, + }; + + self.images.push(svg); + } + + 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; + } + } + + self.pending_meshes.push(mesh); + } + + pub fn draw_mesh_group( + &mut self, + meshes: Vec, + transformation: Transformation, + ) { + self.flush_meshes(); + + self.triangles.push(triangle::Item::Group { + meshes, + transformation, + }); + } + + pub fn draw_mesh_cache( + &mut self, + cache: mesh::Cache, + transformation: Transformation, + ) { + self.flush_meshes(); + + self.triangles.push(triangle::Item::Cached { + cache, + transformation, + }); + } + + pub fn draw_text_group( + &mut self, + text: Vec, + transformation: Transformation, + ) { + self.flush_text(); + + 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: impl Primitive, + transformation: Transformation, + ) { + let bounds = bounds * transformation; + + self.primitives + .push(primitive::Instance::new(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 bounds(&self) -> Rectangle { + self.bounds + } + + 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(); + } + + fn start(&self) -> usize { + if !self.quads.is_empty() { + return 1; + } + + if !self.triangles.is_empty() { + return 2; + } + + if !self.primitives.is_empty() { + return 3; + } + + if !self.images.is_empty() { + return 4; + } + + if !self.text.is_empty() { + return 5; + } + + usize::MAX + } + + fn end(&self) -> usize { + if !self.text.is_empty() { + return 5; + } + + if !self.images.is_empty() { + return 4; + } + + if !self.primitives.is_empty() { + return 3; + } + + if !self.triangles.is_empty() { + return 2; + } + + if !self.quads.is_empty() { + return 1; + } + + 0 + } + + fn merge(&mut self, layer: &mut Self) { + self.quads.append(&mut layer.quads); + self.triangles.append(&mut layer.triangles); + self.primitives.append(&mut layer.primitives); + self.images.append(&mut layer.images); + self.text.append(&mut layer.text); + } +} + +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/crates/iced_wgpu/src/lib.rs b/crates/iced_wgpu/src/lib.rs new file mode 100644 index 000000000..ec103cf35 --- /dev/null +++ b/crates/iced_wgpu/src/lib.rs @@ -0,0 +1,984 @@ +//! A [`wgpu`] renderer for [Iced]. +//! +//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true) +//! +//! [`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX11, and +//! DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the +//! incoming [WebGPU API]. +//! +//! Currently, `iced_wgpu` supports the following primitives: +//! - Text, which is rendered using [`glyphon`]. +//! - Quads or rectangles, with rounded borders and a solid background color. +//! - Clip areas, useful to implement scrollables or hide overflowing content. +//! - Images and SVG, loaded from memory or the file system. +//! - Meshes of triangles, useful to draw geometry freely. +//! +//! [Iced]: https://github.com/iced-rs/iced +//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs +//! [WebGPU API]: https://gpuweb.github.io/gpuweb/ +//! [`glyphon`]: https://github.com/grovesNL/glyphon +#![doc( + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(missing_docs)] +pub mod layer; +pub mod primitive; +pub mod settings; +pub mod window; + +#[cfg(feature = "geometry")] +pub mod geometry; + +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; + +use iced_debug as debug; +pub use iced_graphics as graphics; +pub use iced_graphics::core; + +pub use wgpu; + +pub use engine::Engine; +pub use layer::Layer; +pub use primitive::Primitive; +pub use settings::Settings; + +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::renderer; +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, +}; +use crate::graphics::mesh; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::{Shell, Viewport}; + +/// A [`wgpu`] graphics renderer for [`iced`]. +/// +/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs +/// [`iced`]: https://github.com/iced-rs/iced +pub struct Renderer { + engine: Engine, + + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + + quad: quad::State, + triangle: triangle::State, + text: text::State, + text_viewport: text::Viewport, + + #[cfg(any(feature = "svg", feature = "image"))] + image: image::State, + + // TODO: Centralize all the image feature handling + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: std::cell::RefCell, + + staging_belt: wgpu::util::StagingBelt, +} + +impl Renderer { + pub fn new( + engine: Engine, + default_font: Font, + default_text_size: Pixels, + ) -> Self { + Self { + default_font, + default_text_size, + layers: layer::Stack::new(), + + quad: quad::State::new(), + triangle: triangle::State::new( + &engine.device, + &engine.triangle_pipeline, + ), + text: text::State::new(), + text_viewport: engine.text_pipeline.create_viewport(&engine.device), + + #[cfg(any(feature = "svg", feature = "image"))] + image: image::State::new(), + + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: std::cell::RefCell::new(engine.create_image_cache()), + + // TODO: Resize belt smartly (?) + // It would be great if the `StagingBelt` API exposed methods + // for introspection to detect when a resize may be worth it. + staging_belt: wgpu::util::StagingBelt::new( + buffer::MAX_WRITE_SIZE as u64, + ), + + engine, + } + } + + fn draw( + &mut self, + clear_color: Option, + target: &wgpu::TextureView, + viewport: &Viewport, + ) -> wgpu::CommandEncoder { + let mut encoder = self.engine.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu encoder"), + }, + ); + + self.prepare(&mut encoder, viewport); + self.render(&mut encoder, target, clear_color, viewport); + + self.quad.trim(); + self.triangle.trim(); + self.text.trim(); + + // TODO: Provide window id (?) + self.engine.trim(); + + #[cfg(any(feature = "svg", feature = "image"))] + { + self.image.trim(); + self.image_cache.borrow_mut().trim(); + } + + encoder + } + + pub fn present( + &mut self, + clear_color: Option, + _format: wgpu::TextureFormat, + frame: &wgpu::TextureView, + viewport: &Viewport, + ) -> wgpu::SubmissionIndex { + let encoder = self.draw(clear_color, frame, viewport); + + self.staging_belt.finish(); + let submission = self.engine.queue.submit([encoder.finish()]); + self.staging_belt.recall(); + submission + } + + /// Renders the current surface to an offscreen buffer. + /// + /// Returns RGBA bytes of the texture data. + pub fn screenshot( + &mut self, + viewport: &Viewport, + background_color: Color, + ) -> Vec { + #[derive(Clone, Copy, Debug)] + struct BufferDimensions { + width: u32, + height: u32, + unpadded_bytes_per_row: usize, + padded_bytes_per_row: usize, + } + + impl BufferDimensions { + fn new(size: Size) -> Self { + let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA + let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256 + let padded_bytes_per_row_padding = (alignment + - unpadded_bytes_per_row % alignment) + % alignment; + let padded_bytes_per_row = + unpadded_bytes_per_row + padded_bytes_per_row_padding; + + Self { + width: size.width, + height: size.height, + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } + } + + let dimensions = BufferDimensions::new(viewport.physical_size()); + + let texture_extent = wgpu::Extent3d { + width: dimensions.width, + height: dimensions.height, + depth_or_array_layers: 1, + }; + + let texture = + self.engine.device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu.offscreen.source_texture"), + size: texture_extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: self.engine.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self.draw(Some(background_color), &view, viewport); + + let texture = crate::color::convert( + &self.engine.device, + &mut encoder, + texture, + if graphics::color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + ); + + let output_buffer = + self.engine.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu.offscreen.output_texture_buffer"), + size: (dimensions.padded_bytes_per_row + * dimensions.height as usize) as u64, + usage: wgpu::BufferUsages::MAP_READ + | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + wgpu::TexelCopyBufferInfo { + buffer: &output_buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(dimensions.padded_bytes_per_row as u32), + rows_per_image: None, + }, + }, + texture_extent, + ); + + self.staging_belt.finish(); + let index = self.engine.queue.submit([encoder.finish()]); + self.staging_belt.recall(); + + let slice = output_buffer.slice(..); + slice.map_async(wgpu::MapMode::Read, |_| {}); + + let _ = self.engine.device.poll(wgpu::PollType::Wait { + submission_index: Some(index), + timeout: None, + }); + + let mapped_buffer = slice.get_mapped_range(); + + mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold( + vec![], + |mut acc, row| { + acc.extend(&row[..dimensions.unpadded_bytes_per_row]); + acc + }, + ) + } + + fn prepare( + &mut self, + encoder: &mut wgpu::CommandEncoder, + viewport: &Viewport, + ) { + let scale_factor = viewport.scale_factor(); + + self.text_viewport + .update(&self.engine.queue, viewport.physical_size()); + + let physical_bounds = Rectangle::::from(Rectangle::with_size( + viewport.physical_size(), + )); + + self.layers.merge(); + + for layer in self.layers.iter() { + let clip_bounds = layer.bounds * scale_factor; + + if physical_bounds + .intersection(&clip_bounds) + .and_then(Rectangle::snap) + .is_none() + { + continue; + } + + if !layer.quads.is_empty() { + let prepare_span = debug::prepare(debug::Primitive::Quad); + + self.quad.prepare( + &self.engine.quad_pipeline, + &self.engine.device, + &mut self.staging_belt, + encoder, + &layer.quads, + viewport.projection(), + scale_factor, + ); + + prepare_span.finish(); + } + + if !layer.triangles.is_empty() { + let prepare_span = debug::prepare(debug::Primitive::Triangle); + + self.triangle.prepare( + &self.engine.triangle_pipeline, + &self.engine.device, + &mut self.staging_belt, + encoder, + &layer.triangles, + Transformation::scale(scale_factor), + viewport.physical_size(), + ); + + prepare_span.finish(); + } + + if !layer.primitives.is_empty() { + let prepare_span = debug::prepare(debug::Primitive::Shader); + + let mut primitive_storage = self + .engine + .primitive_storage + .write() + .expect("Write primitive storage"); + + for instance in &layer.primitives { + instance.primitive.prepare( + &mut primitive_storage, + &self.engine.device, + &self.engine.queue, + self.engine.format, + &instance.bounds, + viewport, + ); + } + + prepare_span.finish(); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + let prepare_span = debug::prepare(debug::Primitive::Image); + + self.image.prepare( + &self.engine.image_pipeline, + &self.engine.device, + &mut self.staging_belt, + encoder, + &mut self.image_cache.borrow_mut(), + &layer.images, + viewport.projection(), + scale_factor, + ); + + prepare_span.finish(); + } + + if !layer.text.is_empty() { + let prepare_span = debug::prepare(debug::Primitive::Text); + + self.text.prepare( + &self.engine.text_pipeline, + &self.engine.device, + &self.engine.queue, + &self.text_viewport, + encoder, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + ); + + prepare_span.finish(); + } + } + } + + fn render( + &mut self, + encoder: &mut wgpu::CommandEncoder, + frame: &wgpu::TextureView, + clear_color: Option, + 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, + depth_slice: None, + 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; + + let scale_factor = viewport.scale_factor(); + let physical_bounds = Rectangle::::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_factor)) + else { + continue; + }; + + let Some(scissor_rect) = physical_bounds.snap() else { + continue; + }; + + if !layer.quads.is_empty() { + let render_span = debug::render(debug::Primitive::Quad); + self.quad.render( + &self.engine.quad_pipeline, + quad_layer, + scissor_rect, + &layer.quads, + &mut render_pass, + ); + render_span.finish(); + + quad_layer += 1; + } + + if !layer.triangles.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + let render_span = debug::render(debug::Primitive::Triangle); + mesh_layer += self.triangle.render( + &self.engine.triangle_pipeline, + encoder, + frame, + mesh_layer, + &layer.triangles, + physical_bounds, + scale, + ); + render_span.finish(); + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + depth_slice: None, + 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 render_span = debug::render(debug::Primitive::Shader); + + let primitive_storage = self + .engine + .primitive_storage + .read() + .expect("Read primitive storage"); + + let mut need_render = Vec::new(); + + for instance in &layer.primitives { + let bounds = instance.bounds * scale; + + if let Some(clip_bounds) = (instance.bounds * scale) + .intersection(&physical_bounds) + .and_then(Rectangle::snap) + { + render_pass.set_viewport( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + 0.0, + 1.0, + ); + + render_pass.set_scissor_rect( + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, + ); + + let drawn = instance + .primitive + .draw(&primitive_storage, &mut render_pass); + + if !drawn { + need_render.push((instance, clip_bounds)); + } + } + } + + render_pass.set_viewport( + 0.0, + 0.0, + viewport.physical_width() as f32, + viewport.physical_height() as f32, + 0.0, + 1.0, + ); + + render_pass.set_scissor_rect( + 0, + 0, + viewport.physical_width(), + viewport.physical_height(), + ); + + if !need_render.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + for (instance, clip_bounds) in need_render { + instance.primitive.render( + &primitive_storage, + encoder, + 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, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + render_span.finish(); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + let render_span = debug::render(debug::Primitive::Image); + self.image.render( + &self.engine.image_pipeline, + image_layer, + scissor_rect, + &mut render_pass, + ); + render_span.finish(); + + image_layer += 1; + } + + if !layer.text.is_empty() { + let render_span = debug::render(debug::Primitive::Text); + text_layer += self.text.render( + &self.engine.text_pipeline, + &self.text_viewport, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + render_span.finish(); + } + } + + let _ = ManuallyDrop::into_inner(render_pass); + + debug::layers_rendered(|| { + self.layers + .iter() + .filter(|layer| { + !layer.is_empty() + && physical_bounds + .intersection(&(layer.bounds * scale_factor)) + .is_some_and(|viewport| viewport.snap().is_some()) + }) + .count() + }); + } +} + +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, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_quad(quad, background.into(), transformation); + } + + fn reset(&mut self, new_bounds: Rectangle) { + self.layers.reset(new_bounds); + } + + fn allocate_image( + &mut self, + _handle: &core::image::Handle, + _callback: impl FnOnce(Result) + + Send + + 'static, + ) { + #[cfg(feature = "image")] + self.image_cache + .get_mut() + .allocate_image(_handle, _callback); + } +} + +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}'; + const ICED_LOGO: char = '\u{e801}'; + const SCROLL_UP_ICON: char = '\u{e802}'; + const SCROLL_DOWN_ICON: char = '\u{e803}'; + const SCROLL_LEFT_ICON: char = '\u{e804}'; + const SCROLL_RIGHT_ICON: char = '\u{e805}'; + + 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); + } +} + +impl graphics::text::Renderer for Renderer { + fn fill_raw(&mut self, raw: graphics::text::Raw) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_text_raw(raw, transformation); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn load_image( + &self, + handle: &Self::Handle, + ) -> Result { + self.image_cache.borrow_mut().load_image( + &self.engine.device, + &self.engine.queue, + handle, + ) + } + + fn measure_image(&self, handle: &Self::Handle) -> Option> { + self.image_cache.borrow_mut().measure_image(handle) + } + + fn draw_image( + &mut self, + image: core::Image, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_raster(image, bounds, clip_bounds, transformation); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg(&self, handle: &core::svg::Handle) -> core::Size { + self.image_cache.borrow_mut().measure_svg(handle) + } + + fn draw_svg( + &mut self, + svg: core::Svg, + bounds: Rectangle, + clip_bounds: Rectangle, + ) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_svg(svg, bounds, clip_bounds, transformation); + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, mesh: graphics::Mesh) { + debug_assert!( + !mesh.indices().is_empty(), + "Mesh must not have empty indices" + ); + + debug_assert!( + mesh.indices().len().is_multiple_of(3), + "Mesh indices length must be a multiple of 3" + ); + + let (layer, transformation) = self.layers.current_mut(); + layer.draw_mesh(mesh, transformation); + } + + fn draw_mesh_cache(&mut self, cache: mesh::Cache) { + let (layer, transformation) = self.layers.current_mut(); + layer.draw_mesh_cache(cache, transformation); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, bounds: Rectangle) -> Self::Frame { + geometry::Frame::new(bounds) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + let (layer, transformation) = self.layers.current_mut(); + + match geometry { + Geometry::Live { + meshes, + images, + text, + } => { + layer.draw_mesh_group(meshes, transformation); + + for image in images { + layer.draw_image(image, 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(images) = cache.images { + for image in images.iter().cloned() { + layer.draw_image(image, 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, primitive, transformation); + } +} + +impl graphics::compositor::Default for crate::Renderer { + type Compositor = window::Compositor; +} + +impl renderer::Headless for Renderer { + async fn new( + default_font: Font, + default_text_size: Pixels, + backend: Option<&str>, + ) -> Option { + if backend.is_some_and(|backend| backend != "wgpu") { + return None; + } + + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::from_env() + .unwrap_or(wgpu::Backends::PRIMARY), + flags: wgpu::InstanceFlags::empty(), + ..wgpu::InstanceDescriptor::default() + }); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + force_fallback_adapter: false, + compatible_surface: None, + }) + .await + .ok()?; + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: Some("iced_wgpu [headless]"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits { + max_bind_groups: 2, + ..wgpu::Limits::default() + }, + memory_hints: wgpu::MemoryHints::MemoryUsage, + trace: wgpu::Trace::Off, + experimental_features: wgpu::ExperimentalFeatures::disabled(), + }) + .await + .ok()?; + + let engine = Engine::new( + &adapter, + device, + queue, + if graphics::color::GAMMA_CORRECTION { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Rgba8Unorm + }, + Some(graphics::Antialiasing::MSAAx4), + Shell::headless(), + ); + + Some(Self::new(engine, default_font, default_text_size)) + } + + fn name(&self) -> String { + "wgpu".to_owned() + } + + fn screenshot( + &mut self, + size: Size, + scale_factor: f32, + background_color: Color, + ) -> Vec { + self.screenshot( + &Viewport::with_physical_size(size, scale_factor), + background_color, + ) + } +} diff --git a/crates/iced_wgpu/src/primitive.rs b/crates/iced_wgpu/src/primitive.rs new file mode 100644 index 000000000..4c18bb6a0 --- /dev/null +++ b/crates/iced_wgpu/src/primitive.rs @@ -0,0 +1,242 @@ +//! Draw custom primitives. +use crate::core::{self, Rectangle}; +use crate::graphics::Viewport; +use crate::graphics::futures::{MaybeSend, MaybeSync}; + +use rustc_hash::FxHashMap; +use std::any::{Any, TypeId}; +use std::fmt::Debug; + +/// A batch of primitives. +pub type Batch = Vec; + +/// A set of methods which allows a [`Primitive`] to be rendered. +pub trait Primitive: Debug + MaybeSend + MaybeSync + 'static { + /// The shared renderer of this [`Primitive`]. + /// + /// Normally, this will contain a bunch of [`wgpu`] state; like + /// a rendering pipeline, buffers, and textures. + /// + /// All instances of this [`Primitive`] type will share the same + /// [`Renderer`]. + type Pipeline: Pipeline + MaybeSend + MaybeSync; + + /// Processes the [`Primitive`], allowing for GPU buffer allocation. + fn prepare( + &self, + pipeline: &mut Self::Pipeline, + device: &wgpu::Device, + queue: &wgpu::Queue, + bounds: &Rectangle, + viewport: &Viewport, + ); + + /// Draws the [`Primitive`] in the given [`wgpu::RenderPass`]. + /// + /// When possible, this should be implemented over [`render`](Self::render) + /// since reusing the existing render pass should be considerably more + /// efficient than issuing a new one. + /// + /// The viewport and scissor rect of the render pass provided is set + /// to the bounds and clip bounds of the [`Primitive`], respectively. + /// + /// If you have complex composition needs, then you can leverage + /// [`render`](Self::render) by returning `false` here. + /// + /// By default, it does nothing and returns `false`. + fn draw( + &self, + _pipeline: &Self::Pipeline, + _render_pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + false + } + + /// Renders the [`Primitive`], using the given [`wgpu::CommandEncoder`]. + /// + /// This will only be called if [`draw`](Self::draw) returns `false`. + /// + /// By default, it does nothing. + fn render( + &self, + _pipeline: &Self::Pipeline, + _encoder: &mut wgpu::CommandEncoder, + _target: &wgpu::TextureView, + _clip_bounds: &Rectangle, + ) { + } +} + +/// The pipeline of a graphics [`Primitive`]. +pub trait Pipeline: Any + MaybeSend + MaybeSync { + /// Creates the [`Pipeline`] of a [`Primitive`]. + /// + /// This will only be called once, when the first [`Primitive`] with this kind + /// of [`Pipeline`] is encountered. + fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + ) -> Self + where + Self: Sized; + + /// Trims any cached data in the [`Pipeline`]. + /// + /// This will normally be called at the end of a frame. + fn trim(&mut self) {} +} + +pub(crate) trait Stored: + Debug + MaybeSend + MaybeSync + 'static +{ + fn prepare( + &self, + storage: &mut Storage, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + bounds: &Rectangle, + viewport: &Viewport, + ); + + fn draw( + &self, + storage: &Storage, + render_pass: &mut wgpu::RenderPass<'_>, + ) -> bool; + + fn render( + &self, + storage: &Storage, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + clip_bounds: &Rectangle, + ); +} + +#[derive(Debug)] +struct BlackBox { + primitive: P, +} + +impl Stored for BlackBox

{ + fn prepare( + &self, + storage: &mut Storage, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + bounds: &Rectangle, + viewport: &Viewport, + ) { + if !storage.has::

() { + storage.store::(P::Pipeline::new(device, queue, format)); + } + + let renderer = storage + .get_mut::

() + .expect("renderer should be initialized") + .downcast_mut::() + .expect("renderer should have the proper type"); + + self.primitive + .prepare(renderer, device, queue, bounds, viewport); + } + + fn draw( + &self, + storage: &Storage, + render_pass: &mut wgpu::RenderPass<'_>, + ) -> bool { + let renderer = storage + .get::

() + .expect("renderer should be initialized") + .downcast_ref::() + .expect("renderer should have the proper type"); + + self.primitive.draw(renderer, render_pass) + } + + fn render( + &self, + storage: &Storage, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + clip_bounds: &Rectangle, + ) { + let renderer = storage + .get::

() + .expect("renderer should be initialized") + .downcast_ref::() + .expect("renderer should have the proper type"); + + self.primitive + .render(renderer, encoder, target, clip_bounds); + } +} + +#[derive(Debug)] +/// An instance of a specific [`Primitive`]. +pub struct Instance { + /// The bounds of the [`Instance`]. + pub(crate) bounds: Rectangle, + + /// The [`Primitive`] to render. + pub(crate) primitive: Box, +} + +impl Instance { + /// Creates a new [`Instance`] with the given [`Primitive`]. + pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { + Instance { + bounds, + primitive: Box::new(BlackBox { 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)] +pub struct Storage { + pipelines: FxHashMap>, +} + +impl Storage { + /// Returns `true` if `Storage` contains a type `T`. + pub fn has(&self) -> bool { + self.pipelines.contains_key(&TypeId::of::()) + } + + /// Inserts the data `T` in to [`Storage`]. + pub fn store(&mut self, pipeline: P) { + let _ = self.pipelines.insert(TypeId::of::(), Box::new(pipeline)); + } + + /// Returns a reference to the data with type `T` if it exists in [`Storage`]. + pub fn get(&self) -> Option<&dyn Any> { + self.pipelines + .get(&TypeId::of::()) + .map(|pipeline| pipeline.as_ref() as &dyn Any) + } + + /// Returns a mutable reference to the data with type `T` if it exists in [`Storage`]. + pub fn get_mut(&mut self) -> Option<&mut dyn Any> { + self.pipelines + .get_mut(&TypeId::of::()) + .map(|pipeline| pipeline.as_mut() as &mut dyn Any) + } + + /// Trims the cache of all the pipelines in the [`Storage`]. + pub fn trim(&mut self) { + for pipeline in self.pipelines.values_mut() { + pipeline.trim(); + } + } +} diff --git a/crates/iced_wgpu/src/quad.rs b/crates/iced_wgpu/src/quad.rs new file mode 100644 index 000000000..31bf69cfb --- /dev/null +++ b/crates/iced_wgpu/src/quad.rs @@ -0,0 +1,362 @@ +mod gradient; +mod solid; + +use gradient::Gradient; +use solid::Solid; + +use crate::core::{Background, Rectangle, Transformation}; +use crate::graphics; +use crate::graphics::color; + +use bytemuck::{Pod, Zeroable}; + +use std::mem; + +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, + + /// Whether the [`Quad`] should be snapped to the pixel grid. + pub snap: u32, +} + +#[derive(Debug, Clone)] +pub struct Pipeline { + solid: solid::Pipeline, + gradient: gradient::Pipeline, + constant_layout: wgpu::BindGroupLayout, +} + +#[derive(Default)] +pub struct State { + layers: Vec, + prepare_layer: usize, +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn prepare( + &mut self, + pipeline: &Pipeline, + device: &wgpu::Device, + belt: &mut wgpu::util::StagingBelt, + encoder: &mut wgpu::CommandEncoder, + quads: &Batch, + transformation: Transformation, + scale: f32, + ) { + if self.layers.len() <= self.prepare_layer { + self.layers + .push(Layer::new(device, &pipeline.constant_layout)); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare(device, encoder, belt, quads, transformation, scale); + + self.prepare_layer += 1; + } + + pub fn render<'a>( + &'a self, + pipeline: &'a Pipeline, + layer: usize, + bounds: Rectangle, + quads: &Batch, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + if let Some(layer) = self.layers.get(layer) { + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + let mut solid_offset = 0; + let mut gradient_offset = 0; + + for (kind, count) in &quads.order { + match kind { + Kind::Solid => { + pipeline.solid.render( + render_pass, + &layer.constants, + &layer.solid, + solid_offset..(solid_offset + count), + ); + + solid_offset += count; + } + Kind::Gradient => { + pipeline.gradient.render( + render_pass, + &layer.constants, + &layer.gradient, + gradient_offset..(gradient_offset + count), + ); + + gradient_offset += count; + } + } + } + } + } + + pub fn trim(&mut self) { + self.prepare_layer = 0; + } +} + +impl Pipeline { + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Pipeline { + let constant_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu::quad uniforms layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new( + mem::size_of::() as wgpu::BufferAddress, + ), + }, + count: None, + }], + }); + + Self { + solid: solid::Pipeline::new(device, format, &constant_layout), + gradient: gradient::Pipeline::new(device, format, &constant_layout), + constant_layout, + } + } +} + +#[derive(Debug)] +pub struct Layer { + constants: wgpu::BindGroup, + constants_buffer: wgpu::Buffer, + solid: solid::Layer, + gradient: gradient::Layer, +} + +impl Layer { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + ) -> Self { + let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::quad uniforms buffer"), + size: mem::size_of::() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::quad uniforms bind group"), + layout: constant_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: constants_buffer.as_entire_binding(), + }], + }); + + Self { + constants, + constants_buffer, + solid: solid::Layer::new(device), + gradient: gradient::Layer::new(device), + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + quads: &Batch, + transformation: Transformation, + scale: f32, + ) { + 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); + + belt.write_buffer( + encoder, + &self.constants_buffer, + 0, + (bytes.len() as u64).try_into().expect("Sized uniforms"), + device, + ) + .copy_from_slice(bytes); + } +} + +/// A group of [`Quad`]s rendered together. +#[derive(Default, Debug)] +pub struct Batch { + /// The solid quads of the [`Layer`]. + solids: Vec, + + /// The gradient quads of the [`Layer`]. + gradients: Vec, + + /// 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 { + self.solids.is_empty() && self.gradients.is_empty() + } + + /// Adds a [`Quad`] with the provided `Background` type to the quad [`Layer`]. + pub fn add(&mut self, quad: Quad, background: &Background) { + let kind = match background { + Background::Color(color) => { + self.solids.push(Solid { + color: color::pack(*color), + quad, + }); + + Kind::Solid + } + Background::Gradient(gradient) => { + self.gradients.push(Gradient { + gradient: graphics::gradient::pack( + gradient, + Rectangle::new(quad.position.into(), quad.size.into()), + ), + quad, + }); + + Kind::Gradient + } + }; + + match self.order.last_mut() { + Some((last_kind, count)) if kind == *last_kind => { + *count += 1; + } + _ => { + self.order.push((kind, 1)); + } + } + } + + pub fn clear(&mut self) { + self.solids.clear(); + self.gradients.clear(); + self.order.clear(); + } + + pub fn append(&mut self, batch: &mut Batch) { + self.solids.append(&mut batch.solids); + self.gradients.append(&mut batch.gradients); + self.order.append(&mut batch.order); + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// The kind of a quad. +enum Kind { + /// A solid quad + Solid, + /// A gradient quad + Gradient, +} + +fn color_target_state( + format: wgpu::TextureFormat, +) -> [Option; 1] { + [Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })] +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)] +struct Uniforms { + transform: [f32; 16], + scale: f32, + // Uniforms must be aligned to their largest member, + // this uses a mat4x4 which aligns to 16, so align to that + _padding: [f32; 3], +} + +impl Uniforms { + fn new(transformation: Transformation, scale: f32) -> Uniforms { + Self { + transform: *transformation.as_ref(), + scale, + _padding: [0.0; 3], + } + } +} + +impl Default for Uniforms { + fn default() -> Self { + Self { + transform: *Transformation::IDENTITY.as_ref(), + scale: 1.0, + _padding: [0.0; 3], + } + } +} diff --git a/crates/iced_wgpu/src/quad/gradient.rs b/crates/iced_wgpu/src/quad/gradient.rs new file mode 100644 index 000000000..6a5eed8dc --- /dev/null +++ b/crates/iced_wgpu/src/quad/gradient.rs @@ -0,0 +1,187 @@ +use crate::Buffer; +use crate::graphics::gradient; +use crate::quad::{self, Quad}; + +use bytemuck::{Pod, Zeroable}; +use std::ops::Range; + +/// A quad filled with interpolated colors. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct Gradient { + /// The background gradient data of the quad. + pub gradient: gradient::Packed, + + /// The [`Quad`] data of the [`Gradient`]. + pub quad: Quad, +} + +#[allow(unsafe_code)] +unsafe impl Pod for Gradient {} + +#[allow(unsafe_code)] +unsafe impl Zeroable for Gradient {} + +#[derive(Debug)] +pub struct Layer { + instances: Buffer, + instance_count: usize, +} + +impl Layer { + pub fn new(device: &wgpu::Device) -> Self { + let instances = Buffer::new( + device, + "iced_wgpu.quad.gradient.buffer", + quad::INITIAL_INSTANCES, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + instances, + instance_count: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + instances: &[Gradient], + ) { + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(device, encoder, belt, 0, instances); + + self.instance_count = instances.len(); + } +} + +#[derive(Debug, Clone)] +pub struct Pipeline { + #[cfg(not(target_arch = "wasm32"))] + pipeline: wgpu::RenderPipeline, +} + +impl Pipeline { + #[allow(unused_variables)] + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + #[cfg(not(target_arch = "wasm32"))] + { + let layout = device.create_pipeline_layout( + &wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.quad.gradient.pipeline"), + push_constant_ranges: &[], + bind_group_layouts: &[constants_layout], + }, + ); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.quad.gradient.shader"), + source: wgpu::ShaderSource::Wgsl( + std::borrow::Cow::Borrowed(concat!( + include_str!("../shader/quad.wgsl"), + "\n", + include_str!("../shader/vertex.wgsl"), + "\n", + include_str!("../shader/quad/gradient.wgsl"), + "\n", + include_str!("../shader/color.wgsl"), + "\n", + include_str!("../shader/color/linear_rgb.wgsl") + )), + ), + }); + + let pipeline = device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.quad.gradient.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("gradient_vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() + as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Colors 1-2 + 0 => Uint32x4, + // Colors 3-4 + 1 => Uint32x4, + // Colors 5-6 + 2 => Uint32x4, + // Colors 7-8 + 3 => Uint32x4, + // Offsets 1-8 + 4 => Uint32x4, + // Direction + 5 => Float32x4, + // Position & Scale + 6 => Float32x4, + // Border color + 7 => Float32x4, + // Border radius + 8 => Float32x4, + // Border width + 9 => Float32, + // Snap + 10 => Uint32, + ), + }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("gradient_fs_main"), + targets: &quad::color_target_state(format), + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }, + ); + + Self { pipeline } + } + + #[cfg(target_arch = "wasm32")] + Self {} + } + + #[allow(unused_variables)] + pub fn render<'a>( + &'a self, + render_pass: &mut wgpu::RenderPass<'a>, + constants: &'a wgpu::BindGroup, + layer: &'a Layer, + range: Range, + ) { + #[cfg(not(target_arch = "wasm32"))] + { + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, constants, &[]); + render_pass.set_vertex_buffer(0, layer.instances.slice(..)); + + render_pass.draw(0..6, range.start as u32..range.end as u32); + } + } +} diff --git a/crates/iced_wgpu/src/quad/solid.rs b/crates/iced_wgpu/src/quad/solid.rs new file mode 100644 index 000000000..7e48358ed --- /dev/null +++ b/crates/iced_wgpu/src/quad/solid.rs @@ -0,0 +1,162 @@ +use crate::Buffer; +use crate::graphics::color; +use crate::quad::{self, Quad}; + +use bytemuck::{Pod, Zeroable}; +use std::ops::Range; + +/// A quad filled with a solid color. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Solid { + /// The background color data of the quad. + pub color: color::Packed, + + /// The [`Quad`] data of the [`Solid`]. + pub quad: Quad, +} + +#[derive(Debug)] +pub struct Layer { + instances: Buffer, + instance_count: usize, +} + +impl Layer { + pub fn new(device: &wgpu::Device) -> Self { + let instances = Buffer::new( + device, + "iced_wgpu.quad.solid.buffer", + quad::INITIAL_INSTANCES, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + instances, + instance_count: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + instances: &[Solid], + ) { + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(device, encoder, belt, 0, instances); + + self.instance_count = instances.len(); + } +} + +#[derive(Debug, Clone)] +pub struct Pipeline { + pipeline: wgpu::RenderPipeline, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.quad.solid.pipeline"), + push_constant_ranges: &[], + bind_group_layouts: &[constants_layout], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.quad.solid.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + concat!( + include_str!("../shader/color.wgsl"), + "\n", + include_str!("../shader/quad.wgsl"), + "\n", + include_str!("../shader/vertex.wgsl"), + "\n", + include_str!("../shader/quad/solid.wgsl"), + ), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.quad.solid.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("solid_vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &wgpu::vertex_attr_array!( + // Color + 0 => Float32x4, + // Position + 1 => Float32x2, + // Size + 2 => Float32x2, + // Border color + 3 => Float32x4, + // Border radius + 4 => Float32x4, + // Border width + 5 => Float32, + // Shadow color + 6 => Float32x4, + // Shadow offset + 7 => Float32x2, + // Shadow blur radius + 8 => Float32, + // Snap + 9 => Uint32, + ), + }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("solid_fs_main"), + targets: &quad::color_target_state(format), + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + Self { pipeline } + } + + pub fn render<'a>( + &'a self, + render_pass: &mut wgpu::RenderPass<'a>, + constants: &'a wgpu::BindGroup, + layer: &'a Layer, + range: Range, + ) { + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, constants, &[]); + render_pass.set_vertex_buffer(0, layer.instances.slice(..)); + + render_pass.draw(0..6, range.start as u32..range.end as u32); + } +} diff --git a/crates/iced_wgpu/src/settings.rs b/crates/iced_wgpu/src/settings.rs new file mode 100644 index 000000000..646263b7b --- /dev/null +++ b/crates/iced_wgpu/src/settings.rs @@ -0,0 +1,84 @@ +//! Configure a renderer. +use crate::core::{Font, Pixels}; +use crate::graphics::{self, Antialiasing}; + +/// The settings of a [`Renderer`]. +/// +/// [`Renderer`]: crate::Renderer +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The present mode of the [`Renderer`]. + /// + /// [`Renderer`]: crate::Renderer + pub present_mode: wgpu::PresentMode, + + /// The graphics backends to use. + pub backends: wgpu::Backends, + + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: Pixels, + + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. + pub antialiasing: Option, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + present_mode: wgpu::PresentMode::AutoVsync, + backends: wgpu::Backends::all(), + default_font: Font::default(), + default_text_size: Pixels(16.0), + antialiasing: None, + } + } +} + +impl From for Settings { + fn from(settings: graphics::Settings) -> Self { + Self { + present_mode: if settings.vsync { + wgpu::PresentMode::AutoVsync + } else { + wgpu::PresentMode::AutoNoVsync + }, + 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 { + 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/crates/iced_wgpu/src/shader/blit.wgsl b/crates/iced_wgpu/src/shader/blit.wgsl new file mode 100644 index 000000000..5e600de4f --- /dev/null +++ b/crates/iced_wgpu/src/shader/blit.wgsl @@ -0,0 +1,37 @@ +var uvs: array, 6> = array, 6>( + vec2(0.0, 0.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 1.0) +); + +@group(0) @binding(0) var u_sampler: sampler; +@group(0) @binding(1) var u_ratio: vec4; +@group(1) @binding(0) var u_texture: texture_2d; + +struct VertexInput { + @builtin(vertex_index) vertex_index: u32, +} + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +} + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + let uv = uvs[input.vertex_index]; + + var out: VertexOutput; + out.uv = uv * u_ratio.xy; + out.position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); + + return out; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + return textureSample(u_texture, u_sampler, input.uv); +} diff --git a/crates/iced_wgpu/src/shader/color.wgsl b/crates/iced_wgpu/src/shader/color.wgsl new file mode 100644 index 000000000..a80663689 --- /dev/null +++ b/crates/iced_wgpu/src/shader/color.wgsl @@ -0,0 +1,14 @@ +fn premultiply(color: vec4) -> vec4 { + return vec4(color.xyz * color.a, color.a); +} + +fn unpack_color(data: vec2) -> vec4 { + return premultiply(unpack_u32(data)); +} + +fn unpack_u32(data: vec2) -> vec4 { + let rg: vec2 = unpack2x16float(data.x); + let ba: vec2 = unpack2x16float(data.y); + + return vec4(rg.y, rg.x, ba.y, ba.x); +} diff --git a/crates/iced_wgpu/src/shader/color/linear_rgb.wgsl b/crates/iced_wgpu/src/shader/color/linear_rgb.wgsl new file mode 100644 index 000000000..a5cf45d4f --- /dev/null +++ b/crates/iced_wgpu/src/shader/color/linear_rgb.wgsl @@ -0,0 +1,3 @@ +fn interpolate_color(from_: vec4, to_: vec4, factor: f32) -> vec4 { + return mix(from_, to_, factor); +} diff --git a/crates/iced_wgpu/src/shader/image.wgsl b/crates/iced_wgpu/src/shader/image.wgsl new file mode 100644 index 000000000..3a237c36a --- /dev/null +++ b/crates/iced_wgpu/src/shader/image.wgsl @@ -0,0 +1,129 @@ +struct Globals { + transform: mat4x4, + scale_factor: f32, +} + +@group(0) @binding(0) var globals: Globals; +@group(0) @binding(1) var u_sampler: sampler; +@group(1) @binding(0) var u_texture: texture_2d_array; + +struct VertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) center: vec2, + @location(1) clip_bounds: vec4, + @location(2) border_radius: vec4, + @location(3) tile: vec4, + @location(4) rotation: f32, + @location(5) opacity: f32, + @location(6) atlas_pos: vec2, + @location(7) atlas_scale: vec2, + @location(8) layer: i32, + @location(9) snap: u32, +} + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) @interpolate(flat) clip_bounds: vec4, + @location(1) @interpolate(flat) border_radius: vec4, + @location(2) @interpolate(flat) atlas: vec4, + @location(3) @interpolate(flat) layer: i32, + @location(4) @interpolate(flat) opacity: f32, + @location(5) uv: vec2, +} + +@vertex +fn vs_main(input: VertexInput) -> VertexOutput { + var out: VertexOutput; + + // Generate a vertex position in the range [0, 1] from the vertex index + let corner = vertex_position(input.vertex_index); + + let tile = input.tile; + let center = input.center; + + // List the unrotated tile corners + let corners = array, 4>( + tile.xy, // Top left + tile.xy + vec2(tile.z, 0.0), // Top right + tile.xy + vec2(0.0, tile.w), // Bottom left + tile.xy + tile.zw // Bottom right + ); + + // Rotate tile corners around center + let cos_r = cos(-input.rotation); // Clockwise + let sin_r = sin(-input.rotation); + var rotated = array, 4>(); + + for (var i = 0u; i < 4u; i++) { + let c = corners[i] - input.center; + rotated[i] = vec2(c.x * cos_r - c.y * sin_r, c.x * sin_r + c.y * cos_r) + input.center; + } + + // Find bounding box of rotated tile + var min_xy = rotated[0]; + var max_xy = rotated[0]; + for (var i = 1u; i < 4u; i++) { + min_xy = min(min_xy, rotated[i]); + max_xy = max(max_xy, rotated[i]); + } + let rotated_bounds = vec4(min_xy, max_xy - min_xy); + + // Intersect with clip bounds + let clip_min = max(rotated_bounds.xy, input.clip_bounds.xy); + let clip_max = min(rotated_bounds.xy + rotated_bounds.zw, input.clip_bounds.xy + input.clip_bounds.zw); + let clipped_tile = vec4(clip_min, max(vec2(0.0), clip_max - clip_min)); + + // Calculate the vertex position + let v_pos = clipped_tile.xy + corner * clipped_tile.zw; + out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * vec4(v_pos, 0.0, 1.0); + out.clip_bounds = globals.scale_factor * input.clip_bounds; + + // Calculate rotated UV + let uv = input.atlas_pos + (v_pos - tile.xy) / tile.zw * input.atlas_scale; + let uv_center = input.atlas_pos + input.atlas_scale / 2.0; + + let d = uv - uv_center; + out.uv = vec2(d.x * cos_r - d.y * sin_r, d.x * sin_r + d.y * cos_r) + uv_center; + + // Snap position to the pixel grid + if bool(input.snap) { + out.position = round(out.position); + out.clip_bounds = vec4( + round(out.clip_bounds.xy), + round(out.clip_bounds.xy + out.clip_bounds.zw) - out.clip_bounds.xy, + ); + } + + out.position = globals.transform * out.position; + out.border_radius = globals.scale_factor * min(input.border_radius, vec4(min(input.clip_bounds.z, input.clip_bounds.w) / 2.0)); + out.atlas = vec4(input.atlas_pos, input.atlas_pos + input.atlas_scale); + out.layer = input.layer; + out.opacity = input.opacity; + + return out; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + let fragment = input.position.xy; + let position = input.clip_bounds.xy; + let scale = input.clip_bounds.zw; + + let d = rounded_box_sdf( + 2.0 * (fragment - position - scale / 2.0), + scale, + input.border_radius * 2.0, + ) / 2.0; + + let antialias: f32 = clamp(1.0 - d, 0.0, 1.0); + let inside = all(input.uv >= input.atlas.xy) && all(input.uv <= input.atlas.zw); + + return textureSample(u_texture, u_sampler, input.uv, input.layer) * vec4(1.0, 1.0, 1.0, antialias * input.opacity * f32(inside)); +} + +fn rounded_box_sdf(p: vec2, size: vec2, corners: vec4) -> f32 { + var box_half = select(corners.yz, corners.xw, p.x > 0.0); + var corner = select(box_half.y, box_half.x, p.y > 0.0); + var q = abs(p) - size + corner; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner; +} diff --git a/crates/iced_wgpu/src/shader/quad.wgsl b/crates/iced_wgpu/src/shader/quad.wgsl new file mode 100644 index 000000000..f8258a4ec --- /dev/null +++ b/crates/iced_wgpu/src/shader/quad.wgsl @@ -0,0 +1,13 @@ +struct Globals { + transform: mat4x4, + scale: f32, +} + +@group(0) @binding(0) var globals: Globals; + +fn rounded_box_sdf(p: vec2, size: vec2, corners: vec4) -> f32 { + var box_half = select(corners.yz, corners.xw, p.x > 0.0); + var corner = select(box_half.y, box_half.x, p.y > 0.0); + var q = abs(p) - size + corner; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner; +} diff --git a/crates/iced_wgpu/src/shader/quad/gradient.wgsl b/crates/iced_wgpu/src/shader/quad/gradient.wgsl new file mode 100644 index 000000000..7c7a6e45f --- /dev/null +++ b/crates/iced_wgpu/src/shader/quad/gradient.wgsl @@ -0,0 +1,184 @@ +struct GradientVertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) @interpolate(flat) colors_1: vec4, + @location(1) @interpolate(flat) colors_2: vec4, + @location(2) @interpolate(flat) colors_3: vec4, + @location(3) @interpolate(flat) colors_4: vec4, + @location(4) @interpolate(flat) offsets: vec4, + @location(5) direction: vec4, + @location(6) position_and_scale: vec4, + @location(7) border_color: vec4, + @location(8) border_radius: vec4, + @location(9) border_width: f32, + @location(10) snap: u32, +} + +struct GradientVertexOutput { + @builtin(position) position: vec4, + @location(1) @interpolate(flat) colors_1: vec4, + @location(2) @interpolate(flat) colors_2: vec4, + @location(3) @interpolate(flat) colors_3: vec4, + @location(4) @interpolate(flat) colors_4: vec4, + @location(5) @interpolate(flat) offsets: vec4, + @location(6) direction: vec4, + @location(7) position_and_scale: vec4, + @location(8) border_color: vec4, + @location(9) border_radius: vec4, + @location(10) border_width: f32, +} + +@vertex +fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { + var out: GradientVertexOutput; + + var pos: vec2 = input.position_and_scale.xy * globals.scale; + var scale: vec2 = input.position_and_scale.zw * globals.scale; + + var pos_snap = vec2(0.0, 0.0); + var scale_snap = vec2(0.0, 0.0); + + if bool(input.snap) { + pos_snap = round(pos + vec2(0.001, 0.001)) - pos; + scale_snap = round(pos + scale + vec2(0.001, 0.001)) - pos - pos_snap - scale; + } + + var min_border_radius = min(input.position_and_scale.z, input.position_and_scale.w) * 0.5; + var border_radius: vec4 = vec4( + min(input.border_radius.x, min_border_radius), + min(input.border_radius.y, min_border_radius), + min(input.border_radius.z, min_border_radius), + min(input.border_radius.w, min_border_radius) + ); + + var transform: mat4x4 = mat4x4( + vec4(scale.x + scale_snap.x + 1.0, 0.0, 0.0, 0.0), + vec4(0.0, scale.y + scale_snap.y + 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(pos + pos_snap - vec2(0.5, 0.5), 0.0, 1.0) + ); + + out.position = globals.transform * transform * vec4(vertex_position(input.vertex_index), 0.0, 1.0); + out.colors_1 = input.colors_1; + out.colors_2 = input.colors_2; + out.colors_3 = input.colors_3; + out.colors_4 = input.colors_4; + out.offsets = input.offsets; + out.direction = input.direction * globals.scale; + out.position_and_scale = vec4(pos + pos_snap, scale + scale_snap); + out.border_color = premultiply(input.border_color); + out.border_radius = border_radius * globals.scale; + out.border_width = input.border_width * globals.scale; + + return out; +} + +fn random(coords: vec2) -> f32 { + return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453); +} + +/// Returns the current interpolated color with a max 8-stop gradient +fn gradient( + raw_position: vec2, + direction: vec4, + colors: array, 8>, + offsets: array, + last_index: i32 +) -> vec4 { + let start = direction.xy; + let end = direction.zw; + + let v1 = end - start; + let v2 = raw_position - start; + let unit = normalize(v1); + let coord_offset = dot(unit, v2) / length(v1); + + //need to store these as a var to use dynamic indexing in a loop + //this is already added to wgsl spec but not in wgpu yet + var colors_arr = colors; + var offsets_arr = offsets; + + var color: vec4; + + let noise_granularity: f32 = 0.3/255.0; + + for (var i: i32 = 0; i < last_index; i++) { + let curr_offset = offsets_arr[i]; + let next_offset = offsets_arr[i+1]; + + if (coord_offset <= offsets_arr[0]) { + color = colors_arr[0]; + } + + if (curr_offset <= coord_offset && coord_offset <= next_offset) { + let from_ = colors_arr[i]; + let to_ = colors_arr[i+1]; + let factor = smoothstep(curr_offset, next_offset, coord_offset); + + color = interpolate_color(from_, to_, factor); + } + + if (coord_offset >= offsets_arr[last_index]) { + color = colors_arr[last_index]; + } + } + + return color + mix(-noise_granularity, noise_granularity, random(raw_position)); +} + +@fragment +fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4 { + let colors = array, 8>( + unpack_color(input.colors_1.xy), + unpack_color(input.colors_1.zw), + unpack_color(input.colors_2.xy), + unpack_color(input.colors_2.zw), + unpack_color(input.colors_3.xy), + unpack_color(input.colors_3.zw), + unpack_color(input.colors_4.xy), + unpack_color(input.colors_4.zw), + ); + + let offsets_1: vec4 = unpack_u32(input.offsets.xy); + let offsets_2: vec4 = unpack_u32(input.offsets.zw); + + var offsets = array( + offsets_1.x, + offsets_1.y, + offsets_1.z, + offsets_1.w, + offsets_2.x, + offsets_2.y, + offsets_2.z, + offsets_2.w, + ); + + //TODO could just pass this in to the shader but is probably more performant to just check it here + var last_index = 7; + for (var i: i32 = 0; i <= 7; i++) { + if (offsets[i] > 1.0) { + last_index = i - 1; + break; + } + } + + var mixed_color: vec4 = gradient(input.position.xy, input.direction, colors, offsets, last_index); + + let pos = input.position_and_scale.xy; + let scale = input.position_and_scale.zw; + + var dist: f32 = rounded_box_sdf( + -(input.position.xy - pos - scale / 2.0) * 2.0, + scale, + input.border_radius * 2.0 + ) / 2.0; + + if (input.border_width > 0.0) { + mixed_color = mix( + mixed_color, + input.border_color, + clamp(0.5 + dist + input.border_width, 0.0, 1.0) + ); + } + + return mixed_color * clamp(0.5-dist, 0.0, 1.0); +} diff --git a/crates/iced_wgpu/src/shader/quad/solid.wgsl b/crates/iced_wgpu/src/shader/quad/solid.wgsl new file mode 100644 index 000000000..2a07e3fa0 --- /dev/null +++ b/crates/iced_wgpu/src/shader/quad/solid.wgsl @@ -0,0 +1,102 @@ +struct SolidVertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) color: vec4, + @location(1) pos: vec2, + @location(2) scale: vec2, + @location(3) border_color: vec4, + @location(4) border_radius: vec4, + @location(5) border_width: f32, + @location(6) shadow_color: vec4, + @location(7) shadow_offset: vec2, + @location(8) shadow_blur_radius: f32, + @location(9) snap: u32, +} + +struct SolidVertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) border_color: vec4, + @location(2) pos: vec2, + @location(3) scale: vec2, + @location(4) border_radius: vec4, + @location(5) border_width: f32, + @location(6) shadow_color: vec4, + @location(7) shadow_offset: vec2, + @location(8) shadow_blur_radius: f32, +} + +@vertex +fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { + var out: SolidVertexOutput; + + var pos: vec2 = (input.pos + min(input.shadow_offset, vec2(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale; + var scale: vec2 = (input.scale + vec2(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale; + + var pos_snap = vec2(0.0, 0.0); + var scale_snap = vec2(0.0, 0.0); + + if bool(input.snap) { + pos_snap = round(pos + vec2(0.001, 0.001)) - pos; + scale_snap = round(pos + scale + vec2(0.001, 0.001)) - pos - pos_snap - scale; + } + + let border_radius = min(input.border_radius, vec4(min(input.scale.x, input.scale.y) / 2.0)); + + var transform: mat4x4 = mat4x4( + vec4(scale.x + scale_snap.x + 1.0, 0.0, 0.0, 0.0), + vec4(0.0, scale.y + scale_snap.y + 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(pos + pos_snap - vec2(0.5, 0.5), 0.0, 1.0) + ); + + out.position = globals.transform * transform * vec4(vertex_position(input.vertex_index), 0.0, 1.0); + out.color = premultiply(input.color); + out.border_color = premultiply(input.border_color); + out.pos = input.pos * globals.scale + pos_snap; + out.scale = input.scale * globals.scale + scale_snap; + out.border_radius = border_radius * globals.scale; + out.border_width = input.border_width * globals.scale; + out.shadow_color = premultiply(input.shadow_color); + out.shadow_offset = input.shadow_offset * globals.scale; + out.shadow_blur_radius = input.shadow_blur_radius * globals.scale; + + return out; +} + +@fragment +fn solid_fs_main( + input: SolidVertexOutput +) -> @location(0) vec4 { + var mixed_color: vec4 = input.color; + + var dist = rounded_box_sdf( + -(input.position.xy - input.pos - input.scale * 0.5) * 2.0, + input.scale, + input.border_radius * 2.0 + ) / 2.0; + + if (input.border_width > 0.0) { + mixed_color = mix( + input.color, + input.border_color, + clamp(0.5 + dist + input.border_width, 0.0, 1.0) + ); + } + + var quad_alpha: f32 = clamp(0.5-dist, 0.0, 1.0); + + let quad_color = mixed_color * quad_alpha; + + if input.shadow_color.a > 0.0 { + var shadow_dist: f32 = rounded_box_sdf( + -(input.position.xy - input.pos - input.shadow_offset - input.scale/2.0) * 2.0, + input.scale, + input.border_radius * 2.0 + ) / 2.0; + let shadow_alpha = 1.0 - smoothstep(-input.shadow_blur_radius, input.shadow_blur_radius, max(shadow_dist, 0.0)); + + return mix(quad_color, input.shadow_color, (1.0 - quad_alpha) * shadow_alpha); + } else { + return quad_color; + } +} diff --git a/crates/iced_wgpu/src/shader/triangle.wgsl b/crates/iced_wgpu/src/shader/triangle.wgsl new file mode 100644 index 000000000..e4c193444 --- /dev/null +++ b/crates/iced_wgpu/src/shader/triangle.wgsl @@ -0,0 +1,5 @@ +struct Globals { + transform: mat4x4, +} + +@group(0) @binding(0) var globals: Globals; diff --git a/crates/iced_wgpu/src/shader/triangle/gradient.wgsl b/crates/iced_wgpu/src/shader/triangle/gradient.wgsl new file mode 100644 index 000000000..31a480269 --- /dev/null +++ b/crates/iced_wgpu/src/shader/triangle/gradient.wgsl @@ -0,0 +1,127 @@ +struct GradientVertexInput { + @location(0) v_pos: vec2, + @location(1) @interpolate(flat) colors_1: vec4, + @location(2) @interpolate(flat) colors_2: vec4, + @location(3) @interpolate(flat) colors_3: vec4, + @location(4) @interpolate(flat) colors_4: vec4, + @location(5) @interpolate(flat) offsets: vec4, + @location(6) direction: vec4, +} + +struct GradientVertexOutput { + @builtin(position) position: vec4, + @location(0) raw_position: vec2, + @location(1) @interpolate(flat) colors_1: vec4, + @location(2) @interpolate(flat) colors_2: vec4, + @location(3) @interpolate(flat) colors_3: vec4, + @location(4) @interpolate(flat) colors_4: vec4, + @location(5) @interpolate(flat) offsets: vec4, + @location(6) direction: vec4, +} + +@vertex +fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput { + var output: GradientVertexOutput; + + output.position = globals.transform * vec4(input.v_pos, 0.0, 1.0); + output.raw_position = input.v_pos; + output.colors_1 = input.colors_1; + output.colors_2 = input.colors_2; + output.colors_3 = input.colors_3; + output.colors_4 = input.colors_4; + output.offsets = input.offsets; + output.direction = input.direction; + + return output; +} + +/// Returns the current interpolated color with a max 8-stop gradient +fn gradient( + raw_position: vec2, + direction: vec4, + colors: array, 8>, + offsets: array, + last_index: i32 +) -> vec4 { + let start = direction.xy; + let end = direction.zw; + + let v1 = end - start; + let v2 = raw_position - start; + let unit = normalize(v1); + let coord_offset = dot(unit, v2) / length(v1); + + //need to store these as a var to use dynamic indexing in a loop + //this is already added to wgsl spec but not in wgpu yet + var colors_arr = colors; + var offsets_arr = offsets; + + var color: vec4; + + let noise_granularity: f32 = 0.3/255.0; + + for (var i: i32 = 0; i < last_index; i++) { + let curr_offset = offsets_arr[i]; + let next_offset = offsets_arr[i+1]; + + if (coord_offset <= offsets_arr[0]) { + color = colors_arr[0]; + } + + if (curr_offset <= coord_offset && coord_offset <= next_offset) { + let from_ = colors_arr[i]; + let to_ = colors_arr[i+1]; + let factor = smoothstep(curr_offset, next_offset, coord_offset); + + color = interpolate_color(from_, to_, factor); + } + + if (coord_offset >= offsets_arr[last_index]) { + color = colors_arr[last_index]; + } + } + + return color + mix(-noise_granularity, noise_granularity, random(raw_position)); +} + +@fragment +fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4 { + let colors = array, 8>( + unpack_color(input.colors_1.xy), + unpack_color(input.colors_1.zw), + unpack_color(input.colors_2.xy), + unpack_color(input.colors_2.zw), + unpack_color(input.colors_3.xy), + unpack_color(input.colors_3.zw), + unpack_color(input.colors_4.xy), + unpack_color(input.colors_4.zw), + ); + + let offsets_1: vec4 = unpack_u32(input.offsets.xy); + let offsets_2: vec4 = unpack_u32(input.offsets.zw); + + var offsets = array( + offsets_1.x, + offsets_1.y, + offsets_1.z, + offsets_1.w, + offsets_2.x, + offsets_2.y, + offsets_2.z, + offsets_2.w, + ); + + var last_index = 7; + for (var i: i32 = 0; i <= 7; i++) { + if (offsets[i] >= 1.0) { + last_index = i; + break; + } + } + + return gradient(input.raw_position, input.direction, colors, offsets, last_index); +} + +fn random(coords: vec2) -> f32 { + return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453); +} diff --git a/crates/iced_wgpu/src/shader/triangle/solid.wgsl b/crates/iced_wgpu/src/shader/triangle/solid.wgsl new file mode 100644 index 000000000..b0d8057bf --- /dev/null +++ b/crates/iced_wgpu/src/shader/triangle/solid.wgsl @@ -0,0 +1,24 @@ +struct SolidVertexInput { + @location(0) position: vec2, + @location(1) color: vec4, +} + +struct SolidVertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, +} + +@vertex +fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { + var out: SolidVertexOutput; + + out.color = premultiply(input.color); + out.position = globals.transform * vec4(input.position, 0.0, 1.0); + + return out; +} + +@fragment +fn solid_fs_main(input: SolidVertexOutput) -> @location(0) vec4 { + return input.color; +} diff --git a/crates/iced_wgpu/src/shader/vertex.wgsl b/crates/iced_wgpu/src/shader/vertex.wgsl new file mode 100644 index 000000000..e6af6fc00 --- /dev/null +++ b/crates/iced_wgpu/src/shader/vertex.wgsl @@ -0,0 +1,7 @@ +// Compute the normalized quad coordinates based on the vertex index. +fn vertex_position(vertex_index: u32) -> vec2 { + // #: 0 1 2 3 4 5 + // x: 1 1 0 0 0 1 + // y: 1 0 0 0 1 1 + return vec2((vec2(1u, 2u) + vertex_index) % vec2(6u) < vec2(3u)); +} diff --git a/crates/iced_wgpu/src/text.rs b/crates/iced_wgpu/src/text.rs new file mode 100644 index 000000000..16efa092c --- /dev/null +++ b/crates/iced_wgpu/src/text.rs @@ -0,0 +1,648 @@ +use crate::core::alignment; +use crate::core::text::Alignment; +use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::cache; +use crate::graphics::color; +use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache}; +use crate::graphics::text::{Editor, Paragraph, font_system, to_color}; + +use rustc_hash::FxHashMap; +use std::collections::hash_map; +use std::sync::atomic::{self, AtomicU64}; +use std::sync::{self, Arc, RwLock}; + +pub use crate::graphics::Text; + +const COLOR_MODE: cryoglyph::ColorMode = if color::GAMMA_CORRECTION { + cryoglyph::ColorMode::Accurate +} else { + cryoglyph::ColorMode::Web +}; + +pub type Batch = Vec; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + text: Vec, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + group: cache::Group, + text: Arc<[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) -> Option { + 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: Arc::from(text), + version: 0, + }) + } + + pub fn update(&mut self, text: Vec) { + if self.text.is_empty() && text.is_empty() { + return; + } + + self.text = Arc::from(text); + self.version += 1; + } +} + +struct Upload { + renderer: cryoglyph::TextRenderer, + buffer_cache: BufferCache, + transformation: Transformation, + version: usize, + group_version: usize, + text: sync::Weak<[Text]>, + _atlas: sync::Weak<()>, +} + +#[derive(Default)] +pub struct Storage { + groups: FxHashMap, + uploads: FxHashMap, +} + +struct Group { + atlas: cryoglyph::TextAtlas, + version: usize, + should_trim: bool, + handle: Arc<()>, // Keeps track of active uploads +} + +impl Storage { + fn get(&self, cache: &Cache) -> Option<(&cryoglyph::TextAtlas, &Upload)> { + if cache.text.is_empty() { + return None; + } + + self.groups + .get(&cache.group) + .map(|group| &group.atlas) + .zip(self.uploads.get(&cache.id)) + } + + fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + viewport: &cryoglyph::Viewport, + encoder: &mut wgpu::CommandEncoder, + format: wgpu::TextureFormat, + state: &cryoglyph::Cache, + cache: &Cache, + new_transformation: Transformation, + bounds: Rectangle, + ) { + 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: cryoglyph::TextAtlas::with_color_mode( + device, queue, state, format, COLOR_MODE, + ), + version: 0, + should_trim: false, + handle: Arc::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, + ); + } + + // Only trim if glyphs have changed + group.should_trim = + group.should_trim || upload.version != cache.version; + + upload.text = Arc::downgrade(&cache.text); + upload.version = cache.version; + upload.group_version = group.version; + upload.transformation = new_transformation; + + upload.buffer_cache.trim(); + } + } + hash_map::Entry::Vacant(entry) => { + let mut renderer = cryoglyph::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, + ); + } + + let _ = entry.insert(Upload { + renderer, + buffer_cache, + transformation: new_transformation, + version: 0, + group_version: group.version, + text: Arc::downgrade(&cache.text), + _atlas: Arc::downgrade(&group.handle), + }); + + group.should_trim = cache.group.is_singleton(); + + log::debug!( + "New text upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); + } + } + } + + pub fn trim(&mut self) { + self.uploads + .retain(|_id, upload| upload.text.strong_count() > 0); + + self.groups.retain(|id, group| { + let active_uploads = Arc::weak_count(&group.handle); + + if active_uploads == 0 { + log::debug!("Dropping text atlas: {id:?}"); + + 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(cryoglyph::Viewport); + +impl Viewport { + pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size) { + self.0.update( + queue, + cryoglyph::Resolution { + width: resolution.width, + height: resolution.height, + }, + ); + } +} + +#[derive(Clone)] +pub struct Pipeline { + format: wgpu::TextureFormat, + cache: cryoglyph::Cache, + atlas: Arc>, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + ) -> Self { + let cache = cryoglyph::Cache::new(device); + let atlas = cryoglyph::TextAtlas::with_color_mode( + device, queue, &cache, format, COLOR_MODE, + ); + + Pipeline { + format, + cache, + atlas: Arc::new(RwLock::new(atlas)), + } + } + + pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport { + Viewport(cryoglyph::Viewport::new(device, &self.cache)) + } + + pub fn trim(&self) { + self.atlas.write().expect("Write text atlas").trim(); + } +} + +#[derive(Default)] +pub struct State { + renderers: Vec, + prepare_layer: usize, + cache: BufferCache, + storage: Storage, +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn prepare( + &mut self, + pipeline: &Pipeline, + device: &wgpu::Device, + queue: &wgpu::Queue, + viewport: &Viewport, + encoder: &mut wgpu::CommandEncoder, + batch: &Batch, + layer_bounds: Rectangle, + layer_transformation: Transformation, + ) { + let mut atlas = pipeline.atlas.write().expect("Write to text atlas"); + + for item in batch { + match item { + Item::Group { + transformation, + text, + } => { + if self.renderers.len() <= self.prepare_layer { + self.renderers.push(cryoglyph::TextRenderer::new( + &mut atlas, + device, + wgpu::MultisampleState::default(), + None, + )); + } + + let renderer = &mut self.renderers[self.prepare_layer]; + let result = prepare( + device, + queue, + &viewport.0, + encoder, + renderer, + &mut atlas, + &mut self.cache, + text, + layer_bounds * layer_transformation, + layer_transformation * *transformation, + ); + + match result { + Ok(()) => { + self.prepare_layer += 1; + } + Err(cryoglyph::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, + } => { + self.storage.prepare( + device, + queue, + &viewport.0, + encoder, + pipeline.format, + &pipeline.cache, + cache, + layer_transformation * *transformation, + layer_bounds * layer_transformation, + ); + } + } + } + } + + pub fn render<'a>( + &'a self, + pipeline: &'a Pipeline, + viewport: &'a Viewport, + start: usize, + batch: &'a Batch, + bounds: Rectangle, + render_pass: &mut wgpu::RenderPass<'a>, + ) -> usize { + let atlas = pipeline.atlas.read().expect("Read text atlas"); + let mut layer_count = 0; + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + for item in batch { + match item { + Item::Group { .. } => { + let renderer = &self.renderers[start + layer_count]; + + renderer + .render(&atlas, &viewport.0, render_pass) + .expect("Render text"); + + layer_count += 1; + } + Item::Cached { cache, .. } => { + if let Some((atlas, upload)) = self.storage.get(cache) { + upload + .renderer + .render(atlas, &viewport.0, render_pass) + .expect("Render cached text"); + } + } + } + } + + layer_count + } + + pub fn trim(&mut self) { + self.cache.trim(); + self.storage.trim(); + + self.prepare_layer = 0; + } +} + +fn prepare( + device: &wgpu::Device, + queue: &wgpu::Queue, + viewport: &cryoglyph::Viewport, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut cryoglyph::TextRenderer, + atlas: &mut cryoglyph::TextAtlas, + buffer_cache: &mut BufferCache, + sections: &[Text], + layer_bounds: Rectangle, + layer_transformation: Transformation, +) -> Result<(), cryoglyph::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), + } + + 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, + align_x, + .. + } => { + let (key, _) = buffer_cache.allocate( + font_system, + text_cache::Key { + content, + size: f32::from(*size), + line_height: f32::from(*line_height), + font: *font, + align_x: *align_x, + 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, position, color, clip_bounds, transformation) = + match section { + Text::Paragraph { + position, + color, + clip_bounds, + transformation, + .. + } => { + let Some(Allocation::Paragraph(paragraph)) = allocation + else { + return None; + }; + + ( + paragraph.buffer(), + *position, + *color, + *clip_bounds, + *transformation, + ) + } + Text::Editor { + position, + color, + clip_bounds, + transformation, + .. + } => { + let Some(Allocation::Editor(editor)) = allocation + else { + return None; + }; + + ( + editor.buffer(), + *position, + *color, + *clip_bounds, + *transformation, + ) + } + Text::Cached { + bounds, + align_x, + align_y, + color, + clip_bounds, + .. + } => { + let Some(Allocation::Cache(key)) = allocation else { + return None; + }; + + let entry = + buffer_cache.get(key).expect("Get cached buffer"); + + let mut position = bounds.position(); + + position.x = match align_x { + Alignment::Default + | Alignment::Left + | Alignment::Justified => position.x, + Alignment::Center => { + position.x - entry.min_bounds.width / 2.0 + } + Alignment::Right => { + position.x - entry.min_bounds.width + } + }; + + position.y = match align_y { + alignment::Vertical::Top => position.y, + alignment::Vertical::Center => { + position.y - entry.min_bounds.height / 2.0 + } + alignment::Vertical::Bottom => { + position.y - entry.min_bounds.height + } + }; + + ( + &entry.buffer, + position, + *color, + *clip_bounds, + Transformation::IDENTITY, + ) + } + Text::Raw { + raw, + transformation, + } => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + ( + buffer.as_ref(), + raw.position, + raw.color, + raw.clip_bounds, + *transformation, + ) + } + }; + + let position = position * transformation * layer_transformation; + + let clip_bounds = layer_bounds.intersection( + &(clip_bounds * transformation * layer_transformation), + )?; + + Some(cryoglyph::TextArea { + buffer, + left: position.x, + top: position.y, + scale: transformation.scale_factor() + * layer_transformation.scale_factor(), + bounds: cryoglyph::TextBounds { + left: clip_bounds.x.round() as i32, + top: clip_bounds.y.round() as i32, + right: (clip_bounds.x + clip_bounds.width).round() as i32, + bottom: (clip_bounds.y + clip_bounds.height).round() as i32, + }, + default_color: to_color(color), + }) + }, + ); + + renderer.prepare( + device, + queue, + encoder, + font_system, + atlas, + viewport, + text_areas, + &mut cryoglyph::SwashCache::new(), + ) +} diff --git a/crates/iced_wgpu/src/triangle.rs b/crates/iced_wgpu/src/triangle.rs new file mode 100644 index 000000000..fa5ecb9c3 --- /dev/null +++ b/crates/iced_wgpu/src/triangle.rs @@ -0,0 +1,963 @@ +//! Draw meshes of triangles. +mod msaa; + +use crate::Buffer; +use crate::core::{Point, Rectangle, Size, Transformation, Vector}; +use crate::graphics::Antialiasing; +use crate::graphics::mesh::{self, Mesh}; + +use rustc_hash::FxHashMap; +use std::collections::hash_map; +use std::sync::Weak; + +const INITIAL_INDEX_COUNT: usize = 1_000; +const INITIAL_VERTEX_COUNT: usize = 1_000; + +pub type Batch = Vec; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + meshes: Vec, + }, + Cached { + transformation: Transformation, + cache: mesh::Cache, + }, +} + +#[derive(Debug)] +struct Upload { + layer: Layer, + transformation: Transformation, + version: usize, + batch: Weak<[Mesh]>, +} + +#[derive(Debug, Default)] +pub struct Storage { + uploads: FxHashMap, +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, cache: &mesh::Cache) -> Option<&Upload> { + if cache.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: &mesh::Cache, + new_transformation: Transformation, + ) { + match self.uploads.entry(cache.id()) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if !cache.is_empty() + && (upload.version != cache.version() + || upload.transformation != new_transformation) + { + upload.layer.prepare( + device, + encoder, + belt, + solid, + gradient, + cache.batch(), + new_transformation, + ); + + upload.batch = cache.downgrade(); + 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: cache.downgrade(), + }); + + log::debug!( + "New mesh upload: {:?} (total: {})", + cache.id(), + self.uploads.len() + ); + } + } + } + + pub fn trim(&mut self) { + self.uploads + .retain(|_id, upload| upload.batch.strong_count() > 0); + } +} + +#[derive(Debug, Clone)] +pub struct Pipeline { + msaa: Option, + solid: solid::Pipeline, + gradient: gradient::Pipeline, +} + +pub struct State { + msaa: Option, + layers: Vec, + prepare_layer: usize, + storage: Storage, +} + +impl State { + pub fn new(device: &wgpu::Device, pipeline: &Pipeline) -> Self { + Self { + msaa: pipeline + .msaa + .as_ref() + .map(|pipeline| msaa::State::new(device, pipeline)), + layers: Vec::new(), + prepare_layer: 0, + storage: Storage::new(), + } + } + + pub fn prepare( + &mut self, + pipeline: &Pipeline, + device: &wgpu::Device, + belt: &mut wgpu::util::StagingBelt, + encoder: &mut wgpu::CommandEncoder, + items: &[Item], + scale: Transformation, + target_size: Size, + ) { + let projection = if let Some((state, pipeline)) = + self.msaa.as_mut().zip(pipeline.msaa.as_ref()) + { + state.prepare(device, encoder, belt, pipeline, target_size) * scale + } else { + Transformation::orthographic(target_size.width, target_size.height) + * scale + }; + + for item in items { + match item { + Item::Group { + transformation, + meshes, + } => { + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &pipeline.solid, + &pipeline.gradient, + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare( + device, + encoder, + belt, + &pipeline.solid, + &pipeline.gradient, + meshes, + projection * *transformation, + ); + + self.prepare_layer += 1; + } + Item::Cached { + transformation, + cache, + } => { + self.storage.prepare( + device, + encoder, + belt, + &pipeline.solid, + &pipeline.gradient, + cache, + projection * *transformation, + ); + } + } + } + } + + pub fn render( + &mut self, + pipeline: &Pipeline, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + 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 = self.storage.get(cache)?; + + Some(( + &upload.layer, + cache.batch(), + screen_transformation * *transformation, + )) + } + }); + + render( + encoder, + target, + self.msaa.as_ref().zip(pipeline.msaa.as_ref()), + &pipeline.solid, + &pipeline.gradient, + bounds, + items, + ); + + layer_count + } + + pub fn trim(&mut self) { + self.storage.trim(); + + self.prepare_layer = 0; + } +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option, + ) -> Pipeline { + Pipeline { + msaa: antialiasing.map(|a| msaa::Pipeline::new(device, format, a)), + solid: solid::Pipeline::new(device, format, antialiasing), + gradient: gradient::Pipeline::new(device, format, antialiasing), + } + } +} + +fn render<'a>( + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + mut msaa: Option<(&msaa::State, &msaa::Pipeline)>, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + bounds: Rectangle, + group: impl Iterator, +) { + { + let mut render_pass = if let Some((_state, pipeline)) = &mut msaa { + pipeline.render_pass(encoder) + } else { + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + depth_slice: None, + 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, + }) + }; + + for (layer, meshes, transformation) in group { + layer.render( + solid, + gradient, + meshes, + bounds, + transformation, + &mut render_pass, + ); + } + } + + if let Some((state, pipeline)) = msaa { + state.render(pipeline, encoder, target); + } +} + +#[derive(Debug)] +pub struct Layer { + index_buffer: Buffer, + solid: solid::Layer, + gradient: gradient::Layer, +} + +impl Layer { + fn new( + device: &wgpu::Device, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + ) -> Self { + Self { + index_buffer: Buffer::new( + device, + "iced_wgpu.triangle.index_buffer", + INITIAL_INDEX_COUNT, + wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + ), + solid: solid::Layer::new(device, &solid.constants_layout), + gradient: gradient::Layer::new(device, &gradient.constants_layout), + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + meshes: &[Mesh], + transformation: Transformation, + ) { + // Count the total amount of vertices & indices we need to handle + let count = mesh::attribute_count_of(meshes); + + // Then we ensure the current attribute buffers are big enough, resizing if necessary. + // We are not currently using the return value of these functions as we have no system in + // place to calculate mesh diff, or to know whether or not that would be more performant for + // the majority of use cases. Therefore we will write GPU data every frame (for now). + let _ = self.index_buffer.resize(device, count.indices); + let _ = self.solid.vertices.resize(device, count.solid_vertices); + let _ = self + .gradient + .vertices + .resize(device, count.gradient_vertices); + + if self.solid.uniforms.resize(device, count.solids) { + self.solid.constants = solid::Layer::bind_group( + device, + &self.solid.uniforms.raw, + &solid.constants_layout, + ); + } + + if self.gradient.uniforms.resize(device, count.gradients) { + self.gradient.constants = gradient::Layer::bind_group( + device, + &self.gradient.uniforms.raw, + &gradient.constants_layout, + ); + } + + let mut solid_vertex_offset = 0; + let mut solid_uniform_offset = 0; + let mut gradient_vertex_offset = 0; + let mut gradient_uniform_offset = 0; + let mut index_offset = 0; + + for mesh in meshes { + let clip_bounds = mesh.clip_bounds() * transformation; + let snap_distance = clip_bounds + .snap() + .map(|snapped_bounds| { + Point::new(snapped_bounds.x as f32, snapped_bounds.y as f32) + - clip_bounds.position() + }) + .unwrap_or(Vector::ZERO); + + let uniforms = Uniforms::new( + transformation + * mesh.transformation() + * Transformation::translate( + snap_distance.x, + snap_distance.y, + ), + ); + + let indices = mesh.indices(); + + index_offset += self.index_buffer.write( + device, + encoder, + belt, + index_offset, + indices, + ); + + match mesh { + Mesh::Solid { buffers, .. } => { + solid_vertex_offset += self.solid.vertices.write( + device, + encoder, + belt, + solid_vertex_offset, + &buffers.vertices, + ); + + solid_uniform_offset += self.solid.uniforms.write( + device, + encoder, + belt, + solid_uniform_offset, + &[uniforms], + ); + } + Mesh::Gradient { buffers, .. } => { + gradient_vertex_offset += self.gradient.vertices.write( + device, + encoder, + belt, + gradient_vertex_offset, + &buffers.vertices, + ); + + gradient_uniform_offset += self.gradient.uniforms.write( + device, + encoder, + belt, + gradient_uniform_offset, + &[uniforms], + ); + } + } + } + } + + fn render<'a>( + &'a self, + solid: &'a solid::Pipeline, + gradient: &'a gradient::Pipeline, + meshes: &[Mesh], + bounds: Rectangle, + transformation: Transformation, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + let mut num_solids = 0; + let mut num_gradients = 0; + let mut solid_offset = 0; + let mut gradient_offset = 0; + let mut index_offset = 0; + let mut last_is_solid = None; + + for mesh in meshes { + let Some(clip_bounds) = bounds + .intersection(&(mesh.clip_bounds() * transformation)) + .and_then(Rectangle::snap) + else { + match mesh { + Mesh::Solid { buffers, .. } => { + solid_offset += buffers.vertices.len(); + num_solids += 1; + } + Mesh::Gradient { buffers, .. } => { + gradient_offset += buffers.vertices.len(); + num_gradients += 1; + } + } + continue; + }; + + render_pass.set_scissor_rect( + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, + ); + + match mesh { + Mesh::Solid { buffers, .. } => { + if !last_is_solid.unwrap_or(false) { + render_pass.set_pipeline(&solid.pipeline); + + last_is_solid = Some(true); + } + + render_pass.set_bind_group( + 0, + &self.solid.constants, + &[(num_solids * std::mem::size_of::()) + as u32], + ); + + render_pass.set_vertex_buffer( + 0, + self.solid.vertices.range( + solid_offset, + solid_offset + buffers.vertices.len(), + ), + ); + + num_solids += 1; + solid_offset += buffers.vertices.len(); + } + Mesh::Gradient { buffers, .. } => { + if last_is_solid.unwrap_or(true) { + render_pass.set_pipeline(&gradient.pipeline); + + last_is_solid = Some(false); + } + + render_pass.set_bind_group( + 0, + &self.gradient.constants, + &[(num_gradients * std::mem::size_of::()) + as u32], + ); + + render_pass.set_vertex_buffer( + 0, + self.gradient.vertices.range( + gradient_offset, + gradient_offset + buffers.vertices.len(), + ), + ); + + num_gradients += 1; + gradient_offset += buffers.vertices.len(); + } + }; + + render_pass.set_index_buffer( + self.index_buffer + .range(index_offset, index_offset + mesh.indices().len()), + wgpu::IndexFormat::Uint32, + ); + + render_pass.draw_indexed(0..mesh.indices().len() as u32, 0, 0..1); + + index_offset += mesh.indices().len(); + } + } +} + +fn fragment_target( + texture_format: wgpu::TextureFormat, +) -> wgpu::ColorTargetState { + wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + } +} + +fn primitive_state() -> wgpu::PrimitiveState { + wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + } +} + +fn multisample_state( + antialiasing: Option, +) -> wgpu::MultisampleState { + wgpu::MultisampleState { + count: antialiasing.map(Antialiasing::sample_count).unwrap_or(1), + mask: !0, + alpha_to_coverage_enabled: false, + } +} + +#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Uniforms { + transform: [f32; 16], + /// Uniform values must be 256-aligned; + /// see: [`wgpu::Limits`] `min_uniform_buffer_offset_alignment`. + _padding: [f32; 48], +} + +impl Uniforms { + pub fn new(transform: Transformation) -> Self { + Self { + transform: transform.into(), + _padding: [0.0; 48], + } + } + + pub fn entry() -> wgpu::BindGroupLayoutEntry { + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }, + count: None, + } + } + + pub fn min_size() -> Option { + wgpu::BufferSize::new(std::mem::size_of::() as u64) + } +} + +mod solid { + use crate::Buffer; + use crate::graphics::Antialiasing; + use crate::graphics::mesh; + use crate::triangle; + + #[derive(Debug, Clone)] + pub struct Pipeline { + pub pipeline: wgpu::RenderPipeline, + pub constants_layout: wgpu::BindGroupLayout, + } + + #[derive(Debug)] + pub struct Layer { + pub vertices: Buffer, + pub uniforms: Buffer, + pub constants: wgpu::BindGroup, + } + + impl Layer { + pub fn new( + device: &wgpu::Device, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + let vertices = Buffer::new( + device, + "iced_wgpu.triangle.solid.vertex_buffer", + triangle::INITIAL_VERTEX_COUNT, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + let uniforms = Buffer::new( + device, + "iced_wgpu.triangle.solid.uniforms", + 1, + wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + ); + + let constants = + Self::bind_group(device, &uniforms.raw, constants_layout); + + Self { + vertices, + uniforms, + constants, + } + } + + pub fn bind_group( + device: &wgpu::Device, + buffer: &wgpu::Buffer, + layout: &wgpu::BindGroupLayout, + ) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.triangle.solid.bind_group"), + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer, + offset: 0, + size: triangle::Uniforms::min_size(), + }, + ), + }], + }) + } + } + + impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option, + ) -> Self { + let constants_layout = device.create_bind_group_layout( + &wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.triangle.solid.bind_group_layout"), + entries: &[triangle::Uniforms::entry()], + }, + ); + + let layout = device.create_pipeline_layout( + &wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.triangle.solid.pipeline_layout"), + bind_group_layouts: &[&constants_layout], + push_constant_ranges: &[], + }, + ); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.triangle.solid.shader"), + source: wgpu::ShaderSource::Wgsl( + std::borrow::Cow::Borrowed(concat!( + include_str!("shader/triangle.wgsl"), + "\n", + include_str!("shader/triangle/solid.wgsl"), + "\n", + include_str!("shader/color.wgsl"), + )), + ), + }); + + let pipeline = + device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu::triangle::solid pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("solid_vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::< + mesh::SolidVertex2D, + >( + ) + as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array!( + // Position + 0 => Float32x2, + // Color + 1 => Float32x4, + ), + }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("solid_fs_main"), + targets: &[Some(triangle::fragment_target(format))], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }), + primitive: triangle::primitive_state(), + depth_stencil: None, + multisample: triangle::multisample_state(antialiasing), + multiview: None, + cache: None, + }, + ); + + Self { + pipeline, + constants_layout, + } + } + } +} + +mod gradient { + use crate::Buffer; + use crate::graphics::Antialiasing; + use crate::graphics::mesh; + use crate::triangle; + + #[derive(Debug, Clone)] + pub struct Pipeline { + pub pipeline: wgpu::RenderPipeline, + pub constants_layout: wgpu::BindGroupLayout, + } + + #[derive(Debug)] + pub struct Layer { + pub vertices: Buffer, + pub uniforms: Buffer, + pub constants: wgpu::BindGroup, + } + + impl Layer { + pub fn new( + device: &wgpu::Device, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + let vertices = Buffer::new( + device, + "iced_wgpu.triangle.gradient.vertex_buffer", + triangle::INITIAL_VERTEX_COUNT, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + let uniforms = Buffer::new( + device, + "iced_wgpu.triangle.gradient.uniforms", + 1, + wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + ); + + let constants = + Self::bind_group(device, &uniforms.raw, constants_layout); + + Self { + vertices, + uniforms, + constants, + } + } + + pub fn bind_group( + device: &wgpu::Device, + uniform_buffer: &wgpu::Buffer, + layout: &wgpu::BindGroupLayout, + ) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.triangle.gradient.bind_group"), + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: uniform_buffer, + offset: 0, + size: triangle::Uniforms::min_size(), + }, + ), + }], + }) + } + } + + impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option, + ) -> Self { + let constants_layout = device.create_bind_group_layout( + &wgpu::BindGroupLayoutDescriptor { + label: Some( + "iced_wgpu.triangle.gradient.bind_group_layout", + ), + entries: &[triangle::Uniforms::entry()], + }, + ); + + let layout = device.create_pipeline_layout( + &wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.triangle.gradient.pipeline_layout"), + bind_group_layouts: &[&constants_layout], + push_constant_ranges: &[], + }, + ); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.triangle.gradient.shader"), + source: wgpu::ShaderSource::Wgsl( + std::borrow::Cow::Borrowed(concat!( + include_str!("shader/triangle.wgsl"), + "\n", + include_str!("shader/triangle/gradient.wgsl"), + "\n", + include_str!("shader/color.wgsl"), + "\n", + include_str!("shader/color/linear_rgb.wgsl") + )), + ), + }); + + let pipeline = device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu.triangle.gradient.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("gradient_vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::< + mesh::GradientVertex2D, + >() + as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array!( + // Position + 0 => Float32x2, + // Colors 1-2 + 1 => Uint32x4, + // Colors 3-4 + 2 => Uint32x4, + // Colors 5-6 + 3 => Uint32x4, + // Colors 7-8 + 4 => Uint32x4, + // Offsets + 5 => Uint32x4, + // Direction + 6 => Float32x4 + ), + }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("gradient_fs_main"), + targets: &[Some(triangle::fragment_target(format))], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }), + primitive: triangle::primitive_state(), + depth_stencil: None, + multisample: triangle::multisample_state(antialiasing), + multiview: None, + cache: None, + }, + ); + + Self { + pipeline, + constants_layout, + } + } + } +} diff --git a/crates/iced_wgpu/src/triangle/msaa.rs b/crates/iced_wgpu/src/triangle/msaa.rs new file mode 100644 index 000000000..8ca494615 --- /dev/null +++ b/crates/iced_wgpu/src/triangle/msaa.rs @@ -0,0 +1,371 @@ +use crate::core::{Size, Transformation}; +use crate::graphics; + +use std::num::NonZeroU64; +use std::sync::{Arc, RwLock}; + +#[derive(Debug, Clone)] +pub struct Pipeline { + format: wgpu::TextureFormat, + sampler: wgpu::Sampler, + raw: wgpu::RenderPipeline, + constant_layout: wgpu::BindGroupLayout, + texture_layout: wgpu::BindGroupLayout, + sample_count: u32, + targets: Arc>>, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: graphics::Antialiasing, + ) -> Pipeline { + let sampler = + device.create_sampler(&wgpu::SamplerDescriptor::default()); + + 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, + }, + 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 texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu::triangle::msaa texture layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu::triangle::msaa pipeline layout"), + push_constant_ranges: &[], + bind_group_layouts: &[&constant_layout, &texture_layout], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu triangle blit_shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shader/blit.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu::triangle::msaa pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some( + wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, + ), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Cw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + Self { + format, + sampler, + raw: pipeline, + constant_layout, + texture_layout, + sample_count: antialiasing.sample_count(), + targets: Arc::new(RwLock::new(None)), + } + } + + fn targets( + &self, + device: &wgpu::Device, + region_size: Size, + ) -> Targets { + let mut targets = self.targets.write().expect("Write MSAA targets"); + + match targets.as_mut() { + Some(targets) + if region_size.width <= targets.size.width + && region_size.height <= targets.size.height => {} + _ => { + *targets = Some(Targets::new( + device, + self.format, + &self.texture_layout, + self.sample_count, + region_size, + )); + } + } + + targets.as_ref().unwrap().clone() + } + + pub fn render_pass<'a>( + &self, + encoder: &'a mut wgpu::CommandEncoder, + ) -> wgpu::RenderPass<'a> { + let targets = self.targets.read().expect("Read MSAA targets"); + let targets = targets.as_ref().unwrap(); + + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &targets.attachment, + depth_slice: None, + resolve_target: Some(&targets.resolve), + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }) + } +} + +#[derive(Debug, Clone)] +struct Targets { + attachment: wgpu::TextureView, + resolve: wgpu::TextureView, + bind_group: wgpu::BindGroup, + size: Size, +} + +impl Targets { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + texture_layout: &wgpu::BindGroupLayout, + sample_count: u32, + size: Size, + ) -> Targets { + let extent = wgpu::Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1, + }; + + let attachment = device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu::triangle::msaa attachment"), + size: extent, + mip_level_count: 1, + sample_count, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + let resolve = device.create_texture(&wgpu::TextureDescriptor { + label: Some("iced_wgpu::triangle::msaa resolve target"), + size: extent, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let attachment = + attachment.create_view(&wgpu::TextureViewDescriptor::default()); + + let resolve = + resolve.create_view(&wgpu::TextureViewDescriptor::default()); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::triangle::msaa texture bind group"), + layout: texture_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&resolve), + }], + }); + + Targets { + attachment, + resolve, + bind_group, + size, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Ratio { + u: f32, + v: f32, + // Padding field for 16-byte alignment. + // See https://docs.rs/wgpu/latest/wgpu/struct.DownlevelFlags.html#associatedconstant.BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED + _padding: [f32; 2], +} + +pub struct State { + ratio: wgpu::Buffer, + constants: wgpu::BindGroup, + last_ratio: Option, +} + +impl State { + pub fn new(device: &wgpu::Device, pipeline: &Pipeline) -> Self { + let ratio = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::triangle::msaa ratio"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + mapped_at_creation: false, + }); + + let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::triangle::msaa uniforms bind group"), + layout: &pipeline.constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&pipeline.sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: ratio.as_entire_binding(), + }, + ], + }); + + Self { + ratio, + constants, + last_ratio: None, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + pipeline: &Pipeline, + region_size: Size, + ) -> Transformation { + let targets = pipeline.targets(device, region_size); + + let ratio = Ratio { + u: region_size.width as f32 / targets.size.width as f32, + v: region_size.height as f32 / targets.size.height as f32, + _padding: [0.0; 2], + }; + + if Some(ratio) != self.last_ratio { + belt.write_buffer( + encoder, + &self.ratio, + 0, + NonZeroU64::new(std::mem::size_of::() as u64) + .expect("non-empty ratio"), + device, + ) + .copy_from_slice(bytemuck::bytes_of(&ratio)); + + self.last_ratio = Some(ratio); + } + + Transformation::orthographic(targets.size.width, targets.size.height) + } + + pub fn render( + &self, + pipeline: &Pipeline, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + ) { + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu::triangle::msaa render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_pass.set_pipeline(&pipeline.raw); + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_bind_group( + 1, + &pipeline + .targets + .read() + .expect("Read MSAA targets") + .as_ref() + .unwrap() + .bind_group, + &[], + ); + render_pass.draw(0..6, 0..1); + } +} diff --git a/crates/iced_wgpu/src/window.rs b/crates/iced_wgpu/src/window.rs new file mode 100644 index 000000000..9545a14e5 --- /dev/null +++ b/crates/iced_wgpu/src/window.rs @@ -0,0 +1,5 @@ +//! Display rendering results on windows. +pub mod compositor; + +pub use compositor::Compositor; +pub use wgpu::Surface; diff --git a/crates/iced_wgpu/src/window/compositor.rs b/crates/iced_wgpu/src/window/compositor.rs new file mode 100644 index 000000000..f89d63102 --- /dev/null +++ b/crates/iced_wgpu/src/window/compositor.rs @@ -0,0 +1,358 @@ +//! Connect a window with a renderer. +use crate::core::Color; +use crate::graphics::color; +use crate::graphics::compositor; +use crate::graphics::error; +use crate::graphics::{self, Shell, Viewport}; +use crate::settings::{self, Settings}; +use crate::{Engine, Renderer}; + +/// A window graphics backend for iced powered by `wgpu`. +pub struct Compositor { + instance: wgpu::Instance, + adapter: wgpu::Adapter, + 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 for graphics::Error { + fn from(error: Error) -> Self { + Self::GraphicsAdapterNotFound { + backend: "wgpu", + reason: error::Reason::RequestFailed(error.to_string()), + } + } +} + +impl Compositor { + /// Requests a new [`Compositor`] with the given [`Settings`]. + /// + /// Returns `None` if no compatible graphics adapter could be found. + pub async fn request( + settings: Settings, + compatible_window: Option, + shell: Shell, + ) -> Result { + let instance = wgpu::util::new_instance_with_webgpu_detection(&wgpu::InstanceDescriptor { + backends: settings.backends, + flags: if cfg!(feature = "strict-assertions") { + wgpu::InstanceFlags::debugging() + } else { + wgpu::InstanceFlags::empty() + }, + ..Default::default() + }) + .await; + + log::info!("{settings:#?}"); + + #[cfg(not(target_arch = "wasm32"))] + if log::max_level() >= log::LevelFilter::Info { + let available_adapters: Vec<_> = instance + .enumerate_adapters(settings.backends) + .iter() + .map(wgpu::Adapter::get_info) + .collect(); + log::info!("Available adapters: {available_adapters:#?}"); + } + + #[allow(unsafe_code)] + let compatible_surface = + compatible_window.and_then(|window| instance.create_surface(window).ok()); + + let adapter_options = wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::from_env() + .unwrap_or(wgpu::PowerPreference::HighPerformance), + compatible_surface: compatible_surface.as_ref(), + force_fallback_adapter: false, + }; + + let adapter = instance + .request_adapter(&adapter_options) + .await + .map_err(|_error| Error::NoAdapterFound(format!("{adapter_options:?}")))?; + + log::info!("Selected: {:#?}", adapter.get_info()); + + let (format, alpha_mode) = compatible_surface + .as_ref() + .and_then(|surface| { + let capabilities = surface.get_capabilities(&adapter); + + let formats = capabilities.formats.iter().copied(); + + log::info!("Available formats: {formats:#?}"); + + let mut formats = + formats.filter(|format| format.required_features() == wgpu::Features::empty()); + + let format = if color::GAMMA_CORRECTION { + formats.find(wgpu::TextureFormat::is_srgb) + } else { + formats.find(|format| !wgpu::TextureFormat::is_srgb(format)) + }; + + let format = format.or_else(|| { + log::warn!("No format found!"); + + capabilities.formats.first().copied() + }); + + let alpha_modes = capabilities.alpha_modes; + + log::info!("Available alpha modes: {alpha_modes:#?}"); + + let preferred_alpha = + if alpha_modes.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:?}"); + + #[cfg(target_arch = "wasm32")] + let limits = if adapter.get_info().backend == wgpu::Backend::BrowserWebGpu { + vec![ + wgpu::Limits::default().using_resolution(adapter.limits()), + wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()), + ] + } else { + vec![wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits())] + }; + + #[cfg(not(target_arch = "wasm32"))] + let limits = vec![wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()]; + + let limits = limits.into_iter().map(|limits| wgpu::Limits { + max_bind_groups: 2, + max_non_sampler_bindings: 2048, + ..limits + }); + + 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.clone(), + memory_hints: wgpu::MemoryHints::MemoryUsage, + trace: wgpu::Trace::Off, + experimental_features: wgpu::ExperimentalFeatures::disabled(), + }) + .await; + + match result { + Ok((device, queue)) => { + let engine = Engine::new( + &adapter, + device, + queue, + format, + settings.antialiasing, + shell, + ); + + return Ok(Compositor { + instance, + adapter, + format, + alpha_mode, + engine, + settings, + }); + } + Err(error) => { + errors.push((required_limits, error)); + } + } + } + + Err(Error::RequestDeviceFailed(errors)) + } +} + +/// Creates a [`Compositor`] with the given [`Settings`] and window. +pub async fn new( + settings: Settings, + compatible_window: W, + shell: Shell, +) -> Result { + Compositor::request(settings, Some(compatible_window), shell).await +} + +/// Presents the given primitives with the given [`Compositor`]. +pub fn present( + renderer: &mut Renderer, + surface: &mut wgpu::Surface<'static>, + viewport: &Viewport, + background_color: Color, + on_pre_present: impl FnOnce(), +) -> Result<(), compositor::SurfaceError> { + match surface.get_current_texture() { + Ok(frame) => { + let view = &frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let _submission = renderer.present( + Some(background_color), + frame.texture.format(), + view, + viewport, + ); + + // Present the frame + on_pre_present(); + frame.present(); + + Ok(()) + } + Err(error) => match error { + wgpu::SurfaceError::Timeout => Err(compositor::SurfaceError::Timeout), + wgpu::SurfaceError::Outdated => Err(compositor::SurfaceError::Outdated), + wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), + wgpu::SurfaceError::OutOfMemory => Err(compositor::SurfaceError::OutOfMemory), + wgpu::SurfaceError::Other => Err(compositor::SurfaceError::Other), + }, + } +} + +impl graphics::Compositor for Compositor { + type Renderer = Renderer; + type Surface = wgpu::Surface<'static>; + + async fn with_backend( + settings: graphics::Settings, + _display: impl compositor::Display, + compatible_window: impl compositor::Window, + shell: Shell, + backend: Option<&str>, + ) -> Result { + match backend { + None | Some("wgpu") => { + let mut settings = Settings::from(settings); + + if let Some(backends) = wgpu::Backends::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, shell).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.engine.clone(), + self.settings.default_font, + self.settings.default_text_size, + ) + } + + fn create_surface( + &mut self, + window: W, + width: u32, + height: u32, + ) -> Self::Surface { + let mut surface = self + .instance + .create_surface(window) + .expect("Create surface"); + + if width > 0 && height > 0 { + self.configure_surface(&mut surface, width, height); + } + + surface + } + + fn configure_surface(&mut self, surface: &mut Self::Surface, width: u32, height: u32) { + surface.configure( + &self.engine.device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: self.format, + present_mode: self.settings.present_mode, + width, + height, + alpha_mode: self.alpha_mode, + view_formats: vec![], + desired_maximum_frame_latency: 1, + }, + ); + } + + fn information(&self) -> compositor::Information { + let information = self.adapter.get_info(); + + compositor::Information { + adapter: information.name, + backend: format!("{:?}", information.backend), + } + } + + fn present( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + on_pre_present: impl FnOnce(), + ) -> Result<(), compositor::SurfaceError> { + present( + renderer, + surface, + viewport, + background_color, + on_pre_present, + ) + } + + fn screenshot( + &mut self, + renderer: &mut Self::Renderer, + viewport: &Viewport, + background_color: Color, + ) -> Vec { + renderer.screenshot(viewport, background_color) + } +} diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml new file mode 100644 index 000000000..f18c2c378 --- /dev/null +++ b/crates/ocs_web_worker/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ocs_web_worker" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +acadrust = { version = "0.4", features = ["serde"] } +bincode = "1.3" +getrandom = { version = "0.3", features = ["wasm_js"] } +js-sys = "0.3" +wasm-bindgen = "0.2" diff --git a/crates/ocs_web_worker/src/lib.rs b/crates/ocs_web_worker/src/lib.rs new file mode 100644 index 000000000..2d8b7465a --- /dev/null +++ b/crates/ocs_web_worker/src/lib.rs @@ -0,0 +1,32 @@ +use std::io::Cursor; + +use acadrust::io::dwg::DwgReader; +use acadrust::DxfReader; +use js_sys::Uint8Array; +use wasm_bindgen::prelude::*; + +/// Parse DWG/DXF on a dedicated browser worker and return a compact serialized +/// document. The main wasm instance only deserializes and installs it, so the +/// expensive bit/handle/object decode never occupies the browser UI thread. +#[wasm_bindgen] +pub fn parse_document(name: String, bytes: Uint8Array) -> Result { + let bytes = bytes.to_vec(); + let ext = name.rsplit('.').next().unwrap_or_default().to_lowercase(); + let document = match ext.as_str() { + "dwg" => DwgReader::from_stream(Cursor::new(bytes)) + .read() + .map_err(|error| JsValue::from_str(&error.to_string()))?, + "dxf" => DxfReader::from_reader(Cursor::new(bytes)) + .map_err(|error| JsValue::from_str(&error.to_string()))? + .read() + .map_err(|error| JsValue::from_str(&error.to_string()))?, + _ => { + return Err(JsValue::from_str(&format!( + "Unsupported file format: .{ext}" + ))) + } + }; + let encoded = + bincode::serialize(&document).map_err(|error| JsValue::from_str(&error.to_string()))?; + Ok(Uint8Array::from(encoded.as_slice())) +} diff --git a/index.html b/index.html index 2908689bd..59627b633 100644 --- a/index.html +++ b/index.html @@ -45,6 +45,8 @@ + +

diff --git a/scripts/build-web-worker.sh b/scripts/build-web-worker.sh new file mode 100644 index 000000000..38da25ec4 --- /dev/null +++ b/scripts/build-web-worker.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu + +cargo build --release --target wasm32-unknown-unknown --package ocs_web_worker +mkdir -p web/worker_pkg +wasm-bindgen \ + --target web \ + --out-dir web/worker_pkg \ + --out-name ocs_web_worker \ + target/wasm32-unknown-unknown/release/ocs_web_worker.wasm diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 72e1f7ad2..fe39ed51f 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1052,9 +1052,10 @@ impl OpenCADStudio { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); - self.push_undo_snapshot(i, "GROUP"); + let undo = self.begin_group_undo(i, "GROUP"); self.tabs[i].scene.create_group(name.clone(), handles); self.tabs[i].dirty = true; + self.commit_group_undo(i, undo); self.command_line .push_info(&format!("Group \"{}\" created.", name)); } @@ -1062,9 +1063,10 @@ impl OpenCADStudio { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); - self.push_undo_snapshot(i, "UNGROUP"); + let undo = self.begin_group_undo(i, "UNGROUP"); let count = self.tabs[i].scene.delete_groups_containing(&handles); self.tabs[i].dirty = true; + self.commit_group_undo(i, undo); if count > 0 { self.command_line .push_info(&format!("{} group(s) dissolved.", count)); diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index fc5af5419..858e6f653 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -817,10 +817,15 @@ impl OpenCADStudio { self.command_line .push_error(&format!("DIMSTYLE: '{}' already exists.", name)); } else { + let undo = self.begin_dim_style_undo( + i, + "DIMSTYLE NEW", + std::slice::from_ref(&name), + ); let style = DimStyle::new(&name); let _ = self.tabs[i].scene.document.dim_styles.add(style); - self.push_undo_snapshot(i, "DIMSTYLE NEW"); self.tabs[i].dirty = true; + self.commit_dim_style_undo(i, undo); self.command_line .push_output(&format!("DIMSTYLE: '{}' created.", name)); } @@ -832,6 +837,11 @@ impl OpenCADStudio { let prop = parts.get(2).map(|s| s.to_lowercase()).unwrap_or_default(); let val_str = parts.get(3).map(|s| s.trim()).unwrap_or(""); if let Ok(val) = val_str.parse::() { + let undo = self.begin_dim_style_undo( + i, + "DIMSTYLE SET", + std::slice::from_ref(&style_name), + ); if let Some(ds) = self.tabs[i].scene.document.dim_styles.get_mut(&style_name) { @@ -882,8 +892,10 @@ impl OpenCADStudio { return Some(Task::none()); } } - self.push_undo_snapshot(i, "DIMSTYLE SET"); self.tabs[i].dirty = true; + self.tabs[i].scene + .invalidate_dim_style_dependencies(&style_name); + self.commit_dim_style_undo(i, undo); self.command_line.push_output(&format!( "DIMSTYLE: '{style_name}'.{prop} = {val:.3}" )); @@ -1121,10 +1133,15 @@ impl OpenCADStudio { self.command_line .push_error(&format!("{prefix}: style '{name}' already exists.")); } else { + let undo = self.begin_text_style_undo( + i, + "STYLE NEW", + std::slice::from_ref(&name), + ); let style = acadrust::tables::TextStyle::new(&name); let _ = self.tabs[i].scene.document.text_styles.add(style); - self.push_undo_snapshot(i, "STYLE NEW"); self.tabs[i].dirty = true; + self.commit_text_style_undo(i, undo); self.command_line .push_output(&format!("{prefix}: style '{name}' created.")); } @@ -1136,18 +1153,28 @@ impl OpenCADStudio { if style_name.is_empty() || font.is_empty() { self.command_line .push_error(&format!("Usage: {prefix} FONT