.
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 b5dbef0..2c92302 100644
--- a/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
+++ b/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
@@ -17,9 +17,7 @@
paint::{ImageId, ImageSource},
};
use vello_example_scenes::{AnyScene, image::ImageScene};
-use vello_hybrid::{
- AllocationStrategy, AtlasConfig, Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene,
-};
+use vello_hybrid::{AtlasConfig, Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene};
use wasm_bindgen::prelude::*;
use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent};
@@ -90,16 +88,7 @@
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,
+ ..AtlasConfig::default()
},
},
);
diff --git a/sparse_strips/vello_hybrid/src/image_cache.rs b/sparse_strips/vello_hybrid/src/image_cache.rs
index b877a7b..fbf1c8d 100644
--- a/sparse_strips/vello_hybrid/src/image_cache.rs
+++ b/sparse_strips/vello_hybrid/src/image_cache.rs
@@ -1,8 +1,6 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
-#![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;
@@ -49,15 +47,6 @@
}
impl ImageCache {
- /// Create a new image cache with default atlas configuration.
- pub(crate) fn new(width: u32, height: u32) -> Self {
- Self {
- atlas_manager: MultiAtlasManager::new_with_initial_atlas(width, height),
- slots: Vec::new(),
- free_idxs: Vec::new(),
- }
- }
-
/// Create a new image cache with custom atlas configuration.
pub(crate) fn new_with_config(config: AtlasConfig) -> Self {
Self {
@@ -89,7 +78,7 @@
});
let image_id = ImageId::new(slot_idx as u32);
- self.slots[slot_idx] = Some(ImageResource {
+ let image_resource = ImageResource {
id: image_id,
width: width as u16,
height: height as u16,
@@ -99,8 +88,9 @@
atlas_alloc.allocation.rectangle.min.y as u16,
],
atlas_alloc_id: atlas_alloc.allocation.id,
- });
- Ok(image_id)
+ };
+ self.slots[slot_idx] = Some(image_resource);
+ Ok(self.slots[slot_idx].as_ref().unwrap().id)
}
/// Deallocate an image from the cache, returning the image resource if it existed.
@@ -108,12 +98,14 @@
let index = id.as_u32() as usize;
if let Some(image_resource) = self.slots.get_mut(index).and_then(Option::take) {
// 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.atlas_manager
+ .deallocate(
+ image_resource.atlas_id,
+ image_resource.atlas_alloc_id,
+ image_resource.width as u32,
+ image_resource.height as u32,
+ )
+ .unwrap();
self.free_idxs.push(index);
Some(image_resource)
} else {
@@ -126,22 +118,10 @@
&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_manager.clear();
- }
}
#[cfg(test)]
@@ -152,7 +132,10 @@
#[test]
fn test_insert_single_image() {
- let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE);
+ let mut cache = ImageCache::new_with_config(AtlasConfig {
+ atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
+ ..Default::default()
+ });
let id = cache.allocate(100, 100).unwrap();
@@ -161,12 +144,16 @@
assert_eq!(resource.id, id);
assert_eq!(resource.width, 100);
assert_eq!(resource.height, 100);
- assert_eq!(resource.offset, [0, 0]); // First image should be at origin
+ // First image should be at origin
+ assert_eq!(resource.offset, [0, 0]);
}
#[test]
fn test_insert_multiple_images() {
- let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE);
+ let mut cache = ImageCache::new_with_config(AtlasConfig {
+ atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
+ ..Default::default()
+ });
let id1 = cache.allocate(50, 50).unwrap();
let id2 = cache.allocate(75, 75).unwrap();
@@ -186,7 +173,10 @@
#[test]
fn test_get_nonexistent_image() {
- let cache: ImageCache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE);
+ let cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
+ atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
+ ..Default::default()
+ });
assert!(cache.get(ImageId::new(0)).is_none());
assert!(cache.get(ImageId::new(999)).is_none());
@@ -194,7 +184,10 @@
#[test]
fn test_remove_image() {
- let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE);
+ let mut cache = ImageCache::new_with_config(AtlasConfig {
+ atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
+ ..Default::default()
+ });
let id = cache.allocate(100, 100).unwrap();
assert!(cache.get(id).is_some());
@@ -205,7 +198,10 @@
#[test]
fn test_remove_nonexistent_image() {
- let mut cache: ImageCache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE);
+ let mut cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
+ atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
+ ..Default::default()
+ });
// Should not panic when unregistering non-existent image
cache.deallocate(ImageId::new(0));
@@ -214,7 +210,10 @@
#[test]
fn test_slot_reuse_after_remove() {
- let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE);
+ let mut cache = ImageCache::new_with_config(AtlasConfig {
+ atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
+ ..Default::default()
+ });
// Register three images
let id1 = cache.allocate(50, 50).unwrap();
@@ -243,7 +242,10 @@
#[test]
fn test_multiple_remove_and_reuse() {
- let mut cache = ImageCache::new(ATLAS_SIZE, ATLAS_SIZE);
+ let mut cache = ImageCache::new_with_config(AtlasConfig {
+ atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
+ ..Default::default()
+ });
// Register several images
let ids: Vec<_> = (0..5)
diff --git a/sparse_strips/vello_hybrid/src/lib.rs b/sparse_strips/vello_hybrid/src/lib.rs
index a236072..1865cc4 100644
--- a/sparse_strips/vello_hybrid/src/lib.rs
+++ b/sparse_strips/vello_hybrid/src/lib.rs
@@ -41,7 +41,7 @@
#[cfg(any(all(target_arch = "wasm32", feature = "webgl"), feature = "wgpu"))]
mod schedule;
pub mod util;
-pub use multi_atlas::{AllocationStrategy, AtlasConfig, AtlasError, AtlasId};
+pub use multi_atlas::{AllocationStrategy, AtlasConfig};
#[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
index 363030f..4a694fd 100644
--- a/sparse_strips/vello_hybrid/src/multi_atlas.rs
+++ b/sparse_strips/vello_hybrid/src/multi_atlas.rs
@@ -8,6 +8,7 @@
use alloc::vec::Vec;
use guillotiere::{AllocId, Allocation, AtlasAllocator, size2};
+use thiserror::Error;
/// Manages multiple texture atlases.
pub(crate) struct MultiAtlasManager {
@@ -40,16 +41,6 @@
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
@@ -86,11 +77,6 @@
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),
@@ -247,13 +233,14 @@
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))
+ // Since atlases only grow (never deallocate) and id is the index into the atlases vec,
+ // we can do a lookup instead of a linear search
+ let atlas = self
+ .atlases
+ .get_mut(atlas_id.0 as usize)
+ .ok_or(AtlasError::AtlasNotFound(atlas_id))?;
+ atlas.deallocate(alloc_id, width, height);
+ Ok(())
}
/// Get statistics for all atlases.
@@ -268,13 +255,6 @@
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 {
@@ -339,14 +319,6 @@
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 {
@@ -360,48 +332,36 @@
}
/// Errors that can occur during atlas operations.
-#[derive(Debug)]
-pub enum AtlasError {
- /// No space available in any atlas.
+#[derive(Debug, Error)]
+pub(crate) enum AtlasError {
+ #[error("No space available in any atlas")]
NoSpaceAvailable,
- /// Maximum number of atlases reached.
+ #[error("Maximum number of atlases reached")]
AtlasLimitReached,
/// The requested texture size is too large for any atlas.
+ #[error("Texture too large ({width}x{height}) for 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.
+ #[error("Atlas with Id {0:?} 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);
+pub(crate) struct AtlasId(pub u32);
impl AtlasId {
/// Create a new atlas ID.
- pub fn new(id: u32) -> Self {
+ pub(crate) fn new(id: u32) -> Self {
Self(id)
}
/// Get the raw ID value.
- pub fn as_u32(self) -> u32 {
+ pub(crate) fn as_u32(self) -> u32 {
self.0
}
}
@@ -460,6 +420,15 @@
impl Default for AtlasConfig {
fn default() -> Self {
Self {
+ // 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
+ #[cfg(all(target_arch = "wasm32", feature = "wgpu"))]
+ initial_atlas_count: 2,
+ #[cfg(not(all(target_arch = "wasm32", feature = "wgpu")))]
initial_atlas_count: 1,
max_atlases: 8,
atlas_size: (4096, 4096),
@@ -536,4 +505,152 @@
let result = manager.try_allocate(300, 300);
assert!(matches!(result, Err(AtlasError::TextureTooLarge { .. })));
}
+
+ #[test]
+ fn test_first_fit_allocation_strategy() {
+ let mut manager = MultiAtlasManager::new(AtlasConfig {
+ initial_atlas_count: 3,
+ max_atlases: 3,
+ atlas_size: (256, 256),
+ allocation_strategy: AllocationStrategy::FirstFit,
+ auto_grow: false,
+ });
+
+ // First allocation should go to atlas 0
+ let allocation0 = manager.try_allocate(100, 100).unwrap();
+ assert_eq!(allocation0.atlas_id.as_u32(), 0);
+
+ // Second allocation should also go to atlas 0 (first fit)
+ let allocation1 = manager.try_allocate(50, 50).unwrap();
+ assert_eq!(allocation1.atlas_id.as_u32(), 0);
+
+ // Third allocation should still go to atlas 0 (first fit continues to use same atlas)
+ let allocation2 = manager.try_allocate(80, 80).unwrap();
+ assert_eq!(allocation2.atlas_id.as_u32(), 0);
+
+ // Try to allocate something very large that definitely won't fit in atlas 0's remaining space
+ // This should force it to go to atlas 1
+ let allocation3 = manager.try_allocate(200, 200).unwrap();
+ assert_eq!(allocation3.atlas_id.as_u32(), 1);
+
+ // Next small allocation should go back to atlas 0 (first fit tries atlas 0 first)
+ let allocation4 = manager.try_allocate(20, 20).unwrap();
+ assert_eq!(allocation4.atlas_id.as_u32(), 0);
+ }
+
+ #[test]
+ fn test_best_fit_allocation_strategy() {
+ let mut manager = MultiAtlasManager::new(AtlasConfig {
+ initial_atlas_count: 3,
+ max_atlases: 3,
+ atlas_size: (256, 256),
+ allocation_strategy: AllocationStrategy::BestFit,
+ auto_grow: false,
+ });
+
+ // All atlases start empty, so first allocation goes to atlas 0 (first available)
+ let allocation0 = manager.try_allocate(150, 150).unwrap();
+ assert_eq!(allocation0.atlas_id.as_u32(), 0);
+
+ // Second allocation should also go to atlas 0 since it still has the least remaining space
+ // that can fit the image (all atlases have same remaining space, so it picks the first)
+ let allocation1 = manager.try_allocate(100, 100).unwrap();
+ assert_eq!(allocation1.atlas_id.as_u32(), 0);
+
+ // Now atlas 0 has less remaining space than atlases 1 and 2
+ // For a small allocation, it should still go to atlas 0 (best fit - least remaining space)
+ let allocation2 = manager.try_allocate(100, 100).unwrap();
+ assert_eq!(allocation2.atlas_id.as_u32(), 0);
+
+ // Now try to allocate something very large that won't fit in atlas 0's remaining space
+ // This should force it to go to atlas 1 (which has the most remaining space)
+ let allocation3 = manager.try_allocate(200, 200).unwrap();
+ assert_eq!(allocation3.atlas_id.as_u32(), 1);
+
+ // Now atlas 1 has less remaining space
+ // A small allocation should go to atlas 0 as it can
+ let allocation4 = manager.try_allocate(80, 80).unwrap();
+ assert_eq!(allocation4.atlas_id.as_u32(), 0);
+
+ // Now atlas 1 has less remaining space but it can't fit the allocation
+ // It should go to atlas 2 (best fit - least remaining space)
+ let allocation5 = manager.try_allocate(80, 80).unwrap();
+ assert_eq!(allocation5.atlas_id.as_u32(), 2);
+ }
+
+ #[test]
+ fn test_least_used_allocation_strategy() {
+ let mut manager = MultiAtlasManager::new(AtlasConfig {
+ initial_atlas_count: 3,
+ max_atlases: 3,
+ atlas_size: (256, 256),
+ allocation_strategy: AllocationStrategy::LeastUsed,
+ auto_grow: false,
+ });
+
+ // First allocation goes to atlas 0 (all atlases have 0% usage, picks first)
+ let allocation0 = manager.try_allocate(100, 100).unwrap();
+ assert_eq!(allocation0.atlas_id.as_u32(), 0);
+
+ // Second allocation should go to atlas 1 (least used among remaining)
+ let allocation1 = manager.try_allocate(50, 50).unwrap();
+ assert_eq!(allocation1.atlas_id.as_u32(), 1);
+
+ // Third allocation should go to atlas 2 (least used)
+ let allocation2 = manager.try_allocate(30, 30).unwrap();
+ assert_eq!(allocation2.atlas_id.as_u32(), 2);
+
+ // Fourth allocation should go to atlas 2 again (still least used)
+ let allocation3 = manager.try_allocate(30, 30).unwrap();
+ assert_eq!(allocation3.atlas_id.as_u32(), 2);
+ }
+
+ #[test]
+ fn test_round_robin_allocation_strategy() {
+ let mut manager = MultiAtlasManager::new(AtlasConfig {
+ initial_atlas_count: 3,
+ max_atlases: 3,
+ atlas_size: (256, 256),
+ allocation_strategy: AllocationStrategy::RoundRobin,
+ auto_grow: false,
+ });
+
+ // Allocations should cycle through atlases in order
+ let allocation0 = manager.try_allocate(50, 50).unwrap();
+ assert_eq!(allocation0.atlas_id.as_u32(), 0);
+
+ let allocation1 = manager.try_allocate(50, 50).unwrap();
+ assert_eq!(allocation1.atlas_id.as_u32(), 1);
+
+ let allocation2 = manager.try_allocate(50, 50).unwrap();
+ assert_eq!(allocation2.atlas_id.as_u32(), 2);
+
+ // Should wrap back to atlas 0
+ let allocation3 = manager.try_allocate(50, 50).unwrap();
+ assert_eq!(allocation3.atlas_id.as_u32(), 0);
+
+ // Continue the cycle
+ let allocation4 = manager.try_allocate(50, 50).unwrap();
+ assert_eq!(allocation4.atlas_id.as_u32(), 1);
+ }
+
+ #[test]
+ fn test_auto_grow() {
+ let mut manager = MultiAtlasManager::new(AtlasConfig {
+ initial_atlas_count: 1,
+ max_atlases: 3,
+ atlas_size: (256, 256),
+ allocation_strategy: AllocationStrategy::FirstFit,
+ auto_grow: true,
+ });
+
+ let allocation0 = manager.try_allocate(256, 256).unwrap();
+ assert_eq!(allocation0.atlas_id.as_u32(), 0);
+
+ let allocation1 = manager.try_allocate(256, 256).unwrap();
+ assert_eq!(allocation1.atlas_id.as_u32(), 1);
+
+ let allocation2 = manager.try_allocate(256, 256).unwrap();
+ assert_eq!(allocation2.atlas_id.as_u32(), 2);
+ }
}
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs
index 09f6cf9..253445d 100644
--- a/sparse_strips/vello_hybrid/src/render/common.rs
+++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -110,18 +110,20 @@
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
#[allow(dead_code, reason = "Clippy fails when --no-default-features")]
pub(crate) struct GpuEncodedImage {
- /// The rendering quality of the image.
- pub quality_and_extend_modes: u32,
+ /// Packed rendering quality, extend modes, and atlas index.
+ /// Bits 6-13: `atlas_index` (8 bits, supports up to 256 atlases)
+ /// Bits 4-5: `extend_y` (2 bits)
+ /// Bits 2-3: `extend_x` (2 bits)
+ /// Bits 0-1: `quality` (2 bits)
+ pub image_params: u32,
/// Packed image width and height.
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; 2],
+ pub _padding: [u32; 3],
}
/// GPU encoded linear gradient data.
@@ -231,16 +233,23 @@
((x as u32) << 16) | (y as u32)
}
-/// Pack image `quality` and extend modes into a single u32.
-/// `extend_y`: stored in bits 4-7
-/// `extend_x`: stored in bits 2-3
-/// `quality`: stored in bits 0-1
+/// Pack image `quality`, extend modes, and `atlas_index` into a single u32.
+/// `atlas_index`: stored in bits 6-13 (8 bits, supports up to 256 atlases)
+/// `extend_y`: stored in bits 4-5 (2 bits)
+/// `extend_x`: stored in bits 2-3 (2 bits)
+/// `quality`: stored in bits 0-1 (2 bits)
#[inline(always)]
-pub(crate) fn pack_quality_and_extend_modes(extend_x: u32, extend_y: u32, quality: u32) -> u32 {
+pub(crate) fn pack_image_params(
+ quality: u32,
+ extend_x: u32,
+ extend_y: u32,
+ atlas_index: u32,
+) -> u32 {
debug_assert!(extend_x <= 3, "extend_x must be 0-3 (2 bits)");
- debug_assert!(extend_y <= 15, "extend_y must be 0-15 (4 bits)");
+ debug_assert!(extend_y <= 3, "extend_y must be 0-3 (2 bits)");
debug_assert!(quality <= 3, "quality must be 0-3 (2 bits)");
- (extend_y << 4) | (extend_x << 2) | quality
+ debug_assert!(atlas_index <= 255, "atlas_index must be 0-255 (8 bits)");
+ (atlas_index << 6) | (extend_y << 4) | (extend_x << 2) | quality
}
#[cfg(all(target_arch = "wasm32", feature = "webgl", feature = "wgpu"))]
diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs
index 27049b0..c0bf343 100644
--- a/sparse_strips/vello_hybrid/src/render/webgl.rs
+++ b/sparse_strips/vello_hybrid/src/render/webgl.rs
@@ -24,14 +24,15 @@
AtlasConfig, GpuStrip, RenderError, RenderSettings, RenderSize,
gradient_cache::GradientRampCache,
image_cache::{ImageCache, ImageResource},
+ multi_atlas::AtlasId,
render::{
Config,
common::{
GPU_ENCODED_IMAGE_SIZE_TEXELS, GPU_LINEAR_GRADIENT_SIZE_TEXELS,
GPU_RADIAL_GRADIENT_SIZE_TEXELS, GPU_SWEEP_GRADIENT_SIZE_TEXELS, GpuEncodedImage,
GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient, GpuSweepGradient,
- pack_image_offset, pack_image_size, pack_quality_and_extend_modes,
- pack_radial_kind_and_swapped, pack_texture_width_and_extend_mode,
+ pack_image_offset, pack_image_params, pack_image_size, pack_radial_kind_and_swapped,
+ pack_texture_width_and_extend_mode,
},
},
scene::Scene,
@@ -308,38 +309,44 @@
}
}
- /// 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];
+ /// Clear a specific region of the atlas texture array.
+ fn clear_atlas_region(&mut self, atlas_id: AtlasId, offset: [u32; 2], width: u32, height: u32) {
+ let _state_guard = WebGlStateGuard::for_clear_atlas_region(&self.gl);
+ let temp_framebuffer = self.gl.create_framebuffer().unwrap();
- self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
- self.gl.bind_texture(
- WebGl2RenderingContext::TEXTURE_2D_ARRAY,
+ // Bind our temporary framebuffer
+ self.gl
+ .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&temp_framebuffer));
+
+ // Attach the specific atlas layer to the framebuffer
+ self.gl.framebuffer_texture_layer(
+ WebGl2RenderingContext::FRAMEBUFFER,
+ WebGl2RenderingContext::COLOR_ATTACHMENT0,
Some(&self.programs.resources.atlas_texture_array.texture),
+ 0,
+ atlas_id.as_u32() as i32,
);
+ // Set viewport to match the atlas texture dimensions
+ let atlas_size = &self.programs.resources.atlas_texture_array.size;
self.gl
- .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),
- )
- .unwrap();
+ .viewport(0, 0, atlas_size.width as i32, atlas_size.height as i32);
+
+ // Enable scissor test and set scissor rectangle to our region
+ self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
+ self.gl.scissor(
+ offset[0] as i32,
+ offset[1] as i32,
+ width as i32,
+ height as i32,
+ );
+
+ // Clear the region to transparent (0, 0, 0, 0)
+ self.gl.clear_color(0.0, 0.0, 0.0, 0.0);
+ self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
+
+ // Clean up temporary framebuffer
+ self.gl.delete_framebuffer(Some(&temp_framebuffer));
}
fn prepare_gpu_encoded_paints(&mut self, encoded_paints: &[EncodedPaint]) {
@@ -395,19 +402,19 @@
let transform = image_transform.as_coeffs().map(|x| x as f32);
let image_size = pack_image_size(image_resource.width, image_resource.height);
let image_offset = pack_image_offset(image_resource.offset[0], image_resource.offset[1]);
- let quality_and_extend_modes = pack_quality_and_extend_modes(
+ let image_params = pack_image_params(
+ image.quality as u32,
image.extends.0 as u32,
image.extends.1 as u32,
- image.quality as u32,
+ image_resource.atlas_id.as_u32(),
);
GpuEncodedPaint::Image(GpuEncodedImage {
- quality_and_extend_modes,
+ image_params,
image_size,
image_offset,
- atlas_index: image_resource.atlas_id.as_u32(),
transform,
- _padding: [0, 0],
+ _padding: [0, 0, 0],
})
}
@@ -704,76 +711,26 @@
) {
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
+ // Copy each layer from the old atlas to the new one
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,
+ copy_to_texture_array_layer(
+ gl,
+ |gl| {
+ // Attach source layer to READ framebuffer
+ gl.framebuffer_texture_layer(
+ WebGl2RenderingContext::READ_FRAMEBUFFER,
+ WebGl2RenderingContext::COLOR_ATTACHMENT0,
+ Some(&self.resources.atlas_texture_array.texture),
+ 0,
+ layer as i32,
+ );
+ },
+ &new_atlas_texture_array.texture,
+ layer,
+ [0, 0],
+ [width, height],
);
}
-
- // 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.
@@ -1094,6 +1051,177 @@
}
}
+/// RAII guard for WebGL state management.
+/// Automatically saves state on creation and restores it on drop.
+/// Only saves/restores the state specified in the configuration.
+struct WebGlStateGuard<'a> {
+ gl: &'a WebGl2RenderingContext,
+ config: WebGlStateConfig,
+ original_framebuffer: Option<web_sys::WebGlFramebuffer>,
+ original_read_framebuffer: Option<web_sys::WebGlFramebuffer>,
+ original_texture_2d_array: Option<web_sys::WebGlTexture>,
+ scissor_enabled: bool,
+ viewport: [i32; 4],
+}
+
+impl<'a> WebGlStateGuard<'a> {
+ /// Create a new state guard with custom configuration.
+ fn with_config(gl: &'a WebGl2RenderingContext, config: WebGlStateConfig) -> Self {
+ // Save current framebuffer binding if requested
+ let original_framebuffer = if config.framebuffer {
+ gl.get_parameter(WebGl2RenderingContext::FRAMEBUFFER_BINDING)
+ .ok()
+ .and_then(|v| v.dyn_into::<web_sys::WebGlFramebuffer>().ok())
+ } else {
+ None
+ };
+
+ // Save current read framebuffer binding if requested
+ let original_read_framebuffer = if config.read_framebuffer {
+ gl.get_parameter(WebGl2RenderingContext::READ_FRAMEBUFFER_BINDING)
+ .ok()
+ .and_then(|v| v.dyn_into::<web_sys::WebGlFramebuffer>().ok())
+ } else {
+ None
+ };
+
+ // Save current 2D array texture binding if requested
+ let original_texture_2d_array = if config.texture_2d_array {
+ gl.get_parameter(WebGl2RenderingContext::TEXTURE_BINDING_2D_ARRAY)
+ .ok()
+ .and_then(|v| v.dyn_into::<web_sys::WebGlTexture>().ok())
+ } else {
+ None
+ };
+
+ // Save current scissor test state if requested
+ let scissor_enabled = if config.scissor {
+ gl.get_parameter(WebGl2RenderingContext::SCISSOR_TEST)
+ .unwrap()
+ .as_bool()
+ .unwrap_or(false)
+ } else {
+ false
+ };
+
+ // Save current viewport if requested
+ let viewport = if config.viewport {
+ let viewport_js = gl
+ .get_parameter(WebGl2RenderingContext::VIEWPORT)
+ .unwrap()
+ .dyn_into::<js_sys::Int32Array>()
+ .unwrap();
+ let viewport_vec = viewport_js.to_vec();
+ [
+ viewport_vec[0],
+ viewport_vec[1],
+ viewport_vec[2],
+ viewport_vec[3],
+ ]
+ } else {
+ [0, 0, 0, 0]
+ };
+
+ Self {
+ gl,
+ config,
+ original_framebuffer,
+ original_read_framebuffer,
+ original_texture_2d_array,
+ scissor_enabled,
+ viewport,
+ }
+ }
+
+ /// Create a state guard for clearing an atlas region operations.
+ fn for_clear_atlas_region(gl: &'a WebGl2RenderingContext) -> Self {
+ Self::with_config(
+ gl,
+ WebGlStateConfig {
+ framebuffer: true,
+ scissor: true,
+ viewport: true,
+ ..Default::default()
+ },
+ )
+ }
+
+ /// Create a state guard for texture copying operations.
+ fn for_texture_copy(gl: &'a WebGl2RenderingContext) -> Self {
+ Self::with_config(
+ gl,
+ WebGlStateConfig {
+ read_framebuffer: true,
+ texture_2d_array: true,
+ ..Default::default()
+ },
+ )
+ }
+}
+
+impl Drop for WebGlStateGuard<'_> {
+ /// Restore WebGL state when the guard goes out of scope.
+ /// Only restores state that was configured to be saved.
+ fn drop(&mut self) {
+ // Restore scissor test state if it was saved
+ if self.config.scissor {
+ if self.scissor_enabled {
+ self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
+ } else {
+ self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
+ }
+ }
+
+ // Restore viewport if it was saved
+ if self.config.viewport {
+ self.gl.viewport(
+ self.viewport[0],
+ self.viewport[1],
+ self.viewport[2],
+ self.viewport[3],
+ );
+ }
+
+ // Restore original framebuffer if it was saved
+ if self.config.framebuffer {
+ self.gl.bind_framebuffer(
+ WebGl2RenderingContext::FRAMEBUFFER,
+ self.original_framebuffer.as_ref(),
+ );
+ }
+
+ // Restore original read framebuffer if it was saved
+ if self.config.read_framebuffer {
+ self.gl.bind_framebuffer(
+ WebGl2RenderingContext::READ_FRAMEBUFFER,
+ self.original_read_framebuffer.as_ref(),
+ );
+ }
+
+ // Restore original 2D array texture binding if it was saved
+ if self.config.texture_2d_array {
+ self.gl.bind_texture(
+ WebGl2RenderingContext::TEXTURE_2D_ARRAY,
+ self.original_texture_2d_array.as_ref(),
+ );
+ }
+ }
+}
+/// Configuration for which WebGL state to save/restore.
+#[derive(Debug, Default)]
+struct WebGlStateConfig {
+ /// Save/restore framebuffer binding (`FRAMEBUFFER_BINDING`)
+ framebuffer: bool,
+ /// Save/restore read framebuffer binding (`READ_FRAMEBUFFER_BINDING`)
+ read_framebuffer: bool,
+ /// Save/restore 2D array texture binding (`TEXTURE_BINDING_2D_ARRAY`)
+ texture_2d_array: bool,
+ /// Save/restore scissor test state
+ scissor: bool,
+ /// Save/restore viewport
+ viewport: bool,
+}
+
/// Create a WebGL shader program from vertex and fragment sources.
fn create_shader_program(
gl: &WebGl2RenderingContext,
@@ -1917,73 +2045,23 @@
width: u32,
height: u32,
) {
- // 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());
-
- // Attach source texture to read framebuffer
- gl.bind_framebuffer(
- WebGl2RenderingContext::READ_FRAMEBUFFER,
- Some(&read_framebuffer),
+ copy_to_texture_array_layer(
+ gl,
+ |gl| {
+ // Attach source texture to read framebuffer
+ gl.framebuffer_texture_2d(
+ WebGl2RenderingContext::READ_FRAMEBUFFER,
+ WebGl2RenderingContext::COLOR_ATTACHMENT0,
+ WebGl2RenderingContext::TEXTURE_2D,
+ Some(self),
+ 0,
+ );
+ },
+ atlas_texture_array,
+ layer,
+ offset,
+ [width, height],
);
- gl.framebuffer_texture_2d(
- WebGl2RenderingContext::READ_FRAMEBUFFER,
- WebGl2RenderingContext::COLOR_ATTACHMENT0,
- WebGl2RenderingContext::TEXTURE_2D,
- Some(self),
- 0,
- );
-
- // 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,
- );
-
- // 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.delete_framebuffer(Some(&read_framebuffer));
- gl.delete_framebuffer(Some(&draw_framebuffer));
}
}
@@ -2059,3 +2137,48 @@
/// The number of layers in the texture array.
depth_or_array_layers: u32,
}
+
+/// Helper function to copy from a source texture/framebuffer to a destination texture array layer.
+fn copy_to_texture_array_layer(
+ gl: &WebGl2RenderingContext,
+ source_setup: impl FnOnce(&WebGl2RenderingContext),
+ dest_texture_array: &web_sys::WebGlTexture,
+ dest_layer: u32,
+ dest_offset: [u32; 2],
+ copy_size: [u32; 2],
+) {
+ let _state_guard = WebGlStateGuard::for_texture_copy(gl);
+ let read_framebuffer = gl.create_framebuffer().unwrap();
+
+ // Bind destination texture array
+ gl.active_texture(WebGl2RenderingContext::TEXTURE0);
+ gl.bind_texture(
+ WebGl2RenderingContext::TEXTURE_2D_ARRAY,
+ Some(dest_texture_array),
+ );
+
+ // Bind the READ framebuffer
+ gl.bind_framebuffer(
+ WebGl2RenderingContext::READ_FRAMEBUFFER,
+ Some(&read_framebuffer),
+ );
+
+ // Let the caller set up the source (attach texture/layer to read framebuffer)
+ source_setup(gl);
+
+ // Copy from READ framebuffer to destination array layer
+ gl.copy_tex_sub_image_3d(
+ WebGl2RenderingContext::TEXTURE_2D_ARRAY,
+ 0,
+ dest_offset[0] as i32,
+ dest_offset[1] as i32,
+ dest_layer as i32,
+ 0,
+ 0,
+ copy_size[0] as i32,
+ copy_size[1] as i32,
+ );
+
+ // Clean up
+ gl.delete_framebuffer(Some(&read_framebuffer));
+}
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs
index a4439c1..225f2e1 100644
--- a/sparse_strips/vello_hybrid/src/render/wgpu.rs
+++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs
@@ -23,7 +23,8 @@
use core::{fmt::Debug, mem, num::NonZeroU64};
use wgpu::Extent3d;
-use crate::{AtlasConfig, AtlasId};
+use crate::AtlasConfig;
+use crate::multi_atlas::AtlasId;
use crate::{
GpuStrip, RenderError, RenderSettings, RenderSize,
gradient_cache::GradientRampCache,
@@ -34,8 +35,8 @@
GPU_ENCODED_IMAGE_SIZE_TEXELS, GPU_LINEAR_GRADIENT_SIZE_TEXELS,
GPU_RADIAL_GRADIENT_SIZE_TEXELS, GPU_SWEEP_GRADIENT_SIZE_TEXELS, GpuEncodedImage,
GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient, GpuSweepGradient,
- pack_image_offset, pack_image_size, pack_quality_and_extend_modes,
- pack_radial_kind_and_swapped, pack_texture_width_and_extend_mode,
+ pack_image_offset, pack_image_params, pack_image_size, pack_radial_kind_and_swapped,
+ pack_texture_width_and_extend_mode,
},
},
scene::Scene,
@@ -190,7 +191,7 @@
Programs::maybe_resize_atlas_texture_array(
device,
- queue,
+ encoder,
&mut self.programs.resources,
&self.programs.atlas_bind_group_layout,
self.image_cache.atlas_count() as u32,
@@ -237,42 +238,58 @@
}
}
- /// Clear a specific region of the atlas texture with uninitialized data.
+ /// Clear a specific region of the atlas texture.
fn clear_atlas_region(
&mut self,
_device: &Device,
- queue: &Queue,
- _encoder: &mut CommandEncoder,
+ _queue: &Queue,
+ encoder: &mut CommandEncoder,
atlas_id: 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];
- queue.write_texture(
- wgpu::TexelCopyTextureInfo {
- texture: &self.programs.resources.atlas_texture_array,
- mip_level: 0,
- origin: wgpu::Origin3d {
- x: offset[0],
- y: offset[1],
- z: atlas_id.as_u32(),
+ // Create a texture view for the specific atlas layer
+ let layer_view =
+ self.programs
+ .resources
+ .atlas_texture_array
+ .create_view(&wgpu::TextureViewDescriptor {
+ label: Some("Atlas Layer Clear View"),
+ format: Some(wgpu::TextureFormat::Rgba8Unorm),
+ dimension: Some(wgpu::TextureViewDimension::D2),
+ aspect: wgpu::TextureAspect::All,
+ base_mip_level: 0,
+ mip_level_count: Some(1),
+ base_array_layer: atlas_id.as_u32(),
+ array_layer_count: Some(1),
+ // Inherit usage from the texture
+ usage: None,
+ });
+
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("Clear Atlas Region"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &layer_view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ // Don't clear entire texture, just the scissor region
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
},
- aspect: wgpu::TextureAspect::All,
- },
- &uninitialized_data,
- wgpu::TexelCopyBufferLayout {
- offset: 0,
- bytes_per_row: Some(4 * width),
- rows_per_image: Some(height),
- },
- wgpu::Extent3d {
- width,
- height,
- depth_or_array_layers: 1,
- },
- );
+ depth_slice: None,
+ })],
+ depth_stencil_attachment: None,
+ occlusion_query_set: None,
+ timestamp_writes: None,
+ });
+
+ // Set scissor rectangle to limit clearing to specific region
+ render_pass.set_scissor_rect(offset[0], offset[1], width, height);
+ // Use atlas clear pipeline to render transparent pixels
+ render_pass.set_pipeline(&self.programs.atlas_clear_pipeline);
+ // Draw fullscreen quad
+ render_pass.draw(0..4, 0..1);
}
fn prepare_gpu_encoded_paints(&mut self, encoded_paints: &[EncodedPaint]) {
@@ -328,19 +345,19 @@
let transform = image_transform.as_coeffs().map(|x| x as f32);
let image_size = pack_image_size(image_resource.width, image_resource.height);
let image_offset = pack_image_offset(image_resource.offset[0], image_resource.offset[1]);
- let quality_and_extend_modes = pack_quality_and_extend_modes(
+ let image_params = pack_image_params(
+ image.quality as u32,
image.extends.0 as u32,
image.extends.1 as u32,
- image.quality as u32,
+ image_resource.atlas_id.as_u32(),
);
GpuEncodedPaint::Image(GpuEncodedImage {
- quality_and_extend_modes,
+ image_params,
image_size,
image_offset,
- atlas_index: image_resource.atlas_id.as_u32(),
transform,
- _padding: [0, 0],
+ _padding: [0, 0, 0],
})
}
@@ -432,6 +449,8 @@
atlas_bind_group_layout: BindGroupLayout,
/// Pipeline for clearing slots in slot textures.
clear_pipeline: RenderPipeline,
+ /// Pipeline for clearing atlas regions.
+ atlas_clear_pipeline: RenderPipeline,
/// GPU resources for rendering (created during prepare)
resources: GpuResources,
/// Dimensions of the rendering target
@@ -714,6 +733,43 @@
cache: None,
});
+ // Create atlas clear pipeline
+ let atlas_clear_pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Atlas Clear Pipeline Layout"),
+ bind_group_layouts: &[],
+ push_constant_ranges: &[],
+ });
+ let atlas_clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Atlas Clear Pipeline"),
+ layout: Some(&atlas_clear_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &clear_shader,
+ // Use a different vertex shader entry point
+ entry_point: Some("vs_main_fullscreen"),
+ buffers: &[],
+ compilation_options: PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &clear_shader,
+ entry_point: Some("fs_main"),
+ targets: &[Some(ColorTargetState {
+ format: wgpu::TextureFormat::Rgba8Unorm,
+ blend: None,
+ write_mask: ColorWrites::ALL,
+ })],
+ compilation_options: PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleStrip,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview: None,
+ cache: None,
+ });
+
let slot_texture_views: [TextureView; 2] = core::array::from_fn(|_| {
device
.create_texture(&wgpu::TextureDescriptor {
@@ -872,6 +928,7 @@
height: render_target_config.height,
},
clear_pipeline,
+ atlas_clear_pipeline,
}
}
@@ -947,7 +1004,8 @@
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
- | wgpu::TextureUsages::COPY_SRC,
+ | wgpu::TextureUsages::COPY_SRC
+ | wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
@@ -1290,7 +1348,7 @@
/// Resize the texture array to accommodate more atlases.
fn maybe_resize_atlas_texture_array(
device: &Device,
- queue: &Queue,
+ encoder: &mut CommandEncoder,
resources: &mut GpuResources,
atlas_bind_group_layout: &BindGroupLayout,
required_atlas_count: u32,
@@ -1307,8 +1365,7 @@
// Copy existing atlas data from old texture array to new one
Self::copy_atlas_texture_data(
- device,
- queue,
+ encoder,
&resources.atlas_texture_array,
&new_atlas_texture_array,
current_atlas_count,
@@ -1333,50 +1390,33 @@
/// 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,
+ encoder: &mut CommandEncoder,
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()]);
+ // Copy all layers from old texture array to new texture array
+ encoder.copy_texture_to_texture(
+ wgpu::TexelCopyTextureInfo {
+ texture: old_atlas_texture_array,
+ mip_level: 0,
+ origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
+ aspect: wgpu::TextureAspect::All,
+ },
+ wgpu::TexelCopyTextureInfo {
+ texture: new_atlas_texture_array,
+ mip_level: 0,
+ origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
+ aspect: wgpu::TextureAspect::All,
+ },
+ wgpu::Extent3d {
+ width,
+ height,
+ depth_or_array_layers: layer_count_to_copy,
+ },
+ );
}
/// Upload alpha data to the texture.
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs
index b30b17a..40234d8 100644
--- a/sparse_strips/vello_hybrid/src/scene.rs
+++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -29,7 +29,17 @@
pub struct RenderSettings {
/// The SIMD level that should be used for rendering operations.
pub level: Level,
- /// The configuration for the atlas.
+ /// The configuration for the texture atlas.
+ ///
+ /// This controls how images are managed in GPU memory through texture atlases.
+ /// The atlas system packs multiple images into larger textures to reduce the
+ /// number of GPU texture bindings. This config allows customizing atlas parameters such as:
+ /// - The number and size of atlases
+ /// - How images are allocated across multiple atlases
+ /// - Whether new atlases are automatically created when needed
+ ///
+ /// Adjusting these settings can affect memory usage and rendering performance
+ /// depending on your application's image usage patterns.
pub atlas_config: AtlasConfig,
}
diff --git a/sparse_strips/vello_sparse_shaders/shaders/clear_slots.wgsl b/sparse_strips/vello_sparse_shaders/shaders/clear_slots.wgsl
index 681a67d..84b53e4 100644
--- a/sparse_strips/vello_sparse_shaders/shaders/clear_slots.wgsl
+++ b/sparse_strips/vello_sparse_shaders/shaders/clear_slots.wgsl
@@ -1,7 +1,7 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
-// This shader clears specific slots in slot textures to transparent pixels.
+// This vertex shader clears specific slots in slot textures to transparent pixels.
// Assumes this texture consists of a single column of slots of `config.slot_height`,
// numbering from 0 to `texture_height / slot_height - 1` from top to bottom.
@@ -44,6 +44,27 @@
return vec4<f32>(ndc_x, ndc_y, 0.0, 1.0);
}
+// This vertex shader is used for clearing atlas regions.
+@vertex
+fn vs_main_fullscreen(
+ @builtin(vertex_index) vertex_index: u32,
+) -> @builtin(position) vec4<f32> {
+ // This generates a quad that covers the entire render target (hence "fullscreen"),
+ // but the actual clearing region is controlled by the scissor test set on the render pass.
+ // This approach is more efficient than generating region-specific geometry because:
+ // 1. No vertex buffer needed - geometry generated from vertex index alone
+ // 2. Same simple shader works for any region size
+ // 3. Hardware scissor test efficiently clips to the desired region
+ // 4. GPU rasterizer + scissor test is faster than complex vertex calculations
+ //
+ // Map vertex_index (0-3) to fullscreen quad corners in NDC:
+ // 0 → (-1,-1), 1 → (1,-1), 2 → (-1,1), 3 → (1,1)
+ let x = f32((vertex_index & 1u) * 2u) - 1.0; // 0->-1, 1->1
+ let y = f32((vertex_index & 2u)) - 1.0; // 0->-1, 2->1
+
+ return vec4<f32>(x, y, 0.0, 1.0);
+}
+
@fragment
fn fs_main(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
// Clear with transparent pixels
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
index 2452142..0a0a132 100644
--- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
+++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -765,16 +765,16 @@
let quality = texel0.x & 0x3u;
let extend_x = (texel0.x >> 2u) & 0x3u;
let extend_y = (texel0.x >> 4u) & 0x3u;
+ let atlas_index = (texel0.x >> 6u) & 0xFFu;
// Unpack image_size from texel0.y (stored as u32, unpack to width/height)
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>(texel1.x), bitcast<f32>(texel1.y),
- bitcast<f32>(texel1.z), bitcast<f32>(texel1.w)
+ bitcast<f32>(texel0.w), bitcast<f32>(texel1.x),
+ bitcast<f32>(texel1.y), bitcast<f32>(texel1.z)
);
- let translate = vec2<f32>(bitcast<f32>(texel2.x), bitcast<f32>(texel2.y));
+ let translate = vec2<f32>(bitcast<f32>(texel1.w), bitcast<f32>(texel2.x));
return EncodedImage(
quality,