diff options
Diffstat (limited to 'wgpu/src/image')
-rw-r--r-- | wgpu/src/image/atlas.rs | 361 | ||||
-rw-r--r-- | wgpu/src/image/atlas/allocation.rs | 35 | ||||
-rw-r--r-- | wgpu/src/image/atlas/allocator.rs | 69 | ||||
-rw-r--r-- | wgpu/src/image/atlas/entry.rs | 26 | ||||
-rw-r--r-- | wgpu/src/image/atlas/layer.rs | 17 | ||||
-rw-r--r-- | wgpu/src/image/raster.rs | 134 | ||||
-rw-r--r-- | wgpu/src/image/vector.rs | 151 |
7 files changed, 793 insertions, 0 deletions
diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs new file mode 100644 index 00000000..86a5ff49 --- /dev/null +++ b/wgpu/src/image/atlas.rs @@ -0,0 +1,361 @@ +pub mod entry; + +mod allocation; +mod allocator; +mod layer; + +pub use allocation::Allocation; +pub use entry::Entry; +pub use layer::Layer; + +use allocator::Allocator; + +pub const SIZE: u32 = 2048; + +#[derive(Debug)] +pub struct Atlas { + texture: wgpu::Texture, + texture_view: wgpu::TextureView, + layers: Vec<Layer>, +} + +impl Atlas { + pub fn new(device: &wgpu::Device) -> Self { + let extent = wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + size: extent, + array_layer_count: 2, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::SAMPLED, + }); + + let texture_view = texture.create_default_view(); + + Atlas { + texture, + texture_view, + layers: vec![Layer::Empty, Layer::Empty], + } + } + + pub fn view(&self) -> &wgpu::TextureView { + &self.texture_view + } + + pub fn layer_count(&self) -> usize { + self.layers.len() + } + + pub fn upload<C>( + &mut self, + width: u32, + height: u32, + data: &[C], + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> Option<Entry> + where + C: Copy + 'static, + { + let entry = { + let current_size = self.layers.len(); + let entry = self.allocate(width, height)?; + + // We grow the internal texture after allocating if necessary + let new_layers = self.layers.len() - current_size; + self.grow(new_layers, device, encoder); + + entry + }; + + log::info!("Allocated atlas entry: {:?}", entry); + + let buffer = device + .create_buffer_mapped(data.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(data); + + match &entry { + Entry::Contiguous(allocation) => { + self.upload_allocation( + &buffer, + width, + height, + 0, + &allocation, + encoder, + ); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + let (x, y) = fragment.position; + let offset = (y * width + x) as usize * 4; + + self.upload_allocation( + &buffer, + width, + height, + offset, + &fragment.allocation, + encoder, + ); + } + } + } + + log::info!("Current atlas: {:?}", self); + + Some(entry) + } + + pub fn remove(&mut self, entry: &Entry) { + log::info!("Removing atlas entry: {:?}", entry); + + match entry { + Entry::Contiguous(allocation) => { + self.deallocate(allocation); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + self.deallocate(&fragment.allocation); + } + } + } + } + + fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> { + // Allocate one layer if texture fits perfectly + if width == SIZE && height == SIZE { + let mut empty_layers = self + .layers + .iter_mut() + .enumerate() + .filter(|(_, layer)| layer.is_empty()); + + if let Some((i, layer)) = empty_layers.next() { + *layer = Layer::Full; + + return Some(Entry::Contiguous(Allocation::Full { layer: i })); + } + + self.layers.push(Layer::Full); + + return Some(Entry::Contiguous(Allocation::Full { + layer: self.layers.len() - 1, + })); + } + + // Split big textures across multiple layers + if width > SIZE || height > SIZE { + let mut fragments = Vec::new(); + let mut y = 0; + + while y < height { + let height = std::cmp::min(height - y, SIZE); + let mut x = 0; + + while x < width { + let width = std::cmp::min(width - x, SIZE); + + let allocation = self.allocate(width, height)?; + + if let Entry::Contiguous(allocation) = allocation { + fragments.push(entry::Fragment { + position: (x, y), + allocation, + }); + } + + x += width; + } + + y += height; + } + + return Some(Entry::Fragmented { + size: (width, height), + fragments, + }); + } + + // Try allocating on an existing layer + for (i, layer) in self.layers.iter_mut().enumerate() { + match layer { + Layer::Empty => { + let mut allocator = Allocator::new(SIZE); + + if let Some(region) = allocator.allocate(width, height) { + *layer = Layer::Busy(allocator); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + })); + } + } + Layer::Busy(allocator) => { + if let Some(region) = allocator.allocate(width, height) { + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + })); + } + } + _ => {} + } + } + + // Create new layer with atlas allocator + let mut allocator = Allocator::new(SIZE); + + if let Some(region) = allocator.allocate(width, height) { + self.layers.push(Layer::Busy(allocator)); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: self.layers.len() - 1, + })); + } + + // We ran out of memory (?) + None + } + + fn deallocate(&mut self, allocation: &Allocation) { + log::info!("Deallocating atlas: {:?}", allocation); + + match allocation { + Allocation::Full { layer } => { + self.layers[*layer] = Layer::Empty; + } + Allocation::Partial { layer, region } => { + let layer = &mut self.layers[*layer]; + + if let Layer::Busy(allocator) = layer { + allocator.deallocate(region); + + if allocator.is_empty() { + *layer = Layer::Empty; + } + } + } + } + } + + fn upload_allocation( + &mut self, + buffer: &wgpu::Buffer, + image_width: u32, + image_height: u32, + offset: usize, + allocation: &Allocation, + encoder: &mut wgpu::CommandEncoder, + ) { + let (x, y) = allocation.position(); + let (width, height) = allocation.size(); + let layer = allocation.layer(); + + let extent = wgpu::Extent3d { + width, + height, + depth: 1, + }; + + encoder.copy_buffer_to_texture( + wgpu::BufferCopyView { + buffer, + offset: offset as u64, + row_pitch: 4 * image_width, + image_height, + }, + wgpu::TextureCopyView { + texture: &self.texture, + array_layer: layer as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: x as f32, + y: y as f32, + z: 0.0, + }, + }, + extent, + ); + } + + fn grow( + &mut self, + amount: usize, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) { + if amount == 0 { + return; + } + + let new_texture = device.create_texture(&wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }, + array_layer_count: self.layers.len() as u32, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::SAMPLED, + }); + + let amount_to_copy = self.layers.len() - amount; + + for (i, layer) in + self.layers.iter_mut().take(amount_to_copy).enumerate() + { + if layer.is_empty() { + continue; + } + + encoder.copy_texture_to_texture( + wgpu::TextureCopyView { + texture: &self.texture, + array_layer: i as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + wgpu::TextureCopyView { + texture: &new_texture, + array_layer: i as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }, + ); + } + + self.texture = new_texture; + self.texture_view = self.texture.create_default_view(); + } +} diff --git a/wgpu/src/image/atlas/allocation.rs b/wgpu/src/image/atlas/allocation.rs new file mode 100644 index 00000000..59b7239f --- /dev/null +++ b/wgpu/src/image/atlas/allocation.rs @@ -0,0 +1,35 @@ +use crate::image::atlas::{self, allocator}; + +#[derive(Debug)] +pub enum Allocation { + Partial { + layer: usize, + region: allocator::Region, + }, + Full { + layer: usize, + }, +} + +impl Allocation { + pub fn position(&self) -> (u32, u32) { + match self { + Allocation::Partial { region, .. } => region.position(), + Allocation::Full { .. } => (0, 0), + } + } + + pub fn size(&self) -> (u32, u32) { + match self { + Allocation::Partial { region, .. } => region.size(), + Allocation::Full { .. } => (atlas::SIZE, atlas::SIZE), + } + } + + pub fn layer(&self) -> usize { + match self { + Allocation::Partial { layer, .. } => *layer, + Allocation::Full { layer } => *layer, + } + } +} diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs new file mode 100644 index 00000000..7a4ff5b1 --- /dev/null +++ b/wgpu/src/image/atlas/allocator.rs @@ -0,0 +1,69 @@ +use guillotiere::{AtlasAllocator, Size}; + +pub struct Allocator { + raw: AtlasAllocator, + allocations: usize, +} + +impl Allocator { + pub fn new(size: u32) -> Allocator { + let raw = AtlasAllocator::new(Size::new(size as i32, size as i32)); + + Allocator { + raw, + allocations: 0, + } + } + + pub fn allocate(&mut self, width: u32, height: u32) -> Option<Region> { + let allocation = + self.raw.allocate(Size::new(width as i32, height as i32))?; + + self.allocations += 1; + + Some(Region { allocation }) + } + + pub fn deallocate(&mut self, region: &Region) { + self.raw.deallocate(region.allocation.id); + + self.allocations = self.allocations.saturating_sub(1); + } + + pub fn is_empty(&self) -> bool { + self.allocations == 0 + } +} + +pub struct Region { + allocation: guillotiere::Allocation, +} + +impl Region { + pub fn position(&self) -> (u32, u32) { + let rectangle = &self.allocation.rectangle; + + (rectangle.min.x as u32, rectangle.min.y as u32) + } + + pub fn size(&self) -> (u32, u32) { + let size = self.allocation.rectangle.size(); + + (size.width as u32, size.height as u32) + } +} + +impl std::fmt::Debug for Allocator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Allocator") + } +} + +impl std::fmt::Debug for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Region") + .field("id", &self.allocation.id) + .field("rectangle", &self.allocation.rectangle) + .finish() + } +} diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs new file mode 100644 index 00000000..0310fc54 --- /dev/null +++ b/wgpu/src/image/atlas/entry.rs @@ -0,0 +1,26 @@ +use crate::image::atlas; + +#[derive(Debug)] +pub enum Entry { + Contiguous(atlas::Allocation), + Fragmented { + size: (u32, u32), + fragments: Vec<Fragment>, + }, +} + +impl Entry { + #[cfg(feature = "image")] + pub fn size(&self) -> (u32, u32) { + match self { + Entry::Contiguous(allocation) => allocation.size(), + Entry::Fragmented { size, .. } => *size, + } + } +} + +#[derive(Debug)] +pub struct Fragment { + pub position: (u32, u32), + pub allocation: atlas::Allocation, +} diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs new file mode 100644 index 00000000..b1084ed9 --- /dev/null +++ b/wgpu/src/image/atlas/layer.rs @@ -0,0 +1,17 @@ +use crate::image::atlas::Allocator; + +#[derive(Debug)] +pub enum Layer { + Empty, + Busy(Allocator), + Full, +} + +impl Layer { + pub fn is_empty(&self) -> bool { + match self { + Layer::Empty => true, + _ => false, + } + } +} diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs new file mode 100644 index 00000000..4f69df8c --- /dev/null +++ b/wgpu/src/image/raster.rs @@ -0,0 +1,134 @@ +use crate::image::atlas::{self, Atlas}; +use iced_native::image; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug)] +pub enum Memory { + Host(::image::ImageBuffer<::image::Bgra<u8>, Vec<u8>>), + Device(atlas::Entry), + NotFound, + Invalid, +} + +impl Memory { + pub fn dimensions(&self) -> (u32, u32) { + match self { + Memory::Host(image) => image.dimensions(), + Memory::Device(entry) => entry.size(), + Memory::NotFound => (1, 1), + Memory::Invalid => (1, 1), + } + } +} + +#[derive(Debug)] +pub struct Cache { + map: HashMap<u64, Memory>, + hits: HashSet<u64>, +} + +impl Cache { + pub fn new() -> Self { + Self { + map: HashMap::new(), + hits: HashSet::new(), + } + } + + pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { + if self.contains(handle) { + return self.get(handle).unwrap(); + } + + let memory = match handle.data() { + image::Data::Path(path) => { + if let Ok(image) = ::image::open(path) { + Memory::Host(image.to_bgra()) + } else { + Memory::NotFound + } + } + image::Data::Bytes(bytes) => { + if let Ok(image) = ::image::load_from_memory(&bytes) { + Memory::Host(image.to_bgra()) + } else { + Memory::Invalid + } + } + image::Data::Pixels { + width, + height, + pixels, + } => { + if let Some(image) = ::image::ImageBuffer::from_vec( + *width, + *height, + pixels.to_vec(), + ) { + Memory::Host(image) + } else { + Memory::Invalid + } + } + }; + + self.insert(handle, memory); + self.get(handle).unwrap() + } + + pub fn upload( + &mut self, + handle: &image::Handle, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + 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(width, height, &image, device, encoder)?; + + *memory = Memory::Device(entry); + } + + if let Memory::Device(allocation) = memory { + Some(allocation) + } else { + None + } + } + + pub fn trim(&mut self, atlas: &mut Atlas) { + let hits = &self.hits; + + self.map.retain(|k, memory| { + let retain = hits.contains(k); + + if !retain { + if let Memory::Device(entry) = memory { + atlas.remove(entry); + } + } + + retain + }); + + self.hits.clear(); + } + + 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()) + } +} diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs new file mode 100644 index 00000000..bae0f82f --- /dev/null +++ b/wgpu/src/image/vector.rs @@ -0,0 +1,151 @@ +use crate::image::atlas::{self, Atlas}; +use iced_native::svg; +use std::collections::{HashMap, HashSet}; + +pub enum Svg { + Loaded(resvg::usvg::Tree), + NotFound, +} + +impl Svg { + pub fn viewport_dimensions(&self) -> (u32, u32) { + match self { + Svg::Loaded(tree) => { + let size = tree.svg_node().size; + + (size.width() as u32, size.height() as u32) + } + Svg::NotFound => (1, 1), + } + } +} + +#[derive(Debug)] +pub struct Cache { + svgs: HashMap<u64, Svg>, + rasterized: HashMap<(u64, u32, u32), atlas::Entry>, + svg_hits: HashSet<u64>, + rasterized_hits: HashSet<(u64, u32, u32)>, +} + +impl Cache { + pub fn new() -> Self { + Self { + svgs: HashMap::new(), + rasterized: HashMap::new(), + svg_hits: HashSet::new(), + rasterized_hits: HashSet::new(), + } + } + + pub fn load(&mut self, handle: &svg::Handle) -> &Svg { + if self.svgs.contains_key(&handle.id()) { + return self.svgs.get(&handle.id()).unwrap(); + } + + let opt = resvg::Options::default(); + + let svg = match resvg::usvg::Tree::from_file(handle.path(), &opt.usvg) { + Ok(tree) => Svg::Loaded(tree), + Err(_) => Svg::NotFound, + }; + + let _ = self.svgs.insert(handle.id(), svg); + self.svgs.get(&handle.id()).unwrap() + } + + pub fn upload( + &mut self, + handle: &svg::Handle, + [width, height]: [f32; 2], + scale: f32, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture_atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let id = handle.id(); + + let (width, height) = ( + (scale * width).round() as u32, + (scale * height).round() as u32, + ); + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + if self.rasterized.contains_key(&(id, width, height)) { + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + + return self.rasterized.get(&(id, width, height)); + } + + match self.load(handle) { + Svg::Loaded(tree) => { + if width == 0 || height == 0 { + return None; + } + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + let screen_size = + resvg::ScreenSize::new(width, height).unwrap(); + + let mut canvas = + resvg::raqote::DrawTarget::new(width as i32, height as i32); + + resvg::backend_raqote::render_to_canvas( + tree, + &resvg::Options::default(), + screen_size, + &mut canvas, + ); + + let allocation = texture_atlas.upload( + width, + height, + canvas.get_data(), + device, + encoder, + )?; + + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + let _ = self.rasterized.insert((id, width, height), allocation); + + self.rasterized.get(&(id, width, height)) + } + Svg::NotFound => None, + } + } + + pub fn trim(&mut self, atlas: &mut Atlas) { + let svg_hits = &self.svg_hits; + let rasterized_hits = &self.rasterized_hits; + + self.svgs.retain(|k, _| svg_hits.contains(k)); + self.rasterized.retain(|k, entry| { + let retain = rasterized_hits.contains(k); + + if !retain { + atlas.remove(entry); + } + + retain + }); + self.svg_hits.clear(); + self.rasterized_hits.clear(); + } +} + +impl std::fmt::Debug for Svg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Svg::Loaded(_) => write!(f, "Svg::Loaded"), + Svg::NotFound => write!(f, "Svg::NotFound"), + } + } +} |