perf: keep heavy work off UI thread

This commit is contained in:
Hakan Seven 2026-07-25 18:49:18 +03:00
commit deec48ad99
75 changed files with 11703 additions and 519 deletions

146
crates/iced_wgpu/Cargo.toml Normal file
View file

@ -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 <hector@hecrj.dev>"]
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"

19
crates/iced_wgpu/LICENSE Normal file
View file

@ -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.

View file

@ -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.

View file

@ -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.
<p align="center">
<img alt="The native target" src="../docs/graphs/native.png" width="80%">
</p>
[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

View file

@ -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<T> {
label: &'static str,
size: u64,
usage: wgpu::BufferUsages,
pub(crate) raw: wgpu::Buffer,
type_: PhantomData<T>,
}
impl<T: bytemuck::Pod> Buffer<T> {
pub fn new(
device: &wgpu::Device,
label: &'static str,
amount: usize,
usage: wgpu::BufferUsages,
) -> Self {
let size = next_copy_size::<T>(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::<T>(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::BufferAddress>,
) -> 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::<T>() as u64
..end as u64 * std::mem::size_of::<T>() as u64,
)
}
}
fn next_copy_size<T>(amount: usize) -> u64 {
let align_mask = wgpu::COPY_BUFFER_ALIGNMENT - 1;
(((std::mem::size_of::<T>() * amount).next_power_of_two() as u64
+ align_mask)
& !align_mask)
.max(wgpu::COPY_BUFFER_ALIGNMENT)
}

View file

@ -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
}

View file

@ -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<RwLock<primitive::Storage>>,
_shell: Shell,
}
impl Engine {
pub fn new(
_adapter: &wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>, // TODO: Initialize AA pipelines lazily
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();
}
}

View file

@ -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<Mesh>,
images: Vec<Image>,
text: Vec<Text>,
},
Cached(Cache),
}
#[derive(Debug, Clone, Default)]
pub struct Cache {
pub meshes: Option<mesh::Cache>,
pub images: Option<Arc<[Image]>>,
pub text: Option<text::Cache>,
}
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>,
) -> 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<Mesh>,
images: Vec<Image>,
text: Vec<Text>,
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<Fill>) {
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<Fill>,
) {
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<Stroke<'a>>) {
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<Stroke<'a>>,
) {
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<geometry::Text>,
stroke: impl Into<Stroke<'a>>,
) {
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<geometry::Text>) {
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<Radians>) {
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<f32>) {
let scale = scale.into();
self.scale_nonuniform(Vector { x: scale, y: scale });
}
#[inline]
fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
let scale = scale.into();
self.transforms.current.0 =
self.transforms.current.0.pre_scale(scale.x, scale.y);
}
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<core::Image>) {
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<Svg>) {
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<mesh::SolidVertex2D, u32>),
Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>),
}
struct BufferStack {
stack: Vec<Buffer>,
}
impl BufferStack {
fn new() -> Self {
Self { stack: Vec::new() }
}
fn get_mut(&mut self, style: &Style) -> &mut Buffer {
match style {
Style::Solid(_) => match self.stack.last() {
Some(Buffer::Solid(_)) => {}
_ => {
self.stack.push(Buffer::Solid(
tessellation::VertexBuffers::new(),
));
}
},
Style::Gradient(_) => match self.stack.last() {
Some(Buffer::Gradient(_)) => {}
_ => {
self.stack.push(Buffer::Gradient(
tessellation::VertexBuffers::new(),
));
}
},
}
self.stack.last_mut().unwrap()
}
fn get_fill<'a>(
&'a mut self,
style: &Style,
) -> Box<dyn tessellation::FillGeometryBuilder + 'a> {
match (style, self.get_mut(style)) {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
TriangleVertex2DBuilder(color::pack(*color)),
))
}
(Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
GradientVertex2DBuilder {
gradient: gradient.pack(),
},
))
}
_ => unreachable!(),
}
}
fn get_stroke<'a>(
&'a mut self,
style: &Style,
) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> {
match (style, self.get_mut(style)) {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
TriangleVertex2DBuilder(color::pack(*color)),
))
}
(Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
GradientVertex2DBuilder {
gradient: gradient.pack(),
},
))
}
_ => unreachable!(),
}
}
fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator<Item = Mesh> {
self.stack
.into_iter()
.filter_map(move |buffer| match buffer {
Buffer::Solid(buffer) if !buffer.indices.is_empty() => {
Some(Mesh::Solid {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
clip_bounds,
transformation: Transformation::IDENTITY,
})
}
Buffer::Gradient(buffer) if !buffer.indices.is_empty() => {
Some(Mesh::Gradient {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
clip_bounds,
transformation: Transformation::IDENTITY,
})
}
_ => None,
})
}
}
#[derive(Debug)]
struct Transforms {
previous: Vec<Transform>,
current: Transform,
}
#[derive(Debug, Clone, Copy)]
struct Transform(lyon::math::Transform);
impl Transform {
fn is_identity(&self) -> bool {
self.0 == lyon::math::Transform::identity()
}
fn is_scale_translation(&self) -> bool {
self.0.m12.abs() < 2.0 * f32::EPSILON
&& self.0.m21.abs() < 2.0 * f32::EPSILON
}
fn scale(&self) -> (f32, f32) {
(self.0.m11, self.0.m22)
}
fn transform_point(&self, point: Point) -> Point {
let transformed = self
.0
.transform_point(euclid::Point2D::new(point.x, point.y));
Point {
x: transformed.x,
y: transformed.y,
}
}
fn transform_style(&self, style: Style) -> Style {
match style {
Style::Solid(color) => Style::Solid(color),
Style::Gradient(gradient) => {
Style::Gradient(self.transform_gradient(gradient))
}
}
}
fn transform_gradient(&self, mut gradient: Gradient) -> Gradient {
match &mut gradient {
Gradient::Linear(linear) => {
linear.start = self.transform_point(linear.start);
linear.end = self.transform_point(linear.end);
}
}
gradient
}
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<mesh::GradientVertex2D>
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<mesh::GradientVertex2D>
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<mesh::SolidVertex2D>
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<mesh::SolidVertex2D>
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),
},
);
})
}

View file

@ -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<wgpu::BindGroup>,
texture_layout: wgpu::BindGroupLayout,
layers: Vec<Layer>,
}
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<wgpu::BindGroup> {
&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<Entry> {
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::<usize>(),
);
}
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<Entry> {
// 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,
),
}],
}));
}
}

View file

@ -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<u32> {
match self {
Allocation::Partial { region, .. } => region.size(),
Allocation::Full { size, .. } => Size::new(*size, *size),
}
}
pub fn padding(&self) -> Size<u32> {
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,
}
}
}

View file

@ -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<Region> {
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<u32>,
}
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<u32> {
let size = self.allocation.rectangle.size();
core::Size::new(size.width as u32, size.height as u32)
}
pub fn padding(&self) -> crate::core::Size<u32> {
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()
}
}

View file

@ -0,0 +1,27 @@
use crate::core::Size;
use crate::image::atlas;
#[derive(Debug)]
pub enum Entry {
Contiguous(atlas::Allocation),
Fragmented {
size: Size<u32>,
fragments: Vec<Fragment>,
},
}
impl Entry {
#[cfg(feature = "image")]
pub fn size(&self) -> Size<u32> {
match self {
Entry::Contiguous(allocation) => allocation.size(),
Entry::Fragmented { size, .. } => *size,
}
}
}
#[derive(Debug)]
pub struct Fragment {
pub position: (u32, u32),
pub allocation: atlas::Allocation,
}

View file

@ -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,
}
}
}

View file

@ -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<core::image::Allocation, core::image::Error>)
+ 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<core::image::Allocation, core::image::Error> {
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<Size<u32>> {
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<u32> {
// 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<wgpu::BindGroup>)> {
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<core::Color>,
size: Size,
scale: f32,
) -> Option<(&atlas::Entry, &Arc<wgpu::BindGroup>)> {
// 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<core::image::Id, Vec<Callback>>,
belt: wgpu::util::StagingBelt,
}
#[cfg(feature = "image")]
type Callback =
Box<dyn FnOnce(Result<core::image::Allocation, core::image::Error>) + Send>;
#[cfg(feature = "image")]
fn load_image<'a>(
cache: &'a mut crate::image::raster::Cache,
pending: &mut HashMap<core::image::Id, Vec<Callback>>,
#[cfg(not(target_arch = "wasm32"))] worker: &Worker,
handle: &core::image::Handle,
callback: Option<Callback>,
) -> 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<Job>,
quit: mpsc::SyncSender<()>,
work: mpsc::Receiver<Work>,
handle: Option<std::thread::JoinHandle<()>>,
}
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<wgpu::BindGroup>) {
let _ = self.jobs.send(Job::Drop(bind_group));
}
pub fn try_recv(&self) -> Result<Work, mpsc::TryRecvError> {
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<Job>,
output: mpsc::SyncSender<Work>,
quit: mpsc::Receiver<()>,
}
#[derive(Debug)]
enum Job {
Load(image::Handle),
Upload {
handle: image::Handle,
rgba: Bytes,
width: u32,
height: u32,
},
Drop(Arc<wgpu::BindGroup>),
Quit,
}
pub enum Work {
Upload {
handle: image::Handle,
entry: atlas::Entry,
bind_group: Arc<wgpu::BindGroup>,
},
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,
});
}
}
}

View file

@ -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<Image>;
#[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::<Uniforms>() 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::<Instance>() 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<Layer>,
prepare_layer: usize,
nearest_instances: Vec<Instance>,
linear_instances: Vec<Instance>,
}
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<Arc<wgpu::BindGroup>> = 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<u32>,
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<Instance>,
nearest: Vec<Group>,
nearest_layout: wgpu::BindGroup,
nearest_total: usize,
linear: Vec<Group>,
linear_layout: wgpu::BindGroup,
linear_total: usize,
}
#[derive(Debug)]
struct Group {
atlas: Arc<wgpu::BindGroup>,
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::<Uniforms>() 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<wgpu::BindGroup>,
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<f32> 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<Instance>,
) {
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<Instance>,
) {
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);
}

View file

@ -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) {}
}

View file

@ -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<Arc<wgpu::BindGroup>>,
allocation: Option<Weak<image::Memory>>,
},
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<u32> {
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<Image> {
match self {
Memory::Host(image) => Some(image.clone()),
Memory::Device { .. } | Memory::Error(_) => None,
}
}
}
#[derive(Debug, Default)]
pub struct Cache {
map: FxHashMap<image::Id, Memory>,
hits: FxHashSet<image::Id>,
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<wgpu::BindGroup>),
) {
// 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;
}
}

View file

@ -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<u32> {
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<u64, Svg>,
rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
svg_hits: FxHashSet<u64>,
rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>,
should_trim: bool,
fontdb: Option<Arc<usvg::fontdb::Database>>,
}
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<Color>,
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"),
}
}
}

View file

@ -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<Layer>;
#[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<Mesh>,
pending_text: Vec<Text>,
}
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<Mesh>,
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<Text>,
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(),
}
}
}

984
crates/iced_wgpu/src/lib.rs Normal file
View file

@ -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<image::Cache>,
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<Color>,
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<Color>,
_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<u8> {
#[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<u32>) -> 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::<f32>::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<Color>,
viewport: &Viewport,
) {
use std::mem::ManuallyDrop;
let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: frame,
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::<f32>::from(Rectangle::with_size(
viewport.physical_size(),
));
let scale = Transformation::scale(scale_factor);
for layer in self.layers.iter() {
let Some(physical_bounds) =
physical_bounds.intersection(&(layer.bounds * scale_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<Background>,
) {
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<core::image::Allocation, core::image::Error>)
+ 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<core::image::Allocation, core::image::Error> {
self.image_cache.borrow_mut().load_image(
&self.engine.device,
&self.engine.queue,
handle,
)
}
fn measure_image(&self, handle: &Self::Handle) -> Option<core::Size<u32>> {
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<u32> {
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<Self> {
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<u32>,
scale_factor: f32,
background_color: Color,
) -> Vec<u8> {
self.screenshot(
&Viewport::with_physical_size(size, scale_factor),
background_color,
)
}
}

View file

@ -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<Instance>;
/// 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<u32>,
) {
}
}
/// 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<u32>,
);
}
#[derive(Debug)]
struct BlackBox<P: Primitive> {
primitive: P,
}
impl<P: Primitive> Stored for BlackBox<P> {
fn prepare(
&self,
storage: &mut Storage,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
bounds: &Rectangle,
viewport: &Viewport,
) {
if !storage.has::<P>() {
storage.store::<P, _>(P::Pipeline::new(device, queue, format));
}
let renderer = storage
.get_mut::<P>()
.expect("renderer should be initialized")
.downcast_mut::<P::Pipeline>()
.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::<P>()
.expect("renderer should be initialized")
.downcast_ref::<P::Pipeline>()
.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<u32>,
) {
let renderer = storage
.get::<P>()
.expect("renderer should be initialized")
.downcast_ref::<P::Pipeline>()
.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<dyn Stored>,
}
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<TypeId, Box<dyn Pipeline>>,
}
impl Storage {
/// Returns `true` if `Storage` contains a type `T`.
pub fn has<T: 'static>(&self) -> bool {
self.pipelines.contains_key(&TypeId::of::<T>())
}
/// Inserts the data `T` in to [`Storage`].
pub fn store<T: 'static, P: Pipeline>(&mut self, pipeline: P) {
let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline));
}
/// Returns a reference to the data with type `T` if it exists in [`Storage`].
pub fn get<T: 'static>(&self) -> Option<&dyn Any> {
self.pipelines
.get(&TypeId::of::<T>())
.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<T: 'static>(&mut self) -> Option<&mut dyn Any> {
self.pipelines
.get_mut(&TypeId::of::<T>())
.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();
}
}
}

View file

@ -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<Layer>,
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<u32>,
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::<Uniforms>() 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::<Uniforms>() 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<Solid>,
/// The gradient quads of the [`Layer`].
gradients: Vec<Gradient>,
/// 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<wgpu::ColorTargetState>; 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<f32> 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],
}
}
}

View file

@ -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<Gradient>,
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::<Gradient>()
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<usize>,
) {
#[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);
}
}
}

View file

@ -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<Solid>,
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::<Solid>() 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<usize>,
) {
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);
}
}

View file

@ -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<Antialiasing>,
}
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<graphics::Settings> 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<wgpu::PresentMode> {
let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?;
match present_mode.to_lowercase().as_str() {
"vsync" => Some(wgpu::PresentMode::AutoVsync),
"no_vsync" => Some(wgpu::PresentMode::AutoNoVsync),
"immediate" => Some(wgpu::PresentMode::Immediate),
"fifo" => Some(wgpu::PresentMode::Fifo),
"fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed),
"mailbox" => Some(wgpu::PresentMode::Mailbox),
_ => None,
}
}

View file

@ -0,0 +1,37 @@
var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 1.0)
);
@group(0) @binding(0) var u_sampler: sampler;
@group(0) @binding(1) var<uniform> u_ratio: vec4<f32>;
@group(1) @binding(0) var u_texture: texture_2d<f32>;
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@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<f32>(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<f32> {
return textureSample(u_texture, u_sampler, input.uv);
}

View file

@ -0,0 +1,14 @@
fn premultiply(color: vec4<f32>) -> vec4<f32> {
return vec4(color.xyz * color.a, color.a);
}
fn unpack_color(data: vec2<u32>) -> vec4<f32> {
return premultiply(unpack_u32(data));
}
fn unpack_u32(data: vec2<u32>) -> vec4<f32> {
let rg: vec2<f32> = unpack2x16float(data.x);
let ba: vec2<f32> = unpack2x16float(data.y);
return vec4<f32>(rg.y, rg.x, ba.y, ba.x);
}

View file

@ -0,0 +1,3 @@
fn interpolate_color(from_: vec4<f32>, to_: vec4<f32>, factor: f32) -> vec4<f32> {
return mix(from_, to_, factor);
}

View file

@ -0,0 +1,129 @@
struct Globals {
transform: mat4x4<f32>,
scale_factor: f32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var u_sampler: sampler;
@group(1) @binding(0) var u_texture: texture_2d_array<f32>;
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) center: vec2<f32>,
@location(1) clip_bounds: vec4<f32>,
@location(2) border_radius: vec4<f32>,
@location(3) tile: vec4<f32>,
@location(4) rotation: f32,
@location(5) opacity: f32,
@location(6) atlas_pos: vec2<f32>,
@location(7) atlas_scale: vec2<f32>,
@location(8) layer: i32,
@location(9) snap: u32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) @interpolate(flat) clip_bounds: vec4<f32>,
@location(1) @interpolate(flat) border_radius: vec4<f32>,
@location(2) @interpolate(flat) atlas: vec4<f32>,
@location(3) @interpolate(flat) layer: i32,
@location(4) @interpolate(flat) opacity: f32,
@location(5) uv: vec2<f32>,
}
@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<vec2<f32>, 4>(
tile.xy, // Top left
tile.xy + vec2<f32>(tile.z, 0.0), // Top right
tile.xy + vec2<f32>(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<vec2<f32>, 4>();
for (var i = 0u; i < 4u; i++) {
let c = corners[i] - input.center;
rotated[i] = vec2<f32>(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<f32>(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<f32>(clip_min, max(vec2<f32>(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<f32>(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<f32>(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<f32> {
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<f32>(1.0, 1.0, 1.0, antialias * input.opacity * f32(inside));
}
fn rounded_box_sdf(p: vec2<f32>, size: vec2<f32>, corners: vec4<f32>) -> 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;
}

View file

@ -0,0 +1,13 @@
struct Globals {
transform: mat4x4<f32>,
scale: f32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
fn rounded_box_sdf(p: vec2<f32>, size: vec2<f32>, corners: vec4<f32>) -> 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;
}

View file

@ -0,0 +1,184 @@
struct GradientVertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) @interpolate(flat) colors_1: vec4<u32>,
@location(1) @interpolate(flat) colors_2: vec4<u32>,
@location(2) @interpolate(flat) colors_3: vec4<u32>,
@location(3) @interpolate(flat) colors_4: vec4<u32>,
@location(4) @interpolate(flat) offsets: vec4<u32>,
@location(5) direction: vec4<f32>,
@location(6) position_and_scale: vec4<f32>,
@location(7) border_color: vec4<f32>,
@location(8) border_radius: vec4<f32>,
@location(9) border_width: f32,
@location(10) snap: u32,
}
struct GradientVertexOutput {
@builtin(position) position: vec4<f32>,
@location(1) @interpolate(flat) colors_1: vec4<u32>,
@location(2) @interpolate(flat) colors_2: vec4<u32>,
@location(3) @interpolate(flat) colors_3: vec4<u32>,
@location(4) @interpolate(flat) colors_4: vec4<u32>,
@location(5) @interpolate(flat) offsets: vec4<u32>,
@location(6) direction: vec4<f32>,
@location(7) position_and_scale: vec4<f32>,
@location(8) border_color: vec4<f32>,
@location(9) border_radius: vec4<f32>,
@location(10) border_width: f32,
}
@vertex
fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput {
var out: GradientVertexOutput;
var pos: vec2<f32> = input.position_and_scale.xy * globals.scale;
var scale: vec2<f32> = input.position_and_scale.zw * globals.scale;
var pos_snap = vec2<f32>(0.0, 0.0);
var scale_snap = vec2<f32>(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<f32> = vec4<f32>(
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<f32> = mat4x4<f32>(
vec4<f32>(scale.x + scale_snap.x + 1.0, 0.0, 0.0, 0.0),
vec4<f32>(0.0, scale.y + scale_snap.y + 1.0, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
vec4<f32>(pos + pos_snap - vec2<f32>(0.5, 0.5), 0.0, 1.0)
);
out.position = globals.transform * transform * vec4<f32>(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<f32>(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>) -> 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<f32>,
direction: vec4<f32>,
colors: array<vec4<f32>, 8>,
offsets: array<f32, 8>,
last_index: i32
) -> vec4<f32> {
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<f32>;
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<f32> {
let colors = array<vec4<f32>, 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<f32> = unpack_u32(input.offsets.xy);
let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw);
var offsets = array<f32, 8>(
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<f32> = 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);
}

View file

@ -0,0 +1,102 @@
struct SolidVertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) color: vec4<f32>,
@location(1) pos: vec2<f32>,
@location(2) scale: vec2<f32>,
@location(3) border_color: vec4<f32>,
@location(4) border_radius: vec4<f32>,
@location(5) border_width: f32,
@location(6) shadow_color: vec4<f32>,
@location(7) shadow_offset: vec2<f32>,
@location(8) shadow_blur_radius: f32,
@location(9) snap: u32,
}
struct SolidVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) border_color: vec4<f32>,
@location(2) pos: vec2<f32>,
@location(3) scale: vec2<f32>,
@location(4) border_radius: vec4<f32>,
@location(5) border_width: f32,
@location(6) shadow_color: vec4<f32>,
@location(7) shadow_offset: vec2<f32>,
@location(8) shadow_blur_radius: f32,
}
@vertex
fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
var out: SolidVertexOutput;
var pos: vec2<f32> = (input.pos + min(input.shadow_offset, vec2<f32>(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale;
var scale: vec2<f32> = (input.scale + vec2<f32>(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale;
var pos_snap = vec2<f32>(0.0, 0.0);
var scale_snap = vec2<f32>(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<f32> = mat4x4<f32>(
vec4<f32>(scale.x + scale_snap.x + 1.0, 0.0, 0.0, 0.0),
vec4<f32>(0.0, scale.y + scale_snap.y + 1.0, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
vec4<f32>(pos + pos_snap - vec2<f32>(0.5, 0.5), 0.0, 1.0)
);
out.position = globals.transform * transform * vec4<f32>(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<f32> {
var mixed_color: vec4<f32> = 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;
}
}

View file

@ -0,0 +1,5 @@
struct Globals {
transform: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;

View file

@ -0,0 +1,127 @@
struct GradientVertexInput {
@location(0) v_pos: vec2<f32>,
@location(1) @interpolate(flat) colors_1: vec4<u32>,
@location(2) @interpolate(flat) colors_2: vec4<u32>,
@location(3) @interpolate(flat) colors_3: vec4<u32>,
@location(4) @interpolate(flat) colors_4: vec4<u32>,
@location(5) @interpolate(flat) offsets: vec4<u32>,
@location(6) direction: vec4<f32>,
}
struct GradientVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) raw_position: vec2<f32>,
@location(1) @interpolate(flat) colors_1: vec4<u32>,
@location(2) @interpolate(flat) colors_2: vec4<u32>,
@location(3) @interpolate(flat) colors_3: vec4<u32>,
@location(4) @interpolate(flat) colors_4: vec4<u32>,
@location(5) @interpolate(flat) offsets: vec4<u32>,
@location(6) direction: vec4<f32>,
}
@vertex
fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput {
var output: GradientVertexOutput;
output.position = globals.transform * vec4<f32>(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<f32>,
direction: vec4<f32>,
colors: array<vec4<f32>, 8>,
offsets: array<f32, 8>,
last_index: i32
) -> vec4<f32> {
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<f32>;
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<f32> {
let colors = array<vec4<f32>, 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<f32> = unpack_u32(input.offsets.xy);
let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw);
var offsets = array<f32, 8>(
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>) -> f32 {
return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453);
}

View file

@ -0,0 +1,24 @@
struct SolidVertexInput {
@location(0) position: vec2<f32>,
@location(1) color: vec4<f32>,
}
struct SolidVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
}
@vertex
fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
var out: SolidVertexOutput;
out.color = premultiply(input.color);
out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0);
return out;
}
@fragment
fn solid_fs_main(input: SolidVertexOutput) -> @location(0) vec4<f32> {
return input.color;
}

View file

@ -0,0 +1,7 @@
// Compute the normalized quad coordinates based on the vertex index.
fn vertex_position(vertex_index: u32) -> vec2<f32> {
// #: 0 1 2 3 4 5
// x: 1 1 0 0 0 1
// y: 1 0 0 0 1 1
return vec2<f32>((vec2(1u, 2u) + vertex_index) % vec2(6u) < vec2(3u));
}

View file

@ -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<Item>;
#[derive(Debug)]
pub enum Item {
Group {
transformation: Transformation,
text: Vec<Text>,
},
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<Text>) -> Option<Self> {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
if text.is_empty() {
return None;
}
Some(Self {
id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
group,
text: Arc::from(text),
version: 0,
})
}
pub fn update(&mut self, text: Vec<Text>) {
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<cache::Group, Group>,
uploads: FxHashMap<Id, Upload>,
}
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<u32>) {
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<RwLock<cryoglyph::TextAtlas>>,
}
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<cryoglyph::TextRenderer>,
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<u32>,
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<cryoglyph::Buffer>),
}
let allocations: Vec<_> = sections
.iter()
.map(|section| match section {
Text::Paragraph { paragraph, .. } => {
paragraph.upgrade().map(Allocation::Paragraph)
}
Text::Editor { editor, .. } => {
editor.upgrade().map(Allocation::Editor)
}
Text::Cached {
content,
bounds,
size,
line_height,
font,
shaping,
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(),
)
}

View file

@ -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<Item>;
#[derive(Debug)]
pub enum Item {
Group {
transformation: Transformation,
meshes: Vec<Mesh>,
},
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<mesh::Id, Upload>,
}
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<msaa::Pipeline>,
solid: solid::Pipeline,
gradient: gradient::Pipeline,
}
pub struct State {
msaa: Option<msaa::State>,
layers: Vec<Layer>,
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<u32>,
) {
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<Antialiasing>,
) -> 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<Item = (&'a Layer, &'a [Mesh], Transformation)>,
) {
{
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<u32>,
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::<Uniforms>())
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::<Uniforms>())
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<Antialiasing>,
) -> 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::<Self>() as u64,
),
},
count: None,
}
}
pub fn min_size() -> Option<wgpu::BufferSize> {
wgpu::BufferSize::new(std::mem::size_of::<Self>() 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<mesh::SolidVertex2D>,
pub uniforms: Buffer<triangle::Uniforms>,
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<Antialiasing>,
) -> 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<mesh::GradientVertex2D>,
pub uniforms: Buffer<triangle::Uniforms>,
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<Antialiasing>,
) -> 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,
}
}
}
}

View file

@ -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<RwLock<Option<Targets>>>,
}
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<u32>,
) -> 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<u32>,
}
impl Targets {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
texture_layout: &wgpu::BindGroupLayout,
sample_count: u32,
size: Size<u32>,
) -> 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<Ratio>,
}
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::<Ratio>() 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<u32>,
) -> 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::<Ratio>() 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);
}
}

View file

@ -0,0 +1,5 @@
//! Display rendering results on windows.
pub mod compositor;
pub use compositor::Compositor;
pub use wgpu::Surface;

View file

@ -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<Error> 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<W: compositor::Window>(
settings: Settings,
compatible_window: Option<W>,
shell: Shell,
) -> Result<Self, Error> {
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<W: compositor::Window>(
settings: Settings,
compatible_window: W,
shell: Shell,
) -> Result<Compositor, Error> {
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<Self, graphics::Error> {
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<W: compositor::Window>(
&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<u8> {
renderer.screenshot(viewport, background_color)
}
}

View file

@ -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"

View file

@ -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<Uint8Array, JsValue> {
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()))
}