oidn-web 0.4.0 → 0.5.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 (106) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +83 -19
  3. package/dist/oidn.js +3239 -2642
  4. package/dist/oidn.umd.cjs +512 -296
  5. package/lib/UNet.d.ts +54 -20
  6. package/lib/UNet.js +194 -118
  7. package/lib/UNet.js.map +1 -1
  8. package/lib/backend.d.ts +1 -8
  9. package/lib/backend.js +1 -9
  10. package/lib/backend.js.map +1 -1
  11. package/lib/finalRgbShader.d.ts +13 -0
  12. package/lib/finalRgbShader.js +160 -0
  13. package/lib/finalRgbShader.js.map +1 -0
  14. package/lib/graphOptimizer.js +1 -2
  15. package/lib/graphOptimizer.js.map +1 -1
  16. package/lib/hdrTransfer.d.ts +14 -0
  17. package/lib/hdrTransfer.js +61 -0
  18. package/lib/hdrTransfer.js.map +1 -0
  19. package/lib/main.d.ts +16 -7
  20. package/lib/main.js +4 -5
  21. package/lib/main.js.map +1 -1
  22. package/lib/nativeUNet.d.ts +39 -3
  23. package/lib/nativeUNet.js +449 -120
  24. package/lib/nativeUNet.js.map +1 -1
  25. package/lib/process.d.ts +5 -11
  26. package/lib/process.js +35 -49
  27. package/lib/process.js.map +1 -1
  28. package/lib/tileScheduler.d.ts +32 -4
  29. package/lib/tileScheduler.js +133 -20
  30. package/lib/tileScheduler.js.map +1 -1
  31. package/package.json +9 -2
  32. package/src/UNet.ts +287 -158
  33. package/src/backend.ts +1 -14
  34. package/src/finalRgbShader.ts +186 -0
  35. package/src/graphOptimizer.ts +1 -2
  36. package/src/hdrTransfer.ts +88 -0
  37. package/src/main.ts +28 -13
  38. package/src/nativeUNet.ts +515 -116
  39. package/src/process.ts +43 -70
  40. package/src/tileScheduler.ts +216 -24
  41. package/benchmarks/compare.mjs +0 -651
  42. package/benchmarks/leak.mjs +0 -255
  43. package/benchmarks/results/before-spatial.json +0 -391
  44. package/benchmarks/results/before-spatial.md +0 -47
  45. package/benchmarks/results/int8-scan.json +0 -2007
  46. package/benchmarks/results/int8-scan.md +0 -160
  47. package/benchmarks/results/int8-w8a8-scan.json +0 -2007
  48. package/benchmarks/results/int8-w8a8-scan.md +0 -160
  49. package/benchmarks/results/int8-weight-channel.json +0 -1413
  50. package/benchmarks/results/int8-weight-channel.md +0 -118
  51. package/benchmarks/results/int8-weight-only.json +0 -1437
  52. package/benchmarks/results/int8-weight-only.md +0 -118
  53. package/benchmarks/results/kernel-webnn-final.json +0 -1115
  54. package/benchmarks/results/kernel-webnn-final.md +0 -104
  55. package/benchmarks/results/latest-optimized.json +0 -375
  56. package/benchmarks/results/latest-optimized.md +0 -47
  57. package/benchmarks/results/latest.json +0 -391
  58. package/benchmarks/results/latest.md +0 -47
  59. package/benchmarks/results/profile-baseline.json +0 -331
  60. package/benchmarks/results/profile-baseline.md +0 -12
  61. package/benchmarks/results/profile-conv2x.json +0 -331
  62. package/benchmarks/results/profile-conv2x.md +0 -12
  63. package/benchmarks/results/profile-fast-init.json +0 -385
  64. package/benchmarks/results/profile-fast-init.md +0 -47
  65. package/benchmarks/results/profile-fp16-fma.json +0 -369
  66. package/benchmarks/results/profile-fp16-fma.md +0 -47
  67. package/benchmarks/results/profile-fp16-tiled-decoder.json +0 -369
  68. package/benchmarks/results/profile-fp16-tiled-decoder.md +0 -47
  69. package/benchmarks/results/profile-fp16-tiled-encoder.json +0 -369
  70. package/benchmarks/results/profile-fp16-tiled-encoder.md +0 -47
  71. package/benchmarks/results/profile-fp16-unfused-pool.json +0 -385
  72. package/benchmarks/results/profile-fp16-unfused-pool.md +0 -47
  73. package/benchmarks/results/profile-input-major.json +0 -347
  74. package/benchmarks/results/profile-input-major.md +0 -12
  75. package/benchmarks/results/profile-k16.json +0 -347
  76. package/benchmarks/results/profile-k16.md +0 -12
  77. package/benchmarks/results/profile-k4.json +0 -347
  78. package/benchmarks/results/profile-k4.md +0 -12
  79. package/benchmarks/results/profile-pool-reuse.json +0 -331
  80. package/benchmarks/results/profile-pool-reuse.md +0 -12
  81. package/benchmarks/results/profile-precompiled.json +0 -385
  82. package/benchmarks/results/profile-precompiled.md +0 -47
  83. package/benchmarks/results/profile-static-channels.json +0 -385
  84. package/benchmarks/results/profile-static-channels.md +0 -47
  85. package/benchmarks/results/profile-static-io.json +0 -385
  86. package/benchmarks/results/profile-static-io.md +0 -47
  87. package/benchmarks/results/profile-tiled-conv.json +0 -331
  88. package/benchmarks/results/profile-tiled-conv.md +0 -12
  89. package/benchmarks/results/profile-tiled-decoder.json +0 -347
  90. package/benchmarks/results/profile-tiled-decoder.md +0 -12
  91. package/benchmarks/results/profile-tiled-matmul.json +0 -331
  92. package/benchmarks/results/profile-tiled-matmul.md +0 -12
  93. package/benchmarks/results/profile-unfused-decoder.json +0 -379
  94. package/benchmarks/results/profile-unfused-decoder.md +0 -12
  95. package/benchmarks/results/profile-unfused-pool.json +0 -347
  96. package/benchmarks/results/profile-unfused-pool.md +0 -12
  97. package/benchmarks/results/spatial-auto.json +0 -575
  98. package/benchmarks/results/spatial-auto.md +0 -61
  99. package/benchmarks/results/subgroup-smoke.json +0 -1094
  100. package/benchmarks/results/subgroup-smoke.md +0 -104
  101. package/benchmarks/results/webnn-smoke.json +0 -739
  102. package/benchmarks/results/webnn-smoke.md +0 -76
  103. package/scripts/inspect-model.mjs +0 -64
  104. package/tests/modelSpec.test.mjs +0 -128
  105. package/tests/resourceLifecycle.test.mjs +0 -383
  106. package/tests/tileScheduler.test.mjs +0 -90
package/src/process.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import { WGPUComputePass } from './WGPUComputePass';
2
+ import {
3
+ hdrTransferFuncCPU,
4
+ hdrTransferFuncInverseCPU,
5
+ type HDRTransfer
6
+ } from './hdrTransfer';
7
+ export { hdrTransferFuncCPU, hdrTransferFuncInverseCPU } from './hdrTransfer';
8
+ export type { HDRTransfer } from './hdrTransfer';
2
9
 
3
10
  const a = 1.41283765e3;
4
11
  const b = 1.64593172;
@@ -37,6 +44,8 @@ const yMax = 65504;
37
44
  const xMax = PUForward(yMax);
38
45
  const normScale = 1 / xMax;
39
46
  const rcpNormScale = xMax;
47
+ const logXMax = Math.log(yMax + 1);
48
+ const logNormScale = 1 / logXMax;
40
49
 
41
50
  export class Tile {
42
51
  constructor(
@@ -75,53 +84,6 @@ export function avgLogLum({
75
84
  return key / Math.pow(2, averageLuminance);
76
85
  }
77
86
 
78
- export function hdrTransferFuncCPU({
79
- data,
80
- channels,
81
- inputScale
82
- }: {
83
- data: Float32Array;
84
- channels: number;
85
- inputScale: number;
86
- }) {
87
- const newData = new Float32Array(data.length);
88
- newData.set(data);
89
- // https://github.com/RenderKit/oidn/blob/713ec7838ba650f99e0a896549c0dca5eeb3652d/core/color.h#L71
90
- for (let i = 0; i < newData.length; i += channels) {
91
- // First three are color
92
- for (let c = 0; c < 3; c++) {
93
- let y = newData[i + c] * inputScale;
94
- // https://github.com/RenderKit/oidn/blob/713ec7838ba650f99e0a896549c0dca5eeb3652d/devices/cpu/color.ispc#L135
95
- newData[i + c] = PUForward(y) * normScale;
96
- }
97
- }
98
-
99
- return newData;
100
- }
101
-
102
- export function hdrTransferFuncInverseCPU({
103
- data,
104
- channels,
105
- inputScale
106
- }: {
107
- data: Float32Array;
108
- channels: number;
109
- inputScale: number;
110
- }) {
111
- const newData = new Float32Array(data.length);
112
- newData.set(data);
113
-
114
- const outputScale = 1 / inputScale;
115
- for (let i = 0; i < newData.length; i += channels) {
116
- for (let c = 0; c < 3; c++) {
117
- let x = newData[i + c] * rcpNormScale;
118
- newData[i + c] = PUInverse(x) * outputScale;
119
- }
120
- }
121
-
122
- return newData;
123
- }
124
-
125
87
  const constsCode = `
126
88
  const a = ${a};
127
89
  const b = ${b};
@@ -147,7 +109,11 @@ export class GPUDataProcess {
147
109
 
148
110
  private _isInputTexture?: boolean;
149
111
 
150
- constructor(private _device: GPUDevice, private _isHDR: boolean) {
112
+ constructor(
113
+ private _device: GPUDevice,
114
+ private _isHDR: boolean,
115
+ private _hdrTransfer: HDRTransfer = 'pu'
116
+ ) {
151
117
  const commonUniforms = [
152
118
  {
153
119
  label: 'inputScale',
@@ -261,26 +227,40 @@ out_color[outIdx] = textureLoad(in_color, globalId.xy, 0);
261
227
 
262
228
  this._isInputTexture = isInputTexture;
263
229
  const isHDR = this._isHDR;
230
+ const hdrForwardCode = this._hdrTransfer === 'log'
231
+ ? `fn HDRForward(y: f32) -> f32 { return log(y + 1.0) * logNormScale; }`
232
+ : `fn HDRForward(y: f32) -> f32 {
233
+ if (y <= y0) { return a * y * normScale; }
234
+ else if (y <= y1) { return (b * pow(y, c) + d) * normScale; }
235
+ else { return (e * log(y + f) + g) * normScale; }
236
+ }`;
237
+ const hdrInverseCode = this._hdrTransfer === 'log'
238
+ ? `fn HDRInverse(x: f32) -> f32 { return exp(x * logXMax) - 1.0; }`
239
+ : `fn HDRInverse(x: f32) -> f32 {
240
+ let y = x * rcpNormScale;
241
+ if (y <= x0) { return y / a; }
242
+ else if (y <= x1) { return pow((y - d) / b, 1 / c); }
243
+ else { return exp((y - g) / e) - f; }
244
+ }`;
264
245
  const commonCSDefine = /* wgsl */ `
265
246
  ${constsCode}
266
- fn PUForward(y: f32) -> f32 {
267
- if (y <= y0) {
268
- return a * y;
269
- } else if (y <= y1) {
270
- return b * pow(y, c) + d;
271
- } else {
272
- return e * log(y + f) + g;
273
- }
274
- }`;
247
+ const logXMax = ${logXMax};
248
+ const logNormScale = ${logNormScale};
249
+ ${hdrForwardCode}`;
275
250
  function readInputCode(inputName: string) {
276
251
  return isInputTexture
277
- ? `textureLoad(in_${inputName}, globalId.xy + vec2u(inputOffset), 0)`
252
+ ? `textureLoad(in_${inputName}, vec2u(inputPosition), 0)`
278
253
  : `in_${inputName}[inIdx]`;
279
254
  }
280
255
  const commonCSMain = /* wgsl */ `
281
256
  let x = i32(globalId.x);
282
257
  let y = i32(globalId.y);
283
- let inIdx = (y + inputOffset.y) * inputSize.x + (x + inputOffset.x);
258
+ let inputPosition = clamp(
259
+ vec2i(x, y) + inputOffset,
260
+ vec2i(0),
261
+ inputSize - vec2i(1)
262
+ );
263
+ let inIdx = inputPosition.y * inputSize.x + inputPosition.x;
284
264
  let col = ${readInputCode('color')};
285
265
 
286
266
  let outIdx = y * outputSize.x + x;
@@ -290,7 +270,7 @@ if (${denoiseAlpha}) {
290
270
  out_color[outIdx] = vec3f(1.0 - col.a);
291
271
  }
292
272
  else if (${isHDR}) {
293
- out_color[outIdx] = vec3f(PUForward(col.r * inputScale), PUForward(col.g * inputScale), PUForward(col.b * inputScale)) * normScale;
273
+ out_color[outIdx] = vec3f(HDRForward(col.r * inputScale), HDRForward(col.g * inputScale), HDRForward(col.b * inputScale));
294
274
  }
295
275
  else {
296
276
  out_color[outIdx] = col.rgb;
@@ -316,15 +296,8 @@ ${commonCSMain}
316
296
  this._outputPass.setCSCode({
317
297
  csDefine: /* wgsl */ `
318
298
  ${constsCode}
319
- fn PUInverse(y: f32) -> f32 {
320
- if (y <= x0) {
321
- return y / a;
322
- } else if (y <= x1) {
323
- return pow((y - d) / b, 1 / c);
324
- } else {
325
- return exp((y - g) / e) - f;
326
- }
327
- }
299
+ const logXMax = ${logXMax};
300
+ ${hdrInverseCode}
328
301
  `,
329
302
  csMain: /* wgsl */ `
330
303
  let x = i32(globalId.x);
@@ -346,7 +319,7 @@ if (${denoiseAlpha}) {
346
319
  }
347
320
  else if (${isHDR}) {
348
321
  out_color[outIdx] = vec4f(
349
- vec3f(PUInverse(col.r * rcpNormScale), PUInverse(col.g * rcpNormScale), PUInverse(col.b * rcpNormScale)) / inputScale,
322
+ vec3f(HDRInverse(col.r), HDRInverse(col.g), HDRInverse(col.b)) / inputScale,
350
323
  // Pick the alpha
351
324
  raw.a
352
325
  );
@@ -1,22 +1,47 @@
1
1
  export interface DynamicTileOptions {
2
2
  /** Smallest output tile edge. Defaults to 256. */
3
3
  minTileSize?: number;
4
- /** Initial output tile edge. Defaults to 384. */
4
+ /** Initial maximum output tile edge. Defaults to 432. */
5
5
  initialTileSize?: number;
6
6
  /** Desired upper bound for one tile's GPU latency. Defaults to 16 ms. */
7
7
  targetTileTimeMs?: number;
8
- /** Amount added or removed after each completed execution. Defaults to 128. */
8
+ /** Amount added or removed after each completed execution. Defaults to 16. */
9
9
  adjustmentStep?: number;
10
10
  }
11
11
 
12
12
  export type DynamicTileSetting = boolean | DynamicTileOptions;
13
13
 
14
14
  const defaultMinTileSize = 256;
15
- const defaultInitialTileSize = 384;
15
+ const defaultInitialTileSize = 432;
16
16
  const defaultTargetTileTimeMs = 16;
17
- const defaultAdjustmentStep = 128;
17
+ const defaultAdjustmentStep = 16;
18
18
  export const OIDN_TILE_ALIGNMENT = 16;
19
19
 
20
+ export interface TileRect {
21
+ x: number;
22
+ y: number;
23
+ width: number;
24
+ height: number;
25
+ }
26
+
27
+ export interface PlannedTile {
28
+ column: number;
29
+ row: number;
30
+ input: TileRect;
31
+ output: TileRect;
32
+ }
33
+
34
+ export interface TilePlan {
35
+ columns: number;
36
+ rows: number;
37
+ overlap: number;
38
+ maxOutputWidth: number;
39
+ maxOutputHeight: number;
40
+ inputPixelCount: number;
41
+ inputShapeCount: number;
42
+ tiles: PlannedTile[];
43
+ }
44
+
20
45
  function alignUp(value: number, alignment: number) {
21
46
  return Math.ceil(value / alignment) * alignment;
22
47
  }
@@ -25,16 +50,180 @@ function alignDown(value: number, alignment: number) {
25
50
  return Math.floor(value / alignment) * alignment;
26
51
  }
27
52
 
53
+ function partitionRange(length: number, parts: number, index: number) {
54
+ const start = Math.round(index * length / parts);
55
+ const end = Math.round((index + 1) * length / parts);
56
+ return { start, end };
57
+ }
58
+
59
+ function alignRangeWithinImage(
60
+ start: number,
61
+ end: number,
62
+ imageSize: number,
63
+ minimumSize = 0
64
+ ) {
65
+ const currentSize = end - start;
66
+ const alignedSize = Math.max(
67
+ minimumSize,
68
+ alignUp(currentSize, OIDN_TILE_ALIGNMENT)
69
+ );
70
+ let alignedStart = start - Math.floor((alignedSize - currentSize) / 2);
71
+ // At an image edge, keep the padded portion on the right/bottom. Input
72
+ // readers replicate the nearest image edge into that portion, while output
73
+ // tiles remain strictly inside the original image.
74
+ alignedStart = clamp(alignedStart, 0, Math.max(0, imageSize - alignedSize));
75
+ return { start: alignedStart, end: alignedStart + alignedSize };
76
+ }
77
+
78
+ /**
79
+ * Splits an image into balanced rectangular output regions. Only sides that
80
+ * touch another tile receive a halo; image edges rely on the model's normal
81
+ * boundary behavior instead of doing redundant off-edge work.
82
+ */
83
+ export function planTileGrid(
84
+ width: number,
85
+ height: number,
86
+ maxTileSize: number,
87
+ overlap: number
88
+ ): TilePlan {
89
+ if (!Number.isInteger(width) || width <= 0 ||
90
+ !Number.isInteger(height) || height <= 0) {
91
+ throw new Error('Tile grid dimensions must be positive integers');
92
+ }
93
+ if (!Number.isFinite(maxTileSize) || maxTileSize <= 0) {
94
+ throw new Error('Maximum tile size must be positive');
95
+ }
96
+ if (!Number.isFinite(overlap) || overlap < 0) {
97
+ throw new Error('Tile overlap must be non-negative');
98
+ }
99
+
100
+ const alignedMaximum = Math.max(
101
+ OIDN_TILE_ALIGNMENT,
102
+ alignDown(maxTileSize, OIDN_TILE_ALIGNMENT)
103
+ );
104
+ const alignedOverlap = alignUp(overlap, OIDN_TILE_ALIGNMENT);
105
+ const columns = Math.max(1, Math.ceil(width / alignedMaximum));
106
+ const rows = Math.max(1, Math.ceil(height / alignedMaximum));
107
+ const tiles: PlannedTile[] = [];
108
+ let maxOutputWidth = 0;
109
+ let maxOutputHeight = 0;
110
+
111
+ for (let row = 0; row < rows; row++) {
112
+ const outputY = partitionRange(height, rows, row);
113
+ for (let column = 0; column < columns; column++) {
114
+ const outputX = partitionRange(width, columns, column);
115
+ const desiredInputX = {
116
+ start: column === 0
117
+ ? outputX.start
118
+ : Math.max(0, outputX.start - alignedOverlap),
119
+ end: column === columns - 1
120
+ ? outputX.end
121
+ : Math.min(width, outputX.end + alignedOverlap)
122
+ };
123
+ const desiredInputY = {
124
+ start: row === 0
125
+ ? outputY.start
126
+ : Math.max(0, outputY.start - alignedOverlap),
127
+ end: row === rows - 1
128
+ ? outputY.end
129
+ : Math.min(height, outputY.end + alignedOverlap)
130
+ };
131
+ const inputX = alignRangeWithinImage(
132
+ desiredInputX.start,
133
+ desiredInputX.end,
134
+ width
135
+ );
136
+ const inputY = alignRangeWithinImage(
137
+ desiredInputY.start,
138
+ desiredInputY.end,
139
+ height
140
+ );
141
+ const output: TileRect = {
142
+ x: outputX.start,
143
+ y: outputY.start,
144
+ width: outputX.end - outputX.start,
145
+ height: outputY.end - outputY.start
146
+ };
147
+ const input: TileRect = {
148
+ x: inputX.start,
149
+ y: inputY.start,
150
+ width: inputX.end - inputX.start,
151
+ height: inputY.end - inputY.start
152
+ };
153
+ maxOutputWidth = Math.max(maxOutputWidth, output.width);
154
+ maxOutputHeight = Math.max(maxOutputHeight, output.height);
155
+ tiles.push({ column, row, input, output });
156
+ }
157
+ }
158
+
159
+ const inputShapes = () => new Set(
160
+ tiles.map(({ input }) => `${input.width}x${input.height}`)
161
+ );
162
+ if (inputShapes().size > 2) {
163
+ const maximumInputWidth = Math.max(
164
+ ...tiles.map(({ input }) => input.width)
165
+ );
166
+ const maximumInputHeight = Math.max(
167
+ ...tiles.map(({ input }) => input.height)
168
+ );
169
+ const uniformWidthPixelCount = tiles.reduce(
170
+ (sum, { input }) => sum + maximumInputWidth * input.height,
171
+ 0
172
+ );
173
+ const uniformHeightPixelCount = tiles.reduce(
174
+ (sum, { input }) => sum + input.width * maximumInputHeight,
175
+ 0
176
+ );
177
+ if (uniformWidthPixelCount <= uniformHeightPixelCount) {
178
+ for (const tile of tiles) {
179
+ const range = alignRangeWithinImage(
180
+ tile.input.x,
181
+ tile.input.x + tile.input.width,
182
+ width,
183
+ maximumInputWidth
184
+ );
185
+ tile.input.x = range.start;
186
+ tile.input.width = range.end - range.start;
187
+ }
188
+ } else {
189
+ for (const tile of tiles) {
190
+ const range = alignRangeWithinImage(
191
+ tile.input.y,
192
+ tile.input.y + tile.input.height,
193
+ height,
194
+ maximumInputHeight
195
+ );
196
+ tile.input.y = range.start;
197
+ tile.input.height = range.end - range.start;
198
+ }
199
+ }
200
+ }
201
+
202
+ const finalInputShapes = inputShapes();
203
+ const inputPixelCount = tiles.reduce(
204
+ (sum, { input }) => sum + input.width * input.height,
205
+ 0
206
+ );
207
+
208
+ return {
209
+ columns,
210
+ rows,
211
+ overlap: alignedOverlap,
212
+ maxOutputWidth,
213
+ maxOutputHeight,
214
+ inputPixelCount,
215
+ inputShapeCount: finalInputShapes.size,
216
+ tiles
217
+ };
218
+ }
219
+
28
220
  function clamp(value: number, min: number, max: number) {
29
221
  return Math.min(Math.max(value, min), max);
30
222
  }
31
223
 
32
- function median(values: number[]) {
224
+ function percentile(values: number[], fraction: number) {
33
225
  const sorted = [...values].sort((a, b) => a - b);
34
- const middle = Math.floor(sorted.length / 2);
35
- return sorted.length % 2
36
- ? sorted[middle]
37
- : (sorted[middle - 1] + sorted[middle]) / 2;
226
+ return sorted[Math.ceil(sorted.length * fraction) - 1];
38
227
  }
39
228
 
40
229
  /** Fits one output dimension without exceeding the configured tile cap. */
@@ -57,6 +246,8 @@ export class DynamicTileController {
57
246
 
58
247
  private _tileSize: number;
59
248
  private _adjustmentStep: number;
249
+ private _smoothedTileTimeMs?: number;
250
+ private _completeExecutionsSinceChange = 0;
60
251
 
61
252
  constructor(maxTileSize: number, setting: DynamicTileSetting = true) {
62
253
  const options = typeof setting === 'object' ? setting : {};
@@ -106,11 +297,22 @@ export class DynamicTileController {
106
297
  );
107
298
  if (validTimes.length === 0) return false;
108
299
 
109
- const observedTime = median(validTimes);
300
+ // The first tile may still pay one-time GPU residency costs even after
301
+ // shape preparation. It should not make a stable layout look too slow.
302
+ const representativeTimes = validTimes.length >= 3
303
+ ? validTimes.slice(1)
304
+ : validTimes;
305
+ const observedTime = percentile(representativeTimes, 0.75);
306
+ this._smoothedTileTimeMs = this._smoothedTileTimeMs === undefined
307
+ ? observedTime
308
+ : this._smoothedTileTimeMs * 0.65 + observedTime * 0.35;
309
+ this._completeExecutionsSinceChange++;
310
+ if (this._completeExecutionsSinceChange < 2) return false;
311
+
110
312
  let nextTileSize = this._tileSize;
111
- if (observedTime > this.targetTileTimeMs * 1.25) {
313
+ if (this._smoothedTileTimeMs > this.targetTileTimeMs * 1.25) {
112
314
  nextTileSize -= this._adjustmentStep;
113
- } else if (observedTime < this.targetTileTimeMs * 0.65) {
315
+ } else if (this._smoothedTileTimeMs < this.targetTileTimeMs * 0.65) {
114
316
  nextTileSize += this._adjustmentStep;
115
317
  }
116
318
 
@@ -121,18 +323,8 @@ export class DynamicTileController {
121
323
  );
122
324
  if (nextTileSize === this._tileSize) return false;
123
325
  this._tileSize = nextTileSize;
326
+ this._smoothedTileTimeMs = undefined;
327
+ this._completeExecutionsSinceChange = 0;
124
328
  return true;
125
329
  }
126
330
  }
127
-
128
- /** Waits for all work submitted before this call and tolerates device loss. */
129
- export async function waitForSubmittedGPUWork(
130
- queue: Pick<GPUQueue, 'onSubmittedWorkDone'>
131
- ) {
132
- try {
133
- await queue.onSubmittedWorkDone();
134
- } catch {
135
- // tileExecute has no error callback. Preserve its completion/cancellation
136
- // behavior and let the owning GPUDevice report device loss separately.
137
- }
138
- }