rayzee 11.4.0 → 11.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.
@@ -1594,7 +1594,7 @@ var vt = class {
1594
1594
  samples: this.samples.slice()
1595
1595
  };
1596
1596
  }
1597
- }, yt = "11.4.0", $ = 1e4, bt = 15e5;
1597
+ }, yt = "11.5.0", $ = 1e4, bt = 15e5;
1598
1598
  function xt(e, t, n = $) {
1599
1599
  return e >= n && t / bt < e / 3;
1600
1600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "11.4.0",
3
+ "version": "11.5.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -123,7 +123,7 @@ export class AIUpscaler extends EventDispatcher {
123
123
  this._worker = null;
124
124
  this._currentModelUrl = null;
125
125
  this._tileId = 0;
126
- this._pendingWorkerHandlers = new Set();
126
+ this._pendingWorkerHandlers = new Map();
127
127
 
128
128
  // Alpha channel cache (bilinear-upscaled from source, applied per tile)
129
129
  this._upscaledAlpha = null;
@@ -227,7 +227,8 @@ export class AIUpscaler extends EventDispatcher {
227
227
 
228
228
  // ─── Public Lifecycle ─────────────────────────────────────────────────────
229
229
 
230
- async start() {
230
+ /** @param {ImageData} [source] - what to enlarge instead of the render canvas */
231
+ async start( source = null ) {
231
232
 
232
233
  if ( ! this.enabled || this.state.isUpscaling || this.state.isLoading ) {
233
234
 
@@ -238,7 +239,7 @@ export class AIUpscaler extends EventDispatcher {
238
239
  this.dispatchEvent( { type: 'start' } );
239
240
 
240
241
  const startTime = performance.now();
241
- const success = await this.execute();
242
+ const success = await this.execute( source );
242
243
 
243
244
  if ( success ) {
244
245
 
@@ -260,7 +261,7 @@ export class AIUpscaler extends EventDispatcher {
260
261
 
261
262
  }
262
263
 
263
- async execute() {
264
+ async execute( source = null ) {
264
265
 
265
266
  if ( ! this.enabled ) return false;
266
267
 
@@ -270,7 +271,11 @@ export class AIUpscaler extends EventDispatcher {
270
271
  // Capture source image SYNCHRONOUSLY before any async work.
271
272
  // WebGPU canvas textures expire after each compositor frame,
272
273
  // so we must grab the pixels before awaiting model load.
273
- if ( this.hdr && this.getGPUTextures ) {
274
+ if ( source ) {
275
+
276
+ this._capturedSource = source;
277
+
278
+ } else if ( this.hdr && this.getGPUTextures ) {
274
279
 
275
280
  // HDR path: float planes tone-mapped on the GPU, no 8-bit step before the network
276
281
  this._capturedSource = await this._captureSourceHDR();
@@ -586,7 +591,7 @@ export class AIUpscaler extends EventDispatcher {
586
591
 
587
592
  };
588
593
 
589
- this._pendingWorkerHandlers.add( handler );
594
+ this._pendingWorkerHandlers.set( handler, reject );
590
595
  this._worker.addEventListener( 'message', handler );
591
596
  this._worker.postMessage(
592
597
  { type: 'infer', tileData, width, height, id },
@@ -814,15 +819,13 @@ export class AIUpscaler extends EventDispatcher {
814
819
 
815
820
  // ─── Disposal ─────────────────────────────────────────────────────────────
816
821
 
822
+ // Rejected, not dropped: an aborted run has to end before the next one starts.
817
823
  _cleanupPendingWorkerHandlers() {
818
824
 
819
- if ( this._worker ) {
820
-
821
- for ( const handler of this._pendingWorkerHandlers ) {
825
+ for ( const [ handler, reject ] of this._pendingWorkerHandlers ) {
822
826
 
823
- this._worker.removeEventListener( 'message', handler );
824
-
825
- }
827
+ this._worker?.removeEventListener( 'message', handler );
828
+ reject( new DOMException( 'Aborted', 'AbortError' ) );
826
829
 
827
830
  }
828
831
 
@@ -275,8 +275,9 @@ export class PackedToneMapper {
275
275
  * @param {number} [tone.saturation=1]
276
276
  * @param {boolean} [tone.flipY=false]
277
277
  * @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
278
- * @param {{key: string, wgsl: string, entries: GPUBindGroupEntry[]}} [tone.gain] - a per-pixel gain before the
279
- * curve: WGSL defining `rayzee_gain( uv, linear, exposure ) -> f32` over bind group 1
278
+ * @param {{key: string, wgsl: string, entries: GPUBindGroupEntry[], device?: GPUDevice, copyTo?: Function}} [tone.gain] - a
279
+ * per-pixel gain before the curve: WGSL defining `rayzee_gain( uv, linear, exposure ) -> f32` over bind group 1.
280
+ * One on another device is copied to this one for the run (`copyTo`).
280
281
  * @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
281
282
  */
282
283
  async toRGBA8( src, tone ) {
@@ -298,7 +299,25 @@ export class PackedToneMapper {
298
299
 
299
300
  }
300
301
 
301
- async _run( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false, gain = null } = {} ) {
302
+ async _run( src, tone = {} ) {
303
+
304
+ const gain = tone.gain;
305
+ if ( ! gain?.device || gain.device === this.device ) return this._dispatch( src, tone );
306
+
307
+ const copy = await gain.copyTo( this.device );
308
+ try {
309
+
310
+ return await this._dispatch( src, { ...tone, gain: copy } );
311
+
312
+ } finally {
313
+
314
+ copy.destroy();
315
+
316
+ }
317
+
318
+ }
319
+
320
+ async _dispatch( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false, gain = null } = {} ) {
302
321
 
303
322
  if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
304
323
  if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
@@ -495,7 +495,8 @@ export class LocalExposure extends RenderStage {
495
495
 
496
496
  /**
497
497
  * The gain for a tone-mapping pass outside three.js (`PackedToneMapper`'s `gain`), or null before the
498
- * first build. `cpu()` reads the grid back for the CPU tone map.
498
+ * first build. `cpu()` reads the grid back for the CPU tone map; `copyTo( device )` copies it to another
499
+ * GPU device (the neural models' own).
499
500
  */
500
501
  toneGain() {
501
502
 
@@ -508,10 +509,12 @@ export class LocalExposure extends RenderStage {
508
509
  this._paramsBuffer ??= backend.device.createBuffer( { label: 'local-exposure-params', size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST } );
509
510
  backend.device.queue.writeBuffer( this._paramsBuffer, 0, this._params );
510
511
  const params = this._params.slice();
512
+ const read = () => Promise.all( [ this.renderer.getArrayBufferAsync( this._grid.value ), this.renderer.getArrayBufferAsync( this._blur.value ) ] );
511
513
 
512
514
  return {
513
515
  key: 'local-exposure',
514
516
  wgsl: LOCAL_EXPOSURE_WGSL,
517
+ device: backend.device,
515
518
  entries: [
516
519
  { binding: 0, resource: { buffer: grid } },
517
520
  { binding: 1, resource: { buffer: blur } },
@@ -519,11 +522,31 @@ export class LocalExposure extends RenderStage {
519
522
  ],
520
523
  cpu: async () => {
521
524
 
522
- const [ g, b ] = await Promise.all( [ this.renderer.getArrayBufferAsync( this._grid.value ), this.renderer.getArrayBufferAsync( this._blur.value ) ] );
525
+ const [ g, b ] = await read();
523
526
  const gridData = new Float32Array( g ), blurData = new Float32Array( b );
524
527
  return ( u, v, lum, logExposure ) => localExposureGain( params, gridData, blurData, u, v, lum, logExposure );
525
528
 
526
529
  },
530
+ copyTo: async device => {
531
+
532
+ const [ g, b ] = await read();
533
+ const buffers = [[ g, GPUBufferUsage.STORAGE ], [ b, GPUBufferUsage.STORAGE ], [ params, GPUBufferUsage.UNIFORM ]].map( ( [ data, usage ] ) => {
534
+
535
+ const buffer = device.createBuffer( { label: 'local-exposure-copy', size: data.byteLength, usage: usage | GPUBufferUsage.COPY_DST } );
536
+ device.queue.writeBuffer( buffer, 0, data );
537
+ return buffer;
538
+
539
+ } );
540
+
541
+ return {
542
+ key: 'local-exposure',
543
+ wgsl: LOCAL_EXPOSURE_WGSL,
544
+ device,
545
+ entries: buffers.map( ( buffer, binding ) => ( { binding, resource: { buffer } } ) ),
546
+ destroy: () => buffers.forEach( buffer => buffer.destroy() ),
547
+ };
548
+
549
+ },
527
550
  };
528
551
 
529
552
  }
@@ -137,6 +137,11 @@ export class DenoisingManager extends EventDispatcher {
137
137
  this._retouch = null;
138
138
  this._superResModule = null;
139
139
  this._neuralPostBusy = false;
140
+ // The finished render's post passes: its guard, the run in flight, a queued rerun, and a cancel.
141
+ this._postIsStillComplete = null;
142
+ this._postRun = null;
143
+ this._postRedo = null;
144
+ this._postCancelled = false;
140
145
 
141
146
  // The tier the finished image uses. The loaded tier is not always this one: while the
142
147
  // image is still accumulating we run a cheaper model (see previewQuality).
@@ -1148,6 +1153,7 @@ export class DenoisingManager extends EventDispatcher {
1148
1153
  /** Turns the neural-rendering (detail) pass on or off, and sets its appearance controls. */
1149
1154
  setNeuralRendering( enabled, settings = null ) {
1150
1155
 
1156
+ const changed = this.neuralRendering !== !! enabled;
1151
1157
  this.neuralRendering = !! enabled;
1152
1158
  if ( settings ) this.neuralRenderingSettings = { ...this.neuralRenderingSettings, ...settings };
1153
1159
  if ( ! this.neuralRendering ) {
@@ -1157,6 +1163,8 @@ export class DenoisingManager extends EventDispatcher {
1157
1163
 
1158
1164
  }
1159
1165
 
1166
+ if ( changed ) this._redoPostPasses();
1167
+
1160
1168
  }
1161
1169
 
1162
1170
  _releaseNeuralRetouch() {
@@ -1167,14 +1175,8 @@ export class DenoisingManager extends EventDispatcher {
1167
1175
  }
1168
1176
 
1169
1177
  /**
1170
- * The neural post-chain over the picture the closing denoise just produced: super resolution
1171
- * first, then the detail pass, either or both.
1172
- *
1173
- * Order is forced by the models. Super resolution hands back light that can be fed onward; the
1174
- * detail pass writes an `rgba8unorm` texture with no `COPY_SRC`, so nothing can read its result
1175
- * and it has to be last — and it presents in its own display space rather than the engine's tone
1176
- * curve. Which shape super resolution returns therefore depends on what follows it: packed
1177
- * halves when the detail pass is next, display bytes when it is the end of the chain.
1178
+ * The post-chain over the picture the closing denoise just produced: the detail pass at render size,
1179
+ * then the chosen upscaler (neural super resolution or the ONNX one), either or both.
1178
1180
  *
1179
1181
  * Deliberately fire-and-forget: the render is already finished and a failure must not break it.
1180
1182
  */
@@ -1182,6 +1184,7 @@ export class DenoisingManager extends EventDispatcher {
1182
1184
 
1183
1185
  const wantSR = this.upscaler?.enabled && this.upscalerBackend === 'neural';
1184
1186
  const wantNR = this.neuralRendering;
1187
+ const wantESRGAN = this.upscaler?.enabled && ! wantSR;
1185
1188
  if ( ! wantSR && ! wantNR ) return;
1186
1189
 
1187
1190
  // A second completion can land while the first pass is still in flight — the models are
@@ -1230,7 +1233,12 @@ export class DenoisingManager extends EventDispatcher {
1230
1233
 
1231
1234
  }
1232
1235
 
1233
- if ( ! wantSR && ! runNR ) return;
1236
+ if ( ! wantSR && ! runNR ) {
1237
+
1238
+ if ( wantESRGAN && ! this._postCancelled ) await this.upscaler.start();
1239
+ return;
1240
+
1241
+ }
1234
1242
 
1235
1243
  // Must be the effective value, not the settings one: AutoExposure overwrites
1236
1244
  // `renderer.toneMappingExposure` every frame and never touches settings, so reading
@@ -1291,6 +1299,15 @@ export class DenoisingManager extends EventDispatcher {
1291
1299
 
1292
1300
  }
1293
1301
 
1302
+ if ( this._postRedo || this._postCancelled ) return;
1303
+
1304
+ if ( wantESRGAN ) {
1305
+
1306
+ await this.upscaler.start( new ImageData( image.rgba8, image.width, image.height ) );
1307
+ return;
1308
+
1309
+ }
1310
+
1294
1311
  sr.presentRGBA8( this.upscalerCanvas, image.rgba8, image.width, image.height );
1295
1312
 
1296
1313
  if ( ! isStillComplete() ) {
@@ -1360,10 +1377,61 @@ export class DenoisingManager extends EventDispatcher {
1360
1377
 
1361
1378
  }
1362
1379
 
1380
+ /** The neural chain or the ONNX upscaler over the finished render, as the switches ask. */
1381
+ _runPostPasses( isStillComplete ) {
1382
+
1383
+ this._postIsStillComplete = isStillComplete;
1384
+ this._postCancelled = false;
1385
+
1386
+ // The neural chain owns the overlay canvas when it runs, and hands its result to the ONNX upscaler.
1387
+ if ( this.neuralRendering || ( this.upscaler?.enabled && this.upscalerBackend === 'neural' ) ) {
1388
+
1389
+ this._postRun = this._runNeuralPost( isStillComplete );
1390
+
1391
+ } else {
1392
+
1393
+ this._postRun = this.upscaler?.enabled ? this.upscaler.start() : null;
1394
+
1395
+ }
1396
+
1397
+ }
1398
+
1399
+ /** Runs the post passes again on a finished render whose switches changed, once the run in flight ends. */
1400
+ _redoPostPasses() {
1401
+
1402
+ const isStillComplete = this._postIsStillComplete;
1403
+ // No chain yet: the closing denoise is still running and the chain reads the switches when it starts.
1404
+ if ( ! isStillComplete?.() || this._postRedo ) return;
1405
+
1406
+ this.upscaler?.abort();
1407
+ this._postCancelled = false;
1408
+ this._postRedo = Promise.resolve( this._postRun ).catch( () => {} ).then( () => {
1409
+
1410
+ this._postRedo = null;
1411
+ if ( this._postCancelled || this._postIsStillComplete !== isStillComplete || ! isStillComplete() ) return;
1412
+ this._restoreRenderDisplay();
1413
+ this._runPostPasses( isStillComplete );
1414
+
1415
+ } );
1416
+
1417
+ }
1418
+
1419
+ /** Stops the post passes on the finished render, which stays on screen as rendered. The switches stay as they are. */
1420
+ cancelPostPasses() {
1421
+
1422
+ this._postCancelled = true;
1423
+ this.upscaler?.abort();
1424
+ this._restoreRenderDisplay();
1425
+ // A retouch in flight cannot be stopped, only discarded, and ends later.
1426
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_END } );
1427
+
1428
+ }
1429
+
1363
1430
  onRenderComplete( { isStillComplete } ) {
1364
1431
 
1365
1432
  // Remove any stale completion-chain listener from a previous render cycle
1366
1433
  this._cleanupCompletionListener();
1434
+ this._postIsStillComplete = null;
1367
1435
 
1368
1436
  // What closes the render, if anything. A full pass at the chosen tier when that switch is
1369
1437
  // on; otherwise, if OIDN owns the live view, one last cheap refresh — the cadence's last
@@ -1383,16 +1451,7 @@ export class DenoisingManager extends EventDispatcher {
1383
1451
  // denoise. Ordered after `_denoiserEndHandler`, which publishes the picture.
1384
1452
  if ( closing ) this._onDisplayRefresh?.();
1385
1453
 
1386
- // One owner of the overlay canvas, so the neural chain and the ONNX upscaler are
1387
- // exclusive. The neural chain also runs on its own when only its detail pass is on.
1388
- if ( this.neuralRendering || ( this.upscaler?.enabled && this.upscalerBackend === 'neural' ) ) {
1389
-
1390
- this._runNeuralPost( isStillComplete );
1391
- return;
1392
-
1393
- }
1394
-
1395
- if ( this.upscaler?.enabled ) this.upscaler.start();
1454
+ this._runPostPasses( isStillComplete );
1396
1455
 
1397
1456
  };
1398
1457
 
@@ -1467,6 +1526,7 @@ export class DenoisingManager extends EventDispatcher {
1467
1526
 
1468
1527
  // Remove stale completion-chain listener before aborting
1469
1528
  this._cleanupCompletionListener();
1529
+ this._postIsStillComplete = null;
1470
1530
 
1471
1531
  // The 2D canvas is the upscaler's alone: a reset means its enlarged result no longer
1472
1532
  // describes anything, so the render canvas comes back.
@@ -1509,6 +1569,7 @@ export class DenoisingManager extends EventDispatcher {
1509
1569
 
1510
1570
  // Remove pending completion-chain listener
1511
1571
  this._cleanupCompletionListener();
1572
+ this._postIsStillComplete = null;
1512
1573
  // Owns a second GPUDevice plus 3 MB of weights, so it must not outlive the manager.
1513
1574
  this._releaseNeuralUpscaler();
1514
1575
  this._releaseNeuralRetouch();
@@ -1737,10 +1798,12 @@ export class DenoisingManager extends EventDispatcher {
1737
1798
  /** Enables or disables the AI upscaler. */
1738
1799
  setUpscalerEnabled( enabled ) {
1739
1800
 
1801
+ const changed = !! this.upscaler && this.upscaler.enabled !== !! enabled;
1740
1802
  if ( this.upscaler ) this.upscaler.enabled = enabled;
1741
1803
  else if ( enabled ) this._recordNoOverlayCanvas( 'upscaler' );
1742
1804
  // The enlarged picture describes a setting that is no longer on.
1743
1805
  if ( ! enabled ) this._restoreRenderDisplay();
1806
+ if ( changed ) this._redoPostPasses();
1744
1807
 
1745
1808
  }
1746
1809