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.
- package/CHANGELOG.md +182 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/oidn.js +6140 -0
- package/dist/oidn.umd.cjs +1513 -0
- package/lib/UNet.d.ts +161 -0
- package/lib/UNet.js +536 -0
- package/lib/UNet.js.map +1 -0
- package/lib/WGPUComputePass.d.ts +62 -0
- package/lib/WGPUComputePass.js +278 -0
- package/lib/WGPUComputePass.js.map +1 -0
- package/lib/WGPUFullQuadPass.d.ts +50 -0
- package/lib/WGPUFullQuadPass.js +261 -0
- package/lib/WGPUFullQuadPass.js.map +1 -0
- package/lib/backend.d.ts +1 -0
- package/lib/backend.js +39 -0
- package/lib/backend.js.map +1 -0
- package/lib/finalRgbShader.d.ts +13 -0
- package/lib/finalRgbShader.js +160 -0
- package/lib/finalRgbShader.js.map +1 -0
- package/lib/gpuTypes.d.ts +158 -0
- package/lib/gpuTypes.js +27 -0
- package/lib/gpuTypes.js.map +1 -0
- package/lib/graphOptimizer.d.ts +54 -0
- package/lib/graphOptimizer.js +216 -0
- package/lib/graphOptimizer.js.map +1 -0
- package/lib/hdr.d.ts +31 -0
- package/lib/hdr.js +340 -0
- package/lib/hdr.js.map +1 -0
- package/lib/hdrTransfer.d.ts +22 -0
- package/lib/hdrTransfer.js +128 -0
- package/lib/hdrTransfer.js.map +1 -0
- package/lib/main.d.ts +57 -0
- package/lib/main.js +24 -0
- package/lib/main.js.map +1 -0
- package/lib/modelSpec.d.ts +80 -0
- package/lib/modelSpec.js +270 -0
- package/lib/modelSpec.js.map +1 -0
- package/lib/native/denoise.d.ts +25 -0
- package/lib/native/denoise.js +223 -0
- package/lib/native/denoise.js.map +1 -0
- package/lib/native/program.d.ts +53 -0
- package/lib/native/program.js +310 -0
- package/lib/native/program.js.map +1 -0
- package/lib/native/recordingDevice.d.ts +125 -0
- package/lib/native/recordingDevice.js +247 -0
- package/lib/native/recordingDevice.js.map +1 -0
- package/lib/nativeUNet.d.ts +25 -0
- package/lib/nativeUNet.js +184 -0
- package/lib/nativeUNet.js.map +1 -0
- package/lib/nativeUNetCore.d.ts +202 -0
- package/lib/nativeUNetCore.js +2945 -0
- package/lib/nativeUNetCore.js.map +1 -0
- package/lib/process.d.ts +37 -0
- package/lib/process.js +328 -0
- package/lib/process.js.map +1 -0
- package/lib/resourceTracker.d.ts +26 -0
- package/lib/resourceTracker.js +65 -0
- package/lib/resourceTracker.js.map +1 -0
- package/lib/sanitize.d.ts +15 -0
- package/lib/sanitize.js +28 -0
- package/lib/sanitize.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/tza.d.ts +15 -0
- package/lib/tza.js +114 -0
- package/lib/tza.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 +43 -4
- package/src/UNet.ts +861 -0
- package/src/WGPUComputePass.ts +382 -0
- package/src/WGPUFullQuadPass.ts +348 -0
- package/src/backend.ts +42 -0
- package/src/finalRgbShader.ts +186 -0
- package/src/gpuTypes.ts +179 -0
- package/src/graphOptimizer.ts +301 -0
- package/src/hdr.ts +398 -0
- package/src/hdrTransfer.ts +169 -0
- package/src/main.ts +126 -0
- package/src/modelSpec.ts +414 -0
- package/src/native/core-profile.json +40 -0
- package/src/native/denoise.ts +268 -0
- package/src/native/program.ts +326 -0
- package/src/native/recordingDevice.ts +341 -0
- package/src/nativeUNet.ts +269 -0
- package/src/nativeUNetCore.ts +3735 -0
- package/src/process.ts +368 -0
- package/src/resourceTracker.ts +94 -0
- package/src/sanitize.ts +29 -0
- package/src/tileScheduler.ts +330 -0
- package/src/tza.ts +136 -0
- package/src/webnnUNet.ts +812 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BufferUsage,
|
|
3
|
+
type OIDNGPUBindGroupEntry,
|
|
4
|
+
type OIDNGPUBuffer,
|
|
5
|
+
type OIDNGPUDevice
|
|
6
|
+
} from '../gpuTypes.js';
|
|
7
|
+
import { hdrForwardWGSL, hdrInverseWGSL, type HDRTransfer } from '../hdrTransfer.js';
|
|
8
|
+
import { sanitizeWGSL } from '../sanitize.js';
|
|
9
|
+
import type { ValidatedUNetModel } from '../modelSpec.js';
|
|
10
|
+
import type { NativeUNetCore, NativeUNetPrecision } from '../nativeUNetCore.js';
|
|
11
|
+
import { OIDN_TILE_ALIGNMENT, planTileGrid } from '../tileScheduler.js';
|
|
12
|
+
import type { RecordedBuffer, RecordingDevice } from './recordingDevice.js';
|
|
13
|
+
|
|
14
|
+
export interface NativeDenoiseOptions {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
hdr: boolean;
|
|
18
|
+
transfer: HDRTransfer;
|
|
19
|
+
/** Maximum output tile edge; 0 denoises the whole image as one tile. */
|
|
20
|
+
maxTileSize: number;
|
|
21
|
+
inputScale: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const WORKGROUP = 8;
|
|
25
|
+
|
|
26
|
+
function alignUp(value: number, alignment: number) {
|
|
27
|
+
return Math.ceil(value / alignment) * alignment;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Input processing of one tile for buffer images: clamp-to-edge reads of the
|
|
32
|
+
* tile's input rectangle, HDR transfer, and conversion directly into the
|
|
33
|
+
* network's packed tensor. Auxiliary RGB channels cross vec4 boundaries;
|
|
34
|
+
* unused lanes must be zero just as in createInputPackShader.
|
|
35
|
+
*/
|
|
36
|
+
function inputShader(sourceCount: number, hdr: boolean, transfer: HDRTransfer, precision: NativeUNetPrecision) {
|
|
37
|
+
const names = ['color', 'albedo', 'normal'].slice(0, sourceCount);
|
|
38
|
+
const bindings = names.map((name, index) => `
|
|
39
|
+
@group(0) @binding(${index}) var<storage, read> in_${name}: array<vec4f>;`).join('');
|
|
40
|
+
// Like OIDN, callers pass signed [-1, 1] normals; the network reads n * 0.5 + 0.5.
|
|
41
|
+
const copies = names.slice(1).map((name) => name === 'normal'
|
|
42
|
+
? 'let normal = sanitize(in_normal[inIdx].rgb, -1.0, 1.0) * 0.5 + 0.5;'
|
|
43
|
+
: `let ${name} = sanitize(in_${name}[inIdx].rgb, 0.0, 1.0);`).join('\n ');
|
|
44
|
+
const channels = names.flatMap((name) => ['r', 'g', 'b'].map((channel) => `${name}.${channel}`));
|
|
45
|
+
const blocks = Math.ceil(channels.length / 4);
|
|
46
|
+
const type = precision === 'fp16' ? 'vec4<f16>' : 'vec4<f32>';
|
|
47
|
+
const stores = Array.from({ length: blocks }, (_, block) =>
|
|
48
|
+
`packed[outIdx * ${blocks} + ${block}] = ${type}(vec4f(${[0, 1, 2, 3].map((lane) => channels[block * 4 + lane] ?? '0.0').join(', ')}));`).join('\n ');
|
|
49
|
+
return /* wgsl */ `${precision === 'fp16' ? 'enable f16;\n' : ''}${hdrForwardWGSL(transfer)}${sanitizeWGSL}
|
|
50
|
+
struct Params {
|
|
51
|
+
imageSize: vec2i,
|
|
52
|
+
tileSize: vec2i,
|
|
53
|
+
offset: vec2i,
|
|
54
|
+
|
|
55
|
+
}${bindings}
|
|
56
|
+
@group(0) @binding(${sourceCount}) var<storage, read_write> packed: array<${type}>;
|
|
57
|
+
@group(0) @binding(${sourceCount + 1}) var<uniform> params: Params;
|
|
58
|
+
${hdr ? `struct Frame { inputScale: f32, }
|
|
59
|
+
@group(0) @binding(${sourceCount + 2}) var<uniform> frame: Frame;` : ''}
|
|
60
|
+
|
|
61
|
+
@compute @workgroup_size(${WORKGROUP}, ${WORKGROUP}, 1)
|
|
62
|
+
fn main(@builtin(global_invocation_id) globalId: vec3u) {
|
|
63
|
+
let position = vec2i(globalId.xy);
|
|
64
|
+
if (position.x >= params.tileSize.x || position.y >= params.tileSize.y) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let inputPosition = clamp(position + params.offset, vec2i(0), params.imageSize - vec2i(1));
|
|
68
|
+
let inIdx = inputPosition.y * params.imageSize.x + inputPosition.x;
|
|
69
|
+
let outIdx = position.y * params.tileSize.x + position.x;
|
|
70
|
+
let col = in_color[inIdx];
|
|
71
|
+
${hdr
|
|
72
|
+
? `let scaled = sanitize(col.rgb * frame.inputScale, 0.0, FLT_MAX);
|
|
73
|
+
let color = vec3f(HDRForward(scaled.r), HDRForward(scaled.g), HDRForward(scaled.b));`
|
|
74
|
+
: 'let color = sanitize(col.rgb, 0.0, 1.0);'}
|
|
75
|
+
${copies}
|
|
76
|
+
${stores}
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Output processing of one tile: the inverse HDR transfer of the network
|
|
83
|
+
* output inside the tile's output rectangle, written with the input color's
|
|
84
|
+
* alpha. Matches the browser runtime's GPU output pass.
|
|
85
|
+
*/
|
|
86
|
+
function outputShader(hdr: boolean, transfer: HDRTransfer) {
|
|
87
|
+
return /* wgsl */ `${hdrInverseWGSL(transfer)}${sanitizeWGSL}
|
|
88
|
+
struct Params {
|
|
89
|
+
imageWidth: i32,
|
|
90
|
+
tileWidth: i32,
|
|
91
|
+
outputOffset: vec2i,
|
|
92
|
+
outputSize: vec2i,
|
|
93
|
+
inputOffset: vec2i,
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
@group(0) @binding(0) var<storage, read> in_color: array<vec4f>;
|
|
97
|
+
@group(0) @binding(1) var<storage, read> in_raw: array<vec4f>;
|
|
98
|
+
@group(0) @binding(2) var<storage, read_write> out_color: array<vec4f>;
|
|
99
|
+
@group(0) @binding(3) var<uniform> params: Params;
|
|
100
|
+
${hdr ? `struct Frame { inputScale: f32, }
|
|
101
|
+
@group(0) @binding(4) var<uniform> frame: Frame;` : ''}
|
|
102
|
+
|
|
103
|
+
@compute @workgroup_size(${WORKGROUP}, ${WORKGROUP}, 1)
|
|
104
|
+
fn main(@builtin(global_invocation_id) globalId: vec3u) {
|
|
105
|
+
let x = i32(globalId.x);
|
|
106
|
+
let y = i32(globalId.y);
|
|
107
|
+
if (x >= params.outputSize.x || y >= params.outputSize.y) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let inIdx = (y + params.inputOffset.y) * params.tileWidth + x + params.inputOffset.x;
|
|
111
|
+
let outIdx = (y + params.outputOffset.y) * params.imageWidth + x + params.outputOffset.x;
|
|
112
|
+
let col = in_color[inIdx];
|
|
113
|
+
let raw = in_raw[outIdx];
|
|
114
|
+
// The network output may contain negative values or NaNs.
|
|
115
|
+
let denoised = sanitize(col.rgb, 0.0, ${hdr ? 'FLT_MAX' : '1.0'});
|
|
116
|
+
${hdr
|
|
117
|
+
? 'out_color[outIdx] = vec4f(vec3f(HDRInverse(denoised.r), HDRInverse(denoised.g), HDRInverse(denoised.b)) / frame.inputScale, raw.a);'
|
|
118
|
+
: 'out_color[outIdx] = vec4f(denoised, raw.a);'}
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function uniformBuffer(device: OIDNGPUDevice, label: string, words: Int32Array) {
|
|
124
|
+
const buffer = device.createBuffer({
|
|
125
|
+
label,
|
|
126
|
+
size: alignUp(words.byteLength, 16),
|
|
127
|
+
usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
|
|
128
|
+
mappedAtCreation: true
|
|
129
|
+
});
|
|
130
|
+
new Uint8Array(buffer.getMappedRange()).set(new Uint8Array(words.buffer));
|
|
131
|
+
buffer.unmap();
|
|
132
|
+
return buffer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function storageBuffer(device: OIDNGPUDevice, label: string, size: number) {
|
|
136
|
+
return device.createBuffer({
|
|
137
|
+
label,
|
|
138
|
+
size,
|
|
139
|
+
usage: BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Records the commands of one denoising of a `width` x `height` image: for
|
|
145
|
+
* every planned tile the input pass, the network and the output pass. The
|
|
146
|
+
* tiles follow `planTileGrid` with the model's receptive field as overlap,
|
|
147
|
+
* like `UNet.tileExecute`.
|
|
148
|
+
*/
|
|
149
|
+
export function recordDenoiseExecution(
|
|
150
|
+
device: RecordingDevice,
|
|
151
|
+
executor: NativeUNetCore,
|
|
152
|
+
model: ValidatedUNetModel,
|
|
153
|
+
options: NativeDenoiseOptions
|
|
154
|
+
) {
|
|
155
|
+
const { width, height } = options;
|
|
156
|
+
if (width <= 0 || height <= 0) throw new Error('Image dimensions must be positive');
|
|
157
|
+
const sourceCount = model.inputChannels / 3;
|
|
158
|
+
const imageBytes = width * height * 16;
|
|
159
|
+
const inputs: RecordedBuffer[] = [];
|
|
160
|
+
for (const name of ['color', 'albedo', 'normal'].slice(0, sourceCount)) {
|
|
161
|
+
inputs.push(storageBuffer(device, `oidn/external/${name}`, imageBytes) as RecordedBuffer);
|
|
162
|
+
}
|
|
163
|
+
const output = storageBuffer(device, 'oidn/external/output', imageBytes) as RecordedBuffer;
|
|
164
|
+
|
|
165
|
+
const overlap = alignUp(model.spec.receptiveField / 2, OIDN_TILE_ALIGNMENT);
|
|
166
|
+
const plan = planTileGrid(
|
|
167
|
+
width,
|
|
168
|
+
height,
|
|
169
|
+
options.maxTileSize > 0 ? options.maxTileSize : alignUp(Math.max(width, height), OIDN_TILE_ALIGNMENT),
|
|
170
|
+
overlap
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const inputPipeline = device.createComputePipeline({
|
|
174
|
+
label: 'oidn/native/input',
|
|
175
|
+
layout: 'auto',
|
|
176
|
+
compute: {
|
|
177
|
+
module: device.createShaderModule({
|
|
178
|
+
label: 'oidn/native/input',
|
|
179
|
+
code: inputShader(sourceCount, options.hdr, options.transfer, executor.precision)
|
|
180
|
+
}),
|
|
181
|
+
entryPoint: 'main'
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const outputPipeline = device.createComputePipeline({
|
|
185
|
+
label: 'oidn/native/output',
|
|
186
|
+
layout: 'auto',
|
|
187
|
+
compute: {
|
|
188
|
+
module: device.createShaderModule({
|
|
189
|
+
label: 'oidn/native/output',
|
|
190
|
+
code: outputShader(options.hdr, options.transfer)
|
|
191
|
+
}),
|
|
192
|
+
entryPoint: 'main'
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
const scaleWord = new Int32Array(new Float32Array([options.inputScale]).buffer)[0];
|
|
196
|
+
|
|
197
|
+
const frame = uniformBuffer(device, 'oidn/native/frame', new Int32Array([scaleWord, 0, 0, 0])) as RecordedBuffer;
|
|
198
|
+
|
|
199
|
+
// Network inputs are per tile shape; tiles run in order on one queue.
|
|
200
|
+
const tileInputs = new Map<string, OIDNGPUBuffer>();
|
|
201
|
+
for (const tile of plan.tiles) {
|
|
202
|
+
const key = `${tile.input.width}x${tile.input.height}`;
|
|
203
|
+
if (tileInputs.has(key)) continue;
|
|
204
|
+
tileInputs.set(key, executor.packedInputBuffer(tile.input.width, tile.input.height));
|
|
205
|
+
}
|
|
206
|
+
device.operations = [];
|
|
207
|
+
|
|
208
|
+
plan.tiles.forEach((tile, index) => {
|
|
209
|
+
const packed = tileInputs.get(`${tile.input.width}x${tile.input.height}`)!;
|
|
210
|
+
const inputParams = uniformBuffer(device, `oidn/native/input/${index}`, new Int32Array([
|
|
211
|
+
width, height, tile.input.width, tile.input.height, tile.input.x, tile.input.y, 0, 0
|
|
212
|
+
]));
|
|
213
|
+
const entries: OIDNGPUBindGroupEntry[] = [];
|
|
214
|
+
for (let source = 0; source < sourceCount; source++) {
|
|
215
|
+
entries.push({ binding: source, resource: { buffer: inputs[source] } });
|
|
216
|
+
}
|
|
217
|
+
entries.push({ binding: sourceCount, resource: { buffer: packed } });
|
|
218
|
+
entries.push({ binding: sourceCount + 1, resource: { buffer: inputParams } });
|
|
219
|
+
if (options.hdr) entries.push({ binding: sourceCount + 2, resource: { buffer: frame } });
|
|
220
|
+
const inputBindings = device.createBindGroup({
|
|
221
|
+
label: `oidn/native/input/${index}`,
|
|
222
|
+
layout: inputPipeline.getBindGroupLayout(0),
|
|
223
|
+
entries
|
|
224
|
+
});
|
|
225
|
+
const inputEncoder = device.createCommandEncoder({ label: `oidn/native/input/${index}` });
|
|
226
|
+
const inputPass = inputEncoder.beginComputePass();
|
|
227
|
+
inputPass.setPipeline(inputPipeline);
|
|
228
|
+
inputPass.setBindGroup(0, inputBindings);
|
|
229
|
+
inputPass.dispatchWorkgroups(
|
|
230
|
+
Math.ceil(tile.input.width / WORKGROUP),
|
|
231
|
+
Math.ceil(tile.input.height / WORKGROUP)
|
|
232
|
+
);
|
|
233
|
+
inputPass.end();
|
|
234
|
+
device.queue.submit([inputEncoder.finish()]);
|
|
235
|
+
|
|
236
|
+
const networkOutput = executor.executePacked(tile.input.width, tile.input.height);
|
|
237
|
+
|
|
238
|
+
const outputParams = uniformBuffer(device, `oidn/native/output/${index}`, new Int32Array([
|
|
239
|
+
width, tile.input.width,
|
|
240
|
+
tile.output.x, tile.output.y,
|
|
241
|
+
tile.output.width, tile.output.height,
|
|
242
|
+
tile.output.x - tile.input.x, tile.output.y - tile.input.y,
|
|
243
|
+
0, 0, 0, 0
|
|
244
|
+
]));
|
|
245
|
+
const outputBindings = device.createBindGroup({
|
|
246
|
+
label: `oidn/native/output/${index}`,
|
|
247
|
+
layout: outputPipeline.getBindGroupLayout(0),
|
|
248
|
+
entries: [
|
|
249
|
+
{ binding: 0, resource: { buffer: networkOutput } },
|
|
250
|
+
{ binding: 1, resource: { buffer: inputs[0] } },
|
|
251
|
+
{ binding: 2, resource: { buffer: output } },
|
|
252
|
+
{ binding: 3, resource: { buffer: outputParams } },
|
|
253
|
+
...(options.hdr ? [{ binding: 4, resource: { buffer: frame } }] : [])
|
|
254
|
+
]
|
|
255
|
+
});
|
|
256
|
+
const outputEncoder = device.createCommandEncoder({ label: `oidn/native/output/${index}` });
|
|
257
|
+
const outputPass = outputEncoder.beginComputePass();
|
|
258
|
+
outputPass.setPipeline(outputPipeline);
|
|
259
|
+
outputPass.setBindGroup(0, outputBindings);
|
|
260
|
+
outputPass.dispatchWorkgroups(
|
|
261
|
+
Math.ceil(tile.output.width / WORKGROUP),
|
|
262
|
+
Math.ceil(tile.output.height / WORKGROUP)
|
|
263
|
+
);
|
|
264
|
+
outputPass.end();
|
|
265
|
+
device.queue.submit([outputEncoder.finish()]);
|
|
266
|
+
});
|
|
267
|
+
return { inputs, output, tiles: plan.tiles.length, frame };
|
|
268
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { BufferUsage } from '../gpuTypes.js';
|
|
2
|
+
import { detectUNetModelSpec, validateUNetModel } from '../modelSpec.js';
|
|
3
|
+
import {
|
|
4
|
+
NativeUNetCore,
|
|
5
|
+
type NativeUNetKernelSetting,
|
|
6
|
+
type NativeUNetPrecisionSetting
|
|
7
|
+
} from '../nativeUNetCore.js';
|
|
8
|
+
import { parseTZA } from '../tza.js';
|
|
9
|
+
import { RecordingDevice, type RecordedBuffer } from './recordingDevice.js';
|
|
10
|
+
import { recordDenoiseExecution } from './denoise.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Records the native U-Net for one input size as a replayable program.
|
|
14
|
+
*
|
|
15
|
+
* Config (little-endian u32 words):
|
|
16
|
+
* 0 version (1), 1 width, 2 height,
|
|
17
|
+
* 3 precision (0 auto, 1 fp32, 2 fp16),
|
|
18
|
+
* 4 kernel (0 auto, 1 implicit-gemm, 2 direct, 3 subgroup-matrix),
|
|
19
|
+
* 5 features (1 shader-f16, 2 subgroups, 4 chromium-experimental-subgroup-matrix,
|
|
20
|
+
* 8 wgpu-cooperative-matrix),
|
|
21
|
+
* 6 maxComputeWorkgroupStorageSize, 7 maxComputeInvocationsPerWorkgroup,
|
|
22
|
+
* 8 maxComputeWorkgroupSizeX, 9 maxComputeWorkgroupSizeY,
|
|
23
|
+
* 10 maxComputeWorkgroupsPerDimension, 11 subgroupMinSize, 12 subgroupMaxSize,
|
|
24
|
+
* 13 matrix configuration count, then per configuration:
|
|
25
|
+
* component type (0 f32, 1 f16), result type, M, N, K,
|
|
26
|
+
* then subgroup-matrix options: double buffering (0 default, 1 off, 2 on),
|
|
27
|
+
* accumulation (0 f32, 1 f16), sub-pixel decoders (0 on, 1 off).
|
|
28
|
+
*
|
|
29
|
+
* Program (little-endian u32 words, strings and byte blobs padded to 4 bytes):
|
|
30
|
+
* magic 0x4e44494f ("OIDN"), version 2,
|
|
31
|
+
* input count, input buffer ids..., output buffer id,
|
|
32
|
+
* output external (1 when the host may supply the output buffer),
|
|
33
|
+
* output width, output height, output channels,
|
|
34
|
+
* buffers: count, then size, usage, data length (0 = zero-filled), data, label
|
|
35
|
+
* shaders: count, then code
|
|
36
|
+
* pipelines: count, then shader id, entry point
|
|
37
|
+
* bind groups: count, then pipeline id, group, entry count,
|
|
38
|
+
* entries (binding, buffer id, offset, size; size 0 = rest of buffer)
|
|
39
|
+
* operations: count, then kind
|
|
40
|
+
* 0 submit: command count, commands as kind
|
|
41
|
+
* 0 dispatch: pipeline id, bind group id, x, y, z
|
|
42
|
+
* 1 copy: source, source offset, destination, destination offset, size
|
|
43
|
+
* 1 write: buffer id, offset, data length, data
|
|
44
|
+
*
|
|
45
|
+
* Input buffers are RGBA f32 images in the transfer space of the network
|
|
46
|
+
* (as produced by the browser runtime's GPU preprocessing); the output buffer
|
|
47
|
+
* holds RGBA f32 pixels in the same space.
|
|
48
|
+
*/
|
|
49
|
+
export function recordNetwork(tza: Uint8Array, config: Uint8Array): Uint8Array {
|
|
50
|
+
const { words, device, model, executor } = createNativeExecutor(tza, config);
|
|
51
|
+
const width = words[1];
|
|
52
|
+
const height = words[2];
|
|
53
|
+
const inputs: RecordedBuffer[] = [];
|
|
54
|
+
for (let index = 0; index < model.inputChannels / 3; index++) {
|
|
55
|
+
inputs.push(device.createBuffer({
|
|
56
|
+
label: `oidn/external/input${index}`,
|
|
57
|
+
size: width * height * 16,
|
|
58
|
+
usage: BufferUsage.STORAGE | BufferUsage.COPY_DST
|
|
59
|
+
}) as RecordedBuffer);
|
|
60
|
+
}
|
|
61
|
+
// The first execution creates the per-shape resources; the second records
|
|
62
|
+
// exactly what every later execution submits.
|
|
63
|
+
executor.execute(inputs, width, height);
|
|
64
|
+
device.operations = [];
|
|
65
|
+
const output = executor.execute(inputs, width, height) as RecordedBuffer;
|
|
66
|
+
return serializeProgram(device, inputs, output, false, width, height, model.outputChannels, executor, 0, undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Records a whole denoising of one image size: tiles, input processing,
|
|
71
|
+
* the network and output processing (see `denoise.ts`). Config: the network
|
|
72
|
+
* config with width and height of the image, followed after the
|
|
73
|
+
* subgroup-matrix options by:
|
|
74
|
+
* hdr (0/1), HDR transfer (0 PU, 1 log), maximum tile edge (0 = whole image),
|
|
75
|
+
* input scale (f32 bits; 0 = 1).
|
|
76
|
+
* Inputs are color, then albedo and normal for auxiliary models, as RGBA f32
|
|
77
|
+
* images; the external output is an RGBA f32 image (color alpha preserved).
|
|
78
|
+
*/
|
|
79
|
+
export function recordDenoise(tza: Uint8Array, config: Uint8Array): Uint8Array {
|
|
80
|
+
const { words, device, model, executor, next } = createNativeExecutor(tza, config);
|
|
81
|
+
const width = words[1];
|
|
82
|
+
const height = words[2];
|
|
83
|
+
const scaleBits = new Uint32Array([words[next + 3]]);
|
|
84
|
+
const inputScale = words[next + 3] === 0 ? 1 : new Float32Array(scaleBits.buffer)[0];
|
|
85
|
+
const recorded = recordDenoiseExecution(device, executor, model, {
|
|
86
|
+
width,
|
|
87
|
+
height,
|
|
88
|
+
hdr: words[next] === 1,
|
|
89
|
+
transfer: words[next + 1] === 1 ? 'log' : 'pu',
|
|
90
|
+
maxTileSize: words[next + 2],
|
|
91
|
+
inputScale
|
|
92
|
+
});
|
|
93
|
+
return serializeProgram(device, recorded.inputs, recorded.output, true, width, height, 4, executor, recorded.tiles, recorded.frame);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createNativeExecutor(tza: Uint8Array, config: Uint8Array) {
|
|
97
|
+
const words = new Uint32Array(config.buffer, config.byteOffset, config.byteLength / 4);
|
|
98
|
+
if (words[0] !== 1) throw new Error(`Unsupported native config version ${words[0]}`);
|
|
99
|
+
const precisions: NativeUNetPrecisionSetting[] = ['auto', 'fp32', 'fp16'];
|
|
100
|
+
const kernels: NativeUNetKernelSetting[] = ['auto', 'implicit-gemm', 'direct', 'subgroup-matrix'];
|
|
101
|
+
const features: string[] = [];
|
|
102
|
+
if (words[5] & 1) features.push('shader-f16');
|
|
103
|
+
if (words[5] & 2) features.push('subgroups');
|
|
104
|
+
if (words[5] & 4) features.push('chromium-experimental-subgroup-matrix');
|
|
105
|
+
if (words[5] & 8) features.push('wgpu-cooperative-matrix');
|
|
106
|
+
const configs: { componentType: string; resultComponentType: string; M: number; N: number; K: number }[] = [];
|
|
107
|
+
for (let index = 0; index < words[13]; index++) {
|
|
108
|
+
const base = 14 + index * 5;
|
|
109
|
+
configs.push({
|
|
110
|
+
componentType: words[base] === 1 ? 'f16' : 'f32',
|
|
111
|
+
resultComponentType: words[base + 1] === 1 ? 'f16' : 'f32',
|
|
112
|
+
M: words[base + 2],
|
|
113
|
+
N: words[base + 3],
|
|
114
|
+
K: words[base + 4]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const options = 14 + words[13] * 5;
|
|
118
|
+
const device = new RecordingDevice({
|
|
119
|
+
features,
|
|
120
|
+
limits: {
|
|
121
|
+
maxComputeWorkgroupStorageSize: words[6],
|
|
122
|
+
maxComputeInvocationsPerWorkgroup: words[7],
|
|
123
|
+
maxComputeWorkgroupSizeX: words[8],
|
|
124
|
+
maxComputeWorkgroupSizeY: words[9],
|
|
125
|
+
maxComputeWorkgroupsPerDimension: words[10]
|
|
126
|
+
},
|
|
127
|
+
adapterInfo: {
|
|
128
|
+
subgroupMinSize: words[11],
|
|
129
|
+
subgroupMaxSize: words[12],
|
|
130
|
+
subgroupMatrixConfigs: configs
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const tensors = parseTZA(
|
|
135
|
+
tza.buffer.slice(tza.byteOffset, tza.byteOffset + tza.byteLength) as ArrayBuffer
|
|
136
|
+
);
|
|
137
|
+
const model = validateUNetModel(tensors, detectUNetModelSpec(tensors));
|
|
138
|
+
const doubleBuffer = words[options] === 0 ? undefined : words[options] === 2;
|
|
139
|
+
const executor = new NativeUNetCore(device, model, {
|
|
140
|
+
precision: precisions[words[3]],
|
|
141
|
+
kernel: kernels[words[4]],
|
|
142
|
+
gemm: { unrollRows: true },
|
|
143
|
+
subgroupMatrix: {
|
|
144
|
+
...(doubleBuffer === undefined ? {} : { doubleBuffer }),
|
|
145
|
+
accumulation: words[options + 1] === 1 ? 'f16' : 'f32',
|
|
146
|
+
subPixel: words[options + 2] !== 1
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return { words, device, model, executor, next: options + 3 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class ProgramWriter {
|
|
153
|
+
private _bytes = new Uint8Array(1 << 16);
|
|
154
|
+
private _view = new DataView(this._bytes.buffer);
|
|
155
|
+
length = 0;
|
|
156
|
+
|
|
157
|
+
private _reserve(count: number) {
|
|
158
|
+
if (this.length + count <= this._bytes.length) return;
|
|
159
|
+
let capacity = this._bytes.length * 2;
|
|
160
|
+
while (capacity < this.length + count) capacity *= 2;
|
|
161
|
+
const bytes = new Uint8Array(capacity);
|
|
162
|
+
bytes.set(this._bytes.subarray(0, this.length));
|
|
163
|
+
this._bytes = bytes;
|
|
164
|
+
this._view = new DataView(bytes.buffer);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
u32(value: number) {
|
|
168
|
+
this._reserve(4);
|
|
169
|
+
this._view.setUint32(this.length, value, true);
|
|
170
|
+
this.length += 4;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
bytes(data: Uint8Array) {
|
|
174
|
+
this.u32(data.length);
|
|
175
|
+
const padded = (data.length + 3) & ~3;
|
|
176
|
+
this._reserve(padded);
|
|
177
|
+
this._bytes.set(data, this.length);
|
|
178
|
+
this._bytes.fill(0, this.length + data.length, this.length + padded);
|
|
179
|
+
this.length += padded;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
string(value: string) {
|
|
183
|
+
this.bytes(new TextEncoder().encode(value));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
finish() {
|
|
187
|
+
return this._bytes.slice(0, this.length);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Device-independent model validation for native Rust hosts. Packet: MODEL
|
|
192
|
+
// magic, version, image input count; errors use the existing ERRO packet.
|
|
193
|
+
export function inspectModelPacket(tza: Uint8Array): Uint8Array {
|
|
194
|
+
const writer = new ProgramWriter();
|
|
195
|
+
try {
|
|
196
|
+
const tensors = parseTZA(tza.buffer.slice(tza.byteOffset, tza.byteOffset + tza.byteLength) as ArrayBuffer);
|
|
197
|
+
const model = validateUNetModel(tensors, detectUNetModelSpec(tensors));
|
|
198
|
+
writer.u32(0x4c444f4d);
|
|
199
|
+
writer.u32(1);
|
|
200
|
+
writer.u32(model.inputChannels / 3);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
writer.u32(0x4f525245);
|
|
203
|
+
writer.string(error instanceof Error ? error.message : String(error));
|
|
204
|
+
}
|
|
205
|
+
return writer.finish();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function serializeProgram(
|
|
209
|
+
device: RecordingDevice,
|
|
210
|
+
inputs: readonly RecordedBuffer[],
|
|
211
|
+
output: RecordedBuffer,
|
|
212
|
+
outputExternal: boolean,
|
|
213
|
+
width: number,
|
|
214
|
+
height: number,
|
|
215
|
+
outputChannels: number,
|
|
216
|
+
executor: NativeUNetCore,
|
|
217
|
+
tiles: number,
|
|
218
|
+
frame: RecordedBuffer | undefined
|
|
219
|
+
) {
|
|
220
|
+
const writer = new ProgramWriter();
|
|
221
|
+
writer.u32(0x4e44494f);
|
|
222
|
+
writer.u32(2);
|
|
223
|
+
writer.u32(inputs.length);
|
|
224
|
+
for (const input of inputs) writer.u32(input.id);
|
|
225
|
+
writer.u32(output.id);
|
|
226
|
+
writer.u32(outputExternal ? 1 : 0);
|
|
227
|
+
writer.u32(width);
|
|
228
|
+
writer.u32(height);
|
|
229
|
+
writer.u32(outputChannels);
|
|
230
|
+
|
|
231
|
+
writer.u32(device.buffers.length);
|
|
232
|
+
for (const buffer of device.buffers) {
|
|
233
|
+
if (!Number.isSafeInteger(buffer.size) || buffer.size <= 0 || buffer.size > 0xffffffff) {
|
|
234
|
+
throw new Error(`Native program buffer ${buffer.label} exceeds the 32-bit size limit`);
|
|
235
|
+
}
|
|
236
|
+
writer.u32(buffer.size);
|
|
237
|
+
writer.u32(buffer.usage);
|
|
238
|
+
writer.bytes(buffer.data ?? new Uint8Array(0));
|
|
239
|
+
writer.string(buffer.label);
|
|
240
|
+
}
|
|
241
|
+
writer.u32(device.shaders.length);
|
|
242
|
+
for (const shader of device.shaders) writer.string(shader.code);
|
|
243
|
+
writer.u32(device.pipelines.length);
|
|
244
|
+
for (const pipeline of device.pipelines) {
|
|
245
|
+
writer.u32(pipeline.shader);
|
|
246
|
+
writer.string(pipeline.entryPoint);
|
|
247
|
+
}
|
|
248
|
+
writer.u32(device.bindGroups.length);
|
|
249
|
+
for (const bindGroup of device.bindGroups) {
|
|
250
|
+
writer.u32(bindGroup.pipeline);
|
|
251
|
+
writer.u32(bindGroup.group);
|
|
252
|
+
writer.u32(bindGroup.entries.length);
|
|
253
|
+
for (const entry of bindGroup.entries) {
|
|
254
|
+
writer.u32(entry.binding);
|
|
255
|
+
writer.u32(entry.buffer);
|
|
256
|
+
writer.u32(entry.offset);
|
|
257
|
+
writer.u32(entry.size);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
writer.u32(device.operations.length);
|
|
261
|
+
for (const operation of device.operations) {
|
|
262
|
+
writer.u32(operation.kind);
|
|
263
|
+
if (operation.kind === 0) {
|
|
264
|
+
writer.u32(operation.commands.length);
|
|
265
|
+
for (const recorded of operation.commands) {
|
|
266
|
+
writer.u32(recorded.kind);
|
|
267
|
+
if (recorded.kind === 0) {
|
|
268
|
+
writer.u32(recorded.pipeline);
|
|
269
|
+
writer.u32(recorded.bindGroup);
|
|
270
|
+
writer.u32(recorded.x);
|
|
271
|
+
writer.u32(recorded.y);
|
|
272
|
+
writer.u32(recorded.z);
|
|
273
|
+
} else {
|
|
274
|
+
writer.u32(recorded.source);
|
|
275
|
+
writer.u32(recorded.sourceOffset);
|
|
276
|
+
writer.u32(recorded.destination);
|
|
277
|
+
writer.u32(recorded.destinationOffset);
|
|
278
|
+
writer.u32(recorded.size);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
writer.u32(operation.buffer);
|
|
283
|
+
writer.u32(operation.offset);
|
|
284
|
+
writer.bytes(operation.data ?? new Uint8Array(0));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Optional extension after v2 operations. Old replayers can ignore it.
|
|
288
|
+
writer.u32(0x4154454d); // META
|
|
289
|
+
writer.u32(1);
|
|
290
|
+
writer.u32(executor.precision === 'fp16' ? 2 : 1);
|
|
291
|
+
writer.u32(tiles);
|
|
292
|
+
let kernels = 0;
|
|
293
|
+
for (const kernel of executor.usedKernels) {
|
|
294
|
+
kernels |= kernel === 'direct' ? 1 : kernel === 'implicit-gemm' ? 2
|
|
295
|
+
: kernel === 'subgroup-matrix' ? 4 : kernel === 'spatial' ? 8 : 16;
|
|
296
|
+
}
|
|
297
|
+
writer.u32(kernels);
|
|
298
|
+
writer.u32(frame ? frame.id : 0xffffffff);
|
|
299
|
+
const weights = executor.weightBuffers as RecordedBuffer[];
|
|
300
|
+
writer.u32(weights.length);
|
|
301
|
+
for (const buffer of weights) writer.u32(buffer.id);
|
|
302
|
+
return writer.finish();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Error packet: magic 0x4f525245 ("ERRO") followed by the message. */
|
|
306
|
+
export function recordNetworkPacket(tza: Uint8Array, config: Uint8Array): Uint8Array {
|
|
307
|
+
try {
|
|
308
|
+
return recordNetwork(tza, config);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const writer = new ProgramWriter();
|
|
311
|
+
writer.u32(0x4f525245);
|
|
312
|
+
writer.string(error instanceof Error ? error.message : String(error));
|
|
313
|
+
return writer.finish();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function recordDenoisePacket(tza: Uint8Array, config: Uint8Array): Uint8Array {
|
|
318
|
+
try {
|
|
319
|
+
return recordDenoise(tza, config);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const writer = new ProgramWriter();
|
|
322
|
+
writer.u32(0x4f525245);
|
|
323
|
+
writer.string(error instanceof Error ? error.message : String(error));
|
|
324
|
+
return writer.finish();
|
|
325
|
+
}
|
|
326
|
+
}
|