rayzee 8.3.0 → 8.4.1

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.
@@ -0,0 +1,530 @@
1
+ /**
2
+ * DLSS-NR — the neural-rendering (detail) pass.
3
+ *
4
+ * Runs over a finished, denoised image and adjusts its appearance: local tone and structure, and a
5
+ * skin-specific term. It does not change resolution and it does not denoise. Measured on path-traced
6
+ * input it is close to a no-op after OIDN, so it is off by default and exposed as an explicit choice.
7
+ *
8
+ * ⚠️ It is always LAST in the neural chain: its output is `rgba8unorm`, i.e. display-referred 8-bit,
9
+ * so nothing downstream can work with it in linear light.
10
+ *
11
+ * Its result IS recoverable, though not the obvious way. The texture has no `COPY_SRC`, and its own
12
+ * canvas is a WebGPU surface that reads back empty at every timing tried (immediately, after one
13
+ * rAF, after two) — so neither `copyTextureToBuffer` nor `drawImage` works. It does carry
14
+ * `TEXTURE_BINDING`, so a small compute pass of our own samples it into a storage buffer instead.
15
+ * That is what makes the pass saveable, and lets it share the engine's one 2D overlay rather than
16
+ * needing a second canvas whose context type conflicts.
17
+ *
18
+ * ⚠️ **It desaturates, and there is no lever for it.** Driven at `intensity: 0`, where the network
19
+ * returns its input untouched, the result still differs from the engine's own render: luminance is
20
+ * nearly exact (94.4 vs 94.0) but chroma is ~10 % low (saturation 0.313 vs 0.348). So the pass
21
+ * reproduces brightness faithfully and loses colour, inside its own preprocessing — its presentation
22
+ * shader is a plain `textureSample` blit with no display controls, and the `override ACES /
23
+ * SATURATION` block in the bundle belongs to the SUPER-RESOLUTION presenter, not this one.
24
+ *
25
+ * Pre-tone-mapping the input to compensate was tried and measured WORSE — passthrough error doubled
26
+ * (RMSE 5.1 -> 10.8) and the image came out over-saturated, because the network then applies its own
27
+ * handling on top. Feeding it scene-referred linear, as here, is both the model's intended domain and
28
+ * the more faithful of the two. A real fix would have to reach inside the vendored runtime.
29
+ */
30
+
31
+ import { PackedToneMapper } from '../Processor/ToneMapGPU.js';
32
+ import { createLogger } from '../utils/Logger.js';
33
+ import { getAssetConfig } from '../AssetConfig.js';
34
+
35
+ /**
36
+ * The model's own settings contract. Every field is numeric and validated hard by the runtime —
37
+ * passing a string throws `"<name> must be finite"`.
38
+ */
39
+ export const DLSS_NR_DEFAULTS = Object.freeze( {
40
+ enabled: true,
41
+ intensity: 1,
42
+ localTone: 1,
43
+ localStructure: 1,
44
+ skinStructure: - 1,
45
+ autoMask: true,
46
+ style: 0,
47
+ preset: 0,
48
+ uiCorrection: false,
49
+ } );
50
+
51
+ /**
52
+ * How much of the network's own colour to take, 0..1. Ours, not the runtime's.
53
+ *
54
+ * ⚠️ It must be kept OUT of the object handed to the runtime: that one is validated by key and
55
+ * throws `Unknown DLSS-NR setting: colorStrength`. `splitSettings` does the separating.
56
+ *
57
+ * It is `color_strength` in the composition kernel, blending between the original chroma relit to
58
+ * the network's luminance (`original * ratio`, at 0) and the network's full colour (at 1). At 0 the
59
+ * chroma is the renderer's own, exactly, so the pass contributes structure and light without
60
+ * regrading.
61
+ *
62
+ * **Defaults to 0**, which is a change: the pass used to desaturate and there was thought to be no
63
+ * lever. Measured on 24155522.glb at 2048 output, mean saturation against an upscale-only render:
64
+ * colour 1 gives -6.1 %, 0.5 gives -5.1 %, **0 gives -1.1 %** — while luminance (+0.9 %) and detail
65
+ * are the same at all three, so nothing the pass is actually for is given up. The renderer's chroma
66
+ * is ground truth here; the network's is a guess about a photograph.
67
+ */
68
+ export const DLSS_NR_COLOR_STRENGTH = 0;
69
+
70
+ /** @returns {{runtime: object, colorStrength: number}} */
71
+ function splitSettings( settings = {} ) {
72
+
73
+ const { colorStrength = DLSS_NR_COLOR_STRENGTH, ...rest } = settings;
74
+ return { runtime: { ...DLSS_NR_DEFAULTS, ...rest }, colorStrength };
75
+
76
+ }
77
+
78
+ /**
79
+ * Largest image the pass survives — 4.19 MP, i.e. 2048x2048.
80
+ *
81
+ * Measured on an M5 Pro, 24155522.glb, warm, end to end with super resolution:
82
+ *
83
+ * | image | pixels | wall | outcome |
84
+ * |---|---|---|---|
85
+ * | 1536² | 2.4 MP | 5.3 s | fine |
86
+ * | 2048² | 4.2 MP | 6.2 s, repeatable | fine |
87
+ * | 4096² | 16.8 MP | 65-108 s | **kills Chrome's GPU process** |
88
+ *
89
+ * Four times the pixels costs ten to seventeen times the wall clock and then dies: every device in
90
+ * the page is lost at once with "A valid external Instance reference no longer exists", which takes
91
+ * the renderer and the finished render with it.
92
+ *
93
+ * ⚠️ The pass runs **before** super resolution precisely so this stays out of reach. On the traced
94
+ * image it sees a quarter of the pixels it would after a 2x upscale, and the engine's own render
95
+ * reserve (`MAX_STORAGE_TEXTURE_SIZE`, 2048) already caps that at exactly 4.19 MP. The check below
96
+ * is therefore unreachable today and exists as a floor under a raised reserve.
97
+ */
98
+ const log = createLogger( 'dlss' );
99
+
100
+ export const DLSS_NR_MAX_PIXELS = 2048 * 2048;
101
+
102
+ /** Ranges the runtime clamps to. `skinStructure: -1` means "follow localStructure". */
103
+ export const DLSS_NR_RANGES = Object.freeze( {
104
+ intensity: [ 0, 1 ],
105
+ localTone: [ 0, 2 ],
106
+ localStructure: [ 0, 2 ],
107
+ skinStructure: [ - 1, 2 ],
108
+ style: [ 0, 2 ],
109
+ preset: [ 0, 3 ],
110
+ } );
111
+
112
+ let _runtimePromise = null;
113
+
114
+ /**
115
+ * The runtime is a plain script that installs `globalThis.DLSSRuntime`, not a module, so it has to
116
+ * be injected rather than imported. Cached: a second tag would re-run a 1 MB bundle.
117
+ */
118
+ function loadRuntime() {
119
+
120
+ if ( _runtimePromise ) return _runtimePromise;
121
+
122
+ const { dlssRuntimeUrl, dlssAssetBaseUrl } = getAssetConfig();
123
+ globalThis.__DLSS5_ASSET_BASE__ = dlssAssetBaseUrl;
124
+
125
+ _runtimePromise = new Promise( ( resolve, reject ) => {
126
+
127
+ if ( globalThis.DLSSRuntime?.getSrNrChain ) return resolve( globalThis.DLSSRuntime.getSrNrChain() );
128
+
129
+ const el = document.createElement( 'script' );
130
+ el.src = dlssRuntimeUrl;
131
+ el.onload = () => {
132
+
133
+ const factory = globalThis.DLSSRuntime?.getSrNrChain;
134
+ if ( ! factory ) return reject( new Error( 'DLSS runtime loaded but exposed no neural-rendering entry' ) );
135
+ resolve( factory() );
136
+
137
+ };
138
+
139
+ el.onerror = () => reject( new Error( `Failed to load the DLSS runtime from ${dlssRuntimeUrl}` ) );
140
+ document.head.appendChild( el );
141
+
142
+ } );
143
+
144
+ return _runtimePromise;
145
+
146
+ }
147
+
148
+ const READBACK_WGSL = /* wgsl */ `
149
+ @group(0) @binding(0) var src: texture_2d<f32>;
150
+ @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
151
+
152
+ @compute @workgroup_size(8, 8)
153
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
154
+ let size = textureDimensions(src);
155
+ if ( gid.x >= size.x || gid.y >= size.y ) { return; }
156
+ let c = textureLoad(src, vec2<i32>(gid.xy), 0);
157
+ let r = u32(clamp(c.r, 0.0, 1.0) * 255.0 + 0.5);
158
+ let g = u32(clamp(c.g, 0.0, 1.0) * 255.0 + 0.5);
159
+ let b = u32(clamp(c.b, 0.0, 1.0) * 255.0 + 0.5);
160
+ dst[gid.y * size.x + gid.x] = r | (g << 8u) | (b << 16u) | (255u << 24u);
161
+ }
162
+ `;
163
+
164
+ /** Same pack, but keeping the light: tight rgba16float, which is what the upscaler consumes. */
165
+ const HDR_READBACK_WGSL = /* wgsl */ `
166
+ @group(0) @binding(0) var src: texture_2d<f32>;
167
+ @group(0) @binding(1) var<storage, read_write> dst: array<u32>;
168
+
169
+ @compute @workgroup_size(8, 8)
170
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
171
+ let size = textureDimensions(src);
172
+ if ( gid.x >= size.x || gid.y >= size.y ) { return; }
173
+ let c = textureLoad(src, vec2<i32>(gid.xy), 0);
174
+ let a = (gid.y * size.x + gid.x) * 2u;
175
+ dst[a] = pack2x16float(c.rg);
176
+ dst[a + 1u] = pack2x16float(vec2<f32>(c.b, 1.0));
177
+ }
178
+ `;
179
+
180
+ /**
181
+ * One loaded network, bound to one image size and one canvas.
182
+ *
183
+ * Holds a GPUDevice of its own (the runtime builds it) plus ~140 MB of weights, so it must be
184
+ * disposed rather than dropped.
185
+ */
186
+ export class DLSSNeural {
187
+
188
+ constructor( chain, canvas ) {
189
+
190
+ this.chain = chain;
191
+ this.canvas = canvas;
192
+ this.slot = 0;
193
+ this.source = null;
194
+ this.staging = null;
195
+ this._readbackPipeline = null;
196
+ this._readbackStorage = null;
197
+ this._readbackMap = null;
198
+ this._hdrPipeline = null;
199
+ this._hdrStorage = null;
200
+ this._hdrMap = null;
201
+ this._toneMapper = null;
202
+ this.disposed = false;
203
+
204
+ }
205
+
206
+ get device() {
207
+
208
+ return this.chain.device;
209
+
210
+ }
211
+
212
+ get width() {
213
+
214
+ return this.chain.pipeline.geometry.validWidth;
215
+
216
+ }
217
+
218
+ get height() {
219
+
220
+ return this.chain.pipeline.geometry.validHeight;
221
+
222
+ }
223
+
224
+ /**
225
+ * @param {object} opts
226
+ * @param {number} opts.width
227
+ * @param {number} opts.height
228
+ * @param {HTMLCanvasElement} [opts.canvas] surface the runtime presents to. Offscreen by
229
+ * default: the result is taken with `readOutput()`, not from this canvas, which cannot be read.
230
+ */
231
+ static async create( { width, height, canvas = null, settings = {}, onProgress = () => {} } ) {
232
+
233
+ if ( ! Number.isInteger( width ) || ! Number.isInteger( height ) || width < 1 || height < 1 ) {
234
+
235
+ throw new Error( 'DLSSNeural.create: width and height must be positive integers' );
236
+
237
+ }
238
+
239
+ if ( width * height > DLSS_NR_MAX_PIXELS ) {
240
+
241
+ throw new Error(
242
+ `DLSSNeural.create: ${width}x${height} is ${( width * height / 1e6 ).toFixed( 1 )} MP, above the ` +
243
+ `${( DLSS_NR_MAX_PIXELS / 1e6 ).toFixed( 1 )} MP this pass survives. Past it the run takes minutes ` +
244
+ 'and then loses every GPU device in the page.'
245
+ );
246
+
247
+ }
248
+
249
+ // The runtime insists on a canvas (it sizes it and takes a WebGPU context), but nothing reads
250
+ // it — so one is made here and kept off screen unless the caller supplies its own.
251
+ const surface = canvas ?? document.createElement( 'canvas' );
252
+ if ( ! canvas ) surface.style.cssText = 'position:fixed;left:-10000px;top:0;pointer-events:none';
253
+
254
+ const SrNrChain = await loadRuntime();
255
+ const chain = await SrNrChain.create(
256
+ surface, width, height, onProgress, splitSettings( settings ).runtime,
257
+ );
258
+
259
+ const instance = new DLSSNeural( chain, surface );
260
+ instance._allocate();
261
+ return instance;
262
+
263
+ }
264
+
265
+ _allocate() {
266
+
267
+ const { validWidth, validHeight } = this.chain.pipeline.geometry;
268
+
269
+ this.source = this.device.createBuffer( {
270
+ label: 'rayzee:dlss-nr-source',
271
+ size: validWidth * validHeight * 8,
272
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
273
+ } );
274
+
275
+ this.staging = new Uint16Array( validWidth * validHeight * 4 );
276
+
277
+ const bytes = validWidth * validHeight * 4;
278
+ this._readbackStorage = this.device.createBuffer( {
279
+ label: 'rayzee:dlss-nr-readback',
280
+ size: bytes,
281
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
282
+ } );
283
+ this._readbackMap = this.device.createBuffer( {
284
+ label: 'rayzee:dlss-nr-readback-map',
285
+ size: bytes,
286
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
287
+ } );
288
+ this._readbackPipeline = this.device.createComputePipeline( {
289
+ label: 'rayzee:dlss-nr-readback',
290
+ layout: 'auto',
291
+ compute: { module: this.device.createShaderModule( { code: READBACK_WGSL } ), entryPoint: 'main' },
292
+ } );
293
+
294
+ const hdrBytes = validWidth * validHeight * 8;
295
+ this._hdrStorage = this.device.createBuffer( {
296
+ label: 'rayzee:dlss-nr-hdr',
297
+ size: hdrBytes,
298
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
299
+ } );
300
+ this._hdrMap = this.device.createBuffer( {
301
+ label: 'rayzee:dlss-nr-hdr-map',
302
+ size: hdrBytes,
303
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
304
+ } );
305
+ this._hdrPipeline = this.device.createComputePipeline( {
306
+ label: 'rayzee:dlss-nr-hdr',
307
+ layout: 'auto',
308
+ compute: { module: this.device.createShaderModule( { code: HDR_READBACK_WGSL } ), entryPoint: 'main' },
309
+ } );
310
+
311
+ }
312
+
313
+ /** Runs the scene-referred texture into `_hdrStorage`, optionally copying it out for mapping. */
314
+ async _packHDR( w, h, forRead = false ) {
315
+
316
+ const texture = this.chain.pipeline.slots[ this.slot ]?.hdrOutput;
317
+ if ( ! texture ) {
318
+
319
+ throw new Error(
320
+ 'DLSSNeural: the runtime has no scene-referred output. Either no frame has been produced yet, '
321
+ + 'or dlss-runtime.js was re-vendored without the patch described in its PATCHES.md.'
322
+ );
323
+
324
+ }
325
+
326
+ const group = this.device.createBindGroup( {
327
+ layout: this._hdrPipeline.getBindGroupLayout( 0 ),
328
+ entries: [
329
+ { binding: 0, resource: texture.createView() },
330
+ { binding: 1, resource: { buffer: this._hdrStorage } },
331
+ ],
332
+ } );
333
+
334
+ const encoder = this.device.createCommandEncoder( { label: 'rayzee:dlss-nr-hdr' } );
335
+ const pass = encoder.beginComputePass();
336
+ pass.setPipeline( this._hdrPipeline );
337
+ pass.setBindGroup( 0, group );
338
+ pass.dispatchWorkgroups( Math.ceil( w / 8 ), Math.ceil( h / 8 ) );
339
+ pass.end();
340
+ if ( forRead ) encoder.copyBufferToBuffer( this._hdrStorage, 0, this._hdrMap, 0, w * h * 8 );
341
+ this.device.queue.submit( [ encoder.finish() ] );
342
+
343
+ }
344
+
345
+ /**
346
+ * The pass's result as scene-referred light, packed the way the upscaler wants it.
347
+ *
348
+ * Reads `hdrOutput`, the `rgba16float` texture the vendored runtime gained in
349
+ * `app/public/dlss/PATCHES.md` — the value one statement before its own 8-bit store, so nothing
350
+ * has been clamped, quantised or pushed through the model's hardcoded ACES curve yet.
351
+ *
352
+ * This is what lets the pass run FIRST, at render size, where it is ~10x cheaper than on an
353
+ * upscaled image and inside the size it survives.
354
+ *
355
+ * @returns {Promise<{half: Uint16Array, width: number, height: number}>}
356
+ */
357
+ async readOutputHDR() {
358
+
359
+ const { validWidth: w, validHeight: h } = this.chain.pipeline.geometry;
360
+ await this._packHDR( w, h, true );
361
+
362
+ await this._hdrMap.mapAsync( GPUMapMode.READ );
363
+ const half = new Uint16Array( this._hdrMap.getMappedRange().slice( 0 ) );
364
+ this._hdrMap.unmap();
365
+ return { half, width: w, height: h };
366
+
367
+ }
368
+
369
+ /**
370
+ * The pass's result as display bytes through the ENGINE's tone curve, for when nothing follows it.
371
+ *
372
+ * Deliberately not the runtime's own `rgba8unorm` output: that one is forced through a hardcoded
373
+ * ACES fit (`webgi_display` in the composition kernel), which is where the pass's ~10 %
374
+ * desaturation came from and why it never answered the engine's tone-mapping setting.
375
+ *
376
+ * @param {{exposure: number, toneMapping: number, saturation: number}} tone
377
+ * @returns {Promise<{rgba8: Uint8ClampedArray, width: number, height: number}>}
378
+ */
379
+ async readOutputToned( tone ) {
380
+
381
+ const { validWidth: w, validHeight: h } = this.chain.pipeline.geometry;
382
+ await this._packHDR( w, h );
383
+
384
+ this._toneMapper ??= new PackedToneMapper( this.device, 'rayzee:dlss-nr-tonemap' );
385
+ this._toneMapper.ensureSize( w, h );
386
+
387
+ const rgba8 = await this._toneMapper.toRGBA8( this._hdrStorage, {
388
+ exposure: tone.exposure ?? 1,
389
+ toneMapping: tone.toneMapping ?? 0,
390
+ saturation: tone.saturation ?? 1,
391
+ } );
392
+
393
+ return { rgba8, width: w, height: h };
394
+
395
+ }
396
+
397
+ /**
398
+ * Samples the pass's own output texture into ordinary RGBA bytes.
399
+ *
400
+ * @returns {Promise<Uint8ClampedArray>} RGBA, width*height*4
401
+ */
402
+ async readOutput() {
403
+
404
+ const { validWidth: w, validHeight: h } = this.chain.pipeline.geometry;
405
+ const texture = this.chain.pipeline.slots[ this.slot ]?.output;
406
+ if ( ! texture ) throw new Error( 'DLSSNeural.readOutput: the pass has not produced a frame yet' );
407
+
408
+ const group = this.device.createBindGroup( {
409
+ layout: this._readbackPipeline.getBindGroupLayout( 0 ),
410
+ entries: [
411
+ { binding: 0, resource: texture.createView() },
412
+ { binding: 1, resource: { buffer: this._readbackStorage } },
413
+ ],
414
+ } );
415
+
416
+ const encoder = this.device.createCommandEncoder( { label: 'rayzee:dlss-nr-readback' } );
417
+ const pass = encoder.beginComputePass();
418
+ pass.setPipeline( this._readbackPipeline );
419
+ pass.setBindGroup( 0, group );
420
+ pass.dispatchWorkgroups( Math.ceil( w / 8 ), Math.ceil( h / 8 ) );
421
+ pass.end();
422
+ encoder.copyBufferToBuffer( this._readbackStorage, 0, this._readbackMap, 0, w * h * 4 );
423
+ this.device.queue.submit( [ encoder.finish() ] );
424
+
425
+ await this._readbackMap.mapAsync( GPUMapMode.READ );
426
+ const bytes = new Uint8ClampedArray( this._readbackMap.getMappedRange().slice( 0 ) );
427
+ this._readbackMap.unmap();
428
+ return bytes;
429
+
430
+ }
431
+
432
+ dispose() {
433
+
434
+ if ( this.disposed ) return;
435
+ this.disposed = true;
436
+ this.source?.destroy();
437
+ this._readbackStorage?.destroy();
438
+ this._readbackMap?.destroy();
439
+ this._hdrStorage?.destroy();
440
+ this._hdrMap?.destroy();
441
+ this._toneMapper?.dispose();
442
+ try {
443
+
444
+ this.chain.destroy();
445
+
446
+ } catch ( e ) {
447
+
448
+ log.warn( 'retouch teardown:', e );
449
+
450
+ }
451
+
452
+ }
453
+
454
+ }
455
+
456
+ /**
457
+ * Smallest `paper_white` the runtime accepts, so the largest exposure it can carry: 1 / 0.05.
458
+ */
459
+ const MAX_EXPOSURE = 20;
460
+
461
+ /**
462
+ * Runs the pass over a finished frame and hands the result back as light.
463
+ *
464
+ * `source.half` is packed rgba16float — what `readDenoisedHalf` produces — carrying **scene-referred**
465
+ * linear light, not exposed and not tone-mapped.
466
+ *
467
+ * Exposure goes through the runtime's own `paper_white`, which is the divisor it uses to bring scene
468
+ * light into the network's working range (`value = scene / paper_white`) and multiplies back out at
469
+ * the end. Setting it to `1 / exposure` therefore shows the network an exposed image while returning
470
+ * the result in scene-referred units — so the engine's own tone curve still applies afterwards, and
471
+ * the model's hardcoded ACES never runs.
472
+ *
473
+ * Pass `tone` when this is the last step and the result should be display bytes; leave it out to get
474
+ * packed halves for the upscaler.
475
+ *
476
+ * @param {object} opts
477
+ * @param {{half: Uint16Array, width: number, height: number}} opts.source
478
+ * @param {number} [opts.exposure=1] clamped to `MAX_EXPOSURE`
479
+ * @param {{exposure: number, toneMapping: number, saturation: number}} [opts.tone] engine display state
480
+ * @param {DLSSNeural} [opts.instance] reuse a network already built for this size
481
+ * @returns {Promise<object>} `{ instance, width, height, ms }` plus `rgba8` or `half`
482
+ */
483
+ export async function enhanceFrame( {
484
+ source, settings = {}, instance = null, exposure = 1, tone = null, onProgress = () => {},
485
+ } ) {
486
+
487
+ let nr = instance;
488
+ if ( nr && ( nr.width !== source.width || nr.height !== source.height ) ) {
489
+
490
+ nr.dispose();
491
+ nr = null;
492
+
493
+ }
494
+
495
+ if ( ! nr ) {
496
+
497
+ nr = await DLSSNeural.create( {
498
+ width: source.width, height: source.height, settings, onProgress,
499
+ } );
500
+
501
+ }
502
+
503
+ const { runtime: merged, colorStrength } = splitSettings( settings );
504
+ const dst = nr.staging;
505
+
506
+ if ( ! source.half || source.half.length !== dst.length ) {
507
+
508
+ throw new Error( `DLSSNeural: expected ${dst.length} packed halfs, got ${source.half?.length}` );
509
+
510
+ }
511
+
512
+ dst.set( source.half );
513
+ nr.device.queue.writeBuffer( nr.source, 0, dst );
514
+
515
+ // ⚠️ On `chain.pipeline`, not `chain`. Both objects carry these two fields, but only the
516
+ // pipeline's copies are read when the composition kernel's params are written each encode —
517
+ // setting them on the chain is silently inert.
518
+ nr.chain.pipeline.paperWhite = 1 / Math.min( Math.max( exposure, 1 / MAX_EXPOSURE ), MAX_EXPOSURE );
519
+ nr.chain.pipeline.colorStrength = colorStrength;
520
+
521
+ const started = performance.now();
522
+ const encoder = nr.device.createCommandEncoder( { label: 'rayzee:dlss-nr' } );
523
+ await nr.chain.encode( encoder, nr.slot, { buffer: nr.source }, merged, () => {}, { reset: true } );
524
+ nr.device.queue.submit( [ encoder.finish() ] );
525
+ await nr.device.queue.onSubmittedWorkDone();
526
+
527
+ const result = tone ? await nr.readOutputToned( tone ) : await nr.readOutputHDR();
528
+ return { instance: nr, ...result, ms: performance.now() - started };
529
+
530
+ }