Merge pull request #3092 from iced-rs/async-image-rendering

Concurrent Image Decoding and Uploading (and more cool stuff)
This commit is contained in:
Héctor 2025-10-28 22:00:40 +01:00 committed by GitHub
commit bed9657ec2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 2262 additions and 712 deletions

View file

@ -42,7 +42,7 @@ impl<T: bytemuck::Pod> Buffer<T> {
}
pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool {
let new_size = (std::mem::size_of::<T>() * new_count) as u64;
let new_size = next_copy_size::<T>(new_count);
if self.size < new_size {
self.raw = device.create_buffer(&wgpu::BufferDescriptor {

View file

@ -1,4 +1,4 @@
use crate::graphics::Antialiasing;
use crate::graphics::{Antialiasing, Shell};
use crate::primitive;
use crate::quad;
use crate::text;
@ -18,6 +18,7 @@ pub struct Engine {
#[cfg(any(feature = "image", feature = "svg"))]
pub(crate) image_pipeline: crate::image::Pipeline,
pub(crate) primitive_storage: Arc<RwLock<primitive::Storage>>,
_shell: Shell,
}
impl Engine {
@ -27,6 +28,7 @@ impl Engine {
queue: wgpu::Queue,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>, // TODO: Initialize AA pipelines lazily
shell: Shell,
) -> Self {
Self {
format,
@ -52,14 +54,16 @@ impl Engine {
device,
queue,
_shell: shell,
}
}
#[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 create_image_cache(&self) -> crate::image::Cache {
self.image_pipeline.create_cache(
&self.device,
&self.queue,
&self._shell,
)
}
}

View file

@ -431,8 +431,14 @@ impl geometry::frame::Backend for Frame {
self.transforms.current.transform_rectangle(bounds);
image.rotation += external_rotation;
image.border_radius =
image.border_radius * self.transforms.current.scale().0;
self.images.push(Image::Raster(image, bounds));
self.images.push(Image::Raster {
image,
bounds,
clip_bounds: self.clip_bounds,
});
}
fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
@ -443,7 +449,11 @@ impl geometry::frame::Backend for Frame {
svg.rotation += external_rotation;
self.images.push(Image::Vector(svg, bounds));
self.images.push(Image::Vector {
svg,
bounds,
clip_bounds: self.clip_bounds,
});
}
}

View file

@ -10,7 +10,8 @@ pub use layer::Layer;
use allocator::Allocator;
pub const SIZE: u32 = 2048;
pub const DEFAULT_SIZE: u32 = 2048;
pub const MAX_SIZE: u32 = 2048;
use crate::core::Size;
use crate::graphics::color;
@ -19,11 +20,12 @@ use std::sync::Arc;
#[derive(Debug)]
pub struct Atlas {
size: u32,
backend: wgpu::Backend,
texture: wgpu::Texture,
texture_view: wgpu::TextureView,
texture_bind_group: wgpu::BindGroup,
texture_layout: Arc<wgpu::BindGroupLayout>,
texture_bind_group: Arc<wgpu::BindGroup>,
texture_layout: wgpu::BindGroupLayout,
layers: Vec<Layer>,
}
@ -31,8 +33,19 @@ impl Atlas {
pub fn new(
device: &wgpu::Device,
backend: wgpu::Backend,
texture_layout: Arc<wgpu::BindGroupLayout>,
texture_layout: wgpu::BindGroupLayout,
) -> Self {
Self::with_size(device, backend, texture_layout, DEFAULT_SIZE)
}
pub fn with_size(
device: &wgpu::Device,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
size: u32,
) -> Self {
let size = size.min(MAX_SIZE);
let layers = match backend {
// On the GL backend we start with 2 layers, to help wgpu figure
// out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D`
@ -42,8 +55,8 @@ impl Atlas {
};
let extent = wgpu::Extent3d {
width: SIZE,
height: SIZE,
width: size,
height: size,
depth_or_array_layers: layers.len() as u32,
};
@ -80,30 +93,28 @@ impl Atlas {
});
Atlas {
size,
backend,
texture,
texture_view,
texture_bind_group,
texture_bind_group: Arc::new(texture_bind_group),
texture_layout,
layers,
}
}
pub fn bind_group(&self) -> &wgpu::BindGroup {
pub fn bind_group(&self) -> &Arc<wgpu::BindGroup> {
&self.texture_bind_group
}
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn upload(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
width: u32,
height: u32,
data: &[u8],
pixels: &[u8],
) -> Option<Entry> {
let entry = {
let current_size = self.layers.len();
@ -111,59 +122,32 @@ impl Atlas {
// We grow the internal texture after allocating if necessary
let new_layers = self.layers.len() - current_size;
self.grow(new_layers, device, encoder);
self.grow(new_layers, device, encoder, self.backend);
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
// So we calculate padded_width by rounding width up to the next
// multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT.
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padding = (align - (4 * width) % align) % align;
let padded_width = (4 * width + padding) as usize;
let padded_data_size = padded_width * height as usize;
let mut padded_data = vec![0; padded_data_size];
for row in 0..height as usize {
let offset = row * padded_width;
padded_data[offset..offset + 4 * width as usize].copy_from_slice(
&data[row * 4 * width as usize..(row + 1) * 4 * width as usize],
);
}
match &entry {
Entry::Contiguous(allocation) => {
self.upload_allocation(
&padded_data,
width,
height,
padding,
0,
allocation,
device,
encoder,
pixels, width, 0, allocation, device, encoder, belt,
);
}
Entry::Fragmented { fragments, .. } => {
for fragment in fragments {
let (x, y) = fragment.position;
let offset = (y * padded_width as u32 + 4 * x) as usize;
let offset = 4 * (y * width + x) as usize;
self.upload_allocation(
&padded_data,
pixels,
width,
height,
padding,
offset,
&fragment.allocation,
device,
encoder,
belt,
);
}
}
@ -172,7 +156,7 @@ impl Atlas {
if log::log_enabled!(log::Level::Debug) {
log::debug!(
"Atlas layers: {} (busy: {}, allocations: {})",
self.layer_count(),
self.layers.len(),
self.layers.iter().filter(|layer| !layer.is_empty()).count(),
self.layers.iter().map(Layer::allocations).sum::<usize>(),
);
@ -198,7 +182,7 @@ impl Atlas {
fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> {
// Allocate one layer if texture fits perfectly
if width == SIZE && height == SIZE {
if width == self.size && height == self.size {
let mut empty_layers = self
.layers
.iter_mut()
@ -208,27 +192,31 @@ impl Atlas {
if let Some((i, layer)) = empty_layers.next() {
*layer = Layer::Full;
return Some(Entry::Contiguous(Allocation::Full { layer: i }));
return Some(Entry::Contiguous(Allocation::Full {
layer: i,
size: self.size,
}));
}
self.layers.push(Layer::Full);
return Some(Entry::Contiguous(Allocation::Full {
layer: self.layers.len() - 1,
size: self.size,
}));
}
// Split big textures across multiple layers
if width > SIZE || height > SIZE {
if width > self.size || height > self.size {
let mut fragments = Vec::new();
let mut y = 0;
while y < height {
let height = std::cmp::min(height - y, SIZE);
let height = std::cmp::min(height - y, self.size);
let mut x = 0;
while x < width {
let width = std::cmp::min(width - x, SIZE);
let width = std::cmp::min(width - x, self.size);
let allocation = self.allocate(width, height)?;
@ -255,7 +243,7 @@ impl Atlas {
for (i, layer) in self.layers.iter_mut().enumerate() {
match layer {
Layer::Empty => {
let mut allocator = Allocator::new(SIZE);
let mut allocator = Allocator::new(self.size);
if let Some(region) = allocator.allocate(width, height) {
*layer = Layer::Busy(allocator);
@ -263,6 +251,7 @@ impl Atlas {
return Some(Entry::Contiguous(Allocation::Partial {
region,
layer: i,
atlas_size: self.size,
}));
}
}
@ -271,6 +260,7 @@ impl Atlas {
return Some(Entry::Contiguous(Allocation::Partial {
region,
layer: i,
atlas_size: self.size,
}));
}
}
@ -279,7 +269,7 @@ impl Atlas {
}
// Create new layer with atlas allocator
let mut allocator = Allocator::new(SIZE);
let mut allocator = Allocator::new(self.size);
if let Some(region) = allocator.allocate(width, height) {
self.layers.push(Layer::Busy(allocator));
@ -287,6 +277,7 @@ impl Atlas {
return Some(Entry::Contiguous(Allocation::Partial {
region,
layer: self.layers.len() - 1,
atlas_size: self.size,
}));
}
@ -298,10 +289,10 @@ impl Atlas {
log::debug!("Deallocating atlas: {allocation:?}");
match allocation {
Allocation::Full { layer } => {
Allocation::Full { layer, .. } => {
self.layers[*layer] = Layer::Empty;
}
Allocation::Partial { layer, region } => {
Allocation::Partial { layer, region, .. } => {
let layer = &mut self.layers[*layer];
if let Layer::Busy(allocator) = layer {
@ -316,55 +307,134 @@ impl Atlas {
}
fn upload_allocation(
&mut self,
data: &[u8],
&self,
pixels: &[u8],
image_width: u32,
image_height: u32,
padding: u32,
offset: usize,
allocation: &Allocation,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
) {
use wgpu::util::DeviceExt;
let (x, y) = allocation.position();
let Size { width, height } = allocation.size();
let layer = allocation.layer();
let padding = allocation.padding();
let extent = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
// It is a webgpu requirement that:
// BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
// So we calculate bytes_per_row by rounding width up to the next
// multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT.
let bytes_per_row = (4 * (width + padding.width * 2))
.next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
as usize;
let total_bytes =
bytes_per_row * (height + padding.height * 2) as usize;
let buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image upload buffer"),
contents: data,
usage: wgpu::BufferUsages::COPY_SRC,
});
let buffer_slice = belt.allocate(
wgpu::BufferSize::new(total_bytes as u64).unwrap(),
wgpu::BufferSize::new(8 * 4).unwrap(),
device,
);
const PIXEL: usize = 4;
let mut fragment = buffer_slice.get_mapped_range_mut();
let w = width as usize;
let h = height as usize;
let pad_w = padding.width as usize;
let pad_h = padding.height as usize;
let stride = PIXEL * w;
// Copy image rows
for row in 0..h {
let src = offset + row * PIXEL * image_width as usize;
let dst = (row + pad_h) * bytes_per_row;
fragment[dst + PIXEL * pad_w..dst + PIXEL * pad_w + stride]
.copy_from_slice(&pixels[src..src + stride]);
// Add padding to the sides, if needed
for i in 0..pad_w {
fragment[dst + PIXEL * i..dst + PIXEL * (i + 1)]
.copy_from_slice(&pixels[src..src + PIXEL]);
fragment[dst + stride + PIXEL * (pad_w + i)
..dst + stride + PIXEL * (pad_w + i + 1)]
.copy_from_slice(
&pixels[src + stride - PIXEL..src + stride],
);
}
}
// Add padding on top and bottom
for row in 0..pad_h {
let dst_top = row * bytes_per_row;
let dst_bottom = (pad_h + h + row) * bytes_per_row;
let src_top = offset;
let src_bottom = offset + (h - 1) * PIXEL * image_width as usize;
// Top
fragment[dst_top + PIXEL * pad_w..dst_top + PIXEL * (pad_w + w)]
.copy_from_slice(&pixels[src_top..src_top + PIXEL * w]);
// Bottom
fragment
[dst_bottom + PIXEL * pad_w..dst_bottom + PIXEL * (pad_w + w)]
.copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL * w]);
// Corners
for i in 0..pad_w {
// Top left
fragment[dst_top + PIXEL * i..dst_top + PIXEL * (i + 1)]
.copy_from_slice(&pixels[offset..offset + PIXEL]);
// Top right
fragment[dst_top + PIXEL * (w + pad_w + i)
..dst_top + PIXEL * (w + pad_w + i + 1)]
.copy_from_slice(
&pixels[offset + PIXEL * (w - 1)..offset + PIXEL * w],
);
// Bottom left
fragment[dst_bottom + PIXEL * i..dst_bottom + PIXEL * (i + 1)]
.copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL]);
// Bottom right
fragment[dst_bottom + PIXEL * (w + pad_w + i)
..dst_bottom + PIXEL * (w + pad_w + i + 1)]
.copy_from_slice(
&pixels[src_bottom + PIXEL * (w - 1)
..src_bottom + PIXEL * w],
);
}
}
// Copy actual image
encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
buffer: buffer_slice.buffer(),
layout: wgpu::TexelCopyBufferLayout {
offset: offset as u64,
bytes_per_row: Some(4 * image_width + padding),
rows_per_image: Some(image_height),
offset: buffer_slice.offset(),
bytes_per_row: Some(bytes_per_row as u32),
rows_per_image: Some(height + padding.height * 2),
},
},
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x,
y,
x: x - padding.width,
y: y - padding.height,
z: layer as u32,
},
aspect: wgpu::TextureAspect::default(),
},
extent,
wgpu::Extent3d {
width: width + padding.width * 2,
height: height + padding.height * 2,
depth_or_array_layers: 1,
},
);
}
@ -373,6 +443,7 @@ impl Atlas {
amount: usize,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
backend: wgpu::Backend,
) {
if amount == 0 {
return;
@ -383,7 +454,7 @@ impl Atlas {
// some unused memory on GL, but it's better than not being able to grow the atlas past a depth
// of 6!
// https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371
let depth_or_array_layers = match self.backend {
let depth_or_array_layers = match backend {
wgpu::Backend::Gl if self.layers.len() == 6 => 7,
_ => self.layers.len() as u32,
};
@ -391,8 +462,8 @@ impl Atlas {
let new_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu::image texture atlas"),
size: wgpu::Extent3d {
width: SIZE,
height: SIZE,
width: self.size,
height: self.size,
depth_or_array_layers,
},
mip_level_count: 1,
@ -440,8 +511,8 @@ impl Atlas {
aspect: wgpu::TextureAspect::default(),
},
wgpu::Extent3d {
width: SIZE,
height: SIZE,
width: self.size,
height: self.size,
depth_or_array_layers: 1,
},
);
@ -455,7 +526,7 @@ impl Atlas {
});
self.texture_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image texture atlas bind group"),
layout: &self.texture_layout,
entries: &[wgpu::BindGroupEntry {
@ -464,6 +535,6 @@ impl Atlas {
&self.texture_view,
),
}],
});
}));
}
}

View file

@ -1,14 +1,16 @@
use crate::core::Size;
use crate::image::atlas::{self, allocator};
use crate::image::atlas::allocator;
#[derive(Debug)]
pub enum Allocation {
Partial {
layer: usize,
region: allocator::Region,
atlas_size: u32,
},
Full {
layer: usize,
size: u32,
},
}
@ -23,14 +25,28 @@ impl Allocation {
pub fn size(&self) -> Size<u32> {
match self {
Allocation::Partial { region, .. } => region.size(),
Allocation::Full { .. } => Size::new(atlas::SIZE, atlas::SIZE),
Allocation::Full { size, .. } => Size::new(*size, *size),
}
}
pub fn padding(&self) -> Size<u32> {
match self {
Allocation::Partial { region, .. } => region.padding(),
Allocation::Full { .. } => Size::new(0, 0),
}
}
pub fn layer(&self) -> usize {
match self {
Allocation::Partial { layer, .. } => *layer,
Allocation::Full { layer } => *layer,
Allocation::Full { layer, .. } => *layer,
}
}
pub fn atlas_size(&self) -> u32 {
match self {
Allocation::Partial { atlas_size, .. } => *atlas_size,
Allocation::Full { size, .. } => *size,
}
}
}

View file

@ -1,3 +1,5 @@
use crate::core;
use guillotiere::{AtlasAllocator, Size};
pub struct Allocator {
@ -6,6 +8,8 @@ pub struct Allocator {
}
impl Allocator {
const PADDING: u32 = 1;
pub fn new(size: u32) -> Allocator {
let raw = AtlasAllocator::new(Size::new(size as i32, size as i32));
@ -16,12 +20,38 @@ impl Allocator {
}
pub fn allocate(&mut self, width: u32, height: u32) -> Option<Region> {
let allocation =
self.raw.allocate(Size::new(width as i32, height as i32))?;
let size = self.raw.size();
let padded_width = width + Self::PADDING * 2;
let padded_height = height + Self::PADDING * 2;
let pad_width = padded_width as i32 <= size.width;
let pad_height = padded_height as i32 <= size.height;
let mut allocation = self.raw.allocate(Size::new(
if pad_width { padded_width } else { width } as i32,
if pad_height { padded_height } else { height } as i32,
))?;
if pad_width {
allocation.rectangle.min.x += Self::PADDING as i32;
allocation.rectangle.max.x -= Self::PADDING as i32;
}
if pad_height {
allocation.rectangle.min.y += Self::PADDING as i32;
allocation.rectangle.max.y -= Self::PADDING as i32;
}
self.allocations += 1;
Some(Region { allocation })
Some(Region {
allocation,
padding: core::Size::new(
if pad_width { Self::PADDING } else { 0 },
if pad_height { Self::PADDING } else { 0 },
),
})
}
pub fn deallocate(&mut self, region: &Region) {
@ -41,6 +71,7 @@ impl Allocator {
pub struct Region {
allocation: guillotiere::Allocation,
padding: core::Size<u32>,
}
impl Region {
@ -50,10 +81,14 @@ impl Region {
(rectangle.min.x as u32, rectangle.min.y as u32)
}
pub fn size(&self) -> crate::core::Size<u32> {
pub fn size(&self) -> core::Size<u32> {
let size = self.allocation.rectangle.size();
crate::core::Size::new(size.width as u32, size.height as u32)
core::Size::new(size.width as u32, size.height as u32)
}
pub fn padding(&self) -> crate::core::Size<u32> {
self.padding
}
}

View file

@ -1,47 +1,199 @@
use crate::core::{self, Size};
use crate::graphics::Shell;
use crate::image::atlas::{self, Atlas};
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
use worker::Worker;
#[cfg(feature = "image")]
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug)]
pub struct Cache {
atlas: Atlas,
#[cfg(feature = "image")]
raster: crate::image::raster::Cache,
raster: Raster,
#[cfg(feature = "svg")]
vector: crate::image::vector::Cache,
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
worker: Worker,
}
impl Cache {
pub fn new(
device: &wgpu::Device,
_queue: &wgpu::Queue,
backend: wgpu::Backend,
layout: Arc<wgpu::BindGroupLayout>,
layout: wgpu::BindGroupLayout,
_shell: &Shell,
) -> Self {
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
let worker =
Worker::new(device, _queue, backend, layout.clone(), _shell);
Self {
atlas: Atlas::new(device, backend, layout),
#[cfg(feature = "image")]
raster: crate::image::raster::Cache::default(),
raster: Raster {
cache: crate::image::raster::Cache::default(),
pending: HashMap::new(),
belt: wgpu::util::StagingBelt::new(2 * 1024 * 1024),
},
#[cfg(feature = "svg")]
vector: crate::image::vector::Cache::default(),
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
worker,
}
}
pub fn bind_group(&self) -> &wgpu::BindGroup {
self.atlas.bind_group()
}
#[cfg(feature = "image")]
pub fn allocate_image(
&mut self,
handle: &core::image::Handle,
callback: impl FnOnce(Result<core::image::Allocation, core::image::Error>)
+ Send
+ 'static,
) {
use crate::image::raster::Memory;
pub fn layer_count(&self) -> usize {
self.atlas.layer_count()
let callback = Box::new(callback);
if let Some(callbacks) = self.raster.pending.get_mut(&handle.id()) {
callbacks.push(callback);
return;
}
if let Some(Memory::Device {
allocation, entry, ..
}) = self.raster.cache.get_mut(handle)
{
if let Some(allocation) = allocation
.as_ref()
.and_then(core::image::Allocation::upgrade)
{
callback(Ok(allocation));
return;
}
#[allow(unsafe_code)]
let new = unsafe { core::image::allocate(handle, entry.size()) };
*allocation = Some(new.downgrade());
callback(Ok(new));
return;
}
let _ = self.raster.pending.insert(handle.id(), vec![callback]);
#[cfg(not(target_arch = "wasm32"))]
self.worker.load(handle);
}
#[cfg(feature = "image")]
pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> {
self.raster.load(handle).dimensions()
pub fn load_image(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
handle: &core::image::Handle,
) -> Result<core::image::Allocation, core::image::Error> {
use crate::image::raster::Memory;
if !self.raster.cache.contains(handle) {
self.raster.cache.insert(handle, Memory::load(handle));
}
match self.raster.cache.get_mut(handle).unwrap() {
Memory::Host(image) => {
let mut encoder = device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("raster image upload"),
},
);
let entry = self.atlas.upload(
device,
&mut encoder,
&mut self.raster.belt,
image.width(),
image.height(),
image,
);
self.raster.belt.finish();
let submission = queue.submit([encoder.finish()]);
self.raster.belt.recall();
let Some(entry) = entry else {
return Err(core::image::Error::OutOfMemory);
};
let _ = device
.poll(wgpu::PollType::WaitForSubmissionIndex(submission));
#[allow(unsafe_code)]
let allocation = unsafe {
core::image::allocate(
handle,
Size::new(image.width(), image.height()),
)
};
self.raster.cache.insert(
handle,
Memory::Device {
entry,
bind_group: None,
allocation: Some(allocation.downgrade()),
},
);
Ok(allocation)
}
Memory::Device {
entry, allocation, ..
} => {
if let Some(allocation) = allocation
.as_ref()
.and_then(core::image::Allocation::upgrade)
{
return Ok(allocation);
}
#[allow(unsafe_code)]
let new =
unsafe { core::image::allocate(handle, entry.size()) };
*allocation = Some(new.downgrade());
Ok(new)
}
Memory::Error(error) => Err(error.clone()),
}
}
#[cfg(feature = "image")]
pub fn measure_image(
&mut self,
handle: &core::image::Handle,
) -> Option<Size<u32>> {
self.receive();
let image = load_image(
&mut self.raster.cache,
&mut self.raster.pending,
#[cfg(not(target_arch = "wasm32"))]
&self.worker,
handle,
None,
)?;
Some(image.dimensions())
}
#[cfg(feature = "svg")]
pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> {
// TODO: Concurrency
self.vector.load(handle).viewport_dimensions()
}
@ -50,9 +202,66 @@ impl Cache {
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &core::image::Handle,
) -> Option<&atlas::Entry> {
self.raster.upload(device, encoder, handle, &mut self.atlas)
) -> Option<(&atlas::Entry, &Arc<wgpu::BindGroup>)> {
use crate::image::raster::Memory;
self.receive();
let memory = load_image(
&mut self.raster.cache,
&mut self.raster.pending,
#[cfg(not(target_arch = "wasm32"))]
&self.worker,
handle,
None,
)?;
if let Memory::Device {
entry, bind_group, ..
} = memory
{
return Some((
entry,
bind_group.as_ref().unwrap_or(self.atlas.bind_group()),
));
}
let image = memory.host()?;
const MAX_SYNC_SIZE: usize = 2 * 1024 * 1024;
// TODO: Concurrent Wasm support
if image.len() < MAX_SYNC_SIZE || cfg!(target_arch = "wasm32") {
let entry = self.atlas.upload(
device,
encoder,
belt,
image.width(),
image.height(),
&image,
)?;
*memory = Memory::Device {
entry,
bind_group: None,
allocation: None,
};
if let Memory::Device { entry, .. } = memory {
return Some((entry, self.atlas.bind_group()));
}
}
if !self.raster.pending.contains_key(&handle.id()) {
let _ = self.raster.pending.insert(handle.id(), Vec::new());
#[cfg(not(target_arch = "wasm32"))]
self.worker.upload(handle, image);
}
None
}
#[cfg(feature = "svg")]
@ -60,27 +269,361 @@ impl Cache {
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &core::svg::Handle,
color: Option<core::Color>,
size: [f32; 2],
size: Size,
scale: f32,
) -> Option<&atlas::Entry> {
self.vector.upload(
device,
encoder,
handle,
color,
size,
scale,
&mut self.atlas,
)
) -> Option<(&atlas::Entry, &Arc<wgpu::BindGroup>)> {
// TODO: Concurrency
self.vector
.upload(
device,
encoder,
belt,
handle,
color,
size,
scale,
&mut self.atlas,
)
.map(|entry| (entry, self.atlas.bind_group()))
}
pub fn trim(&mut self) {
#[cfg(feature = "image")]
self.raster.trim(&mut self.atlas);
self.raster.cache.trim(&mut self.atlas, |_bind_group| {
#[cfg(not(target_arch = "wasm32"))]
self.worker.drop(_bind_group);
});
#[cfg(feature = "svg")]
self.vector.trim(&mut self.atlas);
self.vector.trim(&mut self.atlas); // TODO: Concurrency
}
#[cfg(feature = "image")]
fn receive(&mut self) {
#[cfg(not(target_arch = "wasm32"))]
while let Ok(work) = self.worker.try_recv() {
use crate::image::raster::Memory;
match work {
worker::Work::Upload {
handle,
entry,
bind_group,
} => {
let callbacks = self.raster.pending.remove(&handle.id());
let allocation = if let Some(callbacks) = callbacks {
#[allow(unsafe_code)]
let allocation = unsafe {
core::image::allocate(&handle, entry.size())
};
let reference = allocation.downgrade();
for callback in callbacks {
callback(Ok(allocation.clone()));
}
Some(reference)
} else {
None
};
self.raster.cache.insert(
&handle,
Memory::Device {
entry,
bind_group: Some(bind_group),
allocation,
},
);
}
worker::Work::Error { handle, error } => {
let callbacks = self.raster.pending.remove(&handle.id());
if let Some(callbacks) = callbacks {
for callback in callbacks {
callback(Err(error.clone()));
}
}
self.raster.cache.insert(&handle, Memory::Error(error));
}
}
}
}
}
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
impl Drop for Cache {
fn drop(&mut self) {
self.worker.quit();
}
}
#[cfg(feature = "image")]
struct Raster {
cache: crate::image::raster::Cache,
pending: HashMap<core::image::Id, Vec<Callback>>,
belt: wgpu::util::StagingBelt,
}
#[cfg(feature = "image")]
type Callback =
Box<dyn FnOnce(Result<core::image::Allocation, core::image::Error>) + Send>;
#[cfg(feature = "image")]
fn load_image<'a>(
cache: &'a mut crate::image::raster::Cache,
pending: &mut HashMap<core::image::Id, Vec<Callback>>,
#[cfg(not(target_arch = "wasm32"))] worker: &Worker,
handle: &core::image::Handle,
callback: Option<Callback>,
) -> Option<&'a mut crate::image::raster::Memory> {
use crate::image::raster::Memory;
if !cache.contains(handle) {
if cfg!(target_arch = "wasm32") {
// TODO: Concurrent support for Wasm
cache.insert(handle, Memory::load(handle));
} else if let core::image::Handle::Rgba { .. } = handle {
// Load RGBA handles synchronously, since it's very cheap
cache.insert(handle, Memory::load(handle));
} else if !pending.contains_key(&handle.id()) {
let _ = pending.insert(handle.id(), Vec::from_iter(callback));
#[cfg(not(target_arch = "wasm32"))]
worker.load(handle);
}
}
cache.get_mut(handle)
}
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
mod worker {
use crate::core::image;
use crate::graphics::Shell;
use crate::image::atlas::{self, Atlas};
use crate::image::raster;
use std::sync::Arc;
use std::sync::mpsc;
use std::thread;
pub struct Worker {
jobs: mpsc::SyncSender<Job>,
quit: mpsc::SyncSender<()>,
work: mpsc::Receiver<Work>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Worker {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
shell: &Shell,
) -> Self {
let (jobs_sender, jobs_receiver) = mpsc::sync_channel(1_000);
let (quit_sender, quit_receiver) = mpsc::sync_channel(1);
let (work_sender, work_receiver) = mpsc::sync_channel(1_000);
let instance = Instance {
device: device.clone(),
queue: queue.clone(),
backend,
texture_layout,
shell: shell.clone(),
belt: wgpu::util::StagingBelt::new(4 * 1024 * 1024),
jobs: jobs_receiver,
output: work_sender,
quit: quit_receiver,
};
let handle = thread::spawn(move || instance.run());
Self {
jobs: jobs_sender,
quit: quit_sender,
work: work_receiver,
handle: Some(handle),
}
}
pub fn load(&self, handle: &image::Handle) {
let _ = self.jobs.send(Job::Load(handle.clone()));
}
pub fn upload(&self, handle: &image::Handle, image: raster::Image) {
let _ = self.jobs.send(Job::Upload {
handle: handle.clone(),
width: image.width(),
height: image.height(),
rgba: image.into_raw(),
});
}
pub fn drop(&self, bind_group: Arc<wgpu::BindGroup>) {
let _ = self.jobs.send(Job::Drop(bind_group));
}
pub fn try_recv(&self) -> Result<Work, mpsc::TryRecvError> {
self.work.try_recv()
}
pub fn quit(&mut self) {
let _ = self.quit.try_send(());
let _ = self.jobs.send(Job::Quit);
let _ = self.handle.take().map(thread::JoinHandle::join);
}
}
pub struct Instance {
device: wgpu::Device,
queue: wgpu::Queue,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
shell: Shell,
belt: wgpu::util::StagingBelt,
jobs: mpsc::Receiver<Job>,
output: mpsc::SyncSender<Work>,
quit: mpsc::Receiver<()>,
}
#[derive(Debug)]
enum Job {
Load(image::Handle),
Upload {
handle: image::Handle,
rgba: image::Bytes,
width: u32,
height: u32,
},
Drop(Arc<wgpu::BindGroup>),
Quit,
}
pub enum Work {
Upload {
handle: image::Handle,
entry: atlas::Entry,
bind_group: Arc<wgpu::BindGroup>,
},
Error {
handle: image::Handle,
error: image::Error,
},
}
impl Instance {
fn run(mut self) {
loop {
if self.quit.try_recv().is_ok() {
return;
}
let Ok(job) = self.jobs.recv() else {
return;
};
match job {
Job::Load(handle) => {
match crate::graphics::image::load(&handle) {
Ok(image) => self.upload(
handle,
image.width(),
image.height(),
image.into_raw(),
Shell::invalidate_layout,
),
Err(error) => {
let _ = self
.output
.send(Work::Error { handle, error });
}
}
}
Job::Upload {
handle,
rgba,
width,
height,
} => {
self.upload(
handle,
width,
height,
rgba,
Shell::request_redraw,
);
}
Job::Drop(bind_group) => {
drop(bind_group);
}
Job::Quit => return,
}
}
}
fn upload(
&mut self,
handle: image::Handle,
width: u32,
height: u32,
rgba: image::Bytes,
callback: fn(&Shell),
) {
let mut encoder = self.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("raster image upload"),
},
);
let mut atlas = Atlas::with_size(
&self.device,
self.backend,
self.texture_layout.clone(),
width.max(height),
);
let Some(entry) = atlas.upload(
&self.device,
&mut encoder,
&mut self.belt,
width,
height,
&rgba,
) else {
return;
};
let output = self.output.clone();
let shell = self.shell.clone();
self.belt.finish();
let submission = self.queue.submit([encoder.finish()]);
self.belt.recall();
let bind_group = atlas.bind_group().clone();
self.queue.on_submitted_work_done(move || {
let _ = output.send(Work::Upload {
handle,
entry,
bind_group,
});
callback(&shell);
});
let _ = self
.device
.poll(wgpu::PollType::WaitForSubmissionIndex(submission));
}
}
}

View file

@ -10,7 +10,9 @@ mod raster;
mod vector;
use crate::Buffer;
use crate::core::border;
use crate::core::{Rectangle, Size, Transformation};
use crate::graphics::Shell;
use bytemuck::{Pod, Zeroable};
@ -27,7 +29,7 @@ pub struct Pipeline {
backend: wgpu::Backend,
nearest_sampler: wgpu::Sampler,
linear_sampler: wgpu::Sampler,
texture_layout: Arc<wgpu::BindGroupLayout>,
texture_layout: wgpu::BindGroupLayout,
constant_layout: wgpu::BindGroupLayout,
}
@ -131,24 +133,26 @@ impl Pipeline {
array_stride: mem::size_of::<Instance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
// Center
1 => Float32x2,
// Scale
2 => Float32x2,
0 => Float32x2,
// Clip bounds
1 => Float32x4,
// Border radius
2 => Float32x4,
// Tile
3 => Float32x4,
// Rotation
3 => Float32,
// Opacity
4 => Float32,
// Opacity
5 => Float32,
// Atlas position
5 => Float32x2,
// Atlas scale
6 => Float32x2,
// Atlas scale
7 => Float32x2,
// Layer
7 => Sint32,
8 => Sint32,
// Snap
8 => Uint32,
9 => Uint32,
),
}],
compilation_options:
@ -196,13 +200,24 @@ impl Pipeline {
backend,
nearest_sampler,
linear_sampler,
texture_layout: Arc::new(texture_layout),
texture_layout,
constant_layout,
}
}
pub fn create_cache(&self, device: &wgpu::Device) -> Cache {
Cache::new(device, self.backend, self.texture_layout.clone())
pub fn create_cache(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
shell: &Shell,
) -> Cache {
Cache::new(
device,
queue,
self.backend,
self.texture_layout.clone(),
shell,
)
}
}
@ -210,6 +225,8 @@ impl Pipeline {
pub struct State {
layers: Vec<Layer>,
prepare_layer: usize,
nearest_instances: Vec<Instance>,
linear_instances: Vec<Instance>,
}
impl State {
@ -228,69 +245,6 @@ impl State {
transformation: Transformation,
scale: f32,
) {
let nearest_instances: &mut Vec<Instance> = &mut Vec::new();
let linear_instances: &mut Vec<Instance> = &mut Vec::new();
for image in images {
match &image {
#[cfg(feature = "image")]
Image::Raster(image, bounds) => {
if let Some(atlas_entry) =
cache.upload_raster(device, encoder, &image.handle)
{
add_instances(
[bounds.x, bounds.y],
[bounds.width, bounds.height],
f32::from(image.rotation),
image.opacity,
image.snap,
atlas_entry,
match image.filter_method {
crate::core::image::FilterMethod::Nearest => {
nearest_instances
}
crate::core::image::FilterMethod::Linear => {
linear_instances
}
},
);
}
}
#[cfg(not(feature = "image"))]
Image::Raster { .. } => {}
#[cfg(feature = "svg")]
Image::Vector(svg, bounds) => {
let size = [bounds.width, bounds.height];
if let Some(atlas_entry) = cache.upload_vector(
device,
encoder,
&svg.handle,
svg.color,
size,
scale,
) {
add_instances(
[bounds.x, bounds.y],
size,
f32::from(svg.rotation),
svg.opacity,
true,
atlas_entry,
nearest_instances,
);
}
}
#[cfg(not(feature = "svg"))]
Image::Vector { .. } => {}
}
}
if nearest_instances.is_empty() && linear_instances.is_empty() {
return;
}
if self.layers.len() <= self.prepare_layer {
self.layers.push(Layer::new(
device,
@ -302,23 +256,129 @@ impl State {
let layer = &mut self.layers[self.prepare_layer];
let mut atlas: Option<Arc<wgpu::BindGroup>> = None;
for image in images {
match &image {
#[cfg(feature = "image")]
Image::Raster {
image,
bounds,
clip_bounds,
} => {
if let Some((atlas_entry, bind_group)) = cache
.upload_raster(device, encoder, belt, &image.handle)
{
match atlas.as_mut() {
None => {
atlas = Some(bind_group.clone());
}
Some(atlas) if atlas != bind_group => {
layer.push(
atlas,
&self.nearest_instances,
&self.linear_instances,
);
*atlas = Arc::clone(bind_group);
}
_ => {}
}
add_instances(
*bounds,
*clip_bounds,
image.border_radius,
f32::from(image.rotation),
image.opacity,
image.snap,
atlas_entry,
match image.filter_method {
crate::core::image::FilterMethod::Nearest => {
&mut self.nearest_instances
}
crate::core::image::FilterMethod::Linear => {
&mut self.linear_instances
}
},
);
}
}
#[cfg(not(feature = "image"))]
Image::Raster { .. } => continue,
#[cfg(feature = "svg")]
Image::Vector {
svg,
bounds,
clip_bounds,
} => {
if let Some((atlas_entry, bind_group)) = cache
.upload_vector(
device,
encoder,
belt,
&svg.handle,
svg.color,
bounds.size(),
scale,
)
{
match atlas.as_mut() {
None => {
atlas = Some(bind_group.clone());
}
Some(atlas) if atlas != bind_group => {
layer.push(
atlas,
&self.nearest_instances,
&self.linear_instances,
);
*atlas = bind_group.clone();
}
_ => {}
}
add_instances(
*bounds,
*clip_bounds,
border::radius(0),
f32::from(svg.rotation),
svg.opacity,
true,
atlas_entry,
&mut self.nearest_instances,
);
}
}
#[cfg(not(feature = "svg"))]
Image::Vector { .. } => continue,
}
}
if let Some(atlas) = &atlas {
layer.push(atlas, &self.nearest_instances, &self.linear_instances);
}
layer.prepare(
device,
encoder,
belt,
nearest_instances,
linear_instances,
transformation,
scale,
&self.nearest_instances,
&self.linear_instances,
);
self.prepare_layer += 1;
self.nearest_instances.clear();
self.linear_instances.clear();
}
pub fn render<'a>(
&'a self,
pipeline: &'a Pipeline,
cache: &'a Cache,
layer: usize,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
@ -333,13 +393,15 @@ impl State {
bounds.height,
);
render_pass.set_bind_group(1, cache.bind_group(), &[]);
layer.render(render_pass);
}
}
pub fn trim(&mut self) {
for layer in &mut self.layers[..self.prepare_layer] {
layer.clear();
}
self.prepare_layer = 0;
}
}
@ -347,8 +409,19 @@ impl State {
#[derive(Debug)]
struct Layer {
uniforms: wgpu::Buffer,
nearest: Data,
linear: Data,
instances: Buffer<Instance>,
nearest: Vec<Group>,
nearest_layout: wgpu::BindGroup,
nearest_total: usize,
linear: Vec<Group>,
linear_layout: wgpu::BindGroup,
linear_total: usize,
}
#[derive(Debug)]
struct Group {
atlas: Arc<wgpu::BindGroup>,
instance_count: usize,
}
impl Layer {
@ -365,16 +438,70 @@ impl Layer {
mapped_at_creation: false,
});
let nearest =
Data::new(device, constant_layout, nearest_sampler, &uniforms);
let instances = Buffer::new(
device,
"iced_wgpu::image instance buffer",
Instance::INITIAL,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
let linear =
Data::new(device, constant_layout, linear_sampler, &uniforms);
let nearest_layout =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image constants bind group"),
layout: constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: &uniforms,
offset: 0,
size: None,
},
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
nearest_sampler,
),
},
],
});
let linear_layout =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image constants bind group"),
layout: constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: &uniforms,
offset: 0,
size: None,
},
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
linear_sampler,
),
},
],
});
Self {
uniforms,
nearest,
linear,
instances,
nearest: Vec::new(),
nearest_layout,
nearest_total: 0,
linear: Vec::new(),
linear_layout,
linear_total: 0,
}
}
@ -383,10 +510,10 @@ impl Layer {
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
nearest_instances: &[Instance],
linear_instances: &[Instance],
transformation: Transformation,
scale_factor: f32,
nearest: &[Instance],
linear: &[Instance],
) {
let uniforms = Uniforms {
transform: transformation.into(),
@ -405,102 +532,96 @@ impl Layer {
)
.copy_from_slice(bytes);
self.nearest
.upload(device, encoder, belt, nearest_instances);
let _ = self
.instances
.resize(device, self.nearest_total + self.linear_total);
self.linear.upload(device, encoder, belt, linear_instances);
}
let mut offset = 0;
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
self.nearest.render(render_pass);
self.linear.render(render_pass);
}
}
if !nearest.is_empty() {
offset += self.instances.write(device, encoder, belt, 0, nearest);
}
#[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,
if !linear.is_empty() {
let _ = self.instances.write(device, encoder, belt, offset, linear);
}
}
fn upload(
fn push(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
instances: &[Instance],
atlas: &Arc<wgpu::BindGroup>,
nearest: &[Instance],
linear: &[Instance],
) {
self.instance_count = instances.len();
let new_nearest = nearest.len() - self.nearest_total;
if self.instance_count == 0 {
return;
if new_nearest > 0 {
self.nearest.push(Group {
atlas: atlas.clone(),
instance_count: new_nearest,
});
self.nearest_total = nearest.len();
}
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(device, encoder, belt, 0, instances);
let new_linear = linear.len() - self.linear_total;
if new_linear > 0 {
self.linear.push(Group {
atlas: atlas.clone(),
instance_count: new_linear,
});
self.linear_total = linear.len();
}
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
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);
let mut offset = 0;
if !self.nearest.is_empty() {
render_pass.set_bind_group(0, &self.nearest_layout, &[]);
for group in &self.nearest {
render_pass.set_bind_group(1, group.atlas.as_ref(), &[]);
render_pass
.draw(0..6, offset..offset + group.instance_count as u32);
offset += group.instance_count as u32;
}
}
if !self.linear.is_empty() {
render_pass.set_bind_group(0, &self.linear_layout, &[]);
for group in &self.linear {
render_pass.set_bind_group(1, group.atlas.as_ref(), &[]);
render_pass
.draw(0..6, offset..offset + group.instance_count as u32);
offset += group.instance_count as u32;
}
}
}
fn clear(&mut self) {
self.nearest.clear();
self.nearest_total = 0;
self.linear.clear();
self.linear_total = 0;
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Instance {
_position: [f32; 2],
_center: [f32; 2],
_size: [f32; 2],
_clip_bounds: [f32; 4],
_border_radius: [f32; 4],
_tile: [f32; 4],
_rotation: f32,
_opacity: f32,
_position_in_atlas: [f32; 2],
@ -524,8 +645,9 @@ struct Uniforms {
}
fn add_instances(
image_position: [f32; 2],
image_size: [f32; 2],
bounds: Rectangle,
clip_bounds: Rectangle,
border_radius: border::Radius,
rotation: f32,
opacity: f32,
snap: bool,
@ -533,16 +655,26 @@ fn add_instances(
instances: &mut Vec<Instance>,
) {
let center = [
image_position[0] + image_size[0] / 2.0,
image_position[1] + image_size[1] / 2.0,
bounds.x + bounds.width / 2.0,
bounds.y + bounds.height / 2.0,
];
let clip_bounds = [
clip_bounds.x,
clip_bounds.y,
clip_bounds.width,
clip_bounds.height,
];
let border_radius = border_radius.into();
match entry {
atlas::Entry::Contiguous(allocation) => {
add_instance(
image_position,
center,
image_size,
clip_bounds,
border_radius,
[bounds.x, bounds.y, bounds.width, bounds.height],
rotation,
opacity,
snap,
@ -551,32 +683,35 @@ fn add_instances(
);
}
atlas::Entry::Fragmented { fragments, size } => {
let scaling_x = image_size[0] / size.width as f32;
let scaling_y = image_size[1] / size.height as f32;
let scaling_x = bounds.width / size.width as f32;
let scaling_y = bounds.height / size.height as f32;
for fragment in fragments {
let allocation = &fragment.allocation;
let [x, y] = image_position;
let (fragment_x, fragment_y) = fragment.position;
let Size {
width: fragment_width,
height: fragment_height,
} = allocation.size();
let position = [
x + fragment_x as f32 * scaling_x,
y + fragment_y as f32 * scaling_y,
];
let size = [
let tile = [
bounds.x + fragment_x as f32 * scaling_x,
bounds.y + fragment_y as f32 * scaling_y,
fragment_width as f32 * scaling_x,
fragment_height as f32 * scaling_y,
];
add_instance(
position, center, size, rotation, opacity, snap,
allocation, instances,
center,
clip_bounds,
border_radius,
tile,
rotation,
opacity,
snap,
allocation,
instances,
);
}
}
@ -585,9 +720,10 @@ fn add_instances(
#[inline]
fn add_instance(
position: [f32; 2],
center: [f32; 2],
size: [f32; 2],
clip_bounds: [f32; 4],
border_radius: [f32; 4],
tile: [f32; 4],
rotation: f32,
opacity: f32,
snap: bool,
@ -597,20 +733,22 @@ fn add_instance(
let (x, y) = allocation.position();
let Size { width, height } = allocation.size();
let layer = allocation.layer();
let atlas_size = allocation.atlas_size();
let instance = Instance {
_position: position,
_center: center,
_size: size,
_clip_bounds: clip_bounds,
_border_radius: border_radius,
_tile: tile,
_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,
x as f32 / atlas_size as f32,
y as f32 / atlas_size as f32,
],
_size_in_atlas: [
(width as f32 - 1.0) / atlas::SIZE as f32,
(height as f32 - 1.0) / atlas::SIZE as f32,
width as f32 / atlas_size as f32,
height as f32 / atlas_size as f32,
],
_layer: layer as u32,
_snap: snap as u32,

View file

@ -1,26 +1,35 @@
use crate::core::Size;
use crate::core::image;
use crate::graphics;
use crate::graphics::image::image_rs;
use crate::image::atlas::{self, Atlas};
use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::{Arc, Weak};
pub type Image = graphics::image::Buffer;
/// Entry in cache corresponding to an image handle
#[derive(Debug)]
pub enum Memory {
/// Image data on host
Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>),
Host(Image),
/// Storage entry
Device(atlas::Entry),
/// Image not found
NotFound,
/// Invalid image data
Invalid,
Device {
entry: atlas::Entry,
bind_group: Option<Arc<wgpu::BindGroup>>,
allocation: Option<Weak<image::Memory>>,
},
Error(image::Error),
}
impl Memory {
/// Width and height of image
pub fn load(handle: &image::Handle) -> Self {
match graphics::image::load(handle) {
Ok(image) => Self::Host(image),
Err(error) => Self::Error(error),
}
}
pub fn dimensions(&self) -> Size<u32> {
match self {
Memory::Host(image) => {
@ -28,14 +37,19 @@ impl Memory {
Size::new(width, height)
}
Memory::Device(entry) => entry.size(),
Memory::NotFound => Size::new(1, 1),
Memory::Invalid => Size::new(1, 1),
Memory::Device { entry, .. } => entry.size(),
Memory::Error(_) => Size::new(1, 1),
}
}
pub fn host(&self) -> Option<Image> {
match self {
Memory::Host(image) => Some(image.clone()),
Memory::Device { .. } | Memory::Error(_) => None,
}
}
}
/// Caches image raster data
#[derive(Debug, Default)]
pub struct Cache {
map: FxHashMap<image::Id, Memory>,
@ -44,51 +58,28 @@ pub struct Cache {
}
impl Cache {
/// Load image
pub fn load(&mut self, handle: &image::Handle) -> &mut Memory {
if self.contains(handle) {
return self.get(handle).unwrap();
}
pub fn get_mut(&mut self, handle: &image::Handle) -> Option<&mut Memory> {
let _ = self.hits.insert(handle.id());
let memory = match graphics::image::load(handle) {
Ok(image) => Memory::Host(image),
Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound,
Err(_) => Memory::Invalid,
};
self.map.get_mut(&handle.id())
}
pub fn insert(&mut self, handle: &image::Handle, memory: Memory) {
let _ = self.map.insert(handle.id(), memory);
let _ = self.hits.insert(handle.id());
self.should_trim = true;
self.insert(handle, memory);
self.get(handle).unwrap()
}
/// Load image and upload raster data
pub fn upload(
pub fn contains(&self, handle: &image::Handle) -> bool {
self.map.contains_key(&handle.id())
}
pub fn trim(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
handle: &image::Handle,
atlas: &mut Atlas,
) -> Option<&atlas::Entry> {
let memory = self.load(handle);
if let Memory::Host(image) = memory {
let (width, height) = image.dimensions();
let entry = atlas.upload(device, encoder, width, height, image)?;
*memory = Memory::Device(entry);
}
if let Memory::Device(allocation) = memory {
Some(allocation)
} else {
None
}
}
/// Trim cache misses from cache
pub fn trim(&mut self, atlas: &mut Atlas) {
on_drop: impl Fn(Arc<wgpu::BindGroup>),
) {
// Only trim if new entries have landed in the `Cache`
if !self.should_trim {
return;
@ -96,11 +87,31 @@ impl Cache {
let hits = &self.hits;
self.map.retain(|k, memory| {
let retain = hits.contains(k);
self.map.retain(|id, memory| {
// Retain active allocations
if let Memory::Device { allocation, .. } = memory
&& allocation
.as_ref()
.is_some_and(|allocation| allocation.strong_count() > 0)
{
return true;
}
if !retain && let Memory::Device(entry) = memory {
atlas.remove(entry);
let retain = hits.contains(id);
if !retain {
log::debug!("Dropping image allocation: {id:?}");
if let Memory::Device {
entry, bind_group, ..
} = memory
{
if let Some(bind_group) = bind_group.take() {
on_drop(bind_group);
} else {
atlas.remove(entry);
}
}
}
retain
@ -109,18 +120,4 @@ impl Cache {
self.hits.clear();
self.should_trim = false;
}
fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> {
let _ = self.hits.insert(handle.id());
self.map.get_mut(&handle.id())
}
fn insert(&mut self, handle: &image::Handle, memory: Memory) {
let _ = self.map.insert(handle.id(), memory);
}
fn contains(&self, handle: &image::Handle) -> bool {
self.map.contains_key(&handle.id())
}
}

View file

@ -94,17 +94,18 @@ impl Cache {
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &svg::Handle,
color: Option<Color>,
[width, height]: [f32; 2],
size: Size,
scale: f32,
atlas: &mut Atlas,
) -> Option<&atlas::Entry> {
let id = handle.id();
let (width, height) = (
(scale * width).ceil() as u32,
(scale * height).ceil() as u32,
(scale * size.width).ceil() as u32,
(scale * size.height).ceil() as u32,
);
let color = color.map(Color::into_rgba8);
@ -167,14 +168,15 @@ impl Cache {
});
}
let allocation =
atlas.upload(device, encoder, width, height, &rgba)?;
let allocation = atlas
.upload(device, encoder, belt, width, height, &rgba)?;
log::debug!("allocating {id} {width}x{height}");
let _ = self.svg_hits.insert(id);
let _ = self.rasterized_hits.insert(key);
let _ = self.rasterized.insert(key, allocation);
self.should_trim = true;
self.rasterized.get(&key)
}

View file

@ -128,11 +128,19 @@ impl Layer {
pub fn draw_image(&mut self, image: Image, transformation: Transformation) {
match image {
Image::Raster(image, bounds) => {
self.draw_raster(image, bounds, transformation);
Image::Raster {
image,
bounds,
clip_bounds,
} => {
self.draw_raster(image, bounds, clip_bounds, transformation);
}
Image::Vector(svg, bounds) => {
self.draw_svg(svg, bounds, transformation);
Image::Vector {
svg,
bounds,
clip_bounds,
} => {
self.draw_svg(svg, bounds, clip_bounds, transformation);
}
}
}
@ -141,9 +149,18 @@ impl Layer {
&mut self,
image: core::Image,
bounds: Rectangle,
clip_bounds: Rectangle,
transformation: Transformation,
) {
let image = Image::Raster(image, bounds * transformation);
let image = Image::Raster {
image: core::Image {
border_radius: image.border_radius
* transformation.scale_factor(),
..image
},
bounds: bounds * transformation,
clip_bounds: clip_bounds * transformation,
};
self.images.push(image);
}
@ -152,9 +169,14 @@ impl Layer {
&mut self,
svg: Svg,
bounds: Rectangle,
clip_bounds: Rectangle,
transformation: Transformation,
) {
let svg = Image::Vector(svg, bounds * transformation);
let svg = Image::Vector {
svg,
bounds: bounds * transformation,
clip_bounds: clip_bounds * transformation,
};
self.images.push(svg);
}

View file

@ -65,8 +65,8 @@ 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};
use crate::graphics::{Shell, Viewport};
/// A [`wgpu`] graphics renderer for [`iced`].
///
@ -117,9 +117,7 @@ impl Renderer {
image: image::State::new(),
#[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell::new(
engine.create_image_cache(&engine.device),
),
image_cache: std::cell::RefCell::new(engine.create_image_cache()),
// TODO: Resize belt smartly (?)
// It would be great if the `StagingBelt` API exposed methods
@ -313,8 +311,10 @@ impl Renderer {
self.layers.merge();
for layer in self.layers.iter() {
let clip_bounds = layer.bounds * scale_factor;
if physical_bounds
.intersection(&(layer.bounds * scale_factor))
.intersection(&clip_bounds)
.and_then(Rectangle::snap)
.is_none()
{
@ -460,8 +460,6 @@ impl Renderer {
#[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();
let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
@ -632,7 +630,6 @@ impl Renderer {
let render_span = debug::render(debug::Primitive::Image);
self.image.render(
&self.engine.image_pipeline,
&image_cache,
image_layer,
scissor_rect,
&mut render_pass,
@ -701,6 +698,19 @@ impl core::Renderer for Renderer {
fn reset(&mut self, new_bounds: Rectangle) {
self.layers.reset(new_bounds);
}
fn allocate_image(
&mut self,
_handle: &core::image::Handle,
_callback: impl FnOnce(Result<core::image::Allocation, core::image::Error>)
+ Send
+ 'static,
) {
#[cfg(feature = "image")]
self.image_cache
.get_mut()
.allocate_image(_handle, _callback);
}
}
impl core::text::Renderer for Renderer {
@ -765,13 +775,29 @@ impl core::text::Renderer for Renderer {
impl core::image::Renderer for Renderer {
type Handle = core::image::Handle;
fn measure_image(&self, handle: &Self::Handle) -> core::Size<u32> {
fn load_image(
&self,
handle: &Self::Handle,
) -> Result<core::image::Allocation, core::image::Error> {
self.image_cache.borrow_mut().load_image(
&self.engine.device,
&self.engine.queue,
handle,
)
}
fn measure_image(&self, handle: &Self::Handle) -> Option<core::Size<u32>> {
self.image_cache.borrow_mut().measure_image(handle)
}
fn draw_image(&mut self, image: core::Image, bounds: Rectangle) {
fn draw_image(
&mut self,
image: core::Image,
bounds: Rectangle,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_raster(image, bounds, transformation);
layer.draw_raster(image, bounds, clip_bounds, transformation);
}
}
@ -781,9 +807,14 @@ impl core::svg::Renderer for Renderer {
self.image_cache.borrow_mut().measure_svg(handle)
}
fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) {
fn draw_svg(
&mut self,
svg: core::Svg,
bounds: Rectangle,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_svg(svg, bounds, transformation);
layer.draw_svg(svg, bounds, clip_bounds, transformation);
}
}
@ -910,6 +941,7 @@ impl renderer::Headless for Renderer {
wgpu::TextureFormat::Rgba8Unorm
},
Some(graphics::Antialiasing::MSAAx4),
Shell::headless(),
);
Some(Self::new(engine, default_font, default_text_size))

View file

@ -9,63 +9,117 @@ struct Globals {
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) pos: vec2<f32>,
@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,
@location(8) snap: u32,
@location(0) center: vec2<f32>,
@location(1) clip_bounds: vec4<f32>,
@location(2) border_radius: vec4<f32>,
@location(3) tile: vec4<f32>,
@location(4) rotation: f32,
@location(5) opacity: f32,
@location(6) atlas_pos: vec2<f32>,
@location(7) atlas_scale: vec2<f32>,
@location(8) layer: i32,
@location(9) snap: u32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation.
@location(2) opacity: f32,
@location(0) @interpolate(flat) clip_bounds: vec4<f32>,
@location(1) @interpolate(flat) border_radius: vec4<f32>,
@location(2) @interpolate(flat) atlas: vec4<f32>,
@location(3) @interpolate(flat) layer: i32,
@location(4) @interpolate(flat) opacity: f32,
@location(5) uv: vec2<f32>,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Generate a vertex position in the range [0, 1] from the vertex index.
var v_pos = vertex_position(input.vertex_index);
// Generate a vertex position in the range [0, 1] from the vertex index
let corner = 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;
let tile = input.tile;
let center = input.center;
// 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>(0.0, 0.0, 0.0, 1.0)
// List the unrotated tile corners
let corners = array<vec2<f32>, 4>(
tile.xy, // Top left
tile.xy + vec2<f32>(tile.z, 0.0), // Top right
tile.xy + vec2<f32>(0.0, tile.w), // Bottom left
tile.xy + tile.zw // Bottom right
);
// Calculate the final position of the vertex
out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0));
// Rotate tile corners around center
let cos_r = cos(-input.rotation); // Clockwise
let sin_r = sin(-input.rotation);
var rotated = array<vec2<f32>, 4>();
for (var i = 0u; i < 4u; i++) {
let c = corners[i] - input.center;
rotated[i] = vec2<f32>(c.x * cos_r - c.y * sin_r, c.x * sin_r + c.y * cos_r) + input.center;
}
// Find bounding box of rotated tile
var min_xy = rotated[0];
var max_xy = rotated[0];
for (var i = 1u; i < 4u; i++) {
min_xy = min(min_xy, rotated[i]);
max_xy = max(max_xy, rotated[i]);
}
let rotated_bounds = vec4<f32>(min_xy, max_xy - min_xy);
// Intersect with clip bounds
let clip_min = max(rotated_bounds.xy, input.clip_bounds.xy);
let clip_max = min(rotated_bounds.xy + rotated_bounds.zw, input.clip_bounds.xy + input.clip_bounds.zw);
let clipped_tile = vec4<f32>(clip_min, max(vec2<f32>(0.0), clip_max - clip_min));
// Calculate the vertex position
let v_pos = clipped_tile.xy + corner * clipped_tile.zw;
out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * vec4<f32>(v_pos, 0.0, 1.0);
// Calculate rotated UV
let uv = input.atlas_pos + (v_pos - tile.xy) / tile.zw * input.atlas_scale;
let uv_center = input.atlas_pos + input.atlas_scale / 2.0;
let d = uv - uv_center;
out.uv = vec2<f32>(d.x * cos_r - d.y * sin_r, d.x * sin_r + d.y * cos_r) + uv_center;
// Snap position to the pixel grid
if bool(input.snap) {
out.position = round(out.position);
}
out.position = globals.transform * out.position;
out.clip_bounds = globals.scale_factor * input.clip_bounds;
out.border_radius = globals.scale_factor * input.border_radius;
out.atlas = vec4(input.atlas_pos, input.atlas_pos + input.atlas_scale);
out.layer = input.layer;
out.opacity = input.opacity;
return out;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
// 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);
let fragment = input.position.xy;
let position = input.clip_bounds.xy;
let scale = input.clip_bounds.zw;
let d = rounded_box_sdf(
2.0 * (fragment - position - scale / 2.0),
scale,
input.border_radius * 2.0,
) / 2.0;
let antialias: f32 = clamp(1.0 - d, 0.0, 1.0);
let inside = all(input.uv >= input.atlas.xy) && all(input.uv <= input.atlas.zw);
return textureSample(u_texture, u_sampler, input.uv, input.layer) * vec4<f32>(1.0, 1.0, 1.0, antialias * input.opacity * f32(inside));
}
fn rounded_box_sdf(p: vec2<f32>, size: vec2<f32>, corners: vec4<f32>) -> f32 {
var box_half = select(corners.yz, corners.xw, p.x > 0.0);
var corner = select(box_half.y, box_half.x, p.y > 0.0);
var q = abs(p) - size + corner;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner;
}

View file

@ -3,7 +3,7 @@ use crate::core::Color;
use crate::graphics::color;
use crate::graphics::compositor;
use crate::graphics::error;
use crate::graphics::{self, Viewport};
use crate::graphics::{self, Shell, Viewport};
use crate::settings::{self, Settings};
use crate::{Engine, Renderer};
@ -50,6 +50,7 @@ impl Compositor {
pub async fn request<W: compositor::Window>(
settings: Settings,
compatible_window: Option<W>,
shell: Shell,
) -> Result<Self, Error> {
let instance = wgpu::util::new_instance_with_webgpu_detection(
&wgpu::InstanceDescriptor {
@ -181,6 +182,7 @@ impl Compositor {
queue,
format,
settings.antialiasing,
shell,
);
return Ok(Compositor {
@ -206,8 +208,9 @@ impl Compositor {
pub async fn new<W: compositor::Window>(
settings: Settings,
compatible_window: W,
shell: Shell,
) -> Result<Compositor, Error> {
Compositor::request(settings, Some(compatible_window)).await
Compositor::request(settings, Some(compatible_window), shell).await
}
/// Presents the given primitives with the given [`Compositor`].
@ -260,6 +263,7 @@ impl graphics::Compositor for Compositor {
async fn with_backend<W: compositor::Window>(
settings: graphics::Settings,
compatible_window: W,
shell: Shell,
backend: Option<&str>,
) -> Result<Self, graphics::Error> {
match backend {
@ -274,7 +278,7 @@ impl graphics::Compositor for Compositor {
settings.present_mode = present_mode;
}
Ok(new(settings, compatible_window).await?)
Ok(new(settings, compatible_window, shell).await?)
}
Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound {
backend: "wgpu",