rayzee 8.0.0 → 8.2.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.
- package/dist/rayzee.es.js +4413 -4411
- package/dist/rayzee.umd.js +80 -215
- package/package.json +2 -2
- package/src/Passes/OIDNDenoiser.js +196 -270
- package/src/PathTracerApp.js +73 -19
- package/src/Processor/GeometryExtractor.js +3 -1
- package/src/Processor/utils.js +8 -20
- package/src/Stages/Compositor.js +1 -0
- package/src/Stages/PathTracerStage.js +25 -0
- package/src/TSL/SortGlobalKernels.js +3 -4
- package/src/TSL/patches.js +22 -82
- package/src/managers/DenoisingManager.js +276 -58
- package/src/managers/VideoRenderManager.js +4 -0
- package/src/Processor/ToneMapWGSL.js +0 -162
|
@@ -27,6 +27,47 @@ const CADENCE_COST_BUDGET_MS = 120;
|
|
|
27
27
|
// refreshes — 2x is the better trade while a person is watching the image resolve.
|
|
28
28
|
const CADENCE_DENOISE_PERIODS = 2;
|
|
29
29
|
|
|
30
|
+
// The first refresh of a fresh accumulation is the one replacing a held frame that is now stale, so
|
|
31
|
+
// it waits one denoise rather than two. Measured over a 2 s reset storm (a slider being dragged):
|
|
32
|
+
// 8.4 -> 12.5-15.3 refreshes/sec for 75.5 -> 70-73 fps.
|
|
33
|
+
const CADENCE_FIRST_REFRESH_PERIODS = 1;
|
|
34
|
+
|
|
35
|
+
// While the camera moves, the denoised frames ARE the picture — the raw render is never shown — so
|
|
36
|
+
// the refresh rate is the frame rate a person sees, and back to back is the right cadence. Path
|
|
37
|
+
// tracing costs little there (1 bounce, no accumulation), and `isDenoising` serialises the runs.
|
|
38
|
+
const CADENCE_INTERACTION_PERIODS = 0;
|
|
39
|
+
|
|
40
|
+
// A denoise slower than this cannot follow a camera: holding the last one would turn navigation
|
|
41
|
+
// into a slideshow, so it goes back to the raw render instead. Measured on a 1.9M-triangle scene
|
|
42
|
+
// at `fast`: 512² 59 ms (35 refreshes/sec while moving, 121 fps), 1024² 135 ms (7/sec, 47 fps),
|
|
43
|
+
// 1536² 1.46 s. Set to keep 1024² on the clean side of the line and reject 1536².
|
|
44
|
+
const INTERACTION_HOLD_BUDGET_MS = 300;
|
|
45
|
+
|
|
46
|
+
// How many refresh timings the navigation decision is taken over. A single denoise is easily twice
|
|
47
|
+
// its own typical cost — a GC pause, a texture upload, another tab — and deciding on one of those
|
|
48
|
+
// left the next camera move entirely raw. The median of a few is stable and still cheap.
|
|
49
|
+
//
|
|
50
|
+
// Only refreshes taken WHILE MOVING count: that is a different regime, because the render loop
|
|
51
|
+
// stops tracing frames nobody will see (skipsTrace) and hands the GPU to the denoise. Measured
|
|
52
|
+
// inside a room at 1024², the same denoise is 596 ms with the loop tracing over it and ~95 ms
|
|
53
|
+
// without, so a still-camera timing would rule out navigation that in fact works.
|
|
54
|
+
//
|
|
55
|
+
// Taken as the MINIMUM, not the median, and reset for each move. The first refresh of a move is
|
|
56
|
+
// always slow — the GPU is still finishing the frame before it — and the estimate has to survive
|
|
57
|
+
// that, because a demoted move stops refreshing and so stops measuring: one unlucky reading used
|
|
58
|
+
// to pin navigation to the raw render for the rest of the session.
|
|
59
|
+
const DENOISE_COST_SAMPLES = 5;
|
|
60
|
+
|
|
61
|
+
// A refresh this far past the budget is not a warm-up reading, it is the wrong resolution for
|
|
62
|
+
// denoised navigation. Remembered until the size changes, so only the first move pays for finding
|
|
63
|
+
// out; anything short of it gets a second reading before navigation is given back to the raw render.
|
|
64
|
+
const INTERACTION_HOPELESS_FACTOR = 3;
|
|
65
|
+
|
|
66
|
+
// Consecutive refreshes that deliver nothing before the held frame is given up and the raw render
|
|
67
|
+
// comes back. Counted in runs, not milliseconds: the loop stops while a finished render sits on
|
|
68
|
+
// screen, so any clock would read a perfectly good frame as abandoned the moment it woke.
|
|
69
|
+
const HELD_FRAME_MAX_FAILURES = 3;
|
|
70
|
+
|
|
30
71
|
/**
|
|
31
72
|
* Orchestrates all denoising, post-processing, and AI upscaling:
|
|
32
73
|
* - Real-time denoiser strategy switching (ASVGF / NRD / EdgeAware / None)
|
|
@@ -44,23 +85,18 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
44
85
|
* @param {Object} params
|
|
45
86
|
* @param {import('three/webgpu').WebGPURenderer} params.renderer
|
|
46
87
|
* @param {HTMLCanvasElement} params.mainCanvas - The primary rendering canvas
|
|
47
|
-
* @param {import('three').Scene} params.scene
|
|
48
|
-
* @param {import('three').PerspectiveCamera} params.camera
|
|
49
88
|
* @param {Object} params.stages - Named references to pipeline stages
|
|
50
89
|
* @param {import('../Pipeline/RenderPipeline.js').RenderPipeline} params.pipeline
|
|
51
90
|
* @param {Function} params.getExposure - () => current exposure value
|
|
52
91
|
* @param {Function} params.getSaturation - () => current saturation value
|
|
53
|
-
* @param {Function} params.getTransparentBg - () => boolean
|
|
54
92
|
*/
|
|
55
|
-
constructor( { renderer, mainCanvas,
|
|
93
|
+
constructor( { renderer, mainCanvas, stages, pipeline, getExposure, getSaturation } ) {
|
|
56
94
|
|
|
57
95
|
super();
|
|
58
96
|
|
|
59
97
|
this.renderer = renderer;
|
|
60
98
|
this.mainCanvas = mainCanvas;
|
|
61
|
-
this.
|
|
62
|
-
this.scene = scene;
|
|
63
|
-
this.camera = camera;
|
|
99
|
+
this.upscalerCanvas = this._createUpscalerCanvas( mainCanvas );
|
|
64
100
|
this.pipeline = pipeline;
|
|
65
101
|
|
|
66
102
|
// Stage references — only used internally for orchestration
|
|
@@ -68,7 +104,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
68
104
|
|
|
69
105
|
this._getExposure = getExposure;
|
|
70
106
|
this._getSaturation = getSaturation;
|
|
71
|
-
this._getTransparentBg = getTransparentBg;
|
|
72
107
|
|
|
73
108
|
this.denoiser = null;
|
|
74
109
|
this.upscaler = null;
|
|
@@ -86,6 +121,16 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
86
121
|
// denoise while performance.now() is still below the interval.
|
|
87
122
|
this._lastCadenceAt = - Infinity;
|
|
88
123
|
this._lastCadenceSamples = 0;
|
|
124
|
+
// Consecutive refreshes that delivered nothing, for the held-frame health check.
|
|
125
|
+
this._failedRefreshes = 0;
|
|
126
|
+
// A final render suspends the live-view refresh without forgetting that the host chose it.
|
|
127
|
+
this._cadenceSuspended = false;
|
|
128
|
+
// Refresh timings from the current camera move, and whether this resolution has already
|
|
129
|
+
// proved far too slow to navigate denoised.
|
|
130
|
+
this._movingDenoiseMs = [];
|
|
131
|
+
this._movingHopeless = false;
|
|
132
|
+
// Whether this camera move is being shown denoised. Decided once per move; null between.
|
|
133
|
+
this._holdWhileMoving = null;
|
|
89
134
|
this._onReset = null;
|
|
90
135
|
this._onPostProcessRefresh = null;
|
|
91
136
|
|
|
@@ -107,7 +152,12 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
107
152
|
|
|
108
153
|
}
|
|
109
154
|
|
|
110
|
-
|
|
155
|
+
/**
|
|
156
|
+
* The one canvas the engine keeps besides the renderer's own, for the AI upscaler: it works in
|
|
157
|
+
* ordinary pixels and shows a picture larger than the render, neither of which the renderer's
|
|
158
|
+
* canvas can do. Hidden until the upscaler has something to show.
|
|
159
|
+
*/
|
|
160
|
+
_createUpscalerCanvas( mainCanvas ) {
|
|
111
161
|
|
|
112
162
|
const parent = mainCanvas.parentNode;
|
|
113
163
|
if ( ! parent ) return null;
|
|
@@ -119,6 +169,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
119
169
|
dc.style.inset = '0';
|
|
120
170
|
dc.style.width = '100%';
|
|
121
171
|
dc.style.height = '100%';
|
|
172
|
+
dc.style.display = 'none';
|
|
122
173
|
|
|
123
174
|
parent.insertBefore( dc, mainCanvas );
|
|
124
175
|
return dc;
|
|
@@ -139,27 +190,48 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
139
190
|
// Cleared only when the tier or the resolution changes — a denoise costs 14 ms at 512²
|
|
140
191
|
// and 800 ms at 2048², so the affordability verdict does not survive a resize.
|
|
141
192
|
this._cadenceDowngraded = false;
|
|
193
|
+
this._holdWhileMoving = null;
|
|
194
|
+
this._movingDenoiseMs.length = 0;
|
|
195
|
+
this._movingHopeless = false;
|
|
196
|
+
// A run in flight was sized for the old resolution and setSize rebuilds the network under
|
|
197
|
+
// it. Resets no longer cancel a run while its frame is on screen, so this has to.
|
|
198
|
+
this.denoiser?.abort();
|
|
199
|
+
// The picture on screen is the old size, and the Compositor would stretch it over the new
|
|
200
|
+
// frame until a denoise lands. Back to the raw render until then.
|
|
201
|
+
this._unpublishOutput();
|
|
142
202
|
this.denoiser?.setSize( width, height );
|
|
203
|
+
|
|
204
|
+
// The 2D canvas exists for the upscaler alone, and sizing it here keeps that ownership in
|
|
205
|
+
// one place. Assigning width or height clears it, so only do it when it actually changed.
|
|
206
|
+
if ( this.upscalerCanvas && ( this.upscalerCanvas.width !== width || this.upscalerCanvas.height !== height ) ) {
|
|
207
|
+
|
|
208
|
+
this.upscalerCanvas.width = width;
|
|
209
|
+
this.upscalerCanvas.height = height;
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
|
|
143
213
|
this.upscaler?.setBaseSize( width, height );
|
|
144
214
|
|
|
145
215
|
}
|
|
146
216
|
|
|
147
217
|
|
|
148
218
|
/**
|
|
149
|
-
*
|
|
219
|
+
* Puts the upscaler's canvas back to the render size — it leaves it two or four times larger.
|
|
150
220
|
* @returns {boolean} true if the canvas was resized
|
|
151
221
|
*/
|
|
152
222
|
restoreBaseResolution() {
|
|
153
223
|
|
|
154
|
-
if ( ! this.
|
|
224
|
+
if ( ! this.upscalerCanvas || ! this._lastRenderWidth || ! this._lastRenderHeight ) return false;
|
|
155
225
|
|
|
156
|
-
const wasResized = this.
|
|
157
|
-
|| this.
|
|
226
|
+
const wasResized = this.upscalerCanvas.width !== this._lastRenderWidth
|
|
227
|
+
|| this.upscalerCanvas.height !== this._lastRenderHeight;
|
|
158
228
|
|
|
159
|
-
|
|
160
|
-
this.denoiserCanvas.height = this._lastRenderHeight;
|
|
229
|
+
if ( ! wasResized ) return false;
|
|
161
230
|
|
|
162
|
-
|
|
231
|
+
this.upscalerCanvas.width = this._lastRenderWidth;
|
|
232
|
+
this.upscalerCanvas.height = this._lastRenderHeight;
|
|
233
|
+
|
|
234
|
+
return true;
|
|
163
235
|
|
|
164
236
|
}
|
|
165
237
|
|
|
@@ -168,11 +240,14 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
168
240
|
*/
|
|
169
241
|
setupDenoiser() {
|
|
170
242
|
|
|
171
|
-
if ( ! this.
|
|
243
|
+
if ( ! this.upscalerCanvas ) return;
|
|
172
244
|
|
|
173
245
|
const pt = this._stages.pathTracer;
|
|
174
246
|
|
|
175
|
-
|
|
247
|
+
// No canvas: the denoiser hands its result to the pipeline as a picture, and the
|
|
248
|
+
// Compositor decides what the single canvas shows. The exposure, grade and tone curve
|
|
249
|
+
// come from the renderer's own output pass, so it is not told about them either.
|
|
250
|
+
this.denoiser = new OIDNDenoiser( this.renderer, {
|
|
176
251
|
...DEFAULT_STATE,
|
|
177
252
|
|
|
178
253
|
backendParams: () => ( {
|
|
@@ -180,13 +255,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
180
255
|
adapterInfo: null
|
|
181
256
|
} ),
|
|
182
257
|
|
|
183
|
-
refreshInput: () => {
|
|
184
|
-
|
|
185
|
-
const ctx = this.pipeline?.context;
|
|
186
|
-
if ( this._stages.compositor && ctx ) this._stages.compositor.render( ctx );
|
|
187
|
-
|
|
188
|
-
},
|
|
189
|
-
|
|
190
258
|
getGPUTextures: () => {
|
|
191
259
|
|
|
192
260
|
if ( ! pt?.storageTextures?.readTarget ) return null;
|
|
@@ -199,11 +267,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
199
267
|
};
|
|
200
268
|
|
|
201
269
|
},
|
|
202
|
-
|
|
203
|
-
getExposure: () => this._getEffectiveExposure(),
|
|
204
|
-
getToneMapping: () => this._getToneMapping(),
|
|
205
|
-
getSaturation: () => this._getSaturation(),
|
|
206
|
-
getTransparentBackground: () => this._getTransparentBg(),
|
|
207
270
|
} );
|
|
208
271
|
|
|
209
272
|
this._syncOIDNInUse();
|
|
@@ -211,8 +274,13 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
211
274
|
// Forward lifecycle events (store refs for removal on re-setup / dispose)
|
|
212
275
|
this._denoiserStartHandler = e =>
|
|
213
276
|
this.dispatchEvent( { type: EngineEvents.DENOISING_START, continuous: !! e.continuous } );
|
|
214
|
-
this._denoiserEndHandler = e =>
|
|
277
|
+
this._denoiserEndHandler = e => {
|
|
278
|
+
|
|
279
|
+
if ( this.denoiser?.hasOutput ) this._publishOutput();
|
|
215
280
|
this.dispatchEvent( { type: EngineEvents.DENOISING_END, continuous: !! e.continuous } );
|
|
281
|
+
|
|
282
|
+
};
|
|
283
|
+
|
|
216
284
|
this.denoiser.addEventListener( 'start', this._denoiserStartHandler );
|
|
217
285
|
this.denoiser.addEventListener( 'end', this._denoiserEndHandler );
|
|
218
286
|
|
|
@@ -223,20 +291,19 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
223
291
|
*/
|
|
224
292
|
setupUpscaler() {
|
|
225
293
|
|
|
226
|
-
if ( ! this.
|
|
294
|
+
if ( ! this.upscalerCanvas ) return;
|
|
227
295
|
|
|
228
296
|
const pt = this._stages.pathTracer;
|
|
229
297
|
|
|
230
|
-
this.upscaler = new AIUpscaler( this.
|
|
298
|
+
this.upscaler = new AIUpscaler( this.upscalerCanvas, this.renderer, {
|
|
231
299
|
scaleFactor: DEFAULT_STATE.upscalerScale || 2,
|
|
232
300
|
quality: DEFAULT_STATE.upscalerQuality || 'fast',
|
|
233
301
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
},
|
|
302
|
+
// One source: the render canvas shows whatever the Compositor picked — the denoised
|
|
303
|
+
// picture when OIDN is on, the raw render otherwise — so the upscaler always enlarges
|
|
304
|
+
// what the viewer is actually looking at. It used to read its own canvas when the
|
|
305
|
+
// denoiser was on, which only worked while the denoiser painted there.
|
|
306
|
+
getSourceCanvas: () => this.renderer.domElement,
|
|
240
307
|
|
|
241
308
|
refreshInput: () => {
|
|
242
309
|
|
|
@@ -514,6 +581,85 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
514
581
|
|
|
515
582
|
}
|
|
516
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Whether the view is moving and the denoiser is quick enough to be that view — in which case the
|
|
586
|
+
* raw render is never shown and every reset keeps the last denoised frame up.
|
|
587
|
+
*
|
|
588
|
+
* Decided once per move rather than per frame, which would flip the viewport between clean and
|
|
589
|
+
* noisy mid-drag. Nothing is known at the first move of a size, so it tries once and then knows.
|
|
590
|
+
* Demotion is the exception: a move that turns out to be a slideshow gives the view back rather
|
|
591
|
+
* than finishing at 1.5 refreshes a second.
|
|
592
|
+
*/
|
|
593
|
+
get holdsWhileMoving() {
|
|
594
|
+
|
|
595
|
+
if ( ! this._stages.pathTracer?.viewIsChanging ) return false;
|
|
596
|
+
if ( ! this.continuousDenoise || this._cadenceSuspended || ! this.denoiser?.enabled ) return false;
|
|
597
|
+
if ( this._movingHopeless ) return false;
|
|
598
|
+
|
|
599
|
+
const best = this.movingCostMs;
|
|
600
|
+
const seen = this._movingDenoiseMs.length;
|
|
601
|
+
|
|
602
|
+
if ( best > INTERACTION_HOLD_BUDGET_MS * INTERACTION_HOPELESS_FACTOR ) this._movingHopeless = true;
|
|
603
|
+
|
|
604
|
+
// One reading is a warm-up; two that are both over budget are the answer.
|
|
605
|
+
const affordable = ! this._movingHopeless && ( seen < 2 || best <= INTERACTION_HOLD_BUDGET_MS );
|
|
606
|
+
this._holdWhileMoving = this._holdWhileMoving === null ? affordable : this._holdWhileMoving && affordable;
|
|
607
|
+
|
|
608
|
+
return this._holdWhileMoving;
|
|
609
|
+
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Whether the render loop should leave this frame untraced. True only while the denoised frame
|
|
614
|
+
* is the live view and a denoise is running — the one case where a traced frame reaches nobody:
|
|
615
|
+
* accumulation is off while the camera moves, the canvas is hidden, and the next denoise reads
|
|
616
|
+
* whatever the newest frame is. Measured inside a room at 512²: 19 -> 32 refreshes/sec.
|
|
617
|
+
*/
|
|
618
|
+
skipsTrace() {
|
|
619
|
+
|
|
620
|
+
return this.holdsWhileMoving && !! this.denoiser?.state.isDenoising;
|
|
621
|
+
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// The best a refresh has managed during this camera move. Zero until one has been timed, which
|
|
625
|
+
// reads as free and buys a first attempt rather than a verdict from the wrong regime.
|
|
626
|
+
get movingCostMs() {
|
|
627
|
+
|
|
628
|
+
return this._movingDenoiseMs.length ? Math.min( ...this._movingDenoiseMs ) : 0;
|
|
629
|
+
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Hands the denoised picture to the pipeline. The Compositor prefers it over the raw render
|
|
634
|
+
* from the next frame on, and keeps drawing it until it is taken away again — which is what
|
|
635
|
+
* makes "hold the last clean frame" free rather than a rule.
|
|
636
|
+
*/
|
|
637
|
+
_publishOutput() {
|
|
638
|
+
|
|
639
|
+
const ctx = this.pipeline?.context;
|
|
640
|
+
const tex = this.denoiser?.outputTexture;
|
|
641
|
+
if ( ctx && tex ) ctx.setTexture( 'oidn:output', tex );
|
|
642
|
+
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Gives the viewport back to the raw render.
|
|
646
|
+
_unpublishOutput() {
|
|
647
|
+
|
|
648
|
+
this.pipeline?.context?.removeTexture( 'oidn:output' );
|
|
649
|
+
this.denoiser?.invalidateOutput();
|
|
650
|
+
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// A picture the denoiser has stopped replacing describes a view that is long gone, so the raw
|
|
654
|
+
// render takes the viewport back until a refresh delivers again.
|
|
655
|
+
_checkHeldFrameHealthy() {
|
|
656
|
+
|
|
657
|
+
if ( ! this.denoiser?.hasOutput || this._failedRefreshes < HELD_FRAME_MAX_FAILURES ) return;
|
|
658
|
+
|
|
659
|
+
this._unpublishOutput();
|
|
660
|
+
|
|
661
|
+
}
|
|
662
|
+
|
|
517
663
|
/**
|
|
518
664
|
* Denoises the accumulating mean on a cadence, so a preview shows a clean image while it
|
|
519
665
|
* refines instead of only once it finishes. Driven from the render loop; call every frame.
|
|
@@ -524,20 +670,32 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
524
670
|
tickContinuousDenoise( sampleCount ) {
|
|
525
671
|
|
|
526
672
|
const dn = this.denoiser;
|
|
527
|
-
if ( ! this.continuousDenoise || ! dn ) return false;
|
|
528
|
-
|
|
673
|
+
if ( ! this.continuousDenoise || this._cadenceSuspended || ! dn ) return false;
|
|
674
|
+
|
|
675
|
+
this._checkHeldFrameHealthy();
|
|
676
|
+
|
|
677
|
+
// Moving with a denoise too slow to follow: the raw render owns the view, and a refresh
|
|
678
|
+
// would only paint a stale clean frame over a moving one.
|
|
679
|
+
const interacting = !! this._stages.pathTracer?.viewIsChanging;
|
|
680
|
+
if ( ! interacting ) {
|
|
529
681
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
682
|
+
this._holdWhileMoving = null;
|
|
683
|
+
this._movingDenoiseMs.length = 0;
|
|
684
|
+
|
|
685
|
+
} else if ( ! this.holdsWhileMoving ) return false;
|
|
686
|
+
|
|
687
|
+
if ( dn.state.isDenoising || dn.state.isLoading ) return false;
|
|
533
688
|
|
|
534
|
-
//
|
|
535
|
-
|
|
689
|
+
// frameCount is frozen while the camera moves — those frames are 1-SPP feedback and do not
|
|
690
|
+
// count toward completion — so this gate would refuse every tick. The clock governs there.
|
|
691
|
+
if ( ! interacting && sampleCount <= this._lastCadenceSamples ) return false;
|
|
536
692
|
|
|
537
693
|
const now = performance.now();
|
|
538
694
|
// `continuousDenoiseInterval` is only the floor's lower bound: on a cheap denoise it is
|
|
539
695
|
// what binds, and past ~1024² the denoise's own cost is.
|
|
540
|
-
const
|
|
696
|
+
const periods = interacting ? CADENCE_INTERACTION_PERIODS
|
|
697
|
+
: this._lastCadenceSamples === 0 ? CADENCE_FIRST_REFRESH_PERIODS : CADENCE_DENOISE_PERIODS;
|
|
698
|
+
const minGap = Math.max( this.continuousDenoiseInterval, dn.lastDenoiseMs * periods );
|
|
541
699
|
if ( now - this._lastCadenceAt < minGap ) return false;
|
|
542
700
|
|
|
543
701
|
this._lastCadenceAt = now;
|
|
@@ -552,7 +710,16 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
552
710
|
const want = this._cadenceDowngraded ? this.previewQuality() : this._finalQuality;
|
|
553
711
|
if ( dn.quality !== want ) dn.updateQuality( want );
|
|
554
712
|
|
|
555
|
-
dn.start( { continuous: true } )
|
|
713
|
+
dn.start( { continuous: true } ).then( ok => {
|
|
714
|
+
|
|
715
|
+
this._failedRefreshes = ok ? 0 : this._failedRefreshes + 1;
|
|
716
|
+
if ( ! ok || ! interacting ) return;
|
|
717
|
+
|
|
718
|
+
this._movingDenoiseMs.push( dn.lastDenoiseMs );
|
|
719
|
+
if ( this._movingDenoiseMs.length > DENOISE_COST_SAMPLES ) this._movingDenoiseMs.shift();
|
|
720
|
+
|
|
721
|
+
} );
|
|
722
|
+
|
|
556
723
|
return true;
|
|
557
724
|
|
|
558
725
|
}
|
|
@@ -571,6 +738,27 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
571
738
|
|
|
572
739
|
}
|
|
573
740
|
|
|
741
|
+
/**
|
|
742
|
+
* Stops the live-view refresh for the duration of a final render, without changing what the
|
|
743
|
+
* host picked in the denoiser list. A final render shows the accumulation and denoises once at
|
|
744
|
+
* the end, so refreshing through it spends samples on frames nobody keeps — and a refresh
|
|
745
|
+
* landing while the renderer resizes itself on the way in raced its output pass, which the
|
|
746
|
+
* device reported as a write to a destroyed buffer.
|
|
747
|
+
*
|
|
748
|
+
* @param {boolean} suspended
|
|
749
|
+
*/
|
|
750
|
+
setCadenceSuspended( suspended ) {
|
|
751
|
+
|
|
752
|
+
this._cadenceSuspended = !! suspended;
|
|
753
|
+
if ( ! this._cadenceSuspended ) return;
|
|
754
|
+
|
|
755
|
+
// The accumulation is what a final render shows; a held preview frame would otherwise sit
|
|
756
|
+
// over it until the render finished.
|
|
757
|
+
this.denoiser?.abort();
|
|
758
|
+
this._unpublishOutput();
|
|
759
|
+
|
|
760
|
+
}
|
|
761
|
+
|
|
574
762
|
setContinuousDenoise( enabled ) {
|
|
575
763
|
|
|
576
764
|
this.continuousDenoise = !! enabled;
|
|
@@ -684,22 +872,36 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
684
872
|
|
|
685
873
|
/**
|
|
686
874
|
* Aborts any in-progress denoising/upscaling (called on reset).
|
|
687
|
-
*
|
|
875
|
+
*
|
|
876
|
+
* @param {HTMLCanvasElement} mainCanvas
|
|
877
|
+
* @param {Object} [options]
|
|
878
|
+
* @param {boolean} [options.keepDisplay] - The viewpoint did not move, so the frame already on
|
|
879
|
+
* screen still describes this view. It stays up until the next denoise replaces it, instead
|
|
880
|
+
* of dropping back to the raw render for one denoise. Ignored unless OIDN owns the live view:
|
|
881
|
+
* nothing else would ever replace the held frame.
|
|
688
882
|
*/
|
|
689
|
-
abort( mainCanvas ) {
|
|
883
|
+
abort( mainCanvas, { keepDisplay = false } = {} ) {
|
|
690
884
|
|
|
691
885
|
// Remove stale completion-chain listener before aborting
|
|
692
886
|
this._cleanupCompletionListener();
|
|
693
887
|
|
|
888
|
+
// The 2D canvas is the upscaler's alone: a reset means its enlarged result no longer
|
|
889
|
+
// describes anything, so the render canvas comes back.
|
|
694
890
|
if ( mainCanvas ) mainCanvas.style.opacity = '1';
|
|
695
|
-
|
|
891
|
+
if ( this.upscalerCanvas ) this.upscalerCanvas.style.display = 'none';
|
|
696
892
|
if ( this.upscaler ) this.upscaler.abort();
|
|
697
893
|
|
|
698
|
-
|
|
894
|
+
const moving = !! this._stages.pathTracer?.viewIsChanging;
|
|
895
|
+
const hold = keepDisplay && this.continuousDenoise && ! this._cadenceSuspended && !! this.denoiser?.hasOutput
|
|
896
|
+
&& ( ! moving || this.holdsWhileMoving );
|
|
897
|
+
|
|
898
|
+
if ( this.denoiser && ! hold ) {
|
|
699
899
|
|
|
900
|
+
// A held picture means the run in flight is the frame that replaces what is on screen.
|
|
901
|
+
// Cancelling it every time reset() runs — which is every frame of a camera drag — means
|
|
902
|
+
// none of them ever lands.
|
|
700
903
|
if ( this.denoiser.enabled ) this.denoiser.abort();
|
|
701
|
-
|
|
702
|
-
this.denoiser.invalidateLatch();
|
|
904
|
+
this._unpublishOutput();
|
|
703
905
|
|
|
704
906
|
}
|
|
705
907
|
|
|
@@ -707,10 +909,26 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
707
909
|
|
|
708
910
|
}
|
|
709
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Takes the denoised frame off screen and throws it away. For a change that makes the frame
|
|
914
|
+
* wrong rather than stale — the scene it describes is gone — where holding it would leave the
|
|
915
|
+
* previous model on screen over an empty or half-built one.
|
|
916
|
+
*/
|
|
917
|
+
dropDisplay() {
|
|
918
|
+
|
|
919
|
+
this._unpublishOutput();
|
|
920
|
+
|
|
921
|
+
if ( this.upscalerCanvas ) this.upscalerCanvas.style.display = 'none';
|
|
922
|
+
if ( this.mainCanvas ) this.mainCanvas.style.opacity = '1';
|
|
923
|
+
|
|
924
|
+
}
|
|
925
|
+
|
|
710
926
|
dispose() {
|
|
711
927
|
|
|
712
928
|
// Remove pending completion-chain listener
|
|
713
929
|
this._cleanupCompletionListener();
|
|
930
|
+
// Before the denoiser destroys the picture the Compositor would otherwise still sample.
|
|
931
|
+
this._unpublishOutput();
|
|
714
932
|
|
|
715
933
|
if ( this.denoiser ) {
|
|
716
934
|
|
|
@@ -739,10 +957,10 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
739
957
|
this._upscalerProgressHandler = null;
|
|
740
958
|
this._upscalerEndHandler = null;
|
|
741
959
|
|
|
742
|
-
if ( this.
|
|
960
|
+
if ( this.upscalerCanvas?.parentNode ) {
|
|
743
961
|
|
|
744
|
-
this.
|
|
745
|
-
this.
|
|
962
|
+
this.upscalerCanvas.parentNode.removeChild( this.upscalerCanvas );
|
|
963
|
+
this.upscalerCanvas = null;
|
|
746
964
|
|
|
747
965
|
}
|
|
748
966
|
|
|
@@ -1014,7 +1232,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1014
1232
|
const keys = [
|
|
1015
1233
|
'asvgf:output', 'asvgf:demodulated', 'asvgf:gradient',
|
|
1016
1234
|
'variance:output', 'bilateralFiltering:output',
|
|
1017
|
-
'edgeFiltering:output', 'nrd:output',
|
|
1235
|
+
'edgeFiltering:output', 'nrd:output', 'oidn:output',
|
|
1018
1236
|
];
|
|
1019
1237
|
keys.forEach( k => ctx.removeTexture( k ) );
|
|
1020
1238
|
|
|
@@ -321,6 +321,10 @@ export class VideoRenderManager {
|
|
|
321
321
|
|
|
322
322
|
app.pauseRendering = state.pauseRendering ?? false;
|
|
323
323
|
|
|
324
|
+
// configureForMode( 'production' ) suspended the live-view refresh and nothing here switches
|
|
325
|
+
// back, so the viewport would stay un-denoised until the host changed tab.
|
|
326
|
+
app.denoisingManager?.setCadenceSuspended( false );
|
|
327
|
+
|
|
324
328
|
// Stop animation playback that seekTo may have started
|
|
325
329
|
app.animationManager?.stop();
|
|
326
330
|
|