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.