Skip to content
Merged
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
287 changes: 151 additions & 136 deletions packages/dev/core/src/Misc/dumpTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DumpToolsEngine>;
let ResourcesPromise: Promise<DumpResources> | null = null;

let EnginePromise: Promise<DumpToolsEngine> | null = null;
async function _CreateDumpResourcesAsync(): Promise<DumpResources> {
// 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<DumpToolsEngine> {
if (!EnginePromise) {
EnginePromise = new Promise((resolve, reject) => {
let canvas: HTMLCanvasElement | OffscreenCanvas;
let engine: Nullable<ThinEngine> = 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;
}

/**
Expand Down Expand Up @@ -165,8 +157,65 @@ export async function DumpDataAsync(
toArrayBuffer = false,
quality?: number
): Promise<string | ArrayBuffer> {
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<string | ArrayBuffer>(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);
}
});
}

Expand All @@ -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;
}

/**
Expand Down