.
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,