Repository navigation
Description
This ONLY repros on debug unopt engine builds with FML assertions enabled. Therefore not super urgent, just a minor annoyance for engine/embedder developers.
Description
When running a Flutter app in debug mode on an Impeller-enabled backend (macOS / iOS) and pressing p in the terminal to toggle construction lines (debugPaintSizeEnabled), the engine crashes with a fatal assertion in Impeller's circle tessellator:
[FATAL:flutter/impeller/tessellator/tessellator.cc(755)] Check failed: data.half_width > 0 && data.half_width < data.radii.width.
Steps to Reproduce
- Build an
ios_debug_unoptbuild of flutter that enablesFML_DCHECKassertions. This WILL NOT work in our default debug opt builds, which is almost certainly why it hasn't been reported by users. - Launch any Flutter application in debug mode using this engine.
flutter run --local-engine ios_debug_unopt --local-engine-host host_debug_unopt_arm64 -d MyDeviceName - Press
pin the terminal to toggle debug construction lines (debugPaintSizeEnabled = true). - The app crashes immediately with the fatal assertion above.
Everything below is Gemini, so take it with a grain of salt.
Root Cause Analysis
-
Debug Painting Behavior:
When debugPaintSizeEnabled is active, the framework paints debug visualizations (such as in RenderSliver.debugPaint in sliver., dart:1836-1846. Some visualizations draw small circles with radius = 2.0 and strokeWidth = 4.0 (which corresponds to half_width = 2.0):final double strokeWidth = math.min(4.0, geometry!.paintExtent / 30.0); // e.g. 4.0 final double padding = math.max(2.0, strokeWidth); // e.g. 4.0 canvas.drawCircle(offset.translate(padding, padding), padding * 0.5, paint); // radius = 2.0 -
Impeller Circle Tessellation:
When CircleGeometry::GetPositionBuffer renders a stroked circle, it delegates to renderer.GetTessellator().StrokedCircle(transform,
center, radius, half_width).In tessellator.cc https://github.
com/flutter/engine/blob/main/src/flutter/impeller/tessellator/tessellator.cc, Tessellator::StrokedCircle currently checks only whether half_width > 0:EllipticalVertexGenerator Tessellator::StrokedCircle( const Matrix& view_transform, const Point& center, Scalar radius, Scalar half_width) { if (half_width > 0) { // Calls Tessellator::GenerateStrokedCircle ... } else { return FilledCircle(view_transform, center, radius); } }However, Tessellator::GenerateStrokedCircle asserts that the inner radius (radii.width - half_width) is strictly positive:
FML_DCHECK(data.half_width > 0 && data.half_width < data.radii.width); -
Why it Fails:
When half_width >= radius (here half_width == 2.0 and radius == 2.0), data.half_width < data.radii.width evaluates to false (2.0 < 2.0), triggering the FML_DCHECK fatal assertion in debug builds.Geometrically, any stroked circle where half_width >= radius has no inner hole (R - W ≤ 0), meaning the stroke covers the entire interior of the circle. A stroked circle with half_width >= radius is geometrically identical to a filled circle of radius radius + half_width.
Timeline
This bug was introduced on December 8, 2023 and first appeared in Flutter 3.19.0:
• Engine Commit: e677fa488ed e677fa4 ("[Impeller] Directly tessellate filled ellipses", flutter-team-archive/engine#48770).
• Rolled into Flutter: Commit 378dad10373 378dad1 (PR
#139837) on December 8, 2023
• First Flutter Stable Release: 3.19.0
What changed in that commit
While commit d0adfc3800f d0adfc3 (flutter-team-archive/engine#48586) initially added direct tessellation for stroked circles on December 1, 2023, commit e677fa4 refactored circle and ellipse tessellation to unify them under EllipticalVertexGenerator. That refactoring introduced:
- The if (half_width > 0) check in Tessellator::StrokedCircle (at tessellator.cc:509).
- The FML_DCHECK(data.half_width > 0 && data.half_width < data.radii.width); assertion in Tessellator::GenerateStrokedCircle (at
tessellator.cc:755).
Proposed Fix
-
Update Tessellator::StrokedCircle in tessellator.cc:
Change the condition in StrokedCircle to check if (half_width > 0 && half_width < radius). When half_width >= radius, fall through to generate a filled circle of radius radius + half_width:EllipticalVertexGenerator Tessellator::StrokedCircle( const Matrix& view_transform, const Point& center, Scalar radius, Scalar half_width) { if (half_width > 0 && half_width < radius) { auto divisions = ComputeQuadrantDivisions( view_transform.GetMaxBasisLengthXY() * radius + half_width); return EllipticalVertexGenerator(Tessellator::GenerateStrokedCircle, GetTrigsForDivisions(divisions), PrimitiveType::kTriangleStrip, 8, { .reference_centers = {center, center}, .radii = {radius, radius}, .half_width = half_width, }); } else { return FilledCircle(view_transform, center, radius + std::max(0.0f, half_width)); }
-
Clamp Inner Radius in Tessellator::GenerateStrokedArc:
Similarly, when stroking an arc with half_width >= radius, clamp inner_radii to zero so that thick stroked arcs do not produce negative inner radii or self-intersecting geometry:Size inner_radii = (base_radii - Size(half_width, half_width)).Max(Size(0.0f, 0.0f)); -
Add Unit Test in tessellator_unittests.cc:
Add a test case verifying that StrokedCircle with half_width >= radius returns a valid FilledCircle generator without asserting or crashing.
Activity
cbracken commentedon Aug 5, 2026
/cc @flar @gaaclarke this only repros on debug unopt local builds which probably explains why we haven't seen it filed. Annoying-ish for us, but non-urgent. I didn't look into this at all other than telling gemini to poke at it a bit and see what it could find.
flar commentedon Aug 10, 2026
Suggested fix sounds good to me.
Impeller: Fix assert on stroked circles wider than their radius
Impeller: Fix assert on stroked circles wider than their radius
Impeller: Fix assert on stroked circles wider than their radius
Impeller: Fix for stroked circles with stroke half-width > radius, te…
github-actions commentedon Aug 29, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.