rayzee 7.16.0 → 7.18.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/assets/BVHSubtreeWorker-D7KMbrQ-.js +2 -0
- package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js.map +1 -0
- package/dist/assets/BVHWorker-DOaGn6uN.js +2 -0
- package/dist/assets/BVHWorker-DOaGn6uN.js.map +1 -0
- package/dist/rayzee.es.js +2817 -2537
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +55 -55
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +27 -11
- package/src/Passes/OIDNDenoiser.js +17 -14
- package/src/PathTracerApp.js +86 -13
- package/src/Processor/BVHBuilder.js +11 -7
- package/src/Processor/BuildTimer.js +20 -6
- package/src/Processor/EmissiveTriangleBuilder.js +11 -7
- package/src/Processor/GeometryExtractor.js +11 -5
- package/src/Processor/KernelManager.js +48 -1
- package/src/Processor/PackedRayBuffer.js +69 -35
- package/src/Processor/ParallelBVHBuilder.js +9 -6
- package/src/Processor/QueueManager.js +6 -8
- package/src/Processor/SceneProcessor.js +47 -21
- package/src/Processor/ShaderBuilder.js +4 -1
- package/src/Processor/StorageTexturePool.js +4 -1
- package/src/Processor/Workers/BVHWorker.js +12 -6
- package/src/RenderSettings.js +1 -1
- package/src/Stages/ASVGF.js +38 -0
- package/src/Stages/BilateralFilter.js +23 -0
- package/src/Stages/EdgeFilter.js +18 -0
- package/src/Stages/MotionVector.js +23 -0
- package/src/Stages/NormalDepth.js +22 -0
- package/src/Stages/PathTracer.js +324 -169
- package/src/Stages/PathTracerStage.js +17 -14
- package/src/Stages/Variance.js +23 -0
- package/src/TSL/FinalWriteKernel.js +30 -18
- package/src/TSL/GenerateKernel.js +28 -17
- package/src/TSL/ShadeKernel.js +186 -87
- package/src/TSL/TextureSampling.js +12 -2
- package/src/index.js +3 -0
- package/src/managers/EnvironmentManager.js +12 -8
- package/src/managers/MaterialDataManager.js +12 -9
- package/src/managers/UniformManager.js +1 -1
- package/src/utils/Logger.js +291 -0
- package/dist/assets/BVHSubtreeWorker-sNzvxn66.js +0 -2
- package/dist/assets/BVHSubtreeWorker-sNzvxn66.js.map +0 -1
- package/dist/assets/BVHWorker-CiVdFrwe.js +0 -2
- package/dist/assets/BVHWorker-CiVdFrwe.js.map +0 -1
package/src/Stages/PathTracer.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { uniform, texture, storage } from 'three/tsl';
|
|
8
8
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
9
9
|
import { PathTracerStage } from './PathTracerStage.js';
|
|
10
|
-
import { PackedRayBuffer, GBUFFER_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
|
|
10
|
+
import { PackedRayBuffer, GBUFFER_STRIDE, RAY_STRIDE, HIT_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
|
|
11
11
|
import { QueueManager, COUNTER } from '../Processor/QueueManager.js';
|
|
12
12
|
import { VRAMTracker } from '../Processor/VRAMTracker.js';
|
|
13
13
|
import { KernelManager } from '../Processor/KernelManager.js';
|
|
@@ -23,11 +23,14 @@ import {
|
|
|
23
23
|
buildResetGlobalHistKernel, buildGlobalHistKernel, buildGlobalPrefixKernel, buildGlobalScatterKernel,
|
|
24
24
|
SORT_GLOBAL_WG_SIZE, SORT_GLOBAL_MAX_BINS,
|
|
25
25
|
} from '../TSL/SortGlobalKernels.js';
|
|
26
|
-
import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
|
|
26
|
+
import { ENGINE_DEFAULTS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
27
|
+
import { createLogger, fmt } from '../utils/Logger.js';
|
|
27
28
|
import {
|
|
28
29
|
Fn, uint, int, atomicStore, atomicLoad, atomicAdd, instanceIndex, If, Return,
|
|
29
30
|
} from 'three/tsl';
|
|
30
31
|
|
|
32
|
+
const log = createLogger( 'wavefront' );
|
|
33
|
+
|
|
31
34
|
export class PathTracer extends PathTracerStage {
|
|
32
35
|
|
|
33
36
|
constructor( renderer, scene, camera, options = {} ) {
|
|
@@ -100,12 +103,24 @@ export class PathTracer extends PathTracerStage {
|
|
|
100
103
|
this._wfMaxRayCount = uniform( 0, 'uint' );
|
|
101
104
|
this._wfCurrentBounce = uniform( 0, 'int' );
|
|
102
105
|
|
|
106
|
+
// Blender-style chunked path pool (docs/internal/specs/wavefront-chunked-pool.md): the per-path SoA
|
|
107
|
+
// pool holds a fixed device-budget B of paths-in-flight, decoupled from resolution. The image is
|
|
108
|
+
// streamed through it in row bands; these uniforms carry the current band's offset so kernels map
|
|
109
|
+
// local path slot r ↔ global pixel p = pixelBase + r (pixelBase = _wfChunkRowBase · renderWidth).
|
|
110
|
+
this._wfChunkRowBase = uniform( 0, 'int' ); // first GLOBAL row of the current chunk
|
|
111
|
+
this._wfChunkRows = uniform( 0, 'int' ); // number of rows in the current chunk (≤ chunkRows)
|
|
112
|
+
this._wfIsFirstChunk = uniform( 1, 'uint' ); // 1 → zero frame-scoped counters (CONVERGED/FROZEN)
|
|
113
|
+
this._pathBudget = 0; // B, paths-in-flight; computed once from device limits in _buildWavefrontKernels
|
|
114
|
+
this._pathBudgetOverride = 0; // test hook: force B (0 = derive from device)
|
|
115
|
+
this._chunkRows = 0; // rows per chunk = floor(B / renderWidth), clamped ≥1
|
|
116
|
+
this._numChunks = 1; // ceil(renderHeight / chunkRows)
|
|
117
|
+
|
|
103
118
|
// VRAM accounting — providers are thunks reading CURRENT live resources,
|
|
104
119
|
// so they survive buffer/texture reallocation (resize, scene/material reload).
|
|
105
120
|
this.vramTracker = new VRAMTracker( this.renderer );
|
|
106
121
|
this._registerVRAMProviders();
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
log.debug( 'initialized (wavefront)' );
|
|
109
124
|
|
|
110
125
|
}
|
|
111
126
|
|
|
@@ -298,151 +313,157 @@ export class PathTracer extends PathTracerStage {
|
|
|
298
313
|
const maxBounces = this.maxBounces.value;
|
|
299
314
|
// 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.
|
|
300
315
|
const loopBound = maxBounces + this.transmissiveBounces.value + this.maxSubsurfaceSteps.value;
|
|
301
|
-
const maxRays = this._wfMaxRayCount.value;
|
|
302
316
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
317
|
+
const frameHeight = this._wfRenderHeight.value;
|
|
318
|
+
const chunkRows = this._chunkRows;
|
|
319
|
+
const singleChunk = this._numChunks <= 1;
|
|
320
|
+
|
|
321
|
+
// The survivor curve is a single per-frame buffer; multi-chunk bands would overwrite each other's
|
|
322
|
+
// counts, so curve-based sizing AND the per-bounce early-exit apply only in the single-chunk regime
|
|
323
|
+
// (the common case → behaviour identical to before). Multi-chunk (huge frame / small budget) runs full
|
|
324
|
+
// dispatch over every bounce — correct, mildly slower.
|
|
325
|
+
//
|
|
326
|
+
// The curve survives a maxBounces change (reset() preserves it): for iterations below BOTH the old and
|
|
327
|
+
// new camera-bounce caps the counts are cap-independent, so trust it up to that cutoff (whole curve when
|
|
328
|
+
// same/decreasing budget; only the [0, oldBudget) overlap when increasing). budget=-1 → cutoff 0 → full
|
|
329
|
+
// dispatch everywhere. This avoids the full-work spike on every bounce-count change.
|
|
330
|
+
const curve = this._lastBounceCounts;
|
|
331
|
+
const curveReliableUpto = ( singleChunk && curve )
|
|
332
|
+
? ( maxBounces <= this._lastBounceCountsBudget ? loopBound + 1 : this._lastBounceCountsBudget )
|
|
333
|
+
: 0;
|
|
308
334
|
|
|
309
|
-
|
|
335
|
+
// Blender-style row-band streaming: the fixed-budget path pool processes the image in bands of ≤ chunkRows
|
|
336
|
+
// rows (one chunk when the frame fits the budget → identical to the pre-chunking path). See spec.
|
|
337
|
+
for ( let chunkIndex = 0, rowBase = 0; rowBase < frameHeight; rowBase += chunkRows, chunkIndex ++ ) {
|
|
310
338
|
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
km.setDispatchCount( 'buildActivePixels', [ Math.ceil( maxRays / 256 ), 1, 1 ] );
|
|
314
|
-
km.setDispatchCount( 'generateList', [ Math.ceil( genSized / 256 ), 1, 1 ] );
|
|
315
|
-
km.dispatch( 'resetFrameCounters' );
|
|
316
|
-
km.dispatch( 'buildActivePixels' );
|
|
317
|
-
km.dispatch( 'seedEnter' );
|
|
318
|
-
km.dispatch( 'generateList' );
|
|
339
|
+
const rows = Math.min( chunkRows, frameHeight - rowBase );
|
|
340
|
+
const maxRays = this._setChunk( rowBase, rows, chunkIndex ); // this band's pixel count + per-chunk grids
|
|
319
341
|
|
|
320
|
-
|
|
342
|
+
// Tier-2 bounce-0 grid size from the stale settled active-pixel count (single-chunk only; else full).
|
|
343
|
+
// Monotonic freeze ⇒ a stale count is a safe over-estimate; reset/camera-move → _curveSizingValid=false.
|
|
344
|
+
const adaptiveFreeze = this.usePixelFreeze.value > 0;
|
|
345
|
+
const activeBounce0 = ( singleChunk && adaptiveFreeze && this._curveSizingValid && this._lastActivePixelCount > 0 )
|
|
346
|
+
? this._lastActivePixelCount : maxRays;
|
|
321
347
|
|
|
322
|
-
|
|
323
|
-
// Generate traces every pixel; seed ACTIVE + ENTERING_COUNT from the full identity active list
|
|
324
|
-
// (initActiveIndices overwrites both counters, so no separate frame-start reset is needed).
|
|
325
|
-
km.dispatch( 'initActiveIndices' );
|
|
348
|
+
if ( adaptiveFreeze ) {
|
|
326
349
|
|
|
327
|
-
|
|
350
|
+
// reset counters → compact non-frozen pixel IDs → publish count → list-driven 1D generate.
|
|
351
|
+
const genSized = Math.min( maxRays, Math.ceil( activeBounce0 * 1.5 ) + 1024 );
|
|
352
|
+
km.setDispatchCount( 'generateList', [ Math.ceil( genSized / 256 ), 1, 1 ] );
|
|
353
|
+
km.dispatch( 'resetFrameCounters' );
|
|
354
|
+
km.dispatch( 'buildActivePixels' );
|
|
355
|
+
km.dispatch( 'seedEnter' );
|
|
356
|
+
km.dispatch( 'generateList' );
|
|
328
357
|
|
|
329
|
-
|
|
330
|
-
// across one: for loop iterations below BOTH the old and new camera-bounce caps, no ray has
|
|
331
|
-
// been killed by either cap, so the counts are cap-independent. Trust the curve up to that
|
|
332
|
-
// cutoff — the whole curve when same-budget or decreasing (old counts only over-estimate, so
|
|
333
|
-
// sizing over-sizes and early-exit fires later — both safe); only the overlap [0, oldBudget)
|
|
334
|
-
// when increasing (beyond it the old cap already culled rays → under-estimate → would drop
|
|
335
|
-
// rays). Past the cutoff, full dispatch + no early-exit. Avoids the full-work spike (a visible
|
|
336
|
-
// hitch) on every bounce-count change while a fresh curve is read back. budget=-1 (no curve,
|
|
337
|
-
// e.g. cold start / post-resize) → cutoff 0 → full dispatch everywhere, matching prior behavior.
|
|
338
|
-
const curve = this._lastBounceCounts;
|
|
339
|
-
const curveReliableUpto = curve
|
|
340
|
-
? ( maxBounces <= this._lastBounceCountsBudget ? loopBound + 1 : this._lastBounceCountsBudget )
|
|
341
|
-
: 0;
|
|
358
|
+
} else {
|
|
342
359
|
|
|
343
|
-
|
|
360
|
+
km.dispatch( 'generate' );
|
|
361
|
+
// Generate traces every pixel in the band; initActiveIndices seeds the identity active list + counts
|
|
362
|
+
// (it overwrites ACTIVE + ENTERING, so no separate frame-start reset is needed).
|
|
363
|
+
km.dispatch( 'initActiveIndices' );
|
|
344
364
|
|
|
345
|
-
|
|
365
|
+
}
|
|
346
366
|
|
|
347
|
-
|
|
348
|
-
// Material sort is compatible: shade reads sortedIndices while compact still reads the UNSORTED active list (getActiveReadRO), so the survivor set is unchanged.
|
|
349
|
-
const useFunctionalCompaction = this._useDynamicDispatch;
|
|
350
|
-
if ( useFunctionalCompaction ) {
|
|
367
|
+
for ( let bounce = 0; bounce <= loopBound; bounce ++ ) {
|
|
351
368
|
|
|
352
|
-
|
|
353
|
-
// Only trust the curve for sizing when it was measured at the CURRENT (settled) view; while
|
|
354
|
-
// the camera moves / before a settled readback, full-size (entering = maxRays) so the tail is
|
|
355
|
-
// never dropped. Early-exit below still uses the curve regardless (a stale one only trims empty deep bounces).
|
|
356
|
-
let entering = maxRays;
|
|
357
|
-
if ( bounce > 0 && this._curveSizingValid ) {
|
|
369
|
+
this._wfCurrentBounce.value = bounce;
|
|
358
370
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
371
|
+
// Functional-compaction path (dynamic dispatch): copyback keeps the read buffer dense, kernels sized to live survivors. Dynamic-off uses the full path (ENTERING=maxRays, identity buffer).
|
|
372
|
+
// Material sort is compatible: shade reads sortedIndices while compact still reads the UNSORTED active list (getActiveReadRO), so the survivor set is unchanged.
|
|
373
|
+
const useFunctionalCompaction = this._useDynamicDispatch;
|
|
374
|
+
if ( useFunctionalCompaction ) {
|
|
362
375
|
|
|
363
|
-
|
|
376
|
+
// ENTERING_COUNT already set (bounce 0 by initActiveIndices, N>0 by snapshotBounceCount); size from last frame's survivor curve with a 1.5×+1024 margin (single-chunk only).
|
|
377
|
+
let entering = maxRays;
|
|
378
|
+
if ( singleChunk && bounce > 0 && this._curveSizingValid ) {
|
|
364
379
|
|
|
365
|
-
|
|
380
|
+
const idx = bounce - 1;
|
|
381
|
+
let prev;
|
|
382
|
+
if ( idx < curveReliableUpto && curve[ idx ] !== undefined ) {
|
|
366
383
|
|
|
367
|
-
|
|
368
|
-
// non-increasing across bounces (rays only terminate), so the last trusted count is
|
|
369
|
-
// a safe upper bound — far below maxRays, so no full-dispatch spike. Safe even if it
|
|
370
|
-
// reads low: a count <= threshold would have tripped the early-exit before this bounce.
|
|
371
|
-
prev = curve[ curveReliableUpto - 1 ];
|
|
384
|
+
prev = curve[ idx ]; // trusted exact count
|
|
372
385
|
|
|
373
|
-
|
|
386
|
+
} else if ( curveReliableUpto > 0 ) {
|
|
374
387
|
|
|
375
|
-
|
|
388
|
+
// Untrusted tail after a maxBounces increase: survivor counts are monotonically
|
|
389
|
+
// non-increasing (rays only terminate), so the last trusted count is a safe upper bound.
|
|
390
|
+
prev = curve[ curveReliableUpto - 1 ];
|
|
376
391
|
|
|
377
|
-
|
|
392
|
+
}
|
|
378
393
|
|
|
379
|
-
|
|
380
|
-
entering = activeBounce0;
|
|
394
|
+
entering = prev > 0 ? prev : maxRays;
|
|
381
395
|
|
|
382
|
-
|
|
396
|
+
} else if ( bounce === 0 && adaptiveFreeze ) {
|
|
383
397
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
km.setDispatchCount( 'extend', wg );
|
|
387
|
-
km.setDispatchCount( 'shade', wg );
|
|
388
|
-
km.setDispatchCount( 'globalHist', wg );
|
|
389
|
-
km.setDispatchCount( 'globalScatter', wg );
|
|
390
|
-
km.setDispatchCount( 'compact', wg );
|
|
391
|
-
km.setDispatchCount( 'compactCopyback', wg );
|
|
398
|
+
// Bounce 0 traces only non-frozen pixels — size from the stale active count (full until a readback).
|
|
399
|
+
entering = activeBounce0;
|
|
392
400
|
|
|
393
|
-
|
|
401
|
+
}
|
|
394
402
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
403
|
+
const sized = Math.min( maxRays, Math.ceil( entering * 1.5 ) + 1024 );
|
|
404
|
+
const wg = [ Math.ceil( sized / 256 ), 1, 1 ];
|
|
405
|
+
km.setDispatchCount( 'extend', wg );
|
|
406
|
+
km.setDispatchCount( 'shade', wg );
|
|
407
|
+
km.setDispatchCount( 'globalHist', wg );
|
|
408
|
+
km.setDispatchCount( 'globalScatter', wg );
|
|
409
|
+
km.setDispatchCount( 'compact', wg );
|
|
410
|
+
km.setDispatchCount( 'compactCopyback', wg );
|
|
411
|
+
|
|
412
|
+
} else {
|
|
413
|
+
|
|
414
|
+
km.dispatch( 'enterFull' );
|
|
415
|
+
const full = [ Math.ceil( maxRays / 256 ), 1, 1 ];
|
|
416
|
+
km.setDispatchCount( 'extend', full );
|
|
417
|
+
km.setDispatchCount( 'shade', full );
|
|
418
|
+
km.setDispatchCount( 'compact', full );
|
|
419
|
+
km.setDispatchCount( 'globalHist', full );
|
|
420
|
+
km.setDispatchCount( 'globalScatter', full );
|
|
402
421
|
|
|
403
|
-
|
|
422
|
+
}
|
|
404
423
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
424
|
+
// Extend/Shade kept separate (not fused): a fused kernel's register pressure drops occupancy more than fusion saves.
|
|
425
|
+
km.dispatch( 'extend' );
|
|
426
|
+
if ( this._sortMaterials ) {
|
|
408
427
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
428
|
+
// Global material counting sort (material-pure workgroups): reset, histogram, prefix-sum, scatter.
|
|
429
|
+
km.dispatch( 'resetGlobalHist' );
|
|
430
|
+
km.dispatch( 'globalHist' );
|
|
431
|
+
km.dispatch( 'globalPrefix' );
|
|
432
|
+
km.dispatch( 'globalScatter' );
|
|
414
433
|
|
|
415
|
-
|
|
434
|
+
}
|
|
416
435
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
436
|
+
km.dispatch( 'shade' ); // shade thread 0 folds resetActiveCounter (zeroes ACTIVE_RAY_COUNT before compact)
|
|
437
|
+
km.dispatch( 'compact' );
|
|
438
|
+
if ( useFunctionalCompaction ) {
|
|
420
439
|
|
|
421
|
-
|
|
422
|
-
|
|
440
|
+
// compactCopyback thread 0 folds snapshotBounceCount (records the survivor curve + seeds ENTERING).
|
|
441
|
+
km.dispatch( 'compactCopyback' );
|
|
423
442
|
|
|
424
|
-
|
|
443
|
+
} else {
|
|
425
444
|
|
|
426
|
-
|
|
445
|
+
km.dispatch( 'snapshotBounceCount' );
|
|
427
446
|
|
|
428
|
-
|
|
429
|
-
|
|
447
|
+
}
|
|
448
|
+
// No swap: pingPong stays 0 (kernels are build-time-bound to buffer A).
|
|
430
449
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
450
|
+
// Early-exit on last frame's per-bounce snapshot (single-chunk only; curveReliableUpto=0 disables it for multi-chunk).
|
|
451
|
+
if (
|
|
452
|
+
bounce < curveReliableUpto
|
|
453
|
+
&& bounce < loopBound
|
|
454
|
+
&& curve[ bounce ] !== undefined
|
|
455
|
+
&& curve[ bounce ] <= this._bounceEarlyExitThreshold
|
|
456
|
+
) {
|
|
438
457
|
|
|
439
|
-
|
|
458
|
+
break;
|
|
459
|
+
|
|
460
|
+
}
|
|
440
461
|
|
|
441
462
|
}
|
|
442
463
|
|
|
443
|
-
|
|
464
|
+
km.dispatch( 'finalWrite' );
|
|
444
465
|
|
|
445
|
-
|
|
466
|
+
}
|
|
446
467
|
|
|
447
468
|
this._maybeReadbackCounters();
|
|
448
469
|
|
|
@@ -578,7 +599,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
578
599
|
|
|
579
600
|
} ).catch( ( e ) => {
|
|
580
601
|
|
|
581
|
-
|
|
602
|
+
log.warn( 'bounceCounts readback failed:', e );
|
|
582
603
|
this._readbackPending = false;
|
|
583
604
|
|
|
584
605
|
} );
|
|
@@ -593,7 +614,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
593
614
|
|
|
594
615
|
this._convergedReadbackPending = true;
|
|
595
616
|
const cgen = this._readbackGeneration;
|
|
596
|
-
|
|
617
|
+
// Full-FRAME pixel count (CONVERGED_COUNT sums across all chunks) — not _wfMaxRayCount, which
|
|
618
|
+
// after the chunk loop holds only the last band's pixel count.
|
|
619
|
+
const total = this._wfRenderWidth.value * this._wfRenderHeight.value;
|
|
597
620
|
this.renderer.getArrayBufferAsync( cAttr ).then( ( buf ) => {
|
|
598
621
|
|
|
599
622
|
if ( cgen === this._readbackGeneration && total > 0 ) {
|
|
@@ -655,12 +678,13 @@ export class PathTracer extends PathTracerStage {
|
|
|
655
678
|
this._convergedFraction = 0;
|
|
656
679
|
this._lastActivePixelCount = 0;
|
|
657
680
|
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
681
|
+
// Chunked path pool: the wavefront buffers are sized to the fixed device budget B (not the
|
|
682
|
+
// resolution), and the per-pixel persistent buffers to the reserved max, so a resolution change
|
|
683
|
+
// reallocates NOTHING and rebuilds NO kernels — it only updates the render-size uniforms + the
|
|
684
|
+
// row-band chunk layout. The one-time build happens on model load / light realloc. This is the fix
|
|
685
|
+
// for the resize freeze (was: capacity-grow → recreate all compute nodes → WGSL regen, ~1.8 s on
|
|
686
|
+
// complex scenes). See docs/internal/specs/wavefront-chunked-pool.md.
|
|
687
|
+
if ( ! this._packedBuffers ) {
|
|
664
688
|
|
|
665
689
|
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
666
690
|
this._wavefrontReady = false;
|
|
@@ -674,23 +698,93 @@ export class PathTracer extends PathTracerStage {
|
|
|
674
698
|
|
|
675
699
|
}
|
|
676
700
|
|
|
677
|
-
//
|
|
701
|
+
// Resolution change with a fixed-budget pool: update render-size uniforms + recompute the row-band chunk
|
|
702
|
+
// layout + rescale the early-exit threshold. No buffer realloc, no kernel recompile.
|
|
678
703
|
_resizeWavefrontInPlace( w, h ) {
|
|
679
704
|
|
|
680
|
-
const maxRays = w * h;
|
|
681
705
|
this._wfRenderWidth.value = w;
|
|
682
706
|
this._wfRenderHeight.value = h;
|
|
683
|
-
this.
|
|
684
|
-
// initActiveIndices is dispatched bare (not re-sized per frame); its grid must cover the grown range or the identity buffer is left unseeded over [oldMaxRays, maxRays).
|
|
685
|
-
this._kernelManager?.setDispatchCount( 'initActiveIndices', [ Math.ceil( maxRays / 256 ), 1, 1 ] );
|
|
707
|
+
this._updateChunkLayout();
|
|
686
708
|
if ( this._bounceEarlyExitThreshold !== - 1 ) {
|
|
687
709
|
|
|
688
|
-
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor(
|
|
710
|
+
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor( ( w * h ) / 1000 ) );
|
|
689
711
|
|
|
690
712
|
}
|
|
691
713
|
|
|
692
714
|
}
|
|
693
715
|
|
|
716
|
+
// Device-adaptive paths-in-flight budget B (Blender path-pool). Constrained by:
|
|
717
|
+
// (1) each single storage buffer ≤ maxStorageBufferBindingSize — the RAY buffer (B·RAY_STRIDE·16 B) binds
|
|
718
|
+
// largest, so B·RAY_STRIDE·16 ≤ 0.9·maxStorageBufferBindingSize;
|
|
719
|
+
// (2) total pool ≤ a fraction of device memory;
|
|
720
|
+
// (3) floor at 512² of paths, cap at the reserved max pixels (no point pooling more paths than pixels).
|
|
721
|
+
// B is resolution-INDEPENDENT, so _cap is fixed and the kernels build once. The render-loop chunking streams
|
|
722
|
+
// any resolution through this pool, so a small B just means more row-bands (works on weak / small-VRAM GPUs).
|
|
723
|
+
_computePathBudget() {
|
|
724
|
+
|
|
725
|
+
if ( this._pathBudgetOverride > 0 ) return this._pathBudgetOverride;
|
|
726
|
+
|
|
727
|
+
const RAY_BYTES = RAY_STRIDE * 16;
|
|
728
|
+
const bytesPerPath = RAY_STRIDE * 16 + HIT_STRIDE * 16 + 4 /* rng */ + GBUFFER_STRIDE * 16
|
|
729
|
+
+ 4 + 4 /* activeIndices A/B */ + 4;
|
|
730
|
+
|
|
731
|
+
const limits = this.renderer?.backend?.device?.limits;
|
|
732
|
+
const maxBinding = limits?.maxStorageBufferBindingSize || ( 128 * 1024 * 1024 );
|
|
733
|
+
const maxBuffer = limits?.maxBufferSize || maxBinding;
|
|
734
|
+
|
|
735
|
+
const bByBinding = Math.floor( ( maxBinding * 0.9 ) / RAY_BYTES );
|
|
736
|
+
const deviceMemBytes = ( ( typeof navigator !== 'undefined' && navigator.deviceMemory ) || 4 ) * 1024 * 1024 * 1024;
|
|
737
|
+
const poolVramBudget = Math.min( deviceMemBytes * 0.25, maxBuffer * 4 );
|
|
738
|
+
const bByVram = Math.floor( poolVramBudget / bytesPerPath );
|
|
739
|
+
|
|
740
|
+
const MIN_B = 512 * 512;
|
|
741
|
+
// Working-set ceiling — deliberately DECOUPLED from the reserved framebuffer size. Capping the pool at
|
|
742
|
+
// ~2048² paths (~940 MB) keeps VRAM bounded; frames larger than B (e.g. 4K) just stream in more row-band
|
|
743
|
+
// chunks rather than reserving a giant pool (4K single-chunk would be ~3 GB of path state for ~no speedup).
|
|
744
|
+
const MAX_B = 2048 * 2048;
|
|
745
|
+
let B = Math.min( bByBinding, bByVram, MAX_B );
|
|
746
|
+
B = Math.max( MIN_B, B );
|
|
747
|
+
return B;
|
|
748
|
+
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Row-aligned chunk layout for the current resolution and budget B. A chunk is `_chunkRows` full rows so the
|
|
752
|
+
// 2D ray-gen grid stays cache-coherent and pixelBase = rowBase·W is exact. chunkPixels = _chunkRows·W ≤ B.
|
|
753
|
+
_updateChunkLayout() {
|
|
754
|
+
|
|
755
|
+
const w = Math.max( 1, this.storageTextures.renderWidth );
|
|
756
|
+
const h = this.storageTextures.renderHeight;
|
|
757
|
+
const B = this._pathBudget || ( w * h );
|
|
758
|
+
this._chunkRows = Math.max( 1, Math.min( Math.floor( B / w ), h ) );
|
|
759
|
+
this._numChunks = Math.max( 1, Math.ceil( h / this._chunkRows ) );
|
|
760
|
+
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Reserved-storage change (e.g. enabling 4K): the MRT StorageTextures are pre-allocated at
|
|
764
|
+
// MAX_STORAGE_TEXTURE_SIZE and can't be resized, so recreate the pool at the (new) live reserved size and
|
|
765
|
+
// rebuild the wavefront kernels in place. The stage OBJECT is unchanged, so manager/event refs stay valid;
|
|
766
|
+
// only GPU textures + compute pipelines are rebuilt. Scene buffers (BVH/tri/material) are resolution-
|
|
767
|
+
// independent and untouched. Caller must have rendering paused. No-op before the first build.
|
|
768
|
+
reallocateReservedStorage() {
|
|
769
|
+
|
|
770
|
+
if ( ! this._packedBuffers || ! ( this.materialData?.materialCount > 0 ) ) return;
|
|
771
|
+
|
|
772
|
+
const w = this.storageTextures.renderWidth || 1;
|
|
773
|
+
const h = this.storageTextures.renderHeight || 1;
|
|
774
|
+
|
|
775
|
+
// Recreate the MRT pool at the new reserved size (create() reallocates the write StorageTextures at the
|
|
776
|
+
// live MAX_STORAGE_TEXTURE_SIZE; the read RenderTarget follows the current render size).
|
|
777
|
+
this.storageTextures.create( w, h );
|
|
778
|
+
this.resolution.value.set( w, h );
|
|
779
|
+
|
|
780
|
+
// Rebuild kernels: re-references the fresh write textures + recreates the per-pixel aux buffers
|
|
781
|
+
// (m2/streak/frozenMask) at the new maxPixels. Prev-frame nodes are repointed per-frame in render().
|
|
782
|
+
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
783
|
+
this._wavefrontReady = false;
|
|
784
|
+
this._buildWavefrontKernels();
|
|
785
|
+
|
|
786
|
+
}
|
|
787
|
+
|
|
694
788
|
_buildWavefrontKernels() {
|
|
695
789
|
|
|
696
790
|
const texNodes = this.shaderBuilder.getSceneTextureNodes();
|
|
@@ -701,51 +795,63 @@ export class PathTracer extends PathTracerStage {
|
|
|
701
795
|
|
|
702
796
|
const w = this.storageTextures.renderWidth;
|
|
703
797
|
const h = this.storageTextures.renderHeight;
|
|
704
|
-
|
|
798
|
+
|
|
799
|
+
// Fixed device-budget path pool B (resolution-independent) + the row-band chunk layout for this frame.
|
|
800
|
+
if ( ! this._pathBudget ) this._pathBudget = this._computePathBudget();
|
|
801
|
+
const B = this._pathBudget;
|
|
802
|
+
this._updateChunkLayout();
|
|
803
|
+
|
|
804
|
+
// Rays in one row-band chunk — the initial dispatch bound for kernel registration. render() overrides
|
|
805
|
+
// every per-ray kernel's dispatch per chunk; this is just a valid starting grid (always ≤ B).
|
|
806
|
+
const maxRays = this._chunkRows * w;
|
|
705
807
|
|
|
706
808
|
if ( this._bounceEarlyExitThreshold !== - 1 ) {
|
|
707
809
|
|
|
708
|
-
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor(
|
|
810
|
+
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor( ( w * h ) / 1000 ) );
|
|
709
811
|
|
|
710
812
|
}
|
|
711
813
|
|
|
814
|
+
// Per-path buffers (RAY/HIT/rng) sized to the budget B and indexed by LOCAL slot r ∈ [0,B). _cap = B is
|
|
815
|
+
// baked into the SoA stride but never changes with resolution, so this build happens once (model load).
|
|
712
816
|
if ( ! this._packedBuffers ) {
|
|
713
817
|
|
|
714
|
-
this._packedBuffers = new PackedRayBuffer(
|
|
818
|
+
this._packedBuffers = new PackedRayBuffer( B, this.renderer );
|
|
715
819
|
|
|
716
820
|
} else {
|
|
717
821
|
|
|
718
|
-
this._packedBuffers.resize(
|
|
822
|
+
this._packedBuffers.resize( B );
|
|
719
823
|
|
|
720
824
|
}
|
|
721
825
|
|
|
722
|
-
// Per-
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
freeStorageAttribute( this.renderer, this._gBufferAttr ); // free the outgoing G-buffer on capacity growth
|
|
826
|
+
// Per-CHUNK first-hit G-buffer (LOCAL slot r, size B), 1 uvec4/slot half-packed (pack2x16). Written by
|
|
827
|
+
// Generate + Shade(bounce 0), read by Shade + FinalWrite within the SAME chunk, so per-chunk suffices.
|
|
828
|
+
// uint (not f32): packed lanes can hit the NaN exponent range (snorm 1.0 → 0x7FFF) that an f32 store may
|
|
829
|
+
// canonicalize; u32 stores the bits verbatim.
|
|
830
|
+
const gBufferVec4s = B * GBUFFER_STRIDE;
|
|
831
|
+
freeStorageAttribute( this.renderer, this._gBufferAttr );
|
|
729
832
|
this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
|
|
730
833
|
const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
|
|
731
834
|
const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
|
|
732
835
|
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
836
|
+
// Per-PIXEL persistent buffers (m2/streak/frozenMask) are GLOBAL-pixel-indexed (p = pixelBase + r) and
|
|
837
|
+
// must span the whole frame across chunks AND persist across frames, so they're sized to the reserved
|
|
838
|
+
// max resolution (small — 12 B/pixel) and NEVER realloc on a resize.
|
|
839
|
+
const maxPixels = MAX_STORAGE_TEXTURE_SIZE * MAX_STORAGE_TEXTURE_SIZE;
|
|
840
|
+
|
|
841
|
+
// Tier-1 convergence: per-pixel running mean of luminance² (Welford second moment), read+written by FinalWrite.
|
|
736
842
|
freeStorageAttribute( this.renderer, this._m2Attr );
|
|
737
|
-
this._m2Attr = new StorageInstancedBufferAttribute( new Float32Array(
|
|
843
|
+
this._m2Attr = new StorageInstancedBufferAttribute( new Float32Array( maxPixels ), 1 );
|
|
738
844
|
const m2RW = storage( this._m2Attr, 'float' );
|
|
739
845
|
|
|
740
|
-
// Tier-2:
|
|
846
|
+
// Tier-2: per-pixel freeze-candidate streak. FinalWrite writes (RW), buildActivePixels reads (RO).
|
|
741
847
|
freeStorageAttribute( this.renderer, this._streakAttr );
|
|
742
|
-
this._streakAttr = new StorageInstancedBufferAttribute( new Uint32Array(
|
|
848
|
+
this._streakAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
743
849
|
const streakRW = storage( this._streakAttr, 'uint' );
|
|
744
850
|
const streakRO = storage( this._streakAttr, 'uint' ).toReadOnly();
|
|
745
851
|
|
|
746
852
|
// Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
|
|
747
853
|
freeStorageAttribute( this.renderer, this._frozenMaskAttr );
|
|
748
|
-
this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array(
|
|
854
|
+
this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
749
855
|
const frozenMaskRW = storage( this._frozenMaskAttr, 'uint' );
|
|
750
856
|
const frozenMaskRO = storage( this._frozenMaskAttr, 'uint' ).toReadOnly();
|
|
751
857
|
|
|
@@ -770,7 +876,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
770
876
|
|
|
771
877
|
this._wfRenderWidth.value = w;
|
|
772
878
|
this._wfRenderHeight.value = h;
|
|
773
|
-
|
|
879
|
+
// _wfMaxRayCount is the CURRENT chunk's pixel count; set per-chunk in render(). Seed with the first chunk.
|
|
880
|
+
this._wfMaxRayCount.value = Math.min( B, this._chunkRows * w );
|
|
774
881
|
|
|
775
882
|
const prevColor = this.shaderBuilder.prevColorTexNode;
|
|
776
883
|
const prevAlbedo = this.shaderBuilder.prevAlbedoTexNode;
|
|
@@ -801,22 +908,30 @@ export class PathTracer extends PathTracerStage {
|
|
|
801
908
|
const initFn = Fn( () => {
|
|
802
909
|
|
|
803
910
|
const tid = instanceIndex;
|
|
804
|
-
|
|
805
|
-
//
|
|
911
|
+
// LOCAL-slot identity for this chunk's active list. Bounded on _wfMaxRayCount (= chunkPixels): the
|
|
912
|
+
// pool has no over-allocation margin now, so the dispatch-grid overshoot must not write past it.
|
|
913
|
+
If( tid.lessThan( this._wfMaxRayCount ), () => {
|
|
914
|
+
|
|
915
|
+
activeWriteA.element( tid ).assign( tid );
|
|
916
|
+
|
|
917
|
+
} );
|
|
918
|
+
// Seed ACTIVE/ENTERING from this chunk's ray count. CONVERGED is a per-FRAME counter (summed across
|
|
919
|
+
// chunks in FinalWrite), so zero it only on the first chunk of the frame.
|
|
806
920
|
If( tid.equal( uint( 0 ) ), () => {
|
|
807
921
|
|
|
808
922
|
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), this._wfMaxRayCount );
|
|
809
923
|
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), this._wfMaxRayCount );
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
924
|
+
If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
|
|
925
|
+
|
|
926
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
927
|
+
|
|
928
|
+
} );
|
|
814
929
|
|
|
815
930
|
} );
|
|
816
931
|
|
|
817
932
|
} );
|
|
818
933
|
this._kernelManager.register( 'initActiveIndices',
|
|
819
|
-
initFn().compute( [ Math.ceil(
|
|
934
|
+
initFn().compute( [ Math.ceil( ( this._chunkRows * w ) / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
820
935
|
);
|
|
821
936
|
|
|
822
937
|
const genParams = {
|
|
@@ -836,6 +951,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
836
951
|
anamorphicRatio: this.anamorphicRatio,
|
|
837
952
|
renderWidth: this._wfRenderWidth,
|
|
838
953
|
renderHeight: this._wfRenderHeight,
|
|
954
|
+
chunkRowBase: this._wfChunkRowBase,
|
|
955
|
+
chunkRows: this._wfChunkRows,
|
|
839
956
|
transmissiveBounces: this.transmissiveBounces,
|
|
840
957
|
transparentBackground: this.transparentBackground,
|
|
841
958
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
@@ -843,7 +960,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
843
960
|
const genFn = buildGenerateKernel( genParams );
|
|
844
961
|
this._kernelManager.register( 'generate',
|
|
845
962
|
genFn().compute(
|
|
846
|
-
[ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil(
|
|
963
|
+
[ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( this._chunkRows / GENERATE_WG_SIZE ), 1 ],
|
|
847
964
|
[ GENERATE_WG_SIZE, GENERATE_WG_SIZE, 1 ]
|
|
848
965
|
)
|
|
849
966
|
);
|
|
@@ -854,9 +971,15 @@ export class PathTracer extends PathTracerStage {
|
|
|
854
971
|
const freezeK = this.pixelFreezeStability;
|
|
855
972
|
const resetFrameFn = Fn( () => {
|
|
856
973
|
|
|
974
|
+
// ACTIVE is per-chunk (zeroed before every chunk's scatter). CONVERGED/FROZEN are per-FRAME counters
|
|
975
|
+
// summed across chunks in FinalWrite → zero them only on the first chunk of the frame.
|
|
857
976
|
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
858
|
-
|
|
859
|
-
|
|
977
|
+
If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
|
|
978
|
+
|
|
979
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
980
|
+
atomicStore( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 0 ) );
|
|
981
|
+
|
|
982
|
+
} );
|
|
860
983
|
|
|
861
984
|
} );
|
|
862
985
|
this._kernelManager.register( 'resetFrameCounters',
|
|
@@ -868,19 +991,23 @@ export class PathTracer extends PathTracerStage {
|
|
|
868
991
|
const tid = instanceIndex;
|
|
869
992
|
If( tid.lessThan( this._wfMaxRayCount ), () => {
|
|
870
993
|
|
|
994
|
+
// tid is the LOCAL slot in this chunk; p is its GLOBAL pixel (streak/frozenMask are full-res,
|
|
995
|
+
// global-indexed and persist across frames). pixelBase = chunkRowBase · renderWidth.
|
|
996
|
+
const p = uint( int( tid ).add( this._wfChunkRowBase.mul( this._wfRenderWidth ) ) ).toVar();
|
|
997
|
+
|
|
871
998
|
// frozen=1 skips the pixel this frame. Frame 0 seeds all (streak may be stale post-reset). A pixel
|
|
872
999
|
// freezes once streak>=K; with dilation ON it stays active if any 8-neighbour is still active
|
|
873
1000
|
// (streak<K), softening the boundary. Mask is read by FinalWrite too → race-free.
|
|
874
1001
|
const frozen = uint( 0 ).toVar();
|
|
875
1002
|
|
|
876
|
-
If( this.frame.greaterThan( uint( 0 ) ).and( streakRO.element(
|
|
1003
|
+
If( this.frame.greaterThan( uint( 0 ) ).and( streakRO.element( p ).greaterThanEqual( uint( freezeK ) ) ), () => {
|
|
877
1004
|
|
|
878
1005
|
frozen.assign( uint( 1 ) );
|
|
879
1006
|
|
|
880
1007
|
If( this._dilateFrozenUniform.greaterThan( int( 0 ) ), () => {
|
|
881
1008
|
|
|
882
|
-
const px = int(
|
|
883
|
-
const py = int(
|
|
1009
|
+
const px = int( p ).mod( this._wfRenderWidth );
|
|
1010
|
+
const py = int( p ).div( this._wfRenderWidth );
|
|
884
1011
|
|
|
885
1012
|
for ( const [ dx, dy ] of [[ - 1, - 1 ], [ 0, - 1 ], [ 1, - 1 ], [ - 1, 0 ], [ 1, 0 ], [ - 1, 1 ], [ 0, 1 ], [ 1, 1 ]] ) {
|
|
886
1013
|
|
|
@@ -903,11 +1030,12 @@ export class PathTracer extends PathTracerStage {
|
|
|
903
1030
|
|
|
904
1031
|
} );
|
|
905
1032
|
|
|
906
|
-
frozenMaskRW.element(
|
|
1033
|
+
frozenMaskRW.element( p ).assign( frozen );
|
|
907
1034
|
|
|
908
1035
|
If( frozen.equal( uint( 0 ) ), () => {
|
|
909
1036
|
|
|
910
1037
|
const slot = atomicAdd( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 1 ) );
|
|
1038
|
+
// Scatter the LOCAL slot (path state + list are local); generateList maps it back to global p.
|
|
911
1039
|
activeWriteA.element( slot ).assign( tid );
|
|
912
1040
|
|
|
913
1041
|
} );
|
|
@@ -916,7 +1044,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
916
1044
|
|
|
917
1045
|
} );
|
|
918
1046
|
this._kernelManager.register( 'buildActivePixels',
|
|
919
|
-
buildActiveFn().compute( [ Math.ceil(
|
|
1047
|
+
buildActiveFn().compute( [ Math.ceil( ( this._chunkRows * w ) / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
920
1048
|
);
|
|
921
1049
|
|
|
922
1050
|
const seedEnterFn = Fn( () => {
|
|
@@ -1068,6 +1196,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1068
1196
|
numSpotLights: this.numSpotLights,
|
|
1069
1197
|
maxBounceCount: this.maxBounces,
|
|
1070
1198
|
maxSubsurfaceSteps: this.maxSubsurfaceSteps,
|
|
1199
|
+
maxTransparentBounces: this.maxTransparentBounces,
|
|
1071
1200
|
transparentBackground: this.transparentBackground,
|
|
1072
1201
|
backgroundIntensity: this.backgroundIntensity,
|
|
1073
1202
|
backgroundColor: this.backgroundColor,
|
|
@@ -1090,6 +1219,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1090
1219
|
reverseMapVec4Offset: this.reverseMapVec4Offset,
|
|
1091
1220
|
currentBounce: this._wfCurrentBounce,
|
|
1092
1221
|
maxRayCount: this._wfMaxRayCount,
|
|
1222
|
+
chunkRowBase: this._wfChunkRowBase,
|
|
1093
1223
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
1094
1224
|
} );
|
|
1095
1225
|
this._kernelManager.register( 'shade',
|
|
@@ -1183,7 +1313,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
1183
1313
|
m2BufferRW: m2RW,
|
|
1184
1314
|
useAdaptiveSampling: this.useAdaptiveSampling,
|
|
1185
1315
|
noiseThreshold: this.noiseThreshold,
|
|
1186
|
-
darkNoiseFloor: this.darkNoiseFloor,
|
|
1187
1316
|
adaptiveMinSamples: this.adaptiveMinSamples,
|
|
1188
1317
|
// Tier-2 freeze (stamp + pass-through)
|
|
1189
1318
|
usePixelFreeze: this.usePixelFreeze,
|
|
@@ -1191,11 +1320,13 @@ export class PathTracer extends PathTracerStage {
|
|
|
1191
1320
|
pixelFreezeStability: this.pixelFreezeStability,
|
|
1192
1321
|
streakBufferRW: streakRW,
|
|
1193
1322
|
frozenMaskRO, // dilated frozen mask (read-only; matches the active-list decision)
|
|
1323
|
+
chunkRowBase: this._wfChunkRowBase,
|
|
1324
|
+
chunkRows: this._wfChunkRows,
|
|
1194
1325
|
} );
|
|
1195
1326
|
this._kernelManager.register( 'finalWrite',
|
|
1196
1327
|
// Per-pixel (w×h) — kernel averages the S sample-slots internally.
|
|
1197
1328
|
fwFn().compute(
|
|
1198
|
-
[ Math.ceil( w / FINALWRITE_WG_SIZE ), Math.ceil(
|
|
1329
|
+
[ Math.ceil( w / FINALWRITE_WG_SIZE ), Math.ceil( this._chunkRows / FINALWRITE_WG_SIZE ), 1 ],
|
|
1199
1330
|
[ FINALWRITE_WG_SIZE, FINALWRITE_WG_SIZE, 1 ]
|
|
1200
1331
|
)
|
|
1201
1332
|
);
|
|
@@ -1239,23 +1370,25 @@ export class PathTracer extends PathTracerStage {
|
|
|
1239
1370
|
);
|
|
1240
1371
|
|
|
1241
1372
|
this._wavefrontReady = true;
|
|
1242
|
-
|
|
1373
|
+
|
|
1374
|
+
const bufferBytes = ( this._packedBuffers?.totalBytes ?? 0 ) + ( this._queueManager?.totalBytes ?? 0 );
|
|
1375
|
+
|
|
1376
|
+
log.info( fmt.list( [
|
|
1377
|
+
fmt.px( w, h ),
|
|
1378
|
+
`${fmt.mb( bufferBytes )} wavefront buffers`,
|
|
1379
|
+
`budget ${fmt.n( B )} paths`,
|
|
1380
|
+
this._numChunks > 1 ? `${this._numChunks} chunks of ≤${this._chunkRows} rows` : null,
|
|
1381
|
+
] ) );
|
|
1243
1382
|
|
|
1244
1383
|
}
|
|
1245
1384
|
|
|
1385
|
+
// Debug viz is a single full-frame pass (no chunking, no ray pool). generate/finalWrite are sized
|
|
1386
|
+
// per row-band chunk inside render()'s chunk loop, not here.
|
|
1246
1387
|
_setWfDispatch() {
|
|
1247
1388
|
|
|
1248
1389
|
const w = this._wfRenderWidth.value;
|
|
1249
1390
|
const h = this._wfRenderHeight.value;
|
|
1250
1391
|
|
|
1251
|
-
this._kernelManager.setDispatchCount( 'generate', [
|
|
1252
|
-
Math.ceil( w / GENERATE_WG_SIZE ),
|
|
1253
|
-
Math.ceil( h / GENERATE_WG_SIZE ), 1
|
|
1254
|
-
] );
|
|
1255
|
-
this._kernelManager.setDispatchCount( 'finalWrite', [
|
|
1256
|
-
Math.ceil( w / FINALWRITE_WG_SIZE ),
|
|
1257
|
-
Math.ceil( h / FINALWRITE_WG_SIZE ), 1
|
|
1258
|
-
] );
|
|
1259
1392
|
this._kernelManager.setDispatchCount( 'debug', [
|
|
1260
1393
|
Math.ceil( w / DEBUG_WG_SIZE ),
|
|
1261
1394
|
Math.ceil( h / DEBUG_WG_SIZE ), 1
|
|
@@ -1263,6 +1396,28 @@ export class PathTracer extends PathTracerStage {
|
|
|
1263
1396
|
|
|
1264
1397
|
}
|
|
1265
1398
|
|
|
1399
|
+
// Program the per-chunk dispatch grids + chunk uniforms for one row band, then return its pixel count.
|
|
1400
|
+
// rowBase/rows are the band's GLOBAL first row + row count; chunkIndex 0 ⇒ first chunk (frame-scoped
|
|
1401
|
+
// counter reset). Generate/FinalWrite run 2D over (w × rows); initActiveIndices/buildActivePixels 1D.
|
|
1402
|
+
_setChunk( rowBase, rows, chunkIndex ) {
|
|
1403
|
+
|
|
1404
|
+
const w = this._wfRenderWidth.value;
|
|
1405
|
+
const chunkPixels = rows * w;
|
|
1406
|
+
this._wfChunkRowBase.value = rowBase;
|
|
1407
|
+
this._wfChunkRows.value = rows;
|
|
1408
|
+
this._wfIsFirstChunk.value = chunkIndex === 0 ? 1 : 0;
|
|
1409
|
+
this._wfMaxRayCount.value = chunkPixels;
|
|
1410
|
+
|
|
1411
|
+
const km = this._kernelManager;
|
|
1412
|
+
km.setDispatchCount( 'generate', [ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( rows / GENERATE_WG_SIZE ), 1 ] );
|
|
1413
|
+
km.setDispatchCount( 'finalWrite', [ Math.ceil( w / FINALWRITE_WG_SIZE ), Math.ceil( rows / FINALWRITE_WG_SIZE ), 1 ] );
|
|
1414
|
+
km.setDispatchCount( 'initActiveIndices', [ Math.ceil( chunkPixels / 256 ), 1, 1 ] );
|
|
1415
|
+
km.setDispatchCount( 'buildActivePixels', [ Math.ceil( chunkPixels / 256 ), 1, 1 ] );
|
|
1416
|
+
|
|
1417
|
+
return chunkPixels;
|
|
1418
|
+
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1266
1421
|
dispose() {
|
|
1267
1422
|
|
|
1268
1423
|
super.dispose();
|