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/lib/UNet.d.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { HostTensor } from './tza';
2
+ import { Tile, type HDRTransfer } from './process';
3
+ import { type DynamicTileSetting } from './tileScheduler';
4
+ import { type UNetModelSpec } from './modelSpec';
5
+ import { type NativeSubgroupMatrixOptions, type NativeUNetGemmOptions, type NativeUNetKernelSetting, type NativeUNetExecutionProfile, type NativeUNetPrecisionSetting } from './nativeUNet';
6
+ export type UNetEngineSetting = 'auto' | 'wgsl' | 'webnn';
7
+ interface HDRImageData {
8
+ data: Float32Array;
9
+ width: number;
10
+ height: number;
11
+ }
12
+ interface GPUImageData {
13
+ data: GPUBuffer | GPUTexture;
14
+ width: number;
15
+ height: number;
16
+ }
17
+ interface GPUImageDataOutput {
18
+ data: GPUBuffer;
19
+ width: number;
20
+ height: number;
21
+ }
22
+ export interface UNetExecutionStats {
23
+ width: number;
24
+ height: number;
25
+ tileCount: number;
26
+ tileColumns: number;
27
+ tileRows: number;
28
+ tileOverlap: number;
29
+ inputPixelCount: number;
30
+ inputShapeCount: number;
31
+ durationMs: number;
32
+ tileTimeMs: {
33
+ min: number;
34
+ median: number;
35
+ mean: number;
36
+ max: number;
37
+ };
38
+ }
39
+ declare class UNet {
40
+ private _device;
41
+ private _aux;
42
+ private _hdr;
43
+ private _hdrTransfer;
44
+ private _dataProcessGPU?;
45
+ private _nativeExecutor?;
46
+ private _webNNExecutor?;
47
+ private _modelSpec;
48
+ private _inputChannels;
49
+ private _engine;
50
+ private _dynamicTileController;
51
+ private _lastExecution?;
52
+ private _profileNextImage;
53
+ private _lastImageProfile?;
54
+ private _activeExecutionFailures;
55
+ private _deviceLostObserved;
56
+ private _deviceLostSettled;
57
+ private _deviceLostReason;
58
+ constructor(hostTensors: Map<string, HostTensor>, device: GPUDevice, opts?: {
59
+ /**
60
+ * If use auxiliary data.
61
+ */
62
+ aux?: boolean;
63
+ /**
64
+ * If input is HDR image.
65
+ */
66
+ hdr?: boolean;
67
+ /** HDR transfer function expected by the trained model. */
68
+ hdrTransfer?: HDRTransfer;
69
+ maxTileSize?: number;
70
+ dynamicTile?: DynamicTileSetting;
71
+ /** Native WGSL or the experimental WebNN backend. */
72
+ engine?: UNetEngineSetting;
73
+ /** Arithmetic/storage precision used by the native WGSL executor. */
74
+ precision?: NativeUNetPrecisionSetting;
75
+ /** Model-independent convolution kernel selection. */
76
+ kernel?: NativeUNetKernelSetting;
77
+ gemm?: NativeUNetGemmOptions;
78
+ subgroupMatrix?: NativeSubgroupMatrixOptions;
79
+ /** Explicit descriptor for a new OIDN topology not in the built-in registry. */
80
+ modelSpec?: UNetModelSpec;
81
+ });
82
+ getDevice(): GPUDevice;
83
+ /** Completes backend compilation before first interactive use. */
84
+ prepare(): Promise<void>;
85
+ /**
86
+ * Prepares the input shapes selected for an image before its first denoise.
87
+ * Hosts can call this while they still display their model-loading state.
88
+ */
89
+ prepareForImage(width: number, height: number, options?: {
90
+ tileOverlap?: number;
91
+ wholeImage?: boolean;
92
+ }): Promise<void>;
93
+ getRuntimeInfo(): {
94
+ configuredEngine: UNetEngineSetting;
95
+ gpuEngine: "wgsl" | "webnn";
96
+ precision: import("./nativeUNetCore").NativeUNetPrecision;
97
+ kernel: {
98
+ configured: NativeUNetKernelSetting;
99
+ gemm: Readonly<Required<NativeUNetGemmOptions>>;
100
+ maxSpatialInputBlocks: number;
101
+ subgroupsAvailable: boolean;
102
+ subgroupMatrix: import("./nativeUNetCore").NativeSubgroupMatrixConfig | undefined;
103
+ } | undefined;
104
+ webnn: import("./webnnUNet").WebNNRuntimeSupport | undefined;
105
+ resources: import("./resourceTracker").OIDNResourceSnapshot;
106
+ model: string;
107
+ modelFamily: (string & {}) | "oidn-unet-small" | "oidn-unet-large";
108
+ inputChannels: number;
109
+ hdrTransfer: HDRTransfer;
110
+ dynamicTile: {
111
+ enabled: boolean;
112
+ currentTileSize: number;
113
+ minTileSize: number;
114
+ maxTileSize: number;
115
+ targetTileTimeMs: number;
116
+ };
117
+ lastExecution: UNetExecutionStats | undefined;
118
+ activeExecutionCount: number;
119
+ };
120
+ private _observeDeviceLoss;
121
+ private _registerExecutionFailure;
122
+ /** Captures network GPU timestamps across every tile of the next image. */
123
+ profileNextExecution(): boolean;
124
+ getLastExecutionProfile(): Promise<NativeUNetExecutionProfile> | undefined;
125
+ private _processImageData;
126
+ private _readTile;
127
+ private _writeTile;
128
+ private _executeTile;
129
+ tileExecute<T extends ImageData | HDRImageData | GPUImageData>({ color, albedo, normal, done, progress, denoiseAlpha, tileOverlap, wholeImage, scheduling, error }: {
130
+ color: T;
131
+ albedo?: ImageData | HDRImageData | GPUImageData;
132
+ normal?: ImageData | HDRImageData | GPUImageData;
133
+ /**
134
+ * If denoise alpha channel. Otherwise denoise RGB channels.
135
+ */
136
+ denoiseAlpha?: boolean;
137
+ /**
138
+ * Execute the complete input image as one tile, ignoring `maxTileSize`.
139
+ * The image must fit the device's buffer and dispatch limits.
140
+ */
141
+ wholeImage?: boolean;
142
+ /**
143
+ * Per-side context for boundaries shared with another tile. Defaults to
144
+ * half of the model receptive field rounded up to 16 pixels.
145
+ */
146
+ tileOverlap?: number;
147
+ /**
148
+ * How JavaScript yields between completed GPU tiles. `event-loop`
149
+ * (default) continues on the next macrotask. `animation-frame` waits for
150
+ * the next display frame, bounded by a short timer so hidden pages still
151
+ * complete.
152
+ */
153
+ scheduling?: 'animation-frame' | 'event-loop';
154
+ done: (outputData: T extends GPUImageData ? GPUImageDataOutput : T) => void | Promise<void>;
155
+ /** Receives asynchronous execution, queue, and callback failures. */
156
+ error?: (reason: unknown) => void | Promise<void>;
157
+ progress?: (outputData: T extends GPUImageData ? GPUImageDataOutput : T, tileData: (T extends GPUImageData ? GPUImageDataOutput : T) | undefined, tile: Tile, currentIdx: number, totalIdx: number) => void | Promise<void>;
158
+ }): () => void;
159
+ dispose(): void;
160
+ }
161
+ export default UNet;
package/lib/UNet.js ADDED
@@ -0,0 +1,536 @@
1
+ import { GPUDataProcess, Tile, avgLogLum, hdrTransferFuncCPU, hdrTransferFuncInverseCPU } from './process';
2
+ import { DynamicTileController, planTileGrid, OIDN_TILE_ALIGNMENT } from './tileScheduler';
3
+ import { detectUNetModelSpec, validateUNetModel } from './modelSpec';
4
+ import { NativeUNetExecutor } from './nativeUNet';
5
+ import { WebNNUNetExecutor } from './webnnUNet';
6
+ /** Upper bound on waiting for a display frame between tiles. */
7
+ const ANIMATION_FRAME_FALLBACK_MS = 100;
8
+ function roundUp(a, b) {
9
+ return Math.ceil(a / b) * b;
10
+ }
11
+ function isGPUImageData(data) {
12
+ return data.data instanceof GPUBuffer || data.data instanceof GPUTexture;
13
+ }
14
+ class UNet {
15
+ _device;
16
+ _aux;
17
+ _hdr;
18
+ _hdrTransfer;
19
+ _dataProcessGPU;
20
+ _nativeExecutor;
21
+ _webNNExecutor;
22
+ _modelSpec;
23
+ _inputChannels;
24
+ _engine;
25
+ _dynamicTileController;
26
+ _lastExecution;
27
+ _profileNextImage = false;
28
+ _lastImageProfile;
29
+ _activeExecutionFailures = new Set();
30
+ _deviceLostObserved = false;
31
+ _deviceLostSettled = false;
32
+ _deviceLostReason;
33
+ constructor(hostTensors, device, opts = {}) {
34
+ this._aux = opts.aux || false;
35
+ this._hdr = opts.hdr || false;
36
+ this._hdrTransfer = opts.hdrTransfer ?? 'pu';
37
+ this._engine = opts.engine ?? 'auto';
38
+ const modelSpec = opts.modelSpec ?? detectUNetModelSpec(hostTensors);
39
+ const validatedModel = validateUNetModel(hostTensors, modelSpec);
40
+ this._modelSpec = validatedModel.spec;
41
+ this._inputChannels = validatedModel.inputChannels;
42
+ const expectedInputChannels = this._aux ? 9 : 3;
43
+ if (validatedModel.inputChannels !== expectedInputChannels) {
44
+ throw new Error(`OIDN model expects ${validatedModel.inputChannels} input channels, ` +
45
+ `but aux=${this._aux} provides ${expectedInputChannels}`);
46
+ }
47
+ this._dynamicTileController = new DynamicTileController(opts.maxTileSize ?? 512, opts.dynamicTile);
48
+ this._device = device;
49
+ this._observeDeviceLoss();
50
+ if (this._engine === 'webnn') {
51
+ this._webNNExecutor = new WebNNUNetExecutor(this._device, validatedModel, { precision: opts.precision });
52
+ }
53
+ else {
54
+ this._nativeExecutor = new NativeUNetExecutor(this._device, validatedModel, {
55
+ precision: opts.precision,
56
+ kernel: opts.kernel,
57
+ gemm: opts.gemm,
58
+ subgroupMatrix: opts.subgroupMatrix
59
+ });
60
+ }
61
+ }
62
+ getDevice() {
63
+ return this._device;
64
+ }
65
+ /** Completes backend compilation before first interactive use. */
66
+ async prepare() {
67
+ if (this._webNNExecutor) {
68
+ await this._webNNExecutor.prepare();
69
+ const overlap = roundUp(this._modelSpec.receptiveField / 2, OIDN_TILE_ALIGNMENT);
70
+ const outputTileEdges = [
71
+ this._dynamicTileController.tileSize,
72
+ this._dynamicTileController.minTileSize
73
+ ];
74
+ await this._webNNExecutor.prewarm([...new Set(outputTileEdges)].map((edge) => ({
75
+ width: edge + 2 * overlap,
76
+ height: edge + 2 * overlap
77
+ })));
78
+ return;
79
+ }
80
+ await this._nativeExecutor.prepare();
81
+ }
82
+ /**
83
+ * Prepares the input shapes selected for an image before its first denoise.
84
+ * Hosts can call this while they still display their model-loading state.
85
+ */
86
+ async prepareForImage(width, height, options = {}) {
87
+ const defaultTileOverlap = roundUp(this._modelSpec.receptiveField / 2, OIDN_TILE_ALIGNMENT);
88
+ const resolvedTileOverlap = options.tileOverlap === undefined
89
+ ? defaultTileOverlap
90
+ : roundUp(Math.max(0, options.tileOverlap), OIDN_TILE_ALIGNMENT);
91
+ const plan = planTileGrid(width, height, options.wholeImage
92
+ ? roundUp(Math.max(width, height), OIDN_TILE_ALIGNMENT)
93
+ : this._dynamicTileController.tileSize, resolvedTileOverlap);
94
+ const shapes = [...new Map(plan.tiles.map(({ input }) => [
95
+ `${input.width}x${input.height}`,
96
+ { width: input.width, height: input.height }
97
+ ])).values()];
98
+ await this._webNNExecutor?.prewarm(shapes);
99
+ this._nativeExecutor?.prewarm(shapes);
100
+ }
101
+ getRuntimeInfo() {
102
+ return {
103
+ configuredEngine: this._engine,
104
+ gpuEngine: this._webNNExecutor ? 'webnn' : 'wgsl',
105
+ precision: (this._webNNExecutor ?? this._nativeExecutor).precision,
106
+ kernel: this._nativeExecutor
107
+ ? {
108
+ configured: this._nativeExecutor.kernelSetting,
109
+ gemm: this._nativeExecutor.gemm,
110
+ maxSpatialInputBlocks: this._nativeExecutor.maxSpatialInputBlocks,
111
+ subgroupsAvailable: this._nativeExecutor.subgroupsAvailable,
112
+ subgroupMatrix: this._nativeExecutor.subgroupMatrix
113
+ }
114
+ : undefined,
115
+ webnn: this._webNNExecutor?.support,
116
+ resources: (this._webNNExecutor ?? this._nativeExecutor).getResourceInfo(),
117
+ model: this._modelSpec.id,
118
+ modelFamily: this._modelSpec.family,
119
+ inputChannels: this._inputChannels,
120
+ hdrTransfer: this._hdrTransfer,
121
+ dynamicTile: {
122
+ enabled: this._dynamicTileController.enabled,
123
+ currentTileSize: this._dynamicTileController.tileSize,
124
+ minTileSize: this._dynamicTileController.minTileSize,
125
+ maxTileSize: this._dynamicTileController.maxTileSize,
126
+ targetTileTimeMs: this._dynamicTileController.targetTileTimeMs
127
+ },
128
+ lastExecution: this._lastExecution,
129
+ activeExecutionCount: this._activeExecutionFailures.size
130
+ };
131
+ }
132
+ _observeDeviceLoss() {
133
+ // Object.create-based embedders/tests can bypass field initializers.
134
+ this._activeExecutionFailures ??= new Set();
135
+ if (this._deviceLostObserved)
136
+ return;
137
+ this._deviceLostObserved = true;
138
+ const deviceLost = this._device.lost;
139
+ if (!deviceLost)
140
+ return;
141
+ const failAll = (reason) => {
142
+ if (this._deviceLostSettled)
143
+ return;
144
+ this._deviceLostSettled = true;
145
+ this._deviceLostReason = reason;
146
+ const active = [...this._activeExecutionFailures];
147
+ this._activeExecutionFailures.clear();
148
+ for (const fail of active)
149
+ fail(reason);
150
+ };
151
+ void deviceLost.then((info) => failAll(new Error(`WebGPU device lost: ${info.message}`)), failAll);
152
+ }
153
+ _registerExecutionFailure(fail) {
154
+ this._observeDeviceLoss();
155
+ this._activeExecutionFailures.add(fail);
156
+ if (this._deviceLostSettled) {
157
+ this._activeExecutionFailures.delete(fail);
158
+ queueMicrotask(() => fail(this._deviceLostReason));
159
+ }
160
+ return () => this._activeExecutionFailures.delete(fail);
161
+ }
162
+ /** Captures network GPU timestamps across every tile of the next image. */
163
+ profileNextExecution() {
164
+ if (!this._nativeExecutor || !this._device.features.has('timestamp-query'))
165
+ return false;
166
+ this._profileNextImage = true;
167
+ return true;
168
+ }
169
+ getLastExecutionProfile() {
170
+ return this._lastImageProfile ? Promise.resolve(this._lastImageProfile) : undefined;
171
+ }
172
+ _processImageData(color, albedo, normal, isHDR) {
173
+ const rawData = color.data;
174
+ const pixelsCount = rawData.length / 4;
175
+ const channels = this._aux ? 9 : 3;
176
+ const tensorData = new Float32Array(pixelsCount * channels);
177
+ if ((albedo && !normal) || (normal && !albedo)) {
178
+ throw new Error('Normal map and albedo map are both required');
179
+ }
180
+ if (albedo && normal) {
181
+ if (albedo.width !== normal.width ||
182
+ albedo.height !== normal.height ||
183
+ color.width !== albedo.width ||
184
+ color.height !== albedo.height) {
185
+ throw new Error('Image size mismatch');
186
+ }
187
+ }
188
+ const albedoData = albedo?.data;
189
+ const normalData = normal?.data;
190
+ // Each guide can be an 8-bit image or normalized float RGBA,
191
+ // independently of the color image's HDR setting and the other guide.
192
+ const albedoDivisor = albedoData instanceof Float32Array ? 1 : 255;
193
+ const normalDivisor = normalData instanceof Float32Array ? 1 : 255;
194
+ for (let i = 0; i < rawData.length; i += 4) {
195
+ const i2 = (i / 4) * channels;
196
+ for (let c = 0; c < 3; c++) {
197
+ if (isHDR) {
198
+ tensorData[i2 + c] = rawData[i + c];
199
+ }
200
+ else {
201
+ tensorData[i2 + c] = rawData[i + c] / 255;
202
+ }
203
+ if (albedoData) {
204
+ tensorData[i2 + c + 3] = albedoData[i + c] / albedoDivisor;
205
+ }
206
+ if (normalData) {
207
+ tensorData[i2 + c + 6] = normalData[i + c] / normalDivisor;
208
+ }
209
+ }
210
+ }
211
+ return tensorData;
212
+ }
213
+ _readTile(data, channels, srcTile, width) {
214
+ const tileData = new Float32Array(srcTile.width * srcTile.height * channels);
215
+ const height = data.length / (width * channels);
216
+ for (let y = 0; y < srcTile.height; y++) {
217
+ for (let x = 0; x < srcTile.width; x++) {
218
+ const sourceX = Math.min(width - 1, x + srcTile.x);
219
+ const sourceY = Math.min(height - 1, y + srcTile.y);
220
+ const i2 = (sourceY * width + sourceX) * channels;
221
+ const i1 = (y * srcTile.width + x) * channels;
222
+ for (let c = 0; c < channels; c++) {
223
+ tileData[i1 + c] = data[i2 + c];
224
+ }
225
+ }
226
+ }
227
+ return tileData;
228
+ }
229
+ _writeTile(imageData, srcTile, dstTile, srcTileData, srcWidth, isHDR) {
230
+ const { data: outImageData, width } = imageData;
231
+ const dx = dstTile.x - srcTile.x;
232
+ const dy = dstTile.y - srcTile.y;
233
+ for (let y = 0; y < dstTile.height; y++) {
234
+ for (let x = 0; x < dstTile.width; x++) {
235
+ const i1 = ((y + dy) * srcWidth + x + dx) * 3;
236
+ const i2 = ((y + dstTile.y) * width + (x + dstTile.x)) * 4;
237
+ for (let c = 0; c < 3; c++) {
238
+ if (isHDR) {
239
+ outImageData[i2 + c] = srcTileData[i1 + c];
240
+ }
241
+ else {
242
+ outImageData[i2 + c] = Math.min(Math.max(srcTileData[i1 + c] * 255, 0), 255);
243
+ }
244
+ }
245
+ imageData.data[i2 + 3] = isHDR ? 1 : 255;
246
+ }
247
+ }
248
+ }
249
+ async _executeTile(inputData, outputTileData, outputImageData, tile, isFirstTile, width, height, isHDR, denoiseAlpha) {
250
+ const channels = this._aux ? 9 : 3;
251
+ const srcTile = new Tile(tile.input.x, tile.input.y, tile.input.width, tile.input.height);
252
+ const dstTile = new Tile(tile.output.x, tile.output.y, tile.output.width, tile.output.height);
253
+ const srcTileWidth = srcTile.width;
254
+ const srcTileHeight = srcTile.height;
255
+ let nativeOutputBuffer;
256
+ let denoisedData;
257
+ let inputScale = 1;
258
+ const device = this._device;
259
+ let dataProcessGPU = this._dataProcessGPU;
260
+ if (inputData instanceof Float32Array) {
261
+ let tileData = this._readTile(inputData, channels, srcTile, width);
262
+ if (isHDR) {
263
+ inputScale = avgLogLum({
264
+ data: tileData,
265
+ channels
266
+ });
267
+ tileData = hdrTransferFuncCPU({
268
+ data: tileData,
269
+ channels,
270
+ inputScale,
271
+ transfer: this._hdrTransfer
272
+ });
273
+ }
274
+ denoisedData = await (this._webNNExecutor ?? this._nativeExecutor).executeCPU(tileData, srcTileWidth, srcTileHeight);
275
+ }
276
+ else {
277
+ if (!dataProcessGPU) {
278
+ dataProcessGPU = this._dataProcessGPU = new GPUDataProcess(device, isHDR, this._hdrTransfer);
279
+ }
280
+ dataProcessGPU.setImageSize(width, height);
281
+ dataProcessGPU.setInputTile(srcTile);
282
+ // Display the noisy input instead of prev denoised result
283
+ if (isFirstTile) {
284
+ dataProcessGPU.copyInputDataToOutput(inputData.color);
285
+ }
286
+ const { color, albedo, normal } = dataProcessGPU.forward(inputData.color, this._aux ? inputData.albedo : undefined, this._aux ? inputData.normal : undefined, denoiseAlpha);
287
+ nativeOutputBuffer = await (this._webNNExecutor ?? this._nativeExecutor).execute(this._aux ? [color, albedo, normal] : [color], srcTileWidth, srcTileHeight);
288
+ }
289
+ let outBuffer;
290
+ if (inputData instanceof Float32Array) {
291
+ if (isHDR) {
292
+ denoisedData = hdrTransferFuncInverseCPU({
293
+ data: denoisedData,
294
+ channels: 3,
295
+ inputScale,
296
+ transfer: this._hdrTransfer
297
+ });
298
+ }
299
+ this._writeTile(outputImageData, srcTile, dstTile, denoisedData, srcTile.width, isHDR);
300
+ for (let y = 0; y < dstTile.height; y++) {
301
+ for (let x = 0; x < dstTile.width; x++) {
302
+ const i1 = (y * dstTile.width + x) * 4;
303
+ const i2 = ((y + dstTile.y) * width + (x + dstTile.x)) * 4;
304
+ for (let c = 0; c < 4; c++) {
305
+ outputTileData.data[i1 + c] = outputImageData.data[i2 + c];
306
+ }
307
+ }
308
+ }
309
+ }
310
+ else {
311
+ dataProcessGPU.setOutputTile(dstTile, srcTile);
312
+ outBuffer = dataProcessGPU.inverse(nativeOutputBuffer, inputData.color);
313
+ }
314
+ return outBuffer;
315
+ }
316
+ tileExecute({ color, albedo, normal, done, progress, denoiseAlpha, tileOverlap, wholeImage, scheduling = 'event-loop', error }) {
317
+ if (this._aux && (!albedo || !normal)) {
318
+ throw new Error('Normal map and albedo map are both required');
319
+ }
320
+ if (!this._aux) {
321
+ if (albedo || normal) {
322
+ throw new Error('Normal map and albedo map are not required');
323
+ }
324
+ }
325
+ const width = color.width;
326
+ const height = color.height;
327
+ const adaptiveTileSize = this._dynamicTileController.tileSize;
328
+ // The planner aligns the maximum down, so round up to keep one tile.
329
+ const requestedTileSize = wholeImage
330
+ ? roundUp(Math.max(width, height), OIDN_TILE_ALIGNMENT)
331
+ : adaptiveTileSize;
332
+ const defaultTileOverlap = roundUp(this._modelSpec.receptiveField / 2, OIDN_TILE_ALIGNMENT);
333
+ const resolvedTileOverlap = tileOverlap === undefined
334
+ ? defaultTileOverlap
335
+ : roundUp(Math.max(0, tileOverlap), OIDN_TILE_ALIGNMENT);
336
+ const plan = planTileGrid(width, height, requestedTileSize, resolvedTileOverlap);
337
+ const shouldAdaptTileSize = plan.tiles.length > 1;
338
+ // TODO should fixed to be hdr when UNet is created.
339
+ // weights of hdr and ldr is different
340
+ const hdr = this._hdr || false;
341
+ let rawData;
342
+ if (!isGPUImageData(color)) {
343
+ rawData = this._processImageData(color, albedo, normal, hdr);
344
+ }
345
+ function makeImageData(width, height) {
346
+ return hdr
347
+ ? {
348
+ data: new Float32Array(width * height * 4),
349
+ width,
350
+ height
351
+ }
352
+ : new ImageData(width, height);
353
+ }
354
+ const outputImageData = isGPUImageData(color)
355
+ ? undefined
356
+ : makeImageData(width, height);
357
+ let state = 'active';
358
+ let scheduledTimer;
359
+ let scheduledAnimationFrame;
360
+ let unregisterDeviceLoss = () => false;
361
+ const now = () => typeof performance === 'undefined' ? Date.now() : performance.now();
362
+ const executionStartTime = now();
363
+ const tileTimesMs = [];
364
+ const profileImage = this._profileNextImage === true;
365
+ this._profileNextImage = false;
366
+ const layerTimes = new Map();
367
+ const cancelScheduledTile = () => {
368
+ if (scheduledTimer !== undefined) {
369
+ clearTimeout(scheduledTimer);
370
+ scheduledTimer = undefined;
371
+ }
372
+ if (scheduledAnimationFrame !== undefined &&
373
+ typeof cancelAnimationFrame !== 'undefined') {
374
+ cancelAnimationFrame(scheduledAnimationFrame);
375
+ scheduledAnimationFrame = undefined;
376
+ }
377
+ };
378
+ const reportCallbackFailure = (reason) => {
379
+ // An error callback is the terminal observer and cannot report its own
380
+ // failure through the same channel. Keep that failure handled.
381
+ console.error('OIDN error callback failed', reason);
382
+ };
383
+ const settleError = (reason) => {
384
+ if (state !== 'active')
385
+ return;
386
+ state = 'settled';
387
+ cancelScheduledTile();
388
+ unregisterDeviceLoss();
389
+ if (error) {
390
+ try {
391
+ void Promise.resolve(error(reason)).catch(reportCallbackFailure);
392
+ }
393
+ catch (callbackReason) {
394
+ reportCallbackFailure(callbackReason);
395
+ }
396
+ }
397
+ else {
398
+ console.error('OIDN execution failed', reason);
399
+ }
400
+ };
401
+ const scheduleNextTile = (callback) => {
402
+ if (scheduling === 'event-loop' ||
403
+ typeof requestAnimationFrame === 'undefined') {
404
+ scheduledTimer = setTimeout(() => {
405
+ scheduledTimer = undefined;
406
+ callback();
407
+ }, 0);
408
+ }
409
+ else {
410
+ // Hidden documents pause requestAnimationFrame. Race it against a
411
+ // timer so animation-frame scheduling still completes in background
412
+ // tabs, minimized windows, and offscreen iframes.
413
+ const run = () => {
414
+ cancelScheduledTile();
415
+ callback();
416
+ };
417
+ scheduledAnimationFrame = requestAnimationFrame(run);
418
+ scheduledTimer = setTimeout(run, ANIMATION_FRAME_FALLBACK_MS);
419
+ }
420
+ };
421
+ const executeTile = async (tileIndex) => {
422
+ if (state !== 'active') {
423
+ return;
424
+ }
425
+ const tile = plan.tiles[tileIndex];
426
+ const outputTileData = isGPUImageData(color)
427
+ ? undefined
428
+ : makeImageData(tile.output.width, tile.output.height);
429
+ const tileStartTime = now();
430
+ if (profileImage)
431
+ this._nativeExecutor.profileNextExecution();
432
+ const resGPUBuffer = await this._executeTile(isGPUImageData(color)
433
+ ? {
434
+ color: color.data,
435
+ albedo: albedo?.data,
436
+ normal: normal?.data
437
+ }
438
+ : rawData, outputTileData, outputImageData, tile, tileIndex === 0, width, height, hdr, denoiseAlpha);
439
+ if (profileImage) {
440
+ const profile = await this._nativeExecutor.getLastExecutionProfile();
441
+ for (const layer of profile?.layers ?? []) {
442
+ layerTimes.set(layer.id, (layerTimes.get(layer.id) ?? 0) + layer.durationMs);
443
+ }
444
+ }
445
+ if (state !== 'active')
446
+ return;
447
+ const output = outputImageData || {
448
+ data: resGPUBuffer,
449
+ width,
450
+ height
451
+ };
452
+ if (progress) {
453
+ await progress(output,
454
+ // Is undefined if using webgpu buffer
455
+ outputTileData, new Tile(tile.output.x, tile.output.y, tile.output.width, tile.output.height), tileIndex, plan.tiles.length);
456
+ }
457
+ if (state !== 'active')
458
+ return;
459
+ const hasNextTile = tileIndex + 1 < plan.tiles.length;
460
+ await this._device.queue.onSubmittedWorkDone();
461
+ if (state !== 'active')
462
+ return;
463
+ const continueAfterGPUWork = async () => {
464
+ tileTimesMs.push(now() - tileStartTime);
465
+ if (state !== 'active')
466
+ return;
467
+ if (hasNextTile) {
468
+ scheduleNextTile(() => {
469
+ if (state !== 'active')
470
+ return;
471
+ void executeTile(tileIndex + 1).catch(settleError);
472
+ });
473
+ }
474
+ else {
475
+ const sortedTileTimes = [...tileTimesMs].sort((a, b) => a - b);
476
+ const middle = Math.floor(sortedTileTimes.length / 2);
477
+ const medianTileTime = sortedTileTimes.length % 2
478
+ ? sortedTileTimes[middle]
479
+ : (sortedTileTimes[middle - 1] + sortedTileTimes[middle]) / 2;
480
+ this._lastExecution = {
481
+ width,
482
+ height,
483
+ tileCount: plan.tiles.length,
484
+ tileColumns: plan.columns,
485
+ tileRows: plan.rows,
486
+ tileOverlap: plan.overlap,
487
+ inputPixelCount: plan.inputPixelCount,
488
+ inputShapeCount: plan.inputShapeCount,
489
+ durationMs: now() - executionStartTime,
490
+ tileTimeMs: {
491
+ min: sortedTileTimes[0],
492
+ median: medianTileTime,
493
+ mean: sortedTileTimes.reduce((sum, value) => sum + value, 0) /
494
+ sortedTileTimes.length,
495
+ max: sortedTileTimes[sortedTileTimes.length - 1]
496
+ }
497
+ };
498
+ // Adapt only from complete executions. Cancelled work is commonly
499
+ // contending with interactive rendering and is not representative.
500
+ if (shouldAdaptTileSize) {
501
+ this._dynamicTileController.observe(tileTimesMs);
502
+ }
503
+ // GPU inference is complete; device loss can no longer affect this
504
+ // execution. Deregister before the user callback resolves so a
505
+ // completed execution never remains retained by the device watcher.
506
+ unregisterDeviceLoss();
507
+ if (profileImage) {
508
+ const layers = [...layerTimes].map(([id, durationMs]) => ({ id, durationMs }));
509
+ this._lastImageProfile = { totalMs: layers.reduce((sum, layer) => sum + layer.durationMs, 0), layers };
510
+ }
511
+ await done(output);
512
+ if (state === 'active') {
513
+ state = 'settled';
514
+ }
515
+ }
516
+ };
517
+ await continueAfterGPUWork();
518
+ };
519
+ unregisterDeviceLoss = this._registerExecutionFailure(settleError);
520
+ void executeTile(0).catch(settleError);
521
+ return () => {
522
+ if (state !== 'active')
523
+ return;
524
+ state = 'aborted';
525
+ cancelScheduledTile();
526
+ unregisterDeviceLoss();
527
+ };
528
+ }
529
+ dispose() {
530
+ this._dataProcessGPU?.dispose();
531
+ this._nativeExecutor?.dispose();
532
+ this._webNNExecutor?.dispose();
533
+ }
534
+ }
535
+ export default UNet;
536
+ //# sourceMappingURL=UNet.js.map