Skip to content

[Impeller] Crash in debug mode when displaying construction lines #190597

Closed

Description

@cbracken

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

  1. Build an ios_debug_unopt build of flutter that enables FML_DCHECK assertions. This WILL NOT work in our default debug opt builds, which is almost certainly why it hasn't been reported by users.
  2. 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
  3. Press p in the terminal to toggle debug construction lines (debugPaintSizeEnabled = true).
  4. The app crashes immediately with the fatal assertion above.

Everything below is Gemini, so take it with a grain of salt.


Root Cause Analysis

  1. 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
    
  2. 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);
    
  3. 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:

  1. The if (half_width > 0) check in Tessellator::StrokedCircle (at tessellator.cc:509).
  2. The FML_DCHECK(data.half_width > 0 && data.half_width < data.radii.width); assertion in Tessellator::GenerateStrokedCircle (at
    tessellator.cc:755).

Proposed Fix

  1. 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));
    }
  2. 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));
  3. 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

  1. added
    engineflutter/engine related. See also e: labels.
    c: fatal crashCrashes that terminate the process
    e: impellerImpeller rendering backend issues and features requests
    on Aug 5, 2026
  2. cbracken commented on Aug 5, 2026

    @cbracken
    MemberAuthor

    /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.

  3. added
    P2Important issues not at the top of the work list
    on Aug 10, 2026
  4. flar commented on Aug 10, 2026

    @flar
    Contributor

    Suggested fix sounds good to me.

  5. self-assigned this
    on Aug 14, 2026
  6. github-actions commented on Aug 29, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  7. locked as resolved and limited conversation to collaborators on Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listc: fatal crashCrashes that terminate the processe: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.team-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions