.
diff --git a/sparse_strips/vello_common/src/multi_atlas.rs b/sparse_strips/vello_common/src/multi_atlas.rs
index 5dd493a..c5ec0c1 100644
--- a/sparse_strips/vello_common/src/multi_atlas.rs
+++ b/sparse_strips/vello_common/src/multi_atlas.rs
@@ -196,19 +196,10 @@
         }
     }
 
-    fn space_diagnostics(
-        &self,
-        width: u32,
-        height: u32,
-        exclude_atlas_id: Option<AtlasId>,
-    ) -> AtlasSpaceDiagnostics {
+    fn space_diagnostics(&self, width: u32, height: u32) -> AtlasSpaceDiagnostics {
         let mut atlases = Vec::new();
 
         for atlas in &self.atlases {
-            if Some(atlas.id) == exclude_atlas_id {
-                continue;
-            }
-
             let mut free_area = 0_u64;
             let mut free_rectangle_count = 0;
             let mut largest_free_width = 0;
@@ -248,24 +239,14 @@
         }
     }
 
-    fn no_space_available(
-        &self,
-        width: u32,
-        height: u32,
-        exclude_atlas_id: Option<AtlasId>,
-    ) -> AtlasError {
-        AtlasError::NoSpaceAvailable(self.space_diagnostics(width, height, exclude_atlas_id))
+    fn no_space_available(&self, width: u32, height: u32) -> AtlasError {
+        AtlasError::NoSpaceAvailable(self.space_diagnostics(width, height))
     }
 
-    fn atlas_limit_reached(
-        &self,
-        width: u32,
-        height: u32,
-        exclude_atlas_id: Option<AtlasId>,
-    ) -> AtlasError {
+    fn atlas_limit_reached(&self, width: u32, height: u32) -> AtlasError {
         AtlasError::AtlasLimitReached {
             max_atlases: self.config.max_atlases,
-            diagnostics: self.space_diagnostics(width, height, exclude_atlas_id),
+            diagnostics: self.space_diagnostics(width, height),
         }
     }
 
@@ -291,10 +272,9 @@
 
         // Try creating a new atlas if auto-grow is enabled
         if self.config.auto_grow {
-            if self.atlases.len() >= self.config.max_atlases {
-                return Err(self.atlas_limit_reached(width, height, exclude_atlas_id));
-            }
-            let atlas_id = self.create_atlas()?;
+            let atlas_id = self
+                .create_atlas()
+                .map_err(|_| self.atlas_limit_reached(width, height))?;
             let atlas = self.atlases.last_mut().unwrap();
             if let Some(allocation) = atlas.allocate(width, height) {
                 return Ok(AtlasAllocation {
@@ -304,7 +284,7 @@
             }
         }
 
-        Err(self.no_space_available(width, height, exclude_atlas_id))
+        Err(self.no_space_available(width, height))
     }
 
     /// Allocate using best-fit strategy: choose the atlas with the smallest remaining space that
@@ -417,10 +397,9 @@
 
         // Try creating a new atlas if auto-grow is enabled
         if self.config.auto_grow {
-            if self.atlases.len() >= self.config.max_atlases {
-                return Err(self.atlas_limit_reached(width, height, exclude_atlas_id));
-            }
-            let atlas_id = self.create_atlas()?;
+            let atlas_id = self
+                .create_atlas()
+                .map_err(|_| self.atlas_limit_reached(width, height))?;
             let atlas = self.atlases.last_mut().unwrap();
             if let Some(allocation) = atlas.allocate(width, height) {
                 self.round_robin_counter = self.atlases.len() - 1;
@@ -431,7 +410,7 @@
             }
         }
 
-        Err(self.no_space_available(width, height, exclude_atlas_id))
+        Err(self.no_space_available(width, height))
     }
 
     /// Deallocate space in the specified atlas.
diff --git a/sparse_strips/vello_hybrid/src/lib.rs b/sparse_strips/vello_hybrid/src/lib.rs
index 1483f72..d92463a 100644
--- a/sparse_strips/vello_hybrid/src/lib.rs
+++ b/sparse_strips/vello_hybrid/src/lib.rs
@@ -113,17 +113,26 @@
 /// Errors that can occur during rendering.
 #[derive(Error, Debug, Clone)]
 pub enum RenderError {
-    /// An atlas allocation failed.
+    /// An image atlas allocation failed.
     #[error("Atlas allocation failed: {0}")]
     AtlasError(#[from] vello_common::multi_atlas::AtlasError),
     /// A draw referenced a [`TextureId`] that was not provided at render time.
     #[error("Missing texture binding for {0:?}")]
     MissingTextureBinding(TextureId),
+    /// An intermediate texture allocation failed.
+    #[error(transparent)]
+    IntermediateTexture(#[from] IntermediateTextureError),
+    // TODO: Consider expanding `RenderError` to replace some `.unwrap` and `.expect`.
+}
+
+/// Errors that can occur while allocating intermediate textures.
+#[derive(Error, Debug, Clone)]
+pub enum IntermediateTextureError {
     /// An intermediate texture allocation exceeds the configured texture dimensions.
     #[error(
         "Intermediate texture allocation {width}x{height} exceeds maximum {max_width}x{max_height}"
     )]
-    IntermediateTextureTooLarge {
+    TooLarge {
         /// The requested allocation width.
         width: u32,
         /// The requested allocation height.
@@ -137,13 +146,12 @@
     #[error(
         "Render requires {required} intermediate textures, exceeding the configured maximum of {max}"
     )]
-    IntermediateTextureLimitReached {
+    LimitReached {
         /// The number of intermediate textures required by the render.
         required: usize,
         /// The configured maximum number of intermediate textures.
         max: usize,
     },
-    // TODO: Consider expanding `RenderError` to replace some `.unwrap` and `.expect`.
 }
 
 #[cfg(test)]
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs
index bc33d4a..a5c84cd 100644
--- a/sparse_strips/vello_hybrid/src/render/common.rs
+++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -8,6 +8,7 @@
     reason = "GPU paint structures have small, fixed sizes that fit in u32"
 )]
 
+use crate::IntermediateTextureError;
 use crate::blend::GpuBlendInstance;
 use crate::copy::GpuCopyInstance;
 use crate::filter::FILTER_ATLAS_PADDING;
@@ -15,7 +16,6 @@
 use alloc::vec::Vec;
 use bytemuck::{Pod, Zeroable};
 use vello_common::geometry::{SizeU16, SizeU32};
-use vello_common::multi_atlas::AtlasError;
 use vello_common::record::CommandRecorder;
 
 // GPU paint structure sizes in texels (1 texel = 16 bytes for RGBA32Uint texture format).
@@ -130,7 +130,7 @@
     pub(crate) fn required_intermediate_texture_size(
         self,
         recorder: &CommandRecorder<RecordedDraw>,
-    ) -> Result<SizeU16, AtlasError> {
+    ) -> Result<SizeU16, IntermediateTextureError> {
         let min_size = self.min_texture_size;
         let max_size = self.max_texture_size;
 
@@ -138,7 +138,7 @@
             if size.width() > u32::from(max_size.width())
                 || size.height() > u32::from(max_size.height())
             {
-                return Err(AtlasError::TextureTooLarge {
+                return Err(IntermediateTextureError::TooLarge {
                     width: size.width(),
                     height: size.height(),
                     max_width: u32::from(max_size.width()),
@@ -174,8 +174,8 @@
 mod tests {
     use super::DeviceLimits;
     use crate::scene::RecordedDraw;
-    use crate::{LayersConfig, MemorySettings, SizeU16};
-    use vello_common::multi_atlas::{AtlasConfig, AtlasError};
+    use crate::{IntermediateTextureError, LayersConfig, MemorySettings, SizeU16};
+    use vello_common::multi_atlas::AtlasConfig;
     use vello_common::record::CommandRecorder;
 
     fn device_limits(max_texture_dimension_2d: u32, max_texture_array_layers: u32) -> DeviceLimits {
@@ -297,7 +297,7 @@
 
         assert!(matches!(
             config.required_intermediate_texture_size(&recorder),
-            Err(AtlasError::TextureTooLarge {
+            Err(IntermediateTextureError::TooLarge {
                 width: 513,
                 height: 10,
                 max_width: 512,
diff --git a/sparse_strips/vello_hybrid/src/schedule/allocate.rs b/sparse_strips/vello_hybrid/src/schedule/allocate.rs
index d5bbd69..da4ebd2 100644
--- a/sparse_strips/vello_hybrid/src/schedule/allocate.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/allocate.rs
@@ -134,7 +134,7 @@
     }
 
     pub(super) fn allocation_size(self) -> SizeU32 {
-        SizeU32::from(self.region.size) + u32::from(self.region.padding) * 2
+        self.region.allocation_size()
     }
 }
 
@@ -147,6 +147,13 @@
     padding: u16,
 }
 
+impl RegionProps {
+    /// Size of the atlas allocation needed to hold the region and its padding.
+    fn allocation_size(self) -> SizeU32 {
+        SizeU32::from(self.size) + u32::from(self.padding) * 2
+    }
+}
+
 /// Texture region and allocator metadata needed to release it.
 #[derive(Debug, Clone, Copy)]
 pub(super) struct AllocatedTextureRegion {
@@ -201,7 +208,7 @@
         let padding = u32::from(props.padding);
         let width = props.size.width();
         let height = props.size.height();
-        let allocation_size = SizeU32::from(props.size) + padding * 2;
+        let allocation_size = props.allocation_size();
         let allocation = self.allocate(allocation_size.width(), allocation_size.height())?;
         let x = u16::try_from(allocation.x + padding).unwrap();
         let y = u16::try_from(allocation.y + padding).unwrap();
diff --git a/sparse_strips/vello_hybrid/src/schedule/cursor.rs b/sparse_strips/vello_hybrid/src/schedule/cursor.rs
index e2df219..f1a78e5 100644
--- a/sparse_strips/vello_hybrid/src/schedule/cursor.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/cursor.rs
@@ -8,10 +8,10 @@
 //! preferring to advance the base round count in case a requested allocation doesn't fit
 //! into the current round, and only resorting to adding more textures as a last resort.
 
-use crate::RenderError;
 use crate::schedule::allocate::{
     AllocatedTextureRegion, Allocation, Atlases, LayerAllocationRequest,
 };
+use crate::{IntermediateTextureError, RenderError};
 use alloc::vec::Vec;
 
 /// The round cursor.
@@ -75,7 +75,7 @@
             .allocate_layer(&request)
             // If we successfully added a new texture but allocation still fails, it means the layer
             // itself is larger than the maximum texture size, so it cannot possibly fit.
-            .ok_or(RenderError::IntermediateTextureTooLarge {
+            .ok_or(IntermediateTextureError::TooLarge {
                 width: requested_size.width(),
                 height: requested_size.height(),
                 max_width: u32::from(texture_size.width()),
@@ -139,9 +139,9 @@
 #[cfg(test)]
 mod tests {
     use super::Cursor;
-    use crate::RenderError;
     use crate::schedule::allocate::{Atlases, LayerAllocationRequest};
     use crate::target::{LayerTextureId, TextureParity};
+    use crate::{IntermediateTextureError, RenderError};
     use vello_common::geometry::{RectU16, SizeU16};
     use vello_common::record::RecordedLayerKind;
 
@@ -212,12 +212,14 @@
 
         assert!(matches!(
             cursor.allocate_layer(request(TextureParity::Even, SizeU16::from_wh(9, 8),)),
-            Err(RenderError::IntermediateTextureTooLarge {
-                width: 9,
-                height: 8,
-                max_width: 8,
-                max_height: 8,
-            })
+            Err(RenderError::IntermediateTexture(
+                IntermediateTextureError::TooLarge {
+                    width: 9,
+                    height: 8,
+                    max_width: 8,
+                    max_height: 8,
+                }
+            ))
         ));
     }
 
diff --git a/sparse_strips/vello_hybrid/src/schedule/mod.rs b/sparse_strips/vello_hybrid/src/schedule/mod.rs
index 5991278..119a298 100644
--- a/sparse_strips/vello_hybrid/src/schedule/mod.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/mod.rs
@@ -141,7 +141,7 @@
 use crate::target::{
     DrawTarget, LayerTextureRegion, RootTarget, RoundBindings, TextureParity, TextureRegion,
 };
-use crate::{RenderError, Scene, blend::BlendStrip};
+use crate::{IntermediateTextureError, RenderError, Scene, blend::BlendStrip};
 use alloc::vec::Vec;
 use vello_common::filter::FilterLayerPlacement;
 use vello_common::geometry::{RectU16, SizeU16};
@@ -250,13 +250,13 @@
         self,
         existing: IntermediateTextureAllocations,
         max_textures: Option<usize>,
-    ) -> Result<(), RenderError> {
+    ) -> Result<(), IntermediateTextureError> {
         let retained_textures = self.allocations.combine(existing).texture_count();
 
         if let Some(max) = max_textures
             && retained_textures > max
         {
-            return Err(RenderError::IntermediateTextureLimitReached {
+            return Err(IntermediateTextureError::LimitReached {
                 required: retained_textures,
                 max,
             });
diff --git a/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs b/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs
index 93887dc..fc085c6 100644
--- a/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs
@@ -3,9 +3,9 @@
 
 use super::test_support::{SceneCase, ScheduledCase};
 use super::{IntermediateTextureAllocations, IntermediateTextureRequirements, ScheduleStorage};
-use crate::RenderError;
 use crate::filter::FILTER_ATLAS_PADDING;
 use crate::target::{RootTarget, TextureParity};
+use crate::{IntermediateTextureError, RenderError};
 use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive};
 use vello_common::geometry::SizeU16;
 use vello_common::kurbo::Rect;
@@ -36,28 +36,28 @@
     assert!(base.validate(allocations([0, 3], false), Some(4)).is_err());
     assert!(matches!(
         base.validate(allocations([2, 0], false), Some(3)),
-        Err(RenderError::IntermediateTextureLimitReached {
+        Err(IntermediateTextureError::LimitReached {
             required: 4,
             max: 3,
         })
     ));
     assert!(matches!(
         base.validate(allocations([1, 0], true), Some(2)),
-        Err(RenderError::IntermediateTextureLimitReached {
+        Err(IntermediateTextureError::LimitReached {
             required: 3,
             max: 2,
         })
     ));
     assert!(matches!(
         base.validate(allocations([0, 2], false), Some(3)),
-        Err(RenderError::IntermediateTextureLimitReached {
+        Err(IntermediateTextureError::LimitReached {
             required: 4,
             max: 3,
         })
     ));
     assert!(matches!(
         base.validate(allocations([2, 2], false), Some(4)),
-        Err(RenderError::IntermediateTextureLimitReached {
+        Err(IntermediateTextureError::LimitReached {
             required: 5,
             max: 4,
         })
@@ -370,10 +370,12 @@
     );
     assert!(matches!(
         case.schedule(RootTarget::UserSurface, SizeU16::new(16), 2,),
-        Err(RenderError::IntermediateTextureLimitReached {
-            required: 3,
-            max: 2,
-        })
+        Err(RenderError::IntermediateTexture(
+            IntermediateTextureError::LimitReached {
+                required: 3,
+                max: 2,
+            }
+        ))
     ));
 }