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(¤t_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(¤t_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(¤t_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, + &[], ); }