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:

Tessellation (non-SDF):

Skia for comparison:

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.

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.
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.
antialiased_linesoruse_sdfsflag is enableduse_sdfsflag is enabled.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
Screenshots (click for big):
UberSDF drawRect and AA (LineContents) drawLine:

Tessellation (non-SDF):

Skia for comparison:

Summary of findings
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.