rayzee 7.11.0 → 7.12.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/README.md +1 -1
- package/dist/rayzee.es.js +1866 -1657
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +18 -18
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +24 -1
- package/src/PathTracerApp.js +14 -0
- package/src/Pipeline/CompletionTracker.js +4 -1
- package/src/Processor/KernelManager.js +0 -1
- package/src/Processor/QueueManager.js +8 -1
- package/src/Processor/ShaderBuilder.js +3 -0
- package/src/RenderSettings.js +9 -0
- package/src/Stages/PathTracer.js +306 -34
- package/src/TSL/BVHTraversal.js +10 -3
- package/src/TSL/Debugger.js +1 -1
- package/src/TSL/FinalWriteKernel.js +110 -8
- package/src/TSL/GenerateKernel.js +74 -46
- package/src/TSL/ShadeKernel.js +80 -3
- package/src/managers/DenoisingManager.js +6 -0
- package/src/managers/UniformManager.js +15 -0
package/src/Stages/PathTracer.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from '../TSL/SortGlobalKernels.js';
|
|
26
26
|
import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
|
|
27
27
|
import {
|
|
28
|
-
Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return,
|
|
28
|
+
Fn, uint, int, atomicStore, atomicLoad, atomicAdd, instanceIndex, If, Return,
|
|
29
29
|
} from 'three/tsl';
|
|
30
30
|
|
|
31
31
|
export class PathTracer extends PathTracerStage {
|
|
@@ -39,6 +39,12 @@ export class PathTracer extends PathTracerStage {
|
|
|
39
39
|
this._queueManager = null;
|
|
40
40
|
this._kernelManager = null;
|
|
41
41
|
this._gBufferAttr = null; // per-pixel first-hit MRT (ND + albedo); see _buildWavefrontKernels
|
|
42
|
+
this._m2Attr = null; // per-pixel running mean of luminance² for the Tier-1 convergence early-stop
|
|
43
|
+
this._streakAttr = null; // Tier-2: per-pixel freeze-candidate streak (u32); frozen := streak >= K
|
|
44
|
+
// Tier-2 dilation: per-pixel frozen mask (1=skip), written race-free in buildActivePixels; a frozen pixel
|
|
45
|
+
// stays active if any 8-neighbour is still active (Cycles box-filter) to avoid hard frozen/active seams.
|
|
46
|
+
this._frozenMaskAttr = null;
|
|
47
|
+
this._dilateFrozenUniform = uniform( 1, 'int' ); // 1 = dilate (default); 0 = plain per-pixel freeze
|
|
42
48
|
this._wavefrontReady = false;
|
|
43
49
|
|
|
44
50
|
// Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. When no denoiser is active the
|
|
@@ -46,6 +52,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
46
52
|
// uniform — NOT baked — so DenoisingManager can toggle it without a (UI-freezing) kernel rebuild.
|
|
47
53
|
this._auxGBufferEnabled = false;
|
|
48
54
|
this._auxGBufferUniform = uniform( 0, 'uint' );
|
|
55
|
+
// Clean-aux mode: temporally accumulate + renormalize the aux NORMAL (like albedo) so a clean-aux
|
|
56
|
+
// OIDN model (calb_cnrm/high, alb_nrm/balanced) gets the prefiltered-ish normal it expects instead
|
|
57
|
+
// of the point-sampled per-frame value that leaks noise. Off for fast/ASVGF (they want the bump normal).
|
|
58
|
+
this._cleanAuxNormalEnabled = false;
|
|
59
|
+
this._cleanAuxNormalUniform = uniform( 0, 'uint' );
|
|
49
60
|
|
|
50
61
|
// CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
|
|
51
62
|
this._useDynamicDispatch = true;
|
|
@@ -74,6 +85,16 @@ export class PathTracer extends PathTracerStage {
|
|
|
74
85
|
// 0.1% of primary ray count, floored at 100; -1 to disable. Updated per-scene in _buildWavefrontKernels.
|
|
75
86
|
this._bounceEarlyExitThreshold = 100;
|
|
76
87
|
|
|
88
|
+
// Tier-1 convergence early-stop: fraction of pixels converged in the last settled-view readback, and a
|
|
89
|
+
// single-flight guard for its async counter read. Zeroed on reset/camera-move/resize; never refreshed
|
|
90
|
+
// mid-motion (the readback early-return + frozen frameCount keep the stop from firing while moving).
|
|
91
|
+
this._convergedFraction = 0;
|
|
92
|
+
this._convergedReadbackPending = false;
|
|
93
|
+
|
|
94
|
+
// Tier-2: last settled active-pixel count (maxRays − frozen), sizes next frame's bounce-0 grid.
|
|
95
|
+
// 0 until a settled readback lands (and on reset/camera-move/resize) → grid stays full-size until then.
|
|
96
|
+
this._lastActivePixelCount = 0;
|
|
97
|
+
|
|
77
98
|
this._wfRenderWidth = uniform( 1920, 'int' );
|
|
78
99
|
this._wfRenderHeight = uniform( 1080, 'int' );
|
|
79
100
|
this._wfMaxRayCount = uniform( 0, 'uint' );
|
|
@@ -112,8 +133,17 @@ export class PathTracer extends PathTracerStage {
|
|
|
112
133
|
|
|
113
134
|
} );
|
|
114
135
|
|
|
115
|
-
// Per-pixel first-hit G-buffer (normal/depth + albedo)
|
|
116
|
-
t.register( 'gbuffer', () =>
|
|
136
|
+
// Per-pixel first-hit G-buffer (normal/depth + albedo) + convergence m2 buffer + Tier-2 freeze streak
|
|
137
|
+
t.register( 'gbuffer', () => {
|
|
138
|
+
|
|
139
|
+
const a = [];
|
|
140
|
+
if ( this._gBufferAttr ) a.push( this._gBufferAttr );
|
|
141
|
+
if ( this._m2Attr ) a.push( this._m2Attr );
|
|
142
|
+
if ( this._streakAttr ) a.push( this._streakAttr );
|
|
143
|
+
if ( this._frozenMaskAttr ) a.push( this._frozenMaskAttr );
|
|
144
|
+
return a.length ? a : null;
|
|
145
|
+
|
|
146
|
+
} );
|
|
117
147
|
|
|
118
148
|
// Accumulation pool: 3 write StorageTextures (2048²) + readable MRT RenderTarget
|
|
119
149
|
t.register( 'accum', () => {
|
|
@@ -169,7 +199,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
169
199
|
// Kernels not built yet (first frame / mid-resize) — skip until ready.
|
|
170
200
|
if ( ! this.isReady || ! this._wavefrontReady ) return;
|
|
171
201
|
|
|
172
|
-
if ( this.isComplete || this.frameCount >= this.completionThreshold ) {
|
|
202
|
+
if ( this.isComplete || this.frameCount >= this.completionThreshold || this._isConvergedComplete() ) {
|
|
173
203
|
|
|
174
204
|
if ( ! this.isComplete ) this.isComplete = true;
|
|
175
205
|
return;
|
|
@@ -207,6 +237,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
207
237
|
|
|
208
238
|
this._curveSizingValid = false;
|
|
209
239
|
this._readbackGeneration ++;
|
|
240
|
+
// Drop the stale converged fraction so the early-stop can't fire on the pose we're leaving.
|
|
241
|
+
this._convergedFraction = 0;
|
|
242
|
+
// Tier-2: drop the stale active-pixel count so bounce 0 full-sizes until the new pose re-measures.
|
|
243
|
+
this._lastActivePixelCount = 0;
|
|
210
244
|
|
|
211
245
|
}
|
|
212
246
|
|
|
@@ -220,6 +254,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
220
254
|
|
|
221
255
|
this.shaderBuilder.prevColorTexNode.value = readTextures.color;
|
|
222
256
|
this.shaderBuilder.prevAlbedoTexNode.value = readTextures.albedo;
|
|
257
|
+
this.shaderBuilder.prevNormalDepthTexNode.value = readTextures.normalDepth;
|
|
223
258
|
|
|
224
259
|
}
|
|
225
260
|
|
|
@@ -249,16 +284,37 @@ export class PathTracer extends PathTracerStage {
|
|
|
249
284
|
|
|
250
285
|
}
|
|
251
286
|
|
|
252
|
-
km.dispatch( 'resetCounters' );
|
|
253
|
-
km.dispatch( 'generate' );
|
|
254
|
-
// Generate traces every pixel; seed ENTERING_COUNT from the full identity active list.
|
|
255
|
-
km.dispatch( 'initActiveIndices' );
|
|
256
|
-
|
|
257
287
|
const maxBounces = this.maxBounces.value;
|
|
258
288
|
// Transmissive/SSS steps consume iterations without advancing camera-bounce depth, so the loop must run far enough for deep glass/subsurface walks (mirror PathTracerCore); the survivor curve + early-exit break it early on non-SSS scenes.
|
|
259
289
|
const loopBound = maxBounces + this.transmissiveBounces.value + this.maxSubsurfaceSteps.value;
|
|
260
290
|
const maxRays = this._wfMaxRayCount.value;
|
|
261
291
|
|
|
292
|
+
// Tier-2 bounce-0 grid size from the stale settled active-pixel count (maxRays until one lands).
|
|
293
|
+
// Monotonic freeze ⇒ a stale count is a safe over-estimate; reset/camera-move → _curveSizingValid=false → maxRays.
|
|
294
|
+
const adaptiveFreeze = this.usePixelFreeze.value > 0;
|
|
295
|
+
const activeBounce0 = ( adaptiveFreeze && this._curveSizingValid && this._lastActivePixelCount > 0 )
|
|
296
|
+
? this._lastActivePixelCount : maxRays;
|
|
297
|
+
|
|
298
|
+
if ( adaptiveFreeze ) {
|
|
299
|
+
|
|
300
|
+
// reset counters → compact non-frozen pixel IDs → publish count → list-driven 1D generate.
|
|
301
|
+
const genSized = Math.min( maxRays, Math.ceil( activeBounce0 * 1.5 ) + 1024 );
|
|
302
|
+
km.setDispatchCount( 'buildActivePixels', [ Math.ceil( maxRays / 256 ), 1, 1 ] );
|
|
303
|
+
km.setDispatchCount( 'generateList', [ Math.ceil( genSized / 256 ), 1, 1 ] );
|
|
304
|
+
km.dispatch( 'resetFrameCounters' );
|
|
305
|
+
km.dispatch( 'buildActivePixels' );
|
|
306
|
+
km.dispatch( 'seedEnter' );
|
|
307
|
+
km.dispatch( 'generateList' );
|
|
308
|
+
|
|
309
|
+
} else {
|
|
310
|
+
|
|
311
|
+
km.dispatch( 'generate' );
|
|
312
|
+
// Generate traces every pixel; seed ACTIVE + ENTERING_COUNT from the full identity active list
|
|
313
|
+
// (initActiveIndices overwrites both counters, so no separate frame-start reset is needed).
|
|
314
|
+
km.dispatch( 'initActiveIndices' );
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
|
|
262
318
|
// The survivor curve survives a maxBounces change (reset() preserves it), and is reusable
|
|
263
319
|
// across one: for loop iterations below BOTH the old and new camera-bounce caps, no ray has
|
|
264
320
|
// been killed by either cap, so the counts are cap-independent. Trust the curve up to that
|
|
@@ -307,6 +363,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
307
363
|
|
|
308
364
|
entering = prev > 0 ? prev : maxRays;
|
|
309
365
|
|
|
366
|
+
} else if ( bounce === 0 && adaptiveFreeze ) {
|
|
367
|
+
|
|
368
|
+
// Bounce 0 traces only non-frozen pixels — size from the stale active count (full until a readback).
|
|
369
|
+
entering = activeBounce0;
|
|
370
|
+
|
|
310
371
|
}
|
|
311
372
|
|
|
312
373
|
const sized = Math.min( maxRays, Math.ceil( entering * 1.5 ) + 1024 );
|
|
@@ -342,12 +403,18 @@ export class PathTracer extends PathTracerStage {
|
|
|
342
403
|
|
|
343
404
|
}
|
|
344
405
|
|
|
345
|
-
km.dispatch( 'shade' );
|
|
346
|
-
|
|
347
|
-
km.dispatch( 'resetActiveCounter' );
|
|
406
|
+
km.dispatch( 'shade' ); // shade thread 0 folds resetActiveCounter (zeroes ACTIVE_RAY_COUNT before compact)
|
|
348
407
|
km.dispatch( 'compact' );
|
|
349
|
-
if ( useFunctionalCompaction )
|
|
350
|
-
|
|
408
|
+
if ( useFunctionalCompaction ) {
|
|
409
|
+
|
|
410
|
+
// compactCopyback thread 0 folds snapshotBounceCount (records the survivor curve + seeds ENTERING).
|
|
411
|
+
km.dispatch( 'compactCopyback' );
|
|
412
|
+
|
|
413
|
+
} else {
|
|
414
|
+
|
|
415
|
+
km.dispatch( 'snapshotBounceCount' );
|
|
416
|
+
|
|
417
|
+
}
|
|
351
418
|
// No swap: pingPong stays 0 (kernels are build-time-bound to buffer A).
|
|
352
419
|
|
|
353
420
|
// Early-exit on last frame's per-bounce snapshot (stale via async readback, fine for a heuristic).
|
|
@@ -385,6 +452,26 @@ export class PathTracer extends PathTracerStage {
|
|
|
385
452
|
|
|
386
453
|
}
|
|
387
454
|
|
|
455
|
+
// Tier-1 convergence early-stop: retire the WHOLE frame once enough samples have accumulated AND ~all pixels
|
|
456
|
+
// hit the relative-error floor. Keeps the global 1/(frame+1) alpha untouched — only the stop condition
|
|
457
|
+
// changes. Naturally gated off while moving: frameCount is frozen in interaction mode (< minSamples) and
|
|
458
|
+
// _convergedFraction is zeroed on camera-move and never refreshed mid-motion (readback early-returns).
|
|
459
|
+
_isConvergedComplete() {
|
|
460
|
+
|
|
461
|
+
return this.useAdaptiveSampling.value > 0
|
|
462
|
+
&& this.frameCount >= this.adaptiveMinSamples.value
|
|
463
|
+
&& this._convergedFraction >= this.adaptiveStopFraction.value;
|
|
464
|
+
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
reset() {
|
|
468
|
+
|
|
469
|
+
super.reset();
|
|
470
|
+
this._convergedFraction = 0;
|
|
471
|
+
this._lastActivePixelCount = 0;
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
|
|
388
475
|
// Parent resizes storageTextures/shaderBuilder; wavefront also needs its buffers/uniforms/kernels rebuilt.
|
|
389
476
|
_handleResize() {
|
|
390
477
|
|
|
@@ -410,6 +497,19 @@ export class PathTracer extends PathTracerStage {
|
|
|
410
497
|
|
|
411
498
|
}
|
|
412
499
|
|
|
500
|
+
// Clean-aux normal: when a clean-aux OIDN model is active (calb_cnrm/high, alb_nrm/balanced), FinalWrite
|
|
501
|
+
// temporally accumulates + renormalizes the aux normal so the model isn't fed per-frame point-sampled
|
|
502
|
+
// noise. DenoisingManager calls this on OIDN enable + quality change. Live uniform → value flip + reset.
|
|
503
|
+
setCleanAuxNormal( enabled ) {
|
|
504
|
+
|
|
505
|
+
enabled = !! enabled;
|
|
506
|
+
if ( this._cleanAuxNormalEnabled === enabled ) return;
|
|
507
|
+
this._cleanAuxNormalEnabled = enabled;
|
|
508
|
+
this._cleanAuxNormalUniform.value = enabled ? 1 : 0;
|
|
509
|
+
this.reset();
|
|
510
|
+
|
|
511
|
+
}
|
|
512
|
+
|
|
413
513
|
// UI-driven resize (Resolution dropdown) — parent bypasses _handleResize(), so hook here too.
|
|
414
514
|
setSize( width, height ) {
|
|
415
515
|
|
|
@@ -472,6 +572,40 @@ export class PathTracer extends PathTracerStage {
|
|
|
472
572
|
|
|
473
573
|
} );
|
|
474
574
|
|
|
575
|
+
// Tier-1 convergence: on the SAME settled-view cadence, read the converged-pixel count and derive the
|
|
576
|
+
// fraction that drives the whole-frame early-stop. Separate single-flight flag (different buffer) + the
|
|
577
|
+
// same _readbackGeneration guard so a count measured before a camera-move/resize is dropped when stale.
|
|
578
|
+
if ( ! this._convergedReadbackPending ) {
|
|
579
|
+
|
|
580
|
+
const cAttr = this._queueManager?.getCountersAttribute();
|
|
581
|
+
if ( cAttr ) {
|
|
582
|
+
|
|
583
|
+
this._convergedReadbackPending = true;
|
|
584
|
+
const cgen = this._readbackGeneration;
|
|
585
|
+
const total = this._wfMaxRayCount.value;
|
|
586
|
+
this.renderer.getArrayBufferAsync( cAttr ).then( ( buf ) => {
|
|
587
|
+
|
|
588
|
+
if ( cgen === this._readbackGeneration && total > 0 ) {
|
|
589
|
+
|
|
590
|
+
const c = new Uint32Array( buf );
|
|
591
|
+
this._convergedFraction = c[ COUNTER.CONVERGED_COUNT ] / total;
|
|
592
|
+
// Tier-2: bounce-0 active-pixel count measured this settled frame → sizes next frame's grid.
|
|
593
|
+
this._lastActivePixelCount = c[ COUNTER.ACTIVE_PIXEL_COUNT ];
|
|
594
|
+
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
this._convergedReadbackPending = false;
|
|
598
|
+
|
|
599
|
+
} ).catch( () => {
|
|
600
|
+
|
|
601
|
+
this._convergedReadbackPending = false;
|
|
602
|
+
|
|
603
|
+
} );
|
|
604
|
+
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
}
|
|
608
|
+
|
|
475
609
|
}
|
|
476
610
|
|
|
477
611
|
// Sync wavefront's texture nodes with current env/material textures; only a changed ref triggers GPU rebind.
|
|
@@ -507,6 +641,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
507
641
|
this._curveSizingValid = false;
|
|
508
642
|
this._readbackFrameCounter = 0;
|
|
509
643
|
this._readbackGeneration ++;
|
|
644
|
+
this._convergedFraction = 0;
|
|
645
|
+
this._lastActivePixelCount = 0;
|
|
510
646
|
|
|
511
647
|
// Recompile only when buffers reallocate (capacity grows); otherwise resize uniforms in place.
|
|
512
648
|
const neededCap = PackedRayBuffer.requiredCapacity( newW * newH );
|
|
@@ -580,6 +716,25 @@ export class PathTracer extends PathTracerStage {
|
|
|
580
716
|
const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
|
|
581
717
|
const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
|
|
582
718
|
|
|
719
|
+
// Tier-1 convergence: one f32/pixel holding the running mean of luminance² (Welford second moment),
|
|
720
|
+
// read+written by FinalWrite. 1.25× margin (same as the per-ray buffers) so it survives in-place resize;
|
|
721
|
+
// allocated only on this capacity-growth/rebuild path, never in _resizeWavefrontInPlace.
|
|
722
|
+
freeStorageAttribute( this.renderer, this._m2Attr );
|
|
723
|
+
this._m2Attr = new StorageInstancedBufferAttribute( new Float32Array( PackedRayBuffer.requiredCapacity( maxRays ) ), 1 );
|
|
724
|
+
const m2RW = storage( this._m2Attr, 'float' );
|
|
725
|
+
|
|
726
|
+
// Tier-2: one u32/pixel freeze-candidate streak. FinalWrite writes (RW), buildActivePixels reads (RO).
|
|
727
|
+
freeStorageAttribute( this.renderer, this._streakAttr );
|
|
728
|
+
this._streakAttr = new StorageInstancedBufferAttribute( new Uint32Array( PackedRayBuffer.requiredCapacity( maxRays ) ), 1 );
|
|
729
|
+
const streakRW = storage( this._streakAttr, 'uint' );
|
|
730
|
+
const streakRO = storage( this._streakAttr, 'uint' ).toReadOnly();
|
|
731
|
+
|
|
732
|
+
// Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
|
|
733
|
+
freeStorageAttribute( this.renderer, this._frozenMaskAttr );
|
|
734
|
+
this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array( PackedRayBuffer.requiredCapacity( maxRays ) ), 1 );
|
|
735
|
+
const frozenMaskRW = storage( this._frozenMaskAttr, 'uint' );
|
|
736
|
+
const frozenMaskRO = storage( this._frozenMaskAttr, 'uint' ).toReadOnly();
|
|
737
|
+
|
|
583
738
|
if ( ! this._queueManager ) {
|
|
584
739
|
|
|
585
740
|
this._queueManager = new QueueManager( this._packedBuffers.capacity, this.renderer );
|
|
@@ -605,28 +760,14 @@ export class PathTracer extends PathTracerStage {
|
|
|
605
760
|
|
|
606
761
|
const prevColor = this.shaderBuilder.prevColorTexNode;
|
|
607
762
|
const prevAlbedo = this.shaderBuilder.prevAlbedoTexNode;
|
|
763
|
+
const prevNormalDepth = this.shaderBuilder.prevNormalDepthTexNode;
|
|
608
764
|
const writeTex = this.storageTextures.getWriteTextures();
|
|
609
765
|
|
|
610
766
|
const counters = qm.getCounters();
|
|
611
|
-
const resetFn = Fn( () => {
|
|
612
|
-
|
|
613
|
-
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
614
|
-
|
|
615
|
-
} );
|
|
616
|
-
this._kernelManager.register( 'resetCounters',
|
|
617
|
-
resetFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
const resetActiveFn = Fn( () => {
|
|
621
|
-
|
|
622
|
-
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
623
|
-
|
|
624
|
-
} );
|
|
625
|
-
this._kernelManager.register( 'resetActiveCounter',
|
|
626
|
-
resetActiveFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
627
|
-
);
|
|
628
767
|
|
|
629
768
|
// Copy ACTIVE_RAY_COUNT into bounceCounts[currentBounce] for the readback survivor curve.
|
|
769
|
+
// Standalone kernel is dispatched only on the non-dynamic path; the dynamic path folds this
|
|
770
|
+
// into compactCopyback's thread 0.
|
|
630
771
|
const bounceCountsBuf = qm.getBounceCounts();
|
|
631
772
|
const wfCurrentBounce = this._wfCurrentBounce;
|
|
632
773
|
const snapshotFn = Fn( () => {
|
|
@@ -652,6 +793,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
652
793
|
|
|
653
794
|
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), this._wfMaxRayCount );
|
|
654
795
|
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), this._wfMaxRayCount );
|
|
796
|
+
// Tier-1 convergence: zero the per-frame converged-pixel counter here — once/frame at frame start,
|
|
797
|
+
// before FinalWrite atomicAdds into it. (Was in the now-removed resetCounters kernel; must NOT go
|
|
798
|
+
// in shade's per-bounce reset, since FinalWrite accumulates only once, at frame end.)
|
|
799
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
655
800
|
|
|
656
801
|
} );
|
|
657
802
|
|
|
@@ -660,7 +805,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
660
805
|
initFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
661
806
|
);
|
|
662
807
|
|
|
663
|
-
const
|
|
808
|
+
const genParams = {
|
|
664
809
|
rayBufferRW: pb.rayBuffer.rw,
|
|
665
810
|
rngBufferRW: pb.rngBuffer.rw,
|
|
666
811
|
gBufferRW,
|
|
@@ -680,7 +825,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
680
825
|
transmissiveBounces: this.transmissiveBounces,
|
|
681
826
|
transparentBackground: this.transparentBackground,
|
|
682
827
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
683
|
-
}
|
|
828
|
+
};
|
|
829
|
+
const genFn = buildGenerateKernel( genParams );
|
|
684
830
|
this._kernelManager.register( 'generate',
|
|
685
831
|
genFn().compute(
|
|
686
832
|
[ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( h / GENERATE_WG_SIZE ), 1 ],
|
|
@@ -688,6 +834,100 @@ export class PathTracer extends PathTracerStage {
|
|
|
688
834
|
)
|
|
689
835
|
);
|
|
690
836
|
|
|
837
|
+
// --- Tier-2 freeze seed path (gated by usePixelFreeze in render()) ---
|
|
838
|
+
// Replaces generate+initActiveIndices with: reset counters → scatter non-frozen pixel IDs into the active
|
|
839
|
+
// list → publish count → 1D list-driven generate. Split so ACTIVE_RAY_COUNT is zeroed BEFORE the scatter.
|
|
840
|
+
const freezeK = this.pixelFreezeStability;
|
|
841
|
+
const resetFrameFn = Fn( () => {
|
|
842
|
+
|
|
843
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
844
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
845
|
+
atomicStore( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 0 ) );
|
|
846
|
+
|
|
847
|
+
} );
|
|
848
|
+
this._kernelManager.register( 'resetFrameCounters',
|
|
849
|
+
resetFrameFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
const buildActiveFn = Fn( () => {
|
|
853
|
+
|
|
854
|
+
const tid = instanceIndex;
|
|
855
|
+
If( tid.lessThan( this._wfMaxRayCount ), () => {
|
|
856
|
+
|
|
857
|
+
// frozen=1 skips the pixel this frame. Frame 0 seeds all (streak may be stale post-reset). A pixel
|
|
858
|
+
// freezes once streak>=K; with dilation ON it stays active if any 8-neighbour is still active
|
|
859
|
+
// (streak<K), softening the boundary. Mask is read by FinalWrite too → race-free.
|
|
860
|
+
const frozen = uint( 0 ).toVar();
|
|
861
|
+
|
|
862
|
+
If( this.frame.greaterThan( uint( 0 ) ).and( streakRO.element( tid ).greaterThanEqual( uint( freezeK ) ) ), () => {
|
|
863
|
+
|
|
864
|
+
frozen.assign( uint( 1 ) );
|
|
865
|
+
|
|
866
|
+
If( this._dilateFrozenUniform.greaterThan( int( 0 ) ), () => {
|
|
867
|
+
|
|
868
|
+
const px = int( tid ).mod( this._wfRenderWidth );
|
|
869
|
+
const py = int( tid ).div( this._wfRenderWidth );
|
|
870
|
+
|
|
871
|
+
for ( const [ dx, dy ] of [[ - 1, - 1 ], [ 0, - 1 ], [ 1, - 1 ], [ - 1, 0 ], [ 1, 0 ], [ - 1, 1 ], [ 0, 1 ], [ 1, 1 ]] ) {
|
|
872
|
+
|
|
873
|
+
const nx = px.add( int( dx ) );
|
|
874
|
+
const ny = py.add( int( dy ) );
|
|
875
|
+
If( nx.greaterThanEqual( int( 0 ) ).and( nx.lessThan( this._wfRenderWidth ) )
|
|
876
|
+
.and( ny.greaterThanEqual( int( 0 ) ) ).and( ny.lessThan( this._wfRenderHeight ) ), () => {
|
|
877
|
+
|
|
878
|
+
If( streakRO.element( uint( ny.mul( this._wfRenderWidth ).add( nx ) ) ).lessThan( uint( freezeK ) ), () => {
|
|
879
|
+
|
|
880
|
+
frozen.assign( uint( 0 ) ); // a still-active neighbour keeps this pixel active
|
|
881
|
+
|
|
882
|
+
} );
|
|
883
|
+
|
|
884
|
+
} );
|
|
885
|
+
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
} );
|
|
889
|
+
|
|
890
|
+
} );
|
|
891
|
+
|
|
892
|
+
frozenMaskRW.element( tid ).assign( frozen );
|
|
893
|
+
|
|
894
|
+
If( frozen.equal( uint( 0 ) ), () => {
|
|
895
|
+
|
|
896
|
+
const slot = atomicAdd( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 1 ) );
|
|
897
|
+
activeWriteA.element( slot ).assign( tid );
|
|
898
|
+
|
|
899
|
+
} );
|
|
900
|
+
|
|
901
|
+
} );
|
|
902
|
+
|
|
903
|
+
} );
|
|
904
|
+
this._kernelManager.register( 'buildActivePixels',
|
|
905
|
+
buildActiveFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
const seedEnterFn = Fn( () => {
|
|
909
|
+
|
|
910
|
+
const active = atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) );
|
|
911
|
+
// ENTERING_COUNT drives the bounce loop; ACTIVE_PIXEL_COUNT is a stable snapshot for the sizing readback.
|
|
912
|
+
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), active );
|
|
913
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_PIXEL_COUNT ) ), active );
|
|
914
|
+
|
|
915
|
+
} );
|
|
916
|
+
this._kernelManager.register( 'seedEnter',
|
|
917
|
+
seedEnterFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
// 1D list-driven generate: one thread per active-list slot, bounded on ENTERING_COUNT.
|
|
921
|
+
const genListFn = buildGenerateKernel( {
|
|
922
|
+
...genParams,
|
|
923
|
+
listDriven: true,
|
|
924
|
+
activeIndicesRO: qm.activeIndicesRO.a,
|
|
925
|
+
counters,
|
|
926
|
+
} );
|
|
927
|
+
this._kernelManager.register( 'generateList',
|
|
928
|
+
genListFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
929
|
+
);
|
|
930
|
+
|
|
691
931
|
const freshBvh = this.bvhStorageNode;
|
|
692
932
|
const freshTri = this.triangleStorageNode;
|
|
693
933
|
const freshMat = this.materialData.materialStorageNode;
|
|
@@ -880,7 +1120,19 @@ export class PathTracer extends PathTracerStage {
|
|
|
880
1120
|
const copyFn = Fn( () => {
|
|
881
1121
|
|
|
882
1122
|
const tid = instanceIndex;
|
|
883
|
-
|
|
1123
|
+
const active = atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) );
|
|
1124
|
+
|
|
1125
|
+
// Folds snapshotBounceCount: thread 0 records the survivor count for the readback curve and
|
|
1126
|
+
// seeds ENTERING_COUNT for the next bounce. Above the guard so it runs even when active is 0.
|
|
1127
|
+
If( tid.equal( uint( 0 ) ), () => {
|
|
1128
|
+
|
|
1129
|
+
const slot = uint( wfCurrentBounce ).clamp( uint( 0 ), uint( qm.MAX_BOUNCE_SNAPSHOTS - 1 ) );
|
|
1130
|
+
bounceCountsBuf.element( slot ).assign( active );
|
|
1131
|
+
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), active );
|
|
1132
|
+
|
|
1133
|
+
} );
|
|
1134
|
+
|
|
1135
|
+
If( tid.greaterThanEqual( active ), () => {
|
|
884
1136
|
|
|
885
1137
|
Return();
|
|
886
1138
|
|
|
@@ -907,10 +1159,24 @@ export class PathTracer extends PathTracerStage {
|
|
|
907
1159
|
transparentBackground: this.transparentBackground,
|
|
908
1160
|
prevAccumTexture: prevColor,
|
|
909
1161
|
prevAlbedoTexture: prevAlbedo,
|
|
1162
|
+
prevNormalDepthTexture: prevNormalDepth,
|
|
910
1163
|
renderWidth: this._wfRenderWidth,
|
|
911
1164
|
renderHeight: this._wfRenderHeight,
|
|
912
1165
|
visMode: this.visMode,
|
|
913
1166
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
1167
|
+
cleanAuxNormalEnabled: this._cleanAuxNormalUniform,
|
|
1168
|
+
counters,
|
|
1169
|
+
m2BufferRW: m2RW,
|
|
1170
|
+
useAdaptiveSampling: this.useAdaptiveSampling,
|
|
1171
|
+
noiseThreshold: this.noiseThreshold,
|
|
1172
|
+
darkNoiseFloor: this.darkNoiseFloor,
|
|
1173
|
+
adaptiveMinSamples: this.adaptiveMinSamples,
|
|
1174
|
+
// Tier-2 freeze (stamp + pass-through)
|
|
1175
|
+
usePixelFreeze: this.usePixelFreeze,
|
|
1176
|
+
pixelFreezeThreshold: this.pixelFreezeThreshold,
|
|
1177
|
+
pixelFreezeStability: this.pixelFreezeStability,
|
|
1178
|
+
streakBufferRW: streakRW,
|
|
1179
|
+
frozenMaskRO, // dilated frozen mask (read-only; matches the active-list decision)
|
|
914
1180
|
} );
|
|
915
1181
|
this._kernelManager.register( 'finalWrite',
|
|
916
1182
|
// Per-pixel (w×h) — kernel averages the S sample-slots internally.
|
|
@@ -990,10 +1256,16 @@ export class PathTracer extends PathTracerStage {
|
|
|
990
1256
|
this._queueManager?.dispose();
|
|
991
1257
|
this._kernelManager?.dispose();
|
|
992
1258
|
this._gBufferAttr?.dispose?.();
|
|
1259
|
+
this._m2Attr?.dispose?.();
|
|
1260
|
+
this._streakAttr?.dispose?.();
|
|
1261
|
+
this._frozenMaskAttr?.dispose?.();
|
|
993
1262
|
this._packedBuffers = null;
|
|
994
1263
|
this._queueManager = null;
|
|
995
1264
|
this._kernelManager = null;
|
|
996
1265
|
this._gBufferAttr = null;
|
|
1266
|
+
this._m2Attr = null;
|
|
1267
|
+
this._streakAttr = null;
|
|
1268
|
+
this._frozenMaskAttr = null;
|
|
997
1269
|
this._wavefrontReady = false;
|
|
998
1270
|
|
|
999
1271
|
}
|
package/src/TSL/BVHTraversal.js
CHANGED
|
@@ -175,7 +175,10 @@ const fastRayAABBDst = wgslFn( `
|
|
|
175
175
|
// Side culling is performed inline inside traverseBVH/traverseBVHShadow using
|
|
176
176
|
// the per-triangle side flag stored in normalCData.w (slot 5, .w channel).
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
// Factory: the boxTests/triTests debug counters are compiled OUT of the hot closest-hit
|
|
179
|
+
// path (extend/normalDepth use traverseBVH = trackStats false) — only the debug viz reads
|
|
180
|
+
// them, and keeping them live across the traversal loop costs registers/occupancy.
|
|
181
|
+
const makeTraverseBVH = ( trackStats ) => Fn( ( [
|
|
179
182
|
ray,
|
|
180
183
|
bvhBuffer,
|
|
181
184
|
triangleBuffer,
|
|
@@ -236,7 +239,7 @@ export const traverseBVH = Fn( ( [
|
|
|
236
239
|
// Inner: vec4(0) = [leftMin.xyz, leftChild], vec4(1) = [leftMax.xyz, rightChild],
|
|
237
240
|
// vec4(2) = [rightMin.xyz, 0], vec4(3) = [rightMax.xyz, 0]
|
|
238
241
|
const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
|
|
239
|
-
closestHit.boxTests.addAssign( 1 );
|
|
242
|
+
if ( trackStats ) closestHit.boxTests.addAssign( 1 );
|
|
240
243
|
|
|
241
244
|
If( nodeData0.w.lessThan( 0.0 ), () => {
|
|
242
245
|
|
|
@@ -250,7 +253,7 @@ export const traverseBVH = Fn( ( [
|
|
|
250
253
|
// Process triangles in leaf
|
|
251
254
|
Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
|
|
252
255
|
|
|
253
|
-
closestHit.triTests.addAssign( 1 );
|
|
256
|
+
if ( trackStats ) closestHit.triTests.addAssign( 1 );
|
|
254
257
|
const triIndex = triStart.add( i ).toVar();
|
|
255
258
|
|
|
256
259
|
// Fetch geometry first (3 fetches from storage buffer)
|
|
@@ -398,6 +401,10 @@ export const traverseBVH = Fn( ( [
|
|
|
398
401
|
|
|
399
402
|
} );
|
|
400
403
|
|
|
404
|
+
// Hot path (extend/normalDepth): no debug counters. Debug viz uses traverseBVHDebug.
|
|
405
|
+
export const traverseBVH = /*@__PURE__*/ makeTraverseBVH( false );
|
|
406
|
+
export const traverseBVHDebug = /*@__PURE__*/ makeTraverseBVH( true );
|
|
407
|
+
|
|
401
408
|
// ================================================================================
|
|
402
409
|
// SHADOW RAY TRAVERSAL (OPTIMIZED - early exit on any hit)
|
|
403
410
|
// ================================================================================
|
package/src/TSL/Debugger.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from 'three/tsl';
|
|
22
22
|
|
|
23
23
|
import { Ray, HitInfo, RayTracingMaterial, MaterialSamples } from './Struct.js';
|
|
24
|
-
import { traverseBVH } from './BVHTraversal.js';
|
|
24
|
+
import { traverseBVHDebug as traverseBVH } from './BVHTraversal.js';
|
|
25
25
|
import { sampleEnvironment } from './Environment.js';
|
|
26
26
|
import { REC709_LUMINANCE_COEFFICIENTS, getMaterial } from './Common.js';
|
|
27
27
|
import { sampleAllMaterialTextures } from './TextureSampling.js';
|