Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 43 additions & 25 deletions lib/web_ui/lib/src/engine/canvaskit/surface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class Surface {
/// due to the browser tab becoming dormant.
final html.Element htmlElement = html.Element.tag('flt-canvas-container');

/// The underlying `<canvas>` element used for this surface.
html.CanvasElement? htmlCanvas;
int _pixelWidth = -1;
int _pixelHeight = -1;

/// Specify the GPU resource cache limits.
void setSkiaResourceCacheMaxBytes(int bytes) {
_skiaCacheBytes = bytes;
Expand Down Expand Up @@ -102,6 +107,7 @@ class Surface {
}

ui.Size? _currentSize;
double _currentDevicePixelRatio = -1;

CkSurface _createOrUpdateSurfaces(ui.Size size) {
if (size.isEmpty) {
Expand All @@ -116,9 +122,13 @@ class Surface {
size.width <= previousSize.width &&
size.height <= previousSize.height) {
// The existing surface is still reusable.
if (window.devicePixelRatio != _currentDevicePixelRatio) {
_updateLogicalHtmlCanvasSize();
}
return _surface!;
}

_currentDevicePixelRatio = window.devicePixelRatio;
_currentSize = _currentSize == null
// First frame. Allocate a canvas of the exact size as the window. The
// window is frequently never resized, particularly on mobile, so using
Expand All @@ -131,36 +141,44 @@ class Surface {
_surface = null;
_addedToScene = false;

return _surface = _wrapHtmlCanvas(_currentSize!);
return _surface = _createNewSurface(_currentSize!);
}

CkSurface _wrapHtmlCanvas(ui.Size physicalSize) {
// Clear the container, if it's not empty.
while (htmlElement.firstChild != null) {
htmlElement.firstChild!.remove();
}
/// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device
/// pixels.
///
/// The logical size of the canvas is not based on the size of the window
/// but on the size of the canvas, which, due to `ceil()` above, may not be
/// the same as the window. We do not round/floor/ceil the logical size as
/// CSS pixels can contain more than one physical pixel and therefore to
/// match the size of the window precisely we use the most precise floating
/// point value we can get.
void _updateLogicalHtmlCanvasSize() {
final double logicalWidth = _pixelWidth / ui.window.devicePixelRatio;
final double logicalHeight = _pixelHeight / ui.window.devicePixelRatio;
htmlCanvas!.style
..width = '${logicalWidth}px'
..height = '${logicalHeight}px';
}

/// This function is expensive.
///
/// It's better to reuse surface if possible.
CkSurface _createNewSurface(ui.Size physicalSize) {
// Clear the container, if it's not empty. We're going to create a new <canvas>.
this.htmlCanvas?.remove();

// If `physicalSize` is not precise, use a slightly bigger canvas. This way
// we ensure that the rendred picture covers the entire browser window.
final int pixelWidth = physicalSize.width.ceil();
final int pixelHeight = physicalSize.height.ceil();
_pixelWidth = physicalSize.width.ceil();
_pixelHeight = physicalSize.height.ceil();
final html.CanvasElement htmlCanvas = html.CanvasElement(
width: pixelWidth,
height: pixelHeight,
width: _pixelWidth,
height: _pixelHeight,
);

// The logical size of the canvas is not based on the size of the window
// but on the size of the canvas, which, due to `ceil()` above, may not be
// the same as the window. We do not round/floor/ceil the logical size as
// CSS pixels can contain more than one physical pixel and therefore to
// match the size of the window precisely we use the most precise floating
// point value we can get.
final double logicalWidth = pixelWidth / ui.window.devicePixelRatio;
final double logicalHeight = pixelHeight / ui.window.devicePixelRatio;
htmlCanvas.style
..position = 'absolute'
..width = '${logicalWidth}px'
..height = '${logicalHeight}px';
this.htmlCanvas = htmlCanvas;
htmlCanvas.style.position = 'absolute';
_updateLogicalHtmlCanvasSize();

// When the browser tab using WebGL goes dormant the browser and/or OS may
// decide to clear GPU resources to let other tabs/programs use the GPU.
Expand Down Expand Up @@ -212,8 +230,8 @@ class Surface {

SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface(
_grContext!,
pixelWidth,
pixelHeight,
_pixelWidth,
_pixelHeight,
SkColorSpaceSRGB,
);

Expand Down
37 changes: 37 additions & 0 deletions lib/web_ui/test/canvaskit/surface_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ void testMain() {
// Expect exact requested dimensions.
expect(original.width(), 9);
expect(original.height(), 19);
expect(surface.htmlCanvas!.style.width, '9px');
expect(surface.htmlCanvas!.style.height, '19px');

// Shrinking reuses the existing surface straight-up.
final CkSurface shrunk = surface.acquireFrame(ui.Size(5, 15)).skiaSurface;
expect(shrunk, same(original));
expect(surface.htmlCanvas!.style.width, '9px');
expect(surface.htmlCanvas!.style.height, '19px');

// The first increase will allocate a new surface, but will overallocate
// by 40% to accommodate future increases.
Expand All @@ -40,6 +44,8 @@ void testMain() {
// Expect overallocated dimensions
expect(firstIncrease.width(), 14);
expect(firstIncrease.height(), 28);
expect(surface.htmlCanvas!.style.width, '14px');
expect(surface.htmlCanvas!.style.height, '28px');

// Subsequent increases within 40% reuse the old surface.
final CkSurface secondIncrease = surface.acquireFrame(ui.Size(11, 22)).skiaSurface;
Expand All @@ -52,6 +58,8 @@ void testMain() {
// Also over-allocated
expect(huge.width(), 28);
expect(huge.height(), 56);
expect(surface.htmlCanvas!.style.width, '28px');
expect(surface.htmlCanvas!.style.height, '56px');

// Shrink again. Reuse the last allocated surface.
final CkSurface shrunk2 = surface.acquireFrame(ui.Size(5, 15)).skiaSurface;
Expand Down Expand Up @@ -88,5 +96,34 @@ void testMain() {
// Firefox doesn't have the WEBGL_lose_context extension.
skip: isFirefox || isIosSafari,
);

// Regression test for https://github.com/flutter/flutter/issues/75286
test('updates canvas logical size when device-pixel ratio changes', () {
final Surface surface = Surface(HtmlViewEmbedder());
final CkSurface original = surface.acquireFrame(ui.Size(10, 16)).skiaSurface;

expect(original.width(), 10);
expect(original.height(), 16);
expect(surface.htmlCanvas!.style.width, '10px');
expect(surface.htmlCanvas!.style.height, '16px');

// Increase device-pixel ratio: this makes CSS pixels bigger, so we need
// fewer of them to cover the browser window.
window.debugOverrideDevicePixelRatio(2.0);
final CkSurface highDpr = surface.acquireFrame(ui.Size(10, 16)).skiaSurface;
expect(highDpr.width(), 10);
expect(highDpr.height(), 16);
expect(surface.htmlCanvas!.style.width, '5px');
expect(surface.htmlCanvas!.style.height, '8px');

// Decrease device-pixel ratio: this makes CSS pixels smaller, so we need
// more of them to cover the browser window.
window.debugOverrideDevicePixelRatio(0.5);
final CkSurface lowDpr = surface.acquireFrame(ui.Size(10, 16)).skiaSurface;
expect(lowDpr.width(), 10);
expect(lowDpr.height(), 16);
expect(surface.htmlCanvas!.style.width, '20px');
expect(surface.htmlCanvas!.style.height, '32px');
});
}, skip: isIosSafari);
}