Implement faster rect rendering
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs
index 71b3b36..2f48a9d 100644
--- a/sparse_strips/vello_hybrid/src/render/common.rs
+++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -47,7 +47,7 @@
     pub _padding: [u32; 3],
 }
 
-/// Represents a GPU strip for rendering.
+/// A GPU strip instance for rendering.
 ///
 /// This struct corresponds to the `StripInstance` struct in the shader.
 /// See the `StripInstance` documentation in `render_strips.wgsl` for detailed field descriptions.
@@ -58,16 +58,16 @@
     pub x: u16,
     /// See `StripInstance::xy` documentation in `render_strips.wgsl`.
     pub y: u16,
-    /// See `StripInstance::widths` documentation in `render_strips.wgsl`.
+    /// See `StripInstance::dense_width_or_rect_height` documentation in `render_strips.wgsl`.
     pub width: u16,
-    /// See `StripInstance::widths` documentation in `render_strips.wgsl`.
-    pub dense_width: u16,
-    /// See `StripInstance::col_idx` documentation in `render_strips.wgsl`.
-    pub col_idx: u32,
+    /// See `StripInstance::dense_width_or_rect_height` documentation in `render_strips.wgsl`.
+    pub dense_width_or_rect_height: u16,
+    /// See `StripInstance::col_idx_or_rect_frac` documentation in `render_strips.wgsl`.
+    pub col_idx_or_rect_frac: u32,
     /// See `StripInstance::payload` documentation in `render_strips.wgsl`.
     pub payload: u32,
-    /// See `StripInstance::paint` documentation in `render_strips.wgsl`.
-    pub paint: u32,
+    /// See `StripInstance::paint_and_rect_flag` documentation in `render_strips.wgsl`.
+    pub paint_and_rect_flag: u32,
 }
 
 /// Different types of GPU encoded paints.
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs
index 988d3a1..160d7a8 100644
--- a/sparse_strips/vello_hybrid/src/scene.rs
+++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -27,7 +27,6 @@
 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};
 
 /// Default tolerance for curve flattening
 pub(crate) const DEFAULT_TOLERANCE: f64 = 0.1;
@@ -72,6 +71,25 @@
     pub(crate) paint: Paint,
 }
 
+/// A rectangle stored in the fast-path buffer.
+#[derive(Debug)]
+pub(crate) struct FastPathRect {
+    pub(crate) x0: f32,
+    pub(crate) y0: f32,
+    pub(crate) x1: f32,
+    pub(crate) y1: f32,
+    pub(crate) paint: Paint,
+}
+
+/// A command in the fast strips buffer.
+#[derive(Debug)]
+pub(crate) enum FastStripCommand {
+    /// A path rendered via the normal strip pipeline.
+    Path(FastStripsPath),
+    /// A rectangle.
+    Rect(FastPathRect),
+}
+
 /// A buffer that collects strips from paths that are rendered directly to the surface,
 /// bypassing coarse rasterization.
 ///
@@ -79,14 +97,14 @@
 /// for one path within that storage.
 #[derive(Debug, Default)]
 pub(crate) struct FastStripsBuffer {
-    /// All paths in the buffer.
-    pub(crate) paths: Vec<FastStripsPath>,
+    /// All commands in the buffer.
+    pub(crate) commands: Vec<FastStripCommand>,
 }
 
 impl FastStripsBuffer {
     #[inline(always)]
     fn clear(&mut self) {
-        self.paths.clear();
+        self.commands.clear();
     }
 }
 
@@ -222,10 +240,13 @@
 macro_rules! submit_strips {
     ($self:ident, $strip_storage:expr, $strip_start:expr, $paint:expr) => {
         if $self.strip_path_mode != StripPathMode::CoarseOnly && !$self.wide.has_layers() {
-            $self.fast_strips_buffer.paths.push(FastStripsPath {
-                strips: $strip_start..$strip_storage.strips.len(),
-                paint: $paint,
-            });
+            $self
+                .fast_strips_buffer
+                .commands
+                .push(FastStripCommand::Path(FastStripsPath {
+                    strips: $strip_start..$strip_storage.strips.len(),
+                    paint: $paint,
+                }));
         } else {
             // In `ReplaceAfter(n)` mode the fast path prefix lives at `[0..n]`
             // and must not be fed into the coarse rasterizer.
@@ -434,40 +455,56 @@
             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.render_state.transform)
-            && is_integer_translation(&self.render_state.paint_transform)
-            && is_integer_rect(rect)
-        {
-            self.fill_rect_fast(rect);
-        } else {
-            self.fill_path(&rect.to_path(DEFAULT_TOLERANCE));
+        if self.try_fast_rect(rect) {
+            return;
         }
+
+        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) {
+    #[expect(
+        clippy::cast_possible_truncation,
+        reason = "f64→f32 truncation is acceptable for pixel coordinates"
+    )]
+    fn try_fast_rect(&mut self, rect: &Rect) -> bool {
+        if self.strip_path_mode == StripPathMode::CoarseOnly || self.wide.has_layers() {
+            return false;
+        }
+
+        if self.clip_context.get().is_some() {
+            return false;
+        }
+
+        // We can't handle skewed rectangles.
+        let coeffs = self.render_state.transform.as_coeffs();
+        if coeffs[1].abs() > 1e-5 || coeffs[2].abs() > 1e-5 {
+            return false;
+        }
+
         let paint = self.encode_current_paint();
         let transformed_rect = self.render_state.transform.transform_rect_bbox(*rect);
-        let strip_storage = &mut self.strip_storage.borrow_mut();
-        let strip_start = strip_storage.strips.len();
-        self.strip_generator.generate_filled_rect_fast(
-            &transformed_rect,
-            strip_storage,
-            self.clip_context.get(),
-        );
 
-        submit_strips!(self, strip_storage, strip_start, paint);
+        let x0 = transformed_rect.x0.max(0.0).min(f64::from(self.width));
+        let y0 = transformed_rect.y0.max(0.0).min(f64::from(self.height));
+        let x1 = transformed_rect.x1.max(0.0).min(f64::from(self.width));
+        let y1 = transformed_rect.y1.max(0.0).min(f64::from(self.height));
+
+        // Can't handle mirrored or zero-sized rectangles.
+        if x1 <= x0 || y1 <= y0 {
+            return false;
+        }
+
+        self.fast_strips_buffer
+            .commands
+            .push(FastStripCommand::Rect(FastPathRect {
+                x0: x0 as f32,
+                y0: y0 as f32,
+                x1: x1 as f32,
+                y1: y1 as f32,
+                paint,
+            }));
+
+        true
     }
 
     /// Stroke a rectangle with the current paint and stroke settings.
@@ -493,15 +530,38 @@
         }
 
         let mut strip_storage = self.strip_storage.borrow_mut();
-        for path in self.fast_strips_buffer.paths.drain(..) {
-            self.wide.generate(
-                &strip_storage.strips[path.strips],
-                path.paint,
-                BlendMode::default(),
-                0,
-                None,
-                &self.encoded_paints,
-            );
+        for cmd in self.fast_strips_buffer.commands.drain(..) {
+            match cmd {
+                FastStripCommand::Path(path) => {
+                    self.wide.generate(
+                        &strip_storage.strips[path.strips],
+                        path.paint,
+                        BlendMode::default(),
+                        0,
+                        None,
+                        &self.encoded_paints,
+                    );
+                }
+                FastStripCommand::Rect(r) => {
+                    let rect = Rect::new(
+                        f64::from(r.x0),
+                        f64::from(r.y0),
+                        f64::from(r.x1),
+                        f64::from(r.y1),
+                    );
+                    let strip_start = strip_storage.strips.len();
+                    self.strip_generator
+                        .generate_filled_rect_fast(&rect, &mut strip_storage, None);
+                    self.wide.generate(
+                        &strip_storage.strips[strip_start..],
+                        r.paint,
+                        BlendMode::default(),
+                        0,
+                        None,
+                        &self.encoded_paints,
+                    );
+                }
+            }
         }
 
         strip_storage.set_generation_mode(GenerationMode::Replace);
@@ -529,7 +589,7 @@
             // split point so the scheduler knows to process one coarse batch after
             // processing fast path strips up to this point.
             if !self.wide.has_layers() {
-                let split = self.fast_strips_buffer.paths.len();
+                let split = self.fast_strips_buffer.commands.len();
                 self.coarse_batch_splits.push(split);
             }
             let mut strip_storage = self.strip_storage.borrow_mut();
@@ -1050,10 +1110,12 @@
             strip_storage
                 .strips
                 .extend_from_slice(&adjusted_strips[start..end]);
-            self.fast_strips_buffer.paths.push(FastStripsPath {
-                strips: strip_start..strip_storage.strips.len(),
-                paint,
-            });
+            self.fast_strips_buffer
+                .commands
+                .push(FastStripCommand::Path(FastStripsPath {
+                    strips: strip_start..strip_storage.strips.len(),
+                    paint,
+                }));
         } else {
             self.wide.generate(
                 &adjusted_strips[start..end],
diff --git a/sparse_strips/vello_hybrid/src/schedule.rs b/sparse_strips/vello_hybrid/src/schedule.rs
index e61315a..40a561e 100644
--- a/sparse_strips/vello_hybrid/src/schedule.rs
+++ b/sparse_strips/vello_hybrid/src/schedule.rs
@@ -176,7 +176,7 @@
 only break in edge cases, and some of them are also only related to conversions from f64 to f32."
 )]
 
-use crate::scene::{FastStripsPath, StripPathMode};
+use crate::scene::{FastStripCommand, FastStripsPath, StripPathMode};
 use crate::{GpuStrip, RenderError, Scene};
 use alloc::collections::VecDeque;
 use alloc::vec::Vec;
@@ -202,6 +202,10 @@
 const PAINT_TYPE_RADIAL_GRADIENT: u32 = 3;
 const PAINT_TYPE_SWEEP_GRADIENT: u32 = 4;
 
+/// Bit 31 of [`GpuStrip::paint_and_rect_flag`] signals that the strip
+/// represents a full rectangle.
+const RECT_STRIP_FLAG: u32 = 1 << 31;
+
 // The sentinel tile index representing the surface.
 const SENTINEL_SLOT_IDX: usize = usize::MAX;
 
@@ -485,7 +489,11 @@
         match scene.strip_path_mode {
             StripPathMode::FastOnly => {
                 // We only have strips.
-                self.push_direct_strips(scene, 0..scene.fast_strips_buffer.paths.len(), paint_idxs);
+                self.push_direct_strips(
+                    scene,
+                    0..scene.fast_strips_buffer.commands.len(),
+                    paint_idxs,
+                );
             }
             StripPathMode::CoarseOnly => {
                 // We only have coarse-rasterized paths.
@@ -527,7 +535,7 @@
 
                 // Handle the last batch of fast strips, which isn't explicitly delimited in the
                 // scene.
-                let tail_end = scene.fast_strips_buffer.paths.len();
+                let tail_end = scene.fast_strips_buffer.commands.len();
                 if prev_split < tail_end {
                     self.push_direct_strips(scene, prev_split..tail_end, paint_idxs);
                 }
@@ -555,7 +563,7 @@
         Ok(())
     }
 
-    /// Generate `GpuStrips` for a range of direct paths and append them
+    /// Generate `GpuStrips` for a range of direct commands and append them
     /// directly into the current round's surface draw array.
     fn push_direct_strips(&mut self, scene: &Scene, range: Range<usize>, paint_idxs: &[u32]) {
         let strip_storage = scene.strip_storage.borrow();
@@ -563,8 +571,51 @@
         // rendered to the final surface.
         let draw = self.draw_mut(self.round, 2);
 
-        for path in &scene.fast_strips_buffer.paths[range] {
-            generate_gpu_strips_for_path(path, &strip_storage, scene, paint_idxs, &mut draw.0);
+        for cmd in &scene.fast_strips_buffer.commands[range] {
+            match cmd {
+                FastStripCommand::Path(path) => {
+                    generate_gpu_strips_for_fast_path(
+                        path,
+                        &strip_storage,
+                        scene,
+                        paint_idxs,
+                        &mut draw.0,
+                    );
+                }
+                FastStripCommand::Rect(r) => {
+                    let sx0 = r.x0.floor();
+                    let sy0 = r.y0.floor();
+                    let sx1 = r.x1.ceil();
+                    let sy1 = r.y1.ceil();
+
+                    let x = sx0 as u16;
+                    let y = sy0 as u16;
+                    // Are guaranteed to be > 0 since we rejected negative rectangles.
+                    let width = (sx1 - sx0) as u16;
+                    let height = (sy1 - sy0) as u16;
+
+                    let (payload, paint_packed) =
+                        Self::process_paint(&r.paint, scene, (x, y), paint_idxs);
+
+                    // Determine the fractional offsets for anti-aliasing and quantize so it
+                    // fits into u8.
+                    let q = |f: f32| -> u8 { (f * 255.0 + 0.5) as u8 };
+                    let frac = u32::from(q(r.x0 - sx0))
+                        | (u32::from(q(r.y0 - sy0)) << 8)
+                        | (u32::from(q(sx1 - r.x1)) << 16)
+                        | (u32::from(q(sy1 - r.y1)) << 24);
+
+                    draw.0.push(GpuStrip {
+                        x,
+                        y,
+                        width,
+                        dense_width_or_rect_height: height,
+                        col_idx_or_rect_frac: frac,
+                        payload,
+                        paint_and_rect_flag: paint_packed | RECT_STRIP_FLAG,
+                    });
+                }
+            }
         }
     }
 
@@ -1207,7 +1258,7 @@
                     has_non_zero_alpha(rgba),
                     "Color fields with 0 alpha are reserved for clipping"
                 );
-                let paint_packed = (COLOR_SOURCE_PAYLOAD << 30) | (PAINT_TYPE_SOLID << 27);
+                let paint_packed = (COLOR_SOURCE_PAYLOAD << 29) | (PAINT_TYPE_SOLID << 26);
                 (rgba, paint_packed)
             }
             Paint::Indexed(indexed_paint) => {
@@ -1217,9 +1268,9 @@
                 match scene.encoded_paints.get(paint_id) {
                     Some(EncodedPaint::Image(encoded_image)) => match &encoded_image.source {
                         ImageSource::OpaqueId { .. } => {
-                            let paint_packed = (COLOR_SOURCE_PAYLOAD << 30)
-                                | (PAINT_TYPE_IMAGE << 27)
-                                | (paint_idx & 0x07FFFFFF);
+                            let paint_packed = (COLOR_SOURCE_PAYLOAD << 29)
+                                | (PAINT_TYPE_IMAGE << 26)
+                                | (paint_idx & 0x03FF_FFFF);
                             let scene_strip_xy =
                                 ((scene_strip_y as u32) << 16) | (scene_strip_x as u32);
                             (scene_strip_xy, paint_packed)
@@ -1233,9 +1284,9 @@
                             EncodedKind::Radial(_) => PAINT_TYPE_RADIAL_GRADIENT,
                             EncodedKind::Sweep(_) => PAINT_TYPE_SWEEP_GRADIENT,
                         };
-                        let paint_packed = (COLOR_SOURCE_PAYLOAD << 30)
-                            | (gradient_paint_type << 27)
-                            | (paint_idx & 0x07FFFFFF);
+                        let paint_packed = (COLOR_SOURCE_PAYLOAD << 29)
+                            | (gradient_paint_type << 26)
+                            | (paint_idx & 0x03FF_FFFF);
                         let scene_strip_xy =
                             ((scene_strip_y as u32) << 16) | (scene_strip_x as u32);
                         (scene_strip_xy, paint_packed)
@@ -1253,8 +1304,8 @@
     x: u16,
     y: u16,
     width: u16,
-    dense_width: u16,
-    col_idx: u32,
+    dense_width_or_rect_height: u16,
+    col_idx_or_rect_frac: u32,
 }
 
 impl GpuStripBuilder {
@@ -1264,8 +1315,8 @@
             x,
             y,
             width,
-            dense_width: 0,
-            col_idx: 0,
+            dense_width_or_rect_height: 0,
+            col_idx_or_rect_frac: 0,
         }
     }
 
@@ -1275,15 +1326,15 @@
             x: x_offset,
             y: u16::try_from(slot_idx).unwrap() * Tile::HEIGHT,
             width,
-            dense_width: 0,
-            col_idx: 0,
+            dense_width_or_rect_height: 0,
+            col_idx_or_rect_frac: 0,
         }
     }
 
     /// Add sparse strip parameters.
     fn with_sparse(mut self, dense_width: u16, col_idx: u32) -> Self {
-        self.dense_width = dense_width;
-        self.col_idx = col_idx;
+        self.dense_width_or_rect_height = dense_width;
+        self.col_idx_or_rect_frac = col_idx;
         self
     }
 
@@ -1293,10 +1344,10 @@
             x: self.x,
             y: self.y,
             width: self.width,
-            dense_width: self.dense_width,
-            col_idx: self.col_idx,
+            dense_width_or_rect_height: self.dense_width_or_rect_height,
+            col_idx_or_rect_frac: self.col_idx_or_rect_frac,
             payload,
-            paint,
+            paint_and_rect_flag: paint,
         }
     }
 
@@ -1306,10 +1357,10 @@
             x: self.x,
             y: self.y,
             width: self.width,
-            dense_width: self.dense_width,
-            col_idx: self.col_idx,
+            dense_width_or_rect_height: self.dense_width_or_rect_height,
+            col_idx_or_rect_frac: self.col_idx_or_rect_frac,
             payload: u32::try_from(from_slot).unwrap(),
-            paint: (COLOR_SOURCE_SLOT << 30) | (opacity as u32),
+            paint_and_rect_flag: (COLOR_SOURCE_SLOT << 29) | (opacity as u32),
         }
     }
 
@@ -1326,11 +1377,11 @@
             x: self.x,
             y: self.y,
             width: self.width,
-            dense_width: self.dense_width,
-            col_idx: self.col_idx,
+            dense_width_or_rect_height: self.dense_width_or_rect_height,
+            col_idx_or_rect_frac: self.col_idx_or_rect_frac,
             payload: (u32::try_from(src_slot).unwrap())
                 | ((u32::try_from(dest_slot).unwrap()) << 16),
-            paint: (COLOR_SOURCE_BLEND << 30)
+            paint_and_rect_flag: (COLOR_SOURCE_BLEND << 29)
                 | ((opacity as u32) << 16)
                 | ((mix_mode as u32) << 8)
                 | (compose_mode as u32),
@@ -1343,7 +1394,7 @@
     rgba >= 0x1_00_00_00
 }
 
-fn generate_gpu_strips_for_path(
+fn generate_gpu_strips_for_fast_path(
     path: &FastStripsPath,
     strip_storage: &StripStorage,
     scene: &Scene,
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
index a54638d..d02eef5 100644
--- a/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
+++ b/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl
@@ -38,8 +38,10 @@
 const PAINT_TYPE_RADIAL_GRADIENT: u32 = 3u;
 const PAINT_TYPE_SWEEP_GRADIENT: u32 = 4u;
 
-// Paint texture index mask (extracts lower 27 bits from paint field).
-const PAINT_TEXTURE_INDEX_MASK: u32 = 0x07FFFFFFu; 
+// Paint texture index mask (extracts lower 26 bits from paint field).
+const PAINT_TEXTURE_INDEX_MASK: u32 = 0x03FFFFFFu;
+
+const RECT_STRIP_FLAG: u32 = 0x80000000u;
 
 // Image quality
 const IMAGE_QUALITY_LOW = 0u;
@@ -119,22 +121,38 @@
     _padding2: u32,
 }
 
-// `paint` bit layout:
-//   - Bits 30-31: `color_source`      0 = use payload, 1 = use slot texture, 2 = blend mode
-//   - Bits 0-29:  Usage depends on color_source:
+// A `StripInstance` can represent either a **normal strip** (representing a sparse fill or alpha fill of height
+// Tile::HEIGHT) or a **rect strip** (an entire rectangle rendered as a single quad, with anti-aliasing support).
+// The two modes are distinguished by RECT_STRIP_FLAG (bit 31 of `paint_and_rect_flag`).
+//
+// Depending on the active mode, the fields are interpreted as follows:
+//
+//   Field                 | Normal strip                      | Rect strip
+//   ----------------------+-----------------------------------+-----------------------------------
+//   xy                    | Strip position                    | Rect top-left (snapped outward)
+//   widths_or_rect_height | [width, dense_width]              | [width, height] (both snapped)
+//   col_idx_or_rect_frac  | Alpha column index                | Packed AA edge fractions (4 × u8)
+//   payload               | Color / scene coords / slot idx   | Color / scene coords
+//   paint_and_rect_flag   | Paint encoding                    | Paint encoding | RECT_STRIP_FLAG
+//
+//
+// `paint_and_rect_flag` bit layout:
+//   - Bit  31:    `RECT_STRIP_FLAG`  0 = normal strip, 1 = rect strip
+//   - Bits 29-30: `color_source`     0 = use payload, 1 = use slot texture, 2 = blend mode
+//   - Bits 0-28:  Usage depends on color_source:
 //
 //     When color_source = 0 (COLOR_SOURCE_PAYLOAD):
-//       - Bits 27-29: `paint_type` (0 = solid, 1 = image, 2 = linear_gradient, 3 = radial_gradient, 4 = sweep_gradient)
-//       - Bits 0-26: 
+//       - Bits 26-28: `paint_type` (0 = solid, 1 = image, 2 = linear_gradient, 3 = radial_gradient, 4 = sweep_gradient)
+//       - Bits 0-25:
 //         - If paint_type = 0: unused
 //         - If paint_type >= 1: `paint_texture_idx`
 //
 //     When color_source = 1 (COLOR_SOURCE_SLOT):
 //       - Bits 0-7: opacity (0-255)
-//       - Bits 8-29: unused
+//       - Bits 8-28: unused
 //
 //     When color_source = 2 (COLOR_SOURCE_BLEND):
-//       - Bits 16-29: `dest_slot` (14 bits)
+//       - Bits 16-28: `dest_slot` (14 bits)
 //       - Bits 8-15: `mix_mode` (8 bits)
 //       - Bits 0-7: `compose_mode` (8 bits)
 //
@@ -151,7 +169,7 @@
 // ├── paint_type = 3 (PAINT_TYPE_RADIAL_GRADIENT) - Radial gradient (with kind discriminator)
 // └── paint_type = 4 (PAINT_TYPE_SWEEP_GRADIENT) - Sweep gradient rendering
 //     ├── payload = [x, y] scene coordinates (packed as u16s)
-//     └── bits 0-27 = paint_texture_idx
+//     └── bits 0-25 = paint_texture_idx
 //
 // color_source = 1 (COLOR_SOURCE_SLOT) - Use slot texture
 // ├── payload = slot_index (u32)
@@ -167,37 +185,44 @@
 //     └── bits 0-7 = compose_mode (compositing operation)
 struct StripInstance {
     // [x, y] packed as u16's
-    // x, y — coordinates of the strip
+    // x, y — coordinates of the strip or rect
     @location(0) xy: u32,
     // [width, dense_width] packed as u16's
-    // width — width of the strip
+    // width — width of the strip or rect
     // dense_width — width of the portion where alpha blending should be applied
     // Note that currently, if the strip instance represents an actual strip (i.e. an anti-aliased region),
     // width = dense_width. If the StripInstance represents a sparse fill region, then dense_width = 0.
+    // For rect strips, dense_width is repurposed to hold the rectangle height.
     // TODO: In the future, this could be optimized such that `width` always represents the width and a simple
     // 1-bit flag is used to distinguish between sparse fill region and strip. This frees up 15 other bits.
     // Otherwise, it might also be possible to merge a strip and sparse fill command into a single strip instance.
-    @location(1) widths: u32,
-    // Alpha texture column index where this strip's alpha values begin
+    @location(1) widths_or_rect_height: u32,
+    // For normal strips: alpha texture column index where this strip's alpha values begin.
     // There are [`Config::strip_height`] alpha values per column.
-    @location(2) col_idx: u32,
+    // For rect strips: packed fractional edge offsets for AA.
+    @location(2) col_idx_or_rect_frac: u32,
     // See StripInstance documentation above.
     @location(3) payload: u32,
     // See StripInstance documentation above.
-    @location(4) paint: u32,
+    @location(4) paint_and_rect_flag: u32,
 }
 
 struct VertexOutput {
     // Render type for the strip
-    @location(0) @interpolate(flat) paint: u32,
+    @location(0) @interpolate(flat) paint_and_rect_flag: u32,
     // Texture coordinates for the current fragment
     @location(1) tex_coord: vec2<f32>,
     // UV coordinates for the current fragment, used for image sampling
     @location(2) sample_xy: vec2<f32>,
-    // Ending x-position of the dense (alpha) region
+    // For normal strips: ending x-position of the dense (alpha) region.
+    // For rect strips: packed dimensions (width | height << 16).
     @location(3) @interpolate(flat) dense_end: u32,
     // Color value or slot index when alpha is 0
     @location(4) @interpolate(flat) payload: u32,
+    // Packed fractional edge offsets for rectangles.
+    // Bits 0-7: x0, 8-15: y0, 16-23: x1, 24-31: y1.
+    // Zero for normal strips.
+    @location(5) @interpolate(flat) rect_frac: u32,
     // Normalized device coordinates (NDC) for the current vertex
     @builtin(position) position: vec4<f32>,
 };
@@ -228,29 +253,36 @@
     // Unpack the x and y coordinates from the packed u32 instance.xy
     let x0 = instance.xy & 0xffffu;
     let y0 = instance.xy >> 16u;
-    // Unpack the total width and dense (alpha) width from the packed u32 instance.widths
-    let width = instance.widths & 0xffffu;
-    let dense_width = instance.widths >> 16u;
-    // Calculate the ending x-position of the dense (alpha) region
-    // This boundary is used in the fragment shader to determine if alpha sampling is needed
-    out.dense_end = instance.col_idx + dense_width;
+    let width = instance.widths_or_rect_height & 0xffffu;
+    let dense_width = instance.widths_or_rect_height >> 16u;
+
+    let is_rect = (instance.paint_and_rect_flag & RECT_STRIP_FLAG) != 0u;
+    var height = config.strip_height;
+    if is_rect {
+        height = dense_width;
+        out.dense_end = width | (dense_width << 16u);
+        out.rect_frac = instance.col_idx_or_rect_frac;
+    } else {
+        out.dense_end = instance.col_idx_or_rect_frac + dense_width;
+        out.rect_frac = 0u;
+    }
     // Calculate the pixel coordinates of the current vertex within the strip
     let pix_x = f32(x0) + x * f32(width);
-    let pix_y = f32(y0) + y * f32(config.strip_height);
+    let pix_y = f32(y0) + y * f32(height);
     // Convert pixel coordinates to normalized device coordinates (NDC)
     // NDC ranges from -1 to 1, with (0,0) at the center of the viewport
     let ndc_x = pix_x * 2.0 / f32(config.width) - 1.0;
     let ndc_y = 1.0 - pix_y * 2.0 / f32(config.height);
 
-    let color_source = (instance.paint >> 30u) & 0x3u;
+    let color_source = (instance.paint_and_rect_flag >> 29u) & 0x3u;
     if color_source == COLOR_SOURCE_PAYLOAD {
-        let paint_type = (instance.paint >> 27u) & 0x7u;
+        let paint_type = (instance.paint_and_rect_flag >> 26u) & 0x7u;
         // Unpack view coordinates for image sampling and gradient calculations
         let scene_strip_x = instance.payload & 0xffffu;
         let scene_strip_y = instance.payload >> 16u;
 
         if paint_type == PAINT_TYPE_IMAGE {
-            let paint_tex_idx = instance.paint & PAINT_TEXTURE_INDEX_MASK;
+            let paint_tex_idx = instance.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
             let encoded_image = unpack_encoded_image(paint_tex_idx);
             // Use view coordinates for image sampling (always in global view space)
             out.sample_xy = encoded_image.translate 
@@ -258,21 +290,24 @@
                 + encoded_image.transform.xy * f32(scene_strip_x) 
                 + encoded_image.transform.zw * f32(scene_strip_y)
                 + encoded_image.transform.xy * x * f32(width)
-                + encoded_image.transform.zw * y * f32(config.strip_height);
+                + encoded_image.transform.zw * y * f32(height);
         } else if paint_type == PAINT_TYPE_LINEAR_GRADIENT || paint_type == PAINT_TYPE_RADIAL_GRADIENT || paint_type == PAINT_TYPE_SWEEP_GRADIENT {
             // Use view coordinates for gradient transform (always in global view space)
             out.sample_xy = vec2<f32>(
                 f32(scene_strip_x) + x * f32(width),
-                f32(scene_strip_y) + y * f32(config.strip_height)
+                f32(scene_strip_y) + y * f32(height)
             );
         }
     }
 
-    // Regular texture coordinates for other render types
-    out.tex_coord = vec2<f32>(f32(instance.col_idx) + x * f32(width), y * f32(config.strip_height));
+    if is_rect {
+        out.tex_coord = vec2<f32>(x * f32(width), y * f32(height));
+    } else {
+        out.tex_coord = vec2<f32>(f32(instance.col_idx_or_rect_frac) + x * f32(width), y * f32(height));
+    }
     out.position = vec4<f32>(ndc_x, ndc_y, 0.0, 1.0);
     out.payload = instance.payload;
-    out.paint = instance.paint;
+    out.paint_and_rect_flag = instance.paint_and_rect_flag;
 
     return out;
 }
@@ -285,13 +320,24 @@
 
 @fragment
 fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    let x = u32(floor(in.tex_coord.x));
     var alpha = 1.0;
-    // This if condition essentially checks whether the current pixel lies within a strip or a sparse
-    // fill region. In the former case, `dense_end` will be bigger than 0 since `dense_width` != 0. In the latter
-    // case, `dense_end` will always be zero since for sparse regions `col_idx` and `dense_width` are both set to
-    // zero.
-    if in.dense_end != 0 {
+    let is_rect = (in.paint_and_rect_flag & RECT_STRIP_FLAG) != 0u;
+    if is_rect && in.rect_frac != 0u {
+        let frac = unpack4x8unorm(in.rect_frac);
+        // Calculate how much of the pixel is actually covered by the rect.
+        // We do this by simply calculating the fractions in the x and y direction, and
+        // then multiplying them.
+        // For (maybe?) better performance, we calculate the x and y dimension in a single
+        // pass by packing everything into a vec2.
+        let rect_size = vec2<f32>(f32(in.dense_end & 0xFFFFu), f32(in.dense_end >> 16u));
+        let tc = in.tex_coord;
+        // + 0.5 and -0.5 since the fragment shader positions the coordinates in the center of the pixel.
+        let bottom_and_right = min(tc + 0.5, rect_size - frac.zw);
+        let top_and_left = max(tc - 0.5, frac.xy);
+        let a = clamp(bottom_and_right - top_and_left, vec2(0.0), vec2(1.0));
+        alpha = a.x * a.y;
+    } else if !is_rect && in.dense_end != 0u {
+        let x = u32(floor(in.tex_coord.x));
         let y = u32(floor(in.tex_coord.y));
         // Retrieve alpha value from the texture. We store 16 1-byte alpha
         // values per texel, with each color channel packing 4 alpha values.
@@ -318,17 +364,17 @@
         alpha = f32((alphas_u32 >> (y * 8u)) & 0xffu) * (1.0 / 255.0);
     }
     // Apply the alpha value to the unpacked RGBA color or slot index
-    let color_source = (in.paint >> 30u) & 0x3u;
+    let color_source = (in.paint_and_rect_flag >> 29u) & 0x3u;
     var final_color: vec4<f32>;
 
     if color_source == COLOR_SOURCE_PAYLOAD {
-        let paint_type = (in.paint >> 27u) & 0x7u;
+        let paint_type = (in.paint_and_rect_flag >> 26u) & 0x7u;
 
         // in.payload encodes a color for PAINT_TYPE_SOLID or sample_xy for PAINT_TYPE_IMAGE
         if paint_type == PAINT_TYPE_SOLID {
             final_color = alpha * unpack4x8unorm(in.payload);
         } else if paint_type == PAINT_TYPE_IMAGE {
-            let paint_tex_idx = in.paint & PAINT_TEXTURE_INDEX_MASK;
+            let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
             let encoded_image = unpack_encoded_image(paint_tex_idx);
             let image_offset = encoded_image.image_offset;
             let image_size = encoded_image.image_size;
@@ -380,7 +426,7 @@
                 is_multiply
             );
         } else if paint_type == PAINT_TYPE_LINEAR_GRADIENT {
-            let paint_tex_idx = in.paint & PAINT_TEXTURE_INDEX_MASK;
+            let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
             let linear_gradient = unpack_linear_gradient(paint_tex_idx);
             
             // Calculate fragment position and apply transform
@@ -405,7 +451,7 @@
             );
             final_color = alpha * gradient_color;
         } else if paint_type == PAINT_TYPE_RADIAL_GRADIENT {
-            let paint_tex_idx = in.paint & PAINT_TEXTURE_INDEX_MASK;
+            let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
             let radial_gradient = unpack_radial_gradient(paint_tex_idx);
             
             // Calculate fragment position and apply transform
@@ -430,7 +476,7 @@
             );
             final_color = alpha * gradient_color;
         } else if paint_type == PAINT_TYPE_SWEEP_GRADIENT {
-            let paint_tex_idx = in.paint & PAINT_TEXTURE_INDEX_MASK;
+            let paint_tex_idx = in.paint_and_rect_flag & PAINT_TEXTURE_INDEX_MASK;
             let sweep_gradient = unpack_sweep_gradient(paint_tex_idx);
             
             // Calculate fragment position and apply transform
@@ -476,13 +522,13 @@
         let clip_in_color = textureLoad(clip_input_texture, vec2(clip_x, clip_y), 0);
 
         // Extract opacity from first 8 bits (quantized from [0, 255])
-        let opacity = f32(in.paint & 0xFFu) * (1.0 / 255.0);
+        let opacity = f32(in.paint_and_rect_flag & 0xFFu) * (1.0 / 255.0);
 
         final_color = alpha * opacity * clip_in_color;
     } else if color_source == COLOR_SOURCE_BLEND {
-        let opacity = f32((in.paint >> 16u) & 0xFFu) * (1.0 / 255.0);
-        let mix_mode = (in.paint >> 8u) & 0xFFu;
-        let compose_mode = in.paint & 0xFFu;
+        let opacity = f32((in.paint_and_rect_flag >> 16u) & 0xFFu) * (1.0 / 255.0);
+        let mix_mode = (in.paint_and_rect_flag >> 8u) & 0xFFu;
+        let compose_mode = in.paint_and_rect_flag & 0xFFu;
         
         // Read source color from slot
         let src_slot = in.payload & 0xFFFFu;