oidn-webgpu 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/LICENSE +21 -0
  3. package/README.md +89 -2
  4. package/dist/oidn.js +6140 -0
  5. package/dist/oidn.umd.cjs +1513 -0
  6. package/lib/UNet.d.ts +161 -0
  7. package/lib/UNet.js +536 -0
  8. package/lib/UNet.js.map +1 -0
  9. package/lib/WGPUComputePass.d.ts +62 -0
  10. package/lib/WGPUComputePass.js +278 -0
  11. package/lib/WGPUComputePass.js.map +1 -0
  12. package/lib/WGPUFullQuadPass.d.ts +50 -0
  13. package/lib/WGPUFullQuadPass.js +261 -0
  14. package/lib/WGPUFullQuadPass.js.map +1 -0
  15. package/lib/backend.d.ts +1 -0
  16. package/lib/backend.js +39 -0
  17. package/lib/backend.js.map +1 -0
  18. package/lib/finalRgbShader.d.ts +13 -0
  19. package/lib/finalRgbShader.js +160 -0
  20. package/lib/finalRgbShader.js.map +1 -0
  21. package/lib/gpuTypes.d.ts +158 -0
  22. package/lib/gpuTypes.js +27 -0
  23. package/lib/gpuTypes.js.map +1 -0
  24. package/lib/graphOptimizer.d.ts +54 -0
  25. package/lib/graphOptimizer.js +216 -0
  26. package/lib/graphOptimizer.js.map +1 -0
  27. package/lib/hdr.d.ts +31 -0
  28. package/lib/hdr.js +340 -0
  29. package/lib/hdr.js.map +1 -0
  30. package/lib/hdrTransfer.d.ts +22 -0
  31. package/lib/hdrTransfer.js +128 -0
  32. package/lib/hdrTransfer.js.map +1 -0
  33. package/lib/main.d.ts +57 -0
  34. package/lib/main.js +24 -0
  35. package/lib/main.js.map +1 -0
  36. package/lib/modelSpec.d.ts +80 -0
  37. package/lib/modelSpec.js +270 -0
  38. package/lib/modelSpec.js.map +1 -0
  39. package/lib/native/denoise.d.ts +25 -0
  40. package/lib/native/denoise.js +223 -0
  41. package/lib/native/denoise.js.map +1 -0
  42. package/lib/native/program.d.ts +53 -0
  43. package/lib/native/program.js +310 -0
  44. package/lib/native/program.js.map +1 -0
  45. package/lib/native/recordingDevice.d.ts +125 -0
  46. package/lib/native/recordingDevice.js +247 -0
  47. package/lib/native/recordingDevice.js.map +1 -0
  48. package/lib/nativeUNet.d.ts +25 -0
  49. package/lib/nativeUNet.js +184 -0
  50. package/lib/nativeUNet.js.map +1 -0
  51. package/lib/nativeUNetCore.d.ts +202 -0
  52. package/lib/nativeUNetCore.js +2945 -0
  53. package/lib/nativeUNetCore.js.map +1 -0
  54. package/lib/process.d.ts +37 -0
  55. package/lib/process.js +328 -0
  56. package/lib/process.js.map +1 -0
  57. package/lib/resourceTracker.d.ts +26 -0
  58. package/lib/resourceTracker.js +65 -0
  59. package/lib/resourceTracker.js.map +1 -0
  60. package/lib/sanitize.d.ts +15 -0
  61. package/lib/sanitize.js +28 -0
  62. package/lib/sanitize.js.map +1 -0
  63. package/lib/tileScheduler.d.ts +61 -0
  64. package/lib/tileScheduler.js +199 -0
  65. package/lib/tileScheduler.js.map +1 -0
  66. package/lib/tza.d.ts +15 -0
  67. package/lib/tza.js +114 -0
  68. package/lib/tza.js.map +1 -0
  69. package/lib/webnnUNet.d.ts +52 -0
  70. package/lib/webnnUNet.js +535 -0
  71. package/lib/webnnUNet.js.map +1 -0
  72. package/package.json +43 -4
  73. package/src/UNet.ts +861 -0
  74. package/src/WGPUComputePass.ts +382 -0
  75. package/src/WGPUFullQuadPass.ts +348 -0
  76. package/src/backend.ts +42 -0
  77. package/src/finalRgbShader.ts +186 -0
  78. package/src/gpuTypes.ts +179 -0
  79. package/src/graphOptimizer.ts +301 -0
  80. package/src/hdr.ts +398 -0
  81. package/src/hdrTransfer.ts +169 -0
  82. package/src/main.ts +126 -0
  83. package/src/modelSpec.ts +414 -0
  84. package/src/native/core-profile.json +40 -0
  85. package/src/native/denoise.ts +268 -0
  86. package/src/native/program.ts +326 -0
  87. package/src/native/recordingDevice.ts +341 -0
  88. package/src/nativeUNet.ts +269 -0
  89. package/src/nativeUNetCore.ts +3735 -0
  90. package/src/process.ts +368 -0
  91. package/src/resourceTracker.ts +94 -0
  92. package/src/sanitize.ts +29 -0
  93. package/src/tileScheduler.ts +330 -0
  94. package/src/tza.ts +136 -0
  95. package/src/webnnUNet.ts +812 -0
package/src/UNet.ts ADDED
@@ -0,0 +1,861 @@
1
+ import { HostTensor } from './tza';
2
+ import {
3
+ GPUDataProcess,
4
+ Tile,
5
+ avgLogLum,
6
+ hdrTransferFuncCPU,
7
+ hdrTransferFuncInverseCPU,
8
+ type HDRTransfer
9
+ } from './process';
10
+ import {
11
+ DynamicTileController,
12
+ type DynamicTileSetting,
13
+ planTileGrid,
14
+ type PlannedTile,
15
+ OIDN_TILE_ALIGNMENT
16
+ } from './tileScheduler';
17
+ import {
18
+ detectUNetModelSpec,
19
+ validateUNetModel,
20
+ type UNetModelSpec
21
+ } from './modelSpec';
22
+ import {
23
+ NativeUNetExecutor,
24
+ type NativeSubgroupMatrixOptions,
25
+ type NativeUNetGemmOptions,
26
+ type NativeUNetKernelSetting,
27
+ type NativeUNetExecutionProfile,
28
+ type NativeUNetPrecisionSetting
29
+ } from './nativeUNet';
30
+ import { WebNNUNetExecutor } from './webnnUNet';
31
+
32
+ export type UNetEngineSetting = 'auto' | 'wgsl' | 'webnn';
33
+
34
+ /** Upper bound on waiting for a display frame between tiles. */
35
+ const ANIMATION_FRAME_FALLBACK_MS = 100;
36
+
37
+ interface HDRImageData {
38
+ data: Float32Array;
39
+ width: number;
40
+ height: number;
41
+ }
42
+
43
+ interface GPUImageData {
44
+ data: GPUBuffer | GPUTexture;
45
+ width: number;
46
+ height: number;
47
+ }
48
+
49
+ interface GPUImageDataOutput {
50
+ data: GPUBuffer;
51
+ width: number;
52
+ height: number;
53
+ }
54
+
55
+ export interface UNetExecutionStats {
56
+ width: number;
57
+ height: number;
58
+ tileCount: number;
59
+ tileColumns: number;
60
+ tileRows: number;
61
+ tileOverlap: number;
62
+ inputPixelCount: number;
63
+ inputShapeCount: number;
64
+ durationMs: number;
65
+ tileTimeMs: {
66
+ min: number;
67
+ median: number;
68
+ mean: number;
69
+ max: number;
70
+ };
71
+ }
72
+
73
+ function roundUp(a: number, b: number) {
74
+ return Math.ceil(a / b) * b;
75
+ }
76
+
77
+ function isGPUImageData(
78
+ data: ImageData | GPUImageData | HDRImageData
79
+ ): data is GPUImageData {
80
+ return data.data instanceof GPUBuffer || data.data instanceof GPUTexture;
81
+ }
82
+
83
+ class UNet {
84
+ private _device: GPUDevice;
85
+
86
+ private _aux;
87
+ private _hdr;
88
+ private _hdrTransfer: HDRTransfer;
89
+
90
+ private _dataProcessGPU?: GPUDataProcess;
91
+ private _nativeExecutor?: NativeUNetExecutor;
92
+ private _webNNExecutor?: WebNNUNetExecutor;
93
+ private _modelSpec: UNetModelSpec;
94
+ private _inputChannels: number;
95
+ private _engine: UNetEngineSetting;
96
+
97
+ private _dynamicTileController: DynamicTileController;
98
+ private _lastExecution?: UNetExecutionStats;
99
+ private _profileNextImage = false;
100
+ private _lastImageProfile?: NativeUNetExecutionProfile;
101
+ private _activeExecutionFailures = new Set<(reason: unknown) => void>();
102
+ private _deviceLostObserved = false;
103
+ private _deviceLostSettled = false;
104
+ private _deviceLostReason: unknown;
105
+
106
+ constructor(
107
+ hostTensors: Map<string, HostTensor>,
108
+ device: GPUDevice,
109
+ opts: {
110
+ /**
111
+ * If use auxiliary data.
112
+ */
113
+ aux?: boolean;
114
+ /**
115
+ * If input is HDR image.
116
+ */
117
+ hdr?: boolean;
118
+ /** HDR transfer function expected by the trained model. */
119
+ hdrTransfer?: HDRTransfer;
120
+ maxTileSize?: number;
121
+ dynamicTile?: DynamicTileSetting;
122
+ /** Native WGSL or the experimental WebNN backend. */
123
+ engine?: UNetEngineSetting;
124
+ /** Arithmetic/storage precision used by the native WGSL executor. */
125
+ precision?: NativeUNetPrecisionSetting;
126
+ /** Model-independent convolution kernel selection. */
127
+ kernel?: NativeUNetKernelSetting;
128
+ gemm?: NativeUNetGemmOptions;
129
+ subgroupMatrix?: NativeSubgroupMatrixOptions;
130
+ /** Explicit descriptor for a new OIDN topology not in the built-in registry. */
131
+ modelSpec?: UNetModelSpec;
132
+ } = {}
133
+ ) {
134
+ this._aux = opts.aux || false;
135
+ this._hdr = opts.hdr || false;
136
+ this._hdrTransfer = opts.hdrTransfer ?? 'pu';
137
+ this._engine = opts.engine ?? 'auto';
138
+ const modelSpec = opts.modelSpec ?? detectUNetModelSpec(hostTensors);
139
+ const validatedModel = validateUNetModel(hostTensors, modelSpec);
140
+ this._modelSpec = validatedModel.spec;
141
+ this._inputChannels = validatedModel.inputChannels;
142
+
143
+ const expectedInputChannels = this._aux ? 9 : 3;
144
+ if (validatedModel.inputChannels !== expectedInputChannels) {
145
+ throw new Error(
146
+ `OIDN model expects ${validatedModel.inputChannels} input channels, ` +
147
+ `but aux=${this._aux} provides ${expectedInputChannels}`
148
+ );
149
+ }
150
+
151
+ this._dynamicTileController = new DynamicTileController(
152
+ opts.maxTileSize ?? 512,
153
+ opts.dynamicTile
154
+ );
155
+
156
+ this._device = device;
157
+ this._observeDeviceLoss();
158
+ if (this._engine === 'webnn') {
159
+ this._webNNExecutor = new WebNNUNetExecutor(
160
+ this._device,
161
+ validatedModel,
162
+ { precision: opts.precision }
163
+ );
164
+ } else {
165
+ this._nativeExecutor = new NativeUNetExecutor(
166
+ this._device,
167
+ validatedModel,
168
+ {
169
+ precision: opts.precision,
170
+ kernel: opts.kernel,
171
+ gemm: opts.gemm,
172
+ subgroupMatrix: opts.subgroupMatrix
173
+ }
174
+ );
175
+ }
176
+ }
177
+
178
+ getDevice() {
179
+ return this._device;
180
+ }
181
+
182
+ /** Completes backend compilation before first interactive use. */
183
+ async prepare() {
184
+ if (this._webNNExecutor) {
185
+ await this._webNNExecutor.prepare();
186
+ const overlap = roundUp(
187
+ this._modelSpec.receptiveField / 2,
188
+ OIDN_TILE_ALIGNMENT
189
+ );
190
+ const outputTileEdges = [
191
+ this._dynamicTileController.tileSize,
192
+ this._dynamicTileController.minTileSize
193
+ ];
194
+ await this._webNNExecutor.prewarm(
195
+ [...new Set(outputTileEdges)].map((edge) => ({
196
+ width: edge + 2 * overlap,
197
+ height: edge + 2 * overlap
198
+ }))
199
+ );
200
+ return;
201
+ }
202
+ await this._nativeExecutor!.prepare();
203
+ }
204
+
205
+ /**
206
+ * Prepares the input shapes selected for an image before its first denoise.
207
+ * Hosts can call this while they still display their model-loading state.
208
+ */
209
+ async prepareForImage(
210
+ width: number,
211
+ height: number,
212
+ options: { tileOverlap?: number; wholeImage?: boolean } = {}
213
+ ) {
214
+ const defaultTileOverlap = roundUp(
215
+ this._modelSpec.receptiveField / 2,
216
+ OIDN_TILE_ALIGNMENT
217
+ );
218
+ const resolvedTileOverlap = options.tileOverlap === undefined
219
+ ? defaultTileOverlap
220
+ : roundUp(Math.max(0, options.tileOverlap), OIDN_TILE_ALIGNMENT);
221
+ const plan = planTileGrid(
222
+ width,
223
+ height,
224
+ options.wholeImage
225
+ ? roundUp(Math.max(width, height), OIDN_TILE_ALIGNMENT)
226
+ : this._dynamicTileController.tileSize,
227
+ resolvedTileOverlap
228
+ );
229
+ const shapes = [...new Map(
230
+ plan.tiles.map(({ input }) => [
231
+ `${input.width}x${input.height}`,
232
+ { width: input.width, height: input.height }
233
+ ])
234
+ ).values()];
235
+ await this._webNNExecutor?.prewarm(shapes);
236
+ this._nativeExecutor?.prewarm(shapes);
237
+ }
238
+
239
+ getRuntimeInfo() {
240
+ return {
241
+ configuredEngine: this._engine,
242
+ gpuEngine: this._webNNExecutor ? 'webnn' as const : 'wgsl' as const,
243
+ precision: (this._webNNExecutor ?? this._nativeExecutor!).precision,
244
+ kernel: this._nativeExecutor
245
+ ? {
246
+ configured: this._nativeExecutor.kernelSetting,
247
+ gemm: this._nativeExecutor.gemm,
248
+ maxSpatialInputBlocks: this._nativeExecutor.maxSpatialInputBlocks,
249
+ subgroupsAvailable: this._nativeExecutor.subgroupsAvailable,
250
+ subgroupMatrix: this._nativeExecutor.subgroupMatrix
251
+ }
252
+ : undefined,
253
+ webnn: this._webNNExecutor?.support,
254
+ resources: (
255
+ this._webNNExecutor ?? this._nativeExecutor!
256
+ ).getResourceInfo(),
257
+ model: this._modelSpec.id,
258
+ modelFamily: this._modelSpec.family,
259
+ inputChannels: this._inputChannels,
260
+ hdrTransfer: this._hdrTransfer,
261
+ dynamicTile: {
262
+ enabled: this._dynamicTileController.enabled,
263
+ currentTileSize: this._dynamicTileController.tileSize,
264
+ minTileSize: this._dynamicTileController.minTileSize,
265
+ maxTileSize: this._dynamicTileController.maxTileSize,
266
+ targetTileTimeMs: this._dynamicTileController.targetTileTimeMs
267
+ },
268
+ lastExecution: this._lastExecution,
269
+ activeExecutionCount: this._activeExecutionFailures.size
270
+ };
271
+ }
272
+
273
+ private _observeDeviceLoss() {
274
+ // Object.create-based embedders/tests can bypass field initializers.
275
+ this._activeExecutionFailures ??= new Set();
276
+ if (this._deviceLostObserved) return;
277
+ this._deviceLostObserved = true;
278
+ const deviceLost = (this._device as GPUDevice & {
279
+ lost?: Promise<GPUDeviceLostInfo>;
280
+ }).lost;
281
+ if (!deviceLost) return;
282
+ const failAll = (reason: unknown) => {
283
+ if (this._deviceLostSettled) return;
284
+ this._deviceLostSettled = true;
285
+ this._deviceLostReason = reason;
286
+ const active = [...this._activeExecutionFailures];
287
+ this._activeExecutionFailures.clear();
288
+ for (const fail of active) fail(reason);
289
+ };
290
+ void deviceLost.then(
291
+ (info) => failAll(new Error(`WebGPU device lost: ${info.message}`)),
292
+ failAll
293
+ );
294
+ }
295
+
296
+ private _registerExecutionFailure(fail: (reason: unknown) => void) {
297
+ this._observeDeviceLoss();
298
+ this._activeExecutionFailures.add(fail);
299
+ if (this._deviceLostSettled) {
300
+ this._activeExecutionFailures.delete(fail);
301
+ queueMicrotask(() => fail(this._deviceLostReason));
302
+ }
303
+ return () => this._activeExecutionFailures.delete(fail);
304
+ }
305
+
306
+ /** Captures network GPU timestamps across every tile of the next image. */
307
+ profileNextExecution() {
308
+ if (!this._nativeExecutor || !this._device.features.has('timestamp-query')) return false;
309
+ this._profileNextImage = true;
310
+ return true;
311
+ }
312
+
313
+ getLastExecutionProfile() {
314
+ return this._lastImageProfile ? Promise.resolve(this._lastImageProfile) : undefined;
315
+ }
316
+
317
+ private _processImageData(
318
+ color: ImageData | HDRImageData,
319
+ albedo: ImageData | HDRImageData | undefined,
320
+ normal: ImageData | HDRImageData | undefined,
321
+ isHDR: boolean
322
+ ) {
323
+ const rawData = color.data;
324
+ const pixelsCount = rawData.length / 4;
325
+ const channels = this._aux ? 9 : 3;
326
+ const tensorData = new Float32Array(pixelsCount * channels);
327
+
328
+ if ((albedo && !normal) || (normal && !albedo)) {
329
+ throw new Error('Normal map and albedo map are both required');
330
+ }
331
+ if (albedo && normal) {
332
+ if (
333
+ albedo.width !== normal.width ||
334
+ albedo.height !== normal.height ||
335
+ color.width !== albedo.width ||
336
+ color.height !== albedo.height
337
+ ) {
338
+ throw new Error('Image size mismatch');
339
+ }
340
+ }
341
+
342
+ const albedoData = albedo?.data;
343
+ const normalData = normal?.data;
344
+ // Each guide can be an 8-bit image or normalized float RGBA,
345
+ // independently of the color image's HDR setting and the other guide.
346
+ const albedoDivisor = albedoData instanceof Float32Array ? 1 : 255;
347
+ const normalDivisor = normalData instanceof Float32Array ? 1 : 255;
348
+ for (let i = 0; i < rawData.length; i += 4) {
349
+ const i2 = (i / 4) * channels;
350
+
351
+ for (let c = 0; c < 3; c++) {
352
+ if (isHDR) {
353
+ tensorData[i2 + c] = rawData[i + c];
354
+ } else {
355
+ tensorData[i2 + c] = rawData[i + c] / 255;
356
+ }
357
+ if (albedoData) {
358
+ tensorData[i2 + c + 3] = albedoData[i + c] / albedoDivisor;
359
+ }
360
+ if (normalData) {
361
+ tensorData[i2 + c + 6] = normalData[i + c] / normalDivisor;
362
+ }
363
+ }
364
+ }
365
+
366
+ return tensorData;
367
+ }
368
+
369
+ private _readTile(
370
+ data: Float32Array,
371
+ channels: number,
372
+ srcTile: Tile,
373
+ width: number
374
+ ) {
375
+ const tileData = new Float32Array(
376
+ srcTile.width * srcTile.height * channels
377
+ );
378
+ const height = data.length / (width * channels);
379
+ for (let y = 0; y < srcTile.height; y++) {
380
+ for (let x = 0; x < srcTile.width; x++) {
381
+ const sourceX = Math.min(width - 1, x + srcTile.x);
382
+ const sourceY = Math.min(height - 1, y + srcTile.y);
383
+ const i2 = (sourceY * width + sourceX) * channels;
384
+ const i1 = (y * srcTile.width + x) * channels;
385
+
386
+ for (let c = 0; c < channels; c++) {
387
+ tileData[i1 + c] = data[i2 + c];
388
+ }
389
+ }
390
+ }
391
+ return tileData;
392
+ }
393
+
394
+ private _writeTile(
395
+ imageData: ImageData | HDRImageData,
396
+ srcTile: Tile,
397
+ dstTile: Tile,
398
+ srcTileData: Float32Array,
399
+ srcWidth: number,
400
+ isHDR: boolean
401
+ ) {
402
+ const { data: outImageData, width } = imageData;
403
+ const dx = dstTile.x - srcTile.x;
404
+ const dy = dstTile.y - srcTile.y;
405
+ for (let y = 0; y < dstTile.height; y++) {
406
+ for (let x = 0; x < dstTile.width; x++) {
407
+ const i1 = ((y + dy) * srcWidth + x + dx) * 3;
408
+ const i2 = ((y + dstTile.y) * width + (x + dstTile.x)) * 4;
409
+
410
+ for (let c = 0; c < 3; c++) {
411
+ if (isHDR) {
412
+ outImageData[i2 + c] = srcTileData[i1 + c];
413
+ } else {
414
+ outImageData[i2 + c] = Math.min(
415
+ Math.max(srcTileData[i1 + c] * 255, 0),
416
+ 255
417
+ );
418
+ }
419
+ }
420
+ imageData.data[i2 + 3] = isHDR ? 1 : 255;
421
+ }
422
+ }
423
+ }
424
+
425
+ private async _executeTile(
426
+ inputData:
427
+ | Float32Array
428
+ | {
429
+ color: GPUBuffer | GPUTexture;
430
+ // TODO optional
431
+ albedo?: GPUBuffer | GPUTexture;
432
+ normal?: GPUBuffer | GPUTexture;
433
+ },
434
+ outputTileData: ImageData | HDRImageData | undefined,
435
+ outputImageData: ImageData | HDRImageData | undefined,
436
+ tile: PlannedTile,
437
+ isFirstTile: boolean,
438
+ width: number,
439
+ height: number,
440
+ isHDR: boolean,
441
+ denoiseAlpha?: boolean
442
+ ) {
443
+ const channels = this._aux ? 9 : 3;
444
+ const srcTile = new Tile(
445
+ tile.input.x,
446
+ tile.input.y,
447
+ tile.input.width,
448
+ tile.input.height
449
+ );
450
+ const dstTile = new Tile(
451
+ tile.output.x,
452
+ tile.output.y,
453
+ tile.output.width,
454
+ tile.output.height
455
+ );
456
+ const srcTileWidth = srcTile.width;
457
+ const srcTileHeight = srcTile.height;
458
+
459
+ let nativeOutputBuffer: GPUBuffer | undefined;
460
+ let denoisedData: Float32Array | undefined;
461
+ let inputScale = 1;
462
+ const device = this._device;
463
+ let dataProcessGPU = this._dataProcessGPU;
464
+
465
+ if (inputData instanceof Float32Array) {
466
+ let tileData = this._readTile(inputData, channels, srcTile, width);
467
+ if (isHDR) {
468
+ inputScale = avgLogLum({
469
+ data: tileData,
470
+ channels
471
+ });
472
+ tileData = hdrTransferFuncCPU({
473
+ data: tileData,
474
+ channels,
475
+ inputScale,
476
+ transfer: this._hdrTransfer
477
+ });
478
+ }
479
+ denoisedData = await (this._webNNExecutor ?? this._nativeExecutor!).executeCPU(
480
+ tileData,
481
+ srcTileWidth,
482
+ srcTileHeight
483
+ );
484
+ } else {
485
+ if (!dataProcessGPU) {
486
+ dataProcessGPU = this._dataProcessGPU = new GPUDataProcess(
487
+ device,
488
+ isHDR,
489
+ this._hdrTransfer
490
+ );
491
+ }
492
+ dataProcessGPU.setImageSize(width, height);
493
+ dataProcessGPU.setInputTile(srcTile);
494
+ // Display the noisy input instead of prev denoised result
495
+ if (isFirstTile) {
496
+ dataProcessGPU.copyInputDataToOutput(inputData.color);
497
+ }
498
+ const { color, albedo, normal } = dataProcessGPU.forward(
499
+ inputData.color,
500
+ this._aux ? inputData.albedo : undefined,
501
+ this._aux ? inputData.normal : undefined,
502
+ denoiseAlpha
503
+ );
504
+
505
+ nativeOutputBuffer = await (this._webNNExecutor ?? this._nativeExecutor!).execute(
506
+ this._aux ? [color, albedo!, normal!] : [color],
507
+ srcTileWidth,
508
+ srcTileHeight
509
+ );
510
+ }
511
+
512
+ let outBuffer: GPUBuffer;
513
+
514
+ if (inputData instanceof Float32Array) {
515
+ if (isHDR) {
516
+ denoisedData = hdrTransferFuncInverseCPU({
517
+ data: denoisedData!,
518
+ channels: 3,
519
+ inputScale,
520
+ transfer: this._hdrTransfer
521
+ });
522
+ }
523
+
524
+ this._writeTile(
525
+ outputImageData!,
526
+ srcTile,
527
+ dstTile,
528
+ denoisedData!,
529
+ srcTile.width,
530
+ isHDR
531
+ );
532
+
533
+ for (let y = 0; y < dstTile.height; y++) {
534
+ for (let x = 0; x < dstTile.width; x++) {
535
+ const i1 = (y * dstTile.width + x) * 4;
536
+ const i2 = ((y + dstTile.y) * width + (x + dstTile.x)) * 4;
537
+ for (let c = 0; c < 4; c++) {
538
+ outputTileData!.data[i1 + c] = outputImageData!.data[i2 + c];
539
+ }
540
+ }
541
+ }
542
+ } else {
543
+ dataProcessGPU!.setOutputTile(dstTile, srcTile);
544
+ outBuffer = dataProcessGPU!.inverse(
545
+ nativeOutputBuffer!,
546
+ inputData.color
547
+ );
548
+ }
549
+ return outBuffer!;
550
+ }
551
+
552
+ tileExecute<T extends ImageData | HDRImageData | GPUImageData>({
553
+ color,
554
+ albedo,
555
+ normal,
556
+ done,
557
+ progress,
558
+ denoiseAlpha,
559
+ tileOverlap,
560
+ wholeImage,
561
+ scheduling = 'event-loop',
562
+ error
563
+ }: {
564
+ color: T;
565
+ albedo?: ImageData | HDRImageData | GPUImageData;
566
+ normal?: ImageData | HDRImageData | GPUImageData;
567
+ /**
568
+ * If denoise alpha channel. Otherwise denoise RGB channels.
569
+ */
570
+ denoiseAlpha?: boolean;
571
+ /**
572
+ * Execute the complete input image as one tile, ignoring `maxTileSize`.
573
+ * The image must fit the device's buffer and dispatch limits.
574
+ */
575
+ wholeImage?: boolean;
576
+ /**
577
+ * Per-side context for boundaries shared with another tile. Defaults to
578
+ * half of the model receptive field rounded up to 16 pixels.
579
+ */
580
+ tileOverlap?: number;
581
+ /**
582
+ * How JavaScript yields between completed GPU tiles. `event-loop`
583
+ * (default) continues on the next macrotask. `animation-frame` waits for
584
+ * the next display frame, bounded by a short timer so hidden pages still
585
+ * complete.
586
+ */
587
+ scheduling?: 'animation-frame' | 'event-loop';
588
+ done: (
589
+ outputData: T extends GPUImageData ? GPUImageDataOutput : T
590
+ ) => void | Promise<void>;
591
+ /** Receives asynchronous execution, queue, and callback failures. */
592
+ error?: (reason: unknown) => void | Promise<void>;
593
+ progress?: (
594
+ outputData: T extends GPUImageData ? GPUImageDataOutput : T,
595
+ tileData: (T extends GPUImageData ? GPUImageDataOutput : T) | undefined,
596
+ tile: Tile,
597
+ currentIdx: number,
598
+ totalIdx: number
599
+ ) => void | Promise<void>;
600
+ }): () => void {
601
+ if (this._aux && (!albedo || !normal)) {
602
+ throw new Error('Normal map and albedo map are both required');
603
+ }
604
+
605
+ if (!this._aux) {
606
+ if (albedo || normal) {
607
+ throw new Error('Normal map and albedo map are not required');
608
+ }
609
+ }
610
+
611
+ const width = color.width;
612
+ const height = color.height;
613
+ const adaptiveTileSize = this._dynamicTileController.tileSize;
614
+ // The planner aligns the maximum down, so round up to keep one tile.
615
+ const requestedTileSize = wholeImage
616
+ ? roundUp(Math.max(width, height), OIDN_TILE_ALIGNMENT)
617
+ : adaptiveTileSize;
618
+ const defaultTileOverlap = roundUp(
619
+ this._modelSpec.receptiveField / 2,
620
+ OIDN_TILE_ALIGNMENT
621
+ );
622
+ const resolvedTileOverlap = tileOverlap === undefined
623
+ ? defaultTileOverlap
624
+ : roundUp(Math.max(0, tileOverlap), OIDN_TILE_ALIGNMENT);
625
+ const plan = planTileGrid(
626
+ width,
627
+ height,
628
+ requestedTileSize,
629
+ resolvedTileOverlap
630
+ );
631
+ const shouldAdaptTileSize = plan.tiles.length > 1;
632
+
633
+ // TODO should fixed to be hdr when UNet is created.
634
+ // weights of hdr and ldr is different
635
+
636
+ const hdr = this._hdr || false;
637
+ let rawData: Float32Array;
638
+ if (!isGPUImageData(color)) {
639
+ rawData = this._processImageData(
640
+ color,
641
+ albedo as ImageData | HDRImageData | undefined,
642
+ normal as ImageData | HDRImageData | undefined,
643
+ hdr
644
+ );
645
+ }
646
+ function makeImageData(width: number, height: number) {
647
+ return hdr
648
+ ? {
649
+ data: new Float32Array(width * height * 4),
650
+ width,
651
+ height
652
+ }
653
+ : new ImageData(width, height);
654
+ }
655
+
656
+ const outputImageData = isGPUImageData(color)
657
+ ? undefined
658
+ : makeImageData(width, height);
659
+
660
+ type ExecutionState = 'active' | 'aborted' | 'settled';
661
+ let state: ExecutionState = 'active';
662
+ let scheduledTimer: ReturnType<typeof setTimeout> | undefined;
663
+ let scheduledAnimationFrame: number | undefined;
664
+ let unregisterDeviceLoss = () => false;
665
+
666
+ const now = () =>
667
+ typeof performance === 'undefined' ? Date.now() : performance.now();
668
+ const executionStartTime = now();
669
+ const tileTimesMs: number[] = [];
670
+ const profileImage = this._profileNextImage === true;
671
+ this._profileNextImage = false;
672
+ const layerTimes = new Map<string, number>();
673
+ const cancelScheduledTile = () => {
674
+ if (scheduledTimer !== undefined) {
675
+ clearTimeout(scheduledTimer);
676
+ scheduledTimer = undefined;
677
+ }
678
+ if (
679
+ scheduledAnimationFrame !== undefined &&
680
+ typeof cancelAnimationFrame !== 'undefined'
681
+ ) {
682
+ cancelAnimationFrame(scheduledAnimationFrame);
683
+ scheduledAnimationFrame = undefined;
684
+ }
685
+ };
686
+ const reportCallbackFailure = (reason: unknown) => {
687
+ // An error callback is the terminal observer and cannot report its own
688
+ // failure through the same channel. Keep that failure handled.
689
+ console.error('OIDN error callback failed', reason);
690
+ };
691
+ const settleError = (reason: unknown) => {
692
+ if (state !== 'active') return;
693
+ state = 'settled';
694
+ cancelScheduledTile();
695
+ unregisterDeviceLoss();
696
+ if (error) {
697
+ try {
698
+ void Promise.resolve(error(reason)).catch(reportCallbackFailure);
699
+ } catch (callbackReason) {
700
+ reportCallbackFailure(callbackReason);
701
+ }
702
+ } else {
703
+ console.error('OIDN execution failed', reason);
704
+ }
705
+ };
706
+ const scheduleNextTile = (callback: () => void) => {
707
+ if (
708
+ scheduling === 'event-loop' ||
709
+ typeof requestAnimationFrame === 'undefined'
710
+ ) {
711
+ scheduledTimer = setTimeout(() => {
712
+ scheduledTimer = undefined;
713
+ callback();
714
+ }, 0);
715
+ } else {
716
+ // Hidden documents pause requestAnimationFrame. Race it against a
717
+ // timer so animation-frame scheduling still completes in background
718
+ // tabs, minimized windows, and offscreen iframes.
719
+ const run = () => {
720
+ cancelScheduledTile();
721
+ callback();
722
+ };
723
+ scheduledAnimationFrame = requestAnimationFrame(run);
724
+ scheduledTimer = setTimeout(run, ANIMATION_FRAME_FALLBACK_MS);
725
+ }
726
+ };
727
+
728
+ const executeTile = async (tileIndex: number) => {
729
+ if (state !== 'active') {
730
+ return;
731
+ }
732
+ const tile = plan.tiles[tileIndex];
733
+ const outputTileData = isGPUImageData(color)
734
+ ? undefined
735
+ : makeImageData(tile.output.width, tile.output.height);
736
+ const tileStartTime = now();
737
+ if (profileImage) this._nativeExecutor!.profileNextExecution();
738
+ const resGPUBuffer = await this._executeTile(
739
+ isGPUImageData(color)
740
+ ? {
741
+ color: color.data,
742
+ albedo: (albedo as GPUImageData | undefined)?.data,
743
+ normal: (normal as GPUImageData | undefined)?.data
744
+ }
745
+ : rawData,
746
+ outputTileData,
747
+ outputImageData,
748
+ tile,
749
+ tileIndex === 0,
750
+ width,
751
+ height,
752
+ hdr,
753
+ denoiseAlpha
754
+ );
755
+ if (profileImage) {
756
+ const profile = await this._nativeExecutor!.getLastExecutionProfile();
757
+ for (const layer of profile?.layers ?? []) {
758
+ layerTimes.set(layer.id, (layerTimes.get(layer.id) ?? 0) + layer.durationMs);
759
+ }
760
+ }
761
+ if (state !== 'active') return;
762
+ const output = outputImageData || {
763
+ data: resGPUBuffer,
764
+ width,
765
+ height
766
+ };
767
+ if (progress) {
768
+ await progress(
769
+ output as any,
770
+ // Is undefined if using webgpu buffer
771
+ outputTileData as any,
772
+ new Tile(
773
+ tile.output.x,
774
+ tile.output.y,
775
+ tile.output.width,
776
+ tile.output.height
777
+ ),
778
+ tileIndex,
779
+ plan.tiles.length
780
+ );
781
+ }
782
+ if (state !== 'active') return;
783
+
784
+ const hasNextTile = tileIndex + 1 < plan.tiles.length;
785
+ await this._device.queue.onSubmittedWorkDone();
786
+ if (state !== 'active') return;
787
+ const continueAfterGPUWork = async () => {
788
+ tileTimesMs.push(now() - tileStartTime);
789
+ if (state !== 'active') return;
790
+
791
+ if (hasNextTile) {
792
+ scheduleNextTile(() => {
793
+ if (state !== 'active') return;
794
+ void executeTile(tileIndex + 1).catch(settleError);
795
+ });
796
+ } else {
797
+ const sortedTileTimes = [...tileTimesMs].sort((a, b) => a - b);
798
+ const middle = Math.floor(sortedTileTimes.length / 2);
799
+ const medianTileTime = sortedTileTimes.length % 2
800
+ ? sortedTileTimes[middle]
801
+ : (sortedTileTimes[middle - 1] + sortedTileTimes[middle]) / 2;
802
+ this._lastExecution = {
803
+ width,
804
+ height,
805
+ tileCount: plan.tiles.length,
806
+ tileColumns: plan.columns,
807
+ tileRows: plan.rows,
808
+ tileOverlap: plan.overlap,
809
+ inputPixelCount: plan.inputPixelCount,
810
+ inputShapeCount: plan.inputShapeCount,
811
+ durationMs: now() - executionStartTime,
812
+ tileTimeMs: {
813
+ min: sortedTileTimes[0],
814
+ median: medianTileTime,
815
+ mean:
816
+ sortedTileTimes.reduce((sum, value) => sum + value, 0) /
817
+ sortedTileTimes.length,
818
+ max: sortedTileTimes[sortedTileTimes.length - 1]
819
+ }
820
+ };
821
+ // Adapt only from complete executions. Cancelled work is commonly
822
+ // contending with interactive rendering and is not representative.
823
+ if (shouldAdaptTileSize) {
824
+ this._dynamicTileController.observe(tileTimesMs);
825
+ }
826
+ // GPU inference is complete; device loss can no longer affect this
827
+ // execution. Deregister before the user callback resolves so a
828
+ // completed execution never remains retained by the device watcher.
829
+ unregisterDeviceLoss();
830
+ if (profileImage) {
831
+ const layers = [...layerTimes].map(([id, durationMs]) => ({ id, durationMs }));
832
+ this._lastImageProfile = { totalMs: layers.reduce((sum, layer) => sum + layer.durationMs, 0), layers };
833
+ }
834
+ await done(output as any);
835
+ if (state === 'active') {
836
+ state = 'settled';
837
+ }
838
+ }
839
+ };
840
+ await continueAfterGPUWork();
841
+ };
842
+
843
+ unregisterDeviceLoss = this._registerExecutionFailure(settleError);
844
+ void executeTile(0).catch(settleError);
845
+
846
+ return () => {
847
+ if (state !== 'active') return;
848
+ state = 'aborted';
849
+ cancelScheduledTile();
850
+ unregisterDeviceLoss();
851
+ };
852
+ }
853
+
854
+ dispose() {
855
+ this._dataProcessGPU?.dispose();
856
+ this._nativeExecutor?.dispose();
857
+ this._webNNExecutor?.dispose();
858
+ }
859
+ }
860
+
861
+ export default UNet;