rayzee 7.11.1 → 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 +1544 -1355
- 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/RenderSettings.js +9 -0
- package/src/Stages/PathTracer.js +284 -34
- package/src/TSL/BVHTraversal.js +10 -3
- package/src/TSL/Debugger.js +1 -1
- package/src/TSL/FinalWriteKernel.js +86 -3
- package/src/TSL/GenerateKernel.js +74 -46
- package/src/TSL/ShadeKernel.js +14 -1
- package/src/managers/UniformManager.js +15 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -83,6 +83,19 @@ export const ENGINE_DEFAULTS = {
|
|
|
83
83
|
bounces: 3,
|
|
84
84
|
transmissiveBounces: 5,
|
|
85
85
|
maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
|
|
86
|
+
|
|
87
|
+
// Adaptive sampling (Blender-style): stop the frame once enough pixels drop below the noise threshold.
|
|
88
|
+
useAdaptiveSampling: true,
|
|
89
|
+
noiseThreshold: 0.02, // per-pixel noise below which a pixel is converged
|
|
90
|
+
darkNoiseFloor: 0.003, // extra absolute-noise floor so dark pixels can converge too
|
|
91
|
+
adaptiveMinSamples: 8, // min samples before adaptive sampling can trigger
|
|
92
|
+
adaptiveStopFraction: 0.95, // retire the frame once this fraction of pixels has converged
|
|
93
|
+
// Per-pixel freeze: skip tracing pixels that individually converged (noise threshold only — no dark floor,
|
|
94
|
+
// which would bake dim regions too dark). Naturally engages only on static/idle views. See docs/specs/tier2-adaptive-sampling-plan.md.
|
|
95
|
+
usePixelFreeze: false,
|
|
96
|
+
pixelFreezeThreshold: 0.02, // per-pixel noise below which a pixel becomes a freeze candidate
|
|
97
|
+
pixelFreezeStability: 8, // consecutive candidate frames before a pixel freezes
|
|
98
|
+
|
|
86
99
|
samplingTechnique: 3,
|
|
87
100
|
enableEmissiveTriangleSampling: false,
|
|
88
101
|
emissiveBoost: 1.0,
|
|
@@ -556,10 +569,18 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
|
|
|
556
569
|
// 'interactive' — low-sample, bounded bounces, no offline denoising, controls enabled.
|
|
557
570
|
// 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
|
|
558
571
|
export const PRODUCTION_RENDER_CONFIG = {
|
|
559
|
-
maxSamples
|
|
572
|
+
// maxSamples is a CEILING: adaptive sampling retires the frame once adaptiveStopFraction of pixels converge,
|
|
573
|
+
// so easy scenes finish well under it while hard GI scenes use the full budget.
|
|
574
|
+
maxSamples: 150, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
|
|
560
575
|
renderMode: 1, enableAlphaShadows: true,
|
|
561
576
|
enableOIDN: true, oidnQuality: 'balance',
|
|
562
577
|
interactionModeEnabled: false,
|
|
578
|
+
// Looser thresholds + stop at 90%, leaning on OIDN to clean the residual tail.
|
|
579
|
+
useAdaptiveSampling: true,
|
|
580
|
+
noiseThreshold: 0.1,
|
|
581
|
+
darkNoiseFloor: 0.01,
|
|
582
|
+
adaptiveStopFraction: 0.9,
|
|
583
|
+
usePixelFreeze: true,
|
|
563
584
|
};
|
|
564
585
|
|
|
565
586
|
export const INTERACTIVE_RENDER_CONFIG = {
|
|
@@ -569,6 +590,8 @@ export const INTERACTIVE_RENDER_CONFIG = {
|
|
|
569
590
|
maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
|
|
570
591
|
enableOIDN: false, oidnQuality: 'fast',
|
|
571
592
|
interactionModeEnabled: true,
|
|
593
|
+
useAdaptiveSampling: true, // idle refine stops early when converged; frozen during motion
|
|
594
|
+
usePixelFreeze: true, // speeds up idle refinement on heavy/high-res views; inert while moving (freeze resets)
|
|
572
595
|
};
|
|
573
596
|
|
|
574
597
|
// Memory management constants
|
package/src/PathTracerApp.js
CHANGED
|
@@ -323,6 +323,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
323
323
|
const tracker = this.stages.pathTracer?.vramTracker;
|
|
324
324
|
const frame = this.stages.pathTracer?.frameCount ?? 0;
|
|
325
325
|
if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
|
|
326
|
+
|
|
326
327
|
updateStats( {
|
|
327
328
|
timeElapsed: this.completion.timeElapsed,
|
|
328
329
|
samples: getDisplaySamples( this.stages.pathTracer ),
|
|
@@ -1236,6 +1237,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1236
1237
|
this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
|
|
1237
1238
|
this.stages.pathTracer?.setUniform( 'enableAlphaShadows', config.enableAlphaShadows ?? false );
|
|
1238
1239
|
|
|
1240
|
+
// Tier-1 convergence early-stop (live uniforms, no kernel rebuild). Threshold/min-samples fall back to
|
|
1241
|
+
// engine defaults when a config doesn't override them.
|
|
1242
|
+
this.stages.pathTracer?.setUniform( 'useAdaptiveSampling', config.useAdaptiveSampling ?? false );
|
|
1243
|
+
this.stages.pathTracer?.setUniform( 'noiseThreshold', config.noiseThreshold ?? DEFAULT_STATE.noiseThreshold );
|
|
1244
|
+
this.stages.pathTracer?.setUniform( 'darkNoiseFloor', config.darkNoiseFloor ?? DEFAULT_STATE.darkNoiseFloor );
|
|
1245
|
+
this.stages.pathTracer?.setUniform( 'adaptiveStopFraction', config.adaptiveStopFraction ?? DEFAULT_STATE.adaptiveStopFraction );
|
|
1246
|
+
this.stages.pathTracer?.setUniform( 'adaptiveMinSamples', config.adaptiveMinSamples ?? DEFAULT_STATE.adaptiveMinSamples );
|
|
1247
|
+
|
|
1248
|
+
// Tier-2 per-pixel freeze (per-mode; falls back off when a config doesn't set it).
|
|
1249
|
+
this.stages.pathTracer?.setUniform( 'usePixelFreeze', config.usePixelFreeze ?? false );
|
|
1250
|
+
this.stages.pathTracer?.setUniform( 'pixelFreezeThreshold', config.pixelFreezeThreshold ?? DEFAULT_STATE.pixelFreezeThreshold );
|
|
1251
|
+
this.stages.pathTracer?.setUniform( 'pixelFreezeStability', config.pixelFreezeStability ?? DEFAULT_STATE.pixelFreezeStability );
|
|
1252
|
+
|
|
1239
1253
|
this.stages.pathTracer?.updateCompletionThreshold?.();
|
|
1240
1254
|
|
|
1241
1255
|
const denoiser = this.denoisingManager?.denoiser;
|
|
@@ -48,7 +48,10 @@ export class CompletionTracker {
|
|
|
48
48
|
|
|
49
49
|
if ( this.isTimeLimitReached( renderLimitMode, renderTimeLimit ) ) return true;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// Tier-1 convergence early-stop must agree with PathTracer.render()'s own check, else the app-level
|
|
52
|
+
// reconcile would flip isComplete back off and keep dispatching after the frame converged.
|
|
53
|
+
return pathTracer.frameCount >= pathTracer.completionThreshold
|
|
54
|
+
|| ( pathTracer._isConvergedComplete?.() ?? false );
|
|
52
55
|
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -16,7 +16,6 @@ const WORKGROUP_SIZES = {
|
|
|
16
16
|
connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
17
17
|
accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
18
18
|
compact: [ 256, 1, 1 ], // 1D ray-parallel
|
|
19
|
-
resetCounters: [ 1, 1, 1 ], // Single thread
|
|
20
19
|
finalWrite: [ 16, 16, 1 ], // 2D screen-space
|
|
21
20
|
};
|
|
22
21
|
|
|
@@ -11,7 +11,14 @@ export const COUNTER = {
|
|
|
11
11
|
ACTIVE_RAY_COUNT: 0,
|
|
12
12
|
// rays entering current bounce; snapshotted before ACTIVE_RAY_COUNT reset so over-sized dispatch is safe.
|
|
13
13
|
ENTERING_COUNT: 1,
|
|
14
|
-
|
|
14
|
+
// per-frame count of pixels whose Tier-1 relative-error dropped below threshold; zeroed at frame start by
|
|
15
|
+
// initActiveIndices, incremented in FinalWrite, read back async to drive the whole-frame convergence early-stop.
|
|
16
|
+
CONVERGED_COUNT: 2,
|
|
17
|
+
// Tier-2 per-pixel freeze: FROZEN_COUNT = pixels skipped this frame; ACTIVE_PIXEL_COUNT = bounce-0 active
|
|
18
|
+
// count (maxRays − frozen), read back to size next frame's grid.
|
|
19
|
+
FROZEN_COUNT: 3,
|
|
20
|
+
ACTIVE_PIXEL_COUNT: 4,
|
|
21
|
+
COUNT: 5,
|
|
15
22
|
};
|
|
16
23
|
|
|
17
24
|
/** Ray flag bits packed into rayBounceFlags (uint) */
|
package/src/RenderSettings.js
CHANGED
|
@@ -40,6 +40,15 @@ const SETTING_ROUTES = {
|
|
|
40
40
|
samplingTechnique: { uniform: 'samplingTechnique', reset: true },
|
|
41
41
|
fireflyThreshold: { uniform: 'fireflyThreshold', reset: true },
|
|
42
42
|
enableAlphaShadows: { uniform: 'enableAlphaShadows', reset: true },
|
|
43
|
+
// Adaptive sampling — whole-frame early-stop (useAdaptiveSampling) + per-pixel freeze (usePixelFreeze)
|
|
44
|
+
useAdaptiveSampling: { uniform: 'useAdaptiveSampling', reset: true },
|
|
45
|
+
noiseThreshold: { uniform: 'noiseThreshold', reset: true },
|
|
46
|
+
darkNoiseFloor: { uniform: 'darkNoiseFloor', reset: true },
|
|
47
|
+
adaptiveMinSamples: { uniform: 'adaptiveMinSamples', reset: true },
|
|
48
|
+
adaptiveStopFraction: { uniform: 'adaptiveStopFraction', reset: true },
|
|
49
|
+
usePixelFreeze: { uniform: 'usePixelFreeze', reset: true },
|
|
50
|
+
pixelFreezeThreshold: { uniform: 'pixelFreezeThreshold', reset: true },
|
|
51
|
+
pixelFreezeStability: { uniform: 'pixelFreezeStability', reset: true },
|
|
43
52
|
enableEmissiveTriangleSampling: { uniform: 'enableEmissiveTriangleSampling', reset: true },
|
|
44
53
|
emissiveBoost: { uniform: 'emissiveBoost', reset: true },
|
|
45
54
|
visMode: { uniform: 'visMode', reset: true },
|
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
|
|
@@ -79,6 +85,16 @@ export class PathTracer extends PathTracerStage {
|
|
|
79
85
|
// 0.1% of primary ray count, floored at 100; -1 to disable. Updated per-scene in _buildWavefrontKernels.
|
|
80
86
|
this._bounceEarlyExitThreshold = 100;
|
|
81
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
|
+
|
|
82
98
|
this._wfRenderWidth = uniform( 1920, 'int' );
|
|
83
99
|
this._wfRenderHeight = uniform( 1080, 'int' );
|
|
84
100
|
this._wfMaxRayCount = uniform( 0, 'uint' );
|
|
@@ -117,8 +133,17 @@ export class PathTracer extends PathTracerStage {
|
|
|
117
133
|
|
|
118
134
|
} );
|
|
119
135
|
|
|
120
|
-
// Per-pixel first-hit G-buffer (normal/depth + albedo)
|
|
121
|
-
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
|
+
} );
|
|
122
147
|
|
|
123
148
|
// Accumulation pool: 3 write StorageTextures (2048²) + readable MRT RenderTarget
|
|
124
149
|
t.register( 'accum', () => {
|
|
@@ -174,7 +199,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
174
199
|
// Kernels not built yet (first frame / mid-resize) — skip until ready.
|
|
175
200
|
if ( ! this.isReady || ! this._wavefrontReady ) return;
|
|
176
201
|
|
|
177
|
-
if ( this.isComplete || this.frameCount >= this.completionThreshold ) {
|
|
202
|
+
if ( this.isComplete || this.frameCount >= this.completionThreshold || this._isConvergedComplete() ) {
|
|
178
203
|
|
|
179
204
|
if ( ! this.isComplete ) this.isComplete = true;
|
|
180
205
|
return;
|
|
@@ -212,6 +237,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
212
237
|
|
|
213
238
|
this._curveSizingValid = false;
|
|
214
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;
|
|
215
244
|
|
|
216
245
|
}
|
|
217
246
|
|
|
@@ -255,16 +284,37 @@ export class PathTracer extends PathTracerStage {
|
|
|
255
284
|
|
|
256
285
|
}
|
|
257
286
|
|
|
258
|
-
km.dispatch( 'resetCounters' );
|
|
259
|
-
km.dispatch( 'generate' );
|
|
260
|
-
// Generate traces every pixel; seed ENTERING_COUNT from the full identity active list.
|
|
261
|
-
km.dispatch( 'initActiveIndices' );
|
|
262
|
-
|
|
263
287
|
const maxBounces = this.maxBounces.value;
|
|
264
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.
|
|
265
289
|
const loopBound = maxBounces + this.transmissiveBounces.value + this.maxSubsurfaceSteps.value;
|
|
266
290
|
const maxRays = this._wfMaxRayCount.value;
|
|
267
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
|
+
|
|
268
318
|
// The survivor curve survives a maxBounces change (reset() preserves it), and is reusable
|
|
269
319
|
// across one: for loop iterations below BOTH the old and new camera-bounce caps, no ray has
|
|
270
320
|
// been killed by either cap, so the counts are cap-independent. Trust the curve up to that
|
|
@@ -313,6 +363,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
313
363
|
|
|
314
364
|
entering = prev > 0 ? prev : maxRays;
|
|
315
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
|
+
|
|
316
371
|
}
|
|
317
372
|
|
|
318
373
|
const sized = Math.min( maxRays, Math.ceil( entering * 1.5 ) + 1024 );
|
|
@@ -348,12 +403,18 @@ export class PathTracer extends PathTracerStage {
|
|
|
348
403
|
|
|
349
404
|
}
|
|
350
405
|
|
|
351
|
-
km.dispatch( 'shade' );
|
|
352
|
-
|
|
353
|
-
km.dispatch( 'resetActiveCounter' );
|
|
406
|
+
km.dispatch( 'shade' ); // shade thread 0 folds resetActiveCounter (zeroes ACTIVE_RAY_COUNT before compact)
|
|
354
407
|
km.dispatch( 'compact' );
|
|
355
|
-
if ( useFunctionalCompaction )
|
|
356
|
-
|
|
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
|
+
}
|
|
357
418
|
// No swap: pingPong stays 0 (kernels are build-time-bound to buffer A).
|
|
358
419
|
|
|
359
420
|
// Early-exit on last frame's per-bounce snapshot (stale via async readback, fine for a heuristic).
|
|
@@ -391,6 +452,26 @@ export class PathTracer extends PathTracerStage {
|
|
|
391
452
|
|
|
392
453
|
}
|
|
393
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
|
+
|
|
394
475
|
// Parent resizes storageTextures/shaderBuilder; wavefront also needs its buffers/uniforms/kernels rebuilt.
|
|
395
476
|
_handleResize() {
|
|
396
477
|
|
|
@@ -491,6 +572,40 @@ export class PathTracer extends PathTracerStage {
|
|
|
491
572
|
|
|
492
573
|
} );
|
|
493
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
|
+
|
|
494
609
|
}
|
|
495
610
|
|
|
496
611
|
// Sync wavefront's texture nodes with current env/material textures; only a changed ref triggers GPU rebind.
|
|
@@ -526,6 +641,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
526
641
|
this._curveSizingValid = false;
|
|
527
642
|
this._readbackFrameCounter = 0;
|
|
528
643
|
this._readbackGeneration ++;
|
|
644
|
+
this._convergedFraction = 0;
|
|
645
|
+
this._lastActivePixelCount = 0;
|
|
529
646
|
|
|
530
647
|
// Recompile only when buffers reallocate (capacity grows); otherwise resize uniforms in place.
|
|
531
648
|
const neededCap = PackedRayBuffer.requiredCapacity( newW * newH );
|
|
@@ -599,6 +716,25 @@ export class PathTracer extends PathTracerStage {
|
|
|
599
716
|
const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
|
|
600
717
|
const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
|
|
601
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
|
+
|
|
602
738
|
if ( ! this._queueManager ) {
|
|
603
739
|
|
|
604
740
|
this._queueManager = new QueueManager( this._packedBuffers.capacity, this.renderer );
|
|
@@ -628,25 +764,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
628
764
|
const writeTex = this.storageTextures.getWriteTextures();
|
|
629
765
|
|
|
630
766
|
const counters = qm.getCounters();
|
|
631
|
-
const resetFn = Fn( () => {
|
|
632
|
-
|
|
633
|
-
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
634
|
-
|
|
635
|
-
} );
|
|
636
|
-
this._kernelManager.register( 'resetCounters',
|
|
637
|
-
resetFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
const resetActiveFn = Fn( () => {
|
|
641
|
-
|
|
642
|
-
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
643
|
-
|
|
644
|
-
} );
|
|
645
|
-
this._kernelManager.register( 'resetActiveCounter',
|
|
646
|
-
resetActiveFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
647
|
-
);
|
|
648
767
|
|
|
649
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.
|
|
650
771
|
const bounceCountsBuf = qm.getBounceCounts();
|
|
651
772
|
const wfCurrentBounce = this._wfCurrentBounce;
|
|
652
773
|
const snapshotFn = Fn( () => {
|
|
@@ -672,6 +793,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
672
793
|
|
|
673
794
|
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), this._wfMaxRayCount );
|
|
674
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 ) );
|
|
675
800
|
|
|
676
801
|
} );
|
|
677
802
|
|
|
@@ -680,7 +805,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
680
805
|
initFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
681
806
|
);
|
|
682
807
|
|
|
683
|
-
const
|
|
808
|
+
const genParams = {
|
|
684
809
|
rayBufferRW: pb.rayBuffer.rw,
|
|
685
810
|
rngBufferRW: pb.rngBuffer.rw,
|
|
686
811
|
gBufferRW,
|
|
@@ -700,7 +825,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
700
825
|
transmissiveBounces: this.transmissiveBounces,
|
|
701
826
|
transparentBackground: this.transparentBackground,
|
|
702
827
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
703
|
-
}
|
|
828
|
+
};
|
|
829
|
+
const genFn = buildGenerateKernel( genParams );
|
|
704
830
|
this._kernelManager.register( 'generate',
|
|
705
831
|
genFn().compute(
|
|
706
832
|
[ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( h / GENERATE_WG_SIZE ), 1 ],
|
|
@@ -708,6 +834,100 @@ export class PathTracer extends PathTracerStage {
|
|
|
708
834
|
)
|
|
709
835
|
);
|
|
710
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
|
+
|
|
711
931
|
const freshBvh = this.bvhStorageNode;
|
|
712
932
|
const freshTri = this.triangleStorageNode;
|
|
713
933
|
const freshMat = this.materialData.materialStorageNode;
|
|
@@ -900,7 +1120,19 @@ export class PathTracer extends PathTracerStage {
|
|
|
900
1120
|
const copyFn = Fn( () => {
|
|
901
1121
|
|
|
902
1122
|
const tid = instanceIndex;
|
|
903
|
-
|
|
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 ), () => {
|
|
904
1136
|
|
|
905
1137
|
Return();
|
|
906
1138
|
|
|
@@ -933,6 +1165,18 @@ export class PathTracer extends PathTracerStage {
|
|
|
933
1165
|
visMode: this.visMode,
|
|
934
1166
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
935
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)
|
|
936
1180
|
} );
|
|
937
1181
|
this._kernelManager.register( 'finalWrite',
|
|
938
1182
|
// Per-pixel (w×h) — kernel averages the S sample-slots internally.
|
|
@@ -1012,10 +1256,16 @@ export class PathTracer extends PathTracerStage {
|
|
|
1012
1256
|
this._queueManager?.dispose();
|
|
1013
1257
|
this._kernelManager?.dispose();
|
|
1014
1258
|
this._gBufferAttr?.dispose?.();
|
|
1259
|
+
this._m2Attr?.dispose?.();
|
|
1260
|
+
this._streakAttr?.dispose?.();
|
|
1261
|
+
this._frozenMaskAttr?.dispose?.();
|
|
1015
1262
|
this._packedBuffers = null;
|
|
1016
1263
|
this._queueManager = null;
|
|
1017
1264
|
this._kernelManager = null;
|
|
1018
1265
|
this._gBufferAttr = null;
|
|
1266
|
+
this._m2Attr = null;
|
|
1267
|
+
this._streakAttr = null;
|
|
1268
|
+
this._frozenMaskAttr = null;
|
|
1019
1269
|
this._wavefrontReady = false;
|
|
1020
1270
|
|
|
1021
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';
|