rayzee 8.2.0 → 8.4.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.
@@ -2,6 +2,11 @@ import { EventDispatcher } from 'three';
2
2
  import { OIDNDenoiser } from '../Passes/OIDNDenoiser.js';
3
3
  import { AIUpscaler } from '../Passes/AIUpscaler.js';
4
4
  import { EngineEvents } from '../EngineEvents.js';
5
+ import { createLogger } from '../utils/Logger.js';
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' );
5
10
  import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS } from '../EngineDefaults.js';
6
11
 
7
12
  // A refresh slower than this is a slideshow, not a live view, so the cadence swaps to a cheaper
@@ -107,6 +112,17 @@ export class DenoisingManager extends EventDispatcher {
107
112
 
108
113
  this.denoiser = null;
109
114
  this.upscaler = null;
115
+ // Which model the AI upscaler runs. 'esrgan' is the ONNX chain; 'dlss' is the neural
116
+ // super-resolution pass, which is fixed at 2x and needs a denoised source.
117
+ this.upscalerBackend = 'esrgan';
118
+ this._dlssUpscaler = null;
119
+ // The neural-rendering (detail) pass is independent of the upscaler: it changes appearance,
120
+ // not resolution, and runs last because its result cannot be read back.
121
+ this.neuralRendering = false;
122
+ this.neuralRenderingSettings = {};
123
+ this._dlssNeural = null;
124
+ this._dlssModule = null;
125
+ this._neuralPostBusy = false;
110
126
 
111
127
  // The tier the finished image uses. The loaded tier is not always this one: while the
112
128
  // image is still accumulating we run a cheaper model (see previewQuality).
@@ -786,6 +802,242 @@ export class DenoisingManager extends EventDispatcher {
786
802
 
787
803
  }
788
804
 
805
+ /**
806
+ * Selects the AI upscaler's model. 'dlss' is fixed at 2x and reads the denoised picture, so it
807
+ * is a no-op without a denoiser — feeding it raw Monte-Carlo noise measurably loses to bilinear.
808
+ *
809
+ * @param {'esrgan'|'dlss'} backend
810
+ */
811
+ setUpscalerBackend( backend ) {
812
+
813
+ const next = backend === 'dlss' ? 'dlss' : 'esrgan';
814
+ if ( next === this.upscalerBackend ) return;
815
+
816
+ this.upscalerBackend = next;
817
+ // Whatever is on screen came from the model being switched away from.
818
+ this._restoreRenderDisplay();
819
+ if ( next !== 'dlss' ) this._releaseDLSSUpscaler();
820
+
821
+ }
822
+
823
+ /**
824
+ * Puts the render canvas back in front and tells the host the output is the render size again.
825
+ *
826
+ * Guarded on the overlay actually being visible: `abort()` runs on every reset, which is every
827
+ * frame of a camera drag, and an unguarded event there would be a per-frame storm.
828
+ *
829
+ * @returns {boolean} whether anything was actually restored
830
+ */
831
+ _restoreRenderDisplay() {
832
+
833
+ const wasShowing = this.upscalerCanvas && this.upscalerCanvas.style.display !== 'none';
834
+
835
+ if ( this.upscalerCanvas ) this.upscalerCanvas.style.display = 'none';
836
+ if ( this.mainCanvas ) this.mainCanvas.style.opacity = '1';
837
+
838
+ if ( wasShowing ) {
839
+
840
+ const stage = this._stages?.pathTracer;
841
+ if ( stage?.width && stage?.height ) {
842
+
843
+ this.dispatchEvent( { type: 'resolution_changed', width: stage.width, height: stage.height } );
844
+
845
+ }
846
+
847
+ }
848
+
849
+ return !! wasShowing;
850
+
851
+ }
852
+
853
+ _releaseDLSSUpscaler() {
854
+
855
+ this._dlssUpscaler?.dispose();
856
+ this._dlssUpscaler = null;
857
+
858
+ // The frame packer lives on the module, not on the upscaler, and holds two buffers the size
859
+ // of the render. Nothing else reaches it, so this is its only release point. Guarded on the
860
+ // module having been loaded, so a teardown never pulls in 1 MB of runtime to call a no-op.
861
+ this._dlssModule?.releaseDenoisedReader();
862
+
863
+ }
864
+
865
+ /** Turns the neural-rendering (detail) pass on or off, and sets its appearance controls. */
866
+ setNeuralRendering( enabled, settings = null ) {
867
+
868
+ this.neuralRendering = !! enabled;
869
+ if ( settings ) this.neuralRenderingSettings = { ...this.neuralRenderingSettings, ...settings };
870
+ if ( ! this.neuralRendering ) {
871
+
872
+ this._releaseDLSSNeural();
873
+ this._restoreRenderDisplay();
874
+
875
+ }
876
+
877
+ }
878
+
879
+ _releaseDLSSNeural() {
880
+
881
+ this._dlssNeural?.dispose();
882
+ this._dlssNeural = null;
883
+
884
+ }
885
+
886
+ /**
887
+ * The neural post-chain over the picture the closing denoise just produced: super resolution
888
+ * first, then the detail pass, either or both.
889
+ *
890
+ * Order is forced by the models. Super resolution hands back light that can be fed onward; the
891
+ * detail pass writes an `rgba8unorm` texture with no `COPY_SRC`, so nothing can read its result
892
+ * and it has to be last — and it presents in its own display space rather than the engine's tone
893
+ * curve. Which shape super resolution returns therefore depends on what follows it: packed
894
+ * halves when the detail pass is next, display bytes when it is the end of the chain.
895
+ *
896
+ * Deliberately fire-and-forget: the render is already finished and a failure must not break it.
897
+ */
898
+ async _runNeuralPost( isStillComplete ) {
899
+
900
+ const wantSR = this.upscaler?.enabled && this.upscalerBackend === 'dlss';
901
+ const wantNR = this.neuralRendering;
902
+ if ( ! wantSR && ! wantNR ) return;
903
+
904
+ // A second completion can land while the first pass is still in flight — the models are
905
+ // created lazily and reused through fields, so two runs would race over the same handles.
906
+ if ( this._neuralPostBusy ) return;
907
+
908
+ if ( ! this.upscalerCanvas ) {
909
+
910
+ dlssLog.warn( 'neural pass skipped: the engine has no overlay canvas to present on' );
911
+ return;
912
+
913
+ }
914
+
915
+ if ( ! this.pipeline?.context?.getTexture( 'oidn:output' ) ) {
916
+
917
+ dlssLog.warn( 'neural pass skipped: no denoised picture — it needs OIDN' );
918
+ return;
919
+
920
+ }
921
+
922
+ this._neuralPostBusy = true;
923
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_START } );
924
+
925
+ try {
926
+
927
+ const sr = await import( '../dlss/DLSSSuperRes.js' );
928
+ this._dlssModule = sr;
929
+
930
+ // The detail pass has a size ceiling — above it a run takes minutes and then loses every GPU
931
+ // device in the page. Running it FIRST is what keeps it under: at render size it sees a
932
+ // quarter of the pixels it would after a 2x upscale, and the render reserve already caps
933
+ // that at 2048. The check stays as a floor under a raised reserve.
934
+ const nrModule = wantNR ? await import( '../dlss/DLSSNeural.js' ) : null;
935
+ const srcSize = this.denoiser?._outTexSize;
936
+ const nrPixels = ( srcSize?.width ?? 0 ) * ( srcSize?.height ?? 0 );
937
+ const runNR = wantNR && nrPixels <= nrModule.DLSS_NR_MAX_PIXELS;
938
+
939
+ if ( wantNR && ! runNR ) {
940
+
941
+ dlssLog.warn(
942
+ `Neural rendering skipped: the render is ${( nrPixels / 1e6 ).toFixed( 1 )} MP, above the ` +
943
+ `${( nrModule.DLSS_NR_MAX_PIXELS / 1e6 ).toFixed( 1 )} MP the pass survives.` +
944
+ ( wantSR ? ' The upscale still ran.' : '' )
945
+ );
946
+ this._releaseDLSSNeural();
947
+
948
+ }
949
+
950
+ if ( ! wantSR && ! runNR ) return;
951
+
952
+ // Must be the effective value, not the settings one: AutoExposure overwrites
953
+ // `renderer.toneMappingExposure` every frame and never touches settings, so reading
954
+ // settings — as this did — silently ignores auto-exposure. Same helper OIDN uses.
955
+ const tone = {
956
+ exposure: this._getEffectiveExposure(),
957
+ toneMapping: this._getToneMapping(),
958
+ saturation: this._getSaturation?.() ?? 1,
959
+ };
960
+
961
+ // Scene-referred throughout. Exposure reaches the detail pass through its own `paper_white`
962
+ // and comes back out again, so the engine's tone curve is still the only one applied.
963
+ const source = await sr.readDenoisedHalf( this.denoiser );
964
+
965
+ if ( wantSR ) {
966
+
967
+ if ( this._dlssUpscaler
968
+ && ( this._dlssUpscaler.inputWidth !== source.width || this._dlssUpscaler.inputHeight !== source.height ) ) {
969
+
970
+ this._releaseDLSSUpscaler();
971
+
972
+ }
973
+
974
+ if ( ! this._dlssUpscaler ) {
975
+
976
+ this._dlssUpscaler = await sr.DLSSSuperRes.create( { width: source.width, height: source.height } );
977
+
978
+ }
979
+
980
+ }
981
+
982
+ let image = source;
983
+
984
+ // Detail first, upscale second. The detail pass hands back scene-referred light (see
985
+ // `app/public/dlss/PATCHES.md`), so the upscaler still gets the linear HDR it expects —
986
+ // which is what makes this order possible at all.
987
+ if ( runNR ) {
988
+
989
+ const result = await nrModule.enhanceFrame( {
990
+ source: image,
991
+ settings: this.neuralRenderingSettings,
992
+ instance: this._dlssNeural,
993
+ exposure: tone.exposure,
994
+ tone: wantSR ? null : tone,
995
+ } );
996
+ this._dlssNeural = result.instance;
997
+ image = result;
998
+
999
+ }
1000
+
1001
+ if ( wantSR ) image = await this._dlssUpscaler.upscaleToRGBA8( image, tone );
1002
+
1003
+ if ( ! isStillComplete() ) {
1004
+
1005
+ this.dropDisplay();
1006
+ return;
1007
+
1008
+ }
1009
+
1010
+ sr.presentRGBA8( this.upscalerCanvas, image.rgba8, image.width, image.height );
1011
+
1012
+ if ( ! isStillComplete() ) {
1013
+
1014
+ this.dropDisplay();
1015
+ return;
1016
+
1017
+ }
1018
+
1019
+ // The 2D canvas sits BEHIND the render canvas in the DOM (`insertBefore`), so making it
1020
+ // `display:block` is not enough — the render canvas has to get out of the way. This is
1021
+ // what `AIUpscaler._revealOutput` does for the other backend, and `abort()` already
1022
+ // restores both sides.
1023
+ if ( this.mainCanvas ) this.mainCanvas.style.opacity = '0';
1024
+
1025
+ // The host shows the output dimensions; without this they keep reading the render size.
1026
+ this.dispatchEvent( { type: 'resolution_changed', width: image.width, height: image.height } );
1027
+
1028
+ } catch ( error ) {
1029
+
1030
+ dlssLog.warn( 'neural pass failed', error );
1031
+
1032
+ } finally {
1033
+
1034
+ this._neuralPostBusy = false;
1035
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_END } );
1036
+
1037
+ }
1038
+
1039
+ }
1040
+
789
1041
  onRenderComplete( { isStillComplete, context } ) {
790
1042
 
791
1043
  // Remove any stale completion-chain listener from a previous render cycle
@@ -805,12 +1057,17 @@ export class DenoisingManager extends EventDispatcher {
805
1057
 
806
1058
  if ( ! isStillComplete() ) return;
807
1059
 
808
- if ( this.upscaler?.enabled ) {
1060
+ // One owner of the overlay canvas, so the neural chain and the ONNX upscaler are
1061
+ // exclusive. The neural chain also runs on its own when only its detail pass is on.
1062
+ if ( this.neuralRendering || ( this.upscaler?.enabled && this.upscalerBackend === 'dlss' ) ) {
809
1063
 
810
- this.upscaler.start();
1064
+ this._runNeuralPost( isStillComplete );
1065
+ return;
811
1066
 
812
1067
  }
813
1068
 
1069
+ if ( this.upscaler?.enabled ) this.upscaler.start();
1070
+
814
1071
  };
815
1072
 
816
1073
  if ( ! closing ) {
@@ -887,8 +1144,8 @@ export class DenoisingManager extends EventDispatcher {
887
1144
 
888
1145
  // The 2D canvas is the upscaler's alone: a reset means its enlarged result no longer
889
1146
  // describes anything, so the render canvas comes back.
890
- if ( mainCanvas ) mainCanvas.style.opacity = '1';
891
- if ( this.upscalerCanvas ) this.upscalerCanvas.style.display = 'none';
1147
+ this._restoreRenderDisplay();
1148
+ if ( mainCanvas && mainCanvas !== this.mainCanvas ) mainCanvas.style.opacity = '1';
892
1149
  if ( this.upscaler ) this.upscaler.abort();
893
1150
 
894
1151
  const moving = !! this._stages.pathTracer?.viewIsChanging;
@@ -917,9 +1174,7 @@ export class DenoisingManager extends EventDispatcher {
917
1174
  dropDisplay() {
918
1175
 
919
1176
  this._unpublishOutput();
920
-
921
- if ( this.upscalerCanvas ) this.upscalerCanvas.style.display = 'none';
922
- if ( this.mainCanvas ) this.mainCanvas.style.opacity = '1';
1177
+ this._restoreRenderDisplay();
923
1178
 
924
1179
  }
925
1180
 
@@ -927,6 +1182,10 @@ export class DenoisingManager extends EventDispatcher {
927
1182
 
928
1183
  // Remove pending completion-chain listener
929
1184
  this._cleanupCompletionListener();
1185
+ // Owns a second GPUDevice plus 3 MB of weights, so it must not outlive the manager.
1186
+ this._releaseDLSSUpscaler();
1187
+ this._releaseDLSSNeural();
1188
+
930
1189
  // Before the denoiser destroys the picture the Compositor would otherwise still sample.
931
1190
  this._unpublishOutput();
932
1191
 
@@ -947,6 +1206,10 @@ export class DenoisingManager extends EventDispatcher {
947
1206
  if ( this._upscalerEndHandler ) this.upscaler.removeEventListener( 'end', this._upscalerEndHandler );
948
1207
  this.upscaler.dispose();
949
1208
  this.upscaler = null;
1209
+ // Which model the AI upscaler runs. 'esrgan' is the ONNX chain; 'dlss' is the neural
1210
+ // super-resolution pass, which is fixed at 2x and needs a denoised source.
1211
+ this.upscalerBackend = 'esrgan';
1212
+ this._dlssUpscaler = null;
950
1213
 
951
1214
  }
952
1215
 
@@ -1128,6 +1391,8 @@ export class DenoisingManager extends EventDispatcher {
1128
1391
  setUpscalerEnabled( enabled ) {
1129
1392
 
1130
1393
  if ( this.upscaler ) this.upscaler.enabled = enabled;
1394
+ // The enlarged picture describes a setting that is no longer on.
1395
+ if ( ! enabled ) this._restoreRenderDisplay();
1131
1396
 
1132
1397
  }
1133
1398