Merge branch 'master' into beacon

This commit is contained in:
Héctor Ramón Jiménez 2024-05-09 12:32:25 +02:00
commit aaf396256e
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
284 changed files with 18747 additions and 15450 deletions

View file

@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
@ -32,6 +35,8 @@ glyphon.workspace = true
guillotiere.workspace = true
log.workspace = true
once_cell.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true
wgpu.workspace = true
lyon.workspace = true
@ -39,6 +44,3 @@ lyon.optional = true
resvg.workspace = true
resvg.optional = true
tracing.workspace = true
tracing.optional = true

View file

@ -1,394 +0,0 @@
use crate::core::{Color, Size, Transformation};
use crate::graphics::backend;
use crate::graphics::color;
use crate::graphics::Viewport;
use crate::primitive::pipeline;
use crate::primitive::{self, Primitive};
use crate::quad;
use crate::text;
use crate::triangle;
use crate::{Layer, Settings};
#[cfg(feature = "tracing")]
use tracing::info_span;
#[cfg(any(feature = "image", feature = "svg"))]
use crate::image;
use std::borrow::Cow;
/// A [`wgpu`] graphics backend for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
#[allow(missing_debug_implementations)]
pub struct Backend {
quad_pipeline: quad::Pipeline,
text_pipeline: text::Pipeline,
triangle_pipeline: triangle::Pipeline,
pipeline_storage: pipeline::Storage,
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline,
}
impl Backend {
/// Creates a new [`Backend`].
pub fn new(
_adapter: &wgpu::Adapter,
device: &wgpu::Device,
queue: &wgpu::Queue,
settings: Settings,
format: wgpu::TextureFormat,
) -> Self {
let text_pipeline = text::Pipeline::new(device, queue, format);
let quad_pipeline = quad::Pipeline::new(device, format);
let triangle_pipeline =
triangle::Pipeline::new(device, format, settings.antialiasing);
#[cfg(any(feature = "image", feature = "svg"))]
let image_pipeline = {
let backend = _adapter.get_info().backend;
image::Pipeline::new(device, format, backend)
};
Self {
quad_pipeline,
text_pipeline,
triangle_pipeline,
pipeline_storage: pipeline::Storage::default(),
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
}
}
/// Draws the provided primitives in the given `TextureView`.
///
/// The text provided as overlay will be rendered on top of the primitives.
/// This is useful for rendering debug information.
pub fn present(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
clear_color: Option<Color>,
format: wgpu::TextureFormat,
frame: &wgpu::TextureView,
primitives: &[Primitive],
viewport: &Viewport,
) {
log::debug!("Drawing");
#[cfg(feature = "tracing")]
let _ = info_span!("Wgpu::Backend", "PRESENT").entered();
let target_size = viewport.physical_size();
let scale_factor = viewport.scale_factor() as f32;
let transformation = viewport.projection();
let layers = Layer::generate(primitives, viewport);
self.prepare(
device,
queue,
format,
encoder,
scale_factor,
target_size,
transformation,
&layers,
);
self.render(
device,
encoder,
frame,
clear_color,
scale_factor,
target_size,
&layers,
);
self.quad_pipeline.end_frame();
self.text_pipeline.end_frame();
self.triangle_pipeline.end_frame();
#[cfg(any(feature = "image", feature = "svg"))]
self.image_pipeline.end_frame();
}
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
_encoder: &mut wgpu::CommandEncoder,
scale_factor: f32,
target_size: Size<u32>,
transformation: Transformation,
layers: &[Layer<'_>],
) {
for layer in layers {
let bounds = (layer.bounds * scale_factor).snap();
if bounds.width < 1 || bounds.height < 1 {
continue;
}
if !layer.quads.is_empty() {
self.quad_pipeline.prepare(
device,
queue,
&layer.quads,
transformation,
scale_factor,
);
}
if !layer.meshes.is_empty() {
let scaled =
transformation * Transformation::scale(scale_factor);
self.triangle_pipeline.prepare(
device,
queue,
&layer.meshes,
scaled,
);
}
#[cfg(any(feature = "image", feature = "svg"))]
{
if !layer.images.is_empty() {
let scaled =
transformation * Transformation::scale(scale_factor);
self.image_pipeline.prepare(
device,
queue,
_encoder,
&layer.images,
scaled,
scale_factor,
);
}
}
if !layer.text.is_empty() {
self.text_pipeline.prepare(
device,
queue,
&layer.text,
layer.bounds,
scale_factor,
target_size,
);
}
if !layer.pipelines.is_empty() {
for pipeline in &layer.pipelines {
pipeline.primitive.prepare(
format,
device,
queue,
pipeline.bounds,
target_size,
scale_factor,
&mut self.pipeline_storage,
);
}
}
}
}
fn render(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clear_color: Option<Color>,
scale_factor: f32,
target_size: Size<u32>,
layers: &[Layer<'_>],
) {
use std::mem::ManuallyDrop;
let mut quad_layer = 0;
let mut triangle_layer = 0;
#[cfg(any(feature = "image", feature = "svg"))]
let mut image_layer = 0;
let mut text_layer = 0;
let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: match clear_color {
Some(background_color) => wgpu::LoadOp::Clear({
let [r, g, b, a] =
color::pack(background_color).components();
wgpu::Color {
r: f64::from(r),
g: f64::from(g),
b: f64::from(b),
a: f64::from(a),
}
}),
None => wgpu::LoadOp::Load,
},
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
for layer in layers {
let bounds = (layer.bounds * scale_factor).snap();
if bounds.width < 1 || bounds.height < 1 {
continue;
}
if !layer.quads.is_empty() {
self.quad_pipeline.render(
quad_layer,
bounds,
&layer.quads,
&mut render_pass,
);
quad_layer += 1;
}
if !layer.meshes.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
self.triangle_pipeline.render(
device,
encoder,
target,
triangle_layer,
target_size,
&layer.meshes,
scale_factor,
);
triangle_layer += 1;
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
#[cfg(any(feature = "image", feature = "svg"))]
{
if !layer.images.is_empty() {
self.image_pipeline.render(
image_layer,
bounds,
&mut render_pass,
);
image_layer += 1;
}
}
if !layer.text.is_empty() {
self.text_pipeline
.render(text_layer, bounds, &mut render_pass);
text_layer += 1;
}
if !layer.pipelines.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
for pipeline in &layer.pipelines {
let viewport = (pipeline.viewport * scale_factor).snap();
if viewport.width < 1 || viewport.height < 1 {
continue;
}
pipeline.primitive.render(
&self.pipeline_storage,
target,
target_size,
viewport,
encoder,
);
}
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
}
let _ = ManuallyDrop::into_inner(render_pass);
}
}
impl crate::graphics::Backend for Backend {
type Primitive = primitive::Custom;
}
impl backend::Text for Backend {
fn load_font(&mut self, font: Cow<'static, [u8]>) {
self.text_pipeline.load_font(font);
}
}
#[cfg(feature = "image")]
impl backend::Image for Backend {
fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
self.image_pipeline.dimensions(handle)
}
}
#[cfg(feature = "svg")]
impl backend::Svg for Backend {
fn viewport_dimensions(
&self,
handle: &crate::core::svg::Handle,
) -> Size<u32> {
self.image_pipeline.viewport_dimensions(handle)
}
}

View file

@ -1,6 +1,13 @@
use std::marker::PhantomData;
use std::num::NonZeroU64;
use std::ops::RangeBounds;
pub const MAX_WRITE_SIZE: usize = 100 * 1024;
#[allow(unsafe_code)]
const MAX_WRITE_SIZE_U64: NonZeroU64 =
unsafe { NonZeroU64::new_unchecked(MAX_WRITE_SIZE as u64) };
#[derive(Debug)]
pub struct Buffer<T> {
label: &'static str,
@ -61,12 +68,46 @@ impl<T: bytemuck::Pod> Buffer<T> {
/// Returns the size of the written bytes.
pub fn write(
&mut self,
queue: &wgpu::Queue,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
offset: usize,
contents: &[T],
) -> usize {
let bytes: &[u8] = bytemuck::cast_slice(contents);
queue.write_buffer(&self.raw, offset as u64, bytes);
let mut bytes_written = 0;
// Split write into multiple chunks if necessary
while bytes_written + MAX_WRITE_SIZE < bytes.len() {
belt.write_buffer(
encoder,
&self.raw,
(offset + bytes_written) as u64,
MAX_WRITE_SIZE_U64,
device,
)
.copy_from_slice(
&bytes[bytes_written..bytes_written + MAX_WRITE_SIZE],
);
bytes_written += MAX_WRITE_SIZE;
}
// There will always be some bytes left, since the previous
// loop guarantees `bytes_written < bytes.len()`
let bytes_left = ((bytes.len() - bytes_written) as u64)
.try_into()
.expect("non-empty write");
// Write them
belt.write_buffer(
encoder,
&self.raw,
(offset + bytes_written) as u64,
bytes_left,
device,
)
.copy_from_slice(&bytes[bytes_written..]);
self.offsets.push(offset as u64);

View file

@ -1,5 +1,7 @@
use std::borrow::Cow;
use wgpu::util::DeviceExt;
pub fn convert(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
@ -15,28 +17,58 @@ pub fn convert(
..wgpu::SamplerDescriptor::default()
});
//sampler in 0
let sampler_layout =
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
#[repr(C)]
struct Ratio {
u: f32,
v: f32,
}
let ratio = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("iced-wgpu::triangle::msaa ratio"),
contents: bytemuck::bytes_of(&Ratio { u: 1.0, v: 1.0 }),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
});
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.sampler_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
}],
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let sampler_bind_group =
let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.offscreen.sampler.bind_group"),
layout: &sampler_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
}],
layout: &constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: ratio.as_entire_binding(),
},
],
});
let texture_layout =
@ -59,7 +91,7 @@ pub fn convert(
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
bind_group_layouts: &[&sampler_layout, &texture_layout],
bind_group_layouts: &[&constant_layout, &texture_layout],
push_constant_ranges: &[],
});
@ -152,16 +184,9 @@ pub fn convert(
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &sampler_bind_group, &[]);
pass.set_bind_group(0, &constant_bind_group, &[]);
pass.set_bind_group(1, &texture_bind_group, &[]);
pass.draw(0..6, 0..1);
texture
}
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Vertex {
ndc: [f32; 2],
uv: [f32; 2],
}

87
wgpu/src/engine.rs Normal file
View file

@ -0,0 +1,87 @@
use crate::buffer;
use crate::graphics::Antialiasing;
use crate::primitive;
use crate::quad;
use crate::text;
use crate::triangle;
#[allow(missing_debug_implementations)]
pub struct Engine {
pub(crate) staging_belt: wgpu::util::StagingBelt,
pub(crate) format: wgpu::TextureFormat,
pub(crate) quad_pipeline: quad::Pipeline,
pub(crate) text_pipeline: text::Pipeline,
pub(crate) triangle_pipeline: triangle::Pipeline,
#[cfg(any(feature = "image", feature = "svg"))]
pub(crate) image_pipeline: crate::image::Pipeline,
pub(crate) primitive_storage: primitive::Storage,
}
impl Engine {
pub fn new(
_adapter: &wgpu::Adapter,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>, // TODO: Initialize AA pipelines lazily
) -> Self {
let text_pipeline = text::Pipeline::new(device, queue, format);
let quad_pipeline = quad::Pipeline::new(device, format);
let triangle_pipeline =
triangle::Pipeline::new(device, format, antialiasing);
#[cfg(any(feature = "image", feature = "svg"))]
let image_pipeline = {
let backend = _adapter.get_info().backend;
crate::image::Pipeline::new(device, format, backend)
};
Self {
// TODO: Resize belt smartly (?)
// It would be great if the `StagingBelt` API exposed methods
// for introspection to detect when a resize may be worth it.
staging_belt: wgpu::util::StagingBelt::new(
buffer::MAX_WRITE_SIZE as u64,
),
format,
quad_pipeline,
text_pipeline,
triangle_pipeline,
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
primitive_storage: primitive::Storage::default(),
}
}
#[cfg(any(feature = "image", feature = "svg"))]
pub fn create_image_cache(
&self,
device: &wgpu::Device,
) -> crate::image::Cache {
self.image_pipeline.create_cache(device)
}
pub fn submit(
&mut self,
queue: &wgpu::Queue,
encoder: wgpu::CommandEncoder,
) -> wgpu::SubmissionIndex {
self.staging_belt.finish();
let index = queue.submit(Some(encoder.finish()));
self.staging_belt.recall();
self.quad_pipeline.end_frame();
self.text_pipeline.end_frame();
self.triangle_pipeline.end_frame();
#[cfg(any(feature = "image", feature = "svg"))]
self.image_pipeline.end_frame();
index
}
}

View file

@ -1,30 +1,358 @@
//! Build and draw geometry.
use crate::core::text::LineHeight;
use crate::core::{Pixels, Point, Rectangle, Size, Transformation, Vector};
use crate::core::{
Pixels, Point, Radians, Rectangle, Size, Transformation, Vector,
};
use crate::graphics::cache::{self, Cached};
use crate::graphics::color;
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::{
LineCap, LineDash, LineJoin, Path, Stroke, Style, Text,
self, LineCap, LineDash, LineJoin, Path, Stroke, Style,
};
use crate::graphics::gradient::{self, Gradient};
use crate::graphics::mesh::{self, Mesh};
use crate::primitive::{self, Primitive};
use crate::graphics::{self, Text};
use crate::text;
use crate::triangle;
use lyon::geom::euclid;
use lyon::tessellation;
use std::borrow::Cow;
#[derive(Debug)]
pub enum Geometry {
Live { meshes: Vec<Mesh>, text: Vec<Text> },
Cached(Cache),
}
#[derive(Debug, Clone)]
pub struct Cache {
pub meshes: Option<triangle::Cache>,
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, text } => {
if let Some(mut previous) = previous {
if let Some(cache) = &mut previous.meshes {
cache.update(meshes);
} else {
previous.meshes = triangle::Cache::new(meshes);
}
if let Some(cache) = &mut previous.text {
cache.update(text);
} else {
previous.text = text::Cache::new(group, text);
}
previous
} else {
Cache {
meshes: triangle::Cache::new(meshes),
text: text::Cache::new(group, text),
}
}
}
Self::Cached(cache) => cache,
}
}
}
/// A frame for drawing some geometry.
#[allow(missing_debug_implementations)]
pub struct Frame {
size: Size,
clip_bounds: Rectangle,
buffers: BufferStack,
primitives: Vec<Primitive>,
meshes: Vec<Mesh>,
text: Vec<Text>,
transforms: Transforms,
fill_tessellator: tessellation::FillTessellator,
stroke_tessellator: tessellation::StrokeTessellator,
}
impl Frame {
/// Creates a new [`Frame`] with the given [`Size`].
pub fn new(size: Size) -> Frame {
Self::with_clip(Rectangle::with_size(size))
}
/// Creates a new [`Frame`] with the given clip bounds.
pub fn with_clip(bounds: Rectangle) -> Frame {
Frame {
clip_bounds: bounds,
buffers: BufferStack::new(),
meshes: Vec::new(),
text: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
current: Transform(lyon::math::Transform::translation(
bounds.x, bounds.y,
)),
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
}
}
}
impl geometry::frame::Backend for Frame {
type Geometry = Geometry;
#[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 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 (position, size, line_height) =
if self.transforms.current.is_identity() {
(text.position, 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)
}
};
(position, size, line_height)
};
let bounds = Rectangle {
x: position.x,
y: position.y,
width: f32::INFINITY,
height: f32::INFINITY,
};
self.text.push(graphics::Text::Cached {
content: text.content,
bounds,
color: text.color,
size,
line_height: line_height.to_absolute(size),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
clip_bounds: 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::with_clip(clip_bounds)
}
fn paste(&mut self, frame: Frame, _at: Point) {
self.meshes
.extend(frame.buffers.into_meshes(frame.clip_bounds));
self.text.extend(frame.text);
}
fn into_geometry(mut self) -> Self::Geometry {
self.meshes
.extend(self.buffers.into_meshes(self.clip_bounds));
Geometry::Live {
meshes: self.meshes,
text: self.text,
}
}
}
enum Buffer {
Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>),
Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>),
@ -107,6 +435,34 @@ impl BufferStack {
_ => 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)]
@ -163,386 +519,6 @@ impl Transform {
gradient
}
}
impl Frame {
/// Creates a new empty [`Frame`] with the given dimensions.
///
/// The default coordinate system of a [`Frame`] has its origin at the
/// top-left corner of its bounds.
pub fn new(size: Size) -> Frame {
Frame {
size,
buffers: BufferStack::new(),
primitives: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
current: Transform(lyon::math::Transform::identity()),
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
}
}
/// Returns the width of the [`Frame`].
#[inline]
pub fn width(&self) -> f32 {
self.size.width
}
/// Returns the height of the [`Frame`].
#[inline]
pub fn height(&self) -> f32 {
self.size.height
}
/// Returns the dimensions of the [`Frame`].
#[inline]
pub fn size(&self) -> Size {
self.size
}
/// Returns the coordinate of the center of the [`Frame`].
#[inline]
pub fn center(&self) -> Point {
Point::new(self.size.width / 2.0, self.size.height / 2.0)
}
/// Draws the given [`Path`] on the [`Frame`] by filling it with the
/// provided style.
pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
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.");
}
/// Draws an axis-aligned rectangle given its top-left corner coordinate and
/// its `Size` on the [`Frame`] by filling it with the provided style.
pub fn fill_rectangle(
&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");
}
/// Draws the stroke of the given [`Path`] on the [`Frame`] with the
/// provided style.
pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
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");
}
/// Draws the characters of the given [`Text`] on the [`Frame`], filling
/// them with the given color.
///
/// __Warning:__ Text currently does not work well with rotations and scale
/// transforms! The position will be correctly transformed, but the
/// resulting glyphs will not be rotated or scaled properly.
///
/// Additionally, all text will be rendered on top of all the layers of
/// a `Canvas`. Therefore, it is currently only meant to be used for
/// overlays, which is the most common use case.
///
/// Support for vectorial text is planned, and should address all these
/// limitations.
pub fn fill_text(&mut self, text: impl Into<Text>) {
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 (position, size, line_height) =
if self.transforms.current.is_identity() {
(text.position, 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)
}
};
(position, size, line_height)
};
let bounds = Rectangle {
x: position.x,
y: position.y,
width: f32::INFINITY,
height: f32::INFINITY,
};
// TODO: Honor layering!
self.primitives.push(Primitive::Text {
content: text.content,
bounds,
color: text.color,
size,
line_height,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
clip_bounds: Rectangle::with_size(Size::INFINITY),
});
} else {
text.draw_with(|path, color| self.fill(&path, color));
}
}
/// Stores the current transform of the [`Frame`] and executes the given
/// drawing operations, restoring the transform afterwards.
///
/// This method is useful to compose transforms and perform drawing
/// operations in different coordinate systems.
#[inline]
pub fn with_save<R>(&mut self, f: impl FnOnce(&mut Frame) -> R) -> R {
self.push_transform();
let result = f(self);
self.pop_transform();
result
}
/// Pushes the current transform in the transform stack.
pub fn push_transform(&mut self) {
self.transforms.previous.push(self.transforms.current);
}
/// Pops a transform from the transform stack and sets it as the current transform.
pub fn pop_transform(&mut self) {
self.transforms.current = self.transforms.previous.pop().unwrap();
}
/// Executes the given drawing operations within a [`Rectangle`] region,
/// clipping any geometry that overflows its bounds. Any transformations
/// performed are local to the provided closure.
///
/// This method is useful to perform drawing operations that need to be
/// clipped.
#[inline]
pub fn with_clip<R>(
&mut self,
region: Rectangle,
f: impl FnOnce(&mut Frame) -> R,
) -> R {
let mut frame = Frame::new(region.size());
let result = f(&mut frame);
let origin = Point::new(region.x, region.y);
self.clip(frame, origin);
result
}
/// Draws the clipped contents of the given [`Frame`] with origin at the given [`Point`].
pub fn clip(&mut self, frame: Frame, at: Point) {
let size = frame.size();
let primitives = frame.into_primitives();
let transformation = Transformation::translate(at.x, at.y);
let (text, meshes) = primitives
.into_iter()
.partition(|primitive| matches!(primitive, Primitive::Text { .. }));
self.primitives.push(Primitive::Group {
primitives: vec![
Primitive::Transform {
transformation,
content: Box::new(Primitive::Group { primitives: meshes }),
},
Primitive::Transform {
transformation,
content: Box::new(Primitive::Clip {
bounds: Rectangle::with_size(size),
content: Box::new(Primitive::Group {
primitives: text,
}),
}),
},
],
});
}
/// Applies a translation to the current transform of the [`Frame`].
#[inline]
pub fn translate(&mut self, translation: Vector) {
self.transforms.current.0 =
self.transforms
.current
.0
.pre_translate(lyon::math::Vector::new(
translation.x,
translation.y,
));
}
/// Applies a rotation in radians to the current transform of the [`Frame`].
#[inline]
pub fn rotate(&mut self, angle: f32) {
self.transforms.current.0 = self
.transforms
.current
.0
.pre_rotate(lyon::math::Angle::radians(angle));
}
/// Applies a uniform scaling to the current transform of the [`Frame`].
#[inline]
pub fn scale(&mut self, scale: impl Into<f32>) {
let scale = scale.into();
self.scale_nonuniform(Vector { x: scale, y: scale });
}
/// Applies a non-uniform scaling to the current transform of the [`Frame`].
#[inline]
pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
let scale = scale.into();
self.transforms.current.0 =
self.transforms.current.0.pre_scale(scale.x, scale.y);
}
/// Produces the [`Primitive`] representing everything drawn on the [`Frame`].
pub fn into_primitive(self) -> Primitive {
Primitive::Group {
primitives: self.into_primitives(),
}
}
fn into_primitives(mut self) -> Vec<Primitive> {
for buffer in self.buffers.stack {
match buffer {
Buffer::Solid(buffer) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::Custom(
primitive::Custom::Mesh(Mesh::Solid {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
}),
));
}
}
Buffer::Gradient(buffer) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::Custom(
primitive::Custom::Mesh(Mesh::Gradient {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
}),
));
}
}
}
}
self.primitives
}
}
struct GradientVertex2DBuilder {
gradient: gradient::Packed,
}
@ -649,7 +625,9 @@ pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path {
let mut draw_line = false;
walk_along_path(
path.raw().iter().flattened(0.01),
path.raw().iter().flattened(
lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
),
0.0,
lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
&mut RepeatedPattern {

View file

@ -15,15 +15,23 @@ pub const SIZE: u32 = 2048;
use crate::core::Size;
use crate::graphics::color;
use std::sync::Arc;
#[derive(Debug)]
pub struct Atlas {
texture: wgpu::Texture,
texture_view: wgpu::TextureView,
texture_bind_group: wgpu::BindGroup,
texture_layout: Arc<wgpu::BindGroupLayout>,
layers: Vec<Layer>,
}
impl Atlas {
pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self {
pub fn new(
device: &wgpu::Device,
backend: wgpu::Backend,
texture_layout: Arc<wgpu::BindGroupLayout>,
) -> Self {
let layers = match backend {
// On the GL backend we start with 2 layers, to help wgpu figure
// out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D`
@ -60,15 +68,27 @@ impl Atlas {
..Default::default()
});
let texture_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image texture atlas bind group"),
layout: &texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
}],
});
Atlas {
texture,
texture_view,
texture_bind_group,
texture_layout,
layers,
}
}
pub fn view(&self) -> &wgpu::TextureView {
&self.texture_view
pub fn bind_group(&self) -> &wgpu::BindGroup {
&self.texture_bind_group
}
pub fn layer_count(&self) -> usize {
@ -94,7 +114,7 @@ impl Atlas {
entry
};
log::info!("Allocated atlas entry: {entry:?}");
log::debug!("Allocated atlas entry: {entry:?}");
// It is a webgpu requirement that:
// BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
@ -147,13 +167,20 @@ impl Atlas {
}
}
log::info!("Current atlas: {self:?}");
if log::log_enabled!(log::Level::Debug) {
log::debug!(
"Atlas layers: {} (busy: {}, allocations: {})",
self.layer_count(),
self.layers.iter().filter(|layer| !layer.is_empty()).count(),
self.layers.iter().map(Layer::allocations).sum::<usize>(),
);
}
Some(entry)
}
pub fn remove(&mut self, entry: &Entry) {
log::info!("Removing atlas entry: {entry:?}");
log::debug!("Removing atlas entry: {entry:?}");
match entry {
Entry::Contiguous(allocation) => {
@ -266,7 +293,7 @@ impl Atlas {
}
fn deallocate(&mut self, allocation: &Allocation) {
log::info!("Deallocating atlas: {allocation:?}");
log::debug!("Deallocating atlas: {allocation:?}");
match allocation {
Allocation::Full { layer } => {
@ -414,5 +441,17 @@ impl Atlas {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
self.texture_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image texture atlas bind group"),
layout: &self.texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&self.texture_view,
),
}],
});
}
}

View file

@ -33,6 +33,10 @@ impl Allocator {
pub fn is_empty(&self) -> bool {
self.allocations == 0
}
pub fn allocations(&self) -> usize {
self.allocations
}
}
pub struct Region {

View file

@ -11,4 +11,12 @@ impl Layer {
pub fn is_empty(&self) -> bool {
matches!(self, Layer::Empty)
}
pub fn allocations(&self) -> usize {
match self {
Layer::Empty => 0,
Layer::Busy(allocator) => allocator.allocations(),
Layer::Full => 1,
}
}
}

86
wgpu/src/image/cache.rs Normal file
View file

@ -0,0 +1,86 @@
use crate::core::{self, Size};
use crate::image::atlas::{self, Atlas};
use std::sync::Arc;
#[derive(Debug)]
pub struct Cache {
atlas: Atlas,
#[cfg(feature = "image")]
raster: crate::image::raster::Cache,
#[cfg(feature = "svg")]
vector: crate::image::vector::Cache,
}
impl Cache {
pub fn new(
device: &wgpu::Device,
backend: wgpu::Backend,
layout: Arc<wgpu::BindGroupLayout>,
) -> Self {
Self {
atlas: Atlas::new(device, backend, layout),
#[cfg(feature = "image")]
raster: crate::image::raster::Cache::default(),
#[cfg(feature = "svg")]
vector: crate::image::vector::Cache::default(),
}
}
pub fn bind_group(&self) -> &wgpu::BindGroup {
self.atlas.bind_group()
}
pub fn layer_count(&self) -> usize {
self.atlas.layer_count()
}
#[cfg(feature = "image")]
pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> {
self.raster.load(handle).dimensions()
}
#[cfg(feature = "svg")]
pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> {
self.vector.load(handle).viewport_dimensions()
}
#[cfg(feature = "image")]
pub fn upload_raster(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
handle: &core::image::Handle,
) -> Option<&atlas::Entry> {
self.raster.upload(device, encoder, handle, &mut self.atlas)
}
#[cfg(feature = "svg")]
pub fn upload_vector(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
handle: &core::svg::Handle,
color: Option<core::Color>,
size: [f32; 2],
scale: f32,
) -> Option<&atlas::Entry> {
self.vector.upload(
device,
encoder,
handle,
color,
size,
scale,
&mut self.atlas,
)
}
pub fn trim(&mut self) {
#[cfg(feature = "image")]
self.raster.trim(&mut self.atlas);
#[cfg(feature = "svg")]
self.vector.trim(&mut self.atlas);
}
}

View file

@ -1,3 +1,6 @@
pub(crate) mod cache;
pub(crate) use cache::Cache;
mod atlas;
#[cfg(feature = "image")]
@ -6,175 +9,30 @@ mod raster;
#[cfg(feature = "svg")]
mod vector;
use atlas::Atlas;
use crate::core::{Rectangle, Size, Transformation};
use crate::layer;
use crate::Buffer;
use std::cell::RefCell;
use std::mem;
use bytemuck::{Pod, Zeroable};
#[cfg(feature = "image")]
use crate::core::image;
use std::mem;
use std::sync::Arc;
#[cfg(feature = "svg")]
use crate::core::svg;
pub use crate::graphics::Image;
#[cfg(feature = "tracing")]
use tracing::info_span;
pub type Batch = Vec<Image>;
#[derive(Debug)]
pub struct Pipeline {
#[cfg(feature = "image")]
raster_cache: RefCell<raster::Cache>,
#[cfg(feature = "svg")]
vector_cache: RefCell<vector::Cache>,
pipeline: wgpu::RenderPipeline,
backend: wgpu::Backend,
nearest_sampler: wgpu::Sampler,
linear_sampler: wgpu::Sampler,
texture: wgpu::BindGroup,
texture_version: usize,
texture_atlas: Atlas,
texture_layout: wgpu::BindGroupLayout,
texture_layout: Arc<wgpu::BindGroupLayout>,
constant_layout: wgpu::BindGroupLayout,
layers: Vec<Layer>,
prepare_layer: usize,
}
#[derive(Debug)]
struct Layer {
uniforms: wgpu::Buffer,
nearest: Data,
linear: Data,
}
impl Layer {
fn new(
device: &wgpu::Device,
constant_layout: &wgpu::BindGroupLayout,
nearest_sampler: &wgpu::Sampler,
linear_sampler: &wgpu::Sampler,
) -> Self {
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu::image uniforms buffer"),
size: mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let nearest =
Data::new(device, constant_layout, nearest_sampler, &uniforms);
let linear =
Data::new(device, constant_layout, linear_sampler, &uniforms);
Self {
uniforms,
nearest,
linear,
}
}
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
nearest_instances: &[Instance],
linear_instances: &[Instance],
transformation: Transformation,
) {
queue.write_buffer(
&self.uniforms,
0,
bytemuck::bytes_of(&Uniforms {
transform: transformation.into(),
}),
);
self.nearest.upload(device, queue, nearest_instances);
self.linear.upload(device, queue, linear_instances);
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
self.nearest.render(render_pass);
self.linear.render(render_pass);
}
}
#[derive(Debug)]
struct Data {
constants: wgpu::BindGroup,
instances: Buffer<Instance>,
instance_count: usize,
}
impl Data {
pub fn new(
device: &wgpu::Device,
constant_layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
uniforms: &wgpu::Buffer,
) -> Self {
let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image constants bind group"),
layout: constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: uniforms,
offset: 0,
size: None,
},
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
let instances = Buffer::new(
device,
"iced_wgpu::image instance buffer",
Instance::INITIAL,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
Self {
constants,
instances,
instance_count: 0,
}
}
fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instances: &[Instance],
) {
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(queue, 0, instances);
self.instance_count = instances.len();
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_bind_group(0, &self.constants, &[]);
render_pass.set_vertex_buffer(0, self.instances.slice(..));
render_pass.draw(0..6, 0..self.instance_count as u32);
}
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
@ -257,9 +115,9 @@ impl Pipeline {
label: Some("iced_wgpu image shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
concat!(
include_str!("shader/vertex.wgsl"),
include_str!("../shader/vertex.wgsl"),
"\n",
include_str!("shader/image.wgsl"),
include_str!("../shader/image.wgsl"),
),
)),
});
@ -277,14 +135,20 @@ impl Pipeline {
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
// Scale
// Center
1 => Float32x2,
// Atlas position
// Scale
2 => Float32x2,
// Rotation
3 => Float32,
// Opacity
4 => Float32,
// Atlas position
5 => Float32x2,
// Atlas scale
3 => Float32x2,
6 => Float32x2,
// Layer
4 => Sint32,
7 => Sint32,
),
}],
},
@ -322,137 +186,95 @@ impl Pipeline {
multiview: None,
});
let texture_atlas = Atlas::new(device, backend);
let texture = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image texture atlas bind group"),
layout: &texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
texture_atlas.view(),
),
}],
});
Pipeline {
#[cfg(feature = "image")]
raster_cache: RefCell::new(raster::Cache::default()),
#[cfg(feature = "svg")]
vector_cache: RefCell::new(vector::Cache::default()),
pipeline,
backend,
nearest_sampler,
linear_sampler,
texture,
texture_version: texture_atlas.layer_count(),
texture_atlas,
texture_layout,
texture_layout: Arc::new(texture_layout),
constant_layout,
layers: Vec::new(),
prepare_layer: 0,
}
}
#[cfg(feature = "image")]
pub fn dimensions(&self, handle: &image::Handle) -> Size<u32> {
let mut cache = self.raster_cache.borrow_mut();
let memory = cache.load(handle);
memory.dimensions()
}
#[cfg(feature = "svg")]
pub fn viewport_dimensions(&self, handle: &svg::Handle) -> Size<u32> {
let mut cache = self.vector_cache.borrow_mut();
let svg = cache.load(handle);
svg.viewport_dimensions()
pub fn create_cache(&self, device: &wgpu::Device) -> Cache {
Cache::new(device, self.backend, self.texture_layout.clone())
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
images: &[layer::Image],
belt: &mut wgpu::util::StagingBelt,
cache: &mut Cache,
images: &Batch,
transformation: Transformation,
_scale: f32,
scale: f32,
) {
#[cfg(feature = "tracing")]
let _ = info_span!("Wgpu::Image", "PREPARE").entered();
#[cfg(feature = "tracing")]
let _ = info_span!("Wgpu::Image", "DRAW").entered();
let transformation = transformation * Transformation::scale(scale);
let nearest_instances: &mut Vec<Instance> = &mut Vec::new();
let linear_instances: &mut Vec<Instance> = &mut Vec::new();
#[cfg(feature = "image")]
let mut raster_cache = self.raster_cache.borrow_mut();
#[cfg(feature = "svg")]
let mut vector_cache = self.vector_cache.borrow_mut();
for image in images {
match &image {
#[cfg(feature = "image")]
layer::Image::Raster {
Image::Raster {
handle,
filter_method,
bounds,
rotation,
opacity,
} => {
if let Some(atlas_entry) = raster_cache.upload(
device,
encoder,
handle,
&mut self.texture_atlas,
) {
if let Some(atlas_entry) =
cache.upload_raster(device, encoder, handle)
{
add_instances(
[bounds.x, bounds.y],
[bounds.width, bounds.height],
f32::from(*rotation),
*opacity,
atlas_entry,
match filter_method {
image::FilterMethod::Nearest => {
crate::core::image::FilterMethod::Nearest => {
nearest_instances
}
image::FilterMethod::Linear => linear_instances,
crate::core::image::FilterMethod::Linear => {
linear_instances
}
},
);
}
}
#[cfg(not(feature = "image"))]
layer::Image::Raster { .. } => {}
Image::Raster { .. } => {}
#[cfg(feature = "svg")]
layer::Image::Vector {
Image::Vector {
handle,
color,
bounds,
rotation,
opacity,
} => {
let size = [bounds.width, bounds.height];
if let Some(atlas_entry) = vector_cache.upload(
device,
encoder,
handle,
*color,
size,
_scale,
&mut self.texture_atlas,
if let Some(atlas_entry) = cache.upload_vector(
device, encoder, handle, *color, size, scale,
) {
add_instances(
[bounds.x, bounds.y],
size,
f32::from(*rotation),
*opacity,
atlas_entry,
nearest_instances,
);
}
}
#[cfg(not(feature = "svg"))]
layer::Image::Vector { .. } => {}
Image::Vector { .. } => {}
}
}
@ -460,26 +282,6 @@ impl Pipeline {
return;
}
let texture_version = self.texture_atlas.layer_count();
if self.texture_version != texture_version {
log::info!("Atlas has grown. Recreating bind group...");
self.texture =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image texture atlas bind group"),
layout: &self.texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
self.texture_atlas.view(),
),
}],
});
self.texture_version = texture_version;
}
if self.layers.len() <= self.prepare_layer {
self.layers.push(Layer::new(
device,
@ -493,7 +295,8 @@ impl Pipeline {
layer.prepare(
device,
queue,
encoder,
belt,
nearest_instances,
linear_instances,
transformation,
@ -504,6 +307,7 @@ impl Pipeline {
pub fn render<'a>(
&'a self,
cache: &'a Cache,
layer: usize,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
@ -518,28 +322,173 @@ impl Pipeline {
bounds.height,
);
render_pass.set_bind_group(1, &self.texture, &[]);
render_pass.set_bind_group(1, cache.bind_group(), &[]);
layer.render(render_pass);
}
}
pub fn end_frame(&mut self) {
#[cfg(feature = "image")]
self.raster_cache.borrow_mut().trim(&mut self.texture_atlas);
#[cfg(feature = "svg")]
self.vector_cache.borrow_mut().trim(&mut self.texture_atlas);
self.prepare_layer = 0;
}
}
#[derive(Debug)]
struct Layer {
uniforms: wgpu::Buffer,
nearest: Data,
linear: Data,
}
impl Layer {
fn new(
device: &wgpu::Device,
constant_layout: &wgpu::BindGroupLayout,
nearest_sampler: &wgpu::Sampler,
linear_sampler: &wgpu::Sampler,
) -> Self {
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu::image uniforms buffer"),
size: mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let nearest =
Data::new(device, constant_layout, nearest_sampler, &uniforms);
let linear =
Data::new(device, constant_layout, linear_sampler, &uniforms);
Self {
uniforms,
nearest,
linear,
}
}
fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
nearest_instances: &[Instance],
linear_instances: &[Instance],
transformation: Transformation,
) {
let uniforms = Uniforms {
transform: transformation.into(),
};
let bytes = bytemuck::bytes_of(&uniforms);
belt.write_buffer(
encoder,
&self.uniforms,
0,
(bytes.len() as u64).try_into().expect("Sized uniforms"),
device,
)
.copy_from_slice(bytes);
self.nearest
.upload(device, encoder, belt, nearest_instances);
self.linear.upload(device, encoder, belt, linear_instances);
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
self.nearest.render(render_pass);
self.linear.render(render_pass);
}
}
#[derive(Debug)]
struct Data {
constants: wgpu::BindGroup,
instances: Buffer<Instance>,
instance_count: usize,
}
impl Data {
pub fn new(
device: &wgpu::Device,
constant_layout: &wgpu::BindGroupLayout,
sampler: &wgpu::Sampler,
uniforms: &wgpu::Buffer,
) -> Self {
let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image constants bind group"),
layout: constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: uniforms,
offset: 0,
size: None,
},
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
let instances = Buffer::new(
device,
"iced_wgpu::image instance buffer",
Instance::INITIAL,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
Self {
constants,
instances,
instance_count: 0,
}
}
fn upload(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
instances: &[Instance],
) {
self.instance_count = instances.len();
if self.instance_count == 0 {
return;
}
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(device, encoder, belt, 0, instances);
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
if self.instance_count == 0 {
return;
}
render_pass.set_bind_group(0, &self.constants, &[]);
render_pass.set_vertex_buffer(0, self.instances.slice(..));
render_pass.draw(0..6, 0..self.instance_count as u32);
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Instance {
_position: [f32; 2],
_center: [f32; 2],
_size: [f32; 2],
_rotation: f32,
_opacity: f32,
_position_in_atlas: [f32; 2],
_size_in_atlas: [f32; 2],
_layer: u32,
@ -558,12 +507,27 @@ struct Uniforms {
fn add_instances(
image_position: [f32; 2],
image_size: [f32; 2],
rotation: f32,
opacity: f32,
entry: &atlas::Entry,
instances: &mut Vec<Instance>,
) {
let center = [
image_position[0] + image_size[0] / 2.0,
image_position[1] + image_size[1] / 2.0,
];
match entry {
atlas::Entry::Contiguous(allocation) => {
add_instance(image_position, image_size, allocation, instances);
add_instance(
image_position,
center,
image_size,
rotation,
opacity,
allocation,
instances,
);
}
atlas::Entry::Fragmented { fragments, size } => {
let scaling_x = image_size[0] / size.width as f32;
@ -589,7 +553,10 @@ fn add_instances(
fragment_height as f32 * scaling_y,
];
add_instance(position, size, allocation, instances);
add_instance(
position, center, size, rotation, opacity, allocation,
instances,
);
}
}
}
@ -598,7 +565,10 @@ fn add_instances(
#[inline]
fn add_instance(
position: [f32; 2],
center: [f32; 2],
size: [f32; 2],
rotation: f32,
opacity: f32,
allocation: &atlas::Allocation,
instances: &mut Vec<Instance>,
) {
@ -608,7 +578,10 @@ fn add_instance(
let instance = Instance {
_position: position,
_center: center,
_size: size,
_rotation: rotation,
_opacity: opacity,
_position_in_atlas: [
(x as f32 + 0.5) / atlas::SIZE as f32,
(y as f32 + 0.5) / atlas::SIZE as f32,

10
wgpu/src/image/null.rs Normal file
View file

@ -0,0 +1,10 @@
pub use crate::graphics::Image;
#[derive(Debug, Default)]
pub struct Batch;
impl Batch {
pub fn push(&mut self, _image: Image) {}
pub fn clear(&mut self) {}
}

View file

@ -4,13 +4,13 @@ use crate::graphics;
use crate::graphics::image::image_rs;
use crate::image::atlas::{self, Atlas};
use std::collections::{HashMap, HashSet};
use rustc_hash::{FxHashMap, FxHashSet};
/// Entry in cache corresponding to an image handle
#[derive(Debug)]
pub enum Memory {
/// Image data on host
Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, Vec<u8>>),
Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>),
/// Storage entry
Device(atlas::Entry),
/// Image not found
@ -38,8 +38,9 @@ impl Memory {
/// Caches image raster data
#[derive(Debug, Default)]
pub struct Cache {
map: HashMap<u64, Memory>,
hits: HashSet<u64>,
map: FxHashMap<image::Id, Memory>,
hits: FxHashSet<image::Id>,
should_trim: bool,
}
impl Cache {
@ -50,11 +51,13 @@ impl Cache {
}
let memory = match graphics::image::load(handle) {
Ok(image) => Memory::Host(image.to_rgba8()),
Ok(image) => Memory::Host(image),
Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound,
Err(_) => Memory::Invalid,
};
self.should_trim = true;
self.insert(handle, memory);
self.get(handle).unwrap()
}
@ -86,6 +89,11 @@ impl Cache {
/// Trim cache misses from cache
pub fn trim(&mut self, atlas: &mut Atlas) {
// Only trim if new entries have landed in the `Cache`
if !self.should_trim {
return;
}
let hits = &self.hits;
self.map.retain(|k, memory| {
@ -101,6 +109,7 @@ impl Cache {
});
self.hits.clear();
self.should_trim = false;
}
fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> {

View file

@ -5,7 +5,7 @@ use crate::image::atlas::{self, Atlas};
use resvg::tiny_skia;
use resvg::usvg::{self, TreeTextToPath};
use std::collections::{HashMap, HashSet};
use rustc_hash::{FxHashMap, FxHashSet};
use std::fs;
/// Entry in cache corresponding to an svg handle
@ -33,10 +33,11 @@ impl Svg {
/// Caches svg vector and raster data
#[derive(Debug, Default)]
pub struct Cache {
svgs: HashMap<u64, Svg>,
rasterized: HashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
svg_hits: HashSet<u64>,
rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>,
svgs: FxHashMap<u64, Svg>,
rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
svg_hits: FxHashSet<u64>,
rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>,
should_trim: bool,
}
type ColorFilter = Option<[u8; 4]>;
@ -76,6 +77,8 @@ impl Cache {
}
}
self.should_trim = true;
let _ = self.svgs.insert(handle.id(), svg);
self.svgs.get(&handle.id()).unwrap()
}
@ -176,6 +179,10 @@ impl Cache {
/// Load svg and upload raster data
pub fn trim(&mut self, atlas: &mut Atlas) {
if !self.should_trim {
return;
}
let svg_hits = &self.svg_hits;
let rasterized_hits = &self.rasterized_hits;
@ -191,6 +198,7 @@ impl Cache {
});
self.svg_hits.clear();
self.rasterized_hits.clear();
self.should_trim = false;
}
}

View file

@ -1,343 +1,302 @@
//! Organize rendering primitives into a flattened list of layers.
mod image;
mod pipeline;
mod text;
pub mod mesh;
pub use image::Image;
pub use mesh::Mesh;
pub use pipeline::Pipeline;
pub use text::Text;
use crate::core;
use crate::core::alignment;
use crate::core::{
Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector,
renderer, Background, Color, Point, Radians, Rectangle, Transformation,
};
use crate::graphics;
use crate::graphics::color;
use crate::graphics::Viewport;
use crate::graphics::layer;
use crate::graphics::text::{Editor, Paragraph};
use crate::graphics::Mesh;
use crate::image::{self, Image};
use crate::primitive::{self, Primitive};
use crate::quad::{self, Quad};
use crate::text::{self, Text};
use crate::triangle;
pub type Stack = layer::Stack<Layer>;
/// A group of primitives that should be clipped together.
#[derive(Debug)]
pub struct Layer<'a> {
/// The clipping bounds of the [`Layer`].
pub struct Layer {
pub bounds: Rectangle,
/// The quads of the [`Layer`].
pub quads: quad::Batch,
/// The triangle meshes of the [`Layer`].
pub meshes: Vec<Mesh<'a>>,
/// The text of the [`Layer`].
pub text: Vec<Text<'a>>,
/// The images of the [`Layer`].
pub images: Vec<Image>,
/// The custom pipelines of this [`Layer`].
pub pipelines: Vec<Pipeline>,
pub triangles: triangle::Batch,
pub primitives: primitive::Batch,
pub text: text::Batch,
pub images: image::Batch,
pending_meshes: Vec<Mesh>,
pending_text: Vec<Text>,
}
impl<'a> Layer<'a> {
/// Creates a new [`Layer`] with the given clipping bounds.
pub fn new(bounds: Rectangle) -> Self {
impl Layer {
pub fn draw_quad(
&mut self,
quad: renderer::Quad,
background: Background,
transformation: Transformation,
) {
let bounds = quad.bounds * transformation;
let quad = Quad {
position: [bounds.x, bounds.y],
size: [bounds.width, bounds.height],
border_color: color::pack(quad.border.color),
border_radius: quad.border.radius.into(),
border_width: quad.border.width,
shadow_color: color::pack(quad.shadow.color),
shadow_offset: quad.shadow.offset.into(),
shadow_blur_radius: quad.shadow.blur_radius,
};
self.quads.add(quad, &background);
}
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,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
clip_bounds: clip_bounds * transformation,
};
self.pending_text.push(text);
}
pub fn draw_image(
&mut self,
handle: crate::core::image::Handle,
filter_method: crate::core::image::FilterMethod,
bounds: Rectangle,
transformation: Transformation,
rotation: Radians,
opacity: f32,
) {
let image = Image::Raster {
handle,
filter_method,
bounds: bounds * transformation,
rotation,
opacity,
};
self.images.push(image);
}
pub fn draw_svg(
&mut self,
handle: crate::core::svg::Handle,
color: Option<Color>,
bounds: Rectangle,
transformation: Transformation,
rotation: Radians,
opacity: f32,
) {
let svg = Image::Vector {
handle,
color,
bounds: bounds * transformation,
rotation,
opacity,
};
self.images.push(svg);
}
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: triangle::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: Box<dyn Primitive>,
transformation: Transformation,
) {
let bounds = bounds * transformation;
self.primitives
.push(primitive::Instance { bounds, primitive });
}
fn flush_meshes(&mut self) {
if !self.pending_meshes.is_empty() {
self.triangles.push(triangle::Item::Group {
transformation: Transformation::IDENTITY,
meshes: self.pending_meshes.drain(..).collect(),
});
}
}
fn flush_text(&mut self) {
if !self.pending_text.is_empty() {
self.text.push(text::Item::Group {
transformation: Transformation::IDENTITY,
text: self.pending_text.drain(..).collect(),
});
}
}
}
impl graphics::Layer for Layer {
fn with_bounds(bounds: Rectangle) -> Self {
Self {
bounds,
..Self::default()
}
}
fn flush(&mut self) {
self.flush_meshes();
self.flush_text();
}
fn resize(&mut self, bounds: Rectangle) {
self.bounds = bounds;
}
fn reset(&mut self) {
self.bounds = Rectangle::INFINITE;
self.quads.clear();
self.triangles.clear();
self.primitives.clear();
self.text.clear();
self.images.clear();
self.pending_meshes.clear();
self.pending_text.clear();
}
}
impl Default for Layer {
fn default() -> Self {
Self {
bounds: Rectangle::INFINITE,
quads: quad::Batch::default(),
meshes: Vec::new(),
text: Vec::new(),
images: Vec::new(),
pipelines: Vec::new(),
}
}
/// Creates a new [`Layer`] for the provided overlay text.
///
/// This can be useful for displaying debug information.
pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self {
let mut overlay =
Layer::new(Rectangle::with_size(viewport.logical_size()));
for (i, line) in lines.iter().enumerate() {
let text = text::Cached {
content: line.as_ref(),
bounds: Rectangle::new(
Point::new(11.0, 11.0 + 25.0 * i as f32),
Size::INFINITY,
),
color: Color::new(0.9, 0.9, 0.9, 1.0),
size: Pixels(20.0),
line_height: core::text::LineHeight::default(),
font: Font::MONOSPACE,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: core::text::Shaping::Basic,
clip_bounds: Rectangle::with_size(Size::INFINITY),
};
overlay.text.push(Text::Cached(text.clone()));
overlay.text.push(Text::Cached(text::Cached {
bounds: text.bounds + Vector::new(-1.0, -1.0),
color: Color::BLACK,
..text
}));
}
overlay
}
/// Distributes the given [`Primitive`] and generates a list of layers based
/// on its contents.
pub fn generate(
primitives: &'a [Primitive],
viewport: &Viewport,
) -> Vec<Self> {
let first_layer =
Layer::new(Rectangle::with_size(viewport.logical_size()));
let mut layers = vec![first_layer];
for primitive in primitives {
Self::process_primitive(
&mut layers,
Transformation::IDENTITY,
primitive,
0,
);
}
layers
}
fn process_primitive(
layers: &mut Vec<Self>,
transformation: Transformation,
primitive: &'a Primitive,
current_layer: usize,
) {
match primitive {
Primitive::Paragraph {
paragraph,
position,
color,
clip_bounds,
} => {
let layer = &mut layers[current_layer];
layer.text.push(Text::Paragraph {
paragraph: paragraph.clone(),
position: *position,
color: *color,
clip_bounds: *clip_bounds,
transformation,
});
}
Primitive::Editor {
editor,
position,
color,
clip_bounds,
} => {
let layer = &mut layers[current_layer];
layer.text.push(Text::Editor {
editor: editor.clone(),
position: *position,
color: *color,
clip_bounds: *clip_bounds,
transformation,
});
}
Primitive::Text {
content,
bounds,
size,
line_height,
color,
font,
horizontal_alignment,
vertical_alignment,
shaping,
clip_bounds,
} => {
let layer = &mut layers[current_layer];
layer.text.push(Text::Cached(text::Cached {
content,
bounds: *bounds + transformation.translation(),
size: *size * transformation.scale_factor(),
line_height: *line_height,
color: *color,
font: *font,
horizontal_alignment: *horizontal_alignment,
vertical_alignment: *vertical_alignment,
shaping: *shaping,
clip_bounds: *clip_bounds * transformation,
}));
}
graphics::Primitive::RawText(raw) => {
let layer = &mut layers[current_layer];
layer.text.push(Text::Raw {
raw: raw.clone(),
transformation,
});
}
Primitive::Quad {
bounds,
background,
border,
shadow,
} => {
let layer = &mut layers[current_layer];
let bounds = *bounds * transformation;
let quad = Quad {
position: [bounds.x, bounds.y],
size: [bounds.width, bounds.height],
border_color: color::pack(border.color),
border_radius: border.radius.into(),
border_width: border.width,
shadow_color: shadow.color.into_linear(),
shadow_offset: shadow.offset.into(),
shadow_blur_radius: shadow.blur_radius,
};
layer.quads.add(quad, background);
}
Primitive::Image {
handle,
filter_method,
bounds,
} => {
let layer = &mut layers[current_layer];
layer.images.push(Image::Raster {
handle: handle.clone(),
filter_method: *filter_method,
bounds: *bounds * transformation,
});
}
Primitive::Svg {
handle,
color,
bounds,
} => {
let layer = &mut layers[current_layer];
layer.images.push(Image::Vector {
handle: handle.clone(),
color: *color,
bounds: *bounds * transformation,
});
}
Primitive::Group { primitives } => {
// TODO: Inspect a bit and regroup (?)
for primitive in primitives {
Self::process_primitive(
layers,
transformation,
primitive,
current_layer,
);
}
}
Primitive::Clip { bounds, content } => {
let layer = &mut layers[current_layer];
let translated_bounds = *bounds * transformation;
// Only draw visible content
if let Some(clip_bounds) =
layer.bounds.intersection(&translated_bounds)
{
let clip_layer = Layer::new(clip_bounds);
layers.push(clip_layer);
Self::process_primitive(
layers,
transformation,
content,
layers.len() - 1,
);
}
}
Primitive::Transform {
transformation: new_transformation,
content,
} => {
Self::process_primitive(
layers,
transformation * *new_transformation,
content,
current_layer,
);
}
Primitive::Cache { content } => {
Self::process_primitive(
layers,
transformation,
content,
current_layer,
);
}
Primitive::Custom(custom) => match custom {
primitive::Custom::Mesh(mesh) => match mesh {
graphics::Mesh::Solid { buffers, size } => {
let layer = &mut layers[current_layer];
let bounds =
Rectangle::with_size(*size) * transformation;
// Only draw visible content
if let Some(clip_bounds) =
layer.bounds.intersection(&bounds)
{
layer.meshes.push(Mesh::Solid {
transformation,
buffers,
clip_bounds,
});
}
}
graphics::Mesh::Gradient { buffers, size } => {
let layer = &mut layers[current_layer];
let bounds =
Rectangle::with_size(*size) * transformation;
// Only draw visible content
if let Some(clip_bounds) =
layer.bounds.intersection(&bounds)
{
layer.meshes.push(Mesh::Gradient {
transformation,
buffers,
clip_bounds,
});
}
}
},
primitive::Custom::Pipeline(pipeline) => {
let layer = &mut layers[current_layer];
let bounds = pipeline.bounds * transformation;
if let Some(clip_bounds) =
layer.bounds.intersection(&bounds)
{
layer.pipelines.push(Pipeline {
bounds,
viewport: clip_bounds,
primitive: pipeline.primitive.clone(),
});
}
}
},
triangles: triangle::Batch::default(),
primitives: primitive::Batch::default(),
text: text::Batch::default(),
images: image::Batch::default(),
pending_meshes: Vec::new(),
pending_text: Vec::new(),
}
}
}

View file

@ -1,30 +0,0 @@
use crate::core::image;
use crate::core::svg;
use crate::core::{Color, Rectangle};
/// A raster or vector image.
#[derive(Debug, Clone)]
pub enum Image {
/// A raster image.
Raster {
/// The handle of a raster image.
handle: image::Handle,
/// The filter method of a raster image.
filter_method: image::FilterMethod,
/// The bounds of the image.
bounds: Rectangle,
},
/// A vector image.
Vector {
/// The handle of a vector image.
handle: svg::Handle,
/// The [`Color`] filter
color: Option<Color>,
/// The bounds of the image.
bounds: Rectangle,
},
}

View file

@ -1,97 +0,0 @@
//! A collection of triangle primitives.
use crate::core::{Rectangle, Transformation};
use crate::graphics::mesh;
/// A mesh of triangles.
#[derive(Debug, Clone, Copy)]
pub enum Mesh<'a> {
/// A mesh of triangles with a solid color.
Solid {
/// The [`Transformation`] for the vertices of the [`Mesh`].
transformation: Transformation,
/// The vertex and index buffers of the [`Mesh`].
buffers: &'a mesh::Indexed<mesh::SolidVertex2D>,
/// The clipping bounds of the [`Mesh`].
clip_bounds: Rectangle<f32>,
},
/// A mesh of triangles with a gradient color.
Gradient {
/// The [`Transformation`] for the vertices of the [`Mesh`].
transformation: Transformation,
/// The vertex and index buffers of the [`Mesh`].
buffers: &'a mesh::Indexed<mesh::GradientVertex2D>,
/// The clipping bounds of the [`Mesh`].
clip_bounds: Rectangle<f32>,
},
}
impl Mesh<'_> {
/// Returns the origin of the [`Mesh`].
pub fn transformation(&self) -> Transformation {
match self {
Self::Solid { transformation, .. }
| Self::Gradient { transformation, .. } => *transformation,
}
}
/// Returns the indices of the [`Mesh`].
pub fn indices(&self) -> &[u32] {
match self {
Self::Solid { buffers, .. } => &buffers.indices,
Self::Gradient { buffers, .. } => &buffers.indices,
}
}
/// Returns the clip bounds of the [`Mesh`].
pub fn clip_bounds(&self) -> Rectangle<f32> {
match self {
Self::Solid { clip_bounds, .. }
| Self::Gradient { clip_bounds, .. } => *clip_bounds,
}
}
}
/// The result of counting the attributes of a set of meshes.
#[derive(Debug, Clone, Copy, Default)]
pub struct AttributeCount {
/// The total amount of solid vertices.
pub solid_vertices: usize,
/// The total amount of solid meshes.
pub solids: usize,
/// The total amount of gradient vertices.
pub gradient_vertices: usize,
/// The total amount of gradient meshes.
pub gradients: usize,
/// The total amount of indices.
pub indices: usize,
}
/// Returns the number of total vertices & total indices of all [`Mesh`]es.
pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount {
meshes
.iter()
.fold(AttributeCount::default(), |mut count, mesh| {
match mesh {
Mesh::Solid { buffers, .. } => {
count.solids += 1;
count.solid_vertices += buffers.vertices.len();
count.indices += buffers.indices.len();
}
Mesh::Gradient { buffers, .. } => {
count.gradients += 1;
count.gradient_vertices += buffers.vertices.len();
count.indices += buffers.indices.len();
}
}
count
})
}

View file

@ -1,17 +0,0 @@
use crate::core::Rectangle;
use crate::primitive::pipeline::Primitive;
use std::sync::Arc;
#[derive(Clone, Debug)]
/// A custom primitive which can be used to render primitives associated with a custom pipeline.
pub struct Pipeline {
/// The bounds of the [`Pipeline`].
pub bounds: Rectangle,
/// The viewport of the [`Pipeline`].
pub viewport: Rectangle,
/// The [`Primitive`] to render.
pub primitive: Arc<dyn Primitive>,
}

View file

@ -1,70 +0,0 @@
use crate::core::alignment;
use crate::core::text;
use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation};
use crate::graphics;
use crate::graphics::text::editor;
use crate::graphics::text::paragraph;
/// A text primitive.
#[derive(Debug, Clone)]
pub enum Text<'a> {
/// A paragraph.
#[allow(missing_docs)]
Paragraph {
paragraph: paragraph::Weak,
position: Point,
color: Color,
clip_bounds: Rectangle,
transformation: Transformation,
},
/// An editor.
#[allow(missing_docs)]
Editor {
editor: editor::Weak,
position: Point,
color: Color,
clip_bounds: Rectangle,
transformation: Transformation,
},
/// Some cached text.
Cached(Cached<'a>),
/// Some raw text.
#[allow(missing_docs)]
Raw {
raw: graphics::text::Raw,
transformation: Transformation,
},
}
#[derive(Debug, Clone)]
pub struct Cached<'a> {
/// The content of the [`Text`].
pub content: &'a str,
/// The layout bounds of the [`Text`].
pub bounds: Rectangle,
/// The color of the [`Text`], in __linear RGB_.
pub color: Color,
/// The size of the [`Text`] in logical pixels.
pub size: Pixels,
/// The line height of the [`Text`].
pub line_height: text::LineHeight,
/// The font of the [`Text`].
pub font: Font,
/// The horizontal alignment of the [`Text`].
pub horizontal_alignment: alignment::Horizontal,
/// The vertical alignment of the [`Text`].
pub vertical_alignment: alignment::Vertical,
/// The shaping strategy of the text.
pub shaping: text::Shaping,
/// The clip bounds of the text.
pub clip_bounds: Rectangle,
}

View file

@ -20,15 +20,8 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![forbid(rust_2018_idioms)]
#![deny(
missing_debug_implementations,
missing_docs,
unsafe_code,
unused_results,
rustdoc::broken_intra_doc_links
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![allow(missing_docs)]
pub mod layer;
pub mod primitive;
pub mod settings;
@ -37,13 +30,21 @@ pub mod window;
#[cfg(feature = "geometry")]
pub mod geometry;
mod backend;
mod buffer;
mod color;
mod engine;
mod quad;
mod text;
mod triangle;
#[cfg(any(feature = "image", feature = "svg"))]
#[path = "image/mod.rs"]
mod image;
#[cfg(not(any(feature = "image", feature = "svg")))]
#[path = "image/null.rs"]
mod image;
use buffer::Buffer;
pub use iced_graphics as graphics;
@ -51,16 +52,525 @@ pub use iced_graphics::core;
pub use wgpu;
pub use backend::Backend;
pub use engine::Engine;
pub use layer::Layer;
pub use primitive::Primitive;
pub use settings::Settings;
#[cfg(any(feature = "image", feature = "svg"))]
mod image;
#[cfg(feature = "geometry")]
pub use geometry::Geometry;
use crate::core::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
use crate::graphics::text::{Editor, Paragraph};
use crate::graphics::Viewport;
/// A [`wgpu`] graphics renderer for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
pub type Renderer = iced_graphics::Renderer<Backend>;
#[allow(missing_debug_implementations)]
pub struct Renderer {
default_font: Font,
default_text_size: Pixels,
layers: layer::Stack,
triangle_storage: triangle::Storage,
text_storage: text::Storage,
text_viewport: text::Viewport,
// TODO: Centralize all the image feature handling
#[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell<image::Cache>,
}
impl Renderer {
pub fn new(
device: &wgpu::Device,
engine: &Engine,
default_font: Font,
default_text_size: Pixels,
) -> Self {
Self {
default_font,
default_text_size,
layers: layer::Stack::new(),
triangle_storage: triangle::Storage::new(),
text_storage: text::Storage::new(),
text_viewport: engine.text_pipeline.create_viewport(device),
#[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell::new(
engine.create_image_cache(device),
),
}
}
pub fn present(
&mut self,
engine: &mut Engine,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
clear_color: Option<Color>,
format: wgpu::TextureFormat,
frame: &wgpu::TextureView,
viewport: &Viewport,
) {
self.prepare(engine, device, queue, format, encoder, viewport);
self.render(engine, encoder, frame, clear_color, viewport);
self.triangle_storage.trim();
self.text_storage.trim();
#[cfg(any(feature = "svg", feature = "image"))]
self.image_cache.borrow_mut().trim();
}
fn prepare(
&mut self,
engine: &mut Engine,
device: &wgpu::Device,
queue: &wgpu::Queue,
_format: wgpu::TextureFormat,
encoder: &mut wgpu::CommandEncoder,
viewport: &Viewport,
) {
let scale_factor = viewport.scale_factor() as f32;
self.text_viewport.update(queue, viewport.physical_size());
for layer in self.layers.iter_mut() {
if !layer.quads.is_empty() {
engine.quad_pipeline.prepare(
device,
encoder,
&mut engine.staging_belt,
&layer.quads,
viewport.projection(),
scale_factor,
);
}
if !layer.triangles.is_empty() {
engine.triangle_pipeline.prepare(
device,
encoder,
&mut engine.staging_belt,
&mut self.triangle_storage,
&layer.triangles,
Transformation::scale(scale_factor),
viewport.physical_size(),
);
}
if !layer.primitives.is_empty() {
for instance in &layer.primitives {
instance.primitive.prepare(
device,
queue,
engine.format,
&mut engine.primitive_storage,
&instance.bounds,
viewport,
);
}
}
if !layer.text.is_empty() {
engine.text_pipeline.prepare(
device,
queue,
&self.text_viewport,
encoder,
&mut self.text_storage,
&layer.text,
layer.bounds,
Transformation::scale(scale_factor),
);
}
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
engine.image_pipeline.prepare(
device,
encoder,
&mut engine.staging_belt,
&mut self.image_cache.borrow_mut(),
&layer.images,
viewport.projection(),
scale_factor,
);
}
}
}
fn render(
&mut self,
engine: &mut Engine,
encoder: &mut wgpu::CommandEncoder,
frame: &wgpu::TextureView,
clear_color: Option<Color>,
viewport: &Viewport,
) {
use std::mem::ManuallyDrop;
let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: frame,
resolve_target: None,
ops: wgpu::Operations {
load: match clear_color {
Some(background_color) => wgpu::LoadOp::Clear({
let [r, g, b, a] =
graphics::color::pack(background_color)
.components();
wgpu::Color {
r: f64::from(r),
g: f64::from(g),
b: f64::from(b),
a: f64::from(a),
}
}),
None => wgpu::LoadOp::Load,
},
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
let mut quad_layer = 0;
let mut mesh_layer = 0;
let mut text_layer = 0;
#[cfg(any(feature = "svg", feature = "image"))]
let mut image_layer = 0;
#[cfg(any(feature = "svg", feature = "image"))]
let image_cache = self.image_cache.borrow();
let scale_factor = viewport.scale_factor() as f32;
let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
viewport.physical_size(),
));
let scale = Transformation::scale(scale_factor);
for layer in self.layers.iter() {
let Some(physical_bounds) =
physical_bounds.intersection(&(layer.bounds * scale))
else {
continue;
};
let Some(scissor_rect) = physical_bounds.snap() else {
continue;
};
if !layer.quads.is_empty() {
engine.quad_pipeline.render(
quad_layer,
scissor_rect,
&layer.quads,
&mut render_pass,
);
quad_layer += 1;
}
if !layer.triangles.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
mesh_layer += engine.triangle_pipeline.render(
encoder,
frame,
&self.triangle_storage,
mesh_layer,
&layer.triangles,
physical_bounds,
scale,
);
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: frame,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
if !layer.primitives.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
for instance in &layer.primitives {
if let Some(clip_bounds) = (instance.bounds * scale)
.intersection(&physical_bounds)
.and_then(Rectangle::snap)
{
instance.primitive.render(
encoder,
&engine.primitive_storage,
frame,
&clip_bounds,
);
}
}
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: frame,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
if !layer.text.is_empty() {
text_layer += engine.text_pipeline.render(
&self.text_viewport,
&self.text_storage,
text_layer,
&layer.text,
scissor_rect,
&mut render_pass,
);
}
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
engine.image_pipeline.render(
&image_cache,
image_layer,
scissor_rect,
&mut render_pass,
);
image_layer += 1;
}
}
let _ = ManuallyDrop::into_inner(render_pass);
}
}
impl core::Renderer for Renderer {
fn start_layer(&mut self, bounds: Rectangle) {
self.layers.push_clip(bounds);
}
fn end_layer(&mut self) {
self.layers.pop_clip();
}
fn start_transformation(&mut self, transformation: Transformation) {
self.layers.push_transformation(transformation);
}
fn end_transformation(&mut self) {
self.layers.pop_transformation();
}
fn fill_quad(
&mut self,
quad: core::renderer::Quad,
background: impl Into<Background>,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_quad(quad, background.into(), transformation);
}
fn clear(&mut self) {
self.layers.clear();
}
}
impl core::text::Renderer for Renderer {
type Font = Font;
type Paragraph = Paragraph;
type Editor = Editor;
const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}';
const ARROW_DOWN_ICON: char = '\u{e800}';
fn default_font(&self) -> Self::Font {
self.default_font
}
fn default_size(&self) -> Pixels {
self.default_text_size
}
fn fill_paragraph(
&mut self,
text: &Self::Paragraph,
position: Point,
color: Color,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_paragraph(
text,
position,
color,
clip_bounds,
transformation,
);
}
fn fill_editor(
&mut self,
editor: &Self::Editor,
position: Point,
color: Color,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_editor(editor, position, color, clip_bounds, transformation);
}
fn fill_text(
&mut self,
text: core::Text,
position: Point,
color: Color,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_text(text, position, color, clip_bounds, transformation);
}
}
#[cfg(feature = "image")]
impl core::image::Renderer for Renderer {
type Handle = core::image::Handle;
fn measure_image(&self, handle: &Self::Handle) -> Size<u32> {
self.image_cache.borrow_mut().measure_image(handle)
}
fn draw_image(
&mut self,
handle: Self::Handle,
filter_method: core::image::FilterMethod,
bounds: Rectangle,
rotation: core::Radians,
opacity: f32,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_image(
handle,
filter_method,
bounds,
transformation,
rotation,
opacity,
);
}
}
#[cfg(feature = "svg")]
impl core::svg::Renderer for Renderer {
fn measure_svg(&self, handle: &core::svg::Handle) -> Size<u32> {
self.image_cache.borrow_mut().measure_svg(handle)
}
fn draw_svg(
&mut self,
handle: core::svg::Handle,
color_filter: Option<Color>,
bounds: Rectangle,
rotation: core::Radians,
opacity: f32,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_svg(
handle,
color_filter,
bounds,
transformation,
rotation,
opacity,
);
}
}
impl graphics::mesh::Renderer for Renderer {
fn draw_mesh(&mut self, mesh: graphics::Mesh) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_mesh(mesh, transformation);
}
}
#[cfg(feature = "geometry")]
impl graphics::geometry::Renderer for Renderer {
type Geometry = Geometry;
type Frame = geometry::Frame;
fn new_frame(&self, size: Size) -> Self::Frame {
geometry::Frame::new(size)
}
fn draw_geometry(&mut self, geometry: Self::Geometry) {
let (layer, transformation) = self.layers.current_mut();
match geometry {
Geometry::Live { meshes, text } => {
layer.draw_mesh_group(meshes, transformation);
layer.draw_text_group(text, transformation);
}
Geometry::Cached(cache) => {
if let Some(meshes) = cache.meshes {
layer.draw_mesh_cache(meshes, transformation);
}
if let Some(text) = cache.text {
layer.draw_text_cache(text, transformation);
}
}
}
}
}
impl primitive::Renderer for Renderer {
fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_primitive(bounds, Box::new(primitive), transformation);
}
}
impl graphics::compositor::Default for crate::Renderer {
type Compositor = window::Compositor;
}

View file

@ -1,30 +1,95 @@
//! Draw using different graphical primitives.
pub mod pipeline;
pub use pipeline::Pipeline;
use crate::core::Rectangle;
use crate::graphics::{Damage, Mesh};
//! Draw custom primitives.
use crate::core::{self, Rectangle};
use crate::graphics::Viewport;
use rustc_hash::FxHashMap;
use std::any::{Any, TypeId};
use std::fmt::Debug;
/// The graphical primitives supported by `iced_wgpu`.
pub type Primitive = crate::graphics::Primitive<Custom>;
/// A batch of primitives.
pub type Batch = Vec<Instance>;
/// The custom primitives supported by `iced_wgpu`.
#[derive(Debug, Clone, PartialEq)]
pub enum Custom {
/// A mesh primitive.
Mesh(Mesh),
/// A custom pipeline primitive.
Pipeline(Pipeline),
/// A set of methods which allows a [`Primitive`] to be rendered.
pub trait Primitive: Debug + Send + Sync + 'static {
/// Processes the [`Primitive`], allowing for GPU buffer allocation.
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
storage: &mut Storage,
bounds: &Rectangle,
viewport: &Viewport,
);
/// Renders the [`Primitive`].
fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
storage: &Storage,
target: &wgpu::TextureView,
clip_bounds: &Rectangle<u32>,
);
}
impl Damage for Custom {
fn bounds(&self) -> Rectangle {
match self {
Self::Mesh(mesh) => mesh.bounds(),
Self::Pipeline(pipeline) => pipeline.bounds,
#[derive(Debug)]
/// An instance of a specific [`Primitive`].
pub struct Instance {
/// The bounds of the [`Instance`].
pub bounds: Rectangle,
/// The [`Primitive`] to render.
pub primitive: Box<dyn Primitive>,
}
impl Instance {
/// Creates a new [`Instance`] with the given [`Primitive`].
pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
Instance {
bounds,
primitive: Box::new(primitive),
}
}
}
/// A renderer than can draw custom primitives.
pub trait Renderer: core::Renderer {
/// Draws a custom primitive.
fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive);
}
/// Stores custom, user-provided types.
#[derive(Default, Debug)]
pub struct Storage {
pipelines: FxHashMap<TypeId, Box<dyn Any + Send>>,
}
impl Storage {
/// Returns `true` if `Storage` contains a type `T`.
pub fn has<T: 'static>(&self) -> bool {
self.pipelines.contains_key(&TypeId::of::<T>())
}
/// Inserts the data `T` in to [`Storage`].
pub fn store<T: 'static + Send>(&mut self, data: T) {
let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(data));
}
/// Returns a reference to the data with type `T` if it exists in [`Storage`].
pub fn get<T: 'static>(&self) -> Option<&T> {
self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| {
pipeline
.downcast_ref::<T>()
.expect("Value with this type does not exist in Storage.")
})
}
/// Returns a mutable reference to the data with type `T` if it exists in [`Storage`].
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| {
pipeline
.downcast_mut::<T>()
.expect("Value with this type does not exist in Storage.")
})
}
}

View file

@ -1,116 +0,0 @@
//! Draw primitives using custom pipelines.
use crate::core::{Rectangle, Size};
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
#[derive(Clone, Debug)]
/// A custom primitive which can be used to render primitives associated with a custom pipeline.
pub struct Pipeline {
/// The bounds of the [`Pipeline`].
pub bounds: Rectangle,
/// The [`Primitive`] to render.
pub primitive: Arc<dyn Primitive>,
}
impl Pipeline {
/// Creates a new [`Pipeline`] with the given [`Primitive`].
pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
Pipeline {
bounds,
primitive: Arc::new(primitive),
}
}
}
impl PartialEq for Pipeline {
fn eq(&self, other: &Self) -> bool {
self.primitive.type_id() == other.primitive.type_id()
}
}
/// A set of methods which allows a [`Primitive`] to be rendered.
pub trait Primitive: Debug + Send + Sync + 'static {
/// Processes the [`Primitive`], allowing for GPU buffer allocation.
fn prepare(
&self,
format: wgpu::TextureFormat,
device: &wgpu::Device,
queue: &wgpu::Queue,
bounds: Rectangle,
target_size: Size<u32>,
scale_factor: f32,
storage: &mut Storage,
);
/// Renders the [`Primitive`].
fn render(
&self,
storage: &Storage,
target: &wgpu::TextureView,
target_size: Size<u32>,
viewport: Rectangle<u32>,
encoder: &mut wgpu::CommandEncoder,
);
}
/// A renderer than can draw custom pipeline primitives.
pub trait Renderer: crate::core::Renderer {
/// Draws a custom pipeline primitive.
fn draw_pipeline_primitive(
&mut self,
bounds: Rectangle,
primitive: impl Primitive,
);
}
impl Renderer for crate::Renderer {
fn draw_pipeline_primitive(
&mut self,
bounds: Rectangle,
primitive: impl Primitive,
) {
self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline(
Pipeline::new(bounds, primitive),
)));
}
}
/// Stores custom, user-provided pipelines.
#[derive(Default, Debug)]
pub struct Storage {
pipelines: HashMap<TypeId, Box<dyn Any + Send>>,
}
impl Storage {
/// Returns `true` if `Storage` contains a pipeline with type `T`.
pub fn has<T: 'static>(&self) -> bool {
self.pipelines.get(&TypeId::of::<T>()).is_some()
}
/// Inserts the pipeline `T` in to [`Storage`].
pub fn store<T: 'static + Send>(&mut self, pipeline: T) {
let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline));
}
/// Returns a reference to pipeline with type `T` if it exists in [`Storage`].
pub fn get<T: 'static>(&self) -> Option<&T> {
self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| {
pipeline
.downcast_ref::<T>()
.expect("Pipeline with this type does not exist in Storage.")
})
}
/// Returns a mutable reference to pipeline `T` if it exists in [`Storage`].
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| {
pipeline
.downcast_mut::<T>()
.expect("Pipeline with this type does not exist in Storage.")
})
}
}

View file

@ -12,11 +12,37 @@ use bytemuck::{Pod, Zeroable};
use std::mem;
#[cfg(feature = "tracing")]
use tracing::info_span;
const INITIAL_INSTANCES: usize = 2_000;
/// The properties of a quad.
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct Quad {
/// The position of the [`Quad`].
pub position: [f32; 2],
/// The size of the [`Quad`].
pub size: [f32; 2],
/// The border color of the [`Quad`], in __linear RGB__.
pub border_color: color::Packed,
/// The border radii of the [`Quad`].
pub border_radius: [f32; 4],
/// The border width of the [`Quad`].
pub border_width: f32,
/// The shadow color of the [`Quad`].
pub shadow_color: color::Packed,
/// The shadow offset of the [`Quad`].
pub shadow_offset: [f32; 2],
/// The shadow blur radius of the [`Quad`].
pub shadow_blur_radius: f32,
}
#[derive(Debug)]
pub struct Pipeline {
solid: solid::Pipeline,
@ -57,7 +83,8 @@ impl Pipeline {
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
quads: &Batch,
transformation: Transformation,
scale: f32,
@ -67,7 +94,7 @@ impl Pipeline {
}
let layer = &mut self.layers[self.prepare_layer];
layer.prepare(device, queue, quads, transformation, scale);
layer.prepare(device, encoder, belt, quads, transformation, scale);
self.prepare_layer += 1;
}
@ -123,7 +150,7 @@ impl Pipeline {
}
#[derive(Debug)]
struct Layer {
pub struct Layer {
constants: wgpu::BindGroup,
constants_buffer: wgpu::Buffer,
solid: solid::Layer,
@ -162,56 +189,46 @@ impl Layer {
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
quads: &Batch,
transformation: Transformation,
scale: f32,
) {
#[cfg(feature = "tracing")]
let _ = info_span!("Wgpu::Quad", "PREPARE").entered();
self.update(device, encoder, belt, transformation, scale);
if !quads.solids.is_empty() {
self.solid.prepare(device, encoder, belt, &quads.solids);
}
if !quads.gradients.is_empty() {
self.gradient
.prepare(device, encoder, belt, &quads.gradients);
}
}
pub fn update(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
transformation: Transformation,
scale: f32,
) {
let uniforms = Uniforms::new(transformation, scale);
let bytes = bytemuck::bytes_of(&uniforms);
queue.write_buffer(
belt.write_buffer(
encoder,
&self.constants_buffer,
0,
bytemuck::bytes_of(&uniforms),
);
self.solid.prepare(device, queue, &quads.solids);
self.gradient.prepare(device, queue, &quads.gradients);
(bytes.len() as u64).try_into().expect("Sized uniforms"),
device,
)
.copy_from_slice(bytes);
}
}
/// The properties of a quad.
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct Quad {
/// The position of the [`Quad`].
pub position: [f32; 2],
/// The size of the [`Quad`].
pub size: [f32; 2],
/// The border color of the [`Quad`], in __linear RGB__.
pub border_color: color::Packed,
/// The border radii of the [`Quad`].
pub border_radius: [f32; 4],
/// The border width of the [`Quad`].
pub border_width: f32,
/// The shadow color of the [`Quad`].
pub shadow_color: [f32; 4],
/// The shadow offset of the [`Quad`].
pub shadow_offset: [f32; 2],
/// The shadow blur radius of the [`Quad`].
pub shadow_blur_radius: f32,
}
/// A group of [`Quad`]s rendered together.
#[derive(Default, Debug)]
pub struct Batch {
@ -221,10 +238,13 @@ pub struct Batch {
/// The gradient quads of the [`Layer`].
gradients: Vec<Gradient>,
/// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count.
order: Vec<(Kind, usize)>,
/// The quad order of the [`Layer`].
order: Order,
}
/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count.
type Order = Vec<(Kind, usize)>;
impl Batch {
/// Returns true if there are no quads of any type in [`Quads`].
pub fn is_empty(&self) -> bool {
@ -264,6 +284,12 @@ impl Batch {
}
}
}
pub fn clear(&mut self) {
self.solids.clear();
self.gradients.clear();
self.order.clear();
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]

View file

@ -46,11 +46,12 @@ impl Layer {
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
instances: &[Gradient],
) {
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(queue, 0, instances);
let _ = self.instances.write(device, encoder, belt, 0, instances);
self.instance_count = instances.len();
}

View file

@ -40,11 +40,12 @@ impl Layer {
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
instances: &[Solid],
) {
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(queue, 0, instances);
let _ = self.instances.write(device, encoder, belt, 0, instances);
self.instance_count = instances.len();
}

View file

@ -1,19 +1,19 @@
//! Configure a renderer.
use crate::core::{Font, Pixels};
use crate::graphics::Antialiasing;
use crate::graphics::{self, Antialiasing};
/// The settings of a [`Backend`].
/// The settings of a [`Renderer`].
///
/// [`Backend`]: crate::Backend
/// [`Renderer`]: crate::Renderer
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Settings {
/// The present mode of the [`Backend`].
/// The present mode of the [`Renderer`].
///
/// [`Backend`]: crate::Backend
/// [`Renderer`]: crate::Renderer
pub present_mode: wgpu::PresentMode,
/// The internal graphics backend to use.
pub internal_backend: wgpu::Backends,
/// The graphics backends to use.
pub backends: wgpu::Backends,
/// The default [`Font`] to use.
pub default_font: Font,
@ -29,38 +29,51 @@ pub struct Settings {
pub antialiasing: Option<Antialiasing>,
}
impl Settings {
/// Creates new [`Settings`] using environment configuration.
///
/// Specifically:
///
/// - The `internal_backend` can be configured using the `WGPU_BACKEND`
/// environment variable. If the variable is not set, the primary backend
/// will be used. The following values are allowed:
/// - `vulkan`
/// - `metal`
/// - `dx12`
/// - `dx11`
/// - `gl`
/// - `webgpu`
/// - `primary`
pub fn from_env() -> Self {
Settings {
internal_backend: wgpu::util::backend_bits_from_env()
.unwrap_or(wgpu::Backends::all()),
..Self::default()
}
}
}
impl Default for Settings {
fn default() -> Settings {
Settings {
present_mode: wgpu::PresentMode::AutoVsync,
internal_backend: wgpu::Backends::all(),
backends: wgpu::Backends::all(),
default_font: Font::default(),
default_text_size: Pixels(16.0),
antialiasing: None,
}
}
}
impl From<graphics::Settings> for Settings {
fn from(settings: graphics::Settings) -> Self {
Self {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
antialiasing: settings.antialiasing,
..Settings::default()
}
}
}
/// Obtains a [`wgpu::PresentMode`] from the current environment
/// configuration, if set.
///
/// The value returned by this function can be changed by setting
/// the `ICED_PRESENT_MODE` env variable. The possible values are:
///
/// - `vsync` → [`wgpu::PresentMode::AutoVsync`]
/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`]
/// - `immediate` → [`wgpu::PresentMode::Immediate`]
/// - `fifo` → [`wgpu::PresentMode::Fifo`]
/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`]
/// - `mailbox` → [`wgpu::PresentMode::Mailbox`]
pub fn present_mode_from_env() -> Option<wgpu::PresentMode> {
let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?;
match present_mode.to_lowercase().as_str() {
"vsync" => Some(wgpu::PresentMode::AutoVsync),
"no_vsync" => Some(wgpu::PresentMode::AutoNoVsync),
"immediate" => Some(wgpu::PresentMode::Immediate),
"fifo" => Some(wgpu::PresentMode::Fifo),
"fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed),
"mailbox" => Some(wgpu::PresentMode::Mailbox),
_ => None,
}
}

View file

@ -1,22 +1,14 @@
var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, -1.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(1.0, -1.0)
);
var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 0.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 1.0)
);
@group(0) @binding(0) var u_sampler: sampler;
@group(0) @binding(1) var<uniform> u_ratio: vec2<f32>;
@group(1) @binding(0) var u_texture: texture_2d<f32>;
struct VertexInput {
@ -30,9 +22,11 @@ struct VertexOutput {
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
let uv = uvs[input.vertex_index];
var out: VertexOutput;
out.uv = uvs[input.vertex_index];
out.position = vec4<f32>(positions[input.vertex_index], 0.0, 1.0);
out.uv = uv * u_ratio;
out.position = vec4<f32>(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0);
return out;
}

View file

@ -9,40 +9,55 @@ struct Globals {
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) pos: vec2<f32>,
@location(1) scale: vec2<f32>,
@location(2) atlas_pos: vec2<f32>,
@location(3) atlas_scale: vec2<f32>,
@location(4) layer: i32,
@location(1) center: vec2<f32>,
@location(2) scale: vec2<f32>,
@location(3) rotation: f32,
@location(4) opacity: f32,
@location(5) atlas_pos: vec2<f32>,
@location(6) atlas_scale: vec2<f32>,
@location(7) layer: i32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation.
@location(2) opacity: f32,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var out: VertexOutput;
let v_pos = vertex_position(input.vertex_index);
// Generate a vertex position in the range [0, 1] from the vertex index.
var v_pos = vertex_position(input.vertex_index);
// Map the vertex position to the atlas texture.
out.uv = vec2<f32>(v_pos * input.atlas_scale + input.atlas_pos);
out.layer = f32(input.layer);
out.opacity = input.opacity;
var transform: mat4x4<f32> = mat4x4<f32>(
vec4<f32>(input.scale.x, 0.0, 0.0, 0.0),
vec4<f32>(0.0, input.scale.y, 0.0, 0.0),
// Calculate the vertex position and move the center to the origin
v_pos = input.pos + v_pos * input.scale - input.center;
// Apply the rotation around the center of the image
let cos_rot = cos(input.rotation);
let sin_rot = sin(input.rotation);
let rotate = mat4x4<f32>(
vec4<f32>(cos_rot, sin_rot, 0.0, 0.0),
vec4<f32>(-sin_rot, cos_rot, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
vec4<f32>(input.pos, 0.0, 1.0)
vec4<f32>(0.0, 0.0, 0.0, 1.0)
);
out.position = globals.transform * transform * vec4<f32>(v_pos, 0.0, 1.0);
// Calculate the final position of the vertex
out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0));
return out;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(u_texture, u_sampler, input.uv, i32(input.layer));
// Sample the texture at the given UV coordinate and layer.
return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4<f32>(1.0, 1.0, 1.0, input.opacity);
}

View file

@ -22,7 +22,7 @@ fn rounded_box_sdf(to_center: vec2<f32>, size: vec2<f32>, radius: f32) -> f32 {
return length(max(abs(to_center) - size + vec2<f32>(radius, radius), vec2<f32>(0.0, 0.0))) - radius;
}
// Based on the fragement position and the center of the quad, select one of the 4 radi.
// Based on the fragment position and the center of the quad, select one of the 4 radi.
// Order matches CSS border radius attribute:
// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left
fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 {

View file

@ -107,13 +107,19 @@ fn solid_fs_main(
let quad_color = vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha);
if input.shadow_color.a > 0.0 {
let shadow_distance = rounded_box_sdf(input.position.xy - input.pos - input.shadow_offset - (input.scale / 2.0), input.scale / 2.0, border_radius);
let shadow_radius = select_border_radius(
input.border_radius,
input.position.xy - input.shadow_offset,
(input.pos + input.scale * 0.5).xy
);
let shadow_distance = max(rounded_box_sdf(input.position.xy - input.pos - input.shadow_offset - (input.scale / 2.0), input.scale / 2.0, shadow_radius), 0.);
let shadow_alpha = 1.0 - smoothstep(-input.shadow_blur_radius, input.shadow_blur_radius, shadow_distance);
let shadow_color = input.shadow_color;
let base_color = select(
let base_color = mix(
vec4<f32>(shadow_color.x, shadow_color.y, shadow_color.z, 0.0),
quad_color,
quad_color.a > 0.0
quad_color.a
);
return mix(base_color, shadow_color, (1.0 - radius_alpha) * shadow_alpha);

View file

@ -1,20 +1,285 @@
use crate::core::alignment;
use crate::core::{Rectangle, Size, Transformation};
use crate::graphics::cache;
use crate::graphics::color;
use crate::graphics::text::cache::{self, Cache};
use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache};
use crate::graphics::text::{font_system, to_color, Editor, Paragraph};
use crate::layer::Text;
use std::borrow::Cow;
use std::cell::RefCell;
use rustc_hash::FxHashMap;
use std::collections::hash_map;
use std::rc::{self, Rc};
use std::sync::atomic::{self, AtomicU64};
use std::sync::Arc;
pub use crate::graphics::Text;
const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION {
glyphon::ColorMode::Accurate
} else {
glyphon::ColorMode::Web
};
pub type Batch = Vec<Item>;
#[derive(Debug)]
pub enum Item {
Group {
transformation: Transformation,
text: Vec<Text>,
},
Cached {
transformation: Transformation,
cache: Cache,
},
}
#[derive(Debug, Clone)]
pub struct Cache {
id: Id,
group: cache::Group,
text: Rc<[Text]>,
version: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Id(u64);
impl Cache {
pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
if text.is_empty() {
return None;
}
Some(Self {
id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
group,
text: Rc::from(text),
version: 0,
})
}
pub fn update(&mut self, text: Vec<Text>) {
if self.text.is_empty() && text.is_empty() {
return;
}
self.text = Rc::from(text);
self.version += 1;
}
}
struct Upload {
renderer: glyphon::TextRenderer,
buffer_cache: BufferCache,
transformation: Transformation,
version: usize,
group_version: usize,
text: rc::Weak<[Text]>,
_atlas: rc::Weak<()>,
}
#[derive(Default)]
pub struct Storage {
groups: FxHashMap<cache::Group, Group>,
uploads: FxHashMap<Id, Upload>,
}
struct Group {
atlas: glyphon::TextAtlas,
version: usize,
should_trim: bool,
handle: Rc<()>, // Keeps track of active uploads
}
impl Storage {
pub fn new() -> Self {
Self::default()
}
fn get(&self, cache: &Cache) -> Option<(&glyphon::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: &glyphon::Viewport,
encoder: &mut wgpu::CommandEncoder,
format: wgpu::TextureFormat,
state: &glyphon::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: glyphon::TextAtlas::with_color_mode(
device, queue, state, format, COLOR_MODE,
),
version: 0,
should_trim: false,
handle: Rc::new(()),
}
});
match self.uploads.entry(cache.id) {
hash_map::Entry::Occupied(entry) => {
let upload = entry.into_mut();
if upload.version != cache.version
|| upload.group_version != group.version
|| upload.transformation != new_transformation
{
if !cache.text.is_empty() {
let _ = prepare(
device,
queue,
viewport,
encoder,
&mut upload.renderer,
&mut group.atlas,
&mut upload.buffer_cache,
&cache.text,
bounds,
new_transformation,
);
}
// Only trim if glyphs have changed
group.should_trim =
group.should_trim || upload.version != cache.version;
upload.text = Rc::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 = glyphon::TextRenderer::new(
&mut group.atlas,
device,
wgpu::MultisampleState::default(),
None,
);
let mut buffer_cache = BufferCache::new();
if !cache.text.is_empty() {
let _ = prepare(
device,
queue,
viewport,
encoder,
&mut renderer,
&mut group.atlas,
&mut buffer_cache,
&cache.text,
bounds,
new_transformation,
);
}
let _ = entry.insert(Upload {
renderer,
buffer_cache,
transformation: new_transformation,
version: 0,
group_version: group.version,
text: Rc::downgrade(&cache.text),
_atlas: Rc::downgrade(&group.handle),
});
group.should_trim = cache.group.is_singleton();
log::debug!(
"New text upload: {} (total: {})",
cache.id.0,
self.uploads.len()
);
}
}
}
pub fn trim(&mut self) {
self.uploads
.retain(|_id, upload| upload.text.strong_count() > 0);
self.groups.retain(|id, group| {
let active_uploads = Rc::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(glyphon::Viewport);
impl Viewport {
pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) {
self.0.update(
queue,
glyphon::Resolution {
width: resolution.width,
height: resolution.height,
},
);
}
}
#[allow(missing_debug_implementations)]
pub struct Pipeline {
renderers: Vec<glyphon::TextRenderer>,
state: glyphon::Cache,
format: wgpu::TextureFormat,
atlas: glyphon::TextAtlas,
renderers: Vec<glyphon::TextRenderer>,
prepare_layer: usize,
cache: RefCell<Cache>,
cache: BufferCache,
}
impl Pipeline {
@ -23,274 +288,102 @@ impl Pipeline {
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
) -> Self {
let state = glyphon::Cache::new(device);
let atlas = glyphon::TextAtlas::with_color_mode(
device, queue, &state, format, COLOR_MODE,
);
Pipeline {
state,
format,
renderers: Vec::new(),
atlas: glyphon::TextAtlas::with_color_mode(
device,
queue,
format,
if color::GAMMA_CORRECTION {
glyphon::ColorMode::Accurate
} else {
glyphon::ColorMode::Web
},
),
atlas,
prepare_layer: 0,
cache: RefCell::new(Cache::new()),
cache: BufferCache::new(),
}
}
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
font_system()
.write()
.expect("Write font system")
.load_font(bytes);
self.cache = RefCell::new(Cache::new());
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
sections: &[Text<'_>],
viewport: &Viewport,
encoder: &mut wgpu::CommandEncoder,
storage: &mut Storage,
batch: &Batch,
layer_bounds: Rectangle,
scale_factor: f32,
target_size: Size<u32>,
layer_transformation: Transformation,
) {
if self.renderers.len() <= self.prepare_layer {
self.renderers.push(glyphon::TextRenderer::new(
&mut self.atlas,
device,
wgpu::MultisampleState::default(),
None,
));
}
for item in batch {
match item {
Item::Group {
transformation,
text,
} => {
if self.renderers.len() <= self.prepare_layer {
self.renderers.push(glyphon::TextRenderer::new(
&mut self.atlas,
device,
wgpu::MultisampleState::default(),
None,
));
}
let mut font_system = font_system().write().expect("Write font system");
let font_system = font_system.raw();
let renderer = &mut self.renderers[self.prepare_layer];
let cache = self.cache.get_mut();
enum Allocation {
Paragraph(Paragraph),
Editor(Editor),
Cache(cache::KeyHash),
Raw(Arc<glyphon::Buffer>),
}
let allocations: Vec<_> = sections
.iter()
.map(|section| match section {
Text::Paragraph { paragraph, .. } => {
paragraph.upgrade().map(Allocation::Paragraph)
}
Text::Editor { editor, .. } => {
editor.upgrade().map(Allocation::Editor)
}
Text::Cached(text) => {
let (key, _) = cache.allocate(
font_system,
cache::Key {
content: text.content,
size: text.size.into(),
line_height: f32::from(
text.line_height.to_absolute(text.size),
),
font: text.font,
bounds: Size {
width: text.bounds.width,
height: text.bounds.height,
},
shaping: text.shaping,
},
let renderer = &mut self.renderers[self.prepare_layer];
let result = prepare(
device,
queue,
&viewport.0,
encoder,
renderer,
&mut self.atlas,
&mut self.cache,
text,
layer_bounds * layer_transformation,
layer_transformation * *transformation,
);
Some(Allocation::Cache(key))
match result {
Ok(()) => {
self.prepare_layer += 1;
}
Err(glyphon::PrepareError::AtlasFull) => {
// If the atlas cannot grow, then all bets are off.
// Instead of panicking, we will just pray that the result
// will be somewhat readable...
}
}
}
Text::Raw { raw, .. } => {
raw.buffer.upgrade().map(Allocation::Raw)
}
})
.collect();
let layer_bounds = layer_bounds * scale_factor;
let text_areas = sections.iter().zip(allocations.iter()).filter_map(
|(section, allocation)| {
let (
buffer,
bounds,
horizontal_alignment,
vertical_alignment,
color,
clip_bounds,
Item::Cached {
transformation,
) = match section {
Text::Paragraph {
position,
color,
clip_bounds,
transformation,
..
} => {
use crate::core::text::Paragraph as _;
let Some(Allocation::Paragraph(paragraph)) = allocation
else {
return None;
};
(
paragraph.buffer(),
Rectangle::new(*position, paragraph.min_bounds()),
paragraph.horizontal_alignment(),
paragraph.vertical_alignment(),
*color,
*clip_bounds,
*transformation,
)
}
Text::Editor {
position,
color,
clip_bounds,
transformation,
..
} => {
use crate::core::text::Editor as _;
let Some(Allocation::Editor(editor)) = allocation
else {
return None;
};
(
editor.buffer(),
Rectangle::new(*position, editor.bounds()),
alignment::Horizontal::Left,
alignment::Vertical::Top,
*color,
*clip_bounds,
*transformation,
)
}
Text::Cached(text) => {
let Some(Allocation::Cache(key)) = allocation else {
return None;
};
let entry = cache.get(key).expect("Get cached buffer");
(
&entry.buffer,
Rectangle::new(
text.bounds.position(),
entry.min_bounds,
),
text.horizontal_alignment,
text.vertical_alignment,
text.color,
text.clip_bounds,
Transformation::IDENTITY,
)
}
Text::Raw {
raw,
transformation,
} => {
let Some(Allocation::Raw(buffer)) = allocation else {
return None;
};
let (width, height) = buffer.size();
(
buffer.as_ref(),
Rectangle::new(
raw.position,
Size::new(width, height),
),
alignment::Horizontal::Left,
alignment::Vertical::Top,
raw.color,
raw.clip_bounds,
*transformation,
)
}
};
let bounds = bounds * transformation * scale_factor;
let left = match horizontal_alignment {
alignment::Horizontal::Left => bounds.x,
alignment::Horizontal::Center => {
bounds.x - bounds.width / 2.0
}
alignment::Horizontal::Right => bounds.x - bounds.width,
};
let top = match vertical_alignment {
alignment::Vertical::Top => bounds.y,
alignment::Vertical::Center => {
bounds.y - bounds.height / 2.0
}
alignment::Vertical::Bottom => bounds.y - bounds.height,
};
let clip_bounds = layer_bounds.intersection(
&(clip_bounds * transformation * scale_factor),
)?;
Some(glyphon::TextArea {
buffer,
left,
top,
scale: scale_factor * transformation.scale_factor(),
bounds: glyphon::TextBounds {
left: clip_bounds.x as i32,
top: clip_bounds.y as i32,
right: (clip_bounds.x + clip_bounds.width) as i32,
bottom: (clip_bounds.y + clip_bounds.height) as i32,
},
default_color: to_color(color),
})
},
);
let result = renderer.prepare(
device,
queue,
font_system,
&mut self.atlas,
glyphon::Resolution {
width: target_size.width,
height: target_size.height,
},
text_areas,
&mut glyphon::SwashCache::new(),
);
match result {
Ok(()) => {
self.prepare_layer += 1;
}
Err(glyphon::PrepareError::AtlasFull) => {
// If the atlas cannot grow, then all bets are off.
// Instead of panicking, we will just pray that the result
// will be somewhat readable...
cache,
} => {
storage.prepare(
device,
queue,
&viewport.0,
encoder,
self.format,
&self.state,
cache,
layer_transformation * *transformation,
layer_bounds * layer_transformation,
);
}
}
}
}
pub fn render<'a>(
&'a self,
layer: usize,
viewport: &'a Viewport,
storage: &'a Storage,
start: usize,
batch: &'a Batch,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
) {
let renderer = &self.renderers[layer];
) -> usize {
let mut layer_count = 0;
render_pass.set_scissor_rect(
bounds.x,
@ -299,15 +392,252 @@ impl Pipeline {
bounds.height,
);
renderer
.render(&self.atlas, render_pass)
.expect("Render text");
for item in batch {
match item {
Item::Group { .. } => {
let renderer = &self.renderers[start + layer_count];
renderer
.render(&self.atlas, &viewport.0, render_pass)
.expect("Render text");
layer_count += 1;
}
Item::Cached { cache, .. } => {
if let Some((atlas, upload)) = storage.get(cache) {
upload
.renderer
.render(atlas, &viewport.0, render_pass)
.expect("Render cached text");
}
}
}
}
layer_count
}
pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport {
Viewport(glyphon::Viewport::new(device, &self.state))
}
pub fn end_frame(&mut self) {
self.atlas.trim();
self.cache.get_mut().trim();
self.cache.trim();
self.prepare_layer = 0;
}
}
fn prepare(
device: &wgpu::Device,
queue: &wgpu::Queue,
viewport: &glyphon::Viewport,
encoder: &mut wgpu::CommandEncoder,
renderer: &mut glyphon::TextRenderer,
atlas: &mut glyphon::TextAtlas,
buffer_cache: &mut BufferCache,
sections: &[Text],
layer_bounds: Rectangle,
layer_transformation: Transformation,
) -> Result<(), glyphon::PrepareError> {
let mut font_system = font_system().write().expect("Write font system");
let font_system = font_system.raw();
enum Allocation {
Paragraph(Paragraph),
Editor(Editor),
Cache(text_cache::KeyHash),
Raw(Arc<glyphon::Buffer>),
}
let allocations: Vec<_> = sections
.iter()
.map(|section| match section {
Text::Paragraph { paragraph, .. } => {
paragraph.upgrade().map(Allocation::Paragraph)
}
Text::Editor { editor, .. } => {
editor.upgrade().map(Allocation::Editor)
}
Text::Cached {
content,
bounds,
size,
line_height,
font,
shaping,
..
} => {
let (key, _) = buffer_cache.allocate(
font_system,
text_cache::Key {
content,
size: f32::from(*size),
line_height: f32::from(*line_height),
font: *font,
bounds: Size {
width: bounds.width,
height: bounds.height,
},
shaping: *shaping,
},
);
Some(Allocation::Cache(key))
}
Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw),
})
.collect();
let text_areas = sections.iter().zip(allocations.iter()).filter_map(
|(section, allocation)| {
let (
buffer,
bounds,
horizontal_alignment,
vertical_alignment,
color,
clip_bounds,
transformation,
) = match section {
Text::Paragraph {
position,
color,
clip_bounds,
transformation,
..
} => {
use crate::core::text::Paragraph as _;
let Some(Allocation::Paragraph(paragraph)) = allocation
else {
return None;
};
(
paragraph.buffer(),
Rectangle::new(*position, paragraph.min_bounds()),
paragraph.horizontal_alignment(),
paragraph.vertical_alignment(),
*color,
*clip_bounds,
*transformation,
)
}
Text::Editor {
position,
color,
clip_bounds,
transformation,
..
} => {
use crate::core::text::Editor as _;
let Some(Allocation::Editor(editor)) = allocation else {
return None;
};
(
editor.buffer(),
Rectangle::new(*position, editor.bounds()),
alignment::Horizontal::Left,
alignment::Vertical::Top,
*color,
*clip_bounds,
*transformation,
)
}
Text::Cached {
bounds,
horizontal_alignment,
vertical_alignment,
color,
clip_bounds,
..
} => {
let Some(Allocation::Cache(key)) = allocation else {
return None;
};
let entry =
buffer_cache.get(key).expect("Get cached buffer");
(
&entry.buffer,
Rectangle::new(bounds.position(), entry.min_bounds),
*horizontal_alignment,
*vertical_alignment,
*color,
*clip_bounds,
Transformation::IDENTITY,
)
}
Text::Raw {
raw,
transformation,
} => {
let Some(Allocation::Raw(buffer)) = allocation else {
return None;
};
let (width, height) = buffer.size();
(
buffer.as_ref(),
Rectangle::new(raw.position, Size::new(width, height)),
alignment::Horizontal::Left,
alignment::Vertical::Top,
raw.color,
raw.clip_bounds,
*transformation,
)
}
};
let bounds = bounds * transformation * layer_transformation;
let left = match horizontal_alignment {
alignment::Horizontal::Left => bounds.x,
alignment::Horizontal::Center => bounds.x - bounds.width / 2.0,
alignment::Horizontal::Right => bounds.x - bounds.width,
};
let top = match vertical_alignment {
alignment::Vertical::Top => bounds.y,
alignment::Vertical::Center => bounds.y - bounds.height / 2.0,
alignment::Vertical::Bottom => bounds.y - bounds.height,
};
let clip_bounds = layer_bounds.intersection(
&(clip_bounds * transformation * layer_transformation),
)?;
Some(glyphon::TextArea {
buffer,
left,
top,
scale: transformation.scale_factor()
* layer_transformation.scale_factor(),
bounds: glyphon::TextBounds {
left: clip_bounds.x as i32,
top: clip_bounds.y as i32,
right: (clip_bounds.x + clip_bounds.width) as i32,
bottom: (clip_bounds.y + clip_bounds.height) as i32,
},
default_color: to_color(color),
})
},
);
renderer.prepare(
device,
queue,
encoder,
font_system,
atlas,
viewport,
text_areas,
&mut glyphon::SwashCache::new(),
)
}

View file

@ -1,14 +1,158 @@
//! Draw meshes of triangles.
mod msaa;
use crate::core::{Size, Transformation};
use crate::core::{Rectangle, Size, Transformation};
use crate::graphics::mesh::{self, Mesh};
use crate::graphics::Antialiasing;
use crate::layer::mesh::{self, Mesh};
use crate::Buffer;
use rustc_hash::FxHashMap;
use std::collections::hash_map;
use std::rc::{self, Rc};
use std::sync::atomic::{self, AtomicU64};
const INITIAL_INDEX_COUNT: usize = 1_000;
const INITIAL_VERTEX_COUNT: usize = 1_000;
pub type Batch = Vec<Item>;
#[derive(Debug)]
pub enum Item {
Group {
transformation: Transformation,
meshes: Vec<Mesh>,
},
Cached {
transformation: Transformation,
cache: Cache,
},
}
#[derive(Debug, Clone)]
pub struct Cache {
id: Id,
batch: Rc<[Mesh]>,
version: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Id(u64);
impl Cache {
pub fn new(meshes: Vec<Mesh>) -> Option<Self> {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
if meshes.is_empty() {
return None;
}
Some(Self {
id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
batch: Rc::from(meshes),
version: 0,
})
}
pub fn update(&mut self, meshes: Vec<Mesh>) {
self.batch = Rc::from(meshes);
self.version += 1;
}
}
#[derive(Debug)]
struct Upload {
layer: Layer,
transformation: Transformation,
version: usize,
batch: rc::Weak<[Mesh]>,
}
#[derive(Debug, Default)]
pub struct Storage {
uploads: FxHashMap<Id, Upload>,
}
impl Storage {
pub fn new() -> Self {
Self::default()
}
fn get(&self, cache: &Cache) -> Option<&Upload> {
if cache.batch.is_empty() {
return None;
}
self.uploads.get(&cache.id)
}
fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
cache: &Cache,
new_transformation: Transformation,
) {
match self.uploads.entry(cache.id) {
hash_map::Entry::Occupied(entry) => {
let upload = entry.into_mut();
if !cache.batch.is_empty()
&& (upload.version != cache.version
|| upload.transformation != new_transformation)
{
upload.layer.prepare(
device,
encoder,
belt,
solid,
gradient,
&cache.batch,
new_transformation,
);
upload.batch = Rc::downgrade(&cache.batch);
upload.version = cache.version;
upload.transformation = new_transformation;
}
}
hash_map::Entry::Vacant(entry) => {
let mut layer = Layer::new(device, solid, gradient);
layer.prepare(
device,
encoder,
belt,
solid,
gradient,
&cache.batch,
new_transformation,
);
let _ = entry.insert(Upload {
layer,
transformation: new_transformation,
version: 0,
batch: Rc::downgrade(&cache.batch),
});
log::debug!(
"New mesh upload: {} (total: {})",
cache.id.0,
self.uploads.len()
);
}
}
}
pub fn trim(&mut self) {
self.uploads
.retain(|_id, upload| upload.batch.strong_count() > 0);
}
}
#[derive(Debug)]
pub struct Pipeline {
blit: Option<msaa::Blit>,
@ -18,8 +162,198 @@ pub struct Pipeline {
prepare_layer: usize,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>,
) -> Pipeline {
Pipeline {
blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
solid: solid::Pipeline::new(device, format, antialiasing),
gradient: gradient::Pipeline::new(device, format, antialiasing),
layers: Vec::new(),
prepare_layer: 0,
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
storage: &mut Storage,
items: &[Item],
scale: Transformation,
target_size: Size<u32>,
) {
let projection = if let Some(blit) = &mut self.blit {
blit.prepare(device, encoder, belt, target_size) * scale
} else {
Transformation::orthographic(target_size.width, target_size.height)
* scale
};
for item in items {
match item {
Item::Group {
transformation,
meshes,
} => {
if self.layers.len() <= self.prepare_layer {
self.layers.push(Layer::new(
device,
&self.solid,
&self.gradient,
));
}
let layer = &mut self.layers[self.prepare_layer];
layer.prepare(
device,
encoder,
belt,
&self.solid,
&self.gradient,
meshes,
projection * *transformation,
);
self.prepare_layer += 1;
}
Item::Cached {
transformation,
cache,
} => {
storage.prepare(
device,
encoder,
belt,
&self.solid,
&self.gradient,
cache,
projection * *transformation,
);
}
}
}
}
pub fn render(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
storage: &Storage,
start: usize,
batch: &Batch,
bounds: Rectangle,
screen_transformation: Transformation,
) -> usize {
let mut layer_count = 0;
let items = batch.iter().filter_map(|item| match item {
Item::Group {
transformation,
meshes,
} => {
let layer = &self.layers[start + layer_count];
layer_count += 1;
Some((
layer,
meshes.as_slice(),
screen_transformation * *transformation,
))
}
Item::Cached {
transformation,
cache,
} => {
let upload = storage.get(cache)?;
Some((
&upload.layer,
&cache.batch,
screen_transformation * *transformation,
))
}
});
render(
encoder,
target,
self.blit.as_mut(),
&self.solid,
&self.gradient,
bounds,
items,
);
layer_count
}
pub fn end_frame(&mut self) {
self.prepare_layer = 0;
}
}
fn render<'a>(
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
mut blit: Option<&mut msaa::Blit>,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
bounds: Rectangle,
group: impl Iterator<Item = (&'a Layer, &'a [Mesh], Transformation)>,
) {
{
let (attachment, resolve_target, load) = if let Some(blit) = &mut blit {
let (attachment, resolve_target) = blit.targets();
(
attachment,
Some(resolve_target),
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
)
} else {
(target, None, wgpu::LoadOp::Load)
};
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu.triangle.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: attachment,
resolve_target,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
for (layer, meshes, transformation) in group {
layer.render(
solid,
gradient,
meshes,
bounds,
transformation,
&mut render_pass,
);
}
}
if let Some(blit) = blit {
blit.draw(encoder, target);
}
}
#[derive(Debug)]
struct Layer {
pub struct Layer {
index_buffer: Buffer<u32>,
index_strides: Vec<u32>,
solid: solid::Layer,
@ -48,10 +382,11 @@ impl Layer {
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
meshes: &[Mesh<'_>],
meshes: &[Mesh],
transformation: Transformation,
) {
// Count the total amount of vertices & indices we need to handle
@ -103,33 +438,47 @@ impl Layer {
let uniforms =
Uniforms::new(transformation * mesh.transformation());
index_offset +=
self.index_buffer.write(queue, index_offset, indices);
index_offset += self.index_buffer.write(
device,
encoder,
belt,
index_offset,
indices,
);
self.index_strides.push(indices.len() as u32);
match mesh {
Mesh::Solid { buffers, .. } => {
solid_vertex_offset += self.solid.vertices.write(
queue,
device,
encoder,
belt,
solid_vertex_offset,
&buffers.vertices,
);
solid_uniform_offset += self.solid.uniforms.write(
queue,
device,
encoder,
belt,
solid_uniform_offset,
&[uniforms],
);
}
Mesh::Gradient { buffers, .. } => {
gradient_vertex_offset += self.gradient.vertices.write(
queue,
device,
encoder,
belt,
gradient_vertex_offset,
&buffers.vertices,
);
gradient_uniform_offset += self.gradient.uniforms.write(
queue,
device,
encoder,
belt,
gradient_uniform_offset,
&[uniforms],
);
@ -142,8 +491,9 @@ impl Layer {
&'a self,
solid: &'a solid::Pipeline,
gradient: &'a gradient::Pipeline,
meshes: &[Mesh<'_>],
scale_factor: f32,
meshes: &[Mesh],
bounds: Rectangle,
transformation: Transformation,
render_pass: &mut wgpu::RenderPass<'a>,
) {
let mut num_solids = 0;
@ -151,11 +501,12 @@ impl Layer {
let mut last_is_solid = None;
for (index, mesh) in meshes.iter().enumerate() {
let clip_bounds = (mesh.clip_bounds() * scale_factor).snap();
if clip_bounds.width < 1 || clip_bounds.height < 1 {
let Some(clip_bounds) = bounds
.intersection(&(mesh.clip_bounds() * transformation))
.and_then(Rectangle::snap)
else {
continue;
}
};
render_pass.set_scissor_rect(
clip_bounds.x,
@ -219,117 +570,6 @@ impl Layer {
}
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>,
) -> Pipeline {
Pipeline {
blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
solid: solid::Pipeline::new(device, format, antialiasing),
gradient: gradient::Pipeline::new(device, format, antialiasing),
layers: Vec::new(),
prepare_layer: 0,
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
meshes: &[Mesh<'_>],
transformation: Transformation,
) {
#[cfg(feature = "tracing")]
let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered();
if self.layers.len() <= self.prepare_layer {
self.layers
.push(Layer::new(device, &self.solid, &self.gradient));
}
let layer = &mut self.layers[self.prepare_layer];
layer.prepare(
device,
queue,
&self.solid,
&self.gradient,
meshes,
transformation,
);
self.prepare_layer += 1;
}
pub fn render(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
layer: usize,
target_size: Size<u32>,
meshes: &[Mesh<'_>],
scale_factor: f32,
) {
#[cfg(feature = "tracing")]
let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered();
{
let (attachment, resolve_target, load) = if let Some(blit) =
&mut self.blit
{
let (attachment, resolve_target) =
blit.targets(device, target_size.width, target_size.height);
(
attachment,
Some(resolve_target),
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
)
} else {
(target, None, wgpu::LoadOp::Load)
};
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu.triangle.render_pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: attachment,
resolve_target,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
let layer = &mut self.layers[layer];
layer.render(
&self.solid,
&self.gradient,
meshes,
scale_factor,
&mut render_pass,
);
}
if let Some(blit) = &mut self.blit {
blit.draw(encoder, target);
}
}
pub fn end_frame(&mut self) {
self.prepare_layer = 0;
}
}
fn fragment_target(
texture_format: wgpu::TextureFormat,
) -> wgpu::ColorTargetState {

View file

@ -1,13 +1,18 @@
use crate::core::{Size, Transformation};
use crate::graphics;
use std::num::NonZeroU64;
#[derive(Debug)]
pub struct Blit {
format: wgpu::TextureFormat,
pipeline: wgpu::RenderPipeline,
constants: wgpu::BindGroup,
ratio: wgpu::Buffer,
texture_layout: wgpu::BindGroupLayout,
sample_count: u32,
targets: Option<Targets>,
last_region: Option<Size<u32>>,
}
impl Blit {
@ -19,27 +24,52 @@ impl Blit {
let sampler =
device.create_sampler(&wgpu::SamplerDescriptor::default());
let ratio = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced-wgpu::triangle::msaa ratio"),
size: std::mem::size_of::<Ratio>() as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
mapped_at_creation: false,
});
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::triangle:msaa uniforms layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
}],
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::triangle::msaa uniforms bind group"),
layout: &constant_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
}],
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: ratio.as_entire_binding(),
},
],
});
let texture_layout =
@ -112,43 +142,61 @@ impl Blit {
format,
pipeline,
constants: constant_bind_group,
ratio,
texture_layout,
sample_count: antialiasing.sample_count(),
targets: None,
last_region: None,
}
}
pub fn targets(
pub fn prepare(
&mut self,
device: &wgpu::Device,
width: u32,
height: u32,
) -> (&wgpu::TextureView, &wgpu::TextureView) {
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
region_size: Size<u32>,
) -> Transformation {
match &mut self.targets {
None => {
Some(targets)
if region_size.width <= targets.size.width
&& region_size.height <= targets.size.height => {}
_ => {
self.targets = Some(Targets::new(
device,
self.format,
&self.texture_layout,
self.sample_count,
width,
height,
region_size,
));
}
Some(targets) => {
if targets.width != width || targets.height != height {
self.targets = Some(Targets::new(
device,
self.format,
&self.texture_layout,
self.sample_count,
width,
height,
));
}
}
}
let targets = self.targets.as_mut().unwrap();
if Some(region_size) != self.last_region {
let ratio = Ratio {
u: region_size.width as f32 / targets.size.width as f32,
v: region_size.height as f32 / targets.size.height as f32,
};
belt.write_buffer(
encoder,
&self.ratio,
0,
NonZeroU64::new(std::mem::size_of::<Ratio>() as u64)
.expect("non-empty ratio"),
device,
)
.copy_from_slice(bytemuck::bytes_of(&ratio));
self.last_region = Some(region_size);
}
Transformation::orthographic(targets.size.width, targets.size.height)
}
pub fn targets(&self) -> (&wgpu::TextureView, &wgpu::TextureView) {
let targets = self.targets.as_ref().unwrap();
(&targets.attachment, &targets.resolve)
@ -191,8 +239,7 @@ struct Targets {
attachment: wgpu::TextureView,
resolve: wgpu::TextureView,
bind_group: wgpu::BindGroup,
width: u32,
height: u32,
size: Size<u32>,
}
impl Targets {
@ -201,12 +248,11 @@ impl Targets {
format: wgpu::TextureFormat,
texture_layout: &wgpu::BindGroupLayout,
sample_count: u32,
width: u32,
height: u32,
size: Size<u32>,
) -> Targets {
let extent = wgpu::Extent3d {
width,
height,
width: size.width,
height: size.height,
depth_or_array_layers: 1,
};
@ -252,8 +298,14 @@ impl Targets {
attachment,
resolve,
bind_group,
width,
height,
size,
}
}
}
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Ratio {
u: f32,
v: f32,
}

View file

@ -1,21 +1,49 @@
//! Connect a window with a renderer.
use crate::core::{Color, Size};
use crate::graphics;
use crate::graphics::color;
use crate::graphics::compositor;
use crate::graphics::{Error, Viewport};
use crate::{Backend, Primitive, Renderer, Settings};
use crate::graphics::error;
use crate::graphics::{self, Viewport};
use crate::settings::{self, Settings};
use crate::{Engine, Renderer};
/// A window graphics backend for iced powered by `wgpu`.
#[allow(missing_debug_implementations)]
pub struct Compositor {
settings: Settings,
instance: wgpu::Instance,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
format: wgpu::TextureFormat,
alpha_mode: wgpu::CompositeAlphaMode,
engine: Engine,
settings: Settings,
}
/// A compositor error.
#[derive(Debug, Clone, thiserror::Error)]
pub enum Error {
/// The surface creation failed.
#[error("the surface creation failed: {0}")]
SurfaceCreationFailed(#[from] wgpu::CreateSurfaceError),
/// The surface is not compatible.
#[error("the surface is not compatible")]
IncompatibleSurface,
/// No adapter was found for the options requested.
#[error("no adapter was found for the options requested: {0:?}")]
NoAdapterFound(String),
/// No device request succeeded.
#[error("no device request succeeded: {0:?}")]
RequestDeviceFailed(Vec<(wgpu::Limits, wgpu::RequestDeviceError)>),
}
impl From<Error> for graphics::Error {
fn from(error: Error) -> Self {
Self::GraphicsAdapterNotFound {
backend: "wgpu",
reason: error::Reason::RequestFailed(error.to_string()),
}
}
}
impl Compositor {
@ -25,9 +53,9 @@ impl Compositor {
pub async fn request<W: compositor::Window>(
settings: Settings,
compatible_window: Option<W>,
) -> Option<Self> {
) -> Result<Self, Error> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: settings.internal_backend,
backends: settings.backends,
..Default::default()
});
@ -36,7 +64,7 @@ impl Compositor {
#[cfg(not(target_arch = "wasm32"))]
if log::max_level() >= log::LevelFilter::Info {
let available_adapters: Vec<_> = instance
.enumerate_adapters(settings.internal_backend)
.enumerate_adapters(settings.backends)
.iter()
.map(wgpu::Adapter::get_info)
.collect();
@ -47,23 +75,27 @@ impl Compositor {
let compatible_surface = compatible_window
.and_then(|window| instance.create_surface(window).ok());
let adapter_options = wgpu::RequestAdapterOptions {
power_preference: wgpu::util::power_preference_from_env()
.unwrap_or(if settings.antialiasing.is_none() {
wgpu::PowerPreference::LowPower
} else {
wgpu::PowerPreference::HighPerformance
}),
compatible_surface: compatible_surface.as_ref(),
force_fallback_adapter: false,
};
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::util::power_preference_from_env()
.unwrap_or(if settings.antialiasing.is_none() {
wgpu::PowerPreference::LowPower
} else {
wgpu::PowerPreference::HighPerformance
}),
compatible_surface: compatible_surface.as_ref(),
force_fallback_adapter: false,
})
.await?;
.request_adapter(&adapter_options)
.await
.ok_or(Error::NoAdapterFound(format!("{:?}", adapter_options)))?;
log::info!("Selected: {:#?}", adapter.get_info());
let (format, alpha_mode) =
compatible_surface.as_ref().and_then(|surface| {
let (format, alpha_mode) = compatible_surface
.as_ref()
.and_then(|surface| {
let capabilities = surface.get_capabilities(&adapter);
let mut formats = capabilities.formats.iter().copied();
@ -90,12 +122,17 @@ impl Compositor {
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else if alpha_modes
.contains(&wgpu::CompositeAlphaMode::PreMultiplied)
{
wgpu::CompositeAlphaMode::PreMultiplied
} else {
wgpu::CompositeAlphaMode::Auto
};
format.zip(Some(preferred_alpha))
})?;
})
.ok_or(Error::IncompatibleSurface)?;
log::info!(
"Selected format: {format:?} with alpha mode: {alpha_mode:?}"
@ -109,74 +146,71 @@ impl Compositor {
let limits =
[wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()];
let mut limits = limits.into_iter().map(|limits| wgpu::Limits {
let limits = limits.into_iter().map(|limits| wgpu::Limits {
max_bind_groups: 2,
..limits
});
let (device, queue) =
loop {
let required_limits = limits.next()?;
let device = adapter.request_device(
let mut errors = Vec::new();
for required_limits in limits {
let result = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: Some(
"iced_wgpu::window::compositor device descriptor",
),
required_features: wgpu::Features::empty(),
required_limits,
required_limits: required_limits.clone(),
},
None,
).await.ok();
)
.await;
if let Some(device) = device {
break Some(device);
match result {
Ok((device, queue)) => {
let engine = Engine::new(
&adapter,
&device,
&queue,
format,
settings.antialiasing,
);
return Ok(Compositor {
instance,
adapter,
device,
queue,
format,
alpha_mode,
engine,
settings,
});
}
}?;
Err(error) => {
errors.push((required_limits, error));
}
}
}
Some(Compositor {
instance,
settings,
adapter,
device,
queue,
format,
alpha_mode,
})
}
/// Creates a new rendering [`Backend`] for this [`Compositor`].
pub fn create_backend(&self) -> Backend {
Backend::new(
&self.adapter,
&self.device,
&self.queue,
self.settings,
self.format,
)
Err(Error::RequestDeviceFailed(errors))
}
}
/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and
/// window.
pub fn new<W: compositor::Window>(
/// Creates a [`Compositor`] with the given [`Settings`] and window.
pub async fn new<W: compositor::Window>(
settings: Settings,
compatible_window: W,
) -> Result<Compositor, Error> {
let compositor = futures::executor::block_on(Compositor::request(
settings,
Some(compatible_window),
))
.ok_or(Error::GraphicsAdapterNotFound)?;
Ok(compositor)
Compositor::request(settings, Some(compatible_window)).await
}
/// Presents the given primitives with the given [`Compositor`] and [`Backend`].
/// Presents the given primitives with the given [`Compositor`].
pub fn present(
compositor: &mut Compositor,
backend: &mut Backend,
renderer: &mut Renderer,
surface: &mut wgpu::Surface<'static>,
primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
) -> Result<(), compositor::SurfaceError> {
@ -192,19 +226,20 @@ pub fn present(
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
backend.present(
renderer.present(
&mut compositor.engine,
&compositor.device,
&compositor.queue,
&mut encoder,
Some(background_color),
frame.texture.format(),
view,
primitives,
viewport,
);
// Submit work
let _submission = compositor.queue.submit(Some(encoder.finish()));
let _ = compositor.engine.submit(&compositor.queue, encoder);
// Present the frame
frame.present();
Ok(())
@ -225,20 +260,41 @@ pub fn present(
}
impl graphics::Compositor for Compositor {
type Settings = Settings;
type Renderer = Renderer;
type Surface = wgpu::Surface<'static>;
fn new<W: compositor::Window>(
settings: Self::Settings,
async fn with_backend<W: compositor::Window>(
settings: graphics::Settings,
compatible_window: W,
) -> Result<Self, Error> {
new(settings, compatible_window)
backend: Option<&str>,
) -> Result<Self, graphics::Error> {
match backend {
None | Some("wgpu") => {
let mut settings = Settings::from(settings);
if let Some(backends) = wgpu::util::backend_bits_from_env() {
settings.backends = backends;
}
if let Some(present_mode) = settings::present_mode_from_env() {
settings.present_mode = present_mode;
}
Ok(new(settings, compatible_window).await?)
}
Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound {
backend: "wgpu",
reason: error::Reason::DidNotMatch {
preferred_backend: backend.to_owned(),
},
}),
}
}
fn create_renderer(&self) -> Self::Renderer {
Renderer::new(
self.create_backend(),
&self.device,
&self.engine,
self.settings.default_font,
self.settings.default_text_size,
)
@ -278,7 +334,7 @@ impl graphics::Compositor for Compositor {
height,
alpha_mode: self.alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 2,
desired_maximum_frame_latency: 1,
},
);
}
@ -299,16 +355,7 @@ impl graphics::Compositor for Compositor {
viewport: &Viewport,
background_color: Color,
) -> Result<(), compositor::SurfaceError> {
renderer.with_primitives(|backend, primitives| {
present(
self,
backend,
surface,
primitives,
viewport,
background_color,
)
})
present(self, renderer, surface, viewport, background_color)
}
fn screenshot(
@ -318,9 +365,7 @@ impl graphics::Compositor for Compositor {
viewport: &Viewport,
background_color: Color,
) -> Vec<u8> {
renderer.with_primitives(|backend, primitives| {
screenshot(self, backend, primitives, viewport, background_color)
})
screenshot(self, renderer, viewport, background_color)
}
}
@ -328,18 +373,11 @@ impl graphics::Compositor for Compositor {
///
/// Returns RGBA bytes of the texture data.
pub fn screenshot(
compositor: &Compositor,
backend: &mut Backend,
primitives: &[Primitive],
compositor: &mut Compositor,
renderer: &mut Renderer,
viewport: &Viewport,
background_color: Color,
) -> Vec<u8> {
let mut encoder = compositor.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("iced_wgpu.offscreen.encoder"),
},
);
let dimensions = BufferDimensions::new(viewport.physical_size());
let texture_extent = wgpu::Extent3d {
@ -363,14 +401,20 @@ pub fn screenshot(
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
backend.present(
let mut encoder = compositor.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("iced_wgpu.offscreen.encoder"),
},
);
renderer.present(
&mut compositor.engine,
&compositor.device,
&compositor.queue,
&mut encoder,
Some(background_color),
texture.format(),
&view,
primitives,
viewport,
);
@ -407,7 +451,7 @@ pub fn screenshot(
texture_extent,
);
let index = compositor.queue.submit(Some(encoder.finish()));
let index = compositor.engine.submit(&compositor.queue, encoder);
let slice = output_buffer.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});