Draft multi-threaded image rendering in iced_wgpu

This commit is contained in:
Héctor Ramón Jiménez 2025-10-24 17:23:40 +02:00
parent 92888a3639
commit cb8d2710da
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
22 changed files with 886 additions and 305 deletions

1
Cargo.lock generated
View file

@ -2620,6 +2620,7 @@ version = "0.14.0-dev"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"bytemuck", "bytemuck",
"bytes",
"cryoglyph", "cryoglyph",
"futures", "futures",
"glam", "glam",

View file

@ -72,7 +72,7 @@ fn benchmark<'a>(
view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>, view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>,
) { ) {
use iced_wgpu::graphics; use iced_wgpu::graphics;
use iced_wgpu::graphics::Antialiasing; use iced_wgpu::graphics::{Antialiasing, Shell};
use iced_wgpu::wgpu; use iced_wgpu::wgpu;
use iced_winit::core; use iced_winit::core;
use iced_winit::runtime; use iced_winit::runtime;
@ -85,6 +85,7 @@ fn benchmark<'a>(
queue.clone(), queue.clone(),
format, format,
Some(Antialiasing::MSAAx4), Some(Antialiasing::MSAAx4),
Shell::headless(),
); );
let mut renderer = Renderer::new(engine, Font::DEFAULT, Pixels::from(16)); let mut renderer = Renderer::new(engine, Font::DEFAULT, Pixels::from(16));

View file

@ -18,7 +18,7 @@ use iced::{
Subscription, Task, Theme, color, Subscription, Task, Theme, color,
}; };
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
fn main() -> iced::Result { fn main() -> iced::Result {
iced::application::timed( iced::application::timed(
@ -35,6 +35,8 @@ fn main() -> iced::Result {
struct Gallery { struct Gallery {
images: Vec<Image>, images: Vec<Image>,
previews: HashMap<Id, Preview>, previews: HashMap<Id, Preview>,
visible: HashSet<Id>,
downloaded: HashSet<Id>,
viewer: Viewer, viewer: Viewer,
now: Instant, now: Instant,
} }
@ -43,6 +45,7 @@ struct Gallery {
enum Message { enum Message {
ImagesListed(Result<Vec<Image>, Error>), ImagesListed(Result<Vec<Image>, Error>),
ImagePoppedIn(Id), ImagePoppedIn(Id),
ImagePoppedOut(Id),
ImageDownloaded(Result<Rgba, Error>), ImageDownloaded(Result<Rgba, Error>),
ThumbnailDownloaded(Id, Result<Rgba, Error>), ThumbnailDownloaded(Id, Result<Rgba, Error>),
ThumbnailHovered(Id, bool), ThumbnailHovered(Id, bool),
@ -58,6 +61,8 @@ impl Gallery {
Self { Self {
images: Vec::new(), images: Vec::new(),
previews: HashMap::new(), previews: HashMap::new(),
visible: HashSet::new(),
downloaded: HashSet::new(),
viewer: Viewer::new(), viewer: Viewer::new(),
now: Instant::now(), now: Instant::now(),
}, },
@ -102,6 +107,14 @@ impl Gallery {
return Task::none(); return Task::none();
}; };
let _ = self.visible.insert(id);
if self.downloaded.contains(&id) {
return Task::none();
}
let _ = self.downloaded.insert(id);
Task::sip( Task::sip(
image.download(Size::Thumbnail { image.download(Size::Thumbnail {
width: Preview::WIDTH, width: Preview::WIDTH,
@ -111,6 +124,11 @@ impl Gallery {
Message::ThumbnailDownloaded.with(id), Message::ThumbnailDownloaded.with(id),
) )
} }
Message::ImagePoppedOut(id) => {
let _ = self.visible.remove(&id);
Task::none()
}
Message::ImageDownloaded(Ok(rgba)) => { Message::ImageDownloaded(Ok(rgba)) => {
self.viewer.show(rgba, self.now); self.viewer.show(rgba, self.now);
@ -181,7 +199,17 @@ impl Gallery {
let images = self let images = self
.images .images
.iter() .iter()
.map(|image| card(image, self.previews.get(&image.id), self.now)) .map(|image| {
card(
image,
if self.visible.contains(&image.id) {
self.previews.get(&image.id)
} else {
None
},
self.now,
)
})
.chain((self.images.len()..=Image::LIMIT).map(|_| placeholder())); .chain((self.images.len()..=Image::LIMIT).map(|_| placeholder()));
let gallery = grid(images) let gallery = grid(images)
@ -248,7 +276,7 @@ fn card<'a>(
.on_enter(Message::ThumbnailHovered(metadata.id, true)) .on_enter(Message::ThumbnailHovered(metadata.id, true))
.on_exit(Message::ThumbnailHovered(metadata.id, false)); .on_exit(Message::ThumbnailHovered(metadata.id, false));
if let Some(preview) = preview { let card: Element<'_, _> = if let Some(preview) = preview {
let is_thumbnail = matches!(preview, Preview::Ready { .. }); let is_thumbnail = matches!(preview, Preview::Ready { .. });
button(card) button(card)
@ -257,10 +285,13 @@ fn card<'a>(
.style(button::text) .style(button::text)
.into() .into()
} else { } else {
sensor(card) card.into()
.on_show(|_| Message::ImagePoppedIn(metadata.id)) };
.into()
} sensor(card)
.on_show(|_| Message::ImagePoppedIn(metadata.id))
.on_hide(Message::ImagePoppedOut(metadata.id))
.into()
} }
fn placeholder<'a>() -> Element<'a, Message> { fn placeholder<'a>() -> Element<'a, Message> {

View file

@ -4,7 +4,7 @@ mod scene;
use controls::Controls; use controls::Controls;
use scene::Scene; use scene::Scene;
use iced_wgpu::graphics::Viewport; use iced_wgpu::graphics::{Shell, Viewport};
use iced_wgpu::{Engine, Renderer, wgpu}; use iced_wgpu::{Engine, Renderer, wgpu};
use iced_winit::Clipboard; use iced_winit::Clipboard;
use iced_winit::conversion; use iced_winit::conversion;
@ -150,6 +150,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
queue.clone(), queue.clone(),
format, format,
None, None,
Shell::headless(),
); );
Renderer::new(engine, Font::default(), Pixels::from(16)) Renderer::new(engine, Font::default(), Pixels::from(16))

View file

@ -2,7 +2,7 @@
//! surfaces. //! surfaces.
use crate::core::Color; use crate::core::Color;
use crate::futures::{MaybeSend, MaybeSync}; use crate::futures::{MaybeSend, MaybeSync};
use crate::{Error, Settings, Viewport}; use crate::{Error, Settings, Shell, Viewport};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use thiserror::Error; use thiserror::Error;
@ -21,8 +21,9 @@ pub trait Compositor: Sized {
fn new<W: Window + Clone>( fn new<W: Window + Clone>(
settings: Settings, settings: Settings,
compatible_window: W, compatible_window: W,
shell: Shell,
) -> impl Future<Output = Result<Self, Error>> { ) -> impl Future<Output = Result<Self, Error>> {
Self::with_backend(settings, compatible_window, None) Self::with_backend(settings, compatible_window, shell, None)
} }
/// Creates a new [`Compositor`] with a backend preference. /// Creates a new [`Compositor`] with a backend preference.
@ -32,6 +33,7 @@ pub trait Compositor: Sized {
fn with_backend<W: Window + Clone>( fn with_backend<W: Window + Clone>(
_settings: Settings, _settings: Settings,
_compatible_window: W, _compatible_window: W,
_shell: Shell,
_backend: Option<&str>, _backend: Option<&str>,
) -> impl Future<Output = Result<Self, Error>>; ) -> impl Future<Output = Result<Self, Error>>;
@ -153,6 +155,7 @@ impl Compositor for () {
async fn with_backend<W: Window + Clone>( async fn with_backend<W: Window + Clone>(
_settings: Settings, _settings: Settings,
_compatible_window: W, _compatible_window: W,
_shell: Shell,
_preferred_backend: Option<&str>, _preferred_backend: Option<&str>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
Ok(()) Ok(())

View file

@ -21,6 +21,7 @@ pub mod gradient;
pub mod image; pub mod image;
pub mod layer; pub mod layer;
pub mod mesh; pub mod mesh;
pub mod shell;
pub mod text; pub mod text;
#[cfg(feature = "geometry")] #[cfg(feature = "geometry")]
@ -35,6 +36,7 @@ pub use image::Image;
pub use layer::Layer; pub use layer::Layer;
pub use mesh::Mesh; pub use mesh::Mesh;
pub use settings::Settings; pub use settings::Settings;
pub use shell::Shell;
pub use text::Text; pub use text::Text;
pub use viewport::Viewport; pub use viewport::Viewport;

45
graphics/src/shell.rs Normal file
View file

@ -0,0 +1,45 @@
//! Control the windowing runtime from a renderer.
use std::sync::Arc;
/// A windowing shell.
#[derive(Clone)]
pub struct Shell(Arc<dyn Notifier>);
impl Shell {
/// Creates a new [`Shell`].
pub fn new(notifier: impl Notifier) -> Self {
Self(Arc::new(notifier))
}
/// Creates a headless [`Shell`].
pub fn headless() -> Self {
struct Headless;
impl Notifier for Headless {
fn request_redraw(&self) {}
fn invalidate_layout(&self) {}
}
Self::new(Headless)
}
/// Requests for all windows of the [`Shell`] to be redrawn.
pub fn request_redraw(&self) {
self.0.request_redraw();
}
/// Requests for all layouts of the [`Shell`] to be recomputed.
pub fn invalidate_layout(&self) {
self.0.invalidate_layout();
}
}
/// A type that can notify a shell of certain events.
pub trait Notifier: Send + Sync + 'static {
/// Requests for all windows of the [`Shell`] to be redrawn.
fn request_redraw(&self);
/// Requests for all layouts of the [`Shell`] to be recomputed.
fn invalidate_layout(&self);
}

View file

@ -6,9 +6,9 @@ use crate::core::{
self, Background, Color, Font, Image, Pixels, Point, Rectangle, Size, Svg, self, Background, Color, Font, Image, Pixels, Point, Rectangle, Size, Svg,
Transformation, Transformation,
}; };
use crate::graphics;
use crate::graphics::compositor; use crate::graphics::compositor;
use crate::graphics::mesh; use crate::graphics::mesh;
use crate::graphics::{self, Shell};
use std::borrow::Cow; use std::borrow::Cow;
@ -216,6 +216,7 @@ where
async fn with_backend<W: compositor::Window + Clone>( async fn with_backend<W: compositor::Window + Clone>(
settings: graphics::Settings, settings: graphics::Settings,
compatible_window: W, compatible_window: W,
shell: Shell,
backend: Option<&str>, backend: Option<&str>,
) -> Result<Self, graphics::Error> { ) -> Result<Self, graphics::Error> {
use std::env; use std::env;
@ -242,8 +243,13 @@ where
let mut errors = vec![]; let mut errors = vec![];
for backend in candidates.iter().map(Option::as_deref) { for backend in candidates.iter().map(Option::as_deref) {
match A::with_backend(settings, compatible_window.clone(), backend) match A::with_backend(
.await settings,
compatible_window.clone(),
shell.clone(),
backend,
)
.await
{ {
Ok(compositor) => return Ok(Self::Primary(compositor)), Ok(compositor) => return Ok(Self::Primary(compositor)),
Err(error) => { Err(error) => {
@ -251,8 +257,13 @@ where
} }
} }
match B::with_backend(settings, compatible_window.clone(), backend) match B::with_backend(
.await settings,
compatible_window.clone(),
shell.clone(),
backend,
)
.await
{ {
Ok(compositor) => return Ok(Self::Secondary(compositor)), Ok(compositor) => return Ok(Self::Secondary(compositor)),
Err(error) => { Err(error) => {

View file

@ -174,6 +174,12 @@ pub enum Action {
/// Set the window size increment. /// Set the window size increment.
SetResizeIncrements(Id, Option<Size>), SetResizeIncrements(Id, Option<Size>),
/// Redraws all the windows.
RedrawAll,
/// Recomputes the layouts of all the windows.
RelayoutAll,
} }
/// Subscribes to the frames of the window of the running application. /// Subscribes to the frames of the window of the running application.

View file

@ -2,7 +2,7 @@ use crate::core::{Color, Rectangle, Size};
use crate::graphics::compositor::{self, Information}; use crate::graphics::compositor::{self, Information};
use crate::graphics::damage; use crate::graphics::damage;
use crate::graphics::error::{self, Error}; use crate::graphics::error::{self, Error};
use crate::graphics::{self, Viewport}; use crate::graphics::{self, Shell, Viewport};
use crate::{Layer, Renderer, Settings}; use crate::{Layer, Renderer, Settings};
use std::collections::VecDeque; use std::collections::VecDeque;
@ -31,6 +31,7 @@ impl crate::graphics::Compositor for Compositor {
async fn with_backend<W: compositor::Window>( async fn with_backend<W: compositor::Window>(
settings: graphics::Settings, settings: graphics::Settings,
compatible_window: W, compatible_window: W,
_shell: Shell,
backend: Option<&str>, backend: Option<&str>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
match backend { match backend {

View file

@ -31,6 +31,7 @@ iced_graphics.workspace = true
bitflags.workspace = true bitflags.workspace = true
bytemuck.workspace = true bytemuck.workspace = true
bytes.workspace = true
futures.workspace = true futures.workspace = true
glam.workspace = true glam.workspace = true
cryoglyph.workspace = true cryoglyph.workspace = true

View file

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

View file

@ -10,20 +10,20 @@ pub use layer::Layer;
use allocator::Allocator; use allocator::Allocator;
pub const SIZE: u32 = 2048; pub const DEFAULT_SIZE: u32 = 512;
pub const MAX_SIZE: u32 = 2048;
use crate::core::Size; use crate::core::Size;
use crate::graphics::color; use crate::graphics::color;
use std::sync::Arc;
#[derive(Debug)] #[derive(Debug)]
pub struct Atlas { pub struct Atlas {
size: u32,
backend: wgpu::Backend, backend: wgpu::Backend,
texture: wgpu::Texture, texture: wgpu::Texture,
texture_view: wgpu::TextureView, texture_view: wgpu::TextureView,
texture_bind_group: wgpu::BindGroup, texture_bind_group: wgpu::BindGroup,
texture_layout: Arc<wgpu::BindGroupLayout>, texture_layout: wgpu::BindGroupLayout,
layers: Vec<Layer>, layers: Vec<Layer>,
} }
@ -31,8 +31,19 @@ impl Atlas {
pub fn new( pub fn new(
device: &wgpu::Device, device: &wgpu::Device,
backend: wgpu::Backend, backend: wgpu::Backend,
texture_layout: Arc<wgpu::BindGroupLayout>, texture_layout: wgpu::BindGroupLayout,
) -> Self { ) -> 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 { let layers = match backend {
// On the GL backend we start with 2 layers, to help wgpu figure // 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` // out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D`
@ -42,8 +53,8 @@ impl Atlas {
}; };
let extent = wgpu::Extent3d { let extent = wgpu::Extent3d {
width: SIZE, width: size,
height: SIZE, height: size,
depth_or_array_layers: layers.len() as u32, depth_or_array_layers: layers.len() as u32,
}; };
@ -80,6 +91,7 @@ impl Atlas {
}); });
Atlas { Atlas {
size,
backend, backend,
texture, texture,
texture_view, texture_view,
@ -93,14 +105,11 @@ impl Atlas {
&self.texture_bind_group &self.texture_bind_group
} }
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn upload( pub fn upload(
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
width: u32, width: u32,
height: u32, height: u32,
data: &[u8], data: &[u8],
@ -111,7 +120,7 @@ impl Atlas {
// We grow the internal texture after allocating if necessary // We grow the internal texture after allocating if necessary
let new_layers = self.layers.len() - current_size; let new_layers = self.layers.len() - current_size;
self.grow(new_layers, device, encoder); self.grow(new_layers, device, encoder, self.backend);
entry entry
}; };
@ -127,7 +136,13 @@ impl Atlas {
let padded_width = (4 * width + padding) as usize; let padded_width = (4 * width + padding) as usize;
let padded_data_size = padded_width * height as usize; let padded_data_size = padded_width * height as usize;
let mut padded_data = vec![0; padded_data_size]; let buffer_slice = belt.allocate(
wgpu::BufferSize::new(padded_data_size as u64).unwrap(),
wgpu::BufferSize::new(8 * 4).unwrap(),
device,
);
let mut padded_data = buffer_slice.get_mapped_range_mut();
for row in 0..height as usize { for row in 0..height as usize {
let offset = row * padded_width; let offset = row * padded_width;
@ -140,13 +155,12 @@ impl Atlas {
match &entry { match &entry {
Entry::Contiguous(allocation) => { Entry::Contiguous(allocation) => {
self.upload_allocation( self.upload_allocation(
&padded_data, buffer_slice.buffer(),
width, width,
height, height,
padding, padding,
0, buffer_slice.offset() as usize,
allocation, allocation,
device,
encoder, encoder,
); );
} }
@ -156,13 +170,12 @@ impl Atlas {
let offset = (y * padded_width as u32 + 4 * x) as usize; let offset = (y * padded_width as u32 + 4 * x) as usize;
self.upload_allocation( self.upload_allocation(
&padded_data, buffer_slice.buffer(),
width, width,
height, height,
padding, padding,
offset, offset + buffer_slice.offset() as usize,
&fragment.allocation, &fragment.allocation,
device,
encoder, encoder,
); );
} }
@ -172,7 +185,7 @@ impl Atlas {
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
log::debug!( log::debug!(
"Atlas layers: {} (busy: {}, allocations: {})", "Atlas layers: {} (busy: {}, allocations: {})",
self.layer_count(), self.layers.len(),
self.layers.iter().filter(|layer| !layer.is_empty()).count(), self.layers.iter().filter(|layer| !layer.is_empty()).count(),
self.layers.iter().map(Layer::allocations).sum::<usize>(), self.layers.iter().map(Layer::allocations).sum::<usize>(),
); );
@ -198,7 +211,7 @@ impl Atlas {
fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> { fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> {
// Allocate one layer if texture fits perfectly // Allocate one layer if texture fits perfectly
if width == SIZE && height == SIZE { if width == self.size && height == self.size {
let mut empty_layers = self let mut empty_layers = self
.layers .layers
.iter_mut() .iter_mut()
@ -208,27 +221,31 @@ impl Atlas {
if let Some((i, layer)) = empty_layers.next() { if let Some((i, layer)) = empty_layers.next() {
*layer = Layer::Full; *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); self.layers.push(Layer::Full);
return Some(Entry::Contiguous(Allocation::Full { return Some(Entry::Contiguous(Allocation::Full {
layer: self.layers.len() - 1, layer: self.layers.len() - 1,
size: self.size,
})); }));
} }
// Split big textures across multiple layers // 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 fragments = Vec::new();
let mut y = 0; let mut y = 0;
while y < height { while y < height {
let height = std::cmp::min(height - y, SIZE); let height = std::cmp::min(height - y, self.size);
let mut x = 0; let mut x = 0;
while x < width { 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)?; let allocation = self.allocate(width, height)?;
@ -255,7 +272,7 @@ impl Atlas {
for (i, layer) in self.layers.iter_mut().enumerate() { for (i, layer) in self.layers.iter_mut().enumerate() {
match layer { match layer {
Layer::Empty => { Layer::Empty => {
let mut allocator = Allocator::new(SIZE); let mut allocator = Allocator::new(self.size);
if let Some(region) = allocator.allocate(width, height) { if let Some(region) = allocator.allocate(width, height) {
*layer = Layer::Busy(allocator); *layer = Layer::Busy(allocator);
@ -263,6 +280,7 @@ impl Atlas {
return Some(Entry::Contiguous(Allocation::Partial { return Some(Entry::Contiguous(Allocation::Partial {
region, region,
layer: i, layer: i,
atlas_size: self.size,
})); }));
} }
} }
@ -271,6 +289,7 @@ impl Atlas {
return Some(Entry::Contiguous(Allocation::Partial { return Some(Entry::Contiguous(Allocation::Partial {
region, region,
layer: i, layer: i,
atlas_size: self.size,
})); }));
} }
} }
@ -279,7 +298,7 @@ impl Atlas {
} }
// Create new layer with atlas allocator // 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) { if let Some(region) = allocator.allocate(width, height) {
self.layers.push(Layer::Busy(allocator)); self.layers.push(Layer::Busy(allocator));
@ -287,6 +306,7 @@ impl Atlas {
return Some(Entry::Contiguous(Allocation::Partial { return Some(Entry::Contiguous(Allocation::Partial {
region, region,
layer: self.layers.len() - 1, layer: self.layers.len() - 1,
atlas_size: self.size,
})); }));
} }
@ -298,10 +318,10 @@ impl Atlas {
log::debug!("Deallocating atlas: {allocation:?}"); log::debug!("Deallocating atlas: {allocation:?}");
match allocation { match allocation {
Allocation::Full { layer } => { Allocation::Full { layer, .. } => {
self.layers[*layer] = Layer::Empty; self.layers[*layer] = Layer::Empty;
} }
Allocation::Partial { layer, region } => { Allocation::Partial { layer, region, .. } => {
let layer = &mut self.layers[*layer]; let layer = &mut self.layers[*layer];
if let Layer::Busy(allocator) = layer { if let Layer::Busy(allocator) = layer {
@ -316,18 +336,15 @@ impl Atlas {
} }
fn upload_allocation( fn upload_allocation(
&mut self, &self,
data: &[u8], buffer: &wgpu::Buffer,
image_width: u32, image_width: u32,
image_height: u32, image_height: u32,
padding: u32, padding: u32,
offset: usize, offset: usize,
allocation: &Allocation, allocation: &Allocation,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
) { ) {
use wgpu::util::DeviceExt;
let (x, y) = allocation.position(); let (x, y) = allocation.position();
let Size { width, height } = allocation.size(); let Size { width, height } = allocation.size();
let layer = allocation.layer(); let layer = allocation.layer();
@ -338,16 +355,9 @@ impl Atlas {
depth_or_array_layers: 1, depth_or_array_layers: 1,
}; };
let buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image upload buffer"),
contents: data,
usage: wgpu::BufferUsages::COPY_SRC,
});
encoder.copy_buffer_to_texture( encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo { wgpu::TexelCopyBufferInfo {
buffer: &buffer, buffer,
layout: wgpu::TexelCopyBufferLayout { layout: wgpu::TexelCopyBufferLayout {
offset: offset as u64, offset: offset as u64,
bytes_per_row: Some(4 * image_width + padding), bytes_per_row: Some(4 * image_width + padding),
@ -373,6 +383,7 @@ impl Atlas {
amount: usize, amount: usize,
device: &wgpu::Device, device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
backend: wgpu::Backend,
) { ) {
if amount == 0 { if amount == 0 {
return; return;
@ -383,7 +394,7 @@ impl Atlas {
// some unused memory on GL, but it's better than not being able to grow the atlas past a depth // some unused memory on GL, but it's better than not being able to grow the atlas past a depth
// of 6! // of 6!
// https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371 // 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, wgpu::Backend::Gl if self.layers.len() == 6 => 7,
_ => self.layers.len() as u32, _ => self.layers.len() as u32,
}; };
@ -391,8 +402,8 @@ impl Atlas {
let new_texture = device.create_texture(&wgpu::TextureDescriptor { let new_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu::image texture atlas"), label: Some("iced_wgpu::image texture atlas"),
size: wgpu::Extent3d { size: wgpu::Extent3d {
width: SIZE, width: self.size,
height: SIZE, height: self.size,
depth_or_array_layers, depth_or_array_layers,
}, },
mip_level_count: 1, mip_level_count: 1,
@ -440,8 +451,8 @@ impl Atlas {
aspect: wgpu::TextureAspect::default(), aspect: wgpu::TextureAspect::default(),
}, },
wgpu::Extent3d { wgpu::Extent3d {
width: SIZE, width: self.size,
height: SIZE, height: self.size,
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
); );

View file

@ -1,14 +1,16 @@
use crate::core::Size; use crate::core::Size;
use crate::image::atlas::{self, allocator}; use crate::image::atlas::allocator;
#[derive(Debug)] #[derive(Debug)]
pub enum Allocation { pub enum Allocation {
Partial { Partial {
layer: usize, layer: usize,
region: allocator::Region, region: allocator::Region,
atlas_size: u32,
}, },
Full { Full {
layer: usize, layer: usize,
size: u32,
}, },
} }
@ -23,14 +25,21 @@ impl Allocation {
pub fn size(&self) -> Size<u32> { pub fn size(&self) -> Size<u32> {
match self { match self {
Allocation::Partial { region, .. } => region.size(), Allocation::Partial { region, .. } => region.size(),
Allocation::Full { .. } => Size::new(atlas::SIZE, atlas::SIZE), Allocation::Full { size, .. } => Size::new(*size, *size),
} }
} }
pub fn layer(&self) -> usize { pub fn layer(&self) -> usize {
match self { match self {
Allocation::Partial { layer, .. } => *layer, 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,47 +1,79 @@
use crate::core::{self, Size}; use crate::core::{self, Size};
use crate::graphics::Shell;
use crate::image::atlas::{self, Atlas}; use crate::image::atlas::{self, Atlas};
use std::sync::Arc; use std::collections::BTreeSet;
use std::sync::mpsc;
use std::thread;
#[derive(Debug)] #[derive(Debug)]
pub struct Cache { pub struct Cache {
atlas: Atlas, atlas: Atlas,
#[cfg(feature = "image")] #[cfg(feature = "image")]
raster: crate::image::raster::Cache, raster: Raster,
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
vector: crate::image::vector::Cache, vector: crate::image::vector::Cache,
#[cfg(feature = "image")]
jobs: mpsc::SyncSender<Job>,
#[cfg(feature = "image")]
work: mpsc::Receiver<Work>,
#[cfg(feature = "image")]
worker_: Option<thread::JoinHandle<()>>,
} }
impl Cache { impl Cache {
pub fn new( pub fn new(
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue,
backend: wgpu::Backend, backend: wgpu::Backend,
layout: Arc<wgpu::BindGroupLayout>, layout: wgpu::BindGroupLayout,
shell: &Shell,
) -> Self { ) -> Self {
#[cfg(feature = "image")]
let (worker, jobs, work) =
Worker::new(device, queue, backend, layout.clone(), shell);
#[cfg(feature = "image")]
let handle = thread::spawn(move || worker.run());
Self { Self {
atlas: Atlas::new(device, backend, layout), atlas: Atlas::new(device, backend, layout),
#[cfg(feature = "image")] #[cfg(feature = "image")]
raster: crate::image::raster::Cache::default(), raster: Raster {
cache: crate::image::raster::Cache::default(),
pending: BTreeSet::new(),
jobs: jobs.clone(),
},
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
vector: crate::image::vector::Cache::default(), vector: crate::image::vector::Cache::default(),
#[cfg(feature = "image")]
jobs,
#[cfg(feature = "image")]
work,
#[cfg(feature = "image")]
worker_: Some(handle),
} }
} }
pub fn bind_group(&self) -> &wgpu::BindGroup {
self.atlas.bind_group()
}
pub fn layer_count(&self) -> usize {
self.atlas.layer_count()
}
#[cfg(feature = "image")] #[cfg(feature = "image")]
pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> { pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> {
self.raster.load(handle).dimensions() self.receive();
if let Some(memory) = load_image(
&mut self.raster.cache,
&mut self.raster.pending,
&mut self.raster.jobs,
handle,
) {
return memory.dimensions();
}
Size::new(0, 0)
} }
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> { pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> {
// TODO: Concurrency
self.vector.load(handle).viewport_dimensions() self.vector.load(handle).viewport_dimensions()
} }
@ -50,9 +82,63 @@ impl Cache {
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &core::image::Handle, handle: &core::image::Handle,
) -> Option<&atlas::Entry> { ) -> Option<(&atlas::Entry, &wgpu::BindGroup)> {
self.raster.upload(device, encoder, handle, &mut self.atlas) use crate::image::raster::Memory;
self.receive();
let memory = load_image(
&mut self.raster.cache,
&mut self.raster.pending,
&mut self.raster.jobs,
handle,
)?;
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;
if image.len() < MAX_SYNC_SIZE {
let entry = self.atlas.upload(
device,
encoder,
belt,
image.width(),
image.height(),
&image,
)?;
*memory = Memory::Device {
entry,
bind_group: None,
};
if let Memory::Device { entry, .. } = memory {
return Some((entry, self.atlas.bind_group()));
}
}
if !self.raster.pending.contains(&handle.id()) {
let _ = self.jobs.send(Job::Upload {
handle: handle.clone(),
rgba: image.clone().into_raw(),
width: image.width(),
height: image.height(),
});
let _ = self.raster.pending.insert(handle.id());
}
None
} }
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
@ -60,27 +146,261 @@ impl Cache {
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &core::svg::Handle, handle: &core::svg::Handle,
color: Option<core::Color>, color: Option<core::Color>,
size: [f32; 2], size: [f32; 2],
scale: f32, scale: f32,
) -> Option<&atlas::Entry> { ) -> Option<(&atlas::Entry, &wgpu::BindGroup)> {
self.vector.upload( // TODO: Concurrency
device, self.vector
encoder, .upload(
handle, device,
color, encoder,
size, belt,
scale, handle,
&mut self.atlas, color,
) size,
scale,
&mut self.atlas,
)
.map(|entry| (entry, self.atlas.bind_group()))
} }
pub fn trim(&mut self) { pub fn trim(&mut self) {
#[cfg(feature = "image")] #[cfg(feature = "image")]
self.raster.trim(&mut self.atlas); self.raster.cache.trim(&mut self.atlas, |bind_group| {
let _ = self.jobs.send(Job::Drop(bind_group));
});
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
self.vector.trim(&mut self.atlas); self.vector.trim(&mut self.atlas); // TODO: Concurrency
}
fn receive(&mut self) {
use crate::image::raster::Memory;
while let Ok(work) = self.work.try_recv() {
match work {
Work::Upload {
handle,
entry,
bind_group,
} => {
self.raster.cache.insert(
&handle,
Memory::Device {
entry,
bind_group: Some(bind_group),
},
);
let _ = self.raster.pending.remove(&handle.id());
}
Work::Error { handle, error } => {
self.raster.cache.insert(&handle, Memory::error(error));
}
}
}
}
}
impl Drop for Cache {
fn drop(&mut self) {
// Stop worker gracefully
let (sender, _) = mpsc::sync_channel(1);
self.jobs = sender.clone();
self.raster.jobs = sender;
let _ = self.worker_.take().unwrap().join();
}
}
#[cfg(feature = "image")]
#[derive(Debug)]
struct Raster {
cache: crate::image::raster::Cache,
pending: BTreeSet<core::image::Id>,
jobs: mpsc::SyncSender<Job>,
}
#[cfg(feature = "image")]
fn load_image<'a>(
cache: &'a mut crate::image::raster::Cache,
pending: &mut BTreeSet<core::image::Id>,
jobs: &mut mpsc::SyncSender<Job>,
handle: &core::image::Handle,
) -> Option<&'a mut crate::image::raster::Memory> {
use crate::image::raster::Memory;
if !cache.contains(handle) {
// Load RGBA handles synchronously, since it's very cheap
if let core::image::Handle::Rgba { .. } = handle {
cache.insert(handle, Memory::load(handle));
} else {
let _ = jobs.send(Job::Load(handle.clone()));
let _ = pending.insert(handle.id());
}
}
cache.get_mut(handle)
}
#[cfg(feature = "image")]
enum Job {
Load(core::image::Handle),
Upload {
handle: core::image::Handle,
rgba: core::image::Bytes,
width: u32,
height: u32,
},
Drop(wgpu::BindGroup),
}
#[cfg(feature = "image")]
enum Work {
Upload {
handle: core::image::Handle,
entry: atlas::Entry,
bind_group: wgpu::BindGroup,
},
Error {
handle: core::image::Handle,
error: crate::graphics::image::image_rs::error::ImageError,
},
}
#[cfg(feature = "image")]
struct Worker {
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>,
}
#[cfg(feature = "image")]
impl Worker {
fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
shell: &Shell,
) -> (Self, mpsc::SyncSender<Job>, mpsc::Receiver<Work>) {
let (jobs_sender, jobs_receiver) = mpsc::sync_channel(1_000);
let (work_sender, work_receiver) = mpsc::sync_channel(1_000);
(
Self {
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,
},
jobs_sender,
work_receiver,
)
}
fn run(mut self) {
while let Ok(job) = self.jobs.recv() {
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);
}
}
}
}
fn upload(
&mut self,
handle: core::image::Handle,
width: u32,
height: u32,
rgba: core::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();
self.queue.on_submitted_work_done(move || {
let _ = output.send(Work::Upload {
handle,
entry,
bind_group: atlas.bind_group().clone(),
});
callback(&shell);
});
let _ = self
.device
.poll(wgpu::PollType::WaitForSubmissionIndex(submission));
} }
} }

View file

@ -11,11 +11,11 @@ mod vector;
use crate::Buffer; use crate::Buffer;
use crate::core::{Rectangle, Size, Transformation}; use crate::core::{Rectangle, Size, Transformation};
use crate::graphics::Shell;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use std::mem; use std::mem;
use std::sync::Arc;
pub use crate::graphics::Image; pub use crate::graphics::Image;
@ -27,7 +27,7 @@ pub struct Pipeline {
backend: wgpu::Backend, backend: wgpu::Backend,
nearest_sampler: wgpu::Sampler, nearest_sampler: wgpu::Sampler,
linear_sampler: wgpu::Sampler, linear_sampler: wgpu::Sampler,
texture_layout: Arc<wgpu::BindGroupLayout>, texture_layout: wgpu::BindGroupLayout,
constant_layout: wgpu::BindGroupLayout, constant_layout: wgpu::BindGroupLayout,
} }
@ -196,13 +196,24 @@ impl Pipeline {
backend, backend,
nearest_sampler, nearest_sampler,
linear_sampler, linear_sampler,
texture_layout: Arc::new(texture_layout), texture_layout,
constant_layout, constant_layout,
} }
} }
pub fn create_cache(&self, device: &wgpu::Device) -> Cache { pub fn create_cache(
Cache::new(device, self.backend, self.texture_layout.clone()) &self,
device: &wgpu::Device,
queue: &wgpu::Queue,
shell: &Shell,
) -> Cache {
Cache::new(
device,
queue,
self.backend,
self.texture_layout.clone(),
shell,
)
} }
} }
@ -228,16 +239,50 @@ impl State {
transformation: Transformation, transformation: Transformation,
scale: f32, scale: f32,
) { ) {
let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); if self.layers.len() <= self.prepare_layer {
let linear_instances: &mut Vec<Instance> = &mut Vec::new(); self.layers.push(Layer::new(
device,
&pipeline.constant_layout,
&pipeline.nearest_sampler,
&pipeline.linear_sampler,
));
}
let layer = &mut self.layers[self.prepare_layer];
layer.prepare(device, encoder, belt, transformation, scale);
let mut atlas = None;
let nearest_instances = &mut Vec::new();
let linear_instances = &mut Vec::new();
for image in images { for image in images {
match &image { match &image {
#[cfg(feature = "image")] #[cfg(feature = "image")]
Image::Raster(image, bounds) => { Image::Raster(image, bounds) => {
if let Some(atlas_entry) = if let Some((atlas_entry, bind_group)) = cache
cache.upload_raster(device, encoder, &image.handle) .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(
device,
encoder,
belt,
atlas,
nearest_instances,
linear_instances,
);
*atlas = bind_group.clone();
nearest_instances.clear();
linear_instances.clear();
}
_ => {}
}
add_instances( add_instances(
[bounds.x, bounds.y], [bounds.x, bounds.y],
[bounds.width, bounds.height], [bounds.width, bounds.height],
@ -257,20 +302,44 @@ impl State {
} }
} }
#[cfg(not(feature = "image"))] #[cfg(not(feature = "image"))]
Image::Raster { .. } => {} Image::Raster { .. } => continue,
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
Image::Vector(svg, bounds) => { Image::Vector(svg, bounds) => {
let size = [bounds.width, bounds.height]; let size = [bounds.width, bounds.height];
if let Some(atlas_entry) = cache.upload_vector( if let Some((atlas_entry, bind_group)) = cache
device, .upload_vector(
encoder, device,
&svg.handle, encoder,
svg.color, belt,
size, &svg.handle,
scale, svg.color,
) { size,
scale,
)
{
match atlas.as_mut() {
None => {
atlas = Some(bind_group.clone());
}
Some(atlas) if atlas != bind_group => {
layer.push(
device,
encoder,
belt,
atlas,
nearest_instances,
linear_instances,
);
*atlas = bind_group.clone();
nearest_instances.clear();
linear_instances.clear();
}
_ => {}
}
add_instances( add_instances(
[bounds.x, bounds.y], [bounds.x, bounds.y],
size, size,
@ -283,42 +352,27 @@ impl State {
} }
} }
#[cfg(not(feature = "svg"))] #[cfg(not(feature = "svg"))]
Image::Vector { .. } => {} Image::Vector { .. } => continue,
} }
} }
if nearest_instances.is_empty() && linear_instances.is_empty() { if !nearest_instances.is_empty() || !linear_instances.is_empty() {
return; layer.push(
}
if self.layers.len() <= self.prepare_layer {
self.layers.push(Layer::new(
device, device,
&pipeline.constant_layout, encoder,
&pipeline.nearest_sampler, belt,
&pipeline.linear_sampler, &atlas.expect("atlas should be defined"),
)); nearest_instances,
linear_instances,
);
} }
let layer = &mut self.layers[self.prepare_layer];
layer.prepare(
device,
encoder,
belt,
nearest_instances,
linear_instances,
transformation,
scale,
);
self.prepare_layer += 1; self.prepare_layer += 1;
} }
pub fn render<'a>( pub fn render<'a>(
&'a self, &'a self,
pipeline: &'a Pipeline, pipeline: &'a Pipeline,
cache: &'a Cache,
layer: usize, layer: usize,
bounds: Rectangle<u32>, bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>, render_pass: &mut wgpu::RenderPass<'a>,
@ -333,13 +387,15 @@ impl State {
bounds.height, bounds.height,
); );
render_pass.set_bind_group(1, cache.bind_group(), &[]);
layer.render(render_pass); layer.render(render_pass);
} }
} }
pub fn trim(&mut self) { pub fn trim(&mut self) {
for layer in &mut self.layers[..self.prepare_layer] {
layer.clear();
}
self.prepare_layer = 0; self.prepare_layer = 0;
} }
} }
@ -347,8 +403,18 @@ impl State {
#[derive(Debug)] #[derive(Debug)]
struct Layer { struct Layer {
uniforms: wgpu::Buffer, uniforms: wgpu::Buffer,
nearest: Data, instances: Buffer<Instance>,
linear: Data, total: usize,
nearest: Vec<Group>,
nearest_layout: wgpu::BindGroup,
linear: Vec<Group>,
linear_layout: wgpu::BindGroup,
}
#[derive(Debug)]
struct Group {
atlas: wgpu::BindGroup,
instance_count: usize,
} }
impl Layer { impl Layer {
@ -365,16 +431,69 @@ impl Layer {
mapped_at_creation: false, mapped_at_creation: false,
}); });
let nearest = let instances = Buffer::new(
Data::new(device, constant_layout, nearest_sampler, &uniforms); device,
"iced_wgpu::image instance buffer",
Instance::INITIAL,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
let linear = let nearest_layout =
Data::new(device, constant_layout, linear_sampler, &uniforms); 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 { Self {
uniforms, uniforms,
nearest, instances,
linear, total: 0,
nearest: Vec::new(),
nearest_layout,
linear: Vec::new(),
linear_layout,
} }
} }
@ -383,8 +502,6 @@ impl Layer {
device: &wgpu::Device, device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt, belt: &mut wgpu::util::StagingBelt,
nearest_instances: &[Instance],
linear_instances: &[Instance],
transformation: Transformation, transformation: Transformation,
scale_factor: f32, scale_factor: f32,
) { ) {
@ -404,94 +521,79 @@ impl Layer {
device, device,
) )
.copy_from_slice(bytes); .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>) { fn push(
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, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt, belt: &mut wgpu::util::StagingBelt,
instances: &[Instance], atlas: &wgpu::BindGroup,
nearest: &[Instance],
linear: &[Instance],
) { ) {
self.instance_count = instances.len(); let new = nearest.len() + linear.len();
if self.instance_count == 0 { let _ = self.instances.resize(device, self.total + new);
return;
if !nearest.is_empty() {
self.total += self
.instances
.write(device, encoder, belt, self.total, nearest);
self.nearest.push(Group {
atlas: atlas.clone(),
instance_count: nearest.len(),
});
} }
let _ = self.instances.resize(device, instances.len()); if !linear.is_empty() {
let _ = self.instances.write(device, encoder, belt, 0, instances); self.total += self
.instances
.write(device, encoder, belt, self.total, linear);
self.linear.push(Group {
atlas: atlas.clone(),
instance_count: linear.len(),
});
}
} }
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { 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.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, &[]);
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, &[]);
render_pass
.draw(0..6, offset..offset + group.instance_count as u32);
offset += group.instance_count as u32;
}
}
}
fn clear(&mut self) {
self.instances.clear();
self.nearest.clear();
self.linear.clear();
self.total = 0;
} }
} }
@ -597,6 +699,7 @@ fn add_instance(
let (x, y) = allocation.position(); let (x, y) = allocation.position();
let Size { width, height } = allocation.size(); let Size { width, height } = allocation.size();
let layer = allocation.layer(); let layer = allocation.layer();
let atlas_size = allocation.atlas_size();
let instance = Instance { let instance = Instance {
_position: position, _position: position,
@ -605,12 +708,12 @@ fn add_instance(
_rotation: rotation, _rotation: rotation,
_opacity: opacity, _opacity: opacity,
_position_in_atlas: [ _position_in_atlas: [
(x as f32 + 0.5) / atlas::SIZE as f32, (x as f32 + 0.5) / atlas_size as f32,
(y as f32 + 0.5) / atlas::SIZE as f32, (y as f32 + 0.5) / atlas_size as f32,
], ],
_size_in_atlas: [ _size_in_atlas: [
(width as f32 - 1.0) / atlas::SIZE as f32, (width as f32 - 1.0) / atlas_size as f32,
(height as f32 - 1.0) / atlas::SIZE as f32, (height as f32 - 1.0) / atlas_size as f32,
], ],
_layer: layer as u32, _layer: layer as u32,
_snap: snap as u32, _snap: snap as u32,

View file

@ -6,13 +6,18 @@ use crate::image::atlas::{self, Atlas};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
type Image = image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>;
/// Entry in cache corresponding to an image handle /// Entry in cache corresponding to an image handle
#[derive(Debug)] #[derive(Debug)]
pub enum Memory { pub enum Memory {
/// Image data on host /// Image data on host
Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, image::Bytes>), Host(Image),
/// Storage entry /// Storage entry
Device(atlas::Entry), Device {
entry: atlas::Entry,
bind_group: Option<wgpu::BindGroup>,
},
/// Image not found /// Image not found
NotFound, NotFound,
/// Invalid image data /// Invalid image data
@ -20,7 +25,20 @@ pub enum Memory {
} }
impl Memory { 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 error(error: image_rs::error::ImageError) -> Self {
match error {
image_rs::error::ImageError::IoError(_) => Self::NotFound,
_ => Self::Invalid,
}
}
pub fn dimensions(&self) -> Size<u32> { pub fn dimensions(&self) -> Size<u32> {
match self { match self {
Memory::Host(image) => { Memory::Host(image) => {
@ -28,14 +46,20 @@ impl Memory {
Size::new(width, height) Size::new(width, height)
} }
Memory::Device(entry) => entry.size(), Memory::Device { entry, .. } => entry.size(),
Memory::NotFound => Size::new(1, 1), Memory::NotFound => Size::new(1, 1),
Memory::Invalid => Size::new(1, 1), Memory::Invalid => Size::new(1, 1),
} }
} }
pub fn host(&self) -> Option<Image> {
match self {
Memory::Host(image) => Some(image.clone()),
Memory::Device { .. } | Memory::NotFound | Memory::Invalid => None,
}
}
} }
/// Caches image raster data
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Cache { pub struct Cache {
map: FxHashMap<image::Id, Memory>, map: FxHashMap<image::Id, Memory>,
@ -44,51 +68,28 @@ pub struct Cache {
} }
impl Cache { impl Cache {
/// Load image pub fn get_mut(&mut self, handle: &image::Handle) -> Option<&mut Memory> {
pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { let _ = self.hits.insert(handle.id());
if self.contains(handle) {
return self.get(handle).unwrap();
}
let memory = match graphics::image::load(handle) { self.map.get_mut(&handle.id())
Ok(image) => Memory::Host(image), }
Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound,
Err(_) => Memory::Invalid, 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.should_trim = true;
self.insert(handle, memory);
self.get(handle).unwrap()
} }
/// Load image and upload raster data pub fn contains(&self, handle: &image::Handle) -> bool {
pub fn upload( self.map.contains_key(&handle.id())
}
pub fn trim(
&mut self, &mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
handle: &image::Handle,
atlas: &mut Atlas, atlas: &mut Atlas,
) -> Option<&atlas::Entry> { on_drop: impl Fn(wgpu::BindGroup),
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) {
// Only trim if new entries have landed in the `Cache` // Only trim if new entries have landed in the `Cache`
if !self.should_trim { if !self.should_trim {
return; return;
@ -99,8 +100,12 @@ impl Cache {
self.map.retain(|k, memory| { self.map.retain(|k, memory| {
let retain = hits.contains(k); let retain = hits.contains(k);
if !retain && let Memory::Device(entry) = memory { if !retain && let Memory::Device { entry, bind_group } = memory {
atlas.remove(entry); if let Some(bind_group) = bind_group.take() {
on_drop(bind_group);
} else {
atlas.remove(entry);
}
} }
retain retain
@ -109,18 +114,4 @@ impl Cache {
self.hits.clear(); self.hits.clear();
self.should_trim = false; 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,6 +94,7 @@ impl Cache {
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &svg::Handle, handle: &svg::Handle,
color: Option<Color>, color: Option<Color>,
[width, height]: [f32; 2], [width, height]: [f32; 2],
@ -167,8 +168,8 @@ impl Cache {
}); });
} }
let allocation = let allocation = atlas
atlas.upload(device, encoder, width, height, &rgba)?; .upload(device, encoder, belt, width, height, &rgba)?;
log::debug!("allocating {id} {width}x{height}"); log::debug!("allocating {id} {width}x{height}");

View file

@ -65,8 +65,8 @@ use crate::core::renderer;
use crate::core::{ use crate::core::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
}; };
use crate::graphics::Viewport;
use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::text::{Editor, Paragraph};
use crate::graphics::{Shell, Viewport};
/// A [`wgpu`] graphics renderer for [`iced`]. /// A [`wgpu`] graphics renderer for [`iced`].
/// ///
@ -117,9 +117,7 @@ impl Renderer {
image: image::State::new(), image: image::State::new(),
#[cfg(any(feature = "svg", feature = "image"))] #[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell::new( image_cache: std::cell::RefCell::new(engine.create_image_cache()),
engine.create_image_cache(&engine.device),
),
// TODO: Resize belt smartly (?) // TODO: Resize belt smartly (?)
// It would be great if the `StagingBelt` API exposed methods // It would be great if the `StagingBelt` API exposed methods
@ -460,8 +458,6 @@ impl Renderer {
#[cfg(any(feature = "svg", feature = "image"))] #[cfg(any(feature = "svg", feature = "image"))]
let mut image_layer = 0; 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 scale_factor = viewport.scale_factor();
let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size( let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
@ -632,7 +628,6 @@ impl Renderer {
let render_span = debug::render(debug::Primitive::Image); let render_span = debug::render(debug::Primitive::Image);
self.image.render( self.image.render(
&self.engine.image_pipeline, &self.engine.image_pipeline,
&image_cache,
image_layer, image_layer,
scissor_rect, scissor_rect,
&mut render_pass, &mut render_pass,
@ -910,6 +905,7 @@ impl renderer::Headless for Renderer {
wgpu::TextureFormat::Rgba8Unorm wgpu::TextureFormat::Rgba8Unorm
}, },
Some(graphics::Antialiasing::MSAAx4), Some(graphics::Antialiasing::MSAAx4),
Shell::headless(),
); );
Some(Self::new(engine, default_font, default_text_size)) Some(Self::new(engine, default_font, default_text_size))

View file

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

View file

@ -49,7 +49,7 @@ use crate::futures::futures::task;
use crate::futures::futures::{Future, StreamExt}; use crate::futures::futures::{Future, StreamExt};
use crate::futures::subscription; use crate::futures::subscription;
use crate::futures::{Executor, Runtime}; use crate::futures::{Executor, Runtime};
use crate::graphics::{Compositor, compositor}; use crate::graphics::{Compositor, Shell, compositor};
use crate::runtime::system; use crate::runtime::system;
use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::{Action, Task}; use crate::runtime::{Action, Task};
@ -587,8 +587,10 @@ async fn run_instance<P>(
let default_fonts = default_fonts.clone(); let default_fonts = default_fonts.clone();
async move { async move {
let shell = Shell::new(proxy.clone());
let mut compositor = let mut compositor =
<P::Renderer as compositor::Default>::Compositor::new(graphics_settings, window).await; <P::Renderer as compositor::Default>::Compositor::new(graphics_settings, window, shell).await;
if let Ok(compositor) = &mut compositor { if let Ok(compositor) = &mut compositor {
for font in default_fonts { for font in default_fonts {
@ -824,7 +826,7 @@ async fn run_instance<P>(
.get_mut(&id) .get_mut(&id)
.expect("Get user interface"); .expect("Get user interface");
let draw_span = debug::draw(id); let interact_span = debug::interact(id);
let mut change_count = 0; let mut change_count = 0;
let state = loop { let state = loop {
@ -947,7 +949,9 @@ async fn run_instance<P>(
user_interfaces.get_mut(&id).unwrap(); user_interfaces.get_mut(&id).unwrap();
} }
}; };
interact_span.finish();
let draw_span = debug::draw(id);
interface.draw( interface.draw(
&mut window.renderer, &mut window.renderer,
window.state.theme(), window.state.theme(),
@ -1646,6 +1650,26 @@ fn run_action<'a, P, C>(
let _ = window.raw.set_cursor_hittest(true); let _ = window.raw.set_cursor_hittest(true);
} }
} }
window::Action::RedrawAll => {
for (_id, window) in window_manager.iter_mut() {
window.raw.request_redraw();
}
}
window::Action::RelayoutAll => {
for (id, window) in window_manager.iter_mut() {
if let Some(ui) = interfaces.remove(&id) {
let _ = interfaces.insert(
id,
ui.relayout(
window.state.logical_size(),
&mut window.renderer,
),
);
}
window.raw.request_redraw();
}
}
}, },
Action::System(action) => match action { Action::System(action) => match action {
system::Action::GetInformation(_channel) => { system::Action::GetInformation(_channel) => {

View file

@ -4,7 +4,9 @@ use crate::futures::futures::{
select, select,
task::{Context, Poll}, task::{Context, Poll},
}; };
use crate::graphics::shell;
use crate::runtime::Action; use crate::runtime::Action;
use crate::runtime::window;
use std::pin::Pin; use std::pin::Pin;
/// An event loop proxy with backpressure that implements `Sink`. /// An event loop proxy with backpressure that implements `Sink`.
@ -134,3 +136,16 @@ impl<T: 'static> Sink<Action<T>> for Proxy<T> {
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} }
} }
impl<T> shell::Notifier for Proxy<T>
where
T: Send,
{
fn request_redraw(&self) {
self.send_action(Action::Window(window::Action::RedrawAll));
}
fn invalidate_layout(&self) {
self.send_action(Action::Window(window::Action::RelayoutAll));
}
}