perf: eliminate overdraw for opaque image fills
diff --git a/Cargo.lock b/Cargo.lock
index f3e5bd3..9565b1c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3944,6 +3944,7 @@
 version = "0.0.0"
 dependencies = [
  "criterion",
+ "image",
  "parley",
  "rand",
  "smallvec",
diff --git a/sparse_strips/vello_bench/Cargo.toml b/sparse_strips/vello_bench/Cargo.toml
index d35ebe2..941ca0c 100644
--- a/sparse_strips/vello_bench/Cargo.toml
+++ b/sparse_strips/vello_bench/Cargo.toml
@@ -14,6 +14,7 @@
 vello_cpu = { workspace = true }
 vello_dev_macros = { workspace = true }
 criterion = { workspace = true }
+image = { workspace = true, features = ["jpeg"] }
 parley = { version = "0.5.0", default-features = true }
 rand = { workspace = true }
 smallvec = { workspace = true }
diff --git a/sparse_strips/vello_bench/benches/main.rs b/sparse_strips/vello_bench/benches/main.rs
index daf4b9d..bc2b6b7 100644
--- a/sparse_strips/vello_bench/benches/main.rs
+++ b/sparse_strips/vello_bench/benches/main.rs
@@ -5,7 +5,7 @@
 #![allow(dead_code, reason = "Might be unused on platforms not supporting SIMD")]
 
 use criterion::{criterion_group, criterion_main};
-use vello_bench::{fine, flatten, glyph, strip, tile};
+use vello_bench::{fine, flatten, glyph, scene, strip, tile};
 
 criterion_group!(fine_solid, fine::fill);
 criterion_group!(fine_strip, fine::strip);
@@ -19,6 +19,7 @@
 criterion_group!(strokes, flatten::strokes);
 criterion_group!(render_strips, strip::render_strips);
 criterion_group!(glyph, glyph::glyph);
+criterion_group!(scene_bench, scene::images);
 criterion_main!(
     tile,
     render_strips,
@@ -31,5 +32,6 @@
     fine_gradient,
     fine_rounded_blurred_rect,
     fine_blend,
-    fine_image
+    fine_image,
+    scene_bench
 );
diff --git a/sparse_strips/vello_bench/src/lib.rs b/sparse_strips/vello_bench/src/lib.rs
index a0d5f57..1ac6e7b 100644
--- a/sparse_strips/vello_bench/src/lib.rs
+++ b/sparse_strips/vello_bench/src/lib.rs
@@ -11,6 +11,7 @@
 pub mod fine;
 pub mod flatten;
 pub mod glyph;
+pub mod scene;
 pub mod strip;
 pub mod tile;
 
diff --git a/sparse_strips/vello_bench/src/scene.rs b/sparse_strips/vello_bench/src/scene.rs
new file mode 100644
index 0000000..09cedd1
--- /dev/null
+++ b/sparse_strips/vello_bench/src/scene.rs
@@ -0,0 +1,97 @@
+// Copyright 2025 the Vello Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+//! Full scene rendering benchmarks.
+
+use std::sync::Arc;
+
+use criterion::Criterion;
+use vello_common::kurbo::{Affine, Rect};
+use vello_common::paint::{Image, ImageSource};
+use vello_common::peniko::ImageSampler;
+use vello_common::peniko::{Extend, ImageQuality};
+use vello_common::pixmap::Pixmap;
+use vello_cpu::RenderContext;
+
+/// Image scene rendering benchmark.
+pub fn images(c: &mut Criterion) {
+    let mut g = c.benchmark_group("images");
+
+    let flower_image = load_flower_image();
+
+    const VIEWPORT_WIDTH: u16 = 1280;
+    const VIEWPORT_HEIGHT: u16 = 960;
+
+    let ImageSource::Pixmap(ref image_pixmap) = flower_image else {
+        panic!("Expected Pixmap");
+    };
+    let original_width = f64::from(image_pixmap.width());
+    let original_height = f64::from(image_pixmap.height());
+    let image_count = VIEWPORT_WIDTH / 256;
+
+    g.bench_function("overlapping", |b| {
+        let mut renderer = RenderContext::new(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
+        let mut pixmap = Pixmap::new(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
+
+        b.iter(|| {
+            renderer.reset();
+
+            for i in (1..=image_count).rev() {
+                let width = 256.0 * i as f64;
+                let scale = width / original_width;
+                let height = original_height * scale;
+
+                renderer.set_transform(Affine::IDENTITY);
+                renderer.set_paint_transform(Affine::scale(scale));
+                renderer.set_paint(Image {
+                    image: flower_image.clone(),
+                    sampler: ImageSampler {
+                        x_extend: Extend::Pad,
+                        y_extend: Extend::Pad,
+                        quality: ImageQuality::Low,
+                        alpha: 1.0,
+                    },
+                });
+                renderer.fill_rect(&Rect::new(0.0, 0.0, width, height));
+            }
+
+            renderer.flush();
+            renderer.render_to_pixmap(&mut pixmap);
+            std::hint::black_box(&pixmap);
+        });
+    });
+
+    g.finish();
+}
+
+fn load_flower_image() -> ImageSource {
+    let image_data = include_bytes!("../../../examples/assets/splash-flower.jpg");
+    let image = image::load_from_memory(image_data).expect("Failed to decode image");
+    let width = image.width();
+    let height = image.height();
+    let rgba_data = image.into_rgba8().into_vec();
+
+    #[expect(
+        clippy::cast_possible_truncation,
+        reason = "Image dimensions fit in u16"
+    )]
+    let pixmap = Pixmap::from_parts(
+        rgba_data
+            .chunks_exact(4)
+            .map(|rgba| {
+                let alpha = u16::from(rgba[3]);
+                let premultiply = |component| (alpha * u16::from(component) / 255) as u8;
+                vello_common::color::PremulRgba8 {
+                    r: premultiply(rgba[0]),
+                    g: premultiply(rgba[1]),
+                    b: premultiply(rgba[2]),
+                    a: alpha as u8,
+                }
+            })
+            .collect(),
+        width as u16,
+        height as u16,
+    );
+
+    ImageSource::Pixmap(Arc::new(pixmap))
+}
diff --git a/sparse_strips/vello_common/src/coarse.rs b/sparse_strips/vello_common/src/coarse.rs
index d4a1592..b092307 100644
--- a/sparse_strips/vello_common/src/coarse.rs
+++ b/sparse_strips/vello_common/src/coarse.rs
@@ -4,6 +4,7 @@
 //! Generating and processing wide tiles.
 
 use crate::color::palette::css::TRANSPARENT;
+use crate::encode::EncodedPaint;
 use crate::filter_effects::Filter;
 use crate::kurbo::{Affine, Rect};
 use crate::mask::Mask;
@@ -401,6 +402,7 @@
         blend_mode: BlendMode,
         thread_idx: u8,
         mask: Option<Mask>,
+        encoded_paints: &[EncodedPaint],
     ) {
         if strip_buf.is_empty() {
             return;
@@ -523,6 +525,7 @@
                         paint.clone(),
                         current_layer_id,
                         mask.clone(),
+                        encoded_paints,
                     );
                     // TODO: This bbox update might be redundant since filled regions are always
                     // bounded by strip regions (which already update the bbox). Consider removing
@@ -1194,50 +1197,73 @@
         paint: Paint,
         current_layer_id: LayerId,
         mask: Option<Mask>,
+        encoded_paints: &[EncodedPaint],
     ) {
         if !self.is_zero_clip() || self.in_clipped_filter_layer {
             match MODE {
                 MODE_CPU => {
-                    let bg = if let Paint::Solid(s) = &paint {
-                        // Note that we could be more aggressive in optimizing a whole-tile opaque fill
-                        // even with a clip stack. It would be valid to elide all drawing commands from
-                        // the enclosing clip push up to the fill. Further, we could extend the clip
-                        // push command to include a background color, rather than always starting with
-                        // a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could
-                        // be replaced with strip/fill with the color (the latter is true even with a
-                        // non-opaque color).
-                        //
-                        // However, the extra cost of tracking such optimizations may outweigh the
-                        // benefit, especially in hybrid mode with GPU painting.
-                        let can_override = x == 0
-                            && width == WideTile::WIDTH
-                            && s.is_opaque()
-                            && mask.is_none()
-                            && self.n_clip == 0
-                            && self.n_bufs == 0;
-                        can_override.then_some(*s)
-                    } else {
-                        // TODO: Implement for indexed paints.
-                        None
-                    };
+                    // Check if we can override (clear all previous commands).
+                    // This optimization applies when filling the entire tile width with an
+                    // opaque paint and no clip/mask/buffer stack.
+                    let can_override = x == 0
+                        && width == WideTile::WIDTH
+                        && mask.is_none()
+                        && self.n_clip == 0
+                        && self.n_bufs == 0;
 
-                    if let Some(bg) = bg {
-                        self.cmds.clear();
-                        self.bg = bg;
-                        // Clear layer ranges when we clear commands
-                        if let Some(ranges) = self.layer_cmd_ranges.get_mut(&current_layer_id) {
-                            ranges.clear();
+                    if can_override {
+                        match &paint {
+                            Paint::Solid(s) if s.is_opaque() => {
+                                // Note that we could be more aggressive in optimizing a whole-tile opaque fill
+                                // even with a clip stack. It would be valid to elide all drawing commands from
+                                // the enclosing clip push up to the fill. Further, we could extend the clip
+                                // push command to include a background color, rather than always starting with
+                                // a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could
+                                // be replaced with strip/fill with the color (the latter is true even with a
+                                // non-opaque color).
+                                //
+                                // However, the extra cost of tracking such optimizations may outweigh the
+                                // benefit, especially in hybrid mode with GPU painting.
+                                self.cmds.clear();
+                                self.bg = *s;
+                                if let Some(ranges) =
+                                    self.layer_cmd_ranges.get_mut(&current_layer_id)
+                                {
+                                    ranges.clear();
+                                }
+                                return;
+                            }
+                            Paint::Indexed(idx) => {
+                                // TODO: Add optimization for gradients.
+                                // Check if the indexed paint is an opaque image.
+                                if let Some(EncodedPaint::Image(img)) =
+                                    encoded_paints.get(idx.index())
+                                    && !img.has_opacities
+                                    && img.sampler.alpha == 1.0
+                                {
+                                    // Opaque image: clear previous commands but still emit the fill.
+                                    self.cmds.clear();
+                                    self.bg = PremulColor::from_alpha_color(TRANSPARENT);
+                                    if let Some(ranges) =
+                                        self.layer_cmd_ranges.get_mut(&current_layer_id)
+                                    {
+                                        ranges.clear();
+                                    }
+                                    // Fall through to emit the fill command below.
+                                }
+                            }
+                            _ => {}
                         }
-                    } else {
-                        self.record_fill_cmd(current_layer_id, self.cmds.len());
-                        self.cmds.push(Cmd::Fill(CmdFill {
-                            x,
-                            width,
-                            paint,
-                            blend_mode,
-                            mask,
-                        }));
                     }
+
+                    self.record_fill_cmd(current_layer_id, self.cmds.len());
+                    self.cmds.push(Cmd::Fill(CmdFill {
+                        x,
+                        width,
+                        paint,
+                        blend_mode,
+                        mask,
+                    }));
                 }
                 MODE_HYBRID => {
                     self.record_fill_cmd(current_layer_id, self.cmds.len());
@@ -1462,11 +1488,11 @@
     /// // 4: PopBuf
     /// ```
     #[allow(dead_code, reason = "useful for debugging")]
-    pub(crate) fn list_commands(&self) -> String {
+    pub fn list_commands(&self, encoded_paints: &[EncodedPaint]) -> String {
         self.cmds
             .iter()
             .enumerate()
-            .map(|(i, cmd)| format!("{}: {}", i, cmd.name()))
+            .map(|(i, cmd)| format!("{}: {}", i, cmd.name(encoded_paints)))
             .collect::<Vec<_>>()
             .join("\n")
     }
@@ -1564,24 +1590,57 @@
     /// in a user-friendly format.
     ///
     /// **Note:** This method is only available in debug builds (`debug_assertions`).
-    pub fn name(&self) -> &'static str {
+    pub fn name(&self, encoded_paints: &[EncodedPaint]) -> String {
         match self {
-            Self::Fill(_) => "FillPath",
-            Self::AlphaFill(_) => "AlphaFillPath",
+            Self::Fill(cmd) => format!("FillPath({})", paint_name(&cmd.paint, encoded_paints)),
+            Self::AlphaFill(cmd) => {
+                format!("AlphaFillPath({})", paint_name(&cmd.paint, encoded_paints))
+            }
             Self::PushBuf(layer_kind) => match layer_kind {
-                LayerKind::Regular(_) => "PushBuf(Regular)",
-                LayerKind::Filtered(_) => "PushBuf(Filtered)",
-                LayerKind::Clip(_) => "PushBuf(Clip)",
+                LayerKind::Regular(_) => "PushBuf(Regular)".into(),
+                LayerKind::Filtered(_) => "PushBuf(Filtered)".into(),
+                LayerKind::Clip(_) => "PushBuf(Clip)".into(),
             },
-            Self::PopBuf => "PopBuf",
-            Self::ClipFill(_) => "ClipPathFill",
-            Self::ClipStrip(_) => "ClipPathStrip",
-            Self::PushZeroClip(_) => "PushZeroClip",
-            Self::PopZeroClip => "PopZeroClip",
-            Self::Filter(_, _) => "Filter",
-            Self::Blend(_) => "Blend",
-            Self::Opacity(_) => "Opacity",
-            Self::Mask(_) => "Mask",
+            Self::PopBuf => "PopBuf".into(),
+            Self::ClipFill(_) => "ClipPathFill".into(),
+            Self::ClipStrip(_) => "ClipPathStrip".into(),
+            Self::PushZeroClip(_) => "PushZeroClip".into(),
+            Self::PopZeroClip => "PopZeroClip".into(),
+            Self::Filter(_, _) => "Filter".into(),
+            Self::Blend(_) => "Blend".into(),
+            Self::Opacity(_) => "Opacity".into(),
+            Self::Mask(_) => "Mask".into(),
+        }
+    }
+}
+
+/// Returns a human-readable description of a paint.
+#[cfg(debug_assertions)]
+fn paint_name(paint: &Paint, encoded_paints: &[EncodedPaint]) -> String {
+    match paint {
+        Paint::Solid(color) => {
+            let rgba = color.as_premul_rgba8();
+            format!(
+                "Solid(#{:02x}{:02x}{:02x}{:02x})",
+                rgba.r, rgba.g, rgba.b, rgba.a
+            )
+        }
+        Paint::Indexed(idx) => {
+            let index = idx.index();
+            if let Some(encoded) = encoded_paints.get(index) {
+                let kind = match encoded {
+                    EncodedPaint::Gradient(g) => match &g.kind {
+                        crate::encode::EncodedKind::Linear(_) => "LinearGradient",
+                        crate::encode::EncodedKind::Radial(_) => "RadialGradient",
+                        crate::encode::EncodedKind::Sweep(_) => "SweepGradient",
+                    },
+                    EncodedPaint::Image(_) => "Image",
+                    EncodedPaint::BlurredRoundedRect(_) => "BlurredRoundedRect",
+                };
+                format!("{}[{}]", kind, index)
+            } else {
+                format!("Indexed({})", index)
+            }
         }
     }
 }
@@ -1738,6 +1797,7 @@
             Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
             0,
             None,
+            &[],
         );
         wide.fill(
             10,
@@ -1746,6 +1806,7 @@
             Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
             0,
             None,
+            &[],
         );
         wide.pop_buf();
 
@@ -1761,8 +1822,8 @@
 
         let mut wide = WideTile::<MODE_CPU>::new(0, 0);
         wide.push_buf(LayerKind::Regular(0));
-        wide.fill(0, 10, BlendMode::default(), paint.clone(), 0, None);
-        wide.fill(10, 10, BlendMode::default(), paint.clone(), 0, None);
+        wide.fill(0, 10, BlendMode::default(), paint.clone(), 0, None, &[]);
+        wide.fill(10, 10, BlendMode::default(), paint.clone(), 0, None, &[]);
         wide.blend(blend_mode);
         wide.pop_buf();
 
@@ -1778,7 +1839,7 @@
 
         let mut wide = WideTile::<MODE_CPU>::new(0, 0);
         wide.push_buf(LayerKind::Regular(0));
-        wide.fill(0, 10, BlendMode::default(), paint.clone(), 0, None);
+        wide.fill(0, 10, BlendMode::default(), paint.clone(), 0, None, &[]);
         wide.blend(blend_mode);
         wide.pop_buf();
 
diff --git a/sparse_strips/vello_cpu/src/dispatch/mod.rs b/sparse_strips/vello_cpu/src/dispatch/mod.rs
index 8b84ddc..25abcd2 100644
--- a/sparse_strips/vello_cpu/src/dispatch/mod.rs
+++ b/sparse_strips/vello_cpu/src/dispatch/mod.rs
@@ -19,7 +19,13 @@
 
 pub(crate) trait Dispatcher: Debug + Send + Sync {
     fn wide(&self) -> &Wide;
-    fn generate_wide_cmd(&mut self, strip_buf: &[Strip], paint: Paint, blend_mode: BlendMode);
+    fn generate_wide_cmd(
+        &mut self,
+        strip_buf: &[Strip],
+        paint: Paint,
+        blend_mode: BlendMode,
+        encoded_paints: &[EncodedPaint],
+    );
     fn fill_path(
         &mut self,
         path: &BezPath,
@@ -29,6 +35,7 @@
         blend_mode: BlendMode,
         aliasing_threshold: Option<u8>,
         mask: Option<Mask>,
+        encoded_paints: &[EncodedPaint],
     );
     fn stroke_path(
         &mut self,
@@ -39,6 +46,7 @@
         blend_mode: BlendMode,
         aliasing_threshold: Option<u8>,
         mask: Option<Mask>,
+        encoded_paints: &[EncodedPaint],
     );
     fn push_clip_path(
         &mut self,
@@ -61,7 +69,7 @@
     );
     fn pop_layer(&mut self);
     fn reset(&mut self);
-    fn flush(&mut self);
+    fn flush(&mut self, encoded_paints: &[EncodedPaint]);
     fn rasterize(
         &self,
         buffer: &mut [u8],
diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
index 8a91a7c..88f8904 100644
--- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
+++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
@@ -270,7 +270,8 @@
             allocation_group,
         };
         task_sender.send(task).unwrap();
-        self.run_coarse(true);
+        // TODO: Support encoded_paints in multithreading.
+        self.run_coarse(true, &[]);
     }
 
     // Currently, we do coarse rasterization in two phases:
@@ -285,7 +286,7 @@
     // new strips that will be generated.
     //
     // This is why we have the `abort_empty`flag.
-    fn run_coarse(&mut self, abort_empty: bool) {
+    fn run_coarse(&mut self, abort_empty: bool, encoded_paints: &[EncodedPaint]) {
         let result_receiver = self.coarse_task_receiver.as_mut().unwrap();
 
         loop {
@@ -307,6 +308,7 @@
                                 blend_mode,
                                 thread_id,
                                 mask,
+                                encoded_paints,
                             ),
                             CoarseTaskType::RenderWideCommand {
                                 strips,
@@ -320,6 +322,7 @@
                                 blend_mode,
                                 thread_id,
                                 mask,
+                                encoded_paints,
                             ),
                             CoarseTaskType::PushLayer {
                                 thread_id,
@@ -429,6 +432,7 @@
         blend_mode: BlendMode,
         aliasing_threshold: Option<u8>,
         mask: Option<Mask>,
+        _encoded_paints: &[EncodedPaint],
     ) {
         let start = self.allocation_group.path.len() as u32;
         self.allocation_group.path.extend(path);
@@ -453,6 +457,7 @@
         blend_mode: BlendMode,
         aliasing_threshold: Option<u8>,
         mask: Option<Mask>,
+        _encoded_paints: &[EncodedPaint],
     ) {
         let start = self.allocation_group.path.len() as u32;
         self.allocation_group.path.extend(path);
@@ -541,7 +546,7 @@
         self.init();
     }
 
-    fn flush(&mut self) {
+    fn flush(&mut self, encoded_paints: &[EncodedPaint]) {
         if self.flushed {
             return;
         }
@@ -551,7 +556,7 @@
         // Note that dropping the sender will signal to the workers that no more new paths
         // can arrive.
         drop(sender);
-        self.run_coarse(false);
+        self.run_coarse(false, encoded_paints);
 
         self.alpha_storage.with_inner(|alphas| {
             // The main thread stores the alphas that are produced by playing a recording.
@@ -596,7 +601,13 @@
         }
     }
 
-    fn generate_wide_cmd(&mut self, strip_buf: &[Strip], paint: Paint, blend_mode: BlendMode) {
+    fn generate_wide_cmd(
+        &mut self,
+        strip_buf: &[Strip],
+        paint: Paint,
+        blend_mode: BlendMode,
+        _encoded_paints: &[EncodedPaint],
+    ) {
         // Note that we are essentially round-tripping here: The wide container is inside of the
         // main thread, but we first send a render task to a child thread which basically just
         // forwards it back to the main thread again. We cannot apply the wide command directly
@@ -880,8 +891,9 @@
                 BlendMode::default(),
                 None,
                 None,
+                &[],
             );
-            dispatcher.flush();
+            dispatcher.flush(&[]);
         }
 
         assert_eq!(dispatcher.allocations.paths.entries.len(), 1);
diff --git a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
index 2f6a50c..1449f41 100644
--- a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
+++ b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
@@ -403,6 +403,7 @@
         blend_mode: BlendMode,
         aliasing_threshold: Option<u8>,
         mask: Option<Mask>,
+        encoded_paints: &[EncodedPaint],
     ) {
         let wide = &mut self.wide;
 
@@ -417,7 +418,14 @@
         );
 
         // Generate coarse-level commands from strips (layer_id 0 = root layer).
-        wide.generate(&self.strip_storage.strips, paint, blend_mode, 0, mask);
+        wide.generate(
+            &self.strip_storage.strips,
+            paint,
+            blend_mode,
+            0,
+            mask,
+            encoded_paints,
+        );
     }
 
     fn stroke_path(
@@ -429,6 +437,7 @@
         blend_mode: BlendMode,
         aliasing_threshold: Option<u8>,
         mask: Option<Mask>,
+        encoded_paints: &[EncodedPaint],
     ) {
         let wide = &mut self.wide;
 
@@ -443,7 +452,14 @@
         );
 
         // Generate coarse-level commands from strips (layer_id 0 = root layer).
-        wide.generate(&self.strip_storage.strips, paint, blend_mode, 0, mask);
+        wide.generate(
+            &self.strip_storage.strips,
+            paint,
+            blend_mode,
+            0,
+            mask,
+            encoded_paints,
+        );
     }
 
     fn push_layer(
@@ -520,7 +536,7 @@
         self.layer_id_next = 0;
     }
 
-    fn flush(&mut self) {
+    fn flush(&mut self, _encoded_paints: &[EncodedPaint]) {
         // No-op for single-threaded dispatcher (no work queue to flush).
     }
 
@@ -567,9 +583,16 @@
         }
     }
 
-    fn generate_wide_cmd(&mut self, strip_buf: &[Strip], paint: Paint, blend_mode: BlendMode) {
+    fn generate_wide_cmd(
+        &mut self,
+        strip_buf: &[Strip],
+        paint: Paint,
+        blend_mode: BlendMode,
+        encoded_paints: &[EncodedPaint],
+    ) {
         // Generate coarse-level commands from pre-computed strips (layer_id 0 = root layer).
-        self.wide.generate(strip_buf, paint, blend_mode, 0, None);
+        self.wide
+            .generate(strip_buf, paint, blend_mode, 0, None, encoded_paints);
     }
 
     fn strip_storage_mut(&mut self) -> &mut StripStorage {
@@ -641,6 +664,7 @@
             BlendMode::default(),
             None,
             None,
+            &[],
         );
 
         // Ensure there is data to clear.
diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs
index 49025a5..6de8eed 100644
--- a/sparse_strips/vello_cpu/src/render.rs
+++ b/sparse_strips/vello_cpu/src/render.rs
@@ -203,6 +203,7 @@
                 ctx.blend_mode,
                 ctx.aliasing_threshold,
                 ctx.mask.clone(),
+                &ctx.encoded_paints,
             );
         });
     }
@@ -219,6 +220,7 @@
                 ctx.blend_mode,
                 ctx.aliasing_threshold,
                 ctx.mask.clone(),
+                &ctx.encoded_paints,
             );
         });
     }
@@ -236,6 +238,7 @@
                 ctx.blend_mode,
                 ctx.aliasing_threshold,
                 ctx.mask.clone(),
+                &ctx.encoded_paints,
             );
         });
     }
@@ -253,6 +256,7 @@
                 ctx.blend_mode,
                 ctx.aliasing_threshold,
                 ctx.mask.clone(),
+                &ctx.encoded_paints,
             );
         });
     }
@@ -307,6 +311,7 @@
             self.blend_mode,
             self.aliasing_threshold,
             self.mask.clone(),
+            &self.encoded_paints,
         );
     }
 
@@ -546,7 +551,7 @@
     /// For multi-threaded rendering, you _have_ to call this before rasterizing, otherwise
     /// the program will panic.
     pub fn flush(&mut self) {
-        self.dispatcher.flush();
+        self.dispatcher.flush(&self.encoded_paints);
     }
 
     /// Render the current context into a buffer.
@@ -630,6 +635,7 @@
                     self.blend_mode,
                     self.aliasing_threshold,
                     self.mask.clone(),
+                    &self.encoded_paints,
                 );
             }
             GlyphType::Bitmap(glyph) => {
@@ -737,6 +743,7 @@
                     self.blend_mode,
                     self.aliasing_threshold,
                     self.mask.clone(),
+                    &self.encoded_paints,
                 );
             }
             GlyphType::Bitmap(_) | GlyphType::Colr(_) => {
@@ -1051,8 +1058,12 @@
             "Invalid strip range"
         );
         let paint = self.encode_current_paint();
-        self.dispatcher
-            .generate_wide_cmd(&adjusted_strips[start..end], paint, self.blend_mode);
+        self.dispatcher.generate_wide_cmd(
+            &adjusted_strips[start..end],
+            paint,
+            self.blend_mode,
+            &self.encoded_paints,
+        );
     }
 
     /// Prepare cached strips for rendering by adjusting indices.
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs
index f6226d7..d755ce5 100644
--- a/sparse_strips/vello_hybrid/src/scene.rs
+++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -228,7 +228,14 @@
             &mut self.strip_storage,
             self.clip_context.get(),
         );
-        wide.generate(&self.strip_storage.strips, paint, self.blend_mode, 0, None);
+        wide.generate(
+            &self.strip_storage.strips,
+            paint,
+            self.blend_mode,
+            0,
+            None,
+            &self.encoded_paints,
+        );
     }
 
     /// Push a new clip path to the clip stack.
@@ -287,7 +294,14 @@
             self.clip_context.get(),
         );
 
-        wide.generate(&self.strip_storage.strips, paint, self.blend_mode, 0, None);
+        wide.generate(
+            &self.strip_storage.strips,
+            paint,
+            self.blend_mode,
+            0,
+            None,
+            &self.encoded_paints,
+        );
     }
 
     /// Set the aliasing threshold.
@@ -751,6 +765,7 @@
             self.blend_mode,
             0,
             None,
+            &self.encoded_paints,
         );
     }
 
diff --git a/sparse_strips/vello_toy/src/debug.rs b/sparse_strips/vello_toy/src/debug.rs
index 857ad26..4a22d13 100644
--- a/sparse_strips/vello_toy/src/debug.rs
+++ b/sparse_strips/vello_toy/src/debug.rs
@@ -94,6 +94,7 @@
             BlendMode::new(Mix::Normal, Compose::SrcOver),
             0,
             None,
+            &[],
         );
     }