@@ -5,92 +5,84 @@ import type { ThinEngine } from "../Engines/thinEngine";
5
5
import { Constants } from "../Engines/constants" ;
6
6
import { EffectRenderer , EffectWrapper } from "../Materials/effectRenderer" ;
7
7
import { Tools } from "./tools" ;
8
- import type { Nullable } from "../types" ;
9
8
import { Clamp } from "../Maths/math.scalar.functions" ;
10
9
import type { AbstractEngine } from "../Engines/abstractEngine" ;
11
10
import { EngineStore } from "../Engines/engineStore" ;
11
+ import { Logger } from "./logger" ;
12
12
13
- type DumpToolsEngine = {
13
+ type DumpResources = {
14
14
canvas : HTMLCanvasElement | OffscreenCanvas ;
15
- engine : ThinEngine ;
16
- renderer : EffectRenderer ;
17
- wrapper : EffectWrapper ;
15
+ dumpEngine ?: {
16
+ engine : ThinEngine ;
17
+ renderer : EffectRenderer ;
18
+ wrapper : EffectWrapper ;
19
+ } ;
18
20
} ;
19
21
20
- let DumpToolsEngine : Nullable < DumpToolsEngine > ;
22
+ let ResourcesPromise : Promise < DumpResources > | null = null ;
21
23
22
- let EnginePromise : Promise < DumpToolsEngine > | null = null ;
24
+ async function _CreateDumpResourcesAsync ( ) : Promise < DumpResources > {
25
+ // Create a compatible canvas. Prefer an HTMLCanvasElement if possible to avoid alpha issues with OffscreenCanvas + WebGL in many browsers.
26
+ const canvas = ( EngineStore . LastCreatedEngine ?. createCanvas ( 100 , 100 ) ?? new OffscreenCanvas ( 100 , 100 ) ) as HTMLCanvasElement | OffscreenCanvas ; // will be resized later
27
+ if ( canvas instanceof OffscreenCanvas ) {
28
+ Logger . Warn ( "DumpData: OffscreenCanvas will be used for dumping data. This may result in lossy alpha values." ) ;
29
+ }
23
30
24
- async function _CreateDumpRendererAsync ( ) : Promise < DumpToolsEngine > {
25
- if ( ! EnginePromise ) {
26
- EnginePromise = new Promise ( ( resolve , reject ) => {
27
- let canvas : HTMLCanvasElement | OffscreenCanvas ;
28
- let engine : Nullable < ThinEngine > = null ;
29
- const options = {
30
- preserveDrawingBuffer : true ,
31
- depth : false ,
32
- stencil : false ,
33
- alpha : true ,
34
- premultipliedAlpha : false ,
35
- antialias : false ,
36
- failIfMajorPerformanceCaveat : false ,
37
- } ;
38
- import ( "../Engines/thinEngine" )
39
- // eslint-disable-next-line github/no-then
40
- . then ( ( { ThinEngine : thinEngineClass } ) => {
41
- const engineInstanceCount = EngineStore . Instances . length ;
42
- try {
43
- canvas = new OffscreenCanvas ( 100 , 100 ) ; // will be resized later
44
- engine = new thinEngineClass ( canvas , false , options ) ;
45
- } catch ( e ) {
46
- if ( engineInstanceCount < EngineStore . Instances . length ) {
47
- // The engine was created by another instance, let's use it
48
- EngineStore . Instances . pop ( ) ?. dispose ( ) ;
49
- }
50
- // The browser either does not support OffscreenCanvas or WebGL context in OffscreenCanvas, fallback on a regular canvas
51
- canvas = document . createElement ( "canvas" ) ;
52
- engine = new thinEngineClass ( canvas , false , options ) ;
53
- }
54
- // remove this engine from the list of instances to avoid using it for other purposes
55
- EngineStore . Instances . pop ( ) ;
56
- // However, make sure to dispose it when no other engines are left
57
- EngineStore . OnEnginesDisposedObservable . add ( ( e ) => {
58
- // guaranteed to run when no other instances are left
59
- // only dispose if it's not the current engine
60
- if ( engine && e !== engine && ! engine . isDisposed && EngineStore . Instances . length === 0 ) {
61
- // Dump the engine and the associated resources
62
- Dispose ( ) ;
63
- }
64
- } ) ;
65
- engine . getCaps ( ) . parallelShaderCompile = undefined ;
66
- const renderer = new EffectRenderer ( engine ) ;
67
- // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then
68
- import ( "../Shaders/pass.fragment" ) . then ( ( { passPixelShader } ) => {
69
- if ( ! engine ) {
70
- // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
71
- reject ( "Engine is not defined" ) ;
72
- return ;
73
- }
74
- const wrapper = new EffectWrapper ( {
75
- engine,
76
- name : passPixelShader . name ,
77
- fragmentShader : passPixelShader . shader ,
78
- samplerNames : [ "textureSampler" ] ,
79
- } ) ;
80
- DumpToolsEngine = {
81
- canvas,
82
- engine,
83
- renderer,
84
- wrapper,
85
- } ;
86
- resolve ( DumpToolsEngine ) ;
87
- } ) ;
88
- } )
89
- // eslint-disable-next-line github/no-then
90
- . catch ( reject ) ;
91
- } ) ;
31
+ // If WebGL via ThinEngine is not available (e.g. Native), use the BitmapRenderer.
32
+ // If https://github.com/whatwg/html/issues/10142 is resolved, we can migrate to just BitmapRenderer and avoid an engine dependency altogether.
33
+ const { ThinEngine : thinEngineClass } = await import ( "../Engines/thinEngine" ) ;
34
+ if ( ! thinEngineClass . IsSupported ) {
35
+ if ( ! canvas . getContext ( "bitmaprenderer" ) ) {
36
+ throw new Error ( "DumpData: No WebGL or bitmap rendering context available. Cannot dump data." ) ;
37
+ }
38
+ return { canvas } ;
39
+ }
40
+
41
+ const options = {
42
+ preserveDrawingBuffer : true ,
43
+ depth : false ,
44
+ stencil : false ,
45
+ alpha : true ,
46
+ premultipliedAlpha : false ,
47
+ antialias : false ,
48
+ failIfMajorPerformanceCaveat : false ,
49
+ } ;
50
+ const engine = new thinEngineClass ( canvas , false , options ) ;
51
+
52
+ // remove this engine from the list of instances to avoid using it for other purposes
53
+ EngineStore . Instances . pop ( ) ;
54
+ // However, make sure to dispose it when no other engines are left
55
+ EngineStore . OnEnginesDisposedObservable . add ( ( e ) => {
56
+ // guaranteed to run when no other instances are left
57
+ // only dispose if it's not the current engine
58
+ if ( engine && e !== engine && ! engine . isDisposed && EngineStore . Instances . length === 0 ) {
59
+ // Dump the engine and the associated resources
60
+ Dispose ( ) ;
61
+ }
62
+ } ) ;
63
+
64
+ engine . getCaps ( ) . parallelShaderCompile = undefined ;
65
+
66
+ const renderer = new EffectRenderer ( engine ) ;
67
+ const { passPixelShader } = await import ( "../Shaders/pass.fragment" ) ;
68
+ const wrapper = new EffectWrapper ( {
69
+ engine,
70
+ name : passPixelShader . name ,
71
+ fragmentShader : passPixelShader . shader ,
72
+ samplerNames : [ "textureSampler" ] ,
73
+ } ) ;
74
+
75
+ return {
76
+ canvas : canvas ,
77
+ dumpEngine : { engine, renderer, wrapper } ,
78
+ } ;
79
+ }
80
+
81
+ async function _GetDumpResourcesAsync ( ) {
82
+ if ( ! ResourcesPromise ) {
83
+ ResourcesPromise = _CreateDumpResourcesAsync ( ) ;
92
84
}
93
- return await EnginePromise ;
85
+ return await ResourcesPromise ;
94
86
}
95
87
96
88
/**
@@ -165,8 +157,65 @@ export async function DumpDataAsync(
165
157
toArrayBuffer = false ,
166
158
quality ?: number
167
159
) : Promise < string | ArrayBuffer > {
168
- return await new Promise ( ( resolve ) => {
169
- DumpData ( width , height , data , ( result ) => resolve ( result ) , mimeType , fileName , invertY , toArrayBuffer , quality ) ;
160
+ // Convert if data are float32
161
+ if ( data instanceof Float32Array ) {
162
+ const data2 = new Uint8Array ( data . length ) ;
163
+ let n = data . length ;
164
+ while ( n -- ) {
165
+ const v = data [ n ] ;
166
+ data2 [ n ] = Math . round ( Clamp ( v ) * 255 ) ;
167
+ }
168
+ data = data2 ;
169
+ }
170
+
171
+ const resources = await _GetDumpResourcesAsync ( ) ;
172
+
173
+ // Keep the async render + read from the shared canvas atomic
174
+ // eslint-disable-next-line no-async-promise-executor
175
+ return await new Promise < string | ArrayBuffer > ( async ( resolve ) => {
176
+ if ( resources . dumpEngine ) {
177
+ const dumpEngine = resources . dumpEngine ;
178
+ dumpEngine . engine . setSize ( width , height , true ) ;
179
+
180
+ // Create the image
181
+ const texture = dumpEngine . engine . createRawTexture ( data , width , height , Constants . TEXTUREFORMAT_RGBA , false , ! invertY , Constants . TEXTURE_NEAREST_NEAREST ) ;
182
+
183
+ dumpEngine . renderer . setViewport ( ) ;
184
+ dumpEngine . renderer . applyEffectWrapper ( dumpEngine . wrapper ) ;
185
+ dumpEngine . wrapper . effect . _bindTexture ( "textureSampler" , texture ) ;
186
+ dumpEngine . renderer . draw ( ) ;
187
+
188
+ texture . dispose ( ) ;
189
+ } else {
190
+ const ctx = resources . canvas . getContext ( "bitmaprenderer" ) as ImageBitmapRenderingContext ;
191
+ resources . canvas . width = width ;
192
+ resources . canvas . height = height ;
193
+
194
+ const imageData = new ImageData ( width , height ) ; // ImageData(data, sw, sh) ctor not yet widely implemented
195
+ imageData . data . set ( data as Uint8ClampedArray ) ;
196
+ const imageBitmap = await createImageBitmap ( imageData , { premultiplyAlpha : "none" , imageOrientation : invertY ? "flipY" : "from-image" } ) ;
197
+
198
+ ctx . transferFromImageBitmap ( imageBitmap ) ;
199
+ }
200
+
201
+ // Download the result
202
+ if ( toArrayBuffer ) {
203
+ Tools . ToBlob (
204
+ resources . canvas ,
205
+ ( blob ) => {
206
+ const fileReader = new FileReader ( ) ;
207
+ fileReader . onload = ( event : any ) => {
208
+ const arrayBuffer = event . target ! . result as ArrayBuffer ;
209
+ resolve ( arrayBuffer ) ;
210
+ } ;
211
+ fileReader . readAsArrayBuffer ( blob ! ) ;
212
+ } ,
213
+ mimeType ,
214
+ quality
215
+ ) ;
216
+ } else {
217
+ Tools . EncodeScreenshotCanvasData ( resources . canvas , resolve , mimeType , fileName , quality ) ;
218
+ }
170
219
} ) ;
171
220
}
172
221
@@ -193,72 +242,38 @@ export function DumpData(
193
242
toArrayBuffer = false ,
194
243
quality ?: number
195
244
) : void {
196
- // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then
197
- _CreateDumpRendererAsync ( ) . then ( ( renderer ) => {
198
- renderer . engine . setSize ( width , height , true ) ;
199
-
200
- // Convert if data are float32
201
- if ( data instanceof Float32Array ) {
202
- const data2 = new Uint8Array ( data . length ) ;
203
- let n = data . length ;
204
- while ( n -- ) {
205
- const v = data [ n ] ;
206
- data2 [ n ] = Math . round ( Clamp ( v ) * 255 ) ;
245
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
246
+ DumpDataAsync ( width , height , data , mimeType , fileName , invertY , toArrayBuffer , quality )
247
+ // eslint-disable-next-line github/no-then
248
+ . then ( ( result ) => {
249
+ if ( successCallback ) {
250
+ successCallback ( result ) ;
207
251
}
208
- data = data2 ;
209
- }
210
-
211
- // Create the image
212
- const texture = renderer . engine . createRawTexture ( data , width , height , Constants . TEXTUREFORMAT_RGBA , false , ! invertY , Constants . TEXTURE_NEAREST_NEAREST ) ;
213
-
214
- renderer . renderer . setViewport ( ) ;
215
- renderer . renderer . applyEffectWrapper ( renderer . wrapper ) ;
216
- renderer . wrapper . effect . _bindTexture ( "textureSampler" , texture ) ;
217
- renderer . renderer . draw ( ) ;
218
-
219
- if ( toArrayBuffer ) {
220
- Tools . ToBlob (
221
- renderer . canvas ,
222
- ( blob ) => {
223
- const fileReader = new FileReader ( ) ;
224
- fileReader . onload = ( event : any ) => {
225
- const arrayBuffer = event . target ! . result as ArrayBuffer ;
226
- if ( successCallback ) {
227
- successCallback ( arrayBuffer ) ;
228
- }
229
- } ;
230
- fileReader . readAsArrayBuffer ( blob ! ) ;
231
- } ,
232
- mimeType ,
233
- quality
234
- ) ;
235
- } else {
236
- Tools . EncodeScreenshotCanvasData ( renderer . canvas , successCallback , mimeType , fileName , quality ) ;
237
- }
238
-
239
- texture . dispose ( ) ;
240
- } ) ;
252
+ } ) ;
241
253
}
242
254
243
255
/**
244
256
* Dispose the dump tools associated resources
245
257
*/
246
258
export function Dispose ( ) {
247
- if ( DumpToolsEngine ) {
248
- DumpToolsEngine . wrapper . dispose ( ) ;
249
- DumpToolsEngine . renderer . dispose ( ) ;
250
- DumpToolsEngine . engine . dispose ( ) ;
251
- } else {
252
- // in cases where the engine is not yet created, we need to wait for it to dispose it
253
- // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then
254
- EnginePromise ?. then ( ( dumpToolsEngine ) => {
255
- dumpToolsEngine . wrapper . dispose ( ) ;
256
- dumpToolsEngine . renderer . dispose ( ) ;
257
- dumpToolsEngine . engine . dispose ( ) ;
258
- } ) ;
259
+ if ( ! ResourcesPromise ) {
260
+ return ;
259
261
}
260
- EnginePromise = null ;
261
- DumpToolsEngine = null ;
262
+
263
+ // in cases where the engine is not yet created, we need to wait for it to dispose it
264
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then
265
+ ResourcesPromise ?. then ( ( resources ) => {
266
+ if ( resources . canvas instanceof HTMLCanvasElement ) {
267
+ resources . canvas . remove ( ) ;
268
+ }
269
+ if ( resources . dumpEngine ) {
270
+ resources . dumpEngine . engine . dispose ( ) ;
271
+ resources . dumpEngine . renderer . dispose ( ) ;
272
+ resources . dumpEngine . wrapper . dispose ( ) ;
273
+ }
274
+ } ) ;
275
+
276
+ ResourcesPromise = null ;
262
277
}
263
278
264
279
/**
0 commit comments