From 01321d6bcd33e8278cc8145e9efee03434568c5a Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 2 Feb 2021 17:37:13 -0800 Subject: [PATCH] [canvaskit] update CSS size of the canvas when device-pixel ratio changes --- .../lib/src/engine/canvaskit/surface.dart | 68 ++++++++++++------- lib/web_ui/test/canvaskit/surface_test.dart | 37 ++++++++++ 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index de9c75aa3b29c..7ad4b2aec1c93 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -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 `` 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; @@ -102,6 +107,7 @@ class Surface { } ui.Size? _currentSize; + double _currentDevicePixelRatio = -1; CkSurface _createOrUpdateSurfaces(ui.Size size) { if (size.isEmpty) { @@ -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 @@ -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 . + 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. @@ -212,8 +230,8 @@ class Surface { SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface( _grContext!, - pixelWidth, - pixelHeight, + _pixelWidth, + _pixelHeight, SkColorSpaceSRGB, ); diff --git a/lib/web_ui/test/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index b5c98bae3661c..ddf45c1f9c926 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -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. @@ -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; @@ -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; @@ -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); }