rayzee 8.0.0 → 8.2.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/dist/rayzee.es.js +4413 -4411
- package/dist/rayzee.umd.js +80 -215
- package/package.json +2 -2
- package/src/Passes/OIDNDenoiser.js +196 -270
- package/src/PathTracerApp.js +73 -19
- package/src/Processor/GeometryExtractor.js +3 -1
- package/src/Processor/utils.js +8 -20
- package/src/Stages/Compositor.js +1 -0
- package/src/Stages/PathTracerStage.js +25 -0
- package/src/TSL/SortGlobalKernels.js +3 -4
- package/src/TSL/patches.js +22 -82
- package/src/managers/DenoisingManager.js +276 -58
- package/src/managers/VideoRenderManager.js +4 -0
- package/src/Processor/ToneMapWGSL.js +0 -162
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { EventDispatcher,
|
|
1
|
+
import { EventDispatcher, HalfFloatType, RGBAFormat, NearestFilter, NoColorSpace, ClampToEdgeWrapping } from 'three';
|
|
2
|
+
import { ExternalTexture } from 'three/webgpu';
|
|
2
3
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
3
4
|
|
|
4
5
|
const log = createLogger( 'oidn' );
|
|
5
6
|
|
|
7
|
+
// The tile alignment oidn-web's own fitTileDimension uses (its tileScheduler.js).
|
|
8
|
+
const OIDN_TILE_ALIGNMENT = 16;
|
|
9
|
+
|
|
6
10
|
let _initUNetFromURL = null;
|
|
7
11
|
async function getInitUNetFromURL() {
|
|
8
12
|
|
|
@@ -16,8 +20,6 @@ async function getInitUNetFromURL() {
|
|
|
16
20
|
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
import { effectiveExposure } from '../Processor/ToneMapCPU.js';
|
|
20
|
-
import { TONE_MAP_WGSL, toneMapMode } from '../Processor/ToneMapWGSL.js';
|
|
21
23
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
22
24
|
|
|
23
25
|
const MODEL_CONFIG = {
|
|
@@ -137,85 +139,69 @@ const SCALE_WG_SIZE = 64;
|
|
|
137
139
|
// 1D would exceed maxComputeWorkgroupsPerDimension (65535) past ~2048², so the dispatch is 2D.
|
|
138
140
|
const SCALE_MAX_WG_X = 32768;
|
|
139
141
|
|
|
140
|
-
// Denoised linear float ->
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
const PACK_WGSL = /* wgsl */`
|
|
146
|
-
${TONE_MAP_WGSL}
|
|
142
|
+
// Denoised linear float -> a picture the pipeline can sample, on the card. The result keeps the
|
|
143
|
+
// renderer's own units: no exposure, no grade, no tone curve. Compositor and the renderer's output
|
|
144
|
+
// pass apply those to this picture exactly as they do to the raw one.
|
|
145
|
+
const UNPACK_WG_SIZE = 16;
|
|
146
|
+
const UNPACK_PARAMS_BYTES = 32;
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
const UNPACK_WGSL = /* wgsl */`
|
|
149
|
+
struct UnpackParams {
|
|
149
150
|
srcWidth: u32,
|
|
150
151
|
tileX: u32,
|
|
151
152
|
tileY: u32,
|
|
152
153
|
tileW: u32,
|
|
153
154
|
tileH: u32,
|
|
154
|
-
mode: u32,
|
|
155
|
-
useAlpha: u32,
|
|
156
155
|
pad0: u32,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
pad1: vec2<f32>,
|
|
156
|
+
pad1: u32,
|
|
157
|
+
pad2: u32,
|
|
160
158
|
};
|
|
161
159
|
|
|
162
160
|
@group(0) @binding(0) var<storage, read> src: array<vec4<f32>>;
|
|
163
|
-
@group(0) @binding(1) var<
|
|
164
|
-
@group(0) @binding(2) var<
|
|
165
|
-
@group(0) @binding(3) var<storage, read>
|
|
166
|
-
@group(0) @binding(4) var
|
|
161
|
+
@group(0) @binding(1) var<uniform> P: UnpackParams;
|
|
162
|
+
@group(0) @binding(2) var<storage, read> scaleBuf: array<f32>;
|
|
163
|
+
@group(0) @binding(3) var<storage, read> inColor: array<vec4<f32>>;
|
|
164
|
+
@group(0) @binding(4) var dst: texture_storage_2d<rgba16float, write>;
|
|
167
165
|
|
|
168
|
-
@compute @workgroup_size(${
|
|
166
|
+
@compute @workgroup_size(${UNPACK_WG_SIZE}, ${UNPACK_WG_SIZE})
|
|
169
167
|
fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
170
168
|
|
|
171
169
|
if ( gid.x >= P.tileW || gid.y >= P.tileH ) { return; }
|
|
172
170
|
|
|
173
|
-
let
|
|
174
|
-
let
|
|
175
|
-
|
|
176
|
-
let rgb = toneMapPixel( src[ si ].xyz, P.exposure / scaleBuf[ 0 ], P.saturation, P.mode );
|
|
171
|
+
let px = P.tileX + gid.x;
|
|
172
|
+
let py = P.tileY + gid.y;
|
|
173
|
+
let si = py * P.srcWidth + px;
|
|
177
174
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
dst[ di ] = q.x | ( q.y << 8u ) | ( q.z << 16u ) | ( a << 24u );
|
|
175
|
+
// scaleBuf is the autoexposure factor the input was multiplied by; dividing it out returns
|
|
176
|
+
// the renderer's units. Alpha rides through from the path tracer so a transparent background
|
|
177
|
+
// survives the denoise.
|
|
178
|
+
textureStore( dst, vec2<u32>( px, py ), vec4<f32>( src[ si ].xyz / scaleBuf[ 0 ], inColor[ si ].w ) );
|
|
183
179
|
}
|
|
184
180
|
`;
|
|
185
181
|
|
|
186
182
|
export class OIDNDenoiser extends EventDispatcher {
|
|
187
183
|
|
|
188
|
-
|
|
184
|
+
/**
|
|
185
|
+
* @param {import('three/webgpu').WebGPURenderer} renderer
|
|
186
|
+
* @param {Object} [options]
|
|
187
|
+
*/
|
|
188
|
+
constructor( renderer, options = {} ) {
|
|
189
189
|
|
|
190
190
|
super();
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
if ( ! output || ! renderer || ! scene || ! camera ) {
|
|
194
|
-
|
|
195
|
-
throw new Error( 'OIDNDenoiser requires output canvas, renderer, scene, and camera' );
|
|
196
|
-
|
|
197
|
-
}
|
|
192
|
+
if ( ! renderer ) throw new Error( 'OIDNDenoiser requires a renderer' );
|
|
198
193
|
|
|
199
194
|
this.renderer = renderer;
|
|
200
|
-
|
|
201
|
-
this
|
|
202
|
-
this.
|
|
203
|
-
this.
|
|
195
|
+
// The size everything is measured in. The denoiser paints no canvas: its result is a
|
|
196
|
+
// picture handed to the pipeline, so this is the only size it knows.
|
|
197
|
+
this._renderWidth = renderer.domElement.width;
|
|
198
|
+
this._renderHeight = renderer.domElement.height;
|
|
204
199
|
|
|
205
200
|
// WebGPU GPU-native path (no CPU readback for inputs)
|
|
206
201
|
// backendParams: () => { device: GPUDevice, adapterInfo: GPUAdapterInfo|null }
|
|
207
202
|
// getGPUTextures: () => { color: GPUTexture, albedo: GPUTexture, normal: GPUTexture }
|
|
208
|
-
// getExposure: () => number (effective exposure multiplier, pre-computed)
|
|
209
|
-
// getToneMapping: () => number (Three.js ToneMapping constant)
|
|
210
203
|
this.backendParamsGetter = options.backendParams || null;
|
|
211
204
|
this.getGPUTextures = options.getGPUTextures || null;
|
|
212
|
-
// refreshInput: () => void — re-renders the compositor so the WebGPU canvas is readable
|
|
213
|
-
this.refreshInput = options.refreshInput || null;
|
|
214
|
-
this.getExposure = options.getExposure || ( () => 1.0 );
|
|
215
|
-
this.getToneMapping = options.getToneMapping || ( () => ACESFilmicToneMapping );
|
|
216
|
-
this.getSaturation = options.getSaturation || ( () => 1.0 );
|
|
217
|
-
this.getTransparentBackground = options.getTransparentBackground || ( () => false );
|
|
218
|
-
this.isGPUMode = !! this.backendParamsGetter;
|
|
219
205
|
this.gpuDevice = null;
|
|
220
206
|
|
|
221
207
|
// Cached GPU storage buffers for texture→buffer copies (reused across denoise calls)
|
|
@@ -238,7 +224,13 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
238
224
|
|
|
239
225
|
this._colorScalePipeline = null;
|
|
240
226
|
this._colorScaleBindGroup = null;
|
|
241
|
-
|
|
227
|
+
|
|
228
|
+
// The denoised picture, written on the card and handed to the pipeline like any other
|
|
229
|
+
// stage's output. `_outTexture` wraps `_outGPUTexture` so three.js can sample it.
|
|
230
|
+
this._unpackPipeline = null;
|
|
231
|
+
this._outGPUTexture = null;
|
|
232
|
+
this._outTexture = null;
|
|
233
|
+
this._outTexSize = { width: 0, height: 0 };
|
|
242
234
|
|
|
243
235
|
// Merge options with defaults
|
|
244
236
|
this.config = { ...MODEL_CONFIG.DEFAULT_OPTIONS, ...options };
|
|
@@ -257,17 +249,15 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
257
249
|
abortController: null
|
|
258
250
|
};
|
|
259
251
|
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this._pendingTileBlits = [];
|
|
252
|
+
// Tiles written into the output picture by the run in flight; zero means the library gave
|
|
253
|
+
// no per-tile progress and the whole image has to be written at the end.
|
|
254
|
+
this._tilesWritten = 0;
|
|
264
255
|
|
|
265
256
|
// Identifies the live run. A run superseded by abort() must not clear the state its
|
|
266
257
|
// successor already owns.
|
|
267
258
|
this._runId = 0;
|
|
268
|
-
// The output
|
|
269
|
-
|
|
270
|
-
this._hasLatchedFrame = false;
|
|
259
|
+
// The output picture holds a denoised frame the pipeline can show.
|
|
260
|
+
this._hasOutput = false;
|
|
271
261
|
// Wall time of the last completed denoise, for cadence policy. 0 until one finishes.
|
|
272
262
|
this.lastDenoiseMs = 0;
|
|
273
263
|
|
|
@@ -294,7 +284,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
294
284
|
|
|
295
285
|
try {
|
|
296
286
|
|
|
297
|
-
this._setupCanvas();
|
|
298
287
|
await this._setupUNetDenoiser();
|
|
299
288
|
|
|
300
289
|
} catch ( error ) {
|
|
@@ -305,37 +294,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
305
294
|
|
|
306
295
|
}
|
|
307
296
|
|
|
308
|
-
_setupCanvas() {
|
|
309
|
-
|
|
310
|
-
if ( ! this.output.getContext ) {
|
|
311
|
-
|
|
312
|
-
throw new Error( 'Output must be a valid Canvas element' );
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Configure canvas for optimal performance
|
|
317
|
-
this.output.willReadFrequently = true;
|
|
318
|
-
this.output.width = this.input.width;
|
|
319
|
-
this.output.height = this.input.height;
|
|
320
|
-
|
|
321
|
-
// Apply styling efficiently
|
|
322
|
-
Object.assign( this.output.style, {
|
|
323
|
-
position: 'absolute',
|
|
324
|
-
top: '0',
|
|
325
|
-
left: '0',
|
|
326
|
-
width: '100%',
|
|
327
|
-
height: '100%',
|
|
328
|
-
borderRadius: '5px',
|
|
329
|
-
background: "repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%) 50% / 20px 20px"
|
|
330
|
-
} );
|
|
331
|
-
|
|
332
|
-
this.ctx = this.output.getContext( '2d', {
|
|
333
|
-
willReadFrequently: true,
|
|
334
|
-
alpha: true
|
|
335
|
-
} );
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
297
|
/**
|
|
340
298
|
* Whether the tier's model expects a clean (accumulated) auxiliary guide rather than a
|
|
341
299
|
* point-sampled one. The aux MRT has to be fed to match, or the guide fights the weights.
|
|
@@ -387,9 +345,20 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
387
345
|
*/
|
|
388
346
|
_resolveTileSize() {
|
|
389
347
|
|
|
390
|
-
const longest = Math.max( this.
|
|
348
|
+
const longest = Math.max( this._renderWidth, this._renderHeight );
|
|
391
349
|
if ( ! longest ) return this.maxTileSize;
|
|
392
|
-
|
|
350
|
+
|
|
351
|
+
// Rounded up to the model's 16-pixel alignment, not clamped to the exact frame. The
|
|
352
|
+
// library's own fitTileDimension aligns the size up and then clamps it back to whatever
|
|
353
|
+
// cap it is handed, so handing it the raw frame size hands it an unaligned tile — and an
|
|
354
|
+
// unaligned tile measured 4x the cost of the whole denoise: 336 ms at 900x900 against
|
|
355
|
+
// 86 ms once aligned, with 896x896 unaffected either way.
|
|
356
|
+
const aligned = Math.ceil( longest / OIDN_TILE_ALIGNMENT ) * OIDN_TILE_ALIGNMENT;
|
|
357
|
+
if ( aligned <= this.maxTileSize ) return aligned;
|
|
358
|
+
|
|
359
|
+
// The cap has to be aligned too, or a cap somebody picked by hand lands on the same slow
|
|
360
|
+
// path. Rounded down, so it never rises above what the caller asked for.
|
|
361
|
+
return Math.max( OIDN_TILE_ALIGNMENT, Math.floor( this.maxTileSize / OIDN_TILE_ALIGNMENT ) * OIDN_TILE_ALIGNMENT );
|
|
393
362
|
|
|
394
363
|
}
|
|
395
364
|
|
|
@@ -462,7 +431,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
462
431
|
// GPU-native path: share the existing GPUDevice so oidn-web uses the
|
|
463
432
|
// same device as the renderer — no second device, no CPU roundtrip for inputs.
|
|
464
433
|
let backendParams;
|
|
465
|
-
if ( this.
|
|
434
|
+
if ( this.backendParamsGetter ) {
|
|
466
435
|
|
|
467
436
|
const params = this.backendParamsGetter();
|
|
468
437
|
this.gpuDevice = params?.device ?? null;
|
|
@@ -569,9 +538,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
569
538
|
|
|
570
539
|
if ( success ) {
|
|
571
540
|
|
|
572
|
-
this._hasLatchedFrame = true;
|
|
573
541
|
this.renderer?.resetState?.();
|
|
574
|
-
this.input.style.opacity = '0';
|
|
575
542
|
|
|
576
543
|
this.lastDenoiseMs = performance.now() - startTime;
|
|
577
544
|
log.debug( `denoise complete in ${fmt.ms( this.lastDenoiseMs )} · quality ${this.quality}` );
|
|
@@ -582,15 +549,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
582
549
|
|
|
583
550
|
}
|
|
584
551
|
|
|
585
|
-
// Revealing before the first paint shows the output's transparent bitmap — and its
|
|
586
|
-
// checker CSS background — over the now-hidden render canvas.
|
|
587
|
-
_revealOutput() {
|
|
588
|
-
|
|
589
|
-
this.output.style.display = 'block';
|
|
590
|
-
this.input.style.opacity = '0';
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
552
|
async execute( continuous = false ) {
|
|
595
553
|
|
|
596
554
|
if ( ! this.enabled || ! this.unet ) return false;
|
|
@@ -602,8 +560,9 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
602
560
|
|
|
603
561
|
try {
|
|
604
562
|
|
|
605
|
-
|
|
606
|
-
|
|
563
|
+
// The GPU path bails out early — no textures yet, no device — by returning false, and
|
|
564
|
+
// a caller that read that as success published a picture nothing had written to.
|
|
565
|
+
return await this._executeUNetGPU( continuous ) !== false;
|
|
607
566
|
|
|
608
567
|
} catch ( error ) {
|
|
609
568
|
|
|
@@ -617,8 +576,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
617
576
|
|
|
618
577
|
}
|
|
619
578
|
|
|
620
|
-
// Restore original rendering on error, unless a previous denoise is still on screen.
|
|
621
|
-
if ( ! this._hasLatchedFrame ) this.input.style.opacity = '1';
|
|
622
579
|
return false;
|
|
623
580
|
|
|
624
581
|
} finally {
|
|
@@ -638,12 +595,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
638
595
|
|
|
639
596
|
}
|
|
640
597
|
|
|
641
|
-
async _executeUNet( continuous = false ) {
|
|
642
|
-
|
|
643
|
-
return this._executeUNetGPU( continuous );
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
598
|
/**
|
|
648
599
|
* GPU-native execution path. Copies render target textures into GPU storage buffers
|
|
649
600
|
* via copyTextureToBuffer (GPU-only, no CPU roundtrip), then passes those buffers to
|
|
@@ -653,7 +604,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
653
604
|
*/
|
|
654
605
|
async _executeUNetGPU( continuous = false ) {
|
|
655
606
|
|
|
656
|
-
const
|
|
607
|
+
const width = this._renderWidth;
|
|
608
|
+
const height = this._renderHeight;
|
|
657
609
|
|
|
658
610
|
if ( ! this.getGPUTextures ) {
|
|
659
611
|
|
|
@@ -678,19 +630,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
678
630
|
|
|
679
631
|
}
|
|
680
632
|
|
|
681
|
-
// Capture the base now: the readback below awaits, and a presented WebGPU canvas only
|
|
682
|
-
// reads back non-empty straight after a compositor pass — hence the refresh. Skipped
|
|
683
|
-
// once a denoised frame is latched: repainting it on every cadence tick would flash
|
|
684
|
-
// the noisy image back over an already-clean one.
|
|
685
|
-
if ( ! this._hasLatchedFrame ) {
|
|
686
|
-
|
|
687
|
-
this.refreshInput?.();
|
|
688
|
-
this.ctx.drawImage( this.input, 0, 0, width, height );
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
this._revealOutput();
|
|
693
|
-
|
|
694
633
|
// Ensure storage buffers are sized correctly (recreate on resolution change)
|
|
695
634
|
this._ensureGPUInputBuffers( width, height );
|
|
696
635
|
|
|
@@ -874,13 +813,13 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
874
813
|
|
|
875
814
|
}
|
|
876
815
|
|
|
877
|
-
if ( ! this.
|
|
816
|
+
if ( ! this._unpackPipeline ) {
|
|
878
817
|
|
|
879
|
-
this.
|
|
880
|
-
label: 'oidn-output-
|
|
818
|
+
this._unpackPipeline = device.createComputePipeline( {
|
|
819
|
+
label: 'oidn-output-unpack',
|
|
881
820
|
layout: 'auto',
|
|
882
821
|
compute: {
|
|
883
|
-
module: device.createShaderModule( { label: 'oidn-output-
|
|
822
|
+
module: device.createShaderModule( { label: 'oidn-output-unpack', code: UNPACK_WGSL } ),
|
|
884
823
|
entryPoint: 'main'
|
|
885
824
|
}
|
|
886
825
|
} );
|
|
@@ -994,115 +933,135 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
994
933
|
}
|
|
995
934
|
|
|
996
935
|
/**
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
*
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
936
|
+
* The picture the pipeline samples, or null until a denoise has produced one. Owned here and
|
|
937
|
+
* replaced on a resize, so callers re-read it rather than holding on to it.
|
|
938
|
+
* @returns {?import('three').Texture}
|
|
939
|
+
*/
|
|
940
|
+
get outputTexture() {
|
|
941
|
+
|
|
942
|
+
return this._outTexture;
|
|
943
|
+
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Makes sure the output picture exists at the render size. A resize makes a new one rather than
|
|
948
|
+
* resizing in place: three.js caches the card-side handle against the wrapper, so the wrapper
|
|
949
|
+
* has to be new too.
|
|
950
|
+
*/
|
|
951
|
+
_ensureOutputTexture( device, width, height ) {
|
|
952
|
+
|
|
953
|
+
if ( this._outGPUTexture && this._outTexSize.width === width && this._outTexSize.height === height ) return;
|
|
954
|
+
|
|
955
|
+
this._releaseOutputTexture();
|
|
956
|
+
|
|
957
|
+
// Half float, like NRD's output: this picture is read once, by the display. Full float
|
|
958
|
+
// would double both the memory it holds and the bandwidth of writing and sampling it, to
|
|
959
|
+
// carry precision the tone curve and an 8-bit canvas immediately throw away. Measured
|
|
960
|
+
// identical to within 0.02/255 of the mean.
|
|
961
|
+
this._outGPUTexture = device.createTexture( {
|
|
962
|
+
label: 'oidn-output',
|
|
963
|
+
size: { width, height, depthOrArrayLayers: 1 },
|
|
964
|
+
format: 'rgba16float',
|
|
965
|
+
usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
|
|
966
|
+
} );
|
|
967
|
+
|
|
968
|
+
const tex = new ExternalTexture( this._outGPUTexture );
|
|
969
|
+
tex.name = 'oidn:output';
|
|
970
|
+
tex.image = { width, height, depth: 1 };
|
|
971
|
+
tex.type = HalfFloatType;
|
|
972
|
+
tex.format = RGBAFormat;
|
|
973
|
+
tex.colorSpace = NoColorSpace;
|
|
974
|
+
tex.minFilter = NearestFilter;
|
|
975
|
+
tex.magFilter = NearestFilter;
|
|
976
|
+
tex.wrapS = ClampToEdgeWrapping;
|
|
977
|
+
tex.wrapT = ClampToEdgeWrapping;
|
|
978
|
+
tex.generateMipmaps = false;
|
|
979
|
+
tex.flipY = false;
|
|
980
|
+
|
|
981
|
+
this._outTexture = tex;
|
|
982
|
+
this._outTexSize = { width, height };
|
|
983
|
+
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
_releaseOutputTexture() {
|
|
987
|
+
|
|
988
|
+
this._outTexture?.dispose();
|
|
989
|
+
this._outGPUTexture?.destroy();
|
|
990
|
+
this._outTexture = null;
|
|
991
|
+
this._outGPUTexture = null;
|
|
992
|
+
this._outTexSize = { width: 0, height: 0 };
|
|
993
|
+
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Writes a rectangle of the denoised buffer into the output picture, on the card.
|
|
1003
998
|
*
|
|
1004
999
|
* @param {GPUBuffer} src - denoised rgba32float, full image
|
|
1005
1000
|
* @param {number} srcWidth
|
|
1006
1001
|
* @param {{x: number, y: number, width: number, height: number}} rect
|
|
1007
|
-
* @returns {Promise<void>}
|
|
1008
1002
|
*/
|
|
1009
|
-
|
|
1003
|
+
_unpackToTexture( src, srcWidth, rect ) {
|
|
1010
1004
|
|
|
1011
1005
|
const device = this.gpuDevice;
|
|
1012
1006
|
if ( ! device || ! this._ensureScalePipelines( device ) ) return;
|
|
1013
1007
|
|
|
1014
1008
|
const { x, y, width, height } = rect;
|
|
1015
|
-
const byteSize = width * height * 4;
|
|
1016
1009
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
1021
|
-
} );
|
|
1022
|
-
const staging = device.createBuffer( {
|
|
1023
|
-
label: 'oidn-pack-staging',
|
|
1024
|
-
size: byteSize,
|
|
1025
|
-
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
1026
|
-
} );
|
|
1010
|
+
this._ensureOutputTexture( device, this._renderWidth, this._renderHeight );
|
|
1011
|
+
if ( ! this._outGPUTexture ) return;
|
|
1012
|
+
|
|
1027
1013
|
const params = device.createBuffer( {
|
|
1028
|
-
label: 'oidn-
|
|
1029
|
-
size:
|
|
1014
|
+
label: 'oidn-unpack-params',
|
|
1015
|
+
size: UNPACK_PARAMS_BYTES,
|
|
1030
1016
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
1031
1017
|
} );
|
|
1032
1018
|
|
|
1033
|
-
this._pendingStagingBuffers.add( dst );
|
|
1034
|
-
this._pendingStagingBuffers.add( staging );
|
|
1035
|
-
this._pendingStagingBuffers.add( params );
|
|
1036
|
-
|
|
1037
|
-
const release = () => {
|
|
1038
|
-
|
|
1039
|
-
for ( const b of [ dst, staging, params ] ) {
|
|
1040
|
-
|
|
1041
|
-
b.destroy();
|
|
1042
|
-
this._pendingStagingBuffers.delete( b );
|
|
1043
|
-
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
};
|
|
1047
|
-
|
|
1048
1019
|
try {
|
|
1049
1020
|
|
|
1050
|
-
|
|
1051
|
-
new Uint32Array( words, 0, 8 ).set( [
|
|
1052
|
-
srcWidth, x, y, width, height,
|
|
1053
|
-
toneMapMode( this.getToneMapping() ),
|
|
1054
|
-
this.getTransparentBackground() ? 1 : 0,
|
|
1055
|
-
0
|
|
1056
|
-
] );
|
|
1057
|
-
new Float32Array( words, 32, 2 ).set( [
|
|
1058
|
-
effectiveExposure( this.getExposure(), this.getToneMapping() ),
|
|
1059
|
-
this.getSaturation()
|
|
1060
|
-
] );
|
|
1061
|
-
device.queue.writeBuffer( params, 0, words );
|
|
1021
|
+
device.queue.writeBuffer( params, 0, new Uint32Array( [ srcWidth, x, y, width, height, 0, 0, 0 ] ) );
|
|
1062
1022
|
|
|
1063
1023
|
const bindGroup = device.createBindGroup( {
|
|
1064
|
-
label: 'oidn-output-
|
|
1065
|
-
layout: this.
|
|
1024
|
+
label: 'oidn-output-unpack',
|
|
1025
|
+
layout: this._unpackPipeline.getBindGroupLayout( 0 ),
|
|
1066
1026
|
entries: [
|
|
1067
1027
|
{ binding: 0, resource: { buffer: src } },
|
|
1068
|
-
{ binding: 1, resource: { buffer:
|
|
1069
|
-
{ binding: 2, resource: { buffer:
|
|
1070
|
-
{ binding: 3, resource: { buffer: this.
|
|
1071
|
-
{ binding: 4, resource:
|
|
1028
|
+
{ binding: 1, resource: { buffer: params } },
|
|
1029
|
+
{ binding: 2, resource: { buffer: this._inputScaleBuffer } },
|
|
1030
|
+
{ binding: 3, resource: { buffer: this._gpuInputBuffers.color } },
|
|
1031
|
+
{ binding: 4, resource: this._outGPUTexture.createView() }
|
|
1072
1032
|
]
|
|
1073
1033
|
} );
|
|
1074
1034
|
|
|
1075
|
-
const encoder = device.createCommandEncoder( { label: 'oidn-output-
|
|
1076
|
-
const pass = encoder.beginComputePass( { label: 'oidn-output-
|
|
1077
|
-
pass.setPipeline( this.
|
|
1035
|
+
const encoder = device.createCommandEncoder( { label: 'oidn-output-unpack' } );
|
|
1036
|
+
const pass = encoder.beginComputePass( { label: 'oidn-output-unpack' } );
|
|
1037
|
+
pass.setPipeline( this._unpackPipeline );
|
|
1078
1038
|
pass.setBindGroup( 0, bindGroup );
|
|
1079
1039
|
pass.dispatchWorkgroups(
|
|
1080
|
-
Math.ceil( width /
|
|
1081
|
-
Math.ceil( height /
|
|
1040
|
+
Math.ceil( width / UNPACK_WG_SIZE ),
|
|
1041
|
+
Math.ceil( height / UNPACK_WG_SIZE ),
|
|
1082
1042
|
1
|
|
1083
1043
|
);
|
|
1084
1044
|
pass.end();
|
|
1085
|
-
encoder.copyBufferToBuffer( dst, 0, staging, 0, byteSize );
|
|
1086
1045
|
device.queue.submit( [ encoder.finish() ] );
|
|
1087
1046
|
|
|
1088
|
-
|
|
1089
|
-
//
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
this.ctx.putImageData( new ImageData( bytes, width, height ), x, y );
|
|
1047
|
+
// Set here rather than when the run resolves: 'end' is dispatched from execute()'s
|
|
1048
|
+
// finally, which runs first, so a listener that waits for the run would see the very
|
|
1049
|
+
// first denoise as having produced nothing.
|
|
1050
|
+
this._hasOutput = true;
|
|
1094
1051
|
|
|
1095
1052
|
} finally {
|
|
1096
1053
|
|
|
1097
|
-
|
|
1054
|
+
params.destroy();
|
|
1098
1055
|
|
|
1099
1056
|
}
|
|
1100
1057
|
|
|
1101
1058
|
}
|
|
1102
1059
|
|
|
1060
|
+
|
|
1103
1061
|
/**
|
|
1104
|
-
* Promise wrapper around tileExecute for the GPU path.
|
|
1105
|
-
*
|
|
1062
|
+
* Promise wrapper around tileExecute for the GPU path. Each tile is written straight into the
|
|
1063
|
+
* output picture on the card, so there is nothing to await between a tile finishing and it
|
|
1064
|
+
* being visible.
|
|
1106
1065
|
*/
|
|
1107
1066
|
_executeWithAbortGPU( config, continuous = false ) {
|
|
1108
1067
|
|
|
@@ -1118,7 +1077,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1118
1077
|
let abortDenoise = null;
|
|
1119
1078
|
|
|
1120
1079
|
// Fresh per-run list of tile-blit promises (the progress callback appends to it).
|
|
1121
|
-
this.
|
|
1080
|
+
this._tilesWritten = 0;
|
|
1122
1081
|
|
|
1123
1082
|
const abortHandler = () => {
|
|
1124
1083
|
|
|
@@ -1144,23 +1103,12 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1144
1103
|
|
|
1145
1104
|
try {
|
|
1146
1105
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
// the same exposure/saturation/tonemap/sRGB math the full-frame readback uses (verified:
|
|
1151
|
-
// tiles tile the image exactly, no overlap). Just wait for those blits — no redundant
|
|
1152
|
-
// full-frame re-read + re-tonemap.
|
|
1153
|
-
await Promise.allSettled( this._pendingTileBlits );
|
|
1154
|
-
|
|
1155
|
-
} else {
|
|
1106
|
+
// Nothing to wait for on the normal path: the progress callback wrote every
|
|
1107
|
+
// tile straight into the output picture, and tiles cover the image exactly.
|
|
1108
|
+
if ( this._tilesWritten === 0 ) this._displayGPUOutput( output );
|
|
1156
1109
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// DENOISING_END (which gates screenshot/video capture) fires only after this resolves,
|
|
1163
|
-
// so the captured canvas is always complete.
|
|
1110
|
+
// DENOISING_END (which gates screenshot/video capture) fires only after this
|
|
1111
|
+
// resolves, so what a capture reads is always a complete picture.
|
|
1164
1112
|
resolve();
|
|
1165
1113
|
|
|
1166
1114
|
} catch ( err ) {
|
|
@@ -1187,20 +1135,16 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1187
1135
|
|
|
1188
1136
|
const rect = { x: tile.x, y: tile.y, width, height };
|
|
1189
1137
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
this.dispatchEvent( {
|
|
1194
|
-
type: 'tileProgress',
|
|
1195
|
-
tile: rect,
|
|
1196
|
-
imageWidth: fullWidth,
|
|
1197
|
-
imageHeight: fullHeight,
|
|
1198
|
-
continuous
|
|
1199
|
-
} );
|
|
1138
|
+
this._unpackToTexture( outputData.data, fullWidth, rect );
|
|
1139
|
+
this._tilesWritten ++;
|
|
1200
1140
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1141
|
+
this.dispatchEvent( {
|
|
1142
|
+
type: 'tileProgress',
|
|
1143
|
+
tile: rect,
|
|
1144
|
+
imageWidth: fullWidth,
|
|
1145
|
+
imageHeight: fullHeight,
|
|
1146
|
+
continuous
|
|
1147
|
+
} );
|
|
1204
1148
|
|
|
1205
1149
|
}
|
|
1206
1150
|
} );
|
|
@@ -1213,16 +1157,16 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1213
1157
|
* Degenerate fallback when no per-tile progress was emitted: one authoritative full paint.
|
|
1214
1158
|
* @param {{ data: GPUBuffer, width: number, height: number }} output
|
|
1215
1159
|
*/
|
|
1216
|
-
|
|
1160
|
+
_displayGPUOutput( { data: gpuBuffer, width, height } ) {
|
|
1217
1161
|
|
|
1218
1162
|
if ( ! this.gpuDevice ) {
|
|
1219
1163
|
|
|
1220
|
-
log.error( 'gpuDevice not available for output
|
|
1164
|
+
log.error( 'gpuDevice not available for the output picture' );
|
|
1221
1165
|
return;
|
|
1222
1166
|
|
|
1223
1167
|
}
|
|
1224
1168
|
|
|
1225
|
-
|
|
1169
|
+
this._unpackToTexture( gpuBuffer, width, { x: 0, y: 0, width, height } );
|
|
1226
1170
|
|
|
1227
1171
|
}
|
|
1228
1172
|
|
|
@@ -1237,12 +1181,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1237
1181
|
// Signal abort to current operation
|
|
1238
1182
|
this.state.abortController?.abort();
|
|
1239
1183
|
|
|
1240
|
-
// Destroy any in-flight tile staging buffers that mapAsync won't resolve
|
|
1241
|
-
this._destroyPendingStagingBuffers();
|
|
1242
|
-
|
|
1243
|
-
// Restore input visibility
|
|
1244
|
-
this.input.style.opacity = '1';
|
|
1245
|
-
|
|
1246
1184
|
// No 'end' here: the aborted execute()'s finally always runs and dispatches one, tagged
|
|
1247
1185
|
// with that run's `continuous`. Dispatching here too emitted a second, untagged end —
|
|
1248
1186
|
// a cancelled cadence run announcing itself as the denoise that finishes a render.
|
|
@@ -1252,10 +1190,18 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1252
1190
|
|
|
1253
1191
|
}
|
|
1254
1192
|
|
|
1255
|
-
//
|
|
1256
|
-
|
|
1193
|
+
// Whether the output picture holds a denoised frame the pipeline can show.
|
|
1194
|
+
get hasOutput() {
|
|
1257
1195
|
|
|
1258
|
-
this.
|
|
1196
|
+
return this._hasOutput;
|
|
1197
|
+
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Call when the picture stops describing anything the viewer should see — a scene change, or a
|
|
1201
|
+
// resize. The picture itself is kept and overwritten by the next denoise.
|
|
1202
|
+
invalidateOutput() {
|
|
1203
|
+
|
|
1204
|
+
this._hasOutput = false;
|
|
1259
1205
|
|
|
1260
1206
|
}
|
|
1261
1207
|
|
|
@@ -1267,9 +1213,11 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1267
1213
|
|
|
1268
1214
|
}
|
|
1269
1215
|
|
|
1270
|
-
this.
|
|
1271
|
-
this.
|
|
1272
|
-
this.
|
|
1216
|
+
this._renderWidth = width;
|
|
1217
|
+
this._renderHeight = height;
|
|
1218
|
+
this._hasOutput = false;
|
|
1219
|
+
// The old measurement describes the old size, and callers size their policy on it.
|
|
1220
|
+
this.lastDenoiseMs = 0;
|
|
1273
1221
|
|
|
1274
1222
|
// Reinitialize denoiser if tile size changes relative to image size
|
|
1275
1223
|
this._setupUNetDenoiser().catch( error => {
|
|
@@ -1280,45 +1228,23 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1280
1228
|
|
|
1281
1229
|
}
|
|
1282
1230
|
|
|
1283
|
-
_destroyPendingStagingBuffers() {
|
|
1284
|
-
|
|
1285
|
-
for ( const buf of this._pendingStagingBuffers ) {
|
|
1286
|
-
|
|
1287
|
-
buf.destroy();
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
this._pendingStagingBuffers.clear();
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
1231
|
dispose() {
|
|
1296
1232
|
|
|
1297
1233
|
// Abort any ongoing operations
|
|
1298
1234
|
this.abort();
|
|
1299
1235
|
|
|
1300
|
-
// Destroy any remaining staging buffers
|
|
1301
|
-
this._destroyPendingStagingBuffers();
|
|
1302
|
-
|
|
1303
1236
|
// Dispose resources
|
|
1304
1237
|
this.unet?.dispose();
|
|
1305
1238
|
this._destroyGPUInputBuffers();
|
|
1239
|
+
this._releaseOutputTexture();
|
|
1306
1240
|
this._colorScalePipeline = null;
|
|
1307
1241
|
this._lumReducePipeline = null;
|
|
1308
1242
|
this._lumFinalizePipeline = null;
|
|
1309
1243
|
this._lumLayout = null;
|
|
1310
|
-
this.
|
|
1311
|
-
|
|
1312
|
-
// Clean up DOM
|
|
1313
|
-
if ( this.output?.parentNode ) {
|
|
1314
|
-
|
|
1315
|
-
this.output.remove();
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1244
|
+
this._unpackPipeline = null;
|
|
1318
1245
|
|
|
1319
1246
|
// Clear references
|
|
1320
1247
|
this.unet = null;
|
|
1321
|
-
this.ctx = null;
|
|
1322
1248
|
this.state.abortController = null;
|
|
1323
1249
|
|
|
1324
1250
|
// Remove all event listeners
|