From ee6805518473d0766ca20e75884e801d91d304f2 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 3 Oct 2026 18:57:23 +0000 Subject: [PATCH 1/3] vello_cpu/vello_hybrid: use rect fast paths for rect fills and clips When the shape passed to fill, push_clip_layer or push_layer is a Rect (Shape::as_rect), call fill_rect / push_clip_rect instead of converting it to a path, so the sparse strip renderers can use their rectangle fast paths. --- crates/anyrender_vello_cpu/src/scene.rs | 80 +++++++++++++++++-- crates/anyrender_vello_hybrid/src/scene.rs | 22 +++-- .../anyrender_vello_hybrid/src/webgl_scene.rs | 22 +++-- 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/crates/anyrender_vello_cpu/src/scene.rs b/crates/anyrender_vello_cpu/src/scene.rs index 1b09a33..584ac40 100644 --- a/crates/anyrender_vello_cpu/src/scene.rs +++ b/crates/anyrender_vello_cpu/src/scene.rs @@ -10,10 +10,16 @@ use crate::image_cache::{ImageCache, ImageCacheConfig}; const DEFAULT_TOLERANCE: f64 = 0.1; +enum LayerKind { + Layer, + ClipRect, +} + pub struct VelloCpuScenePainter { pub(crate) render_ctx: vello_cpu::RenderContext, pub(crate) resources: vello_cpu::Resources, pub(crate) image_cache: ImageCache, + layer_stack: Vec, } impl VelloCpuScenePainter { @@ -26,6 +32,7 @@ impl VelloCpuScenePainter { render_ctx: vello_cpu::RenderContext::new(width, height), resources: vello_cpu::Resources::new(), image_cache: ImageCache::new(config), + layer_stack: Vec::new(), } } @@ -68,6 +75,7 @@ impl RenderContext for VelloCpuScenePainter {} impl PaintScene for VelloCpuScenePainter { fn reset(&mut self) { self.render_ctx.reset(); + self.layer_stack.clear(); } fn push_layer( @@ -93,6 +101,7 @@ impl PaintScene for VelloCpuScenePainter { self.render_ctx.set_transform(transform); self.render_ctx.set_fill_rule(fill); + self.layer_stack.push(LayerKind::Layer); self.render_ctx.push_layer( Some(&clip.into_path(DEFAULT_TOLERANCE)), Some(blend.into()), @@ -105,12 +114,22 @@ impl PaintScene for VelloCpuScenePainter { fn push_clip_layer(&mut self, fill: Fill, transform: Affine, clip: &impl Shape) { self.render_ctx.set_transform(transform); self.render_ctx.set_fill_rule(fill); - self.render_ctx - .push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = clip.as_rect() { + self.layer_stack.push(LayerKind::ClipRect); + self.render_ctx.push_clip_rect(&rect); + } else { + self.layer_stack.push(LayerKind::Layer); + self.render_ctx + .push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); + } } fn pop_layer(&mut self) { - self.render_ctx.pop_layer(); + match self.layer_stack.pop() { + Some(LayerKind::Layer) => self.render_ctx.pop_layer(), + Some(LayerKind::ClipRect) => self.render_ctx.pop_clip(), + None => {} + } } fn stroke<'a>( @@ -145,8 +164,12 @@ impl PaintScene for VelloCpuScenePainter { self.render_ctx.set_paint(paint); self.render_ctx .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); - self.render_ctx - .fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = shape.as_rect() { + self.render_ctx.fill_rect(&rect); + } else { + self.render_ctx + .fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + } } fn draw_glyphs<'a, 's: 'a>( @@ -287,6 +310,53 @@ mod clip_rule_tests { assert_pixels(true, false); } + #[test] + fn rect_clips_and_fills() { + let buffer = render_to_buffer::( + |scene| { + scene.push_layer( + Fill::NonZero, + Mix::Normal, + 1.0, + Affine::IDENTITY, + &Rect::new(0.0, 0.0, 80.0, 100.0), + None, + None, + ); + scene.push_clip_layer( + Fill::NonZero, + Affine::translate((20.0, 20.0)), + &Rect::new(0.0, 0.0, 80.0, 60.0), + ); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + RED, + None, + &Rect::new(0.0, 0.0, 100.0, 100.0), + ); + scene.pop_layer(); + scene.pop_layer(); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + RED, + None, + &Rect::new(90.0, 90.0, 100.0, 100.0), + ); + }, + 100, + 100, + ); + let pixel = |x: usize, y: usize| &buffer[(y * 100 + x) * 4..(y * 100 + x) * 4 + 4]; + assert_eq!(pixel(50, 50), &[255, 0, 0, 255]); + assert_eq!(pixel(10, 50), &[0, 0, 0, 0]); + assert_eq!(pixel(50, 10), &[0, 0, 0, 0]); + assert_eq!(pixel(50, 90), &[0, 0, 0, 0]); + assert_eq!(pixel(85, 50), &[0, 0, 0, 0]); + assert_eq!(pixel(95, 95), &[255, 0, 0, 255]); + } + #[test] fn scene_replay_preserves_clip_rules() { assert_pixels(false, true); diff --git a/crates/anyrender_vello_hybrid/src/scene.rs b/crates/anyrender_vello_hybrid/src/scene.rs index e39aa5f..df7784a 100644 --- a/crates/anyrender_vello_hybrid/src/scene.rs +++ b/crates/anyrender_vello_hybrid/src/scene.rs @@ -210,8 +210,12 @@ impl PaintScene for VelloHybridScenePainter<'_> { self.scene.set_transform(transform); self.scene.set_fill_rule(fill); self.layer_stack.push(LayerKind::Layer); - self.scene - .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = clip.as_rect() { + self.scene.push_clip_rect(&rect); + } else { + self.scene + .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + } self.scene .push_layer(None, Some(blend.into()), Some(alpha), None, filter); } @@ -220,8 +224,12 @@ impl PaintScene for VelloHybridScenePainter<'_> { self.scene.set_transform(transform); self.scene.set_fill_rule(fill); self.layer_stack.push(LayerKind::Clip); - self.scene - .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = clip.as_rect() { + self.scene.push_clip_rect(&rect); + } else { + self.scene + .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + } } fn pop_layer(&mut self) { @@ -267,7 +275,11 @@ impl PaintScene for VelloHybridScenePainter<'_> { self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); - self.scene.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = shape.as_rect() { + self.scene.fill_rect(&rect); + } else { + self.scene.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + } } fn draw_glyphs<'a, 's: 'a>( diff --git a/crates/anyrender_vello_hybrid/src/webgl_scene.rs b/crates/anyrender_vello_hybrid/src/webgl_scene.rs index 58d9f43..8758dcf 100644 --- a/crates/anyrender_vello_hybrid/src/webgl_scene.rs +++ b/crates/anyrender_vello_hybrid/src/webgl_scene.rs @@ -122,8 +122,12 @@ impl PaintScene for WebGlScenePainter<'_> { self.scene.set_transform(transform); self.scene.set_fill_rule(fill); self.layer_stack.push(LayerKind::Layer); - self.scene - .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = clip.as_rect() { + self.scene.push_clip_rect(&rect); + } else { + self.scene + .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + } self.scene .push_layer(None, Some(blend.into()), Some(alpha), None, filter); } @@ -132,8 +136,12 @@ impl PaintScene for WebGlScenePainter<'_> { self.scene.set_transform(transform); self.scene.set_fill_rule(fill); self.layer_stack.push(LayerKind::Clip); - self.scene - .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = clip.as_rect() { + self.scene.push_clip_rect(&rect); + } else { + self.scene + .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); + } } fn pop_layer(&mut self) { @@ -179,7 +187,11 @@ impl PaintScene for WebGlScenePainter<'_> { self.scene.set_paint(paint); self.scene .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); - self.scene.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + if let Some(rect) = shape.as_rect() { + self.scene.fill_rect(&rect); + } else { + self.scene.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); + } } fn draw_glyphs<'a, 's2: 'a>( From 190b5d8d1912d8c40a3f325d37dd2de81b5d27d0 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 3 Oct 2026 20:00:37 +0000 Subject: [PATCH 2/3] vello_cpu: use non-isolated push_clip_path for push_clip_layer --- crates/anyrender_vello_cpu/src/scene.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/anyrender_vello_cpu/src/scene.rs b/crates/anyrender_vello_cpu/src/scene.rs index 584ac40..1d27ec8 100644 --- a/crates/anyrender_vello_cpu/src/scene.rs +++ b/crates/anyrender_vello_cpu/src/scene.rs @@ -12,7 +12,7 @@ const DEFAULT_TOLERANCE: f64 = 0.1; enum LayerKind { Layer, - ClipRect, + Clip, } pub struct VelloCpuScenePainter { @@ -114,20 +114,19 @@ impl PaintScene for VelloCpuScenePainter { fn push_clip_layer(&mut self, fill: Fill, transform: Affine, clip: &impl Shape) { self.render_ctx.set_transform(transform); self.render_ctx.set_fill_rule(fill); + self.layer_stack.push(LayerKind::Clip); if let Some(rect) = clip.as_rect() { - self.layer_stack.push(LayerKind::ClipRect); self.render_ctx.push_clip_rect(&rect); } else { - self.layer_stack.push(LayerKind::Layer); self.render_ctx - .push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); + .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); } } fn pop_layer(&mut self) { match self.layer_stack.pop() { Some(LayerKind::Layer) => self.render_ctx.pop_layer(), - Some(LayerKind::ClipRect) => self.render_ctx.pop_clip(), + Some(LayerKind::Clip) => self.render_ctx.pop_clip(), None => {} } } From fb797365fab690e461e71809263a9a793d10cdee Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 3 Oct 2026 20:15:04 +0000 Subject: [PATCH 3/3] vello_cpu: keep isolated clip layers for non-rect clips when multi-threaded --- crates/anyrender_vello_cpu/src/scene.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/anyrender_vello_cpu/src/scene.rs b/crates/anyrender_vello_cpu/src/scene.rs index 1d27ec8..58de81c 100644 --- a/crates/anyrender_vello_cpu/src/scene.rs +++ b/crates/anyrender_vello_cpu/src/scene.rs @@ -114,10 +114,17 @@ impl PaintScene for VelloCpuScenePainter { fn push_clip_layer(&mut self, fill: Fill, transform: Affine, clip: &impl Shape) { self.render_ctx.set_transform(transform); self.render_ctx.set_fill_rule(fill); - self.layer_stack.push(LayerKind::Clip); if let Some(rect) = clip.as_rect() { + self.layer_stack.push(LayerKind::Clip); self.render_ctx.push_clip_rect(&rect); + } else if self.render_ctx.is_multi_threaded() { + // The multi-threaded dispatcher flushes its pending work and rasterizes the clip on + // the main thread for every `push_clip_path`, which is slower than an isolated layer. + self.layer_stack.push(LayerKind::Layer); + self.render_ctx + .push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); } else { + self.layer_stack.push(LayerKind::Clip); self.render_ctx .push_clip_path(&clip.into_path(DEFAULT_TOLERANCE)); }