Skip to content

Variety of issues with Impeller drawing thin lines/rects #188590

Description

@b-luk

On Impeller, various issues and inconsistencies are present when drawing thin (strokeWidth less than 1 pixel) lines.

Lines are drawn via one of two underlying API calls: drawLine and drawRect.

  • drawLine is the simplest and most straightforward way to draw a line between two points.
  • drawRect with a filled rectangle is mathematically equivalent to drawing an axis-aligned line with length and stroke width equal to the rectangle's width and height.

Note that drawPath can also draw a path that is equivalent to these same line or axis aligned rectangle shapes. Our drawPath DisplayList code simplifies these degenerate cases to use drawRect and drawLine.

drawLine is the most straightforward way to draw a line. In practice, it is also very common for drawRect to be used to draw lines. One common case is borders in the Flutter framework, which are drawn using linear or rectangular paths, which degenerate to use drawLine or drawRect.

Impeller has different rendering paths for drawLine and drawRect, each of which can be rendered in two different ways.

  • drawLine
    • Is drawn with SDF-based LineGeometry and LineContents when the antialiased_lines or use_sdfs flag is enabled
    • Otherwise is drawn with tesselation based LineGeometry.
  • drawRect for a filled rectangle
    • Is drawn with UberSDF when the use_sdfs flag is enabled.
    • Otherwise is drawn with tesslation based FillRectGeometry.

For all of these rendering methods, thin lines have rendering issues or inconsistencies. These may be related to hairline pixel alignment. I made an example demo app, which renders the same thin horizontal line using both drawRect and drawLine, at a variety of y offset values to demonstrate pixel alignment issues.

Demo app
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InteractiveViewer(
      maxScale: 10,
      child: Container(
        color: Colors.black,
        child: CustomPaint(painter: TestPainter()),
      ),
    );
  }
}

class TestPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Length and stroke width for our lines
    double length = 50.0;
    double width = 0.5;

    double padding = 20.0;

    // Paint for circles marking start and end points
    Paint startPaint = Paint()..color = Colors.green;
    Paint endPaint = Paint()..color = Colors.red;

    // Paint for lines drawn with drawRect
    Paint rectPaint = Paint()..color = Colors.white;

    // Paint for lines drawn with drawLine
    Paint linePaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.stroke
      ..strokeWidth = width;

    canvas.translate(20, 20);

    // Loop and draw lines with y-offset values of X.RC, where R is a digit
    // corresponding to the row and C is a digit corresponding to the column.
    // R and C each go from 0 to 9.
    for (int row = 0; row < 10; row++) {
      for (int column = 0; column < 10; column++) {
        double x = column * (length + padding);
        double y = row * padding;

        // Add y offset with tenths and hundreths digits from row and column.
        y += row * 0.1 + column * 0.01;

        canvas.drawCircle(Offset(x, y), 2, startPaint);
        canvas.drawCircle(Offset(x + length, y), 2, endPaint);
        canvas.drawRect(Rect.fromLTWH(x, y, length, width), rectPaint);

        canvas.save();
        canvas.translate(0, 250);
        canvas.drawCircle(Offset(x, y), 2, startPaint);
        canvas.drawCircle(Offset(x + length, y), 2, endPaint);
        Offset p0 = Offset(x, y + width * 0.5);
        Offset p1 = p0.translate(length, 0);
        canvas.drawLine(p0, p1, linePaint);
        canvas.restore();
      }
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Screenshots (click for big):

UberSDF drawRect and AA (LineContents) drawLine:
Image

Tessellation (non-SDF):
Image

Skia for comparison:
Image

Summary of findings

  • Skia is closest to what I would expect.
    • Its drawRect vs drawLine look identical
    • The 0.5 width lines are a middlish-gray color, and the 0.15 width lines are a barely visible gray. That's what I'd expect for these width values.
    • It does still have some small inconsistencies with the brightness of the lines. Some are slightly darker/lighter or thinner/thicker looking. But the range is pretty narrow so they look fairly consistent overall.
  • UberSDF drawRect: For both 0.5 and 0.15 line width, it has lines with brightness ranging from invisible to almost white. The almost white lines are clearly brighter than we'd expect for a 0.5 and especially for a 0.15 line width. And the invisible lines are an egregious rendering issue.
  • LineContents AA lines: These have a pretty consistent brightness. However, they're way too bright. The 0.5 and 0.15 line widths look equally bright, which is wrong. It looks like there may be something clamping lines to a minimum width/brightness that is incorrect for very thin lines like this.
  • FillRectGeometry tessellation: The 0.5 width lines have some brightness inconsistencies, but they're not too bad compared to some of the other problems we see. With 0.15 line width, many lines are invisible which is a similar egregious issue to the UberSDF drawRect's invisible lines.
  • LineGeometry tessellation: These lines are brighter than I would expect for 0.5 and 0.15 width lines. There are moderate inconsistencies with their brightness.
  • Impeller's results are different for each of the four rendering paths. None of the four are the same as any other of the four.
    • The differences between the drawRect and drawLine results may end up as unexpected rendering differences in Flutter apps. Using the Flutter framework borders as an example, Flutter apps can draw lines with drawLine or drawRect sometimes interchangeably, with the expectation that geometrically equivalent lines/rects will be drawn identically.

I created some sub-issues that cover some of these specific problems. I didn't create one for the problem of the lines just having inconsistent brightness. Not sure if there needs to be a separate issue for that, of it'll just be fixed if all the other issues are fixed, or if it's a subtle enough problem that it doesn't need to be addressed.

Related previous issue

#162378 is an old issue regarding inconsistent brightnesses when rendering borders. It's marked as fixed, but the issue is still present and worse than before now. It either regressed, or was never completely fixed. The example app in that issue draws borders with line width 0.15. These get rendered using drawRect. As the screenshots above show, both UberSDF drawRect and tessellation drawRect have missing lines for many pixel offsets. Running the example app with SDFs disabled/enabled gives the following results. The example screenshot in the original issue just showed inconsistencies with line brightness. But running this now shows even worse results that have missing lines.

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: renderingUI glitches reported at the engine/skia or impeller rendering levele: impellerImpeller rendering backend issues and features requeststeam-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