diff --git a/packages/dev/core/src/Misc/dumpTools.ts b/packages/dev/core/src/Misc/dumpTools.ts index 00840a58395..947b42aec68 100644 --- a/packages/dev/core/src/Misc/dumpTools.ts +++ b/packages/dev/core/src/Misc/dumpTools.ts @@ -5,92 +5,84 @@ import type { ThinEngine } from "../Engines/thinEngine"; import { Constants } from "../Engines/constants"; import { EffectRenderer, EffectWrapper } from "../Materials/effectRenderer"; import { Tools } from "./tools"; -import type { Nullable } from "../types"; import { Clamp } from "../Maths/math.scalar.functions"; import type { AbstractEngine } from "../Engines/abstractEngine"; import { EngineStore } from "../Engines/engineStore"; +import { Logger } from "./logger"; -type DumpToolsEngine = { +type DumpResources = { canvas: HTMLCanvasElement | OffscreenCanvas; - engine: ThinEngine; - renderer: EffectRenderer; - wrapper: EffectWrapper; + dumpEngine?: { + engine: ThinEngine; + renderer: EffectRenderer; + wrapper: EffectWrapper; + }; }; -let DumpToolsEngine: Nullable; +let ResourcesPromise: Promise | null = null; -let EnginePromise: Promise | null = null; +async function _CreateDumpResourcesAsync(): Promise { + // Create a compatible canvas. Prefer an HTMLCanvasElement if possible to avoid alpha issues with OffscreenCanvas + WebGL in many browsers. + const canvas = (EngineStore.LastCreatedEngine?.createCanvas(100, 100) ?? new OffscreenCanvas(100, 100)) as HTMLCanvasElement | OffscreenCanvas; // will be resized later + if (canvas instanceof OffscreenCanvas) { + Logger.Warn("DumpData: OffscreenCanvas will be used for dumping data. This may result in lossy alpha values."); + } -async function _CreateDumpRendererAsync(): Promise { - if (!EnginePromise) { - EnginePromise = new Promise((resolve, reject) => { - let canvas: HTMLCanvasElement | OffscreenCanvas; - let engine: Nullable = null; - const options = { - preserveDrawingBuffer: true, - depth: false, - stencil: false, - alpha: true, - premultipliedAlpha: false, - antialias: false, - failIfMajorPerformanceCaveat: false, - }; - import("../Engines/thinEngine") - // eslint-disable-next-line github/no-then - .then(({ ThinEngine: thinEngineClass }) => { - const engineInstanceCount = EngineStore.Instances.length; - try { - canvas = new OffscreenCanvas(100, 100); // will be resized later - engine = new thinEngineClass(canvas, false, options); - } catch (e) { - if (engineInstanceCount < EngineStore.Instances.length) { - // The engine was created by another instance, let's use it - EngineStore.Instances.pop()?.dispose(); - } - // The browser either does not support OffscreenCanvas or WebGL context in OffscreenCanvas, fallback on a regular canvas - canvas = document.createElement("canvas"); - engine = new thinEngineClass(canvas, false, options); - } - // remove this engine from the list of instances to avoid using it for other purposes - EngineStore.Instances.pop(); - // However, make sure to dispose it when no other engines are left - EngineStore.OnEnginesDisposedObservable.add((e) => { - // guaranteed to run when no other instances are left - // only dispose if it's not the current engine - if (engine && e !== engine && !engine.isDisposed && EngineStore.Instances.length === 0) { - // Dump the engine and the associated resources - Dispose(); - } - }); - engine.getCaps().parallelShaderCompile = undefined; - const renderer = new EffectRenderer(engine); - // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then - import("../Shaders/pass.fragment").then(({ passPixelShader }) => { - if (!engine) { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject("Engine is not defined"); - return; - } - const wrapper = new EffectWrapper({ - engine, - name: passPixelShader.name, - fragmentShader: passPixelShader.shader, - samplerNames: ["textureSampler"], - }); - DumpToolsEngine = { - canvas, - engine, - renderer, - wrapper, - }; - resolve(DumpToolsEngine); - }); - }) - // eslint-disable-next-line github/no-then - .catch(reject); - }); + // If WebGL via ThinEngine is not available (e.g. Native), use the BitmapRenderer. + // If https://github.com/whatwg/html/issues/10142 is resolved, we can migrate to just BitmapRenderer and avoid an engine dependency altogether. + const { ThinEngine: thinEngineClass } = await import("../Engines/thinEngine"); + if (!thinEngineClass.IsSupported) { + if (!canvas.getContext("bitmaprenderer")) { + throw new Error("DumpData: No WebGL or bitmap rendering context available. Cannot dump data."); + } + return { canvas }; + } + + const options = { + preserveDrawingBuffer: true, + depth: false, + stencil: false, + alpha: true, + premultipliedAlpha: false, + antialias: false, + failIfMajorPerformanceCaveat: false, + }; + const engine = new thinEngineClass(canvas, false, options); + + // remove this engine from the list of instances to avoid using it for other purposes + EngineStore.Instances.pop(); + // However, make sure to dispose it when no other engines are left + EngineStore.OnEnginesDisposedObservable.add((e) => { + // guaranteed to run when no other instances are left + // only dispose if it's not the current engine + if (engine && e !== engine && !engine.isDisposed && EngineStore.Instances.length === 0) { + // Dump the engine and the associated resources + Dispose(); + } + }); + + engine.getCaps().parallelShaderCompile = undefined; + + const renderer = new EffectRenderer(engine); + const { passPixelShader } = await import("../Shaders/pass.fragment"); + const wrapper = new EffectWrapper({ + engine, + name: passPixelShader.name, + fragmentShader: passPixelShader.shader, + samplerNames: ["textureSampler"], + }); + + return { + canvas: canvas, + dumpEngine: { engine, renderer, wrapper }, + }; +} + +async function _GetDumpResourcesAsync() { + if (!ResourcesPromise) { + ResourcesPromise = _CreateDumpResourcesAsync(); } - return await EnginePromise; + return await ResourcesPromise; } /** @@ -165,8 +157,65 @@ export async function DumpDataAsync( toArrayBuffer = false, quality?: number ): Promise { - return await new Promise((resolve) => { - DumpData(width, height, data, (result) => resolve(result), mimeType, fileName, invertY, toArrayBuffer, quality); + // Convert if data are float32 + if (data instanceof Float32Array) { + const data2 = new Uint8Array(data.length); + let n = data.length; + while (n--) { + const v = data[n]; + data2[n] = Math.round(Clamp(v) * 255); + } + data = data2; + } + + const resources = await _GetDumpResourcesAsync(); + + // Keep the async render + read from the shared canvas atomic + // eslint-disable-next-line no-async-promise-executor + return await new Promise(async (resolve) => { + if (resources.dumpEngine) { + const dumpEngine = resources.dumpEngine; + dumpEngine.engine.setSize(width, height, true); + + // Create the image + const texture = dumpEngine.engine.createRawTexture(data, width, height, Constants.TEXTUREFORMAT_RGBA, false, !invertY, Constants.TEXTURE_NEAREST_NEAREST); + + dumpEngine.renderer.setViewport(); + dumpEngine.renderer.applyEffectWrapper(dumpEngine.wrapper); + dumpEngine.wrapper.effect._bindTexture("textureSampler", texture); + dumpEngine.renderer.draw(); + + texture.dispose(); + } else { + const ctx = resources.canvas.getContext("bitmaprenderer") as ImageBitmapRenderingContext; + resources.canvas.width = width; + resources.canvas.height = height; + + const imageData = new ImageData(width, height); // ImageData(data, sw, sh) ctor not yet widely implemented + imageData.data.set(data as Uint8ClampedArray); + const imageBitmap = await createImageBitmap(imageData, { premultiplyAlpha: "none", imageOrientation: invertY ? "flipY" : "from-image" }); + + ctx.transferFromImageBitmap(imageBitmap); + } + + // Download the result + if (toArrayBuffer) { + Tools.ToBlob( + resources.canvas, + (blob) => { + const fileReader = new FileReader(); + fileReader.onload = (event: any) => { + const arrayBuffer = event.target!.result as ArrayBuffer; + resolve(arrayBuffer); + }; + fileReader.readAsArrayBuffer(blob!); + }, + mimeType, + quality + ); + } else { + Tools.EncodeScreenshotCanvasData(resources.canvas, resolve, mimeType, fileName, quality); + } }); } @@ -193,72 +242,38 @@ export function DumpData( toArrayBuffer = false, quality?: number ): void { - // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then - _CreateDumpRendererAsync().then((renderer) => { - renderer.engine.setSize(width, height, true); - - // Convert if data are float32 - if (data instanceof Float32Array) { - const data2 = new Uint8Array(data.length); - let n = data.length; - while (n--) { - const v = data[n]; - data2[n] = Math.round(Clamp(v) * 255); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + DumpDataAsync(width, height, data, mimeType, fileName, invertY, toArrayBuffer, quality) + // eslint-disable-next-line github/no-then + .then((result) => { + if (successCallback) { + successCallback(result); } - data = data2; - } - - // Create the image - const texture = renderer.engine.createRawTexture(data, width, height, Constants.TEXTUREFORMAT_RGBA, false, !invertY, Constants.TEXTURE_NEAREST_NEAREST); - - renderer.renderer.setViewport(); - renderer.renderer.applyEffectWrapper(renderer.wrapper); - renderer.wrapper.effect._bindTexture("textureSampler", texture); - renderer.renderer.draw(); - - if (toArrayBuffer) { - Tools.ToBlob( - renderer.canvas, - (blob) => { - const fileReader = new FileReader(); - fileReader.onload = (event: any) => { - const arrayBuffer = event.target!.result as ArrayBuffer; - if (successCallback) { - successCallback(arrayBuffer); - } - }; - fileReader.readAsArrayBuffer(blob!); - }, - mimeType, - quality - ); - } else { - Tools.EncodeScreenshotCanvasData(renderer.canvas, successCallback, mimeType, fileName, quality); - } - - texture.dispose(); - }); + }); } /** * Dispose the dump tools associated resources */ export function Dispose() { - if (DumpToolsEngine) { - DumpToolsEngine.wrapper.dispose(); - DumpToolsEngine.renderer.dispose(); - DumpToolsEngine.engine.dispose(); - } else { - // in cases where the engine is not yet created, we need to wait for it to dispose it - // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then - EnginePromise?.then((dumpToolsEngine) => { - dumpToolsEngine.wrapper.dispose(); - dumpToolsEngine.renderer.dispose(); - dumpToolsEngine.engine.dispose(); - }); + if (!ResourcesPromise) { + return; } - EnginePromise = null; - DumpToolsEngine = null; + + // in cases where the engine is not yet created, we need to wait for it to dispose it + // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then + ResourcesPromise?.then((resources) => { + if (resources.canvas instanceof HTMLCanvasElement) { + resources.canvas.remove(); + } + if (resources.dumpEngine) { + resources.dumpEngine.engine.dispose(); + resources.dumpEngine.renderer.dispose(); + resources.dumpEngine.wrapper.dispose(); + } + }); + + ResourcesPromise = null; } /**