vello_sparse_strips: Optimize pixel-aligned rect filling by bypassing path pipeline
diff --git a/sparse_strips/vello_bench/benches/main.rs b/sparse_strips/vello_bench/benches/main.rs index b9f50d4..ec5a1f0 100644 --- a/sparse_strips/vello_bench/benches/main.rs +++ b/sparse_strips/vello_bench/benches/main.rs
@@ -18,11 +18,13 @@ criterion_group!(flatten, flatten::flatten); criterion_group!(strokes, flatten::strokes); criterion_group!(render_strips, strip::render_strips); +criterion_group!(render_rect, strip::render_rect); criterion_group!(glyph, glyph::glyph); criterion_group!(integration_bench, integration::images); criterion_main!( tile, render_strips, + render_rect, flatten, strokes, glyph,
diff --git a/sparse_strips/vello_bench/src/strip.rs b/sparse_strips/vello_bench/src/strip.rs index 8690941..484fa7f 100644 --- a/sparse_strips/vello_bench/src/strip.rs +++ b/sparse_strips/vello_bench/src/strip.rs
@@ -4,7 +4,9 @@ use crate::data::get_data_items; use criterion::Criterion; use vello_common::fearless_simd::Level; +use vello_common::kurbo::{Affine, Rect, Shape}; use vello_common::peniko::Fill; +use vello_common::strip_generator::{StripGenerator, StripStorage}; pub fn render_strips(c: &mut Criterion) { let mut g = c.benchmark_group("render_strips"); @@ -47,3 +49,48 @@ } } } + +pub fn render_rect(c: &mut Criterion) { + let mut g = c.benchmark_group("render_rect"); + g.sample_size(50); + + let rect = Rect::new(10.0, 10.0, 24.0, 24.0); + let width = 100; + let height = 100; + let level = Level::new(); + + // Benchmark: generate_filled_path (path-based approach) + g.bench_function("14x14_via_path", |b| { + let mut generator = StripGenerator::new(width, height, level); + let mut storage = StripStorage::default(); + + b.iter(|| { + storage.clear(); + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage, + None, + ); + generator.reset(); + std::hint::black_box(&storage); + }); + }); + + // Benchmark: generate_rect_strips_with_clip (optimized rect approach) + g.bench_function("14x14_via_rect", |b| { + let mut generator = StripGenerator::new(width, height, level); + let mut storage = StripStorage::default(); + + b.iter(|| { + storage.clear(); + generator.generate_filled_rect_fast(&rect, &mut storage, None); + generator.reset(); + std::hint::black_box(&storage); + }); + }); + + g.finish(); +}
diff --git a/sparse_strips/vello_common/src/strip.rs b/sparse_strips/vello_common/src/strip.rs index a4c297b..bc2c9ce 100644 --- a/sparse_strips/vello_common/src/strip.rs +++ b/sparse_strips/vello_common/src/strip.rs
@@ -4,6 +4,9 @@ //! Rendering strips. use crate::flatten::Line; +use crate::kurbo::Rect; +#[cfg(not(feature = "std"))] +use crate::kurbo::common::FloatFuncs as _; use crate::peniko::Fill; use crate::tile::{Tile, Tiles}; use crate::util::f32_to_u8; @@ -431,3 +434,184 @@ accumulated_winding += acc; } } + +/// Render a pixel-aligned rectangle directly into strips. +/// +/// This bypasses the full path processing pipeline (flatten → tiles → strips) +/// by directly creating strip coverage data for the rectangle. +/// +/// The rect bounds should already be clamped to the viewport. +pub fn render_rect_fast( + level: Level, + rect: Rect, + strip_buf: &mut Vec<Strip>, + alpha_buf: &mut Vec<u8>, +) { + dispatch!(level, simd => render_rect_fast_impl(simd, rect, strip_buf, alpha_buf)); +} + +/// Generates strip data for a pixel-aligned rectangle. +/// +/// # Strip layout strategy +/// +/// Tile rows are classified into two kinds: +/// +/// - **Edge rows** (top/bottom of rect): the rect boundary crosses partway +/// through the tile vertically, so individual pixels need per-cell alpha. +/// We emit a *single wide strip* spanning all tile columns, with alpha = +/// `x_mask & y_mask` (each is 0x00 or 0xFF, so AND gives the intersection). +/// +/// - **Interior rows**: every pixel in the tile has full vertical coverage, +/// so we only need to handle the left and right partial-column edges. +/// We emit a **left edge strip** (with its x-alpha mask) and, when the rect +/// spans more than one tile column, a **right edge strip** with `fill_gap = +/// true` so the renderer fills solid 0xFF between them. +/// +/// The x-alpha masks for the left/right edge tiles are y-independent, so they +/// are precomputed once and reused across all interior rows. +// TODO: Consider extending this to handle arbitrary axis-aligned rectangles (with fractional +// coordinates) by computing partial coverage alpha values for edge pixels instead of +// binary 0/255. +fn render_rect_fast_impl<S: Simd>( + s: S, + rect: Rect, + strip_buf: &mut Vec<Strip>, + alpha_buf: &mut Vec<u8>, +) { + let rect_x0 = rect.x0.floor() as u16; + let rect_y0 = rect.y0.floor() as u16; + let rect_x1 = rect.x1.ceil() as u16; + let rect_y1 = rect.y1.ceil() as u16; + + let left_tile_x = (rect_x0 / Tile::WIDTH) * Tile::WIDTH; + let right_tile_x = (rect_x1 / Tile::WIDTH) * Tile::WIDTH; + + let y0 = (rect_y0 / Tile::HEIGHT) * Tile::HEIGHT; + let y1 = (rect_y1.saturating_add(Tile::HEIGHT - 1) / Tile::HEIGHT) * Tile::HEIGHT; + // Include one tile past the right edge so the right-edge tile column is + // covered by the edge-row wide-strip loop. + let x_end = right_tile_x.saturating_add(Tile::WIDTH); + + if x_end <= left_tile_x || y1 <= y0 { + return; + } + + let tile_start_y = y0 / Tile::HEIGHT; + let tile_end_y = y1 / Tile::HEIGHT; + + let (max_strips, max_alphas) = + calc_rect_reserve_capacity(x_end - left_tile_x, tile_end_y - tile_start_y); + strip_buf.reserve(max_strips); + alpha_buf.reserve(max_alphas); + + // A right strip is only needed when the rect spans more than one tile column. + let needs_right_strip = right_tile_x > left_tile_x; + + let left_x_mask = x_alpha_tile(s, left_tile_x, rect_x0, rect_x1); + let right_x_mask = x_alpha_tile(s, right_tile_x, rect_x0, rect_x1); + + for tile_y in tile_start_y..tile_end_y { + let strip_y = tile_y * Tile::HEIGHT; + + // A row is an "edge" if the rect's top or bottom boundary falls + // *inside* it (i.e. partial vertical coverage). + let is_top_edge = strip_y < rect_y0 && rect_y0 < strip_y + Tile::HEIGHT; + let is_bottom_edge = strip_y < rect_y1 && rect_y1 < strip_y + Tile::HEIGHT; + + if is_top_edge || is_bottom_edge { + let alpha_start = alpha_buf.len() as u32; + let y_mask = y_alpha_tile(s, strip_y, rect_y0, rect_y1); + + // Walk every tile column, AND the per-column x-mask with the + // per-row y-mask to get the final per-pixel alpha. + let mut col = left_tile_x; + while col + Tile::WIDTH <= x_end { + let combined = x_alpha_tile(s, col, rect_x0, rect_x1) & y_mask; + alpha_buf.extend_from_slice(combined.as_slice()); + col += Tile::WIDTH; + } + + strip_buf.push(Strip::new(left_tile_x, strip_y, alpha_start, false)); + } else { + let alpha_start = alpha_buf.len() as u32; + alpha_buf.extend_from_slice(left_x_mask.as_slice()); + strip_buf.push(Strip::new(left_tile_x, strip_y, alpha_start, false)); + + if needs_right_strip { + // `fill_gap = true` tells the renderer to fill solid 0xFF + // between the previous strip's end and this strip's start. + let alpha_start = alpha_buf.len() as u32; + alpha_buf.extend_from_slice(right_x_mask.as_slice()); + strip_buf.push(Strip::new(right_tile_x, strip_y, alpha_start, true)); + } + } + } + + // Sentinel strip: marks the end of the strip list for this shape. + let last_strip_y = (tile_end_y - 1) * Tile::HEIGHT; + strip_buf.push(Strip::new( + u16::MAX, + last_strip_y, + alpha_buf.len() as u32, + false, + )); +} + +/// Build a column-major x-alpha mask for one tile-width of columns. +/// +/// Each column gets `Tile::HEIGHT` lanes, all 0x00 or all 0xFF depending on +/// whether the column falls inside `[rect_x0, rect_x1)`. +#[inline(always)] +fn x_alpha_tile<S: Simd>(s: S, tile_x: u16, rect_x0: u16, rect_x1: u16) -> u8x16<S> { + let mut buf = [0_u8; 16]; + for col in 0..Tile::WIDTH { + let px = tile_x + col; + let alpha = if px >= rect_x0 && px < rect_x1 { + 255 + } else { + 0 + }; + let base = (col * Tile::HEIGHT) as usize; + buf[base..base + Tile::HEIGHT as usize].fill(alpha); + } + u8x16::from_slice(s, &buf) +} + +/// Build a column-major y-alpha mask for one tile row. +/// +/// Each of the `Tile::HEIGHT` rows is 0x00 or 0xFF depending on whether it +/// falls inside `[rect_y0, rect_y1)`. The pattern is identical across all +/// `Tile::WIDTH` columns. +#[inline(always)] +fn y_alpha_tile<S: Simd>(s: S, strip_y: u16, rect_y0: u16, rect_y1: u16) -> u8x16<S> { + let mut y_mask = [0_u8; 4]; + for row in 0..Tile::HEIGHT { + let py = strip_y + row; + y_mask[row as usize] = if py >= rect_y0 && py < rect_y1 { + 255 + } else { + 0 + }; + } + let mut buf = [0_u8; 16]; + for col in 0..Tile::WIDTH as usize { + let base = col * Tile::HEIGHT as usize; + buf[base..base + Tile::HEIGHT as usize].copy_from_slice(&y_mask); + } + u8x16::from_slice(s, &buf) +} + +/// Calculate the maximum buffer capacity needed for rendering a pixel-aligned rect. +/// +/// Returns `(max_strips, max_alphas)` based on the tile dimensions covered. +#[inline] +fn calc_rect_reserve_capacity(x_span: u16, tile_rows: u16) -> (usize, usize) { + let tile_cols = (x_span / Tile::WIDTH) as usize; + let tile_rows = tile_rows as usize; + // Max strips: 2 per row (left + right) + 1 sentinel + let max_strips = tile_rows * 2 + 1; + // Max alphas: worst case is edge rows covering all columns + // Each column needs Tile::HEIGHT bytes + let max_alphas = tile_cols * tile_rows * Tile::WIDTH as usize * Tile::HEIGHT as usize; + (max_strips, max_alphas) +}
diff --git a/sparse_strips/vello_common/src/strip_generator.rs b/sparse_strips/vello_common/src/strip_generator.rs index 2441431..9a05613 100644 --- a/sparse_strips/vello_common/src/strip_generator.rs +++ b/sparse_strips/vello_common/src/strip_generator.rs
@@ -6,7 +6,7 @@ use crate::clip::{PathDataRef, intersect}; use crate::fearless_simd::Level; use crate::flatten::{FlattenCtx, Line}; -use crate::kurbo::{Affine, PathEl, Stroke}; +use crate::kurbo::{Affine, PathEl, Rect, Stroke}; use crate::peniko::Fill; use crate::strip::Strip; use crate::tile::Tiles; @@ -175,6 +175,56 @@ } } + /// Generate strips directly for a pixel-aligned rectangle. + /// + /// This bypasses the full path processing pipeline (flatten -> tiles -> strips) + /// by directly creating strip coverage data for the rectangle. + pub fn generate_filled_rect_fast( + &mut self, + rect: &Rect, + strip_storage: &mut StripStorage, + clip_path: Option<PathDataRef<'_>>, + ) { + if strip_storage.generation_mode == GenerationMode::Replace { + strip_storage.strips.clear(); + } + + // Clamp rect to viewport bounds. + let viewport = Rect::new(0.0, 0.0, self.width as f64, self.height as f64); + let clamped = rect.intersect(viewport); + + // Early exit if clamped rect is empty (entirely outside viewport or degenerate). + if clamped.is_zero_area() { + return; + } + + // When clipping is active, generate rect strips into temp_storage first, + // then intersect with the clip path into strip_storage. + if let Some(clip_data) = clip_path { + self.temp_storage.clear(); + + strip::render_rect_fast( + self.level, + clamped, + &mut self.temp_storage.strips, + &mut self.temp_storage.alphas, + ); + let rect_data = PathDataRef { + strips: &self.temp_storage.strips, + alphas: &self.temp_storage.alphas, + }; + + intersect(self.level, clip_data, rect_data, strip_storage); + } else { + strip::render_rect_fast( + self.level, + clamped, + &mut strip_storage.strips, + &mut strip_storage.alphas, + ); + } + } + /// Reset the strip generator. pub fn reset(&mut self) { self.line_buf.clear(); @@ -214,4 +264,186 @@ assert!(generator.line_buf.is_empty()); assert!(storage.is_empty()); } + + /// Helper to compare strip storage results + fn assert_strips_equal(expected: &StripStorage, actual: &StripStorage, test_name: &str) { + assert_eq!( + expected.strips, actual.strips, + "{}: strips mismatch", + test_name + ); + assert_eq!( + expected.alphas, actual.alphas, + "{}: alphas mismatch", + test_name + ); + } + + #[test] + fn rect_small_single_tile() { + // Small rect within a single tile (4x4) + let rect = Rect::new(1.0, 1.0, 3.0, 3.0); + let mut generator = StripGenerator::new(100, 100, Level::fallback()); + + let mut storage_path = StripStorage::default(); + let mut storage_rect = StripStorage::default(); + + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage_path, + None, + ); + generator.reset(); + + generator.generate_filled_rect_fast(&rect, &mut storage_rect, None); + + assert_strips_equal(&storage_path, &storage_rect, "small_single_tile"); + } + + #[test] + fn rect_spanning_multiple_tiles_horizontally() { + // Rect spanning multiple tiles horizontally (Tile::WIDTH = 4) + let rect = Rect::new(2.0, 1.0, 14.0, 3.0); + let mut generator = StripGenerator::new(100, 100, Level::fallback()); + + let mut storage_path = StripStorage::default(); + let mut storage_rect = StripStorage::default(); + + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage_path, + None, + ); + generator.reset(); + + generator.generate_filled_rect_fast(&rect, &mut storage_rect, None); + + assert_strips_equal(&storage_path, &storage_rect, "spanning_horizontal"); + } + + #[test] + fn rect_spanning_multiple_tiles_vertically() { + // Rect spanning multiple tiles vertically (Tile::HEIGHT = 4) + let rect = Rect::new(1.0, 2.0, 3.0, 14.0); + let mut generator = StripGenerator::new(100, 100, Level::fallback()); + + let mut storage_path = StripStorage::default(); + let mut storage_rect = StripStorage::default(); + + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage_path, + None, + ); + generator.reset(); + + generator.generate_filled_rect_fast(&rect, &mut storage_rect, None); + + assert_strips_equal(&storage_path, &storage_rect, "spanning_vertical"); + } + + #[test] + fn rect_spanning_multiple_tiles_both_directions() { + // Rect spanning multiple tiles in both directions + let rect = Rect::new(2.0, 2.0, 18.0, 18.0); + let mut generator = StripGenerator::new(100, 100, Level::fallback()); + + let mut storage_path = StripStorage::default(); + let mut storage_rect = StripStorage::default(); + + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage_path, + None, + ); + generator.reset(); + + generator.generate_filled_rect_fast(&rect, &mut storage_rect, None); + + assert_strips_equal(&storage_path, &storage_rect, "spanning_both"); + } + + #[test] + fn rect_tile_aligned() { + // Rect aligned to tile boundaries (4x4 tiles) + let rect = Rect::new(0.0, 0.0, 8.0, 8.0); + let mut generator = StripGenerator::new(100, 100, Level::fallback()); + + let mut storage_path = StripStorage::default(); + let mut storage_rect = StripStorage::default(); + + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage_path, + None, + ); + generator.reset(); + + generator.generate_filled_rect_fast(&rect, &mut storage_rect, None); + + assert_strips_equal(&storage_path, &storage_rect, "tile_aligned"); + } + + #[test] + fn rect_one_pixel_wide() { + // Very thin rect (1 pixel wide) + let rect = Rect::new(5.0, 2.0, 6.0, 12.0); + let mut generator = StripGenerator::new(100, 100, Level::fallback()); + + let mut storage_path = StripStorage::default(); + let mut storage_rect = StripStorage::default(); + + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage_path, + None, + ); + generator.reset(); + + generator.generate_filled_rect_fast(&rect, &mut storage_rect, None); + + assert_strips_equal(&storage_path, &storage_rect, "one_pixel_wide"); + } + + #[test] + fn rect_one_pixel_tall() { + // Very thin rect (1 pixel tall) + let rect = Rect::new(2.0, 5.0, 12.0, 6.0); + let mut generator = StripGenerator::new(100, 100, Level::fallback()); + + let mut storage_path = StripStorage::default(); + let mut storage_rect = StripStorage::default(); + + generator.generate_filled_path( + rect.to_path(0.1), + Fill::NonZero, + Affine::IDENTITY, + None, + &mut storage_path, + None, + ); + generator.reset(); + + generator.generate_filled_rect_fast(&rect, &mut storage_rect, None); + + assert_strips_equal(&storage_path, &storage_rect, "one_pixel_tall"); + } }
diff --git a/sparse_strips/vello_common/src/util.rs b/sparse_strips/vello_common/src/util.rs index 16477bc..0d8836f 100644 --- a/sparse_strips/vello_common/src/util.rs +++ b/sparse_strips/vello_common/src/util.rs
@@ -6,9 +6,9 @@ use fearless_simd::{ Bytes, Simd, SimdBase, SimdFloat, f32x16, u8x16, u8x32, u16x16, u16x32, u32x16, }; -use peniko::kurbo::Affine; #[cfg(not(feature = "std"))] use peniko::kurbo::common::FloatFuncs as _; +use peniko::kurbo::{Affine, Rect}; /// Convert f32x16 to u8x16. #[inline(always)] @@ -61,6 +61,33 @@ (S::widen_u8x16(a.simd, a) * S::widen_u8x16(b.simd, b)).div_255() } +/// Check if an affine transform is a pure integer translation. +/// +/// Returns true if the transform only contains integer translation (no rotation, +/// skew, or scaling), meaning rectangles will remain pixel-aligned after transformation. +#[inline] +pub fn is_integer_translation(transform: &Affine) -> bool { + let [a, b, c, d, e, f] = transform.as_coeffs(); + (a - 1.0).abs() < 1e-9 + && b.abs() < 1e-9 + && c.abs() < 1e-9 + && (d - 1.0).abs() < 1e-9 + && (e - e.round()).abs() < 1e-9 + && (f - f.round()).abs() < 1e-9 +} + +/// Check if rect coordinates are all integers (no fractional parts). +/// +/// The optimized rect path doesn't handle anti-aliasing for fractional edges, +/// so non-integer coordinates require path-based rendering. +#[inline] +pub fn is_integer_rect(rect: &Rect) -> bool { + (rect.x0 - rect.x0.round()).abs() < 1e-9 + && (rect.y0 - rect.y0.round()).abs() < 1e-9 + && (rect.x1 - rect.x1.round()).abs() < 1e-9 + && (rect.y1 - rect.y1.round()).abs() < 1e-9 +} + /// Extract scale factors from an affine transform using singular value decomposition. /// /// Returns a tuple of (`scale_x`, `scale_y`) representing the scale along each axis.
diff --git a/sparse_strips/vello_cpu/src/dispatch/mod.rs b/sparse_strips/vello_cpu/src/dispatch/mod.rs index 63aa85d..02011ac 100644 --- a/sparse_strips/vello_cpu/src/dispatch/mod.rs +++ b/sparse_strips/vello_cpu/src/dispatch/mod.rs
@@ -6,7 +6,7 @@ pub(crate) mod single_threaded; use crate::RenderMode; -use crate::kurbo::{Affine, BezPath, Stroke}; +use crate::kurbo::{Affine, BezPath, Rect, Stroke}; use crate::peniko::{BlendMode, Fill}; use core::fmt::Debug; use vello_common::coarse::Wide; @@ -48,6 +48,15 @@ mask: Option<Mask>, encoded_paints: &[EncodedPaint], ); + /// Fill a pixel-aligned rectangle with the current paint. + fn fill_rect_fast( + &mut self, + rect: &Rect, + paint: Paint, + blend_mode: BlendMode, + mask: Option<Mask>, + encoded_paints: &[EncodedPaint], + ); fn push_clip_path( &mut self, path: &BezPath,
diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs index c2137be..8bff1ce 100644 --- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
@@ -6,7 +6,7 @@ use crate::dispatch::multi_threaded::cost::{COST_THRESHOLD, estimate_render_task_cost}; use crate::dispatch::multi_threaded::worker::Worker; use crate::fine::{Fine, FineKernel}; -use crate::kurbo::{Affine, BezPath, PathEl, Stroke}; +use crate::kurbo::{Affine, BezPath, PathEl, Rect, Shape, Stroke}; use crate::peniko::{BlendMode, Fill}; use crate::region::Regions; use alloc::boxed::Box; @@ -476,6 +476,31 @@ }); } + fn fill_rect_fast( + &mut self, + rect: &Rect, + paint: Paint, + blend_mode: BlendMode, + mask: Option<Mask>, + _encoded_paints: &[EncodedPaint], + ) { + // For multi-threaded, fall back to path-based rendering. + // TODO: Implement optimized rect strip generation in worker threads. + let path = rect.to_path(0.0); + let start = self.allocation_group.path.len() as u32; + self.allocation_group.path.extend(&path); + let end = self.allocation_group.path.len() as u32; + self.register_task(RenderTaskType::FillPath { + path_range: start..end, + transform: Affine::IDENTITY, + paint, + fill_rule: Fill::NonZero, + blend_mode, + aliasing_threshold: None, + mask, + }); + } + fn push_layer( &mut self, clip_path: Option<&BezPath>,
diff --git a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs index 1467db2..73bcba1 100644 --- a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
@@ -4,7 +4,7 @@ use crate::RenderMode; use crate::dispatch::Dispatcher; use crate::fine::{Fine, FineKernel}; -use crate::kurbo::{Affine, BezPath, Stroke}; +use crate::kurbo::{Affine, BezPath, Rect, Stroke}; use crate::layer_manager::LayerManager; use crate::peniko::{BlendMode, Fill}; use crate::region::Regions; @@ -566,6 +566,34 @@ ); } + fn fill_rect_fast( + &mut self, + rect: &Rect, + paint: Paint, + blend_mode: BlendMode, + mask: Option<Mask>, + encoded_paints: &[EncodedPaint], + ) { + let wide = &mut self.wide; + + // Generate strips directly for the rectangle (bypasses path processing). + self.strip_generator.generate_filled_rect_fast( + rect, + &mut self.strip_storage, + self.clip_context.get(), + ); + + // Generate coarse-level commands from strips (layer_id 0 = root layer). + wide.generate( + &self.strip_storage.strips, + paint, + blend_mode, + 0, + mask, + encoded_paints, + ); + } + fn push_layer( &mut self, clip_path: Option<&BezPath>,
diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index 58f14fa..181d4e8 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs
@@ -28,6 +28,7 @@ use vello_common::recording::{PushLayerCommand, Recordable, Recorder, Recording, RenderCommand}; use vello_common::strip::Strip; use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage}; +use vello_common::util::{is_integer_rect, is_integer_translation}; #[cfg(feature = "text")] use vello_common::{ color::{AlphaColor, Srgb}, @@ -227,18 +228,42 @@ /// Fill a rectangle. pub fn fill_rect(&mut self, rect: &Rect) { self.with_optional_filter(|ctx| { - ctx.rect_to_temp_path(rect); let paint = ctx.encode_current_paint(); - ctx.dispatcher.fill_path( - &ctx.temp_path, - ctx.fill_rule, - ctx.transform, - paint, - ctx.blend_mode, - ctx.aliasing_threshold, - ctx.mask.clone(), - &ctx.encoded_paints, - ); + + // Fast path: use optimized rect filling when transforms are integer translations + // AND rect coordinates are integers. This bypasses path processing by generating + // strips directly for the rectangle. + // - Requires integer translation to ensure pixel-aligned rect boundaries. + // - Requires integer rect coordinates because the optimized path doesn't handle + // anti-aliasing for fractional edges. + // - Also requires simple paint transform to avoid precision differences with complex paints. + if is_integer_translation(&ctx.transform) + && is_integer_translation(&ctx.paint_transform) + && is_integer_rect(rect) + { + // Transform the rect to screen coordinates. + let transformed_rect = ctx.transform.transform_rect_bbox(*rect); + ctx.dispatcher.fill_rect_fast( + &transformed_rect, + paint, + ctx.blend_mode, + ctx.mask.clone(), + &ctx.encoded_paints, + ); + } else { + // Fall back to path-based rendering for rotated/skewed transforms. + ctx.rect_to_temp_path(rect); + ctx.dispatcher.fill_path( + &ctx.temp_path, + ctx.fill_rule, + ctx.transform, + paint, + ctx.blend_mode, + ctx.aliasing_threshold, + ctx.mask.clone(), + &ctx.encoded_paints, + ); + } }); }
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index deb2ce7..19e515a 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -22,6 +22,7 @@ use vello_common::render_graph::RenderGraph; use vello_common::strip::Strip; use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage}; +use vello_common::util::{is_integer_rect, is_integer_translation}; use crate::AtlasConfig; @@ -324,7 +325,49 @@ /// Fill a rectangle with the current paint and fill rule. pub fn fill_rect(&mut self, rect: &Rect) { - self.fill_path(&rect.to_path(DEFAULT_TOLERANCE)); + if !self.paint_visible { + return; + } + + // Fast path: use optimized rect filling when transforms are integer translations + // AND rect coordinates are integers. This bypasses path processing by generating + // strips directly for the rectangle. + // - Requires integer translation to ensure pixel-aligned rect boundaries. + // - Requires integer rect coordinates because the optimized path doesn't handle + // anti-aliasing for fractional edges. + // - Also requires simple paint transform to avoid precision differences with complex paints. + if is_integer_translation(&self.transform) + && is_integer_translation(&self.paint_transform) + && is_integer_rect(rect) + { + self.fill_rect_fast(rect); + } else { + self.fill_path(&rect.to_path(DEFAULT_TOLERANCE)); + } + } + + /// Fast path for filling a pixel-aligned rectangle. + /// + /// Bypasses path processing by generating strips directly for the rectangle. + /// The caller must ensure the transform is an integer translation and the rect + /// has integer coordinates. + fn fill_rect_fast(&mut self, rect: &Rect) { + let paint = self.encode_current_paint(); + let transformed_rect = self.transform.transform_rect_bbox(*rect); + let strip_storage = &mut self.strip_storage.borrow_mut(); + self.strip_generator.generate_filled_rect_fast( + &transformed_rect, + strip_storage, + self.clip_context.get(), + ); + self.wide.generate( + &strip_storage.strips, + paint, + self.blend_mode, + 0, + None, + &self.encoded_paints, + ); } /// Stroke a rectangle with the current paint and stroke settings.