[`vello_hybrid`]: Multiple image atlases support
diff --git a/sparse_strips/vello_example_scenes/src/svg.rs b/sparse_strips/vello_example_scenes/src/svg.rs index 8c6e377..9165ebb 100644 --- a/sparse_strips/vello_example_scenes/src/svg.rs +++ b/sparse_strips/vello_example_scenes/src/svg.rs
@@ -173,10 +173,8 @@ let cargo_dir = Path::new(env!("CARGO_MANIFEST_DIR")) .canonicalize() .unwrap(); - &std::fs::read_to_string( - cargo_dir.join("../../../../examples/assets/Ghostscript_Tiger.svg"), - ) - .unwrap() + &std::fs::read_to_string(cargo_dir.join("../../examples/assets/Ghostscript_Tiger.svg")) + .unwrap() }; let svg = PicoSvg::load(svg_content, 1.0).expect("Failed to parse Ghost Tiger SVG");
diff --git a/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs b/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs index 4052aae..b5dbef0 100644 --- a/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs +++ b/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
@@ -12,11 +12,14 @@ use std::cell::RefCell; use std::rc::Rc; use vello_common::{ + fearless_simd::Level, kurbo::{Affine, Point}, paint::{ImageId, ImageSource}, }; use vello_example_scenes::{AnyScene, image::ImageScene}; -use vello_hybrid::{Pixmap, Scene}; +use vello_hybrid::{ + AllocationStrategy, AtlasConfig, Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene, +}; use wasm_bindgen::prelude::*; use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent}; @@ -75,13 +78,30 @@ }; surface.configure(&device, &surface_config); - let renderer = vello_hybrid::Renderer::new( + let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; + let renderer = Renderer::new_with( &device, - &vello_hybrid::RenderTargetConfig { + &RenderTargetConfig { format: surface_format, width, height, }, + RenderSettings { + level: Level::try_detect().unwrap_or(Level::fallback()), + atlas_config: AtlasConfig { + atlas_size: (max_texture_dimension_2d, max_texture_dimension_2d), + // In WGPU’s GLES backend, heuristics are used to decide whether a texture + // should be treated as D2 or D2Array. However, this can cause a mismatch: + // - when depth_or_array_layers == 1, the backend assumes the texture is D2, + // even if it was actually created as a D2Array. This issue only occurs with the GLES backend. + // + // @see https://github.com/gfx-rs/wgpu/blob/61e5124eb9530d3b3865556a7da4fd320d03ddc5/wgpu-hal/src/gles/mod.rs#L470-L517 + initial_atlas_count: 2, + max_atlases: 10, + auto_grow: true, + allocation_strategy: AllocationStrategy::FirstFit, + }, + }, ); Self {
diff --git a/sparse_strips/vello_hybrid/src/image_cache.rs b/sparse_strips/vello_hybrid/src/image_cache.rs index 15b6d64..b877a7b 100644 --- a/sparse_strips/vello_hybrid/src/image_cache.rs +++ b/sparse_strips/vello_hybrid/src/image_cache.rs
@@ -3,93 +3,82 @@ #![allow(dead_code, reason = "Clippy fails when --no-default-features")] +use crate::multi_atlas::{AtlasConfig, AtlasError, AtlasId, MultiAtlasManager}; use alloc::vec::Vec; -use guillotiere::{AllocId, AtlasAllocator, size2}; +use guillotiere::AllocId; use vello_common::paint::ImageId; -const DEFAULT_ATLAS_SIZE: u32 = 1024; - -/// Represents an image resource for rendering +/// Represents an image resource for rendering. #[derive(Debug)] pub(crate) struct ImageResource { - /// The ID of the image + /// The ID of the image. pub(crate) id: ImageId, - /// The width of the image + /// The width of the image. pub(crate) width: u16, - /// The height of the image + /// The height of the image. pub(crate) height: u16, - /// The offset of the image in the atlas + /// The Id of the atlas containing this image. + pub(crate) atlas_id: AtlasId, + /// The offset of the image within its atlas. pub(crate) offset: [u16; 2], - /// The atlas allocation ID for deallocation + /// The atlas allocation ID for deallocation. atlas_alloc_id: AllocId, } -/// Manages image resources for the renderer +/// Manages image resources for the renderer. pub(crate) struct ImageCache { - /// Atlas allocator for the images - atlas: AtlasAllocator, - /// Vector of optional image resources (None = free slot) + /// Multi-atlas manager for handling multiple texture atlases. + atlas_manager: MultiAtlasManager, + /// Vector of optional image resources (None = free slot). slots: Vec<Option<ImageResource>>, - /// Stack of free indices for O(1) allocation/deallocation + /// Stack of free indices. free_idxs: Vec<usize>, } impl core::fmt::Debug for ImageCache { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - // Count allocated and free rectangles in the atlas - let mut allocated_count = 0; - let mut free_count = 0; - - self.atlas.for_each_allocated_rectangle(|_id, _rect| { - allocated_count += 1; - }); - - self.atlas.for_each_free_rectangle(|_rect| { - free_count += 1; - }); + let atlas_stats = self.atlas_manager.atlas_stats(); f.debug_struct("ImageCache") .field("slots", &self.slots) .field("free_idxs", &self.free_idxs) - .field("atlas_size", &self.atlas.size()) - .field("atlas_is_empty", &self.atlas.is_empty()) - .field("atlas_allocated_count", &allocated_count) - .field("atlas_free_count", &free_count) + .field("atlas_count", &self.atlas_manager.atlas_count()) + .field("atlas_stats", &atlas_stats) .finish() } } -impl Default for ImageCache { - fn default() -> Self { - Self::new(DEFAULT_ATLAS_SIZE, DEFAULT_ATLAS_SIZE) - } -} - impl ImageCache { - /// Create a new image cache + /// Create a new image cache with default atlas configuration. pub(crate) fn new(width: u32, height: u32) -> Self { Self { - atlas: AtlasAllocator::new(size2(width as i32, height as i32)), + atlas_manager: MultiAtlasManager::new_with_initial_atlas(width, height), slots: Vec::new(), free_idxs: Vec::new(), } } - /// Get an image resource by its Id + /// Create a new image cache with custom atlas configuration. + pub(crate) fn new_with_config(config: AtlasConfig) -> Self { + Self { + atlas_manager: MultiAtlasManager::new(config), + slots: Vec::new(), + free_idxs: Vec::new(), + } + } + + /// Get an image resource by its Id. pub(crate) fn get(&self, id: ImageId) -> Option<&ImageResource> { self.slots.get(id.as_u32() as usize)?.as_ref() } - /// Allocate an image in the cache + /// Allocate an image in the cache. #[expect( clippy::cast_possible_truncation, reason = "u16 is enough for the offset and width/height" )] - pub(crate) fn allocate(&mut self, width: u32, height: u32) -> ImageId { - let alloc = self - .atlas - .allocate(size2(width as i32, height as i32)) - .expect("Failed to allocate texture"); + pub(crate) fn allocate(&mut self, width: u32, height: u32) -> Result<ImageId, AtlasError> { + let atlas_alloc = self.atlas_manager.try_allocate(width, height)?; let slot_idx = self.free_idxs.pop().unwrap_or_else(|| { // No free slots, append to vector @@ -104,18 +93,27 @@ id: image_id, width: width as u16, height: height as u16, - offset: [alloc.rectangle.min.x as u16, alloc.rectangle.min.y as u16], - atlas_alloc_id: alloc.id, + atlas_id: atlas_alloc.atlas_id, + offset: [ + atlas_alloc.allocation.rectangle.min.x as u16, + atlas_alloc.allocation.rectangle.min.y as u16, + ], + atlas_alloc_id: atlas_alloc.allocation.id, }); - image_id + Ok(image_id) } - /// Deallocate an image from the cache, returning true if it existed + /// Deallocate an image from the cache, returning the image resource if it existed. pub(crate) fn deallocate(&mut self, id: ImageId) -> Option<ImageResource> { let index = id.as_u32() as usize; if let Some(image_resource) = self.slots.get_mut(index).and_then(Option::take) { - // Deallocate from the atlas using the stored allocation ID - self.atlas.deallocate(image_resource.atlas_alloc_id); + // Deallocate from the appropriate atlas + let _ = self.atlas_manager.deallocate( + image_resource.atlas_id, + image_resource.atlas_alloc_id, + image_resource.width as u32, + image_resource.height as u32, + ); self.free_idxs.push(index); Some(image_resource) } else { @@ -123,11 +121,26 @@ } } - /// Clear all images from the cache + /// Get access to the atlas manager. + pub(crate) fn atlas_manager(&self) -> &MultiAtlasManager { + &self.atlas_manager + } + + /// Get atlas stats. + pub(crate) fn atlas_stats(&self) -> Vec<(AtlasId, &crate::multi_atlas::AtlasUsageStats)> { + self.atlas_manager.atlas_stats() + } + + /// Get the number of atlases. + pub(crate) fn atlas_count(&self) -> usize { + self.atlas_manager.atlas_count() + } + + /// Clear all images from the cache. pub(crate) fn clear(&mut self) { self.slots.clear(); self.free_idxs.clear(); - self.atlas.clear(); + self.atlas_manager.clear(); } } @@ -135,11 +148,13 @@ mod tests { use super::*; + const ATLAS_SIZE: u32 = 1024; + #[test] fn test_insert_single_image() { - let mut cache = ImageCache::default(); + let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE); - let id = cache.allocate(100, 100); + let id = cache.allocate(100, 100).unwrap(); assert_eq!(id.as_u32(), 0); let resource = cache.get(id).unwrap(); @@ -151,10 +166,10 @@ #[test] fn test_insert_multiple_images() { - let mut cache = ImageCache::default(); + let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE); - let id1 = cache.allocate(50, 50); - let id2 = cache.allocate(75, 75); + let id1 = cache.allocate(50, 50).unwrap(); + let id2 = cache.allocate(75, 75).unwrap(); assert_eq!(id1.as_u32(), 0); assert_eq!(id2.as_u32(), 1); @@ -171,7 +186,7 @@ #[test] fn test_get_nonexistent_image() { - let cache: ImageCache = ImageCache::default(); + let cache: ImageCache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE); assert!(cache.get(ImageId::new(0)).is_none()); assert!(cache.get(ImageId::new(999)).is_none()); @@ -179,9 +194,9 @@ #[test] fn test_remove_image() { - let mut cache = ImageCache::default(); + let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE); - let id = cache.allocate(100, 100); + let id = cache.allocate(100, 100).unwrap(); assert!(cache.get(id).is_some()); cache.deallocate(id); @@ -190,7 +205,7 @@ #[test] fn test_remove_nonexistent_image() { - let mut cache: ImageCache = ImageCache::default(); + let mut cache: ImageCache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE); // Should not panic when unregistering non-existent image cache.deallocate(ImageId::new(0)); @@ -199,12 +214,12 @@ #[test] fn test_slot_reuse_after_remove() { - let mut cache = ImageCache::default(); + let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE); // Register three images - let id1 = cache.allocate(50, 50); - let id2 = cache.allocate(60, 60); - let id3 = cache.allocate(70, 70); + let id1 = cache.allocate(50, 50).unwrap(); + let id2 = cache.allocate(60, 60).unwrap(); + let id3 = cache.allocate(70, 70).unwrap(); assert_eq!(id1.as_u32(), 0); assert_eq!(id2.as_u32(), 1); @@ -215,7 +230,7 @@ assert!(cache.get(id2).is_none()); // Register a new image - should reuse slot 1 - let id4 = cache.allocate(80, 80); + let id4 = cache.allocate(80, 80).unwrap(); // Reused slot 1 assert_eq!(id4.as_u32(), 1); @@ -228,11 +243,11 @@ #[test] fn test_multiple_remove_and_reuse() { - let mut cache = ImageCache::default(); + let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE); // Register several images let ids: Vec<_> = (0..5) - .map(|i| cache.allocate(100 + i * 10, 100 + i * 10)) + .map(|i| cache.allocate(100 + i * 10, 100 + i * 10).unwrap()) .collect(); // Unregister some in the middle @@ -240,12 +255,12 @@ cache.deallocate(ids[3]); // Register new images - should reuse the freed slots - let new_id1 = cache.allocate(200, 200); - let new_id2 = cache.allocate(300, 300); + let new_id1 = cache.allocate(200, 200).unwrap(); + let new_id2 = cache.allocate(300, 300).unwrap(); // Should have reused slots 3 and 1 (in reverse order due to stack behavior) - assert!(new_id1.as_u32() == 3); - assert!(new_id2.as_u32() == 1); + assert_eq!(new_id1.as_u32(), 3); + assert_eq!(new_id2.as_u32(), 1); assert_ne!(new_id1.as_u32(), new_id2.as_u32()); } }
diff --git a/sparse_strips/vello_hybrid/src/lib.rs b/sparse_strips/vello_hybrid/src/lib.rs index 683a7b6..a236072 100644 --- a/sparse_strips/vello_hybrid/src/lib.rs +++ b/sparse_strips/vello_hybrid/src/lib.rs
@@ -35,11 +35,13 @@ mod gradient_cache; mod image_cache; +mod multi_atlas; mod render; mod scene; #[cfg(any(all(target_arch = "wasm32", feature = "webgl"), feature = "wgpu"))] mod schedule; pub mod util; +pub use multi_atlas::{AllocationStrategy, AtlasConfig, AtlasError, AtlasId}; #[cfg(feature = "wgpu")] pub use render::{AtlasWriter, RenderTargetConfig, Renderer}; pub use render::{Config, GpuStrip, RenderSize};
diff --git a/sparse_strips/vello_hybrid/src/multi_atlas.rs b/sparse_strips/vello_hybrid/src/multi_atlas.rs new file mode 100644 index 0000000..363030f --- /dev/null +++ b/sparse_strips/vello_hybrid/src/multi_atlas.rs
@@ -0,0 +1,539 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Multi-atlas management for `vello_hybrid`. +//! +//! This module provides support for managing multiple texture atlases, allowing for handling of +//! large numbers of images. + +use alloc::vec::Vec; +use guillotiere::{AllocId, Allocation, AtlasAllocator, size2}; + +/// Manages multiple texture atlases. +pub(crate) struct MultiAtlasManager { + /// All atlases managed by this instance. + atlases: Vec<Atlas>, + /// Configuration for atlas management. + config: AtlasConfig, + /// Next atlas Id to assign. + next_atlas_id: u32, + /// Round-robin counter for allocation strategy. + round_robin_counter: usize, +} + +impl MultiAtlasManager { + /// Create a new multi-atlas manager with the given configuration. + pub(crate) fn new(config: AtlasConfig) -> Self { + let mut manager = Self { + atlases: Vec::new(), + config, + next_atlas_id: 0, + round_robin_counter: 0, + }; + + for _ in 0..config.initial_atlas_count { + manager + .create_atlas() + .expect("Failed to create initial atlas"); + } + + manager + } + + /// Create a new multi-atlas manager with default configuration. + pub(crate) fn new_with_initial_atlas(width: u32, height: u32) -> Self { + let config = AtlasConfig { + atlas_size: (width, height), + ..Default::default() + }; + + Self::new(config) + } + + /// Get the current configuration. + pub(crate) fn config(&self) -> &AtlasConfig { + &self.config + } + + /// Create a new atlas and return its ID. + pub(crate) fn create_atlas(&mut self) -> Result<AtlasId, AtlasError> { + if self.atlases.len() >= self.config.max_atlases { + return Err(AtlasError::AtlasLimitReached); + } + + let atlas_id = AtlasId::new(self.next_atlas_id); + self.next_atlas_id += 1; + + let atlas = Atlas::new(atlas_id, self.config.atlas_size.0, self.config.atlas_size.1); + self.atlases.push(atlas); + + Ok(atlas_id) + } + + /// Check if a new atlas can be created. + pub(crate) fn can_create_new_atlas(&self) -> bool { + self.atlases.len() < self.config.max_atlases + } + + /// Try to allocate space for an image with the given dimensions. + pub(crate) fn try_allocate( + &mut self, + width: u32, + height: u32, + ) -> Result<AtlasAllocation, AtlasError> { + // Check if the image is too large for any atlas + if width > self.config.atlas_size.0 || height > self.config.atlas_size.1 { + return Err(AtlasError::TextureTooLarge { width, height }); + } + + // If no atlases exist and auto-grow is enabled, create one + if self.atlases.is_empty() && self.config.auto_grow { + self.create_atlas()?; + } + + // Try allocation based on strategy + match self.config.allocation_strategy { + AllocationStrategy::FirstFit => self.allocate_first_fit(width, height), + AllocationStrategy::BestFit => self.allocate_best_fit(width, height), + AllocationStrategy::LeastUsed => self.allocate_least_used(width, height), + AllocationStrategy::RoundRobin => self.allocate_round_robin(width, height), + } + } + + /// Allocate using first-fit strategy: try atlases in order until one has space. + fn allocate_first_fit( + &mut self, + width: u32, + height: u32, + ) -> Result<AtlasAllocation, AtlasError> { + for atlas in &mut self.atlases { + if let Some(allocation) = atlas.allocate(width, height) { + return Ok(AtlasAllocation { + atlas_id: atlas.id, + allocation, + }); + } + } + + // Try creating a new atlas if auto-grow is enabled + if self.config.auto_grow && self.can_create_new_atlas() { + let atlas_id = self.create_atlas()?; + let atlas = self.atlases.last_mut().unwrap(); + if let Some(allocation) = atlas.allocate(width, height) { + return Ok(AtlasAllocation { + atlas_id, + allocation, + }); + } + } + + Err(AtlasError::NoSpaceAvailable) + } + + /// Allocate using best-fit strategy: choose the atlas with the smallest remaining space that + /// can fit the image. + fn allocate_best_fit( + &mut self, + width: u32, + height: u32, + ) -> Result<AtlasAllocation, AtlasError> { + let mut best_atlas_idx = None; + let mut best_remaining_space = u32::MAX; + + // Find the atlas with the least remaining space that can fit the image + for (idx, atlas) in self.atlases.iter().enumerate() { + let stats = atlas.stats(); + let remaining_space = stats.total_area - stats.allocated_area; + + if remaining_space >= width * height && remaining_space < best_remaining_space { + best_remaining_space = remaining_space; + best_atlas_idx = Some(idx); + } + } + + if let Some(idx) = best_atlas_idx { + let atlas = &mut self.atlases[idx]; + if let Some(allocation) = atlas.allocate(width, height) { + return Ok(AtlasAllocation { + atlas_id: atlas.id, + allocation, + }); + } + } + + // Fallback to first-fit if best-fit didn't work + self.allocate_first_fit(width, height) + } + + /// Allocate using least-used strategy: prefer the atlas with the lowest usage percentage. + fn allocate_least_used( + &mut self, + width: u32, + height: u32, + ) -> Result<AtlasAllocation, AtlasError> { + let mut best_atlas_idx = None; + let mut lowest_usage = f32::MAX; + + // Find the atlas with the lowest usage percentage + for (idx, atlas) in self.atlases.iter().enumerate() { + let usage = atlas.stats().usage_percentage(); + if usage < lowest_usage { + lowest_usage = usage; + best_atlas_idx = Some(idx); + } + } + + if let Some(idx) = best_atlas_idx { + if let Some(allocation) = self.atlases[idx].allocate(width, height) { + let atlas_id = self.atlases[idx].id; + return Ok(AtlasAllocation { + atlas_id, + allocation, + }); + } + } + + // Fallback to first-fit if least-used didn't work + self.allocate_first_fit(width, height) + } + + /// Allocate using round-robin strategy: cycle through atlases using a round-robin counter. + fn allocate_round_robin( + &mut self, + width: u32, + height: u32, + ) -> Result<AtlasAllocation, AtlasError> { + if self.atlases.is_empty() { + return self.allocate_first_fit(width, height); + } + + let start_idx = self.round_robin_counter % self.atlases.len(); + + // Try starting from the round-robin position + for i in 0..self.atlases.len() { + let idx = (start_idx + i) % self.atlases.len(); + + if let Some(allocation) = self.atlases[idx].allocate(width, height) { + let atlas_id = self.atlases[idx].id; + self.round_robin_counter = (idx + 1) % self.atlases.len(); + return Ok(AtlasAllocation { + atlas_id, + allocation, + }); + } + } + + // Try creating a new atlas if auto-grow is enabled + if self.config.auto_grow && self.can_create_new_atlas() { + let atlas_id = self.create_atlas()?; + let atlas = self.atlases.last_mut().unwrap(); + if let Some(allocation) = atlas.allocate(width, height) { + self.round_robin_counter = self.atlases.len() - 1; + return Ok(AtlasAllocation { + atlas_id, + allocation, + }); + } + } + + Err(AtlasError::NoSpaceAvailable) + } + + /// Deallocate space in the specified atlas. + pub(crate) fn deallocate( + &mut self, + atlas_id: AtlasId, + alloc_id: AllocId, + width: u32, + height: u32, + ) -> Result<(), AtlasError> { + for atlas in &mut self.atlases { + if atlas.id == atlas_id { + atlas.deallocate(alloc_id, width, height); + return Ok(()); + } + } + Err(AtlasError::AtlasNotFound(atlas_id)) + } + + /// Get statistics for all atlases. + pub(crate) fn atlas_stats(&self) -> Vec<(AtlasId, &AtlasUsageStats)> { + self.atlases + .iter() + .map(|atlas| (atlas.id, atlas.stats())) + .collect() + } + + /// Get the number of atlases. + pub(crate) fn atlas_count(&self) -> usize { + self.atlases.len() + } + + /// Clear all atlases. + pub(crate) fn clear(&mut self) { + for atlas in &mut self.atlases { + atlas.clear(); + } + } +} + +impl core::fmt::Debug for MultiAtlasManager { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("MultiAtlasManager") + .field("atlas_count", &self.atlases.len()) + .field("config", &self.config) + .field("next_atlas_id", &self.next_atlas_id) + .field("round_robin_counter", &self.round_robin_counter) + .field("atlases", &self.atlases) + .finish() + } +} + +/// Represents a single atlas in the multi-atlas system. +pub(crate) struct Atlas { + /// Unique identifier for this atlas. + pub id: AtlasId, + /// Guillotiere allocator for this atlas. + allocator: AtlasAllocator, + /// Current usage statistics. + stats: AtlasUsageStats, + /// Round-robin allocation counter. + allocation_counter: u32, +} + +impl Atlas { + /// Create a new atlas with the given ID and size. + pub(crate) fn new(id: AtlasId, width: u32, height: u32) -> Self { + Self { + id, + allocator: AtlasAllocator::new(size2(width as i32, height as i32)), + stats: AtlasUsageStats { + allocated_area: 0, + total_area: width * height, + allocated_count: 0, + }, + allocation_counter: 0, + } + } + + /// Try to allocate an image in this atlas. + pub(crate) fn allocate(&mut self, width: u32, height: u32) -> Option<Allocation> { + if let Some(allocation) = self.allocator.allocate(size2(width as i32, height as i32)) { + self.stats.allocated_area += width * height; + self.stats.allocated_count += 1; + self.allocation_counter += 1; + Some(allocation) + } else { + None + } + } + + /// Deallocate an image from this atlas. + pub(crate) fn deallocate(&mut self, alloc_id: AllocId, width: u32, height: u32) { + self.allocator.deallocate(alloc_id); + self.stats.allocated_area = self.stats.allocated_area.saturating_sub(width * height); + self.stats.allocated_count = self.stats.allocated_count.saturating_sub(1); + } + + /// Get current usage statistics. + pub(crate) fn stats(&self) -> &AtlasUsageStats { + &self.stats + } + + /// Clear all allocations in this atlas. + pub(crate) fn clear(&mut self) { + self.allocator.clear(); + self.stats.allocated_area = 0; + self.stats.allocated_count = 0; + self.allocation_counter = 0; + } +} + +impl core::fmt::Debug for Atlas { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Atlas") + .field("id", &self.id) + .field("stats", &self.stats) + .field("allocation_counter", &self.allocation_counter) + .finish_non_exhaustive() + } +} + +/// Errors that can occur during atlas operations. +#[derive(Debug)] +pub enum AtlasError { + /// No space available in any atlas. + NoSpaceAvailable, + /// Maximum number of atlases reached. + AtlasLimitReached, + /// The requested texture size is too large for any atlas. + TextureTooLarge { + /// The width of the requested texture. + width: u32, + /// The height of the requested texture. + height: u32, + }, + /// Atlas with the given ID was not found. + AtlasNotFound(AtlasId), +} + +impl core::fmt::Display for AtlasError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::NoSpaceAvailable => write!(f, "No space available in any atlas"), + Self::AtlasLimitReached => write!(f, "Maximum number of atlases reached"), + Self::TextureTooLarge { width, height } => { + write!(f, "Texture too large ({width}x{height}) for atlas") + } + Self::AtlasNotFound(id) => write!(f, "Atlas with ID {:?} not found", id), + } + } +} + +/// Unique identifier for an atlas. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct AtlasId(pub u32); + +impl AtlasId { + /// Create a new atlas ID. + pub fn new(id: u32) -> Self { + Self(id) + } + + /// Get the raw ID value. + pub fn as_u32(self) -> u32 { + self.0 + } +} + +impl Default for AllocationStrategy { + fn default() -> Self { + Self::FirstFit + } +} + +/// Usage statistics for an atlas. +#[derive(Debug, Clone)] +pub(crate) struct AtlasUsageStats { + /// Total allocated area in pixels. + pub allocated_area: u32, + /// Total available area in pixels. + pub total_area: u32, + /// Number of allocated images. + pub allocated_count: u32, +} + +impl AtlasUsageStats { + /// Calculate usage percentage (0.0 to 1.0). + pub(crate) fn usage_percentage(&self) -> f32 { + if self.total_area == 0 { + 0.0 + } else { + self.allocated_area as f32 / self.total_area as f32 + } + } +} + +/// Result of an atlas allocation attempt. +pub(crate) struct AtlasAllocation { + /// The atlas where the allocation was made. + pub atlas_id: AtlasId, + /// The allocation details from guillotiere. + pub allocation: Allocation, +} + +/// Configuration for multiple atlas support. +#[derive(Debug, Clone, Copy)] +pub struct AtlasConfig { + /// Initial number of atlases to create. + pub initial_atlas_count: usize, + /// Maximum number of atlases to create. + pub max_atlases: usize, + /// Size of each atlas texture. + pub atlas_size: (u32, u32), + /// Whether to automatically create new atlases when needed. + pub auto_grow: bool, + /// Strategy for allocating images across atlases. + pub allocation_strategy: AllocationStrategy, +} + +impl Default for AtlasConfig { + fn default() -> Self { + Self { + initial_atlas_count: 1, + max_atlases: 8, + atlas_size: (4096, 4096), + auto_grow: true, + allocation_strategy: AllocationStrategy::FirstFit, + } + } +} + +/// Strategy for allocating images across multiple atlases. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AllocationStrategy { + /// Try atlases in order until one has space. + FirstFit, + /// Choose the atlas with the smallest remaining space that can fit the image. + BestFit, + /// Prefer the atlas with the lowest usage percentage. + LeastUsed, + /// Cycle through atlases in round-robin fashion. + RoundRobin, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_atlas_creation() { + let mut manager = MultiAtlasManager::new(AtlasConfig { + initial_atlas_count: 0, + ..Default::default() + }); + + let atlas_id = manager.create_atlas().unwrap(); + assert_eq!(atlas_id.as_u32(), 0); + assert_eq!(manager.atlas_count(), 1); + } + + #[test] + fn test_allocation_strategies() { + let mut manager = MultiAtlasManager::new(AtlasConfig { + initial_atlas_count: 1, + max_atlases: 3, + atlas_size: (256, 256), + allocation_strategy: AllocationStrategy::FirstFit, + auto_grow: true, + }); + + // Should create atlas automatically + let allocation = manager.try_allocate(100, 100).unwrap(); + assert_eq!(allocation.atlas_id.as_u32(), 0); + } + + #[test] + fn test_atlas_limit() { + let mut manager = MultiAtlasManager::new(AtlasConfig { + initial_atlas_count: 1, + max_atlases: 1, + atlas_size: (256, 256), + allocation_strategy: AllocationStrategy::FirstFit, + auto_grow: false, + }); + + assert!(manager.create_atlas().is_err()); + } + + #[test] + fn test_texture_too_large() { + let mut manager = MultiAtlasManager::new(AtlasConfig { + atlas_size: (256, 256), + ..Default::default() + }); + + let result = manager.try_allocate(300, 300); + assert!(matches!(result, Err(AtlasError::TextureTooLarge { .. }))); + } +}
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs index fc574fb..09f6cf9 100644 --- a/sparse_strips/vello_hybrid/src/render/common.rs +++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -116,10 +116,12 @@ pub image_size: u32, /// The offset of the image in the atlas texture in pixels. pub image_offset: u32, + /// The atlas index containing this image. + pub atlas_index: u32, /// Transform matrix [a, b, c, d, tx, ty]. pub transform: [f32; 6], - /// Padding for 16-byte alignment - pub _padding: [u32; 3], + /// Padding for 16-byte alignment. + pub _padding: [u32; 2], } /// GPU encoded linear gradient data.
diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs index 217a11b..27049b0 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl.rs
@@ -21,7 +21,7 @@ )] use crate::{ - GpuStrip, RenderError, RenderSettings, RenderSize, + AtlasConfig, GpuStrip, RenderError, RenderSettings, RenderSize, gradient_cache::GradientRampCache, image_cache::{ImageCache, ImageResource}, render::{ @@ -38,6 +38,7 @@ schedule::{LoadOp, RendererBackend, Scheduler}, }; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; use bytemuck::{Pod, Zeroable}; @@ -48,6 +49,7 @@ kurbo::Affine, paint::ImageSource, peniko, + pixmap::Pixmap, tile::Tile, }; use vello_sparse_shaders::{clear_slots, render_strips}; @@ -139,7 +141,7 @@ let max_texture_dimension_2d = get_max_texture_dimension_2d(&gl); let total_slots: usize = (max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize; - let image_cache = ImageCache::new(max_texture_dimension_2d, max_texture_dimension_2d); + let image_cache = ImageCache::new_with_config(settings.atlas_config); // Estimate the maximum number of gradient cache entries based on the max texture dimension // and the maximum gradient LUT size - worst case scenario. let max_gradient_cache_size = @@ -147,7 +149,7 @@ let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level); Self { - programs: WebGlPrograms::new(gl.clone(), total_slots), + programs: WebGlPrograms::new(gl.clone(), &image_cache, total_slots), scheduler: Scheduler::new(total_slots), gl, image_cache, @@ -249,6 +251,13 @@ Ok(()) } + /// Get a reference to the underlying WebGL context. + /// + /// This allows direct access to WebGL operations for advanced use cases like texture creation. + pub fn gl_context(&self) -> &WebGl2RenderingContext { + &self.gl + } + /// Upload image to cache and atlas in one step. Returns the `ImageId`. /// /// This is the WebGL analogue of the wgpu Renderer's `upload_image` method. @@ -259,19 +268,23 @@ ) -> vello_common::paint::ImageId { let width = writer.width(); let height = writer.height(); - let image_id = self.image_cache.allocate(width, height); + let image_id = self.image_cache.allocate(width, height).unwrap(); let image_resource = self .image_cache .get(image_id) .expect("Image resource not found"); + + self.programs + .maybe_resize_atlas_texture_array(&self.gl, self.image_cache.atlas_count() as u32); let offset = [ image_resource.offset[0] as u32, image_resource.offset[1] as u32, ]; - - writer.write_to_atlas( + // Write to the appropriate layer in the atlas texture array + writer.write_to_atlas_layer( &self.gl, - &self.programs.resources.atlas_texture, + &self.programs.resources.atlas_texture_array.texture, + image_resource.atlas_id.as_u32(), offset, width, height, @@ -280,17 +293,11 @@ image_id } - /// Get a reference to the underlying WebGL context. - /// - /// This allows direct access to WebGL operations for advanced use cases like texture creation. - pub fn gl_context(&self) -> &WebGl2RenderingContext { - &self.gl - } - /// Destroy an image from the cache and clear the allocated slot in the atlas. pub fn destroy_image(&mut self, image_id: vello_common::paint::ImageId) { if let Some(image_resource) = self.image_cache.deallocate(image_id) { self.clear_atlas_region( + image_resource.atlas_id, [ image_resource.offset[0] as u32, image_resource.offset[1] as u32, @@ -301,25 +308,33 @@ } } - /// Clear a specific region of the atlas texture with uninitialized data. - fn clear_atlas_region(&mut self, offset: [u32; 2], width: u32, height: u32) { + /// Clear a specific region of the atlas texture array with uninitialized data. + fn clear_atlas_region( + &mut self, + atlas_id: crate::AtlasId, + offset: [u32; 2], + width: u32, + height: u32, + ) { // Rgba8Unorm is 4 bytes per pixel let uninitialized_data = vec![0_u8; (width * height * 4) as usize]; self.gl.active_texture(WebGl2RenderingContext::TEXTURE0); self.gl.bind_texture( - WebGl2RenderingContext::TEXTURE_2D, - Some(&self.programs.resources.atlas_texture), + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + Some(&self.programs.resources.atlas_texture_array.texture), ); self.gl - .tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array( - WebGl2RenderingContext::TEXTURE_2D, - 0, // mip level - offset[0] as i32, // x offset - offset[1] as i32, // y offset + .tex_sub_image_3d_with_opt_u8_array( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + 0, + offset[0] as i32, + offset[1] as i32, + atlas_id.as_u32() as i32, width as i32, height as i32, + 1, WebGl2RenderingContext::RGBA, WebGl2RenderingContext::UNSIGNED_BYTE, Some(&uninitialized_data), @@ -390,8 +405,9 @@ quality_and_extend_modes, image_size, image_offset, + atlas_index: image_resource.atlas_id.as_u32(), transform, - _padding: [0, 0, 0], + _padding: [0, 0], }) } @@ -501,7 +517,7 @@ /// Clip input texture location. clip_input_texture: WebGlUniformLocation, /// Atlas texture location. - atlas_texture: WebGlUniformLocation, + atlas_texture_array: WebGlUniformLocation, /// Encoded paints texture location for fragment shader. encoded_paints_texture_fs: WebGlUniformLocation, /// Encoded paints texture location for vertex shader. @@ -528,9 +544,8 @@ alphas_texture: WebGlTexture, /// Height of alpha texture. alpha_texture_height: u32, - /// Texture for atlas data - // TODO: Support more than one atlas texture - atlas_texture: WebGlTexture, + /// Texture array for atlas data (multiple atlases supported) + atlas_texture_array: WebGlTextureArray, /// Encoded paints texture for image metadata. encoded_paints_texture: WebGlTexture, /// Height of encoded paints texture. @@ -583,7 +598,7 @@ impl WebGlPrograms { /// Creates programs and initializes resources. - fn new(gl: WebGl2RenderingContext, slot_count: usize) -> Self { + fn new(gl: WebGl2RenderingContext, image_cache: &ImageCache, slot_count: usize) -> Self { let strip_program = create_shader_program( &gl, render_strips::VERTEX_SOURCE, @@ -598,7 +613,7 @@ let strip_uniforms = get_strip_uniforms(&gl, &strip_program); let clear_uniforms = get_clear_uniforms(&gl, &clear_program); - let resources = create_webgl_resources(&gl, slot_count); + let resources = create_webgl_resources(&gl, image_cache, slot_count); initialize_strip_vao(&gl, &resources); initialize_clear_vao(&gl, &resources); @@ -655,6 +670,112 @@ self.clear_view_framebuffer(gl); } + /// Resize atlas texture array to accommodate more atlases. + fn maybe_resize_atlas_texture_array( + &mut self, + gl: &WebGl2RenderingContext, + required_atlas_count: u32, + ) { + let WebGlTextureSize { + width, + height, + depth_or_array_layers: current_atlas_count, + } = self.resources.atlas_texture_array.size(); + if required_atlas_count > current_atlas_count { + // Create new texture array with more layers + let new_atlas_texture_array = + create_atlas_texture_array(gl, width, height, required_atlas_count); + + // Copy existing atlas data from old texture array to new one + self.copy_atlas_texture_data(gl, &new_atlas_texture_array, current_atlas_count); + + // Replace the old resources + self.resources.atlas_texture_array = new_atlas_texture_array; + } + } + + /// Copy texture data from the old atlas texture array to a new one. + /// This is necessary when resizing the texture array to preserve existing atlas data. + fn copy_atlas_texture_data( + &self, + gl: &WebGl2RenderingContext, + new_atlas_texture_array: &WebGlTextureArray, + layer_count_to_copy: u32, + ) { + let WebGlTextureSize { width, height, .. } = self.resources.atlas_texture_array.size(); + + // Create framebuffers for read and draw operations + let read_framebuffer = gl.create_framebuffer().unwrap(); + let draw_framebuffer = gl.create_framebuffer().unwrap(); + + // Save original framebuffer bindings + let original_read_fb = gl + .get_parameter(WebGl2RenderingContext::READ_FRAMEBUFFER_BINDING) + .ok() + .and_then(|v| v.dyn_into::<web_sys::WebGlFramebuffer>().ok()); + let original_draw_fb = gl + .get_parameter(WebGl2RenderingContext::DRAW_FRAMEBUFFER_BINDING) + .ok() + .and_then(|v| v.dyn_into::<web_sys::WebGlFramebuffer>().ok()); + + // Copy each layer using blitFramebuffer + for layer in 0..layer_count_to_copy { + // Attach source layer to read framebuffer + gl.bind_framebuffer( + WebGl2RenderingContext::READ_FRAMEBUFFER, + Some(&read_framebuffer), + ); + gl.framebuffer_texture_layer( + WebGl2RenderingContext::READ_FRAMEBUFFER, + WebGl2RenderingContext::COLOR_ATTACHMENT0, + Some(&self.resources.atlas_texture_array.texture), + 0, + layer as i32, + ); + + // Attach destination layer to draw framebuffer + gl.bind_framebuffer( + WebGl2RenderingContext::DRAW_FRAMEBUFFER, + Some(&draw_framebuffer), + ); + gl.framebuffer_texture_layer( + WebGl2RenderingContext::DRAW_FRAMEBUFFER, + WebGl2RenderingContext::COLOR_ATTACHMENT0, + Some(&new_atlas_texture_array.texture), + 0, + layer as i32, + ); + + // Perform the blit operation + gl.blit_framebuffer( + 0, + 0, + width as i32, + height as i32, + 0, + 0, + width as i32, + height as i32, + WebGl2RenderingContext::COLOR_BUFFER_BIT, + WebGl2RenderingContext::NEAREST, + ); + } + + // Restore original framebuffer bindings + gl.bind_framebuffer( + WebGl2RenderingContext::READ_FRAMEBUFFER, + original_read_fb.as_ref(), + ); + gl.bind_framebuffer( + WebGl2RenderingContext::DRAW_FRAMEBUFFER, + original_draw_fb.as_ref(), + ); + + // Clean up + gl.delete_framebuffer(Some(&read_framebuffer)); + gl.delete_framebuffer(Some(&draw_framebuffer)); + } + /// Update the alpha texture size if needed. fn maybe_resize_alphas_tex(&mut self, max_texture_dimension_2d: u32, alphas: &[u8]) { let required_alpha_height = (alphas.len() as u32) @@ -1064,7 +1185,7 @@ // Get texture uniform locations. let alphas_texture_name = render_strips::fragment::ALPHAS_TEXTURE; let clip_input_texture_name = render_strips::fragment::CLIP_INPUT_TEXTURE; - let atlas_texture_name = render_strips::fragment::ATLAS_TEXTURE; + let atlas_texture_array_name = render_strips::fragment::ATLAS_TEXTURE_ARRAY; let encoded_paints_texture_fs_name = render_strips::fragment::ENCODED_PAINTS_TEXTURE; let encoded_paints_texture_vs_name = render_strips::vertex::ENCODED_PAINTS_TEXTURE; let gradient_texture_name = render_strips::fragment::GRADIENT_TEXTURE; @@ -1078,8 +1199,8 @@ clip_input_texture: gl .get_uniform_location(program, clip_input_texture_name) .unwrap(), - atlas_texture: gl - .get_uniform_location(program, atlas_texture_name) + atlas_texture_array: gl + .get_uniform_location(program, atlas_texture_array_name) .unwrap(), encoded_paints_texture_fs: gl .get_uniform_location(program, encoded_paints_texture_fs_name) @@ -1111,7 +1232,11 @@ } /// Create all WebGL resources needed for rendering. -fn create_webgl_resources(gl: &WebGl2RenderingContext, slot_count: usize) -> WebGlResources { +fn create_webgl_resources( + gl: &WebGl2RenderingContext, + image_cache: &ImageCache, + slot_count: usize, +) -> WebGlResources { let strip_vao = gl.create_vertex_array().unwrap(); let clear_vao = gl.create_vertex_array().unwrap(); @@ -1148,47 +1273,13 @@ ); } - // Create and configure atlas texture. - let atlas_texture = gl.create_texture().unwrap(); - { - gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&atlas_texture)); - gl.tex_parameteri( - WebGl2RenderingContext::TEXTURE_2D, - WebGl2RenderingContext::TEXTURE_MIN_FILTER, - WebGl2RenderingContext::LINEAR as i32, - ); - gl.tex_parameteri( - WebGl2RenderingContext::TEXTURE_2D, - WebGl2RenderingContext::TEXTURE_MAG_FILTER, - WebGl2RenderingContext::LINEAR as i32, - ); - gl.tex_parameteri( - WebGl2RenderingContext::TEXTURE_2D, - WebGl2RenderingContext::TEXTURE_WRAP_S, - WebGl2RenderingContext::CLAMP_TO_EDGE as i32, - ); - gl.tex_parameteri( - WebGl2RenderingContext::TEXTURE_2D, - WebGl2RenderingContext::TEXTURE_WRAP_T, - WebGl2RenderingContext::CLAMP_TO_EDGE as i32, - ); - - // Initialize with empty texture data - let max_texture_dimension_2d = get_max_texture_dimension_2d(gl); - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( - WebGl2RenderingContext::TEXTURE_2D, - 0, - WebGl2RenderingContext::RGBA8 as i32, - max_texture_dimension_2d as i32, - max_texture_dimension_2d as i32, - 0, - WebGl2RenderingContext::RGBA, - WebGl2RenderingContext::UNSIGNED_BYTE, - None, - ) - .unwrap(); - } + let AtlasConfig { + atlas_size: (atlas_width, atlas_height), + initial_atlas_count, + .. + } = image_cache.atlas_manager().config(); + let atlas_texture_array = + create_atlas_texture_array(gl, *atlas_width, *atlas_height, *initial_atlas_count as u32); // Create and configure encoded paints texture. let encoded_paints_texture = gl.create_texture().unwrap(); @@ -1294,7 +1385,7 @@ strips_buffer, alphas_texture, alpha_texture_height: 0, - atlas_texture, + atlas_texture_array, encoded_paints_texture, encoded_paints_texture_height: 0, gradient_texture, @@ -1312,6 +1403,59 @@ } } +/// Create an atlas texture array. +fn create_atlas_texture_array( + gl: &WebGl2RenderingContext, + width: u32, + height: u32, + layer_count: u32, +) -> WebGlTextureArray { + let atlas_texture = gl.create_texture().unwrap(); + gl.active_texture(WebGl2RenderingContext::TEXTURE0); + gl.bind_texture( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + Some(&atlas_texture), + ); + + gl.tex_parameteri( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + WebGl2RenderingContext::TEXTURE_MIN_FILTER, + WebGl2RenderingContext::LINEAR as i32, + ); + gl.tex_parameteri( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + WebGl2RenderingContext::TEXTURE_MAG_FILTER, + WebGl2RenderingContext::LINEAR as i32, + ); + gl.tex_parameteri( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + WebGl2RenderingContext::TEXTURE_WRAP_S, + WebGl2RenderingContext::CLAMP_TO_EDGE as i32, + ); + gl.tex_parameteri( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + WebGl2RenderingContext::TEXTURE_WRAP_T, + WebGl2RenderingContext::CLAMP_TO_EDGE as i32, + ); + + // Initialize with empty texture array data + gl.tex_image_3d_with_opt_u8_array( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + 0, + WebGl2RenderingContext::RGBA8 as i32, + width as i32, + height as i32, + layer_count as i32, + 0, + WebGl2RenderingContext::RGBA, + WebGl2RenderingContext::UNSIGNED_BYTE, + None, + ) + .unwrap(); + + WebGlTextureArray::new(atlas_texture, width, height, layer_count) +} + /// Create a texture for slot rendering. fn create_slot_texture(gl: &WebGl2RenderingContext, slot_count: usize) -> WebGlTexture { let texture = gl.create_texture().unwrap(); @@ -1533,14 +1677,14 @@ self.gl .uniform1i(Some(&self.programs.strip_uniforms.clip_input_texture), 1); - // Bind atlas texture for image rendering + // Bind atlas texture array for image rendering self.gl.active_texture(WebGl2RenderingContext::TEXTURE2); self.gl.bind_texture( - WebGl2RenderingContext::TEXTURE_2D, - Some(&self.programs.resources.atlas_texture), + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + Some(&self.programs.resources.atlas_texture_array.texture), ); self.gl - .uniform1i(Some(&self.programs.strip_uniforms.atlas_texture), 2); + .uniform1i(Some(&self.programs.strip_uniforms.atlas_texture_array), 2); // Bind encoded paints texture for image metadata self.gl.active_texture(WebGl2RenderingContext::TEXTURE3); @@ -1665,19 +1809,20 @@ /// Get the height of the image. fn height(&self) -> u32; - /// Write image data to the atlas texture at the specified offset. - fn write_to_atlas( + /// Write image data to a specific layer of an atlas texture array at the specified offset. + fn write_to_atlas_layer( &self, gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, + atlas_texture_array: &WebGlTexture, + layer: u32, offset: [u32; 2], width: u32, height: u32, ); } -/// Implementation for `Pixmap` - direct upload using raw pixel data -impl WebGlAtlasWriter for crate::Pixmap { +/// Implementation for `Pixmap` - direct upload using raw pixel data. +impl WebGlAtlasWriter for Pixmap { fn width(&self) -> u32 { self.width() as u32 } @@ -1686,29 +1831,35 @@ self.height() as u32 } - fn write_to_atlas( + fn write_to_atlas_layer( &self, gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, + atlas_texture_array: &WebGlTexture, + layer: u32, offset: [u32; 2], width: u32, height: u32, ) { - // Bind the atlas texture + // Bind the atlas texture array gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(atlas_texture)); + gl.bind_texture( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + Some(atlas_texture_array), + ); // Convert pixmap data to the format expected by WebGL let rgba_data = self.data_as_u8_slice(); - // Upload the image data to the specific region of the atlas texture - gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array( - WebGl2RenderingContext::TEXTURE_2D, - 0, // mip level - offset[0] as i32, // x offset - offset[1] as i32, // y offset + // Upload the image data to the specific layer and region of the atlas texture array + gl.tex_sub_image_3d_with_opt_u8_array( + WebGl2RenderingContext::TEXTURE_2D_ARRAY, + 0, + offset[0] as i32, + offset[1] as i32, + layer as i32, width as i32, height as i32, + 1, WebGl2RenderingContext::RGBA, WebGl2RenderingContext::UNSIGNED_BYTE, Some(rgba_data), @@ -1717,8 +1868,8 @@ } } -/// Implementation for `Arc<Pixmap>` -impl WebGlAtlasWriter for alloc::sync::Arc<crate::Pixmap> { +/// Implementation for `Arc<Pixmap>`. +impl WebGlAtlasWriter for Arc<Pixmap> { fn width(&self) -> u32 { self.as_ref().width() as u32 } @@ -1727,20 +1878,21 @@ self.as_ref().height() as u32 } - fn write_to_atlas( + fn write_to_atlas_layer( &self, gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, + atlas_texture_array: &WebGlTexture, + layer: u32, offset: [u32; 2], width: u32, height: u32, ) { self.as_ref() - .write_to_atlas(gl, atlas_texture, offset, width, height); + .write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height); } } -/// Implementation for `WebGlTexture` - texture-to-texture copy +/// Implementation for `WebGlTexture` - texture-to-texture copy. impl WebGlAtlasWriter for WebGlTexture { fn width(&self) -> u32 { // WebGL textures don't expose their dimensions directly @@ -1756,56 +1908,86 @@ unreachable!("WebGlTexture height must be provided by caller") } - fn write_to_atlas( + fn write_to_atlas_layer( &self, gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, + atlas_texture_array: &WebGlTexture, + layer: u32, offset: [u32; 2], width: u32, height: u32, ) { - // Create a temporary framebuffer for the source texture - let framebuffer = gl.create_framebuffer().unwrap(); - gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&framebuffer)); + // Create framebuffers for read and draw operations + let read_framebuffer = gl.create_framebuffer().unwrap(); + let draw_framebuffer = gl.create_framebuffer().unwrap(); - // Attach the source texture to the framebuffer + // Save original framebuffer bindings + let original_read_fb = gl + .get_parameter(WebGl2RenderingContext::READ_FRAMEBUFFER_BINDING) + .ok() + .and_then(|v| v.dyn_into::<web_sys::WebGlFramebuffer>().ok()); + let original_draw_fb = gl + .get_parameter(WebGl2RenderingContext::DRAW_FRAMEBUFFER_BINDING) + .ok() + .and_then(|v| v.dyn_into::<web_sys::WebGlFramebuffer>().ok()); + + // Attach source texture to read framebuffer + gl.bind_framebuffer( + WebGl2RenderingContext::READ_FRAMEBUFFER, + Some(&read_framebuffer), + ); gl.framebuffer_texture_2d( - WebGl2RenderingContext::FRAMEBUFFER, + WebGl2RenderingContext::READ_FRAMEBUFFER, WebGl2RenderingContext::COLOR_ATTACHMENT0, WebGl2RenderingContext::TEXTURE_2D, Some(self), - 0, // mip level + 0, ); - // Verify framebuffer is complete - let status = gl.check_framebuffer_status(WebGl2RenderingContext::FRAMEBUFFER); - if status != WebGl2RenderingContext::FRAMEBUFFER_COMPLETE { - panic!("Framebuffer not complete: {status}"); - } + // Attach destination layer to draw framebuffer + gl.bind_framebuffer( + WebGl2RenderingContext::DRAW_FRAMEBUFFER, + Some(&draw_framebuffer), + ); + gl.framebuffer_texture_layer( + WebGl2RenderingContext::DRAW_FRAMEBUFFER, + WebGl2RenderingContext::COLOR_ATTACHMENT0, + Some(atlas_texture_array), + 0, + layer as i32, + ); - // Bind the atlas texture as the target - gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(atlas_texture)); - - // Copy from framebuffer to atlas texture - gl.copy_tex_sub_image_2d( - WebGl2RenderingContext::TEXTURE_2D, - 0, // mip level - offset[0] as i32, // x offset in atlas - offset[1] as i32, // y offset in atlas - 0, // x coordinate in source framebuffer - 0, // y coordinate in source framebuffer + // Perform the blit operation + gl.blit_framebuffer( + 0, + 0, width as i32, height as i32, + offset[0] as i32, + offset[1] as i32, + (offset[0] + width) as i32, + (offset[1] + height) as i32, + WebGl2RenderingContext::COLOR_BUFFER_BIT, + WebGl2RenderingContext::NEAREST, + ); + + // Restore original framebuffer bindings + gl.bind_framebuffer( + WebGl2RenderingContext::READ_FRAMEBUFFER, + original_read_fb.as_ref(), + ); + gl.bind_framebuffer( + WebGl2RenderingContext::DRAW_FRAMEBUFFER, + original_draw_fb.as_ref(), ); // Clean up - gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, None); - gl.delete_framebuffer(Some(&framebuffer)); + gl.delete_framebuffer(Some(&read_framebuffer)); + gl.delete_framebuffer(Some(&draw_framebuffer)); } } -/// Wrapper for `WebGlTexture` with known dimensions to work around WebGL API limitations +/// Wrapper for `WebGlTexture` with known dimensions. #[derive(Debug)] pub struct WebGlTextureWithDimensions { /// The WebGL texture. @@ -1825,15 +2007,55 @@ self.height } - fn write_to_atlas( + fn write_to_atlas_layer( &self, gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, + atlas_texture_array: &WebGlTexture, + layer: u32, offset: [u32; 2], width: u32, height: u32, ) { self.texture - .write_to_atlas(gl, atlas_texture, offset, width, height); + .write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height); } } + +/// Wrapper for `WebGlTexture` array with known dimensions. +#[derive(Debug)] +struct WebGlTextureArray { + /// The WebGL texture array. + texture: WebGlTexture, + /// The size of the texture array. + size: WebGlTextureSize, +} + +impl WebGlTextureArray { + /// Create a new WebGL texture array wrapper. + fn new(texture: WebGlTexture, width: u32, height: u32, depth_or_array_layers: u32) -> Self { + Self { + texture, + size: WebGlTextureSize { + width, + height, + depth_or_array_layers, + }, + } + } + + /// Get the size of the texture array, similar to WGPU's `texture.size()`. + fn size(&self) -> WebGlTextureSize { + self.size + } +} + +/// Size information for WebGL texture arrays, similar to WGPU's `Extent3d`. +#[derive(Debug, Clone, Copy)] +struct WebGlTextureSize { + /// The width of the texture. + width: u32, + /// The height of the texture. + height: u32, + /// The number of layers in the texture array. + depth_or_array_layers: u32, +}
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs index 2fb20b7..a4439c1 100644 --- a/sparse_strips/vello_hybrid/src/render/wgpu.rs +++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs
@@ -18,10 +18,12 @@ only break in edge cases, and some of them are also only related to conversions from f64 to f32." )] -use alloc::vec; use alloc::vec::Vec; +use alloc::{sync::Arc, vec}; use core::{fmt::Debug, mem, num::NonZeroU64}; +use wgpu::Extent3d; +use crate::{AtlasConfig, AtlasId}; use crate::{ GpuStrip, RenderError, RenderSettings, RenderSize, gradient_cache::GradientRampCache, @@ -106,7 +108,7 @@ let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; let total_slots = (max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize; - let image_cache = ImageCache::new(max_texture_dimension_2d, max_texture_dimension_2d); + let image_cache = ImageCache::new_with_config(settings.atlas_config); // Estimate the maximum number of gradient cache entries based on the max texture dimension // and the maximum gradient LUT size - worst case scenario. let max_gradient_cache_size = @@ -114,7 +116,7 @@ let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level); Self { - programs: Programs::new(device, render_target_config, total_slots), + programs: Programs::new(device, &image_cache, render_target_config, total_slots), scheduler: Scheduler::new(total_slots), image_cache, gradient_cache, @@ -180,21 +182,29 @@ ) -> vello_common::paint::ImageId { let width = writer.width(); let height = writer.height(); - let image_id = self.image_cache.allocate(width, height); + let image_id = self.image_cache.allocate(width, height).unwrap(); let image_resource = self .image_cache .get(image_id) .expect("Image resource not found"); + + Programs::maybe_resize_atlas_texture_array( + device, + queue, + &mut self.programs.resources, + &self.programs.atlas_bind_group_layout, + self.image_cache.atlas_count() as u32, + ); let offset = [ image_resource.offset[0] as u32, image_resource.offset[1] as u32, ]; - - writer.write_to_atlas( + writer.write_to_atlas_layer( device, queue, encoder, - &self.programs.resources.atlas_texture, + &self.programs.resources.atlas_texture_array, + image_resource.atlas_id.as_u32(), offset, width, height, @@ -216,6 +226,7 @@ device, queue, encoder, + image_resource.atlas_id, [ image_resource.offset[0] as u32, image_resource.offset[1] as u32, @@ -232,6 +243,7 @@ _device: &Device, queue: &Queue, _encoder: &mut CommandEncoder, + atlas_id: AtlasId, offset: [u32; 2], width: u32, height: u32, @@ -240,12 +252,12 @@ let uninitialized_data = vec![0_u8; (width * height * 4) as usize]; queue.write_texture( wgpu::TexelCopyTextureInfo { - texture: &self.programs.resources.atlas_texture, + texture: &self.programs.resources.atlas_texture_array, mip_level: 0, origin: wgpu::Origin3d { x: offset[0], y: offset[1], - z: 0, + z: atlas_id.as_u32(), }, aspect: wgpu::TextureAspect::All, }, @@ -326,8 +338,9 @@ quality_and_extend_modes, image_size, image_offset, + atlas_index: image_resource.atlas_id.as_u32(), transform, - _padding: [0, 0, 0], + _padding: [0, 0], }) } @@ -415,10 +428,10 @@ encoded_paints_bind_group_layout: BindGroupLayout, /// Bind group layout for gradient texture gradient_bind_group_layout: BindGroupLayout, - + /// Bind group layout for atlas textures + atlas_bind_group_layout: BindGroupLayout, /// Pipeline for clearing slots in slot textures. clear_pipeline: RenderPipeline, - /// GPU resources for rendering (created during prepare) resources: GpuResources, /// Dimensions of the rendering target @@ -436,10 +449,11 @@ strips_buffer: Buffer, /// Texture for alpha values (used by both view and slot rendering) alphas_texture: Texture, - /// Texture for atlas data - // TODO: Support more than one atlas texture - atlas_texture: Texture, - /// Bind group for atlas texture + /// Textures for atlas data (multiple atlases supported) + atlas_texture_array: Texture, + /// View for atlas texture array + atlas_texture_array_view: TextureView, + /// Bind group for atlas textures (as texture array) atlas_bind_group: BindGroup, /// Texture for encoded paints encoded_paints_texture: Texture, @@ -496,7 +510,12 @@ } impl Programs { - fn new(device: &Device, render_target_config: &RenderTargetConfig, slot_count: usize) -> Self { + fn new( + device: &Device, + image_cache: &ImageCache, + render_target_config: &RenderTargetConfig, + slot_count: usize, + ) -> Self { let strip_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Strip Bind Group Layout"), @@ -542,7 +561,7 @@ visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, + view_dimension: wgpu::TextureViewDimension::D2Array, multisampled: false, }, count: None, @@ -734,12 +753,12 @@ resource: clear_config_buffer.as_entire_binding(), }], }); - let clear_slot_indices_buffer = Self::make_clear_slot_indices_buffer( + let clear_slot_indices_buffer = Self::create_clear_slot_indices_buffer( device, slot_count as u64 * size_of::<u32>() as u64, ); - let slot_config_buffer = Self::make_config_buffer( + let slot_config_buffer = Self::create_config_buffer( device, &RenderSize { width: u32::from(WideTile::WIDTH), @@ -750,14 +769,14 @@ let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; const INITIAL_ALPHA_TEXTURE_HEIGHT: u32 = 1; - let alphas_texture = Self::make_alphas_texture( + let alphas_texture = Self::create_alphas_texture( device, max_texture_dimension_2d, INITIAL_ALPHA_TEXTURE_HEIGHT, ); let alpha_data = vec![0; ((max_texture_dimension_2d * INITIAL_ALPHA_TEXTURE_HEIGHT) << 4) as usize]; - let view_config_buffer = Self::make_config_buffer( + let view_config_buffer = Self::create_config_buffer( device, &RenderSize { width: render_target_config.width, @@ -765,45 +784,54 @@ }, max_texture_dimension_2d, ); - let atlas_texture = Self::make_atlas_texture( + + let AtlasConfig { + atlas_size: (atlas_width, atlas_height), + initial_atlas_count, + .. + } = image_cache.atlas_manager().config(); + let (atlas_texture_array, atlas_texture_array_view) = Self::create_atlas_texture_array( device, - max_texture_dimension_2d, - max_texture_dimension_2d, - wgpu::TextureFormat::Rgba8Unorm, + *atlas_width, + *atlas_height, + *initial_atlas_count as u32, ); - let atlas_bind_group = Self::make_atlas_bind_group( + let atlas_bind_group = Self::create_atlas_bind_group( device, &atlas_bind_group_layout, - &atlas_texture.create_view(&Default::default()), + &atlas_texture_array_view, ); + const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1; let encoded_paints_data = vec![ 0; ((max_texture_dimension_2d * INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT) << 4) as usize ]; - let encoded_paints_texture = Self::make_encoded_paints_texture( + let encoded_paints_texture = Self::create_encoded_paints_texture( device, max_texture_dimension_2d, INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT, ); - let encoded_paints_bind_group = Self::make_encoded_paints_bind_group( + let encoded_paints_bind_group = Self::create_encoded_paints_bind_group( device, &encoded_paints_bind_group_layout, &encoded_paints_texture.create_view(&Default::default()), ); + const INITIAL_GRADIENT_TEXTURE_HEIGHT: u32 = 1; - let gradient_texture = Self::make_gradient_texture( + let gradient_texture = Self::create_gradient_texture( device, max_texture_dimension_2d, INITIAL_GRADIENT_TEXTURE_HEIGHT, ); - let gradient_bind_group = Self::make_gradient_bind_group( + let gradient_bind_group = Self::create_gradient_bind_group( device, &gradient_bind_group_layout, &gradient_texture.create_view(&Default::default()), ); - let slot_bind_groups = Self::make_strip_bind_groups( + + let slot_bind_groups = Self::create_strip_bind_groups( device, &strip_bind_group_layout, &alphas_texture.create_view(&Default::default()), @@ -813,14 +841,15 @@ ); let resources = GpuResources { - strips_buffer: Self::make_strips_buffer(device, 0), + strips_buffer: Self::create_strips_buffer(device, 0), clear_slot_indices_buffer, slot_texture_views, slot_config_buffer, slot_bind_groups, clear_bind_group, alphas_texture, - atlas_texture, + atlas_texture_array, + atlas_texture_array_view, atlas_bind_group, encoded_paints_texture, encoded_paints_bind_group, @@ -834,6 +863,7 @@ strip_bind_group_layout, encoded_paints_bind_group_layout, gradient_bind_group_layout, + atlas_bind_group_layout, resources, alpha_data, encoded_paints_data, @@ -841,12 +871,11 @@ width: render_target_config.width, height: render_target_config.height, }, - clear_pipeline, } } - fn make_strips_buffer(device: &Device, required_strips_size: u64) -> Buffer { + fn create_strips_buffer(device: &Device, required_strips_size: u64) -> Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some("Strips Buffer"), size: required_strips_size, @@ -855,7 +884,7 @@ }) } - fn make_clear_slot_indices_buffer(device: &Device, required_size: u64) -> Buffer { + fn create_clear_slot_indices_buffer(device: &Device, required_size: u64) -> Buffer { device.create_buffer(&wgpu::BufferDescriptor { label: Some("Slot Indices Buffer"), size: required_size, @@ -864,7 +893,7 @@ }) } - fn make_config_buffer( + fn create_config_buffer( device: &Device, render_size: &RenderSize, alpha_texture_width: u32, @@ -881,7 +910,7 @@ }) } - fn make_alphas_texture(device: &Device, width: u32, height: u32) -> Texture { + fn create_alphas_texture(device: &Device, width: u32, height: u32) -> Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("Alpha Texture"), size: wgpu::Extent3d { @@ -898,44 +927,62 @@ }) } - fn make_atlas_texture( + fn create_atlas_texture_array( device: &Device, width: u32, height: u32, - format: wgpu::TextureFormat, - ) -> Texture { - device.create_texture(&wgpu::TextureDescriptor { - label: Some("Atlas Texture"), + atlas_count: u32, + ) -> (Texture, TextureView) { + // Create a single texture array with multiple layers + let atlas_texture_array = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Atlas Texture Array"), size: wgpu::Extent3d { width, height, - depth_or_array_layers: 1, + depth_or_array_layers: atlas_count, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC, view_formats: &[], - }) + }); + + let atlas_texture_array_view = + atlas_texture_array.create_view(&wgpu::TextureViewDescriptor { + label: Some("Atlas Texture Array View"), + format: None, + dimension: Some(wgpu::TextureViewDimension::D2Array), + aspect: wgpu::TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: Some(atlas_count), + usage: None, + }); + + (atlas_texture_array, atlas_texture_array_view) } - fn make_atlas_bind_group( + fn create_atlas_bind_group( device: &Device, atlas_bind_group_layout: &BindGroupLayout, - atlas_texture_view: &TextureView, + atlas_texture_array_view: &TextureView, ) -> BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Atlas Bind Group"), layout: atlas_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(atlas_texture_view), + resource: wgpu::BindingResource::TextureView(atlas_texture_array_view), }], }) } - fn make_encoded_paints_texture(device: &Device, width: u32, height: u32) -> Texture { + fn create_encoded_paints_texture(device: &Device, width: u32, height: u32) -> Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("Encoded Paints Texture"), size: wgpu::Extent3d { @@ -952,7 +999,7 @@ }) } - fn make_encoded_paints_bind_group( + fn create_encoded_paints_bind_group( device: &Device, encoded_paints_bind_group_layout: &BindGroupLayout, encoded_paints_texture_view: &TextureView, @@ -967,7 +1014,7 @@ }) } - fn make_gradient_texture(device: &Device, width: u32, height: u32) -> Texture { + fn create_gradient_texture(device: &Device, width: u32, height: u32) -> Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("Gradient Texture"), size: wgpu::Extent3d { @@ -984,7 +1031,7 @@ }) } - fn make_gradient_bind_group( + fn create_gradient_bind_group( device: &Device, gradient_bind_group_layout: &BindGroupLayout, gradient_texture_view: &TextureView, @@ -999,7 +1046,7 @@ }) } - fn make_strip_bind_groups( + fn create_strip_bind_groups( device: &Device, strip_bind_group_layout: &BindGroupLayout, alphas_texture_view: &TextureView, @@ -1008,21 +1055,21 @@ strip_texture_views: &[TextureView], ) -> [BindGroup; 3] { [ - Self::make_strip_bind_group( + Self::create_strip_bind_group( device, strip_bind_group_layout, alphas_texture_view, strip_config_buffer, &strip_texture_views[1], ), - Self::make_strip_bind_group( + Self::create_strip_bind_group( device, strip_bind_group_layout, alphas_texture_view, strip_config_buffer, &strip_texture_views[0], ), - Self::make_strip_bind_group( + Self::create_strip_bind_group( device, strip_bind_group_layout, alphas_texture_view, @@ -1032,7 +1079,7 @@ ] } - fn make_strip_bind_group( + fn create_strip_bind_group( device: &Device, strip_bind_group_layout: &BindGroupLayout, alphas_texture_view: &TextureView, @@ -1115,12 +1162,15 @@ let required_alpha_size = (max_texture_dimension_2d * required_alpha_height) << 4; self.alpha_data.resize(required_alpha_size as usize, 0); // The alpha texture encodes 16 1-byte alpha values per texel, with 4 alpha values packed in each channel - let alphas_texture = - Self::make_alphas_texture(device, max_texture_dimension_2d, required_alpha_height); + let alphas_texture = Self::create_alphas_texture( + device, + max_texture_dimension_2d, + required_alpha_height, + ); self.resources.alphas_texture = alphas_texture; // Since the alpha texture has changed, we need to update the clip bind groups. - self.resources.slot_bind_groups = Self::make_strip_bind_groups( + self.resources.slot_bind_groups = Self::create_strip_bind_groups( device, &self.strip_bind_group_layout, &self @@ -1157,7 +1207,7 @@ (max_texture_dimension_2d * required_encoded_paints_height) << 4; self.encoded_paints_data .resize(required_encoded_paints_size as usize, 0); - let encoded_paints_texture = Self::make_encoded_paints_texture( + let encoded_paints_texture = Self::create_encoded_paints_texture( device, max_texture_dimension_2d, required_encoded_paints_height, @@ -1165,7 +1215,7 @@ self.resources.encoded_paints_texture = encoded_paints_texture; // Since the encoded paints texture has changed, we need to update the strip bind groups. - self.resources.encoded_paints_bind_group = Self::make_encoded_paints_bind_group( + self.resources.encoded_paints_bind_group = Self::create_encoded_paints_bind_group( device, &self.encoded_paints_bind_group_layout, &self @@ -1195,7 +1245,7 @@ required_gradient_height <= max_texture_dimension_2d, "Gradient texture height exceeds max texture dimensions" ); - let gradient_texture = Self::make_gradient_texture( + let gradient_texture = Self::create_gradient_texture( device, max_texture_dimension_2d, required_gradient_height, @@ -1203,7 +1253,7 @@ self.resources.gradient_texture = gradient_texture; // Since the gradient texture has changed, we need to update the gradient bind group. - self.resources.gradient_bind_group = Self::make_gradient_bind_group( + self.resources.gradient_bind_group = Self::create_gradient_bind_group( device, &self.gradient_bind_group_layout, &self @@ -1237,6 +1287,98 @@ } } + /// Resize the texture array to accommodate more atlases. + fn maybe_resize_atlas_texture_array( + device: &Device, + queue: &Queue, + resources: &mut GpuResources, + atlas_bind_group_layout: &BindGroupLayout, + required_atlas_count: u32, + ) { + let Extent3d { + width, + height, + depth_or_array_layers: current_atlas_count, + } = resources.atlas_texture_array.size(); + if required_atlas_count > current_atlas_count { + // Create new texture array with more layers + let (new_atlas_texture_array, new_atlas_texture_array_view) = + Self::create_atlas_texture_array(device, width, height, required_atlas_count); + + // Copy existing atlas data from old texture array to new one + Self::copy_atlas_texture_data( + device, + queue, + &resources.atlas_texture_array, + &new_atlas_texture_array, + current_atlas_count, + width, + height, + ); + + // Update the bind group with the new texture array view + let new_atlas_bind_group = Self::create_atlas_bind_group( + device, + atlas_bind_group_layout, + &new_atlas_texture_array_view, + ); + + // Replace the old resources + resources.atlas_texture_array = new_atlas_texture_array; + resources.atlas_texture_array_view = new_atlas_texture_array_view; + resources.atlas_bind_group = new_atlas_bind_group; + } + } + + /// Copy texture data from the old atlas texture array to a new one. + /// This is necessary when resizing the texture array to preserve existing atlas data. + fn copy_atlas_texture_data( + device: &Device, + queue: &Queue, + old_atlas_texture_array: &Texture, + new_atlas_texture_array: &Texture, + layer_count_to_copy: u32, + width: u32, + height: u32, + ) { + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Atlas Resize Copy"), + }); + + // Copy each layer from old texture array to new texture array + for layer in 0..layer_count_to_copy { + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + texture: old_atlas_texture_array, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: layer, + }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyTextureInfo { + texture: new_atlas_texture_array, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: layer, + }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + } + + queue.submit([encoder.finish()]); + } + /// Upload alpha data to the texture. fn upload_alpha_texture(&mut self, queue: &Queue, alphas: &[u8]) { let texture_width = self.resources.alphas_texture.width(); @@ -1349,7 +1491,7 @@ /// Upload the strip data by creating and assigning a new `self.resources.strips_buffer`. fn upload_strips(&mut self, device: &Device, queue: &Queue, strips: &[GpuStrip]) { let required_strips_size = size_of_val(strips) as u64; - self.resources.strips_buffer = Self::make_strips_buffer(device, required_strips_size); + self.resources.strips_buffer = Self::create_strips_buffer(device, required_strips_size); // TODO: Consider using a staging belt to avoid an extra staging buffer allocation. let mut buffer = queue .write_buffer_with( @@ -1427,7 +1569,7 @@ // TODO: We currently allocate a new strips buffer for each render pass. A more efficient // approach would be to re-use buffers or slices of a larger buffer. resources.clear_slot_indices_buffer = - Programs::make_clear_slot_indices_buffer(self.device, size); + Programs::create_clear_slot_indices_buffer(self.device, size); // TODO: Consider using a staging belt to avoid an extra staging buffer allocation. let mut buffer = self .queue @@ -1499,13 +1641,14 @@ /// Get the height of the image. fn height(&self) -> u32; - /// Write image data to the atlas texture at the specified offset. - fn write_to_atlas( + /// Write image data to a specific layer of an atlas texture array at the specified offset. + fn write_to_atlas_layer( &self, device: &Device, queue: &Queue, encoder: &mut CommandEncoder, atlas_texture: &wgpu::Texture, + layer: u32, offset: [u32; 2], width: u32, height: u32, @@ -1522,12 +1665,13 @@ self.height() } - fn write_to_atlas( + fn write_to_atlas_layer( &self, _device: &Device, _queue: &Queue, encoder: &mut CommandEncoder, atlas_texture: &wgpu::Texture, + layer: u32, offset: [u32; 2], width: u32, height: u32, @@ -1545,7 +1689,7 @@ origin: wgpu::Origin3d { x: offset[0], y: offset[1], - z: 0, + z: layer, }, aspect: wgpu::TextureAspect::All, }, @@ -1568,12 +1712,13 @@ self.height() as u32 } - fn write_to_atlas( + fn write_to_atlas_layer( &self, _device: &Device, queue: &Queue, _encoder: &mut CommandEncoder, atlas_texture: &wgpu::Texture, + layer: u32, offset: [u32; 2], width: u32, height: u32, @@ -1585,7 +1730,7 @@ origin: wgpu::Origin3d { x: offset[0], y: offset[1], - z: 0, + z: layer, }, aspect: wgpu::TextureAspect::All, }, @@ -1605,7 +1750,7 @@ } /// Implementation for `Arc<Pixmap>` -impl AtlasWriter for alloc::sync::Arc<Pixmap> { +impl AtlasWriter for Arc<Pixmap> { fn width(&self) -> u32 { self.as_ref().width() as u32 } @@ -1614,17 +1759,26 @@ self.as_ref().height() as u32 } - fn write_to_atlas( + fn write_to_atlas_layer( &self, device: &Device, queue: &Queue, encoder: &mut CommandEncoder, atlas_texture: &wgpu::Texture, + layer: u32, offset: [u32; 2], width: u32, height: u32, ) { - self.as_ref() - .write_to_atlas(device, queue, encoder, atlas_texture, offset, width, height); + self.as_ref().write_to_atlas_layer( + device, + queue, + encoder, + atlas_texture, + layer, + offset, + width, + height, + ); } }
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index 6040e83..b30b17a 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -19,6 +19,8 @@ use vello_common::strip::Strip; use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage}; +use crate::AtlasConfig; + /// Default tolerance for curve flattening pub(crate) const DEFAULT_TOLERANCE: f64 = 0.1; @@ -27,12 +29,15 @@ pub struct RenderSettings { /// The SIMD level that should be used for rendering operations. pub level: Level, + /// The configuration for the atlas. + pub atlas_config: AtlasConfig, } impl Default for RenderSettings { fn default() -> Self { Self { level: Level::try_detect().unwrap_or(Level::fallback()), + atlas_config: AtlasConfig::default(), } } }
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl index b1306ba..2452142 100644 --- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl +++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -195,7 +195,7 @@ var<uniform> config: Config; @group(1) @binding(0) -var atlas_texture: texture_2d<f32>; +var atlas_texture_array: texture_2d_array<f32>; @group(2) @binding(0) var encoded_paints_texture: texture_2d<u32>; @@ -321,26 +321,34 @@ if encoded_image.quality == IMAGE_QUALITY_HIGH { let final_xy = image_offset + extended_xy; let sample_color = bicubic_sample( - atlas_texture, + atlas_texture_array, final_xy, + i32(encoded_image.atlas_index), image_offset, image_size, - encoded_image.extend_modes + encoded_image.extend_modes, ); final_color = alpha * sample_color; } else if encoded_image.quality == IMAGE_QUALITY_MEDIUM { let final_xy = image_offset + extended_xy - vec2(0.5); let sample_color = bilinear_sample( - atlas_texture, + atlas_texture_array, final_xy, + i32(encoded_image.atlas_index), image_offset, image_size, - encoded_image.extend_modes + encoded_image.extend_modes, ); final_color = alpha * sample_color; } else if encoded_image.quality == IMAGE_QUALITY_LOW { let final_xy = image_offset + extended_xy; - final_color = alpha * textureLoad(atlas_texture, vec2<u32>(final_xy), 0); + let sample_color = textureLoad( + atlas_texture_array, + vec2<u32>(final_xy), + i32(encoded_image.atlas_index), + 0, + ); + final_color = alpha * sample_color; } } else if paint_type == PAINT_TYPE_LINEAR_GRADIENT { let paint_tex_idx = in.paint & PAINT_TEXTURE_INDEX_MASK; @@ -737,6 +745,8 @@ image_size: vec2<f32>, /// The offset of the image in pixels. image_offset: vec2<f32>, + /// The atlas index containing this image. + atlas_index: u32, /// Linear transformation matrix coefficients for 2D affine transformation. /// Contains [a, b, c, d] where the transformation matrix is: /// This enables scaling, rotation, and skewing of the image coordinates. @@ -759,17 +769,19 @@ let image_size = vec2<f32>(f32(texel0.y >> 16u), f32(texel0.y & 0xFFFFu)); // Unpack image_offset from texel0.z (stored as u32, unpack to x/y) let image_offset = vec2<f32>(f32(texel0.z >> 16u), f32(texel0.z & 0xFFFFu)); + let atlas_index = texel0.w; let transform = vec4<f32>( - bitcast<f32>(texel0.w), bitcast<f32>(texel1.x), - bitcast<f32>(texel1.y), bitcast<f32>(texel1.z) + bitcast<f32>(texel1.x), bitcast<f32>(texel1.y), + bitcast<f32>(texel1.z), bitcast<f32>(texel1.w) ); - let translate = vec2<f32>(bitcast<f32>(texel1.w), bitcast<f32>(texel2.x)); + let translate = vec2<f32>(bitcast<f32>(texel2.x), bitcast<f32>(texel2.y)); return EncodedImage( quality, vec2<u32>(extend_x, extend_y), image_size, image_offset, + atlas_index, transform, translate ); @@ -822,20 +834,21 @@ // Bilinear filtering consists of sampling the 4 surrounding pixels of the target point and // interpolating them with a bilinear filter. fn bilinear_sample( - tex: texture_2d<f32>, + tex: texture_2d_array<f32>, coords: vec2<f32>, + atlas_idx: i32, image_offset: vec2<f32>, image_size: vec2<f32>, - extend_modes: vec2<u32> + extend_modes: vec2<u32>, ) -> vec4<f32> { let atlas_max = image_offset + image_size - vec2(1.0); let atlas_uv_clamped = clamp(coords, image_offset, atlas_max); let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); let uv_frac = fract(coords); - let a = textureLoad(tex, vec2<i32>(uv_quad.xy), 0); - let b = textureLoad(tex, vec2<i32>(uv_quad.xw), 0); - let c = textureLoad(tex, vec2<i32>(uv_quad.zy), 0); - let d = textureLoad(tex, vec2<i32>(uv_quad.zw), 0); + let a = textureLoad(tex, vec2<i32>(uv_quad.xy), atlas_idx, 0); + let b = textureLoad(tex, vec2<i32>(uv_quad.xw), atlas_idx, 0); + let c = textureLoad(tex, vec2<i32>(uv_quad.zy), atlas_idx, 0); + let d = textureLoad(tex, vec2<i32>(uv_quad.zw), atlas_idx, 0); return mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); } @@ -846,8 +859,9 @@ // of the cubic function used to calculate weights based on the `x_fract` and `y_fract` of the // location we are looking at. fn bicubic_sample( - tex: texture_2d<f32>, + tex: texture_2d_array<f32>, coords: vec2<f32>, + atlas_idx: i32, image_offset: vec2<f32>, image_size: vec2<f32>, extend_modes: vec2<u32>, @@ -859,25 +873,25 @@ let cy = cubic_weights(frac_coords.y); // Sample 4x4 grid around coords - let s00 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, -1.5), image_offset, atlas_max)), 0); - let s10 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, -1.5), image_offset, atlas_max)), 0); - let s20 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, -1.5), image_offset, atlas_max)), 0); - let s30 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, -1.5), image_offset, atlas_max)), 0); + let s00 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); + let s10 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); + let s20 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); + let s30 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s01 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, -0.5), image_offset, atlas_max)), 0); - let s11 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, -0.5), image_offset, atlas_max)), 0); - let s21 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, -0.5), image_offset, atlas_max)), 0); - let s31 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, -0.5), image_offset, atlas_max)), 0); + let s01 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); + let s11 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); + let s21 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); + let s31 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s02 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, 0.5), image_offset, atlas_max)), 0); - let s12 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, 0.5), image_offset, atlas_max)), 0); - let s22 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, 0.5), image_offset, atlas_max)), 0); - let s32 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, 0.5), image_offset, atlas_max)), 0); + let s02 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); + let s12 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); + let s22 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); + let s32 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s03 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, 1.5), image_offset, atlas_max)), 0); - let s13 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, 1.5), image_offset, atlas_max)), 0); - let s23 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, 1.5), image_offset, atlas_max)), 0); - let s33 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, 1.5), image_offset, atlas_max)), 0); + let s03 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); + let s13 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); + let s23 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); + let s33 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); // Interpolate in x direction for each row let row0 = cx.x * s00 + cx.y * s10 + cx.z * s20 + cx.w * s30;