feat(vello_hybrid): Add clipping support for image rendering
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/image.rs b/sparse_strips/vello_hybrid/examples/scenes/src/image.rs
index 0e7ca3f..038f6a7 100644
--- a/sparse_strips/vello_hybrid/examples/scenes/src/image.rs
+++ b/sparse_strips/vello_hybrid/examples/scenes/src/image.rs
@@ -5,7 +5,7 @@
 use std::f64::consts::PI;
 
 use vello_common::color::PremulRgba8;
-use vello_common::kurbo::BezPath;
+use vello_common::kurbo::{BezPath, Point, Shape, Vec2};
 use vello_common::peniko::ImageFormat;
 use vello_common::pixmap::Pixmap;
 use vello_common::{
@@ -69,23 +69,21 @@
             y_extend: Extend::Pad,
             quality: ImageQuality::Low,
         });
-        let path = heart_shape();
-        scene.fill_path(&path);
+        scene.fill_path(&heart_shape());
 
-        scene.set_transform(
-            root_transform
-                * Affine::translate((700.0, 500.0))
-                * Affine::rotate(PI / 4.0)
-                * Affine::scale(0.5),
+        scene.set_transform(root_transform * Affine::translate((400.0, 480.0)));
+        scene.push_clip_layer(
+            &circular_star(Point::new(300.0, 220.0), 5, 100.0, 150.0).to_path(0.1),
         );
-        scene.set_paint_transform(Affine::scale(0.25));
+        scene.set_paint_transform(Affine::IDENTITY);
         scene.set_paint(Image {
-            source: ImageSource::OpaqueId(ImageId::new(0)),
+            source: ImageSource::OpaqueId(splash_flower_id),
             x_extend: Extend::Repeat,
             y_extend: Extend::Repeat,
             quality: ImageQuality::Low,
         });
         scene.fill_rect(&Rect::new(0.0, 0.0, 640.0, 480.0));
+        scene.pop_layer();
 
         scene.set_transform(root_transform * Affine::translate((1000.0, 50.0)));
         scene.set_paint_transform(Affine::scale(0.25));
@@ -126,6 +124,21 @@
             quality: ImageQuality::High,
         });
         scene.fill_rect(&Rect::new(0.0, 0.0, 800.0, 160.0));
+
+        scene.set_transform(
+            root_transform
+                * Affine::translate((200.0, 1200.0))
+                * Affine::rotate(PI / 4.0)
+                * Affine::scale(0.5),
+        );
+        scene.set_paint_transform(Affine::scale(0.25));
+        scene.set_paint(Image {
+            source: ImageSource::OpaqueId(ImageId::new(0)),
+            x_extend: Extend::Repeat,
+            y_extend: Extend::Repeat,
+            quality: ImageQuality::Low,
+        });
+        scene.fill_rect(&Rect::new(0.0, 0.0, 640.0, 480.0));
     }
 }
 
@@ -208,3 +221,16 @@
     path.close_path();
     path
 }
+
+fn circular_star(center: Point, n: usize, inner: f64, outer: f64) -> BezPath {
+    let mut path = BezPath::new();
+    let start_angle = -std::f64::consts::FRAC_PI_2;
+    path.move_to(center + outer * Vec2::from_angle(start_angle));
+    for i in 1..n * 2 {
+        let th = start_angle + i as f64 * std::f64::consts::PI / n as f64;
+        let r = if i % 2 == 0 { outer } else { inner };
+        path.line_to(center + r * Vec2::from_angle(th));
+    }
+    path.close_path();
+    path
+}
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs
index 0feda7b..bfffcf5 100644
--- a/sparse_strips/vello_hybrid/src/render/common.rs
+++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -46,7 +46,7 @@
     /// There are [`Config::strip_height`] alpha values per column.
     pub col_idx: u32,
     /// Color value or slot index when alpha is 0
-    pub rgba_or_slot: u32,
+    pub payload: u32,
     /// Packed paint type (2 bits) and paint texture id (30 bits)
     /// Paint type: 0 = solid, 1 = alpha, 2 = image
     /// Paint texture id locates the encoded image data `EncodedImage`
diff --git a/sparse_strips/vello_hybrid/src/schedule.rs b/sparse_strips/vello_hybrid/src/schedule.rs
index a5fedde..ddb9097 100644
--- a/sparse_strips/vello_hybrid/src/schedule.rs
+++ b/sparse_strips/vello_hybrid/src/schedule.rs
@@ -389,7 +389,7 @@
                     width: WideTile::WIDTH,
                     dense_width: 0,
                     col_idx: 0,
-                    rgba_or_slot: bg,
+                    payload: bg,
                     paint: 0,
                 });
             }
@@ -402,10 +402,12 @@
                     let el = state.stack.last().unwrap();
                     let draw = self.draw_mut(el.round, clip_depth);
 
-                    let (col_idx, rgba_or_slot, paint) = Self::process_paint(&fill.paint, scene, 0);
+                    let (scene_strip_x, scene_strip_y) = (wide_tile_x + fill.x, wide_tile_y);
+                    let (payload, paint) =
+                        Self::process_paint(&fill.paint, scene, (scene_strip_x, scene_strip_y));
 
                     let (x, y) = if clip_depth == 1 {
-                        (wide_tile_x + fill.x, wide_tile_y)
+                        (scene_strip_x, scene_strip_y)
                     } else {
                         (fill.x, el.slot_ix as u16 * Tile::HEIGHT)
                     };
@@ -415,8 +417,8 @@
                         y,
                         width: fill.width,
                         dense_width: 0,
-                        col_idx,
-                        rgba_or_slot,
+                        col_idx: 0,
+                        payload,
                         paint,
                     });
                 }
@@ -424,15 +426,19 @@
                     let el = state.stack.last().unwrap();
                     let draw = self.draw_mut(el.round, clip_depth);
 
-                    let alpha_col = (alpha_fill.alpha_idx / usize::from(Tile::HEIGHT))
+                    let col_idx = (alpha_fill.alpha_idx / usize::from(Tile::HEIGHT))
                         .try_into()
                         .expect("Sparse strips are bound to u32 range");
 
-                    let (col_idx, rgba_or_slot, paint) =
-                        Self::process_paint(&alpha_fill.paint, scene, alpha_col);
+                    let (scene_strip_x, scene_strip_y) = (wide_tile_x + alpha_fill.x, wide_tile_y);
+                    let (payload, paint) = Self::process_paint(
+                        &alpha_fill.paint,
+                        scene,
+                        (scene_strip_x, scene_strip_y),
+                    );
 
                     let (x, y) = if clip_depth == 1 {
-                        (wide_tile_x + alpha_fill.x, wide_tile_y)
+                        (scene_strip_x, scene_strip_y)
                     } else {
                         (alpha_fill.x, el.slot_ix as u16 * Tile::HEIGHT)
                     };
@@ -443,7 +449,7 @@
                         width: alpha_fill.width,
                         dense_width: alpha_fill.width,
                         col_idx,
-                        rgba_or_slot,
+                        payload,
                         paint,
                     });
                 }
@@ -488,14 +494,15 @@
                     } else {
                         (clip_fill.x as u16, nos.slot_ix as u16 * Tile::HEIGHT)
                     };
+                    let paint = COLOR_SOURCE_SLOT << 31;
                     draw.0.push(GpuStrip {
                         x,
                         y,
                         width: clip_fill.width as u16,
                         dense_width: 0,
                         col_idx: 0,
-                        rgba_or_slot: tos.slot_ix as u32,
-                        paint: 0,
+                        payload: tos.slot_ix as u32,
+                        paint,
                     });
                 }
                 Cmd::ClipStrip(clip_alpha_fill) => {
@@ -509,6 +516,7 @@
                     } else {
                         (clip_alpha_fill.x as u16, nos.slot_ix as u16 * Tile::HEIGHT)
                     };
+                    let paint = COLOR_SOURCE_SLOT << 31;
                     draw.0.push(GpuStrip {
                         x,
                         y,
@@ -517,8 +525,8 @@
                         col_idx: (clip_alpha_fill.alpha_idx / usize::from(Tile::HEIGHT))
                             .try_into()
                             .expect("Sparse strips are bound to u32 range"),
-                        rgba_or_slot: tos.slot_ix as u32,
-                        paint: 0,
+                        payload: tos.slot_ix as u32,
+                        paint,
                     });
                 }
                 _ => unimplemented!(),
@@ -528,8 +536,12 @@
         Ok(())
     }
 
-    /// Process a paint and return (`col_idx`, `rgba_or_slot`, `paint_type`)
-    fn process_paint(paint: &Paint, scene: &Scene, alpha_col: u32) -> (u32, u32, u32) {
+    /// Process a paint and return (`payload`, `paint`)
+    fn process_paint(
+        paint: &Paint,
+        scene: &Scene,
+        (scene_strip_x, scene_strip_y): (u16, u16),
+    ) -> (u32, u32) {
         match paint {
             Paint::Solid(color) => {
                 let rgba = color.as_premul_rgba8().to_u32();
@@ -537,7 +549,8 @@
                     has_non_zero_alpha(rgba),
                     "Color fields with 0 alpha are reserved for clipping"
                 );
-                (alpha_col, rgba, 0)
+                let paint_packed = (COLOR_SOURCE_PAYLOAD << 31) | (PAINT_TYPE_SOLID << 29);
+                (rgba, paint_packed)
             }
             Paint::Indexed(indexed_paint) => {
                 let paint_id = indexed_paint.index();
@@ -547,19 +560,28 @@
                 match scene.encoded_paints.get(paint_id) {
                     Some(EncodedPaint::Image(encoded_image)) => match &encoded_image.source {
                         ImageSource::OpaqueId(_) => {
-                            let paint_type = 2_u32;
-                            let paint_packed = (paint_type << 30) | (paint_tex_id & 0x3FFFFFFF);
-                            (alpha_col, 0, paint_packed)
+                            let paint_packed = (COLOR_SOURCE_PAYLOAD << 31)
+                                | (PAINT_TYPE_IMAGE << 29)
+                                | (paint_tex_id & 0x1FFFFFFF);
+                            let scene_strip_xy =
+                                ((scene_strip_y as u32) << 16) | (scene_strip_x as u32);
+                            (scene_strip_xy, paint_packed)
                         }
                         _ => unimplemented!("unsupported image source"),
                     },
-                    _ => (0, 0, 0),
+                    _ => (0, 0),
                 }
             }
         }
     }
 }
 
+const COLOR_SOURCE_PAYLOAD: u32 = 0;
+const COLOR_SOURCE_SLOT: u32 = 1;
+
+const PAINT_TYPE_SOLID: u32 = 0;
+const PAINT_TYPE_IMAGE: u32 = 1;
+
 #[inline(always)]
 fn has_non_zero_alpha(rgba: u32) -> bool {
     rgba >= 0x1_00_00_00
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
index 7b8bc1b..002d7cd 100644
--- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
+++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -10,14 +10,19 @@
 // The alpha values are stored in a texture and sampled during fragment shading.
 // This approach optimizes memory usage by only storing alpha data where needed.
 //
-// The `StripInstance`'s `rgba_or_slot` field can either encode a color or a slot index.
+// The `StripInstance`'s `payload` field can either encode a color or a slot index.
 // If the alpha value is non-zero, the fragment shader samples the alpha texture.
 // Otherwise, the fragment shader samples the source clip texture using the given slot index.
 
-// Paint types to determine how to process a strip
+// Color source modes - where the fragment shader gets color data from
+// Use payload (color or image coordinates)
+const COLOR_SOURCE_PAYLOAD: u32 = 0u;
+// Sample from clip texture slot
+const COLOR_SOURCE_SLOT: u32 = 1u;
+
+// Paint types
 const PAINT_TYPE_SOLID: u32 = 0u;  
-const PAINT_TYPE_ALPHA: u32 = 1u;  
-const PAINT_TYPE_IMAGE: u32 = 2u;  
+const PAINT_TYPE_IMAGE: u32 = 1u;  
 
 // Image quality
 const IMAGE_QUALITY_LOW = 0u;
@@ -40,8 +45,9 @@
 
 // Strip instance data
 //
-// `rgba_or_slot` field can either encode a color or a slot index.
-// If the alpha value is non-zero, the fragment shader samples the alpha texture.
+// `payload` field can either encode a color, [x, y] for image sampling or a slot index
+// If color source is payload and the paint type is solid, the fragment shader uses the color directly.
+// If color source is payload and the paint type is image, the fragment shader samples the image.
 // Otherwise, the fragment shader samples the source clip texture using the given slot index.
 struct StripInstance {
     // [x, y] packed as u16's
@@ -50,9 +56,16 @@
     @location(1) widths: u32,
     // Alpha texture column index where this strip's alpha values begin
     @location(2) col_idx: u32,
-    // [r, g, b, a] packed as u8's or a slot index when alpha is 0
-    @location(3) rgba_or_slot: u32,
-    // Packed paint type (2 bits) and paint texture id (30 bits)
+    // Packed data:
+    // - [r, g, b, a] packed as u8's (when color source is payload and paint type is solid)
+    // - [x, y] packed as u16's (when color source is payload and paint type is image)
+    // - a slot index (when color source is slot)
+    @location(3) payload: u32,
+    // Packed data:
+    // - color source (1 bit)
+    // - paint type (2 bits)
+    // - paint texture id (29 bits)
+    // Color source: 0 = use payload, 1 = sample from slot
     // Paint type: 0 = solid, 1 = alpha, 2 = image
     // Paint texture id locates the encoded image data `EncodedImage` in the encoded_paints_texture
     @location(4) paint: u32,
@@ -68,7 +81,7 @@
     // Ending x-position of the dense (alpha) region
     @location(3) @interpolate(flat) dense_end: u32,
     // Color value or slot index when alpha is 0
-    @location(4) @interpolate(flat) rgba_or_slot: u32,
+    @location(4) @interpolate(flat) payload: u32,
     // Normalized device coordinates (NDC) for the current vertex
     @builtin(position) position: vec4<f32>,
 };
@@ -109,28 +122,29 @@
     // NDC ranges from -1 to 1, with (0,0) at the center of the viewport
     let ndc_x = pix_x * 2.0 / f32(config.width) - 1.0;
     let ndc_y = 1.0 - pix_y * 2.0 / f32(config.height);
-    let paint_type = instance.paint >> 30u;
+    let paint_type = (instance.paint >> 29u) & 0x3u;
 
     if paint_type == PAINT_TYPE_IMAGE {
-        let paint_tex_id = instance.paint & 0x3FFFFFFF;
+        let paint_tex_id = instance.paint & 0x1FFFFFFF;
         
         let encoded_image = unpack_encoded_image(paint_tex_id);
-        // Vertex position within the texture
+        // Unpack view coordinates for image sampling
+        let scene_strip_x = instance.payload & 0xffffu;
+        let scene_strip_y = instance.payload >> 16u;
+        // Use view coordinates for image sampling (always in global view space)
         out.sample_xy = encoded_image.translate 
             + encoded_image.image_offset
-            + encoded_image.transform.xy * f32(x0) 
-            + encoded_image.transform.zw * f32(y0)
+            + encoded_image.transform.xy * f32(scene_strip_x) 
+            + encoded_image.transform.zw * f32(scene_strip_y)
             + encoded_image.transform.xy * x * f32(width)
             + encoded_image.transform.zw * y * f32(config.strip_height);
-        out.paint = instance.paint;
-    } else {
-        out.sample_xy = vec2<f32>(0.0, 0.0);
     }
 
     // Regular texture coordinates for other render types
     out.tex_coord = vec2<f32>(f32(instance.col_idx) + x * f32(width), y * f32(config.strip_height));
     out.position = vec4<f32>(ndc_x, ndc_y, 0.0, 1.0);
-    out.rgba_or_slot = instance.rgba_or_slot;
+    out.payload = instance.payload;
+    out.paint = instance.paint;
 
     return out;
 }
@@ -174,57 +188,58 @@
         alpha = f32((alphas_u32 >> (y * 8u)) & 0xffu) * (1.0 / 255.0);
     }
     // Apply the alpha value to the unpacked RGBA color or slot index
-    let alpha_byte = in.rgba_or_slot >> 24u;
-    var final_color = unpack4x8unorm(in.rgba_or_slot);
-    let paint_type = in.paint >> 30u;
+    let alpha_byte = in.payload >> 24u;
+    let color_source = (in.paint >> 31u) & 0x1u;
+    var final_color = unpack4x8unorm(in.payload);
+    let paint_type = (in.paint >> 29u) & 0x3u;
 
-    if paint_type == PAINT_TYPE_IMAGE {
-        let paint_tex_id = in.paint & 0x3FFFFFFF;
-        let encoded_image = unpack_encoded_image(paint_tex_id);
-        let image_offset = encoded_image.image_offset;
-        let image_size = encoded_image.image_size;
-        let local_xy = in.sample_xy - image_offset;
-        let offset = 0.00001;
-        let extended_xy = vec2<f32>(
-            extend_mode(local_xy.x, encoded_image.extend_modes.x, image_size.x - offset),
-            extend_mode(local_xy.y, encoded_image.extend_modes.y, image_size.y - offset)
-        );
-        
-        if encoded_image.quality == IMAGE_QUALITY_HIGH {
-            let final_xy = image_offset + extended_xy;
-            let sample_color = bicubic_sample(
-                atlas_texture,
-                final_xy,
-                image_offset,
-                image_size,
-                encoded_image.extend_modes
+    if color_source == COLOR_SOURCE_PAYLOAD {
+        // in.payload encodes a color for PAINT_TYPE_SOLID or sample_xy for PAINT_TYPE_IMAGE
+        final_color = alpha * final_color;
+
+        if paint_type == PAINT_TYPE_IMAGE {
+            let paint_tex_id = in.paint & 0x1FFFFFFF;
+            let encoded_image = unpack_encoded_image(paint_tex_id);
+            let image_offset = encoded_image.image_offset;
+            let image_size = encoded_image.image_size;
+            let local_xy = in.sample_xy - image_offset;
+            let offset = 0.00001;
+            let extended_xy = vec2<f32>(
+                extend_mode(local_xy.x, encoded_image.extend_modes.x, image_size.x - offset),
+                extend_mode(local_xy.y, encoded_image.extend_modes.y, image_size.y - offset)
             );
-            final_color = alpha * sample_color;
-        } else if encoded_image.quality == IMAGE_QUALITY_MEDIUM {
-            let final_xy = image_offset + extended_xy - vec2(0.5);
-            let sample_color = bilinear_sample(
-                atlas_texture,
-                final_xy,
-                image_offset,
-                image_size,
-                encoded_image.extend_modes
-            );
-            final_color = alpha * sample_color;
-        } else if encoded_image.quality == IMAGE_QUALITY_LOW {
-            let final_xy = image_offset + extended_xy;
-            final_color = alpha * textureLoad(atlas_texture, vec2<u32>(final_xy), 0);
+            
+            if encoded_image.quality == IMAGE_QUALITY_HIGH {
+                let final_xy = image_offset + extended_xy;
+                let sample_color = bicubic_sample(
+                    atlas_texture,
+                    final_xy,
+                    image_offset,
+                    image_size,
+                    encoded_image.extend_modes
+                );
+                final_color = alpha * sample_color;
+            } else if encoded_image.quality == IMAGE_QUALITY_MEDIUM {
+                let final_xy = image_offset + extended_xy - vec2(0.5);
+                let sample_color = bilinear_sample(
+                    atlas_texture,
+                    final_xy,
+                    image_offset,
+                    image_size,
+                    encoded_image.extend_modes
+                );
+                final_color = alpha * sample_color;
+            } else if encoded_image.quality == IMAGE_QUALITY_LOW {
+                let final_xy = image_offset + extended_xy;
+                final_color = alpha * textureLoad(atlas_texture, vec2<u32>(final_xy), 0);
+            }
         }
     } else {
-        if alpha_byte != 0 {
-            // in.rgba_or_slot encodes a color    
-            final_color = alpha * final_color;
-        } else {
-            // in.rgba_or_slot encodes a slot in the source clip texture
-            let clip_x = u32(in.position.x) & 0xFFu;
-            let clip_y = (u32(in.position.y) & 3) + in.rgba_or_slot * config.strip_height;
-            let clip_in_color = textureLoad(clip_input_texture, vec2(clip_x, clip_y), 0);
-            final_color = alpha * clip_in_color;
-        }
+        // in.payload encodes a slot in the source clip texture
+        let clip_x = u32(in.position.x) & 0xFFu;
+        let clip_y = (u32(in.position.y) & 3) + in.payload * config.strip_height;
+        let clip_in_color = textureLoad(clip_input_texture, vec2(clip_x, clip_y), 0);
+        final_color = alpha * clip_in_color;
     }
 
     return final_color;
diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_with_multiple_clip_layers.png b/sparse_strips/vello_sparse_tests/snapshots/image_with_multiple_clip_layers.png
new file mode 100644
index 0000000..9b6b77c
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/image_with_multiple_clip_layers.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb94d43df371385e172f29059c205df61bae88b68f1fafa817ee53ec74501c98
+size 731
diff --git a/sparse_strips/vello_sparse_tests/tests/image.rs b/sparse_strips/vello_sparse_tests/tests/image.rs
index 5b1608d..caba16c 100644
--- a/sparse_strips/vello_sparse_tests/tests/image.rs
+++ b/sparse_strips/vello_sparse_tests/tests/image.rs
@@ -10,6 +10,7 @@
 use vello_common::kurbo::{Affine, Point, Rect};
 use vello_common::paint::{Image, ImageSource};
 use vello_common::peniko::{Extend, ImageQuality};
+use vello_cpu::kurbo::{Shape, Triangle};
 use vello_dev_macros::vello_test;
 
 fn rgb_img_10x10(ctx: &mut impl Renderer) -> ImageSource {
@@ -487,3 +488,28 @@
         Extend::Reflect,
     );
 }
+
+#[vello_test]
+fn image_with_multiple_clip_layers(ctx: &mut impl Renderer) {
+    let image_source = rgb_img_2x2(ctx);
+    let image_rect = Rect::new(10.0, 10.0, 90.0, 90.0);
+    let clipped_area1 = Rect::new(20.0, 20.0, 80.0, 80.0);
+    let clipped_area2 = Triangle::new(
+        Point::new(90.0, 10.0),
+        Point::new(32.0, 46.0),
+        Point::new(54.0, 68.0),
+    );
+
+    ctx.push_clip_layer(&clipped_area1.to_path(0.1));
+    ctx.push_clip_layer(&clipped_area2.to_path(0.1));
+    ctx.set_paint_transform(Affine::IDENTITY);
+    ctx.set_paint(Image {
+        source: image_source,
+        x_extend: Extend::Repeat,
+        y_extend: Extend::Repeat,
+        quality: ImageQuality::Low,
+    });
+    ctx.fill_rect(&image_rect);
+    ctx.pop_layer();
+    ctx.pop_layer();
+}