oidn-web 0.3.5 → 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.
- package/CHANGELOG.md +94 -0
- package/README.md +208 -8
- package/dist/oidn.js +4699 -22516
- package/dist/oidn.umd.cjs +989 -5796
- package/lib/UNet.d.ts +111 -26
- package/lib/UNet.js +310 -329
- package/lib/UNet.js.map +1 -1
- package/lib/WGPUComputePass.d.ts +1 -1
- package/lib/WGPUComputePass.js +6 -4
- package/lib/WGPUComputePass.js.map +1 -1
- package/lib/backend.d.ts +1 -4
- package/lib/backend.js +28 -44
- package/lib/backend.js.map +1 -1
- package/lib/finalRgbShader.d.ts +13 -0
- package/lib/finalRgbShader.js +160 -0
- package/lib/finalRgbShader.js.map +1 -0
- package/lib/graphOptimizer.d.ts +54 -0
- package/lib/graphOptimizer.js +215 -0
- package/lib/graphOptimizer.js.map +1 -0
- package/lib/hdrTransfer.d.ts +14 -0
- package/lib/hdrTransfer.js +61 -0
- package/lib/hdrTransfer.js.map +1 -0
- package/lib/main.d.ts +43 -11
- package/lib/main.js +9 -5
- package/lib/main.js.map +1 -1
- package/lib/modelSpec.d.ts +80 -0
- package/lib/modelSpec.js +270 -0
- package/lib/modelSpec.js.map +1 -0
- package/lib/nativeUNet.d.ts +103 -0
- package/lib/nativeUNet.js +2064 -0
- package/lib/nativeUNet.js.map +1 -0
- package/lib/process.d.ts +5 -11
- package/lib/process.js +38 -49
- package/lib/process.js.map +1 -1
- package/lib/resourceTracker.d.ts +26 -0
- package/lib/resourceTracker.js +65 -0
- package/lib/resourceTracker.js.map +1 -0
- package/lib/tileScheduler.d.ts +61 -0
- package/lib/tileScheduler.js +199 -0
- package/lib/tileScheduler.js.map +1 -0
- package/lib/webnnUNet.d.ts +52 -0
- package/lib/webnnUNet.js +535 -0
- package/lib/webnnUNet.js.map +1 -0
- package/package.json +16 -5
- package/src/UNet.ts +463 -437
- package/src/WGPUComputePass.ts +6 -4
- package/src/backend.ts +33 -59
- package/src/finalRgbShader.ts +186 -0
- package/src/graphOptimizer.ts +300 -0
- package/src/hdrTransfer.ts +88 -0
- package/src/main.ts +95 -20
- package/src/modelSpec.ts +414 -0
- package/src/nativeUNet.ts +2655 -0
- package/src/process.ts +46 -71
- package/src/resourceTracker.ts +94 -0
- package/src/tileScheduler.ts +330 -0
- package/src/webnnUNet.ts +812 -0
- package/lib/helper.d.ts +0 -4
- package/lib/helper.js +0 -33
- package/lib/helper.js.map +0 -1
- package/lib/kernels.d.ts +0 -1
- package/lib/kernels.js +0 -26
- package/lib/kernels.js.map +0 -1
- package/src/helper.ts +0 -43
- package/src/kernels.ts +0 -31
package/src/process.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { buffer, data } from '@tensorflow/tfjs';
|
|
2
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';
|
|
3
9
|
|
|
4
10
|
const a = 1.41283765e3;
|
|
5
11
|
const b = 1.64593172;
|
|
@@ -38,6 +44,8 @@ const yMax = 65504;
|
|
|
38
44
|
const xMax = PUForward(yMax);
|
|
39
45
|
const normScale = 1 / xMax;
|
|
40
46
|
const rcpNormScale = xMax;
|
|
47
|
+
const logXMax = Math.log(yMax + 1);
|
|
48
|
+
const logNormScale = 1 / logXMax;
|
|
41
49
|
|
|
42
50
|
export class Tile {
|
|
43
51
|
constructor(
|
|
@@ -76,53 +84,6 @@ export function avgLogLum({
|
|
|
76
84
|
return key / Math.pow(2, averageLuminance);
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
export function hdrTransferFuncCPU({
|
|
80
|
-
data,
|
|
81
|
-
channels,
|
|
82
|
-
inputScale
|
|
83
|
-
}: {
|
|
84
|
-
data: Float32Array;
|
|
85
|
-
channels: number;
|
|
86
|
-
inputScale: number;
|
|
87
|
-
}) {
|
|
88
|
-
const newData = new Float32Array(data.length);
|
|
89
|
-
newData.set(data);
|
|
90
|
-
// https://github.com/RenderKit/oidn/blob/713ec7838ba650f99e0a896549c0dca5eeb3652d/core/color.h#L71
|
|
91
|
-
for (let i = 0; i < newData.length; i += channels) {
|
|
92
|
-
// First three are color
|
|
93
|
-
for (let c = 0; c < 3; c++) {
|
|
94
|
-
let y = newData[i + c] * inputScale;
|
|
95
|
-
// https://github.com/RenderKit/oidn/blob/713ec7838ba650f99e0a896549c0dca5eeb3652d/devices/cpu/color.ispc#L135
|
|
96
|
-
newData[i + c] = PUForward(y) * normScale;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return newData;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function hdrTransferFuncInverseCPU({
|
|
104
|
-
data,
|
|
105
|
-
channels,
|
|
106
|
-
inputScale
|
|
107
|
-
}: {
|
|
108
|
-
data: Float32Array;
|
|
109
|
-
channels: number;
|
|
110
|
-
inputScale: number;
|
|
111
|
-
}) {
|
|
112
|
-
const newData = new Float32Array(data.length);
|
|
113
|
-
newData.set(data);
|
|
114
|
-
|
|
115
|
-
const outputScale = 1 / inputScale;
|
|
116
|
-
for (let i = 0; i < newData.length; i += channels) {
|
|
117
|
-
for (let c = 0; c < 3; c++) {
|
|
118
|
-
let x = newData[i + c] * rcpNormScale;
|
|
119
|
-
newData[i + c] = PUInverse(x) * outputScale;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return newData;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
87
|
const constsCode = `
|
|
127
88
|
const a = ${a};
|
|
128
89
|
const b = ${b};
|
|
@@ -148,7 +109,11 @@ export class GPUDataProcess {
|
|
|
148
109
|
|
|
149
110
|
private _isInputTexture?: boolean;
|
|
150
111
|
|
|
151
|
-
constructor(
|
|
112
|
+
constructor(
|
|
113
|
+
private _device: GPUDevice,
|
|
114
|
+
private _isHDR: boolean,
|
|
115
|
+
private _hdrTransfer: HDRTransfer = 'pu'
|
|
116
|
+
) {
|
|
152
117
|
const commonUniforms = [
|
|
153
118
|
{
|
|
154
119
|
label: 'inputScale',
|
|
@@ -262,26 +227,40 @@ out_color[outIdx] = textureLoad(in_color, globalId.xy, 0);
|
|
|
262
227
|
|
|
263
228
|
this._isInputTexture = isInputTexture;
|
|
264
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
|
+
}`;
|
|
265
245
|
const commonCSDefine = /* wgsl */ `
|
|
266
246
|
${constsCode}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
} else if (y <= y1) {
|
|
271
|
-
return b * pow(y, c) + d;
|
|
272
|
-
} else {
|
|
273
|
-
return e * log(y + f) + g;
|
|
274
|
-
}
|
|
275
|
-
}`;
|
|
247
|
+
const logXMax = ${logXMax};
|
|
248
|
+
const logNormScale = ${logNormScale};
|
|
249
|
+
${hdrForwardCode}`;
|
|
276
250
|
function readInputCode(inputName: string) {
|
|
277
251
|
return isInputTexture
|
|
278
|
-
? `textureLoad(in_${inputName},
|
|
252
|
+
? `textureLoad(in_${inputName}, vec2u(inputPosition), 0)`
|
|
279
253
|
: `in_${inputName}[inIdx]`;
|
|
280
254
|
}
|
|
281
255
|
const commonCSMain = /* wgsl */ `
|
|
282
256
|
let x = i32(globalId.x);
|
|
283
257
|
let y = i32(globalId.y);
|
|
284
|
-
let
|
|
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;
|
|
285
264
|
let col = ${readInputCode('color')};
|
|
286
265
|
|
|
287
266
|
let outIdx = y * outputSize.x + x;
|
|
@@ -291,7 +270,7 @@ if (${denoiseAlpha}) {
|
|
|
291
270
|
out_color[outIdx] = vec3f(1.0 - col.a);
|
|
292
271
|
}
|
|
293
272
|
else if (${isHDR}) {
|
|
294
|
-
out_color[outIdx] = vec3f(
|
|
273
|
+
out_color[outIdx] = vec3f(HDRForward(col.r * inputScale), HDRForward(col.g * inputScale), HDRForward(col.b * inputScale));
|
|
295
274
|
}
|
|
296
275
|
else {
|
|
297
276
|
out_color[outIdx] = col.rgb;
|
|
@@ -317,15 +296,8 @@ ${commonCSMain}
|
|
|
317
296
|
this._outputPass.setCSCode({
|
|
318
297
|
csDefine: /* wgsl */ `
|
|
319
298
|
${constsCode}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return y / a;
|
|
323
|
-
} else if (y <= x1) {
|
|
324
|
-
return pow((y - d) / b, 1 / c);
|
|
325
|
-
} else {
|
|
326
|
-
return exp((y - g) / e) - f;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
299
|
+
const logXMax = ${logXMax};
|
|
300
|
+
${hdrInverseCode}
|
|
329
301
|
`,
|
|
330
302
|
csMain: /* wgsl */ `
|
|
331
303
|
let x = i32(globalId.x);
|
|
@@ -347,7 +319,7 @@ if (${denoiseAlpha}) {
|
|
|
347
319
|
}
|
|
348
320
|
else if (${isHDR}) {
|
|
349
321
|
out_color[outIdx] = vec4f(
|
|
350
|
-
vec3f(
|
|
322
|
+
vec3f(HDRInverse(col.r), HDRInverse(col.g), HDRInverse(col.b)) / inputScale,
|
|
351
323
|
// Pick the alpha
|
|
352
324
|
raw.a
|
|
353
325
|
);
|
|
@@ -490,5 +462,8 @@ else {
|
|
|
490
462
|
dispose() {
|
|
491
463
|
this._outputPass.dispose();
|
|
492
464
|
this._inputPassAux.dispose();
|
|
465
|
+
this._inputPassColor.dispose();
|
|
466
|
+
// copyPass borrows outputPass's target and must not destroy it again.
|
|
467
|
+
this._copyPass.dispose(false);
|
|
493
468
|
}
|
|
494
469
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type OIDNResourceKind =
|
|
2
|
+
| 'gpu-buffer'
|
|
3
|
+
| 'gpu-query-set'
|
|
4
|
+
| 'ml-context'
|
|
5
|
+
| 'ml-graph'
|
|
6
|
+
| 'ml-tensor';
|
|
7
|
+
|
|
8
|
+
export interface OIDNResourceStats {
|
|
9
|
+
created: number;
|
|
10
|
+
destroyed: number;
|
|
11
|
+
live: number;
|
|
12
|
+
peakLive: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface OIDNResourceSnapshot {
|
|
16
|
+
live: number;
|
|
17
|
+
created: number;
|
|
18
|
+
destroyed: number;
|
|
19
|
+
peakLive: number;
|
|
20
|
+
pending: number;
|
|
21
|
+
byKind: Partial<Record<OIDNResourceKind, OIDNResourceStats>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MutableResourceStats extends OIDNResourceStats {
|
|
25
|
+
resources: Set<object>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Counts explicitly owned browser resources. Keeping the ownership registry in
|
|
30
|
+
* production makes cache eviction and failure cleanup testable without relying
|
|
31
|
+
* on JS heap measurements, which do not include WebGPU allocations.
|
|
32
|
+
*/
|
|
33
|
+
export class OIDNResourceTracker {
|
|
34
|
+
private _stats = new Map<OIDNResourceKind, MutableResourceStats>();
|
|
35
|
+
|
|
36
|
+
track<T extends object>(kind: OIDNResourceKind, resource: T): T {
|
|
37
|
+
let stats = this._stats.get(kind);
|
|
38
|
+
if (!stats) {
|
|
39
|
+
stats = {
|
|
40
|
+
created: 0,
|
|
41
|
+
destroyed: 0,
|
|
42
|
+
live: 0,
|
|
43
|
+
peakLive: 0,
|
|
44
|
+
resources: new Set()
|
|
45
|
+
};
|
|
46
|
+
this._stats.set(kind, stats);
|
|
47
|
+
}
|
|
48
|
+
if (stats.resources.has(resource)) return resource;
|
|
49
|
+
stats.resources.add(resource);
|
|
50
|
+
stats.created++;
|
|
51
|
+
stats.live++;
|
|
52
|
+
stats.peakLive = Math.max(stats.peakLive, stats.live);
|
|
53
|
+
return resource;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
release(
|
|
57
|
+
kind: OIDNResourceKind,
|
|
58
|
+
resource: object | undefined,
|
|
59
|
+
destroy: () => void
|
|
60
|
+
) {
|
|
61
|
+
if (!resource) return false;
|
|
62
|
+
const stats = this._stats.get(kind);
|
|
63
|
+
if (!stats?.resources.delete(resource)) return false;
|
|
64
|
+
try {
|
|
65
|
+
destroy();
|
|
66
|
+
} finally {
|
|
67
|
+
stats.destroyed++;
|
|
68
|
+
stats.live--;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
snapshot(pending = 0): OIDNResourceSnapshot {
|
|
74
|
+
let live = 0;
|
|
75
|
+
let created = 0;
|
|
76
|
+
let destroyed = 0;
|
|
77
|
+
let peakLive = 0;
|
|
78
|
+
const byKind: OIDNResourceSnapshot['byKind'] = {};
|
|
79
|
+
for (const [kind, stats] of this._stats) {
|
|
80
|
+
const entry = {
|
|
81
|
+
created: stats.created,
|
|
82
|
+
destroyed: stats.destroyed,
|
|
83
|
+
live: stats.live,
|
|
84
|
+
peakLive: stats.peakLive
|
|
85
|
+
};
|
|
86
|
+
byKind[kind] = entry;
|
|
87
|
+
live += entry.live;
|
|
88
|
+
created += entry.created;
|
|
89
|
+
destroyed += entry.destroyed;
|
|
90
|
+
peakLive += entry.peakLive;
|
|
91
|
+
}
|
|
92
|
+
return { live, created, destroyed, peakLive, pending, byKind };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export interface DynamicTileOptions {
|
|
2
|
+
/** Smallest output tile edge. Defaults to 256. */
|
|
3
|
+
minTileSize?: number;
|
|
4
|
+
/** Initial maximum output tile edge. Defaults to 432. */
|
|
5
|
+
initialTileSize?: number;
|
|
6
|
+
/** Desired upper bound for one tile's GPU latency. Defaults to 16 ms. */
|
|
7
|
+
targetTileTimeMs?: number;
|
|
8
|
+
/** Amount added or removed after each completed execution. Defaults to 16. */
|
|
9
|
+
adjustmentStep?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type DynamicTileSetting = boolean | DynamicTileOptions;
|
|
13
|
+
|
|
14
|
+
const defaultMinTileSize = 256;
|
|
15
|
+
const defaultInitialTileSize = 432;
|
|
16
|
+
const defaultTargetTileTimeMs = 16;
|
|
17
|
+
const defaultAdjustmentStep = 16;
|
|
18
|
+
export const OIDN_TILE_ALIGNMENT = 16;
|
|
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
|
+
|
|
45
|
+
function alignUp(value: number, alignment: number) {
|
|
46
|
+
return Math.ceil(value / alignment) * alignment;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function alignDown(value: number, alignment: number) {
|
|
50
|
+
return Math.floor(value / alignment) * alignment;
|
|
51
|
+
}
|
|
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
|
+
|
|
220
|
+
function clamp(value: number, min: number, max: number) {
|
|
221
|
+
return Math.min(Math.max(value, min), max);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function percentile(values: number[], fraction: number) {
|
|
225
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
226
|
+
return sorted[Math.ceil(sorted.length * fraction) - 1];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Fits one output dimension without exceeding the configured tile cap. */
|
|
230
|
+
export function fitTileDimension(imageSize: number, maxTileSize: number) {
|
|
231
|
+
return imageSize <= maxTileSize
|
|
232
|
+
? Math.min(alignUp(imageSize, OIDN_TILE_ALIGNMENT), maxTileSize)
|
|
233
|
+
: maxTileSize;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Chooses an output tile size from completed GPU timings. A size change is
|
|
238
|
+
* applied to the next execution so one in-flight tiled pass keeps a stable
|
|
239
|
+
* overlap, allocation plan, and pipeline set.
|
|
240
|
+
*/
|
|
241
|
+
export class DynamicTileController {
|
|
242
|
+
readonly enabled: boolean;
|
|
243
|
+
readonly maxTileSize: number;
|
|
244
|
+
readonly minTileSize: number;
|
|
245
|
+
readonly targetTileTimeMs: number;
|
|
246
|
+
|
|
247
|
+
private _tileSize: number;
|
|
248
|
+
private _adjustmentStep: number;
|
|
249
|
+
private _smoothedTileTimeMs?: number;
|
|
250
|
+
private _completeExecutionsSinceChange = 0;
|
|
251
|
+
|
|
252
|
+
constructor(maxTileSize: number, setting: DynamicTileSetting = true) {
|
|
253
|
+
const options = typeof setting === 'object' ? setting : {};
|
|
254
|
+
this.enabled = setting !== false;
|
|
255
|
+
this.maxTileSize = Math.max(
|
|
256
|
+
OIDN_TILE_ALIGNMENT,
|
|
257
|
+
alignDown(maxTileSize, OIDN_TILE_ALIGNMENT)
|
|
258
|
+
);
|
|
259
|
+
this.minTileSize = clamp(
|
|
260
|
+
alignUp(options.minTileSize ?? defaultMinTileSize, OIDN_TILE_ALIGNMENT),
|
|
261
|
+
OIDN_TILE_ALIGNMENT,
|
|
262
|
+
this.maxTileSize
|
|
263
|
+
);
|
|
264
|
+
this.targetTileTimeMs = Math.max(
|
|
265
|
+
1,
|
|
266
|
+
options.targetTileTimeMs ?? defaultTargetTileTimeMs
|
|
267
|
+
);
|
|
268
|
+
this._adjustmentStep = Math.max(
|
|
269
|
+
OIDN_TILE_ALIGNMENT,
|
|
270
|
+
alignUp(
|
|
271
|
+
options.adjustmentStep ?? defaultAdjustmentStep,
|
|
272
|
+
OIDN_TILE_ALIGNMENT
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
this._tileSize = this.enabled
|
|
276
|
+
? clamp(
|
|
277
|
+
alignUp(
|
|
278
|
+
options.initialTileSize ?? defaultInitialTileSize,
|
|
279
|
+
OIDN_TILE_ALIGNMENT
|
|
280
|
+
),
|
|
281
|
+
this.minTileSize,
|
|
282
|
+
this.maxTileSize
|
|
283
|
+
)
|
|
284
|
+
: this.maxTileSize;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
get tileSize() {
|
|
288
|
+
return this._tileSize;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Returns true when the next execution should use a different tile size. */
|
|
292
|
+
observe(tileTimesMs: number[]) {
|
|
293
|
+
if (!this.enabled || tileTimesMs.length === 0) return false;
|
|
294
|
+
|
|
295
|
+
const validTimes = tileTimesMs.filter(
|
|
296
|
+
(duration) => Number.isFinite(duration) && duration >= 0
|
|
297
|
+
);
|
|
298
|
+
if (validTimes.length === 0) return false;
|
|
299
|
+
|
|
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
|
+
|
|
312
|
+
let nextTileSize = this._tileSize;
|
|
313
|
+
if (this._smoothedTileTimeMs > this.targetTileTimeMs * 1.25) {
|
|
314
|
+
nextTileSize -= this._adjustmentStep;
|
|
315
|
+
} else if (this._smoothedTileTimeMs < this.targetTileTimeMs * 0.65) {
|
|
316
|
+
nextTileSize += this._adjustmentStep;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
nextTileSize = clamp(
|
|
320
|
+
alignUp(nextTileSize, OIDN_TILE_ALIGNMENT),
|
|
321
|
+
this.minTileSize,
|
|
322
|
+
this.maxTileSize
|
|
323
|
+
);
|
|
324
|
+
if (nextTileSize === this._tileSize) return false;
|
|
325
|
+
this._tileSize = nextTileSize;
|
|
326
|
+
this._smoothedTileTimeMs = undefined;
|
|
327
|
+
this._completeExecutionsSinceChange = 0;
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|