rayzee 8.6.0 → 8.7.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.
@@ -1,12 +1,13 @@
1
1
  /**
2
- * AnimationManager — Drives GLTF skeletal/morph animations for BVH refit.
2
+ * AnimationManager — Drives animation clips for the path tracer.
3
3
  *
4
- * Owns the Three.js AnimationMixer, advances clips each frame,
5
- * extracts deformed vertex positions via CPU skinning, and returns
6
- * a per-mesh reader in the shape PathTracerApp.refitBVH() accepts.
4
+ * Deforming clips (a SkinnedMesh or morph track) return a per-mesh position reader for
5
+ * PathTracerApp.refitBVH(). Rigid clips return nothing and hand the moved meshes to
6
+ * `applyPoseCallback`, which updates placements only — a refit would bake world positions into
7
+ * triangles that other placements share.
7
8
  */
8
9
 
9
- import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce } from 'three';
10
+ import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce, PropertyBinding } from 'three';
10
11
  import { EngineEvents } from '../EngineEvents.js';
11
12
 
12
13
  export class AnimationManager extends EventDispatcher {
@@ -31,9 +32,20 @@ export class AnimationManager extends EventDispatcher {
31
32
  this._savedTimeScale = 1;
32
33
  this.onFinished = null; // callback when a non-looping clip ends
33
34
 
35
+ this._rigid = false;
36
+ this._plans = null; // per clip: { meshes: Int32Array, visibility: boolean, cameras: Camera[] }
37
+ this._allPlan = null;
38
+ this._activeClip = 0;
39
+ this._slotOf = null; // meshIndex -> row in the two caches below, or -1
40
+ this._lastWorld = null; // world matrix last handed to applyPoseCallback, 16 per row
41
+ this._lastVisible = null; // 0 / 1, or 2 before the first pose
42
+
34
43
  /** Injected by PathTracerApp — wakes the render loop after play/resume. */
35
44
  this.wakeCallback = null;
36
45
 
46
+ /** Injected by PathTracerApp — receives `{ meshIndices, visibilityChanged, cameras }`. */
47
+ this.applyPoseCallback = null;
48
+
37
49
  }
38
50
 
39
51
  /**
@@ -88,6 +100,10 @@ export class AnimationManager extends EventDispatcher {
88
100
 
89
101
  this.actions = actions;
90
102
 
103
+ this._rigid = ! meshes.some( m => m?.isSkinnedMesh ) &&
104
+ ! animations.some( clip => clip.tracks.some( t => t.name.includes( 'morphTargetInfluences' ) ) );
105
+ this._planClips( animations );
106
+
91
107
  // Listen for non-looping clip completion
92
108
  this.mixer.addEventListener( 'finished', () => {
93
109
 
@@ -121,7 +137,154 @@ export class AnimationManager extends EventDispatcher {
121
137
  this._meshPositions = [];
122
138
 
123
139
  const skinnedCount = meshes.filter( m => m.isSkinnedMesh ).length;
124
- console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles` );
140
+ console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles, ${this._rigid ? 'rigid' : 'deforming'}` );
141
+
142
+ }
143
+
144
+ /** Which meshes and cameras each clip moves, directly or through an ancestor. @private */
145
+ _planClips( animations ) {
146
+
147
+ const root = this._mixerRoot;
148
+ const byName = new Map();
149
+ // First name in pre-order wins, as in PropertyBinding.findNode.
150
+ root.traverse?.( node => {
151
+
152
+ if ( ! byName.has( node.name ) ) byName.set( node.name, node );
153
+ byName.set( node.uuid, node );
154
+
155
+ } );
156
+
157
+ this._plans = animations.map( clip => {
158
+
159
+ const animated = new Set();
160
+ let visibility = false;
161
+ for ( const track of clip.tracks ) {
162
+
163
+ const { nodeName, propertyName } = PropertyBinding.parseTrackName( track.name );
164
+ const node = byName.get( nodeName );
165
+ if ( ! node ) continue;
166
+ animated.add( node );
167
+ if ( propertyName === 'visible' ) visibility = true;
168
+
169
+ }
170
+
171
+ const meshes = [];
172
+ for ( let m = 0; m < this._meshes.length; m ++ ) {
173
+
174
+ for ( let o = this._meshes[ m ]; o; o = o.parent ) {
175
+
176
+ if ( animated.has( o ) ) {
177
+
178
+ meshes.push( m );
179
+ break;
180
+
181
+ }
182
+
183
+ }
184
+
185
+ }
186
+
187
+ const cameras = new Set();
188
+ for ( const node of animated ) node.traverse( o => o.isCamera && cameras.add( o ) );
189
+
190
+ return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
191
+
192
+ } );
193
+
194
+ this._allPlan = null;
195
+ if ( ! this._rigid ) return;
196
+
197
+ this._slotOf = new Int32Array( this._meshes.length ).fill( - 1 );
198
+ let rows = 0;
199
+ for ( const plan of this._plans ) for ( const m of plan.meshes ) if ( this._slotOf[ m ] < 0 ) this._slotOf[ m ] = rows ++;
200
+
201
+ // Unknown until the first pose, which therefore sends every animated mesh.
202
+ this._lastWorld = new Float64Array( rows * 16 ).fill( NaN );
203
+ this._lastVisible = new Uint8Array( rows ).fill( 2 );
204
+
205
+ }
206
+
207
+ /** The plan for a clip index, or the union of every clip for -1. @private */
208
+ _planFor( clipIndex ) {
209
+
210
+ if ( ! this._plans ) return null;
211
+ if ( clipIndex >= 0 ) return this._plans[ clipIndex ] ?? null;
212
+
213
+ if ( ! this._allPlan ) {
214
+
215
+ const meshes = new Set(), cameras = new Set();
216
+ let visibility = false;
217
+ for ( const plan of this._plans ) {
218
+
219
+ plan.meshes.forEach( m => meshes.add( m ) );
220
+ plan.cameras.forEach( c => cameras.add( c ) );
221
+ visibility ||= plan.visibility;
222
+
223
+ }
224
+
225
+ this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
226
+
227
+ }
228
+
229
+ return this._allPlan;
230
+
231
+ }
232
+
233
+ /** Send meshes whose world matrix or visibility changed since the last pose. @private */
234
+ _applyRigidPose( clipIndex ) {
235
+
236
+ const plan = this._planFor( clipIndex );
237
+ if ( ! plan ) return;
238
+
239
+ const moved = [];
240
+ let visibilityChanged = false;
241
+ const last = this._lastWorld, lastVisible = this._lastVisible;
242
+
243
+ for ( const m of plan.meshes ) {
244
+
245
+ const mesh = this._meshes[ m ];
246
+ const row = this._slotOf[ m ];
247
+ const e = mesh.matrixWorld.elements;
248
+ const o = row * 16;
249
+
250
+ for ( let i = 0; i < 16; i ++ ) {
251
+
252
+ if ( last[ o + i ] !== e[ i ] ) {
253
+
254
+ for ( let j = 0; j < 16; j ++ ) last[ o + j ] = e[ j ];
255
+ moved.push( m );
256
+ break;
257
+
258
+ }
259
+
260
+ }
261
+
262
+ if ( plan.visibility || lastVisible[ row ] === 2 ) {
263
+
264
+ let visible = 1;
265
+ for ( let n = mesh; n; n = n.parent ) if ( ! n.visible ) {
266
+
267
+ visible = 0;
268
+ break;
269
+
270
+ }
271
+
272
+ if ( lastVisible[ row ] !== visible ) {
273
+
274
+ lastVisible[ row ] = visible;
275
+ visibilityChanged = true;
276
+
277
+ }
278
+
279
+ }
280
+
281
+ }
282
+
283
+ if ( moved.length || visibilityChanged || plan.cameras.length ) {
284
+
285
+ this.applyPoseCallback?.( { meshIndices: moved, visibilityChanged, cameras: plan.cameras } );
286
+
287
+ }
125
288
 
126
289
  }
127
290
 
@@ -145,6 +308,7 @@ export class AnimationManager extends EventDispatcher {
145
308
 
146
309
  }
147
310
 
311
+ this._activeClip = clipIndex;
148
312
  this.timer.reset();
149
313
  this.isPlaying = true;
150
314
  this.wakeCallback?.();
@@ -188,10 +352,19 @@ export class AnimationManager extends EventDispatcher {
188
352
 
189
353
  if ( ! this.mixer ) return;
190
354
 
355
+ // Restores every animated property to its loaded value.
191
356
  this.mixer.stopAllAction();
192
357
  this.mixer.timeScale = this._savedTimeScale || 1;
193
358
  this.timer.reset();
194
359
  this.isPlaying = false;
360
+
361
+ if ( this._rigid ) {
362
+
363
+ this._mixerRoot.updateMatrixWorld( true );
364
+ this._applyRigidPose( - 1 );
365
+
366
+ }
367
+
195
368
  this.dispatchEvent( { type: EngineEvents.ANIMATION_STOPPED } );
196
369
 
197
370
  }
@@ -229,12 +402,15 @@ export class AnimationManager extends EventDispatcher {
229
402
  *
230
403
  * @param {number} time - Absolute time in seconds
231
404
  * @param {number} [clipIndex=0] - Clip to evaluate, or -1 for all active
232
- * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if no mixer
405
+ * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if no
406
+ * mixer or the clips are rigid
233
407
  */
234
408
  seekTo( time, clipIndex = 0 ) {
235
409
 
236
410
  if ( ! this.mixer || this.actions.length === 0 ) return null;
237
411
 
412
+ this._activeClip = clipIndex;
413
+
238
414
  // Ensure the target action(s) are active so setTime evaluates them.
239
415
  // Actions must NOT be paused — setTime() calls update() internally,
240
416
  // which skips paused actions entirely.
@@ -279,7 +455,8 @@ export class AnimationManager extends EventDispatcher {
279
455
  * Advance animation and prepare deformed positions.
280
456
  * Call once per frame from the animate loop.
281
457
  *
282
- * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if not playing
458
+ * @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if not
459
+ * playing or the clips are rigid
283
460
  */
284
461
  update() {
285
462
 
@@ -312,6 +489,16 @@ export class AnimationManager extends EventDispatcher {
312
489
  // once here, on mixerRoot rather than the scene, keeps unrelated static objects out of it.
313
490
  this._mixerRoot.updateMatrixWorld( true );
314
491
 
492
+ if ( this._rigid ) {
493
+
494
+ this._applyRigidPose( this._activeClip );
495
+ return null;
496
+
497
+ }
498
+
499
+ const cameras = this._planFor( this._activeClip )?.cameras;
500
+ if ( cameras?.length ) this.applyPoseCallback?.( { meshIndices: [], visibilityChanged: false, cameras } );
501
+
315
502
  return m => this._computeMeshPositions( m );
316
503
 
317
504
  }
@@ -446,6 +633,13 @@ export class AnimationManager extends EventDispatcher {
446
633
  this._meshPositions = null;
447
634
  this._skinnedCache = null;
448
635
  this._clipsCache = null;
636
+ this._rigid = false;
637
+ this._plans = null;
638
+ this._allPlan = null;
639
+ this._activeClip = 0;
640
+ this._slotOf = null;
641
+ this._lastWorld = null;
642
+ this._lastVisible = null;
449
643
 
450
644
  }
451
645
 
@@ -4,9 +4,9 @@ import { AIUpscaler } from '../Passes/AIUpscaler.js';
4
4
  import { EngineEvents } from '../EngineEvents.js';
5
5
  import { createLogger } from '../utils/Logger.js';
6
6
 
7
- // The neural passes live in `../dlss/` but report through the manager that drives them, so they
8
- // share one namespace: `rayzee.log.only( 'dlss' )` shows the whole chain.
9
- const dlssLog = createLogger( 'dlss' );
7
+ // The neural passes live in `../neural/` but report through the manager that drives them, so they
8
+ // share one namespace: `rayzee.log.only( 'neural' )` shows the whole chain.
9
+ const neuralLog = createLogger( 'neural' );
10
10
  import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS } from '../EngineDefaults.js';
11
11
 
12
12
  // A refresh slower than this is a slideshow, not a live view, so the cadence swaps to a cheaper
@@ -112,16 +112,16 @@ export class DenoisingManager extends EventDispatcher {
112
112
 
113
113
  this.denoiser = null;
114
114
  this.upscaler = null;
115
- // Which model the AI upscaler runs. 'esrgan' is the ONNX chain; 'dlss' is the neural
115
+ // Which model the AI upscaler runs. 'esrgan' is the ONNX chain; 'neural' is the neural
116
116
  // super-resolution pass, which is fixed at 2x and needs a denoised source.
117
117
  this.upscalerBackend = 'esrgan';
118
- this._dlssUpscaler = null;
118
+ this._neuralUpscaler = null;
119
119
  // The neural-rendering (detail) pass is independent of the upscaler: it changes appearance,
120
120
  // not resolution, and runs last because its result cannot be read back.
121
121
  this.neuralRendering = false;
122
122
  this.neuralRenderingSettings = {};
123
- this._dlssNeural = null;
124
- this._dlssModule = null;
123
+ this._retouch = null;
124
+ this._superResModule = null;
125
125
  this._neuralPostBusy = false;
126
126
 
127
127
  // The tier the finished image uses. The loaded tier is not always this one: while the
@@ -162,6 +162,7 @@ export class DenoisingManager extends EventDispatcher {
162
162
  // Bound event forwarding handlers (stored for removal on re-setup / dispose)
163
163
  this._denoiserStartHandler = null;
164
164
  this._denoiserEndHandler = null;
165
+ this._denoiserTileHandler = null;
165
166
  this._upscalerResChangedHandler = null;
166
167
  this._upscalerStartHandler = null;
167
168
  this._upscalerProgressHandler = null;
@@ -298,8 +299,21 @@ export class DenoisingManager extends EventDispatcher {
298
299
 
299
300
  };
300
301
 
302
+ // The loop has stopped by the time a final denoise runs, so each tile has to redraw the
303
+ // canvas itself. Not through the display-refresh callback: that restarts the loop, which a
304
+ // video export drives by hand.
305
+ this._denoiserTileHandler = e => {
306
+
307
+ if ( e.continuous ) return;
308
+ this._publishOutput();
309
+ const ctx = this.pipeline?.context;
310
+ if ( this._stages.compositor && ctx ) this._stages.compositor.render( ctx );
311
+
312
+ };
313
+
301
314
  this.denoiser.addEventListener( 'start', this._denoiserStartHandler );
302
315
  this.denoiser.addEventListener( 'end', this._denoiserEndHandler );
316
+ this.denoiser.addEventListener( 'tileProgress', this._denoiserTileHandler );
303
317
 
304
318
  }
305
319
 
@@ -804,20 +818,20 @@ export class DenoisingManager extends EventDispatcher {
804
818
  }
805
819
 
806
820
  /**
807
- * Selects the AI upscaler's model. 'dlss' is fixed at 2x and reads the denoised picture, so it
821
+ * Selects the AI upscaler's model. 'neural' is fixed at 2x and reads the denoised picture, so it
808
822
  * is a no-op without a denoiser — feeding it raw Monte-Carlo noise measurably loses to bilinear.
809
823
  *
810
- * @param {'esrgan'|'dlss'} backend
824
+ * @param {'esrgan'|'neural'} backend
811
825
  */
812
826
  setUpscalerBackend( backend ) {
813
827
 
814
- const next = backend === 'dlss' ? 'dlss' : 'esrgan';
828
+ const next = backend === 'neural' ? 'neural' : 'esrgan';
815
829
  if ( next === this.upscalerBackend ) return;
816
830
 
817
831
  this.upscalerBackend = next;
818
832
  // Whatever is on screen came from the model being switched away from.
819
833
  this._restoreRenderDisplay();
820
- if ( next !== 'dlss' ) this._releaseDLSSUpscaler();
834
+ if ( next !== 'neural' ) this._releaseNeuralUpscaler();
821
835
 
822
836
  }
823
837
 
@@ -847,15 +861,15 @@ export class DenoisingManager extends EventDispatcher {
847
861
 
848
862
  }
849
863
 
850
- _releaseDLSSUpscaler() {
864
+ _releaseNeuralUpscaler() {
851
865
 
852
- this._dlssUpscaler?.dispose();
853
- this._dlssUpscaler = null;
866
+ this._neuralUpscaler?.dispose();
867
+ this._neuralUpscaler = null;
854
868
 
855
869
  // The frame packer lives on the module, not on the upscaler, and holds two buffers the size
856
870
  // of the render. Nothing else reaches it, so this is its only release point. Guarded on the
857
871
  // module having been loaded, so a teardown never pulls in 1 MB of runtime to call a no-op.
858
- this._dlssModule?.releaseDenoisedReader();
872
+ this._superResModule?.releaseDenoisedReader();
859
873
 
860
874
  }
861
875
 
@@ -866,17 +880,17 @@ export class DenoisingManager extends EventDispatcher {
866
880
  if ( settings ) this.neuralRenderingSettings = { ...this.neuralRenderingSettings, ...settings };
867
881
  if ( ! this.neuralRendering ) {
868
882
 
869
- this._releaseDLSSNeural();
883
+ this._releaseNeuralRetouch();
870
884
  this._restoreRenderDisplay();
871
885
 
872
886
  }
873
887
 
874
888
  }
875
889
 
876
- _releaseDLSSNeural() {
890
+ _releaseNeuralRetouch() {
877
891
 
878
- this._dlssNeural?.dispose();
879
- this._dlssNeural = null;
892
+ this._retouch?.dispose();
893
+ this._retouch = null;
880
894
 
881
895
  }
882
896
 
@@ -894,7 +908,7 @@ export class DenoisingManager extends EventDispatcher {
894
908
  */
895
909
  async _runNeuralPost( isStillComplete ) {
896
910
 
897
- const wantSR = this.upscaler?.enabled && this.upscalerBackend === 'dlss';
911
+ const wantSR = this.upscaler?.enabled && this.upscalerBackend === 'neural';
898
912
  const wantNR = this.neuralRendering;
899
913
  if ( ! wantSR && ! wantNR ) return;
900
914
 
@@ -904,14 +918,14 @@ export class DenoisingManager extends EventDispatcher {
904
918
 
905
919
  if ( ! this.upscalerCanvas ) {
906
920
 
907
- dlssLog.warn( 'neural pass skipped: the engine has no overlay canvas to present on' );
921
+ neuralLog.warn( 'neural pass skipped: the engine has no overlay canvas to present on' );
908
922
  return;
909
923
 
910
924
  }
911
925
 
912
926
  if ( ! this.pipeline?.context?.getTexture( 'oidn:output' ) ) {
913
927
 
914
- dlssLog.warn( 'neural pass skipped: no denoised picture — it needs OIDN' );
928
+ neuralLog.warn( 'neural pass skipped: no denoised picture — it needs OIDN' );
915
929
  return;
916
930
 
917
931
  }
@@ -921,26 +935,26 @@ export class DenoisingManager extends EventDispatcher {
921
935
 
922
936
  try {
923
937
 
924
- const sr = await import( '../dlss/DLSSSuperRes.js' );
925
- this._dlssModule = sr;
938
+ const sr = await import( '../neural/NeuralSuperRes.js' );
939
+ this._superResModule = sr;
926
940
 
927
941
  // The detail pass has a size ceiling — above it a run takes minutes and then loses every GPU
928
942
  // device in the page. Running it FIRST is what keeps it under: at render size it sees a
929
943
  // quarter of the pixels it would after a 2x upscale, and the render reserve already caps
930
944
  // that at 2048. The check stays as a floor under a raised reserve.
931
- const nrModule = wantNR ? await import( '../dlss/DLSSNeural.js' ) : null;
945
+ const nrModule = wantNR ? await import( '../neural/NeuralRetouch.js' ) : null;
932
946
  const srcSize = this.denoiser?._outTexSize;
933
947
  const nrPixels = ( srcSize?.width ?? 0 ) * ( srcSize?.height ?? 0 );
934
- const runNR = wantNR && nrPixels <= nrModule.DLSS_NR_MAX_PIXELS;
948
+ const runNR = wantNR && nrPixels <= nrModule.RETOUCH_MAX_PIXELS;
935
949
 
936
950
  if ( wantNR && ! runNR ) {
937
951
 
938
- dlssLog.warn(
952
+ neuralLog.warn(
939
953
  `Neural rendering skipped: the render is ${( nrPixels / 1e6 ).toFixed( 1 )} MP, above the ` +
940
- `${( nrModule.DLSS_NR_MAX_PIXELS / 1e6 ).toFixed( 1 )} MP the pass survives.` +
954
+ `${( nrModule.RETOUCH_MAX_PIXELS / 1e6 ).toFixed( 1 )} MP the pass survives.` +
941
955
  ( wantSR ? ' The upscale still ran.' : '' )
942
956
  );
943
- this._releaseDLSSNeural();
957
+ this._releaseNeuralRetouch();
944
958
 
945
959
  }
946
960
 
@@ -961,16 +975,16 @@ export class DenoisingManager extends EventDispatcher {
961
975
 
962
976
  if ( wantSR ) {
963
977
 
964
- if ( this._dlssUpscaler
965
- && ( this._dlssUpscaler.inputWidth !== source.width || this._dlssUpscaler.inputHeight !== source.height ) ) {
978
+ if ( this._neuralUpscaler
979
+ && ( this._neuralUpscaler.inputWidth !== source.width || this._neuralUpscaler.inputHeight !== source.height ) ) {
966
980
 
967
- this._releaseDLSSUpscaler();
981
+ this._releaseNeuralUpscaler();
968
982
 
969
983
  }
970
984
 
971
- if ( ! this._dlssUpscaler ) {
985
+ if ( ! this._neuralUpscaler ) {
972
986
 
973
- this._dlssUpscaler = await sr.DLSSSuperRes.create( { width: source.width, height: source.height } );
987
+ this._neuralUpscaler = await sr.NeuralSuperRes.create( { width: source.width, height: source.height } );
974
988
 
975
989
  }
976
990
 
@@ -979,23 +993,23 @@ export class DenoisingManager extends EventDispatcher {
979
993
  let image = source;
980
994
 
981
995
  // Detail first, upscale second. The detail pass hands back scene-referred light (see
982
- // `app/public/dlss/PATCHES.md`), so the upscaler still gets the linear HDR it expects —
996
+ // the runtime's `rayzee-patch` edits), so the upscaler still gets the linear HDR it expects —
983
997
  // which is what makes this order possible at all.
984
998
  if ( runNR ) {
985
999
 
986
1000
  const result = await nrModule.enhanceFrame( {
987
1001
  source: image,
988
1002
  settings: this.neuralRenderingSettings,
989
- instance: this._dlssNeural,
1003
+ instance: this._retouch,
990
1004
  exposure: tone.exposure,
991
1005
  tone: wantSR ? null : tone,
992
1006
  } );
993
- this._dlssNeural = result.instance;
1007
+ this._retouch = result.instance;
994
1008
  image = result;
995
1009
 
996
1010
  }
997
1011
 
998
- if ( wantSR ) image = await this._dlssUpscaler.upscaleToRGBA8( image, tone );
1012
+ if ( wantSR ) image = await this._neuralUpscaler.upscaleToRGBA8( image, tone );
999
1013
 
1000
1014
  if ( ! isStillComplete() ) {
1001
1015
 
@@ -1024,7 +1038,7 @@ export class DenoisingManager extends EventDispatcher {
1024
1038
 
1025
1039
  } catch ( error ) {
1026
1040
 
1027
- dlssLog.warn( 'neural pass failed', error );
1041
+ neuralLog.warn( 'neural pass failed', error );
1028
1042
 
1029
1043
  } finally {
1030
1044
 
@@ -1060,7 +1074,7 @@ export class DenoisingManager extends EventDispatcher {
1060
1074
 
1061
1075
  // One owner of the overlay canvas, so the neural chain and the ONNX upscaler are
1062
1076
  // exclusive. The neural chain also runs on its own when only its detail pass is on.
1063
- if ( this.neuralRendering || ( this.upscaler?.enabled && this.upscalerBackend === 'dlss' ) ) {
1077
+ if ( this.neuralRendering || ( this.upscaler?.enabled && this.upscalerBackend === 'neural' ) ) {
1064
1078
 
1065
1079
  this._runNeuralPost( isStillComplete );
1066
1080
  return;
@@ -1184,8 +1198,8 @@ export class DenoisingManager extends EventDispatcher {
1184
1198
  // Remove pending completion-chain listener
1185
1199
  this._cleanupCompletionListener();
1186
1200
  // Owns a second GPUDevice plus 3 MB of weights, so it must not outlive the manager.
1187
- this._releaseDLSSUpscaler();
1188
- this._releaseDLSSNeural();
1201
+ this._releaseNeuralUpscaler();
1202
+ this._releaseNeuralRetouch();
1189
1203
 
1190
1204
  // Before the denoiser destroys the picture the Compositor would otherwise still sample.
1191
1205
  this._unpublishOutput();
@@ -1194,6 +1208,7 @@ export class DenoisingManager extends EventDispatcher {
1194
1208
 
1195
1209
  if ( this._denoiserStartHandler ) this.denoiser.removeEventListener( 'start', this._denoiserStartHandler );
1196
1210
  if ( this._denoiserEndHandler ) this.denoiser.removeEventListener( 'end', this._denoiserEndHandler );
1211
+ if ( this._denoiserTileHandler ) this.denoiser.removeEventListener( 'tileProgress', this._denoiserTileHandler );
1197
1212
  this.denoiser.dispose();
1198
1213
  this.denoiser = null;
1199
1214
 
@@ -1207,15 +1222,16 @@ export class DenoisingManager extends EventDispatcher {
1207
1222
  if ( this._upscalerEndHandler ) this.upscaler.removeEventListener( 'end', this._upscalerEndHandler );
1208
1223
  this.upscaler.dispose();
1209
1224
  this.upscaler = null;
1210
- // Which model the AI upscaler runs. 'esrgan' is the ONNX chain; 'dlss' is the neural
1225
+ // Which model the AI upscaler runs. 'esrgan' is the ONNX chain; 'neural' is the neural
1211
1226
  // super-resolution pass, which is fixed at 2x and needs a denoised source.
1212
1227
  this.upscalerBackend = 'esrgan';
1213
- this._dlssUpscaler = null;
1228
+ this._neuralUpscaler = null;
1214
1229
 
1215
1230
  }
1216
1231
 
1217
1232
  this._denoiserStartHandler = null;
1218
1233
  this._denoiserEndHandler = null;
1234
+ this._denoiserTileHandler = null;
1219
1235
  this._upscalerResChangedHandler = null;
1220
1236
  this._upscalerStartHandler = null;
1221
1237
  this._upscalerProgressHandler = null;