iced-yoda/wgpu/src/lib.rs
2025-08-12 22:26:43 +02:00

883 lines
27 KiB
Rust

//! A [`wgpu`] renderer for [Iced].
//!
//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
//!
//! [`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX11, and
//! DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the
//! incoming [WebGPU API].
//!
//! Currently, `iced_wgpu` supports the following primitives:
//! - Text, which is rendered using [`glyphon`].
//! - Quads or rectangles, with rounded borders and a solid background color.
//! - Clip areas, useful to implement scrollables or hide overflowing content.
//! - Images and SVG, loaded from memory or the file system.
//! - Meshes of triangles, useful to draw geometry freely.
//!
//! [Iced]: https://github.com/iced-rs/iced
//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
//! [WebGPU API]: https://gpuweb.github.io/gpuweb/
//! [`glyphon`]: https://github.com/grovesNL/glyphon
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![allow(missing_docs)]
pub mod layer;
pub mod primitive;
pub mod settings;
pub mod window;
#[cfg(feature = "geometry")]
pub mod geometry;
mod buffer;
mod color;
mod engine;
mod quad;
mod text;
mod triangle;
#[cfg(any(feature = "image", feature = "svg"))]
#[path = "image/mod.rs"]
mod image;
#[cfg(not(any(feature = "image", feature = "svg")))]
#[path = "image/null.rs"]
mod image;
use buffer::Buffer;
use iced_debug as debug;
pub use iced_graphics as graphics;
pub use iced_graphics::core;
pub use wgpu;
pub use engine::Engine;
pub use layer::Layer;
pub use primitive::Primitive;
pub use settings::Settings;
#[cfg(feature = "geometry")]
pub use geometry::Geometry;
use crate::core::renderer;
use crate::core::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
use crate::graphics::Viewport;
use crate::graphics::text::{Editor, Paragraph};
/// A [`wgpu`] graphics renderer for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
#[allow(missing_debug_implementations)]
pub struct Renderer {
engine: Engine,
default_font: Font,
default_text_size: Pixels,
layers: layer::Stack,
quad: quad::State,
triangle: triangle::State,
text: text::State,
text_viewport: text::Viewport,
#[cfg(any(feature = "svg", feature = "image"))]
image: image::State,
// TODO: Centralize all the image feature handling
#[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell<image::Cache>,
staging_belt: wgpu::util::StagingBelt,
}
impl Renderer {
pub fn new(
engine: Engine,
default_font: Font,
default_text_size: Pixels,
) -> Self {
Self {
default_font,
default_text_size,
layers: layer::Stack::new(),
quad: quad::State::new(),
triangle: triangle::State::new(
&engine.device,
&engine.triangle_pipeline,
),
text: text::State::new(),
text_viewport: engine.text_pipeline.create_viewport(&engine.device),
#[cfg(any(feature = "svg", feature = "image"))]
image: image::State::new(),
#[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell::new(
engine.create_image_cache(&engine.device),
),
// TODO: Resize belt smartly (?)
// It would be great if the `StagingBelt` API exposed methods
// for introspection to detect when a resize may be worth it.
staging_belt: wgpu::util::StagingBelt::new(
buffer::MAX_WRITE_SIZE as u64,
),
engine,
}
}
fn draw(
&mut self,
clear_color: Option<Color>,
target: &wgpu::TextureView,
viewport: &Viewport,
) -> wgpu::CommandEncoder {
let mut encoder = self.engine.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("iced_wgpu encoder"),
},
);
self.prepare(&mut encoder, viewport);
self.render(&mut encoder, target, clear_color, viewport);
self.quad.trim();
self.triangle.trim();
self.text.trim();
// TODO: Move to runtime!
self.engine.text_pipeline.trim();
#[cfg(any(feature = "svg", feature = "image"))]
{
self.image.trim();
self.image_cache.borrow_mut().trim();
}
encoder
}
pub fn present(
&mut self,
clear_color: Option<Color>,
_format: wgpu::TextureFormat,
frame: &wgpu::TextureView,
viewport: &Viewport,
) -> wgpu::SubmissionIndex {
let encoder = self.draw(clear_color, frame, viewport);
self.staging_belt.finish();
let submission = self.engine.queue.submit([encoder.finish()]);
self.staging_belt.recall();
submission
}
/// Renders the current surface to an offscreen buffer.
///
/// Returns RGBA bytes of the texture data.
pub fn screenshot(
&mut self,
viewport: &Viewport,
background_color: Color,
) -> Vec<u8> {
#[derive(Clone, Copy, Debug)]
struct BufferDimensions {
width: u32,
height: u32,
unpadded_bytes_per_row: usize,
padded_bytes_per_row: usize,
}
impl BufferDimensions {
fn new(size: Size<u32>) -> Self {
let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA
let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256
let padded_bytes_per_row_padding = (alignment
- unpadded_bytes_per_row % alignment)
% alignment;
let padded_bytes_per_row =
unpadded_bytes_per_row + padded_bytes_per_row_padding;
Self {
width: size.width,
height: size.height,
unpadded_bytes_per_row,
padded_bytes_per_row,
}
}
}
let dimensions = BufferDimensions::new(viewport.physical_size());
let texture_extent = wgpu::Extent3d {
width: dimensions.width,
height: dimensions.height,
depth_or_array_layers: 1,
};
let texture =
self.engine.device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu.offscreen.source_texture"),
size: texture_extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.engine.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.draw(Some(background_color), &view, viewport);
let texture = crate::color::convert(
&self.engine.device,
&mut encoder,
texture,
if graphics::color::GAMMA_CORRECTION {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
);
let output_buffer =
self.engine.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu.offscreen.output_texture_buffer"),
size: (dimensions.padded_bytes_per_row
* dimensions.height as usize) as u64,
usage: wgpu::BufferUsages::MAP_READ
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
texture.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &output_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(dimensions.padded_bytes_per_row as u32),
rows_per_image: None,
},
},
texture_extent,
);
self.staging_belt.finish();
let index = self.engine.queue.submit([encoder.finish()]);
self.staging_belt.recall();
let slice = output_buffer.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
let _ = self
.engine
.device
.poll(wgpu::PollType::WaitForSubmissionIndex(index));
let mapped_buffer = slice.get_mapped_range();
mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold(
vec![],
|mut acc, row| {
acc.extend(&row[..dimensions.unpadded_bytes_per_row]);
acc
},
)
}
fn prepare(
&mut self,
encoder: &mut wgpu::CommandEncoder,
viewport: &Viewport,
) {
let scale_factor = viewport.scale_factor() as f32;
self.text_viewport
.update(&self.engine.queue, viewport.physical_size());
let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
viewport.physical_size(),
));
for layer in self.layers.iter_mut() {
if physical_bounds
.intersection(&(layer.bounds * scale_factor))
.and_then(Rectangle::snap)
.is_none()
{
continue;
}
if !layer.quads.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Quad);
self.quad.prepare(
&self.engine.quad_pipeline,
&self.engine.device,
&mut self.staging_belt,
encoder,
&layer.quads,
viewport.projection(),
scale_factor,
);
prepare_span.finish();
}
if !layer.triangles.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Triangle);
self.triangle.prepare(
&self.engine.triangle_pipeline,
&self.engine.device,
&mut self.staging_belt,
encoder,
&layer.triangles,
Transformation::scale(scale_factor),
viewport.physical_size(),
);
prepare_span.finish();
}
if !layer.primitives.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Shader);
let mut primitive_storage = self
.engine
.primitive_storage
.write()
.expect("Write primitive storage");
for instance in &layer.primitives {
instance.primitive.prepare(
&self.engine.device,
&self.engine.queue,
self.engine.format,
&mut primitive_storage,
&instance.bounds,
viewport,
);
}
prepare_span.finish();
}
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Image);
self.image.prepare(
&self.engine.image_pipeline,
&self.engine.device,
&mut self.staging_belt,
encoder,
&mut self.image_cache.borrow_mut(),
&layer.images,
viewport.projection(),
scale_factor,
);
prepare_span.finish();
}
if !layer.text.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Text);
self.text.prepare(
&self.engine.text_pipeline,
&self.engine.device,
&self.engine.queue,
&self.text_viewport,
encoder,
&layer.text,
layer.bounds,
Transformation::scale(scale_factor),
);
prepare_span.finish();
}
}
}
fn render(
&mut self,
encoder: &mut wgpu::CommandEncoder,
frame: &wgpu::TextureView,
clear_color: Option<Color>,
viewport: &Viewport,
) {
use std::mem::ManuallyDrop;
let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: frame,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: match clear_color {
Some(background_color) => wgpu::LoadOp::Clear({
let [r, g, b, a] =
graphics::color::pack(background_color)
.components();
wgpu::Color {
r: f64::from(r),
g: f64::from(g),
b: f64::from(b),
a: f64::from(a),
}
}),
None => wgpu::LoadOp::Load,
},
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
let mut quad_layer = 0;
let mut mesh_layer = 0;
let mut text_layer = 0;
#[cfg(any(feature = "svg", feature = "image"))]
let mut image_layer = 0;
#[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_factor))
else {
continue;
};
let Some(scissor_rect) = physical_bounds.snap() else {
continue;
};
if !layer.quads.is_empty() {
let render_span = debug::render(debug::Primitive::Quad);
self.quad.render(
&self.engine.quad_pipeline,
quad_layer,
scissor_rect,
&layer.quads,
&mut render_pass,
);
render_span.finish();
quad_layer += 1;
}
if !layer.triangles.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
let render_span = debug::render(debug::Primitive::Triangle);
mesh_layer += self.triangle.render(
&self.engine.triangle_pipeline,
encoder,
frame,
mesh_layer,
&layer.triangles,
physical_bounds,
scale,
);
render_span.finish();
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: frame,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
if !layer.primitives.is_empty() {
let render_span = debug::render(debug::Primitive::Shader);
let _ = ManuallyDrop::into_inner(render_pass);
let primitive_storage = self
.engine
.primitive_storage
.read()
.expect("Read primitive storage");
for instance in &layer.primitives {
if let Some(clip_bounds) = (instance.bounds * scale)
.intersection(&physical_bounds)
.and_then(Rectangle::snap)
{
instance.primitive.render(
encoder,
&primitive_storage,
frame,
&clip_bounds,
);
}
}
render_span.finish();
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: frame,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
let render_span = debug::render(debug::Primitive::Image);
self.image.render(
&self.engine.image_pipeline,
&image_cache,
image_layer,
scissor_rect,
&mut render_pass,
);
render_span.finish();
image_layer += 1;
}
if !layer.text.is_empty() {
let render_span = debug::render(debug::Primitive::Text);
text_layer += self.text.render(
&self.engine.text_pipeline,
&self.text_viewport,
text_layer,
&layer.text,
scissor_rect,
&mut render_pass,
);
render_span.finish();
}
}
let _ = ManuallyDrop::into_inner(render_pass);
debug::layers_rendered(|| {
self.layers
.iter()
.filter(|layer| {
!layer.is_empty()
&& physical_bounds
.intersection(&(layer.bounds * scale_factor))
.is_some_and(|viewport| viewport.snap().is_some())
})
.count()
});
}
}
impl core::Renderer for Renderer {
fn start_layer(&mut self, bounds: Rectangle) {
self.layers.push_clip(bounds);
}
fn end_layer(&mut self) {
self.layers.pop_clip();
}
fn start_transformation(&mut self, transformation: Transformation) {
self.layers.push_transformation(transformation);
}
fn end_transformation(&mut self) {
self.layers.pop_transformation();
}
fn fill_quad(
&mut self,
quad: core::renderer::Quad,
background: impl Into<Background>,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_quad(quad, background.into(), transformation);
}
fn 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) -> core::Size<u32> {
self.image_cache.borrow_mut().measure_image(handle)
}
fn draw_image(&mut self, image: core::Image, bounds: Rectangle) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_raster(image, bounds, transformation);
}
}
#[cfg(feature = "svg")]
impl core::svg::Renderer for Renderer {
fn measure_svg(&self, handle: &core::svg::Handle) -> core::Size<u32> {
self.image_cache.borrow_mut().measure_svg(handle)
}
fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_svg(svg, bounds, transformation);
}
}
impl graphics::mesh::Renderer for Renderer {
fn draw_mesh(&mut self, mesh: graphics::Mesh) {
debug_assert!(
!mesh.indices().is_empty(),
"Mesh must not have empty indices"
);
debug_assert!(
mesh.indices().len() % 3 == 0,
"Mesh indices length must be a multiple of 3"
);
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: core::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,
images,
text,
} => {
layer.draw_mesh_group(meshes, transformation);
for image in images {
layer.draw_image(image, transformation);
}
layer.draw_text_group(text, transformation);
}
Geometry::Cached(cache) => {
if let Some(meshes) = cache.meshes {
layer.draw_mesh_cache(meshes, transformation);
}
if let Some(images) = cache.images {
for image in images.iter().cloned() {
layer.draw_image(image, transformation);
}
}
if let Some(text) = cache.text {
layer.draw_text_cache(text, transformation);
}
}
}
}
}
impl primitive::Renderer for Renderer {
fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_primitive(bounds, Box::new(primitive), transformation);
}
}
impl graphics::compositor::Default for crate::Renderer {
type Compositor = window::Compositor;
}
impl renderer::Headless for Renderer {
async fn new(
default_font: Font,
default_text_size: Pixels,
backend: Option<&str>,
) -> Option<Self> {
if backend.is_some_and(|backend| backend != "wgpu") {
return None;
}
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::from_env()
.unwrap_or(wgpu::Backends::PRIMARY),
flags: wgpu::InstanceFlags::empty(),
..wgpu::InstanceDescriptor::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.ok()?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("iced_wgpu [headless]"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits {
max_bind_groups: 2,
..wgpu::Limits::default()
},
memory_hints: wgpu::MemoryHints::MemoryUsage,
trace: wgpu::Trace::Off,
})
.await
.ok()?;
let engine = Engine::new(
&adapter,
device,
queue,
if graphics::color::GAMMA_CORRECTION {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
Some(graphics::Antialiasing::MSAAx4),
);
Some(Self::new(engine, default_font, default_text_size))
}
fn name(&self) -> String {
"wgpu".to_owned()
}
fn screenshot(
&mut self,
size: Size<u32>,
scale_factor: f32,
background_color: Color,
) -> Vec<u8> {
self.screenshot(
&Viewport::with_physical_size(size, f64::from(scale_factor)),
background_color,
)
}
}