Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 4b7380b

Browse files
authored
[web] Workaround for compositing bug in chrome/webkit (#18128)
* Fix compositing error when multiple canvas(s) are reused * address review comments * track all element counts
1 parent 8e91235 commit 4b7380b

File tree

3 files changed

+104
-28
lines changed

3 files changed

+104
-28
lines changed

lib/web_ui/lib/src/engine/canvas_pool.dart

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ part of engine;
1010
/// [BitmapCanvas] signals allocation of first canvas using allocateCanvas.
1111
/// When a painting command such as drawImage or drawParagraph requires
1212
/// multiple canvases for correct compositing, it calls [closeCurrentCanvas]
13-
/// and adds the canvas(s) to a [_pool] of active canvas(s).
13+
/// and adds the canvas(s) to [_activeCanvasList].
1414
///
1515
/// To make sure transformations and clips are preserved correctly when a new
1616
/// canvas is allocated, [_CanvasPool] replays the current stack on the newly
@@ -25,14 +25,16 @@ class _CanvasPool extends _SaveStackTracking {
2525
ContextStateHandle _contextHandle;
2626
final int _widthInBitmapPixels, _heightInBitmapPixels;
2727
// List of canvases that have been allocated and used in this paint cycle.
28-
List<html.CanvasElement> _pool;
28+
List<html.CanvasElement> _activeCanvasList;
2929
// List of canvases available to reuse from prior paint cycle.
3030
List<html.CanvasElement> _reusablePool;
3131
// Current canvas element or null if marked for lazy allocation.
3232
html.CanvasElement _canvas;
3333

3434
html.HtmlElement _rootElement;
3535
int _saveContextCount = 0;
36+
// Number of elements that have been added to flt-canvas.
37+
int _activeElementCount = 0;
3638

3739
_CanvasPool(this._widthInBitmapPixels, this._heightInBitmapPixels);
3840

@@ -66,12 +68,13 @@ class _CanvasPool extends _SaveStackTracking {
6668
if (_canvas != null) {
6769
_restoreContextSave();
6870
_contextHandle.reset();
69-
_pool ??= [];
70-
_pool.add(_canvas);
71+
_activeCanvasList ??= [];
72+
_activeCanvasList.add(_canvas);
7173
_canvas = null;
7274
_context = null;
7375
_contextHandle = null;
7476
}
77+
_activeElementCount++;
7578
}
7679

7780
void allocateCanvas(html.HtmlElement rootElement) {
@@ -83,8 +86,8 @@ class _CanvasPool extends _SaveStackTracking {
8386
bool reused = false;
8487
if (_reusablePool != null && _reusablePool.isNotEmpty) {
8588
_canvas = _reusablePool.removeAt(0);
86-
reused = true;
8789
requiresClearRect = true;
90+
reused = true;
8891
} else {
8992
// Compute the final CSS canvas size given the actual pixel count we
9093
// allocated. This is done for the following reasons:
@@ -120,24 +123,16 @@ class _CanvasPool extends _SaveStackTracking {
120123
if (_rootElement.lastChild != _canvas) {
121124
_rootElement.append(_canvas);
122125
}
123-
// When the picture has a 90-degree transform and clip in its
124-
// ancestor layers, it triggers a bug in Blink and Webkit browsers
125-
// that results in canvas obscuring text that should be painted on
126-
// top. Setting z-index to any negative value works around the bug.
127-
// This workaround only works with the first canvas. If more than
128-
// one element have negative z-index, the bug is triggered again.
129-
//
130-
// Possible Blink bugs that are causing this:
131-
// * https://bugs.chromium.org/p/chromium/issues/detail?id=370604
132-
// * https://bugs.chromium.org/p/chromium/issues/detail?id=586601
133-
if (_rootElement.firstChild == _canvas) {
126+
127+
if (_activeElementCount == 0) {
134128
_canvas.style.zIndex = '-1';
135129
} else if (reused) {
136130
// If a canvas is the first element we set z-index = -1 to workaround
137131
// blink compositing bug. To make sure this does not leak when reused
138132
// reset z-index.
139133
_canvas.style.removeProperty('z-index');
140134
}
135+
++_activeElementCount;
141136

142137
_context = _canvas.context2D;
143138
_contextHandle = ContextStateHandle(_context);
@@ -250,16 +245,17 @@ class _CanvasPool extends _SaveStackTracking {
250245
if (_canvas != null) {
251246
_restoreContextSave();
252247
_contextHandle.reset();
253-
_pool ??= [];
254-
_pool.add(_canvas);
248+
_activeCanvasList ??= [];
249+
_activeCanvasList.add(_canvas);
255250
_context = null;
256251
_contextHandle = null;
257252
}
258-
_reusablePool = _pool;
259-
_pool = null;
253+
_reusablePool = _activeCanvasList;
254+
_activeCanvasList = null;
260255
_canvas = null;
261256
_context = null;
262257
_contextHandle = null;
258+
_activeElementCount = 0;
263259
}
264260

265261
void endOfPaint() {
@@ -666,19 +662,19 @@ class _CanvasPool extends _SaveStackTracking {
666662
if (browserEngine == BrowserEngine.webkit && _canvas != null) {
667663
_canvas.width = _canvas.height = 0;
668664
}
669-
_clearPool();
665+
_clearActiveCanvasList();
670666
}
671667

672-
void _clearPool() {
673-
if (_pool != null) {
674-
for (html.CanvasElement c in _pool) {
668+
void _clearActiveCanvasList() {
669+
if (_activeCanvasList != null) {
670+
for (html.CanvasElement c in _activeCanvasList) {
675671
if (browserEngine == BrowserEngine.webkit) {
676672
c.width = c.height = 0;
677673
}
678674
c.remove();
679675
}
680676
}
681-
_pool = null;
677+
_activeCanvasList = null;
682678
}
683679
}
684680

lib/web_ui/lib/src/engine/surface/clip.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ mixin _DomClip on PersistedContainerSurface {
2828
if (!debugShowClipLayers) {
2929
// Hide overflow in production mode. When debugging we want to see the
3030
// clipped picture in full.
31-
element.style.overflow = 'hidden';
31+
element.style
32+
..overflow = 'hidden'
33+
..zIndex = '0';
3234
} else {
3335
// Display the outline of the clipping region. When debugShowClipLayers is
3436
// `true` we don't hide clip overflow (see above). This outline helps
@@ -41,6 +43,7 @@ mixin _DomClip on PersistedContainerSurface {
4143
_surfaceStatsFor(this).allocatedDomNodeCount++;
4244
}
4345
_childContainer.style.position = 'absolute';
46+
4447
element.append(_childContainer);
4548
return element;
4649
}

lib/web_ui/test/compositing_test.dart

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
// @dart = 2.6
66
import 'dart:html' as html;
7-
7+
import 'dart:js_util' as js_util;
88
import 'package:ui/src/engine.dart';
99
import 'package:ui/ui.dart';
1010

@@ -183,7 +183,7 @@ void main() {
183183
//
184184
// When BitmapCanvas uses multiple elements to paint, the very first
185185
// canvas needs to have a -1 zIndex so it can preserve compositing order.
186-
test('First canvas element should retain -1 zIndex after update', () async {
186+
test('Canvas element should retain -1 zIndex after update', () async {
187187
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
188188
final Picture picture1 = _drawPicture();
189189
EngineLayer oldLayer = builder.pushClipRect(
@@ -208,6 +208,35 @@ void main() {
208208
html.HtmlElement contentAfterReuse = builder2.build().webOnlyRootElement;
209209
expect(contentAfterReuse.querySelector('canvas').style.zIndex, '-1');
210210
});
211+
212+
test('Multiple canvas elements should retain zIndex after update', () async {
213+
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
214+
final Picture picture1 = _drawPathImagePath();
215+
EngineLayer oldLayer = builder.pushClipRect(
216+
const Rect.fromLTRB(10, 10, 300, 300),
217+
);
218+
builder.addPicture(Offset.zero, picture1);
219+
builder.pop();
220+
221+
html.HtmlElement content = builder.build().webOnlyRootElement;
222+
expect(content.querySelector('canvas').style.zIndex, '-1');
223+
224+
// Force update to scene which will utilize reuse code path.
225+
final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder();
226+
builder2.pushClipRect(
227+
const Rect.fromLTRB(5, 10, 300, 300),
228+
oldLayer: oldLayer
229+
);
230+
final Picture picture2 = _drawPathImagePath();
231+
builder2.addPicture(Offset.zero, picture2);
232+
builder2.pop();
233+
234+
html.HtmlElement contentAfterReuse = builder2.build().webOnlyRootElement;
235+
List<html.CanvasElement> list =
236+
contentAfterReuse.querySelectorAll('canvas');
237+
expect(list[0].style.zIndex, '-1');
238+
expect(list[1].style.zIndex, '');
239+
});
211240
});
212241

213242
}
@@ -374,3 +403,51 @@ Picture _drawPicture() {
374403
..color = const Color.fromRGBO(0, 0, 255, 1));
375404
return recorder.endRecording();
376405
}
406+
407+
Picture _drawPathImagePath() {
408+
const double offsetX = 50;
409+
const double offsetY = 50;
410+
final EnginePictureRecorder recorder = PictureRecorder();
411+
final RecordingCanvas canvas =
412+
recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
413+
canvas.drawCircle(
414+
Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill);
415+
canvas.drawCircle(
416+
Offset(offsetX + 60, offsetY + 10),
417+
10,
418+
Paint()
419+
..style = PaintingStyle.fill
420+
..color = const Color.fromRGBO(255, 0, 0, 1));
421+
canvas.drawCircle(
422+
Offset(offsetX + 10, offsetY + 60),
423+
10,
424+
Paint()
425+
..style = PaintingStyle.fill
426+
..color = const Color.fromRGBO(0, 255, 0, 1));
427+
canvas.drawImage(createTestImage(), Offset(0, 0), Paint());
428+
canvas.drawCircle(
429+
Offset(offsetX + 60, offsetY + 60),
430+
10,
431+
Paint()
432+
..style = PaintingStyle.fill
433+
..color = const Color.fromRGBO(0, 0, 255, 1));
434+
return recorder.endRecording();
435+
}
436+
437+
HtmlImage createTestImage({int width = 100, int height = 50}) {
438+
html.CanvasElement canvas =
439+
new html.CanvasElement(width: width, height: height);
440+
html.CanvasRenderingContext2D ctx = canvas.context2D;
441+
ctx.fillStyle = '#E04040';
442+
ctx.fillRect(0, 0, 33, 50);
443+
ctx.fill();
444+
ctx.fillStyle = '#40E080';
445+
ctx.fillRect(33, 0, 33, 50);
446+
ctx.fill();
447+
ctx.fillStyle = '#2040E0';
448+
ctx.fillRect(66, 0, 33, 50);
449+
ctx.fill();
450+
html.ImageElement imageElement = html.ImageElement();
451+
imageElement.src = js_util.callMethod(canvas, 'toDataURL', <dynamic>[]);
452+
return HtmlImage(imageElement, width, height);
453+
}

0 commit comments

Comments
 (0)