rayzee 5.6.0 → 5.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "5.6.0",
3
+ "version": "5.6.1",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -104,6 +104,7 @@ export class AIUpscaler extends EventDispatcher {
104
104
  this._worker = null;
105
105
  this._currentModelUrl = null;
106
106
  this._tileId = 0;
107
+ this._pendingWorkerHandlers = new Set();
107
108
 
108
109
  // Alpha channel cache (bilinear-upscaled from source, applied per tile)
109
110
  this._upscaledAlpha = null;
@@ -315,6 +316,7 @@ export class AIUpscaler extends EventDispatcher {
315
316
 
316
317
  this._capturedSource = null;
317
318
  this._upscaledAlpha = null;
319
+ this._backupCanvas = null;
318
320
 
319
321
  // Only clean up if abort() hasn't already done it
320
322
  if ( this.state.isUpscaling ) {
@@ -334,6 +336,7 @@ export class AIUpscaler extends EventDispatcher {
334
336
  if ( ! this.state.isUpscaling ) return;
335
337
 
336
338
  this.state.abortController?.abort();
339
+ this._cleanupPendingWorkerHandlers();
337
340
 
338
341
  // Restore input visibility and canvas state
339
342
  this.input.style.opacity = '1';
@@ -610,6 +613,7 @@ export class AIUpscaler extends EventDispatcher {
610
613
 
611
614
  if ( e.data.id !== id ) return;
612
615
  this._worker.removeEventListener( 'message', handler );
616
+ this._pendingWorkerHandlers.delete( handler );
613
617
 
614
618
  if ( e.data.type === 'inferred' ) {
615
619
 
@@ -623,6 +627,7 @@ export class AIUpscaler extends EventDispatcher {
623
627
 
624
628
  };
625
629
 
630
+ this._pendingWorkerHandlers.add( handler );
626
631
  this._worker.addEventListener( 'message', handler );
627
632
  this._worker.postMessage(
628
633
  { type: 'infer', tileData, width, height, id },
@@ -858,9 +863,26 @@ export class AIUpscaler extends EventDispatcher {
858
863
 
859
864
  // ─── Disposal ─────────────────────────────────────────────────────────────
860
865
 
866
+ _cleanupPendingWorkerHandlers() {
867
+
868
+ if ( this._worker ) {
869
+
870
+ for ( const handler of this._pendingWorkerHandlers ) {
871
+
872
+ this._worker.removeEventListener( 'message', handler );
873
+
874
+ }
875
+
876
+ }
877
+
878
+ this._pendingWorkerHandlers.clear();
879
+
880
+ }
881
+
861
882
  async dispose() {
862
883
 
863
884
  this.abort();
885
+ this._cleanupPendingWorkerHandlers();
864
886
 
865
887
  if ( this._worker ) {
866
888
 
@@ -87,6 +87,7 @@ export class OIDNDenoiser extends EventDispatcher {
87
87
  // renderer.getArrayBufferAsync because the source is a raw GPUBuffer,
88
88
  // not a Three.js BufferAttribute.
89
89
  this._alphaReadbackBuffer = null;
90
+ this._alphaReadbackMapped = false;
90
91
 
91
92
  // Cached alpha channel from the input color buffer (OIDN discards alpha)
92
93
  this._cachedAlpha = null;
@@ -108,6 +109,9 @@ export class OIDNDenoiser extends EventDispatcher {
108
109
  abortController: null
109
110
  };
110
111
 
112
+ // Track in-flight tile staging buffers so they can be destroyed on abort
113
+ this._pendingStagingBuffers = new Set();
114
+
111
115
  this.currentTZAUrl = null;
112
116
  this.unet = null;
113
117
 
@@ -514,7 +518,22 @@ export class OIDNDenoiser extends EventDispatcher {
514
518
  this._gpuInputBuffers.albedo?.destroy();
515
519
  this._gpuInputBuffers.normal?.destroy();
516
520
  this._gpuInputPadBuffer?.destroy();
521
+
522
+ // Unmap before destroying if a mapAsync resolved but unmap hasn't been called yet.
523
+ // If mapAsync is still pending, destroy() will reject it — _cacheInputAlpha's
524
+ // catch handler covers that case.
525
+ if ( this._alphaReadbackMapped && this._alphaReadbackBuffer ) {
526
+
527
+ try {
528
+
529
+ this._alphaReadbackBuffer.unmap();
530
+
531
+ } catch { /* already unmapped or destroyed */ }
532
+
533
+ }
534
+
517
535
  this._alphaReadbackBuffer?.destroy();
536
+ this._alphaReadbackMapped = false;
518
537
  this._gpuInputBuffers = { color: null, albedo: null, normal: null };
519
538
  this._gpuInputPadBuffer = null;
520
539
  this._gpuInputPaddedRowBytes = 0;
@@ -550,7 +569,19 @@ export class OIDNDenoiser extends EventDispatcher {
550
569
  enc.copyBufferToBuffer( this._gpuInputBuffers.color, 0, staging, 0, byteSize );
551
570
  device.queue.submit( [ enc.finish() ] );
552
571
 
553
- await staging.mapAsync( GPUMapMode.READ );
572
+ this._alphaReadbackMapped = true;
573
+ try {
574
+
575
+ await staging.mapAsync( GPUMapMode.READ );
576
+
577
+ } catch {
578
+
579
+ // Buffer was destroyed while mapAsync was pending (resize or dispose)
580
+ this._alphaReadbackMapped = false;
581
+ return;
582
+
583
+ }
584
+
554
585
  const f32 = new Float32Array( staging.getMappedRange() );
555
586
 
556
587
  // Extract alpha channel as uint8 (pre-multiplied is not needed — alpha is 0 or 1)
@@ -563,6 +594,7 @@ export class OIDNDenoiser extends EventDispatcher {
563
594
  }
564
595
 
565
596
  staging.unmap();
597
+ this._alphaReadbackMapped = false;
566
598
 
567
599
  this._cachedAlpha = alpha;
568
600
  this._cachedAlphaWidth = width;
@@ -653,6 +685,8 @@ export class OIDNDenoiser extends EventDispatcher {
653
685
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
654
686
  } );
655
687
 
688
+ this._pendingStagingBuffers.add( staging );
689
+
656
690
  // Copy each tile row from its position in the full output buffer
657
691
  const enc = device.createCommandEncoder();
658
692
 
@@ -710,8 +744,15 @@ export class OIDNDenoiser extends EventDispatcher {
710
744
 
711
745
  staging.unmap();
712
746
  staging.destroy();
747
+ this._pendingStagingBuffers.delete( staging );
713
748
  this.ctx.putImageData( tileImageData, tile.x, tile.y );
714
749
 
750
+ } ).catch( () => {
751
+
752
+ // mapAsync rejected (abort or GPU lost) — destroy the buffer
753
+ staging.destroy();
754
+ this._pendingStagingBuffers.delete( staging );
755
+
715
756
  } );
716
757
 
717
758
  }
@@ -745,44 +786,50 @@ export class OIDNDenoiser extends EventDispatcher {
745
786
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
746
787
  } );
747
788
 
748
- // Queue a copy from the oidn output buffer (STORAGE|COPY_SRC) to staging
749
- const encoder = device.createCommandEncoder( { label: 'oidn-readback' } );
750
- encoder.copyBufferToBuffer( gpuBuffer, 0, stagingBuffer, 0, byteSize );
751
- device.queue.submit( [ encoder.finish() ] );
789
+ try {
790
+
791
+ // Queue a copy from the oidn output buffer (STORAGE|COPY_SRC) to staging
792
+ const encoder = device.createCommandEncoder( { label: 'oidn-readback' } );
793
+ encoder.copyBufferToBuffer( gpuBuffer, 0, stagingBuffer, 0, byteSize );
794
+ device.queue.submit( [ encoder.finish() ] );
795
+
796
+ await stagingBuffer.mapAsync( GPUMapMode.READ );
797
+ const float32 = new Float32Array( stagingBuffer.getMappedRange() );
798
+
799
+ const imageData = new ImageData( width, height );
800
+ const exposure = this.getExposure();
801
+ const saturation = this.getSaturation();
802
+ const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
803
+ const alpha = this._cachedAlpha;
752
804
 
753
- await stagingBuffer.mapAsync( GPUMapMode.READ );
754
- const float32 = new Float32Array( stagingBuffer.getMappedRange() );
805
+ for ( let i = 0, len = float32.length; i < len; i += 4 ) {
755
806
 
756
- const imageData = new ImageData( width, height );
757
- const exposure = this.getExposure();
758
- const saturation = this.getSaturation();
759
- const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
760
- const alpha = this._cachedAlpha;
807
+ // Exposure + saturation (pre-tonemap, matching Display)
808
+ let er = float32[ i ] * exposure, eg = float32[ i + 1 ] * exposure, eb = float32[ i + 2 ] * exposure;
809
+ if ( saturation !== 1.0 ) {
761
810
 
762
- for ( let i = 0, len = float32.length; i < len; i += 4 ) {
811
+ _tmOut[ 0 ] = er; _tmOut[ 1 ] = eg; _tmOut[ 2 ] = eb;
812
+ applySaturation( _tmOut, saturation );
813
+ er = _tmOut[ 0 ]; eg = _tmOut[ 1 ]; eb = _tmOut[ 2 ];
763
814
 
764
- // Exposure + saturation (pre-tonemap, matching Display)
765
- let er = float32[ i ] * exposure, eg = float32[ i + 1 ] * exposure, eb = float32[ i + 2 ] * exposure;
766
- if ( saturation !== 1.0 ) {
815
+ }
767
816
 
768
- _tmOut[ 0 ] = er; _tmOut[ 1 ] = eg; _tmOut[ 2 ] = eb;
769
- applySaturation( _tmOut, saturation );
770
- er = _tmOut[ 0 ]; eg = _tmOut[ 1 ]; eb = _tmOut[ 2 ];
817
+ tmFn( er, eg, eb, 1.0, _tmOut );
818
+ imageData.data[ i ] = _tmOut[ 0 ] ** SRGB_GAMMA * 255 | 0;
819
+ imageData.data[ i + 1 ] = _tmOut[ 1 ] ** SRGB_GAMMA * 255 | 0;
820
+ imageData.data[ i + 2 ] = _tmOut[ 2 ] ** SRGB_GAMMA * 255 | 0;
821
+ imageData.data[ i + 3 ] = alpha ? alpha[ i >> 2 ] : 255;
771
822
 
772
823
  }
773
824
 
774
- tmFn( er, eg, eb, 1.0, _tmOut );
775
- imageData.data[ i ] = _tmOut[ 0 ] ** SRGB_GAMMA * 255 | 0;
776
- imageData.data[ i + 1 ] = _tmOut[ 1 ] ** SRGB_GAMMA * 255 | 0;
777
- imageData.data[ i + 2 ] = _tmOut[ 2 ] ** SRGB_GAMMA * 255 | 0;
778
- imageData.data[ i + 3 ] = alpha ? alpha[ i >> 2 ] : 255;
825
+ stagingBuffer.unmap();
826
+ this.ctx.putImageData( imageData, 0, 0 );
779
827
 
780
- }
828
+ } finally {
781
829
 
782
- stagingBuffer.unmap();
783
- stagingBuffer.destroy();
830
+ stagingBuffer.destroy();
784
831
 
785
- this.ctx.putImageData( imageData, 0, 0 );
832
+ }
786
833
 
787
834
  }
788
835
 
@@ -793,6 +840,9 @@ export class OIDNDenoiser extends EventDispatcher {
793
840
  // Signal abort to current operation
794
841
  this.state.abortController?.abort();
795
842
 
843
+ // Destroy any in-flight tile staging buffers that mapAsync won't resolve
844
+ this._destroyPendingStagingBuffers();
845
+
796
846
  // Restore input visibility
797
847
  this.input.style.opacity = '1';
798
848
 
@@ -896,11 +946,26 @@ export class OIDNDenoiser extends EventDispatcher {
896
946
 
897
947
  }
898
948
 
949
+ _destroyPendingStagingBuffers() {
950
+
951
+ for ( const buf of this._pendingStagingBuffers ) {
952
+
953
+ buf.destroy();
954
+
955
+ }
956
+
957
+ this._pendingStagingBuffers.clear();
958
+
959
+ }
960
+
899
961
  dispose() {
900
962
 
901
963
  // Abort any ongoing operations
902
964
  this.abort();
903
965
 
966
+ // Destroy any remaining staging buffers
967
+ this._destroyPendingStagingBuffers();
968
+
904
969
  // Dispose resources
905
970
  this.unet?.dispose();
906
971
  this._destroyGPUInputBuffers();
@@ -120,6 +120,9 @@ export class RenderPipeline {
120
120
 
121
121
  }
122
122
 
123
+ // Prune stale stats entry for removed stage
124
+ this.stats.timings.delete( stage.name );
125
+
123
126
  return true;
124
127
 
125
128
  }
@@ -832,6 +832,24 @@ export class ASVGF extends RenderStage {
832
832
  this._heatmapComputeNode?.dispose();
833
833
  this._heatmapStorageTex?.dispose();
834
834
  this.heatmapTarget?.dispose();
835
+
836
+ // Dispose input TextureNode objects
837
+ this._colorTexNode?.dispose();
838
+ this._normalDepthTexNode?.dispose();
839
+ this._motionTexNode?.dispose();
840
+ this._readTemporalTexNode?.dispose();
841
+ this._readPrevNDTexNode?.dispose();
842
+ this._gradientReadTexNode?.dispose();
843
+
844
+ // Dispose heatmap TextureNode objects
845
+ this._heatmapRawColorTexNode?.dispose();
846
+ this._heatmapColorTexNode?.dispose();
847
+ this._heatmapTemporalTexNode?.dispose();
848
+ this._heatmapNDTexNode?.dispose();
849
+ this._heatmapMotionTexNode?.dispose();
850
+ this._heatmapGradientTexNode?.dispose();
851
+
852
+ // dispose() also removes the DOM node
835
853
  this.heatmapHelper?.dispose();
836
854
 
837
855
  }
@@ -483,6 +483,7 @@ export class AdaptiveSampling extends RenderStage {
483
483
  this._heatmapStorageTex?.dispose();
484
484
  this._outputStorageTex?.dispose();
485
485
  this.heatmapTarget?.dispose();
486
+ this._varianceTexNode?.dispose();
486
487
  this.helper?.dispose();
487
488
 
488
489
  this._computeNode = null;
@@ -490,6 +491,7 @@ export class AdaptiveSampling extends RenderStage {
490
491
  this._heatmapStorageTex = null;
491
492
  this._outputStorageTex = null;
492
493
  this.heatmapTarget = null;
494
+ this._varianceTexNode = null;
493
495
  this.helper = null;
494
496
 
495
497
  }
@@ -698,6 +698,8 @@ export class AutoExposure extends RenderStage {
698
698
  this._reductionStorageTex?.dispose();
699
699
  this._reductionReadTarget?.dispose();
700
700
  this._readbackBuffer?.dispose();
701
+ this._inputTexNode?.dispose();
702
+ this._reductionReadTexNode?.dispose();
701
703
 
702
704
  }
703
705
 
@@ -321,6 +321,8 @@ export class BilateralFilter extends RenderStage {
321
321
  this._computeNodeB?.dispose();
322
322
  this._storageTexA?.dispose();
323
323
  this._storageTexB?.dispose();
324
+ this._readTexNode?.dispose();
325
+ this._normalDepthTexNode?.dispose();
324
326
 
325
327
  }
326
328
 
@@ -110,6 +110,7 @@ export class Display extends RenderStage {
110
110
 
111
111
  dispose() {
112
112
 
113
+ this._displayTexNode?.dispose();
113
114
  this.displayMaterial?.dispose();
114
115
  // QuadMesh extends Mesh — no dispose method; material already disposed.
115
116
  this.displayQuad = null;
@@ -275,6 +275,7 @@ export class EdgeFilter extends RenderStage {
275
275
  this._computeNode?.dispose();
276
276
  this._outputStorageTex?.dispose();
277
277
  this.outputTarget?.dispose();
278
+ this._inputTexNode?.dispose();
278
279
 
279
280
  }
280
281
 
@@ -547,6 +547,7 @@ export class MotionVector extends RenderStage {
547
547
  this._worldSpaceStorageTex?.dispose();
548
548
  this.screenSpaceTarget?.dispose();
549
549
  this.worldSpaceTarget?.dispose();
550
+ this._normalDepthTexNode?.dispose();
550
551
 
551
552
  }
552
553
 
@@ -194,6 +194,12 @@ export class SSRC extends RenderStage {
194
194
  this._prevNDTexA.dispose();
195
195
  this._prevNDTexB.dispose();
196
196
  this._outputTex.dispose();
197
+ this._colorTexNode?.dispose();
198
+ this._ndTexNode?.dispose();
199
+ this._motionTexNode?.dispose();
200
+ this._readCacheTexNode?.dispose();
201
+ this._readPrevNDTexNode?.dispose();
202
+ this._readPass1CacheTexNode?.dispose();
197
203
 
198
204
  }
199
205
 
@@ -371,6 +371,9 @@ export class Variance extends RenderStage {
371
371
  this._computeNodeB?.dispose();
372
372
  this._storageTexA?.dispose();
373
373
  this._storageTexB?.dispose();
374
+ this._colorTexNode?.dispose();
375
+ this._readTexNodeA?.dispose();
376
+ this._readTexNodeB?.dispose();
374
377
 
375
378
  }
376
379
 
@@ -54,6 +54,17 @@ export class DenoisingManager extends EventDispatcher {
54
54
  this._lastRenderWidth = 0;
55
55
  this._lastRenderHeight = 0;
56
56
 
57
+ // Track the current completion-chain listener so it can be removed on re-trigger
58
+ this._pendingStartUpscaler = null;
59
+
60
+ // Bound event forwarding handlers (stored for removal on re-setup / dispose)
61
+ this._denoiserStartHandler = null;
62
+ this._denoiserEndHandler = null;
63
+ this._upscalerResChangedHandler = null;
64
+ this._upscalerStartHandler = null;
65
+ this._upscalerProgressHandler = null;
66
+ this._upscalerEndHandler = null;
67
+
57
68
  }
58
69
 
59
70
  _createDenoiserCanvas( mainCanvas ) {
@@ -146,11 +157,13 @@ export class DenoisingManager extends EventDispatcher {
146
157
 
147
158
  this.denoiser.enabled = DEFAULT_STATE.enableOIDN;
148
159
 
149
- // Forward lifecycle events
150
- this.denoiser.addEventListener( 'start', () =>
151
- this.dispatchEvent( { type: EngineEvents.DENOISING_START } ) );
152
- this.denoiser.addEventListener( 'end', () =>
153
- this.dispatchEvent( { type: EngineEvents.DENOISING_END } ) );
160
+ // Forward lifecycle events (store refs for removal on re-setup / dispose)
161
+ this._denoiserStartHandler = () =>
162
+ this.dispatchEvent( { type: EngineEvents.DENOISING_START } );
163
+ this._denoiserEndHandler = () =>
164
+ this.dispatchEvent( { type: EngineEvents.DENOISING_END } );
165
+ this.denoiser.addEventListener( 'start', this._denoiserStartHandler );
166
+ this.denoiser.addEventListener( 'end', this._denoiserEndHandler );
154
167
 
155
168
  }
156
169
 
@@ -189,15 +202,19 @@ export class DenoisingManager extends EventDispatcher {
189
202
 
190
203
  this.upscaler.enabled = DEFAULT_STATE.enableUpscaler || false;
191
204
 
192
- // Forward lifecycle events
193
- this.upscaler.addEventListener( 'resolution_changed', ( e ) =>
194
- this.dispatchEvent( { type: 'resolution_changed', width: e.width, height: e.height } ) );
195
- this.upscaler.addEventListener( 'start', () =>
196
- this.dispatchEvent( { type: EngineEvents.UPSCALING_START } ) );
197
- this.upscaler.addEventListener( 'progress', ( e ) =>
198
- this.dispatchEvent( { type: EngineEvents.UPSCALING_PROGRESS, progress: e.progress } ) );
199
- this.upscaler.addEventListener( 'end', () =>
200
- this.dispatchEvent( { type: EngineEvents.UPSCALING_END } ) );
205
+ // Forward lifecycle events (store refs for removal on re-setup / dispose)
206
+ this._upscalerResChangedHandler = ( e ) =>
207
+ this.dispatchEvent( { type: 'resolution_changed', width: e.width, height: e.height } );
208
+ this._upscalerStartHandler = () =>
209
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_START } );
210
+ this._upscalerProgressHandler = ( e ) =>
211
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_PROGRESS, progress: e.progress } );
212
+ this._upscalerEndHandler = () =>
213
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_END } );
214
+ this.upscaler.addEventListener( 'resolution_changed', this._upscalerResChangedHandler );
215
+ this.upscaler.addEventListener( 'start', this._upscalerStartHandler );
216
+ this.upscaler.addEventListener( 'progress', this._upscalerProgressHandler );
217
+ this.upscaler.addEventListener( 'end', this._upscalerEndHandler );
201
218
 
202
219
  }
203
220
 
@@ -353,8 +370,23 @@ export class DenoisingManager extends EventDispatcher {
353
370
  * @param {Function} params.isStillComplete - () => boolean, guard for async race
354
371
  * @param {import('../Pipeline/PipelineContext.js').PipelineContext} params.context
355
372
  */
373
+ _cleanupCompletionListener() {
374
+
375
+ if ( this._pendingStartUpscaler && this.denoiser ) {
376
+
377
+ this.denoiser.removeEventListener( 'end', this._pendingStartUpscaler );
378
+
379
+ }
380
+
381
+ this._pendingStartUpscaler = null;
382
+
383
+ }
384
+
356
385
  onRenderComplete( { isStillComplete, context } ) {
357
386
 
387
+ // Remove any stale completion-chain listener from a previous render cycle
388
+ this._cleanupCompletionListener();
389
+
358
390
  // Show post-process canvas if any post-process is enabled
359
391
  if ( ( this.denoiser?.enabled || this.upscaler?.enabled ) && this.denoiserCanvas ) {
360
392
 
@@ -365,6 +397,8 @@ export class DenoisingManager extends EventDispatcher {
365
397
  // Chain: denoise first (if enabled), then upscale (if enabled)
366
398
  const startUpscaler = () => {
367
399
 
400
+ this._pendingStartUpscaler = null;
401
+
368
402
  if ( ! isStillComplete() ) return;
369
403
 
370
404
  if ( this.upscaler?.enabled ) {
@@ -377,6 +411,7 @@ export class DenoisingManager extends EventDispatcher {
377
411
 
378
412
  if ( this.denoiser?.enabled ) {
379
413
 
414
+ this._pendingStartUpscaler = startUpscaler;
380
415
  this.denoiser.addEventListener( 'end', startUpscaler, { once: true } );
381
416
  this.denoiser.start();
382
417
 
@@ -401,6 +436,9 @@ export class DenoisingManager extends EventDispatcher {
401
436
  */
402
437
  abort( mainCanvas ) {
403
438
 
439
+ // Remove stale completion-chain listener before aborting
440
+ this._cleanupCompletionListener();
441
+
404
442
  if ( mainCanvas ) mainCanvas.style.opacity = '1';
405
443
 
406
444
  if ( this.upscaler ) this.upscaler.abort();
@@ -416,8 +454,13 @@ export class DenoisingManager extends EventDispatcher {
416
454
 
417
455
  dispose() {
418
456
 
457
+ // Remove pending completion-chain listener
458
+ this._cleanupCompletionListener();
459
+
419
460
  if ( this.denoiser ) {
420
461
 
462
+ if ( this._denoiserStartHandler ) this.denoiser.removeEventListener( 'start', this._denoiserStartHandler );
463
+ if ( this._denoiserEndHandler ) this.denoiser.removeEventListener( 'end', this._denoiserEndHandler );
421
464
  this.denoiser.dispose();
422
465
  this.denoiser = null;
423
466
 
@@ -425,11 +468,22 @@ export class DenoisingManager extends EventDispatcher {
425
468
 
426
469
  if ( this.upscaler ) {
427
470
 
471
+ if ( this._upscalerResChangedHandler ) this.upscaler.removeEventListener( 'resolution_changed', this._upscalerResChangedHandler );
472
+ if ( this._upscalerStartHandler ) this.upscaler.removeEventListener( 'start', this._upscalerStartHandler );
473
+ if ( this._upscalerProgressHandler ) this.upscaler.removeEventListener( 'progress', this._upscalerProgressHandler );
474
+ if ( this._upscalerEndHandler ) this.upscaler.removeEventListener( 'end', this._upscalerEndHandler );
428
475
  this.upscaler.dispose();
429
476
  this.upscaler = null;
430
477
 
431
478
  }
432
479
 
480
+ this._denoiserStartHandler = null;
481
+ this._denoiserEndHandler = null;
482
+ this._upscalerResChangedHandler = null;
483
+ this._upscalerStartHandler = null;
484
+ this._upscalerProgressHandler = null;
485
+ this._upscalerEndHandler = null;
486
+
433
487
  if ( this.denoiserCanvas?.parentNode ) {
434
488
 
435
489
  this.denoiserCanvas.parentNode.removeChild( this.denoiserCanvas );