diff --git a/crates/paint/src/render.rs b/crates/paint/src/render.rs
--- a/crates/paint/src/render.rs
+++ b/crates/paint/src/render.rs
@@ -342,7 +342,9 @@ impl BrushContext {
         if !(mips.is_some() || small || (aliased && b.square) || noise || tex_tip || wet)
             && depth.is_none()
             && proj.is_none()
-            && d.angle == 0.0
+            // Rotation preserves a circle's distance; ellipses still need the transform.
+            && (d.angle == 0.0 || ro == 1.0)
+            && d.angle.is_finite()
             && d.radius.is_finite()
             && d.roundness.is_finite()
             && rm.is_finite()
@@ -1229,7 +1231,8 @@ pub fn render_stroke(
     r.composite(&pre, target, selection, lock_transparency, true)
 }
 
-// The zero-angle transform only changes signs; squaring gives the same radial distance.
+// With zero angle, the transform only changes signs. For circles, any rotation preserves
+// radial distance. Both cases can omit the transform (projection is excluded by the caller).
 #[inline(never)]
 fn rasterize_round(d: &Dab, rect: Rect, out: &mut [f32], hardness: f32, aliased: bool) {
     let (cx, cy) = if aliased { grid_center(d.center.x, d.center.y, 2.0 * d.radius) } else { (d.center.x as f32, d.center.y as f32) };
diff --git a/crates/paint/src/render/reference.rs b/crates/paint/src/render/reference.rs
--- a/crates/paint/src/render/reference.rs
+++ b/crates/paint/src/render/reference.rs
@@ -161,6 +161,64 @@ fn computed_tips_match_scalar_bits() {
     }
 }
 
+#[test]
+fn circular_tips_ignore_finite_rotation() {
+    for aliased in [false, true] {
+        for hardness in [0.0, 0.5, 1.0] {
+            let ctx = BrushContext::new(&BrushSettings { aliased, hardness, ..Default::default() });
+            for radius in [3.0, 15.25, 150.0] {
+                for roundness in [1.0, 1.5] {
+                    let mut d = Dab::round(Point::new(-31.25, 63.75), radius, 0.37);
+                    d.roundness = roundness;
+                    let rect = ctx.dab_rect(&d, false);
+                    let expected: Vec<_> = [false, true]
+                        .into_iter()
+                        .map(|dual| {
+                            let mut coverage = Vec::new();
+                            ctx.rasterize_reference(&d, dual, rect, &mut coverage, None);
+                            coverage
+                        })
+                        .collect();
+                    for angle in [-2.3, 0.7, std::f32::consts::FRAC_PI_2, std::f32::consts::PI] {
+                        for (flip_x, flip_y) in [(false, false), (true, false), (false, true), (true, true)] {
+                            d.angle = angle;
+                            d.flip_x = flip_x;
+                            d.flip_y = flip_y;
+                            for (dual, expected) in [false, true].into_iter().zip(&expected) {
+                                let mut actual = vec![0.75; 19];
+                                ctx.rasterize(&d, dual, rect, &mut actual);
+                                assert_eq!(actual.len(), expected.len());
+                                let mismatch = actual.iter().zip(expected).enumerate().find(|(_, (a, b))| a.to_bits() != b.to_bits());
+                                assert!(
+                                    mismatch.is_none(),
+                                    "circle changed with rotation: {mismatch:?}; {d:?}, aliased={aliased}, hardness={hardness}, dual={dual}"
+                                );
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[test]
+fn rotated_ellipses_and_projected_circles_match_scalar_bits() {
+    for aliased in [false, true] {
+        let ctx = BrushContext::new(&BrushSettings { aliased, hardness: 0.5, ..Default::default() });
+        for (roundness, proj_scale) in [(0.4, 1.0), (1.0, 0.6)] {
+            for angle in [0.0, 0.7, -2.3] {
+                let mut d = Dab::round(Point::new(-31.25, 63.75), 15.25, 0.37);
+                d.roundness = roundness;
+                d.angle = angle;
+                d.proj_scale = proj_scale;
+                d.proj_angle = 0.3;
+                compare(&ctx, &d, false, ctx.dab_rect(&d, false), false);
+            }
+        }
+    }
+}
+
 #[test]
 fn large_corner_dabs_keep_off_canvas_coverage() {
     for (hardness, aliased) in [(0.0, false), (1.0, false), (1.0, true)] {
