Summary
On Windows, the new default Impeller backend (banner: Using the Impeller rendering backend (OpenGLESSDF)) renders stroked lines and paths with a roughly constant +1.3–1.6 physical px of extra coverage ("ink"), independent of stroke width, compared to Skia on the same machine. For wide strokes this is a subtle softening; for strokes in the 0.5–2 logical px range it makes lines read 1.7×–2.7× thicker and noticeably darker. Sub-pixel strokes get no coverage compensation (Skia renders a 0.75-physical-px line as faint ~38% gray; OpenGLESSDF renders a ~2px band with a 76%-opacity core).
drawLine and drawPath produce identical results, so this is the stroke rasterization itself, not an API-specific path. Filled paths look correct.
Context: we develop a desktop design tool (Figma-like). After moving to master for Impeller-on-Windows, all thin-stroke UI — 1.2px custom-painted toolbar icons, 0.5px snap-guide hairlines — reads 2–4× bolder than intended. The same code on macOS (Impeller/Metal) renders at expected weight, so this appears specific to the SDF rasterizer rather than Impeller generally.
Steps to reproduce
- Run the code sample below on Windows on master:
flutter run -d windows --debug → banner reports OpenGLESSDF.
- Run again with
--no-enable-impeller → Skia.
- Screenshot both (150% display scaling, DPR 1.5) and scan a vertical pixel column through the lines.
Code sample (complete main.dart)
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: TestPage()));
class TestPage extends StatelessWidget {
const TestPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: CustomPaint(size: Size.infinite, painter: LinePainter()),
);
}
}
/// Horizontal black lines at fixed logical y positions, alternating
/// drawLine vs drawPath at matching stroke widths, so a vertical pixel
/// scan of a screenshot can compare rendered thickness per API.
class LinePainter extends CustomPainter {
static const rows = <(double y, double width, bool useLine, StrokeCap cap)>[
(40, 0.5, true, StrokeCap.butt),
(80, 0.5, false, StrokeCap.butt),
(120, 1.0, true, StrokeCap.butt),
(160, 1.0, false, StrokeCap.butt),
(200, 1.19, true, StrokeCap.round),
(240, 1.19, false, StrokeCap.round),
(280, 2.0, true, StrokeCap.butt),
(320, 2.0, false, StrokeCap.butt),
(360, 4.0, true, StrokeCap.butt),
(400, 4.0, false, StrokeCap.butt),
];
@override
void paint(Canvas canvas, Size size) {
for (final (y, w, useLine, cap) in rows) {
final paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = w
..strokeCap = cap;
final a = Offset(20, y);
final b = Offset(size.width - 20, y);
if (useLine) {
canvas.drawLine(a, b, paint);
} else {
canvas.drawPath(Path()..moveTo(a.dx, a.dy)..lineTo(b.dx, b.dy), paint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Measured results
Luminance profiles from an OS-level screen capture (values 0–255, white background = 255), vertical scan through the lines, away from the caps. "Ink" = Σ(255 − lum)/255, i.e. total coverage in physical px. DPR 1.5.
| strokeWidth (logical → physical) |
Skia profile |
Skia ink |
OpenGLESSDF profile |
SDF ink |
ratio |
| 0.5 → 0.75 px |
[159,159] |
0.75 |
[193,62,62,193] |
2.00 |
2.7× |
| 1.0 → 1.5 px |
[64,64] |
1.50 |
[150,5,5,150] |
2.78 |
1.9× |
| 1.19 → 1.79 px |
[28,28] |
1.78 |
[124,0,0,124] |
3.03 |
1.7× |
| 2.0 → 3.0 px |
[127,0,0,128] |
3.00 |
[176,4,0,0,4,176] |
4.59 |
1.5× |
| 4.0 → 6.0 px |
[0,0,0,0,0,0] |
6.00 |
[64,0,0,0,0,0,0,64] |
7.50 |
1.25× |
Two observations:
- Skia's total coverage equals the stroke width exactly at every row (analytic coverage AA). OpenGLESSDF adds a near-constant +1.24 to +1.59 px of coverage regardless of width — consistent with a fixed-width smoothstep/falloff around the stroke edge rather than derivative-scaled coverage.
- Sub-pixel strokes are not coverage-compensated: Skia renders the 0.75px line as 2 columns of ~38% gray (faint, reads "thin"); OpenGLESSDF renders a 76%-opacity 2px core plus fringes — 2.7× the ink, and much darker.
drawLine and drawPath rows are pixel-identical in both backends. The 1.19 rows use StrokeCap.round, the rest StrokeCap.butt — the inflation is the same across both. The effect is most visible on axis-aligned lines (fringe accumulates into uniform darker bands); diagonal/curved strokes read as softening.
Expected results
Thin strokes match Skia's weight/darkness (total coverage ≈ stroke width; sub-pixel strokes proportionally fainter), as Impeller/Metal on macOS does.
Actual results
Constant additive fringe at all widths; near-full-opacity core even for sub-pixel strokes. Hairline-heavy UIs (dividers, guides, stroked icons) visibly change weight between Skia/macOS and Windows-OpenGLESSDF.
Possibly related
Environment
[✓] Flutter (Channel master, 3.46.0-1.0.pre-364, on Microsoft Windows [Version 10.0.26200.8655], locale en-IL)
• Framework revision 7400c96c37 (2026-07-01)
• Engine revision 7400c96c37
• Dart version 3.13.0 (build 3.13.0-256.0.dev)
[✓] Windows Version (Windows 11 or higher, 25H2, 2009)
GPU: Intel(R) Graphics (iGPU), driver 32.0.101.7082. Display at 150% scaling (DPR 1.5).
Renderer banner (default run): [IMPORTANT:flutter/shell/platform/embedder/embedder_surface_gl_impeller.cc(126)] Using the Impeller rendering backend (OpenGLESSDF).
Side-by-side screenshots available — happy to attach.
Summary
On Windows, the new default Impeller backend (banner:
Using the Impeller rendering backend (OpenGLESSDF)) renders stroked lines and paths with a roughly constant +1.3–1.6 physical px of extra coverage ("ink"), independent of stroke width, compared to Skia on the same machine. For wide strokes this is a subtle softening; for strokes in the 0.5–2 logical px range it makes lines read 1.7×–2.7× thicker and noticeably darker. Sub-pixel strokes get no coverage compensation (Skia renders a 0.75-physical-px line as faint ~38% gray; OpenGLESSDF renders a ~2px band with a 76%-opacity core).drawLineanddrawPathproduce identical results, so this is the stroke rasterization itself, not an API-specific path. Filled paths look correct.Context: we develop a desktop design tool (Figma-like). After moving to master for Impeller-on-Windows, all thin-stroke UI — 1.2px custom-painted toolbar icons, 0.5px snap-guide hairlines — reads 2–4× bolder than intended. The same code on macOS (Impeller/Metal) renders at expected weight, so this appears specific to the SDF rasterizer rather than Impeller generally.
Steps to reproduce
flutter run -d windows --debug→ banner reportsOpenGLESSDF.--no-enable-impeller→ Skia.Code sample (complete main.dart)
Measured results
Luminance profiles from an OS-level screen capture (values 0–255, white background = 255), vertical scan through the lines, away from the caps. "Ink" = Σ(255 − lum)/255, i.e. total coverage in physical px. DPR 1.5.
[159,159][193,62,62,193][64,64][150,5,5,150][28,28][124,0,0,124][127,0,0,128][176,4,0,0,4,176][0,0,0,0,0,0][64,0,0,0,0,0,0,64]Two observations:
drawLineanddrawPathrows are pixel-identical in both backends. The 1.19 rows useStrokeCap.round, the restStrokeCap.butt— the inflation is the same across both. The effect is most visible on axis-aligned lines (fringe accumulates into uniform darker bands); diagonal/curved strokes read as softening.Expected results
Thin strokes match Skia's weight/darkness (total coverage ≈ stroke width; sub-pixel strokes proportionally fainter), as Impeller/Metal on macOS does.
Actual results
Constant additive fringe at all widths; near-full-opacity core even for sub-pixel strokes. Hairline-heavy UIs (dividers, guides, stroked icons) visibly change weight between Skia/macOS and Windows-OpenGLESSDF.
Possibly related
Environment
GPU: Intel(R) Graphics (iGPU), driver 32.0.101.7082. Display at 150% scaling (DPR 1.5).
Renderer banner (default run):
[IMPORTANT:flutter/shell/platform/embedder/embedder_surface_gl_impeller.cc(126)] Using the Impeller rendering backend (OpenGLESSDF).Side-by-side screenshots available — happy to attach.