diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 9bb6ef9780c..89b2ad6563e 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -170,11 +170,23 @@ extension type CanvasKit(JSObject _) implements JSObject { bool srcIsPremultiplied, ); - SkImage? MakeLazyImageFromTextureSourceWithInfo(Object src, SkPartialImageInfo info) => - _MakeLazyImageFromTextureSource2(src.toJSAnyShallow, info); + SkImage? MakeLazyImageFromTextureSourceWithInfo(Object src, SkPartialImageInfo info) { + assert( + !CanvasKitRenderer.instance.isSoftware, + 'Cannot use `MakeLazyImageFromTextureSourceWithInfo` in CPU-only mode.', + ); + return _MakeLazyImageFromTextureSource2(src.toJSAnyShallow, info); + } - SkImage? MakeLazyImageFromImageBitmap(DomImageBitmap imageBitmap, bool hasPremultipliedAlpha) => - _MakeLazyImageFromTextureSource3(imageBitmap, 0, hasPremultipliedAlpha); + SkImage? MakeLazyImageFromImageBitmap(DomImageBitmap imageBitmap, bool hasPremultipliedAlpha) { + assert( + !CanvasKitRenderer.instance.isSoftware, + 'Cannot use `MakeLazyImageFromImageBitmap` in CPU-only mode.', + ); + return _MakeLazyImageFromTextureSource3(imageBitmap, 0, hasPremultipliedAlpha); + } + + external SkImage? MakeImageFromCanvasImageSource(JSAny src); } extension type CanvasKitModule(JSObject _) implements JSObject { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart index ed42d1861a5..1bec48d6314 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -115,7 +115,12 @@ class CkResizingCodec extends ResizingCodec { scaledHeight, ); final DomImageBitmap bitmap = offscreenCanvas.transferToImageBitmap(); - final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true); + SkImage? skImage; + if (CanvasKitRenderer.instance.isSoftware) { + skImage = canvasKit.MakeImageFromCanvasImageSource(bitmap); + } else { + skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true); + } // Resize the canvas to 0x0 to cause the browser to eagerly reclaim its // memory. @@ -137,16 +142,21 @@ ui.Image createCkImageFromImageElement( int naturalWidth, int naturalHeight, ) { - final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( - image, - SkPartialImageInfo( - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - colorSpace: SkColorSpaceSRGB, - width: naturalWidth.toDouble(), - height: naturalHeight.toDouble(), - ), - ); + SkImage? skImage; + if (CanvasKitRenderer.instance.isSoftware) { + skImage = canvasKit.MakeImageFromCanvasImageSource(image); + } else { + skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( + image, + SkPartialImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: naturalWidth.toDouble(), + height: naturalHeight.toDouble(), + ), + ); + } if (skImage == null) { throw ImageCodecException('Failed to create image from Image.decode'); } @@ -334,16 +344,7 @@ Future skiaInstantiateWebImageCodec( debugSource: url, ); } else { - final DomBlob blob = createDomBlob([list.buffer]); - final codec = CkImageBlobCodec(blob, chunkCallback: chunkCallback); - - try { - await codec.decode(); - return codec; - } on ImageCodecException { - codec.dispose(); - return CkAnimatedImage.decodeFromBytes(list, url); - } + return CkAnimatedImage.decodeFromBytes(list, url); } } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart index 399fad8f1d0..8864109f522 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart @@ -42,16 +42,26 @@ class CkBrowserImageDecoder extends BrowserImageDecoder { @override ui.Image generateImageFromVideoFrame(VideoFrame frame) { - final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( - frame, - SkPartialImageInfo( - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - colorSpace: SkColorSpaceSRGB, - width: frame.displayWidth, - height: frame.displayHeight, - ), - ); + SkImage? skImage; + if (CanvasKitRenderer.instance.isSoftware) { + final int width = frame.displayWidth.toInt(); + final int height = frame.displayHeight.toInt(); + final DomHTMLCanvasElement canvas = createDomCanvasElement(width: width, height: height); + final DomCanvasRenderingContext2D ctx = canvas.context2D; + ctx.drawImage(frame, 0, 0); + skImage = canvasKit.MakeImageFromCanvasImageSource(canvas); + } else { + skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( + frame, + SkPartialImageInfo( + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + width: frame.displayWidth, + height: frame.displayHeight, + ), + ); + } if (skImage == null) { throw ImageCodecException( "Failed to create image from pixel data decoded using the browser's ImageDecoder.", diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 451319043ac..26018c88a2e 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -23,6 +23,9 @@ class CanvasKitRenderer extends Renderer { @override String get rendererTag => 'canvaskit'; + /// Whether the renderer is using software rendering. + bool get isSoftware => _pictureToImageSurface.isSoftware; + late final FlutterFontCollection _fontCollection = isExperimentalWebParagraph ? WebFontCollection() : SkiaFontCollection(); @@ -211,7 +214,12 @@ class CanvasKitRenderer extends Renderer { @override ui.Image createImageFromImageBitmap(DomImageBitmap imageBitmap) { - final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(imageBitmap, true); + SkImage? skImage; + if (isSoftware) { + skImage = canvasKit.MakeImageFromCanvasImageSource(imageBitmap); + } else { + skImage = canvasKit.MakeLazyImageFromImageBitmap(imageBitmap, true); + } if (skImage == null) { throw Exception('Failed to convert image bitmap to an SkImage.'); } @@ -234,16 +242,31 @@ class CanvasKitRenderer extends Renderer { )); return createImageFromImageBitmap(bitmap); } - final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( - object, - SkPartialImageInfo( - width: width.toDouble(), - height: height.toDouble(), - alphaType: canvasKit.AlphaType.Premul, - colorType: canvasKit.ColorType.RGBA_8888, - colorSpace: SkColorSpaceSRGB, - ), - ); + SkImage? skImage; + if (isSoftware) { + if (object.isA()) { + // If the object is a VideoFrame, we need to draw it to a canvas first to + // avoid a bug in CanvasKit where MakeImageFromCanvasImageSource doesn't + // work with VideoFrames. + final DomHTMLCanvasElement canvas = createDomCanvasElement(width: width, height: height); + final DomCanvasRenderingContext2D ctx = canvas.context2D; + ctx.drawImage(object as VideoFrame, 0, 0); + skImage = canvasKit.MakeImageFromCanvasImageSource(canvas); + } else { + skImage = canvasKit.MakeImageFromCanvasImageSource(object); + } + } else { + skImage = canvasKit.MakeLazyImageFromTextureSourceWithInfo( + object, + SkPartialImageInfo( + width: width.toDouble(), + height: height.toDouble(), + alphaType: canvasKit.AlphaType.Premul, + colorType: canvasKit.ColorType.RGBA_8888, + colorSpace: SkColorSpaceSRGB, + ), + ); + } if (skImage == null) { throw Exception('Failed to convert image bitmap to an SkImage.'); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 058f34a91b2..c5ebc1f6d2e 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -52,6 +52,9 @@ abstract class CkSurface extends Surface { return true; } + /// Whether this surface is using software rendering. + bool get isSoftware => !supportsWebGl; + String? _fallbackToSoftwareReason; /// When true, the surface will fail to create a GL context and fall back to diff --git a/engine/src/flutter/lib/web_ui/test/ui/image_cpu_only_test.dart b/engine/src/flutter/lib/web_ui/test/ui/image_cpu_only_test.dart new file mode 100644 index 00000000000..9a6f59d9e53 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/ui/image_cpu_only_test.dart @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; +import 'package:web_engine_tester/golden_tester.dart'; + +import '../common/test_initialization.dart'; +import 'utils.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false); + + test('Renders image from encoded bytes in CPU-only mode', () async { + debugOverrideJsConfiguration(JsFlutterConfiguration(canvasKitForceCpuOnly: true)); + + // A 1x1 yellow PNG image. + const kBase64Png = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; + final Uint8List bytes = base64.decode(kBase64Png); + + final ui.Codec codec = await ui.instantiateImageCodec(bytes); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + final ui.Image image = frameInfo.image; + + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder, const ui.Rect.fromLTWH(0, 0, 10, 10)); + + // Draw the image scaled up so we can see it + canvas.drawImageRect( + image, + const ui.Rect.fromLTWH(0, 0, 1, 1), + const ui.Rect.fromLTWH(0, 0, 10, 10), + ui.Paint(), + ); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + + await matchGoldenFile('image_cpu_only.png', region: const ui.Rect.fromLTWH(0, 0, 10, 10)); + + debugOverrideJsConfiguration(null); // Reset configuration + }); + + test('Renders image from URL in CPU-only mode', () async { + debugOverrideJsConfiguration(JsFlutterConfiguration(canvasKitForceCpuOnly: true)); + + final Uri uri = Uri.base.resolve('/test/ui/image/sample_image1.png'); + final ui.Codec codec = await ui_web.createImageCodecFromUrl(uri); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + final ui.Image image = frameInfo.image; + + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder, const ui.Rect.fromLTWH(0, 0, 100, 100)); + + // Draw the image + canvas.drawImage(image, ui.Offset.zero, ui.Paint()); + + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + + await matchGoldenFile('image_cpu_only_url.png', region: const ui.Rect.fromLTWH(0, 0, 100, 100)); + + debugOverrideJsConfiguration(null); // Reset configuration + }); +}