Skip to content

UberSDF round rectangles are sometimes incorrectly drawn as non-rounded rectangles #189807

Description

@b-luk

Affects platforms where UberSDF is enabled (desktop platforms).

Demo:
void main() => runApp(
  CustomPaint(
    painter: _RRectPainter(),
    size: Size.infinite,
  ),
);
class _RRectPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()..color = Colors.blue;
    // Buggy: fractional width (64.2)
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        const Rect.fromLTWH(100, 100, 64.2, 40.0),
        const Radius.circular(20),
      ),
      paint,
    );
    // Working: integer width (64.0)
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        const Rect.fromLTWH(100, 160, 64.0, 40.0),
        const Radius.circular(20),
      ),
      paint,
    );
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Expected: two rounded rectangles

Actual:
Image
The first rounded rectangle, with fractional width, renders as a non-rounded rectangle.

Root cause and fix

Rect::ExpandToMinTransformedSize always constructs a new expanded rectangle from the input Rect from its center point and half-extents.

If the input rectangle is already large enough and does not need expansion, this should return an identical rectangle. The returned rectangle is equality-checked against the input in Canvas::DrawRoundRect to determine whether expansion happened. That code assumes that if the output does not equal the input, pixel-expansion happened, which means that the rounded rect had sub-pixel dimensions and can be safely drawn as a non-rounded rectangle.

The issue is that for certain cases, particularly with non-integer sizes, ExpandToMinTransformedSize returns a different rectangle even if no expansion happened. This is due to small floating point imprecisions when constructing the new rectangle using the input rectangle's center and half-extents. If the returned rectangle incorrectly is not equal to the input, a non-rounded rectangle gets drawn instead of a rounded rectangle.

To fix this, ExpandToMinTransformedSize should not construct a new rectangle unless expansion is actually needed.

Activity

  1. self-assigned this
    on Jul 21, 2026
  2. added
    c: renderingUI glitches reported at the engine/skia or impeller rendering level
    e: impellerImpeller rendering backend issues and features requests
    on Jul 21, 2026
  3. b-luk commented on Jul 21, 2026

    @b-luk
    ContributorAuthor

    Originally reported internally: b/537444793

  4. flar commented on Jul 22, 2026

    @flar
    Contributor

    I actually think the optimization to simplify the output to a normal rectangle is a bad idea as well. Fixing that will fix the problem without worrying whether we slightly disturb the bit-precision of the result.

    The reason it is an issue is that a rounded rectangle will have ends that are less opaque than the pixels along its length even when it is just a single pixel across. As a result, if we continue to allow simplifying it to a simple rect we will see a snap when the round rect goes from 0.99 pixels wide to 1.0 pixels wide. The 0.99 pixel wide round rect will actually have more opaque ends than the one at 1.0 pixels wide. Growing the round rect would make it dimmer (on the end pixels). That's a problem.

    So, I think the main way this should be fixed should be to not simplify the round rect to a rectangle when it "appears" to have been adjusted. Doing that would fix the problem identified in the issue as well as other problems we would see with rectangles that are transitioning across the pixel wide threshold.

  5. Razorex001 commented on Jul 22, 2026

    @Razorex001

    I can reproduce this on Windows with Impeller OpenGLESSDF using ordinary Flutter widgets. Seven cards are assigned fractional widths through (constraints.maxWidth - spacing) / 7. A Container with a rounded BoxDecoration renders square corners under Impeller, while Skia renders it correctly. Note how it happens only for some of the cards. This appears consistent with the fractional-width root cause described here.

    see these screenshots with impeller (1st one) and with skia (2nd one):

    Image Image
  6. github-actions commented on Aug 5, 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 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

c: renderingUI glitches reported at the engine/skia or impeller rendering levele: impellerImpeller rendering backend issues and features requeststeam-engineOwned by Engine team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions