[vello_hybrid]: add example for image destruction
diff --git a/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs b/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs
index d36bf7e..23e1784 100644
--- a/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs
+++ b/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs
@@ -11,8 +11,11 @@
 
 use std::cell::RefCell;
 use std::rc::Rc;
-use vello_common::kurbo::{Affine, Vec2};
-use vello_hybrid_scenes::AnyScene;
+use vello_common::{
+    kurbo::{Affine, Vec2},
+    paint::ImageId,
+};
+use vello_hybrid_scenes::{AnyScene, SceneResources};
 use wasm_bindgen::prelude::*;
 use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent};
 
@@ -41,6 +44,8 @@
     renderer_wrapper: RendererWrapper,
     need_render: bool,
     canvas: HtmlCanvasElement,
+    scene_resources: SceneResources,
+    uploaded_images: Vec<ImageId>,
 }
 
 impl AppState {
@@ -62,6 +67,8 @@
             renderer_wrapper,
             need_render: true,
             canvas,
+            scene_resources: SceneResources::new(),
+            uploaded_images: Vec::new(),
         };
 
         app_state.upload_images_to_atlas();
@@ -77,7 +84,11 @@
         self.scene.reset();
 
         // Render the current scene with transform
-        self.scenes[self.current_scene].render(&mut self.scene, self.transform);
+        self.scenes[self.current_scene].render(
+            &mut self.scene,
+            self.transform,
+            &self.scene_resources,
+        );
 
         let render_size = vello_hybrid::RenderSize {
             width: self.width,
@@ -118,8 +129,9 @@
         self.need_render = true;
     }
 
-    fn reset_transform(&mut self) {
+    fn reset_scene(&mut self) {
         self.transform = Affine::IDENTITY;
+        self.upload_images_to_atlas();
         self.need_render = true;
     }
 
@@ -180,14 +192,27 @@
     fn upload_images_to_atlas(&mut self) {
         use vello_hybrid_scenes::image::ImageScene;
 
+        self.scene_resources.images.clear();
+        self.uploaded_images.clear();
+
         // 1st example — uploading pixmap directly to WebGL atlas
         let pixmap1 = ImageScene::read_flower_image();
-        self.renderer_wrapper.renderer.upload_image(&pixmap1);
+        let image_id1 = self.renderer_wrapper.renderer.upload_image(&pixmap1);
 
         // 2nd example — uploading from a WebGL texture
         let pixmap2 = ImageScene::read_cowboy_image();
         let texture2 = self.pixmap_to_webgl_texture(&pixmap2);
-        self.renderer_wrapper.renderer.upload_image(&texture2);
+        let image_id2 = self.renderer_wrapper.renderer.upload_image(&texture2);
+
+        self.scene_resources.images = vec![image_id1, image_id2];
+        self.uploaded_images = vec![image_id2, image_id1];
+    }
+
+    fn handle_destroy_image(&mut self) {
+        if let Some(image_id) = self.uploaded_images.pop() {
+            self.renderer_wrapper.renderer.destroy_image(image_id);
+        }
+        self.need_render = true;
     }
 
     /// Convert a pixmap to WebGL texture
@@ -382,7 +407,8 @@
                 Box::new(move |event: KeyboardEvent| match event.key().as_str() {
                     "ArrowRight" => app_state.borrow_mut().next_scene(),
                     "ArrowLeft" => app_state.borrow_mut().prev_scene(),
-                    " " => app_state.borrow_mut().reset_transform(),
+                    "d" => app_state.borrow_mut().handle_destroy_image(),
+                    " " => app_state.borrow_mut().reset_scene(),
                     _ => {}
                 }) as Box<dyn FnMut(_)>,
             );
@@ -396,7 +422,7 @@
     let document = web_sys::window().unwrap().document().unwrap();
     let instructions = document.create_element("div").unwrap();
     instructions.set_inner_html(
-        "Left/Right Arrow: Change scene | Space: Reset view | Mouse Drag: Pan | Mouse Wheel: Zoom",
+        "Left/Right Arrow: Change scene | Space: Reset scene | D: Destroy image | Mouse Drag: Pan | Mouse Wheel: Zoom",
     );
     let style = instructions
         .dyn_ref::<web_sys::HtmlElement>()
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/clip.rs b/sparse_strips/vello_hybrid/examples/scenes/src/clip.rs
index 009402c..63979c5 100644
--- a/sparse_strips/vello_hybrid/examples/scenes/src/clip.rs
+++ b/sparse_strips/vello_hybrid/examples/scenes/src/clip.rs
@@ -22,7 +22,7 @@
 pub struct ClipScene {}
 
 impl ExampleScene for ClipScene {
-    fn render(&mut self, ctx: &mut Scene, root_transform: Affine) {
+    fn render(&mut self, ctx: &mut Scene, root_transform: Affine, _resources: &crate::SceneResources) {
         render(ctx, root_transform);
     }
 }
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/image.rs b/sparse_strips/vello_hybrid/examples/scenes/src/image.rs
index 0e7ca3f..0659c43 100644
--- a/sparse_strips/vello_hybrid/examples/scenes/src/image.rs
+++ b/sparse_strips/vello_hybrid/examples/scenes/src/image.rs
@@ -10,28 +10,28 @@
 use vello_common::pixmap::Pixmap;
 use vello_common::{
     kurbo::{Affine, Rect},
-    paint::{Image, ImageId, ImageSource},
+    paint::{Image, ImageSource},
     peniko::{Extend, ImageQuality},
 };
 use vello_hybrid::Scene;
 
-use crate::ExampleScene;
+use crate::{ExampleScene, SceneResources};
 
-/// Image scene state
+/// Image scene shows how to use images in the scene.
 #[derive(Debug, Default)]
-pub struct ImageScene {}
+pub struct ImageScene;
 
 impl ImageScene {
     /// Create a new image scene
     pub fn new() -> Self {
-        Self {}
+        Self
     }
 }
 
 impl ExampleScene for ImageScene {
-    fn render(&mut self, scene: &mut Scene, root_transform: Affine) {
-        let splash_flower_id = ImageId::new(0);
-        let cowboy_id = ImageId::new(1);
+    fn render(&mut self, scene: &mut Scene, root_transform: Affine, resources: &SceneResources) {
+        let splash_flower_id = resources.images.first().copied().unwrap();
+        let cowboy_id = resources.images.get(1).copied().unwrap();
 
         scene.set_transform(
             root_transform
@@ -80,7 +80,7 @@
         );
         scene.set_paint_transform(Affine::scale(0.25));
         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,
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/lib.rs b/sparse_strips/vello_hybrid/examples/scenes/src/lib.rs
index f9f5394..9290424 100644
--- a/sparse_strips/vello_hybrid/examples/scenes/src/lib.rs
+++ b/sparse_strips/vello_hybrid/examples/scenes/src/lib.rs
@@ -10,12 +10,30 @@
 pub mod text;
 
 use vello_common::kurbo::Affine;
+use vello_common::paint::ImageId;
 use vello_hybrid::Scene;
 
-/// Example scene that can maintain state between renders
+/// Resources that scenes may need each frame.
+#[derive(Debug, Clone, Default)]
+pub struct SceneResources {
+    /// Image ids that have been uploaded to the GPU.
+    pub images: Vec<ImageId>,
+}
+
+impl SceneResources {
+    /// Create a new instance
+    pub fn new() -> Self {
+        Self { images: Vec::new() }
+    }
+}
+
+/// Example scene that can maintain state between renders.
+///
+/// The `resources` parameter contains data such a uploaded image ids.
+/// Scenes that do not require external resources can ignore this parameter.
 pub trait ExampleScene {
-    /// Render the scene using the current state
-    fn render(&mut self, scene: &mut Scene, root_transform: Affine);
+    /// Render the scene using the current state.
+    fn render(&mut self, scene: &mut Scene, root_transform: Affine, resources: &SceneResources);
 }
 
 /// A type-erased example scene
@@ -25,7 +43,7 @@
 }
 
 /// A type-erased render function
-type RenderFn = Box<dyn FnMut(&mut Scene, Affine)>;
+type RenderFn = Box<dyn FnMut(&mut Scene, Affine, &SceneResources)>;
 
 impl std::fmt::Debug for AnyScene {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -37,13 +55,18 @@
     /// Create a new `AnyScene` from any type that implements `ExampleScene`
     pub fn new<T: ExampleScene + 'static>(mut scene: T) -> Self {
         Self {
-            render_fn: Box::new(move |s, transform| scene.render(s, transform)),
+            render_fn: Box::new(move |s, transform, res| scene.render(s, transform, res)),
         }
     }
 
     /// Render the scene
-    pub fn render(&mut self, scene: &mut Scene, root_transform: Affine) {
-        (self.render_fn)(scene, root_transform);
+    pub fn render(
+        &mut self,
+        scene: &mut Scene,
+        root_transform: Affine,
+        resources: &SceneResources,
+    ) {
+        (self.render_fn)(scene, root_transform, resources);
     }
 }
 
@@ -80,7 +103,7 @@
         AnyScene::new(text::TextScene::new("Hello, Vello!")),
         AnyScene::new(simple::SimpleScene::new()),
         AnyScene::new(clip::ClipScene::new()),
-        AnyScene::new(image::ImageScene {}),
+        AnyScene::new(image::ImageScene::new()),
     ]
     .into_boxed_slice()
 }
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/simple.rs b/sparse_strips/vello_hybrid/examples/scenes/src/simple.rs
index b8da87b..7c491d6 100644
--- a/sparse_strips/vello_hybrid/examples/scenes/src/simple.rs
+++ b/sparse_strips/vello_hybrid/examples/scenes/src/simple.rs
@@ -14,7 +14,7 @@
 pub struct SimpleScene {}
 
 impl ExampleScene for SimpleScene {
-    fn render(&mut self, ctx: &mut Scene, root_transform: Affine) {
+    fn render(&mut self, ctx: &mut Scene, root_transform: Affine, _resources: &crate::SceneResources) {
         render(ctx, root_transform);
     }
 }
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/svg.rs b/sparse_strips/vello_hybrid/examples/scenes/src/svg.rs
index 11c4317..53b9e77 100644
--- a/sparse_strips/vello_hybrid/examples/scenes/src/svg.rs
+++ b/sparse_strips/vello_hybrid/examples/scenes/src/svg.rs
@@ -26,7 +26,7 @@
 }
 
 impl ExampleScene for SvgScene {
-    fn render(&mut self, scene: &mut Scene, root_transform: Affine) {
+    fn render(&mut self, scene: &mut Scene, root_transform: Affine, _resources: &crate::SceneResources) {
         render_svg(scene, &self.svg.items, root_transform * self.transform);
     }
 }
diff --git a/sparse_strips/vello_hybrid/examples/scenes/src/text.rs b/sparse_strips/vello_hybrid/examples/scenes/src/text.rs
index bdc5b70..fc8fc16 100644
--- a/sparse_strips/vello_hybrid/examples/scenes/src/text.rs
+++ b/sparse_strips/vello_hybrid/examples/scenes/src/text.rs
@@ -45,7 +45,7 @@
 }
 
 impl ExampleScene for TextScene {
-    fn render(&mut self, scene: &mut Scene, root_transform: Affine) {
+    fn render(&mut self, scene: &mut Scene, root_transform: Affine, _resources: &crate::SceneResources) {
         scene.set_transform(root_transform);
         render_text(self, scene);
     }
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 4ed8269..21be7a8 100644
--- a/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
+++ b/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
@@ -11,9 +11,12 @@
 
 use std::cell::RefCell;
 use std::rc::Rc;
-use vello_common::kurbo::{Affine, Point};
+use vello_common::{
+    kurbo::{Affine, Point},
+    paint::ImageId,
+};
 use vello_hybrid::Pixmap;
-use vello_hybrid_scenes::AnyScene;
+use vello_hybrid_scenes::{AnyScene, SceneResources};
 use wasm_bindgen::prelude::*;
 use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent};
 
@@ -123,6 +126,8 @@
     renderer_wrapper: RendererWrapper,
     need_render: bool,
     canvas: HtmlCanvasElement,
+    scene_resources: SceneResources,
+    uploaded_images: Vec<ImageId>,
 }
 
 impl AppState {
@@ -144,6 +149,8 @@
             renderer_wrapper,
             need_render: true,
             canvas,
+            scene_resources: SceneResources::new(),
+            uploaded_images: Vec::new(),
         };
 
         // Upload images to the WebGL atlas
@@ -160,7 +167,11 @@
         self.scene.reset();
 
         // Render the current scene with transform
-        self.scenes[self.current_scene].render(&mut self.scene, self.transform);
+        self.scenes[self.current_scene].render(
+            &mut self.scene,
+            self.transform,
+            &self.scene_resources,
+        );
 
         let render_size = vello_hybrid::RenderSize {
             width: self.width,
@@ -223,8 +234,9 @@
         self.need_render = true;
     }
 
-    fn reset_transform(&mut self) {
+    fn reset_scene(&mut self) {
         self.transform = Affine::IDENTITY;
+        self.upload_images_to_atlas();
         self.need_render = true;
     }
 
@@ -267,9 +279,19 @@
         self.need_render = true;
     }
 
+    fn handle_destroy_image(&mut self) {
+        if let Some(image_id) = self.uploaded_images.pop() {
+            self.destroy_image(image_id);
+        }
+        self.need_render = true;
+    }
+
     fn upload_images_to_atlas(&mut self) {
         use vello_hybrid_scenes::image::ImageScene;
 
+        self.scene_resources.images.clear();
+        self.uploaded_images.clear();
+
         let mut encoder =
             self.renderer_wrapper
                 .device
@@ -279,7 +301,7 @@
 
         // 1st example — uploading pixmap directly to WebGL atlas
         let pixmap1 = ImageScene::read_flower_image();
-        self.renderer_wrapper.renderer.upload_image(
+        let image_id1 = self.renderer_wrapper.renderer.upload_image(
             &self.renderer_wrapper.device,
             &self.renderer_wrapper.queue,
             &mut encoder,
@@ -293,13 +315,16 @@
             &self.renderer_wrapper.queue,
             &pixmap2,
         );
-        self.renderer_wrapper.renderer.upload_image(
+        let image_id2 = self.renderer_wrapper.renderer.upload_image(
             &self.renderer_wrapper.device,
             &self.renderer_wrapper.queue,
             &mut encoder,
             &texture2,
         );
 
+        self.scene_resources.images = vec![image_id1, image_id2];
+        self.uploaded_images = vec![image_id2, image_id1];
+
         self.renderer_wrapper.queue.submit([encoder.finish()]);
     }
 
@@ -352,6 +377,22 @@
 
         texture
     }
+
+    fn destroy_image(&mut self, image_id: ImageId) {
+        let mut encoder =
+            self.renderer_wrapper
+                .device
+                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+                    label: Some("Destroy Image pass"),
+                });
+        self.renderer_wrapper.renderer.destroy_image(
+            &self.renderer_wrapper.device,
+            &self.renderer_wrapper.queue,
+            &mut encoder,
+            image_id,
+        );
+        self.renderer_wrapper.queue.submit([encoder.finish()]);
+    }
 }
 
 #[wasm_bindgen]
@@ -494,7 +535,8 @@
                 Box::new(move |event: KeyboardEvent| match event.key().as_str() {
                     "ArrowRight" => app_state.borrow_mut().next_scene(),
                     "ArrowLeft" => app_state.borrow_mut().prev_scene(),
-                    " " => app_state.borrow_mut().reset_transform(),
+                    "d" => app_state.borrow_mut().handle_destroy_image(),
+                    " " => app_state.borrow_mut().reset_scene(),
                     _ => {}
                 }) as Box<dyn FnMut(_)>,
             );
@@ -508,7 +550,7 @@
     let document = web_sys::window().unwrap().document().unwrap();
     let instructions = document.create_element("div").unwrap();
     instructions.set_inner_html(
-        "Left/Right Arrow: Change scene | Space: Reset view | Mouse Drag: Pan | Mouse Wheel: Zoom",
+        "Left/Right Arrow: Change scene | Space: Reset scene | D: Destroy image | Mouse Drag: Pan | Mouse Wheel: Zoom",
     );
     let style = instructions
         .dyn_ref::<web_sys::HtmlElement>()
diff --git a/sparse_strips/vello_hybrid/examples/winit/src/main.rs b/sparse_strips/vello_hybrid/examples/winit/src/main.rs
index d56b682..da308ed 100644
--- a/sparse_strips/vello_hybrid/examples/winit/src/main.rs
+++ b/sparse_strips/vello_hybrid/examples/winit/src/main.rs
@@ -11,9 +11,10 @@
 use vello_common::color::palette::css::WHITE;
 use vello_common::color::{AlphaColor, Srgb};
 use vello_common::kurbo::{Affine, Point};
+use vello_common::paint::ImageId;
 use vello_hybrid::{Pixmap, RenderSize, Renderer, Scene};
 use vello_hybrid_scenes::image::ImageScene;
-use vello_hybrid_scenes::{AnyScene, get_example_scenes};
+use vello_hybrid_scenes::{AnyScene, SceneResources, get_example_scenes};
 use winit::{
     application::ApplicationHandler,
     event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent},
@@ -45,6 +46,8 @@
     transform: Affine,
     mouse_down: bool,
     last_cursor_position: Option<Point>,
+    scene_resources: SceneResources,
+    uploaded_images: Vec<ImageId>,
 }
 
 fn main() {
@@ -87,6 +90,8 @@
         transform: Affine::IDENTITY,
         mouse_down: false,
         last_cursor_position: None,
+        scene_resources: SceneResources::new(),
+        uploaded_images: vec![],
     };
 
     let event_loop = EventLoop::new().unwrap();
@@ -195,13 +200,27 @@
                     window.request_redraw();
                 }
                 Key::Named(NamedKey::Space) => {
+                    let dev_id = surface.dev_id;
+                    let window = window.clone();
                     // Reset transform on spacebar
                     self.transform = Affine::IDENTITY;
+                    self.upload_images_to_atlas(dev_id);
                     window.request_redraw();
                 }
                 Key::Named(NamedKey::Escape) => {
                     event_loop.exit();
                 }
+                Key::Character(str) => {
+                    if str == "d" && self.current_scene == 4 {
+                        let dev_id = surface.dev_id;
+                        let window = window.clone();
+
+                        if let Some(image_id) = self.uploaded_images.pop() {
+                            self.destroy_image(dev_id, image_id);
+                        }
+                        window.request_redraw();
+                    }
+                }
                 _ => {}
             },
             WindowEvent::MouseInput { state, button, .. } => {
@@ -264,7 +283,11 @@
                 self.scene.reset();
 
                 self.scene.set_transform(self.transform);
-                self.scenes[self.current_scene].render(&mut self.scene, self.transform);
+                self.scenes[self.current_scene].render(
+                    &mut self.scene,
+                    self.transform,
+                    &self.scene_resources,
+                );
 
                 let device_handle = &self.context.devices[surface.dev_id];
                 let render_size = RenderSize {
@@ -312,6 +335,9 @@
 
 impl App<'_> {
     fn upload_images_to_atlas(&mut self, device_id: usize) {
+        self.scene_resources.images.clear();
+        self.uploaded_images.clear();
+
         let device_handle = &self.context.devices[device_id];
         let mut encoder =
             device_handle
@@ -322,7 +348,7 @@
 
         // 1st example — uploading pixmap directly
         let pixmap1 = ImageScene::read_flower_image();
-        self.renderers[device_id].as_mut().unwrap().upload_image(
+        let image_id1 = self.renderers[device_id].as_mut().unwrap().upload_image(
             &device_handle.device,
             &device_handle.queue,
             &mut encoder,
@@ -333,13 +359,16 @@
         let pixmap2 = ImageScene::read_cowboy_image();
         let texture2 =
             self.upload_image_to_texture(&device_handle.device, &device_handle.queue, &pixmap2);
-        self.renderers[device_id].as_mut().unwrap().upload_image(
+        let image_id2 = self.renderers[device_id].as_mut().unwrap().upload_image(
             &device_handle.device,
             &device_handle.queue,
             &mut encoder,
             &texture2,
         );
 
+        self.scene_resources.images = vec![image_id1, image_id2];
+        self.uploaded_images = vec![image_id2, image_id1];
+
         device_handle.queue.submit([encoder.finish()]);
     }
 
@@ -392,4 +421,21 @@
 
         texture
     }
+
+    fn destroy_image(&mut self, device_id: usize, image_id: ImageId) {
+        let device_handle = &self.context.devices[device_id];
+        let mut encoder =
+            device_handle
+                .device
+                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+                    label: Some("Destroy Image pass"),
+                });
+        self.renderers[device_id].as_mut().unwrap().destroy_image(
+            &device_handle.device,
+            &device_handle.queue,
+            &mut encoder,
+            image_id,
+        );
+        device_handle.queue.submit([encoder.finish()]);
+    }
 }
diff --git a/sparse_strips/vello_hybrid/src/image_cache.rs b/sparse_strips/vello_hybrid/src/image_cache.rs
index 15b6d64..f201352 100644
--- a/sparse_strips/vello_hybrid/src/image_cache.rs
+++ b/sparse_strips/vello_hybrid/src/image_cache.rs
@@ -80,6 +80,14 @@
         self.slots.get(id.as_u32() as usize)?.as_ref()
     }
 
+    /// Check if an image is in the cache
+    pub(crate) fn contains(&self, id: ImageId) -> bool {
+        self.slots
+            .get(id.as_u32() as usize)
+            .and_then(|slot| slot.as_ref())
+            .is_some()
+    }
+
     /// Allocate an image in the cache
     #[expect(
         clippy::cast_possible_truncation,
@@ -89,7 +97,7 @@
         let alloc = self
             .atlas
             .allocate(size2(width as i32, height as i32))
-            .expect("Failed to allocate texture");
+            .expect("Failed to allocate image in atlas");
 
         let slot_idx = self.free_idxs.pop().unwrap_or_else(|| {
             // No free slots, append to vector
@@ -248,4 +256,13 @@
         assert!(new_id2.as_u32() == 1);
         assert_ne!(new_id1.as_u32(), new_id2.as_u32());
     }
+
+    #[test]
+    fn test_contains() {
+        let mut cache = ImageCache::default();
+        let id = cache.allocate(100, 100);
+        assert!(cache.contains(id));
+        cache.deallocate(id);
+        assert!(!cache.contains(id));
+    }
 }
diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs
index ecbb1a1..dd8276b 100644
--- a/sparse_strips/vello_hybrid/src/render/webgl.rs
+++ b/sparse_strips/vello_hybrid/src/render/webgl.rs
@@ -22,7 +22,7 @@
 
 use crate::{
     GpuStrip, RenderError, RenderSize,
-    image_cache::{ImageCache, ImageResource},
+    image_cache::ImageCache,
     render::Config,
     scene::Scene,
     schedule::{LoadOp, RendererBackend, Scheduler},
@@ -116,7 +116,8 @@
             programs: &mut self.programs,
             gl: &self.gl,
         };
-        self.scheduler.do_scene(&mut ctx, scene)?;
+        self.scheduler
+            .do_scene(&mut ctx, scene, &self.image_cache)?;
 
         // Blit the view framebuffer to the default framebuffer (canvas element), reflecting the
         // image along the Y axis to complete the WebGPU to WebGL2 coordinate transform.
@@ -257,32 +258,40 @@
     }
 
     fn prepare_gpu_encoded_paints(&self, encoded_paints: &[EncodedPaint]) -> Vec<GpuEncodedImage> {
-        let mut bytes: Vec<GpuEncodedImage> = Vec::new();
+        let mut gpu_encoded_images: Vec<GpuEncodedImage> = Vec::with_capacity(encoded_paints.len());
         for paint in encoded_paints {
             match paint {
                 EncodedPaint::Image(img) => {
                     if let ImageSource::OpaqueId(image_id) = img.source {
-                        let image_resource: Option<&ImageResource> = self.image_cache.get(image_id);
-                        if let Some(image_resource) = image_resource {
-                            let transform = img.transform * Affine::translate((-0.5, -0.5));
-                            // pack two u16 as u32
-                            let image_size = ((image_resource.width as u32) << 16)
-                                | image_resource.height as u32;
-                            let image_offset = ((image_resource.offset[0] as u32) << 16)
-                                | image_resource.offset[1] as u32;
-                            let quality_and_extend_modes = ((img.extends.1 as u32) << 4)
-                                | ((img.extends.0 as u32) << 2)
-                                | img.quality as u32;
+                        let (width, height, offset) =
+                            if let Some(image_resource) = self.image_cache.get(image_id) {
+                                (
+                                    image_resource.width,
+                                    image_resource.height,
+                                    image_resource.offset,
+                                )
+                            } else {
+                                (0, 0, [0, 0])
+                            };
+                        let transform = img.transform * Affine::translate((-0.5, -0.5));
+                        // Pack two u16 as u32 (width in high 16 bits, height in low 16 bits)
+                        let image_size = ((width as u32) << 16) | height as u32;
+                        // Pack offset similarly (x in high 16 bits, y in low 16 bits)
+                        let image_offset = ((offset[0] as u32) << 16) | offset[1] as u32;
+                        // Pack quality and extend modes:
+                        // first 2 bits for quality, next 4 bits for extend mode
+                        let quality_and_extend_modes = ((img.extends.1 as u32) << 4)
+                            | ((img.extends.0 as u32) << 2)
+                            | img.quality as u32;
 
-                            bytes.push(GpuEncodedImage {
-                                quality_and_extend_modes,
-                                image_size,
-                                image_offset,
-                                _padding1: 0,
-                                transform: transform.as_coeffs().map(|x| x as f32),
-                                _padding2: [0, 0],
-                            });
-                        }
+                        gpu_encoded_images.push(GpuEncodedImage {
+                            quality_and_extend_modes,
+                            image_size,
+                            image_offset,
+                            _padding1: 0,
+                            transform: transform.as_coeffs().map(|x| x as f32),
+                            _padding2: [0, 0],
+                        });
                     }
                 }
                 _ => {
@@ -290,7 +299,7 @@
                 }
             }
         }
-        bytes
+        gpu_encoded_images
     }
 }
 
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs
index 6968e22..31d2f2c 100644
--- a/sparse_strips/vello_hybrid/src/render/wgpu.rs
+++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs
@@ -35,7 +35,7 @@
 
 use crate::{
     GpuStrip, RenderError, RenderSize,
-    image_cache::{ImageCache, ImageResource},
+    image_cache::ImageCache,
     render::{Config, common::GpuEncodedImage},
     scene::Scene,
     schedule::{LoadOp, RendererBackend, Scheduler},
@@ -103,7 +103,7 @@
             view,
         };
 
-        self.scheduler.do_scene(&mut junk, scene)
+        self.scheduler.do_scene(&mut junk, scene, &self.image_cache)
     }
 
     /// Upload image to cache and atlas in one step. Returns the `ImageId`.
@@ -207,32 +207,40 @@
     }
 
     fn prepare_gpu_encoded_paints(&self, encoded_paints: &[EncodedPaint]) -> Vec<GpuEncodedImage> {
-        let mut bytes: Vec<GpuEncodedImage> = Vec::new();
+        let mut gpu_encoded_images: Vec<GpuEncodedImage> = Vec::with_capacity(encoded_paints.len());
         for paint in encoded_paints {
             match paint {
                 EncodedPaint::Image(img) => {
                     if let ImageSource::OpaqueId(image_id) = img.source {
-                        let image_resource: Option<&ImageResource> = self.image_cache.get(image_id);
-                        if let Some(image_resource) = image_resource {
-                            let transform = img.transform * Affine::translate((-0.5, -0.5));
-                            // pack two u16 as u32
-                            let image_size = ((image_resource.width as u32) << 16)
-                                | image_resource.height as u32;
-                            let image_offset = ((image_resource.offset[0] as u32) << 16)
-                                | image_resource.offset[1] as u32;
-                            let quality_and_extend_modes = ((img.extends.1 as u32) << 4)
-                                | ((img.extends.0 as u32) << 2)
-                                | img.quality as u32;
+                        let (width, height, offset) =
+                            if let Some(image_resource) = self.image_cache.get(image_id) {
+                                (
+                                    image_resource.width,
+                                    image_resource.height,
+                                    image_resource.offset,
+                                )
+                            } else {
+                                (0, 0, [0, 0])
+                            };
+                        let transform = img.transform * Affine::translate((-0.5, -0.5));
+                        // Pack two u16 as u32 (width in high 16 bits, height in low 16 bits)
+                        let image_size = ((width as u32) << 16) | height as u32;
+                        // Pack offset similarly (x in high 16 bits, y in low 16 bits)
+                        let image_offset = ((offset[0] as u32) << 16) | offset[1] as u32;
+                        // Pack quality and extend modes:
+                        // first 2 bits for quality, next 4 bits for extend mode
+                        let quality_and_extend_modes = ((img.extends.1 as u32) << 4)
+                            | ((img.extends.0 as u32) << 2)
+                            | img.quality as u32;
 
-                            bytes.push(GpuEncodedImage {
-                                quality_and_extend_modes,
-                                image_size,
-                                image_offset,
-                                _padding1: 0,
-                                transform: transform.as_coeffs().map(|x| x as f32),
-                                _padding2: [0, 0],
-                            });
-                        }
+                        gpu_encoded_images.push(GpuEncodedImage {
+                            quality_and_extend_modes,
+                            image_size,
+                            image_offset,
+                            _padding1: 0,
+                            transform: transform.as_coeffs().map(|x| x as f32),
+                            _padding2: [0, 0],
+                        });
                     }
                 }
                 _ => {
@@ -240,7 +248,7 @@
                 }
             }
         }
-        bytes
+        gpu_encoded_images
     }
 }
 
diff --git a/sparse_strips/vello_hybrid/src/schedule.rs b/sparse_strips/vello_hybrid/src/schedule.rs
index a5fedde..e3851a8 100644
--- a/sparse_strips/vello_hybrid/src/schedule.rs
+++ b/sparse_strips/vello_hybrid/src/schedule.rs
@@ -176,6 +176,7 @@
 only break in edge cases, and some of them are also only related to conversions from f64 to f32."
 )]
 
+use crate::image_cache::ImageCache;
 use crate::render::common::GpuEncodedImage;
 use crate::{GpuStrip, RenderError, Scene};
 use alloc::collections::VecDeque;
@@ -269,6 +270,7 @@
         &mut self,
         renderer: &mut R,
         scene: &Scene,
+        image_cache: &ImageCache,
     ) -> Result<(), RenderError> {
         let mut tile_state = mem::take(&mut self.tile_state);
         let wide_tiles_per_row = scene.wide.width_tiles();
@@ -287,6 +289,7 @@
                     wide_tile_y,
                     wide_tile,
                     &mut tile_state,
+                    image_cache,
                 )?;
             }
         }
@@ -370,6 +373,7 @@
         wide_tile_y: u16,
         tile: &WideTile,
         state: &mut TileState,
+        image_cache: &ImageCache,
     ) -> Result<(), RenderError> {
         state.stack.clear();
         // Sentinel `TileEl` to indicate the end of the stack where we draw all
@@ -402,7 +406,8 @@
                     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 (col_idx, rgba_or_slot, paint) =
+                        Self::process_paint(&fill.paint, scene, 0, image_cache);
 
                     let (x, y) = if clip_depth == 1 {
                         (wide_tile_x + fill.x, wide_tile_y)
@@ -429,7 +434,7 @@
                         .expect("Sparse strips are bound to u32 range");
 
                     let (col_idx, rgba_or_slot, paint) =
-                        Self::process_paint(&alpha_fill.paint, scene, alpha_col);
+                        Self::process_paint(&alpha_fill.paint, scene, alpha_col, image_cache);
 
                     let (x, y) = if clip_depth == 1 {
                         (wide_tile_x + alpha_fill.x, wide_tile_y)
@@ -528,8 +533,13 @@
         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 (`col_idx`, `rgba_or_slot`, `paint`)
+    fn process_paint(
+        paint: &Paint,
+        scene: &Scene,
+        alpha_col: u32,
+        image_cache: &ImageCache,
+    ) -> (u32, u32, u32) {
         match paint {
             Paint::Solid(color) => {
                 let rgba = color.as_premul_rgba8().to_u32();
@@ -546,9 +556,12 @@
 
                 match scene.encoded_paints.get(paint_id) {
                     Some(EncodedPaint::Image(encoded_image)) => match &encoded_image.source {
-                        ImageSource::OpaqueId(_) => {
+                        ImageSource::OpaqueId(image_id) => {
                             let paint_type = 2_u32;
-                            let paint_packed = (paint_type << 30) | (paint_tex_id & 0x3FFFFFFF);
+                            let skip_paint = !image_cache.contains(*image_id) as u32;
+                            let paint_packed = (paint_type << 30)
+                                | (skip_paint << 29)
+                                | (paint_tex_id & 0x1FFFFFFF);
                             (alpha_col, 0, paint_packed)
                         }
                         _ => unimplemented!("unsupported image source"),
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
index 7b8bc1b..3d33ebc 100644
--- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
+++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -112,8 +112,7 @@
     let paint_type = instance.paint >> 30u;
 
     if paint_type == PAINT_TYPE_IMAGE {
-        let paint_tex_id = instance.paint & 0x3FFFFFFF;
-        
+        let paint_tex_id = instance.paint & 0x1FFFFFFFu;
         let encoded_image = unpack_encoded_image(paint_tex_id);
         // Vertex position within the texture
         out.sample_xy = encoded_image.translate 
@@ -179,7 +178,12 @@
     let paint_type = in.paint >> 30u;
 
     if paint_type == PAINT_TYPE_IMAGE {
-        let paint_tex_id = in.paint & 0x3FFFFFFF;
+        let paint_tex_id = in.paint & 0x1FFFFFFFu;
+        let skip_paint = in.paint >> 29u & 1u;
+        if skip_paint == 1u {
+            discard;
+        }
+        
         let encoded_image = unpack_encoded_image(paint_tex_id);
         let image_offset = encoded_image.image_offset;
         let image_size = encoded_image.image_size;