You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Impeller] GetLocalCoverageLimit unconditionally clamps to root render target, causing incorrect subpass texture sizing and culling #195859
Problem
Canvas::GetLocalCoverageLimit() in engine/src/flutter/impeller/display_list/canvas.cc ends with: return maybe_coverage_limit->Intersection( Rect::MakeSize(render_target_.GetRenderTargetSize()));
This intersects the coverage limit with the root render target size. For the root pass this is a no-op (the pass texture IS the root RT). For subpasses (saveLayer) it's wrong: a subpass texture can extend beyond the root RT — shifted by a translate, or expanded by an image filter — and the root RT intersection clips off the valid portion outside the screen.
The line above already computes pass_texture ∩ clip_coverage, which is the correct bound.
Symptom
Scaled-down overview of a full-size page (e.g. launcher editing mode at 0.3×): the background fills correctly, but child elements beyond a certain distance from the screen center disappear. Also seen as edge artifacts with ImageFilter.blur on layers near the screen edge.
Suggested fix
Make the root RT intersection conditional: std::optional<Rect> Canvas::GetLocalCoverageLimit(bool clamp_to_root = true) const { // ... existing pass_texture ∩ clip logic unchanged ... if (!clamp_to_root) return maybe_coverage_limit; return maybe_coverage_limit->Intersection( Rect::MakeSize(render_target_.GetRenderTargetSize())); }
Pass false from SaveLayer (texture sizing) and DrawDisplayList (cull rect). Root pass behavior unchanged.
We have been running with this fix internally and it resolves the reported symptoms.
Environment
Flutter main branch (also in 3.38.3 stable)
Impeller (Vulkan / OpenGLES)
File: impeller/display_list/canvas.cc, function GetLocalCoverageLimit()
Runnable repro with plain dart:ui (no plugins). A SaveLayer with a matrix image filter moves its content +300 px. Inside it, two circles are drawn at x=-200: one with drawCircle, one through drawPicture. Both should appear at x=100.
import'dart:typed_data';
import'dart:ui'as ui;
import'package:flutter_test/flutter_test.dart';
voidmain() {
test('drawPicture inside translated saveLayer is not culled', () async {
constint w =400, h =200;
final pr = ui.PictureRecorder();
ui.Canvas(pr).drawCircle(const ui.Offset(-200, 150), 30,
ui.Paint()..color =const ui.Color(0xFF00FF00));
final inner = pr.endRecording();
final rec = ui.PictureRecorder();
final c = ui.Canvas(rec, ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble()));
c.drawRect(ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble()),
ui.Paint()..color =const ui.Color(0xFFFFFFFF));
final m =Float64List.fromList(
<double>[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 300, 0, 0, 1]);
c.saveLayer(null, ui.Paint()..imageFilter = ui.ImageFilter.matrix(m));
c.drawCircle(const ui.Offset(-200, 50), 30,
ui.Paint()..color =const ui.Color(0xFFFF0000)); // direct (control)
c.drawPicture(inner); // via DrawDisplayList
c.restore();
final img =await rec.endRecording().toImage(w, h);
final px = (await img.toByteData(format: ui.ImageByteFormat.rawRgba))!
.buffer.asUint8List();
List<int> at(int x, int y) {
final i = (y * w + x) *4;
return [px[i], px[i +1], px[i +2]];
}
expect(at(100, 50), [255, 0, 0]); // direct circleexpect(at(100, 150), [0, 255, 0]); // drawPicture circle
});
}
Results on Flutter 3.38.3, Linux host:
Backend
direct circle
drawPicture circle
flutter test --enable-impeller
visible
missing (white)
flutter test (Skia)
visible
visible
The direct circle shows that the SaveLayer texture is large enough; only the nested DisplayList is culled. Fix: #195860
@caimengnan-ops Thanks for the detailed report. I reproduced this on Flutter 3.47.9 stable and current master 3.50.0 on macOS 27.2. The test fails with Impeller on both versions, while the same test passes under Skia. Labelling and routing it to the engine team for further investigation.
A second case from review on #195860
: the same translated SaveLayer, but the off-screen content is drawn inside a child saveLayer instead of a Picture. The child layer is sized against the root render target, so its off-screen part is dropped.
import'dart:typed_data';
import'dart:ui'as ui;
import'package:flutter_test/flutter_test.dart';
voidmain() {
test('child saveLayer inside translated saveLayer is not culled', () async {
constint w =400, h =200;
final rec = ui.PictureRecorder();
final c = ui.Canvas(rec, ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble()));
c.drawRect(ui.Rect.fromLTWH(0, 0, w.toDouble(), h.toDouble()),
ui.Paint()..color =const ui.Color(0xFFFFFFFF));
final m =Float64List.fromList(
<double>[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 300, 0, 0, 1]);
c.saveLayer(null, ui.Paint()..imageFilter = ui.ImageFilter.matrix(m));
c.drawCircle(const ui.Offset(-200, 50), 30,
ui.Paint()..color =const ui.Color(0xFFFF0000)); // parent layer (control)
c.saveLayer(null, ui.Paint()); // child layer
c.drawCircle(const ui.Offset(-200, 150), 30,
ui.Paint()..color =const ui.Color(0xFF00FF00));
c.restore();
c.restore();
final img =await rec.endRecording().toImage(w, h);
final px = (await img.toByteData(format: ui.ImageByteFormat.rawRgba))!
.buffer.asUint8List();
List<int> at(int x, int y) {
final i = (y * w + x) *4;
return [px[i], px[i +1], px[i +2]];
}
expect(at(100, 50), [255, 0, 0]); // circle in parent layerexpect(at(100, 150), [0, 255, 0]); // circle in child layer
});
}
Activity
Add golden test for DrawDisplayList cull rect not clamped to root RT
caimengnan-ops commentedon Nov 3, 2026
Runnable repro with plain
dart:ui(no plugins). A SaveLayer with a matrix image filter moves its content +300 px. Inside it, two circles are drawn at x=-200: one withdrawCircle, one throughdrawPicture. Both should appear at x=100.Results on Flutter 3.38.3, Linux host:
flutter test --enable-impellerflutter test(Skia)The direct circle shows that the SaveLayer texture is large enough; only the nested DisplayList is culled. Fix: #195860
[Impeller] Do not clamp DrawDisplayList cull rect to the root render …
saurabh-mirajkar commentedon Nov 3, 2026
@caimengnan-ops Thanks for the detailed report. I reproduced this on Flutter 3.47.9 stable and current master 3.50.0 on macOS 27.2. The test fails with
Impelleron both versions, while the same test passes underSkia. Labelling and routing it to the engine team for further investigation.Related: #195666
caimengnan-ops commentedon Nov 7, 2026
A second case from review on
#195860
: the same translated SaveLayer, but the off-screen content is drawn inside a child saveLayer instead of a Picture. The child layer is sized against the root render target, so its off-screen part is dropped.
[Impeller] Do not clamp the local coverage limit to the root render t…