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