rayzee 7.5.0 → 7.5.1
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 +1491 -1353
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +56 -55
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +4 -0
- package/src/Processor/KernelManager.js +1 -2
- package/src/Processor/QueueManager.js +37 -1
- package/src/Stages/PathTracer.js +75 -2
- package/src/TSL/SortGlobalKernels.js +135 -0
- package/src/TSL/patches.js +90 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -88,6 +88,10 @@ export const ENGINE_DEFAULTS = {
|
|
|
88
88
|
performanceModeAdaptive: 'medium',
|
|
89
89
|
|
|
90
90
|
fireflyThreshold: 3.0,
|
|
91
|
+
// Wavefront material-coherence sort: global counting-sort of entering rays by material before
|
|
92
|
+
// Shade (material-pure workgroups), under dynamic dispatch. Measured −8% at 1024²/8b. Gated on
|
|
93
|
+
// material count > 8; the histogram bin count is sized per-scene to the material count.
|
|
94
|
+
wavefrontSortMaterials: true,
|
|
91
95
|
renderLimitMode: 'frames',
|
|
92
96
|
renderTimeLimit: 30,
|
|
93
97
|
renderMode: 0,
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
const WORKGROUP_SIZES = {
|
|
13
13
|
generate: [ 16, 16, 1 ], // 2D screen-space
|
|
14
14
|
extend: [ 256, 1, 1 ], // 1D ray-parallel
|
|
15
|
-
|
|
16
|
-
shade: [ 256, 1, 1 ], // 1D ray-parallel (sorted)
|
|
15
|
+
shade: [ 256, 1, 1 ], // 1D ray-parallel (material-sorted when sort enabled)
|
|
17
16
|
connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
18
17
|
accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
19
18
|
compact: [ 256, 1, 1 ], // 1D ray-parallel
|
|
@@ -40,6 +40,9 @@ export class QueueManager {
|
|
|
40
40
|
// A/B alternate: one read by current bounce, other written by compaction
|
|
41
41
|
this.activeIndices = null;
|
|
42
42
|
this.activeIndicesRO = null;
|
|
43
|
+
this.sortedIndices = null;
|
|
44
|
+
this.sortedIndicesRO = null;
|
|
45
|
+
this.sortGlobalHistogram = null;
|
|
43
46
|
this.pingPong = 0; // 0 = read A / write B, 1 = read B / write A
|
|
44
47
|
|
|
45
48
|
if ( maxRays > 0 ) {
|
|
@@ -83,11 +86,23 @@ export class QueueManager {
|
|
|
83
86
|
b: storage( attrB, 'uint' ).toReadOnly(),
|
|
84
87
|
};
|
|
85
88
|
|
|
89
|
+
// Material-sort output: a material-reordered permutation of the active index
|
|
90
|
+
// list, written by the global material sort and read by Shade in place of activeIndices.
|
|
91
|
+
const sortAttr = new StorageInstancedBufferAttribute( new Uint32Array( capacity ), 1 );
|
|
92
|
+
this._sortAttr = sortAttr;
|
|
93
|
+
this.sortedIndices = storage( sortAttr, 'uint' );
|
|
94
|
+
this.sortedIndicesRO = storage( sortAttr, 'uint' ).toReadOnly();
|
|
95
|
+
|
|
96
|
+
// Global material-sort histogram, sized to the bin cap (SORT_GLOBAL_MAX_BINS=256); kernels use
|
|
97
|
+
// only the first `bins` entries (= per-scene material count). 256 × 4B = 1KB.
|
|
98
|
+
this._sortGlobalHistAttr = new StorageInstancedBufferAttribute( new Uint32Array( 256 ), 1 );
|
|
99
|
+
this.sortGlobalHistogram = storage( this._sortGlobalHistAttr, 'uint' ).toAtomic();
|
|
100
|
+
|
|
86
101
|
this.pingPong = 0;
|
|
87
102
|
|
|
88
103
|
const totalBytes = (
|
|
89
104
|
COUNTER.COUNT * 4 +
|
|
90
|
-
capacity * 4 *
|
|
105
|
+
capacity * 4 * 3
|
|
91
106
|
);
|
|
92
107
|
|
|
93
108
|
console.log(
|
|
@@ -131,6 +146,24 @@ export class QueueManager {
|
|
|
131
146
|
|
|
132
147
|
}
|
|
133
148
|
|
|
149
|
+
getSortedRW() {
|
|
150
|
+
|
|
151
|
+
return this.sortedIndices;
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getSortedRO() {
|
|
156
|
+
|
|
157
|
+
return this.sortedIndicesRO;
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getSortGlobalHistogram() {
|
|
162
|
+
|
|
163
|
+
return this.sortGlobalHistogram;
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
134
167
|
// raw attribute for `renderer.getArrayBufferAsync(...)` readback
|
|
135
168
|
getCountersAttribute() {
|
|
136
169
|
|
|
@@ -167,6 +200,9 @@ export class QueueManager {
|
|
|
167
200
|
this.counters = null;
|
|
168
201
|
this.activeIndices = null;
|
|
169
202
|
this.activeIndicesRO = null;
|
|
203
|
+
this.sortedIndices = null;
|
|
204
|
+
this.sortedIndicesRO = null;
|
|
205
|
+
this.sortGlobalHistogram = null;
|
|
170
206
|
this.capacity = 0;
|
|
171
207
|
|
|
172
208
|
}
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -17,6 +17,11 @@ import { buildShadeKernel, SHADE_WG_SIZE } from '../TSL/ShadeKernel.js';
|
|
|
17
17
|
import { buildCompactKernel, buildCompactSubgroupKernel, COMPACT_WG_SIZE } from '../TSL/CompactKernel.js';
|
|
18
18
|
import { buildFinalWriteKernel, FINALWRITE_WG_SIZE } from '../TSL/FinalWriteKernel.js';
|
|
19
19
|
import { buildDebugKernel, DEBUG_WG_SIZE } from '../TSL/DebugKernel.js';
|
|
20
|
+
import {
|
|
21
|
+
buildResetGlobalHistKernel, buildGlobalHistKernel, buildGlobalPrefixKernel, buildGlobalScatterKernel,
|
|
22
|
+
SORT_GLOBAL_WG_SIZE, SORT_GLOBAL_MAX_BINS,
|
|
23
|
+
} from '../TSL/SortGlobalKernels.js';
|
|
24
|
+
import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
|
|
20
25
|
import {
|
|
21
26
|
Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return,
|
|
22
27
|
} from 'three/tsl';
|
|
@@ -43,6 +48,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
43
48
|
// CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
|
|
44
49
|
this._useDynamicDispatch = true;
|
|
45
50
|
|
|
51
|
+
// Global material-coherence sort: set per-build from ENGINE_DEFAULTS + material count (>8).
|
|
52
|
+
// Reorders entering rays into material-pure workgroups before Shade; runs under dynamic dispatch
|
|
53
|
+
// (compact reads the unsorted active list, so the survivor set is unchanged). Measured −8% at 1024²/8b.
|
|
54
|
+
this._sortMaterials = false;
|
|
55
|
+
|
|
46
56
|
// Flag-gated off: perf-neutral vs atomic-append and adds a 'subgroups' feature dependency.
|
|
47
57
|
this._useSubgroupCompact = false;
|
|
48
58
|
|
|
@@ -90,7 +100,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
90
100
|
if ( ! qm ) return null;
|
|
91
101
|
return [
|
|
92
102
|
qm._countersAttr, qm._bounceCountsAttr,
|
|
93
|
-
qm._attrA, qm._attrB,
|
|
103
|
+
qm._attrA, qm._attrB, qm._sortAttr,
|
|
94
104
|
];
|
|
95
105
|
|
|
96
106
|
} );
|
|
@@ -250,6 +260,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
250
260
|
this._wfCurrentBounce.value = bounce;
|
|
251
261
|
|
|
252
262
|
// 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).
|
|
263
|
+
// Material sort is compatible: shade reads sortedIndices while compact still reads the UNSORTED active list (getActiveReadRO), so the survivor set is unchanged.
|
|
253
264
|
const useFunctionalCompaction = this._useDynamicDispatch;
|
|
254
265
|
if ( useFunctionalCompaction ) {
|
|
255
266
|
|
|
@@ -281,6 +292,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
281
292
|
const wg = [ Math.ceil( sized / 256 ), 1, 1 ];
|
|
282
293
|
km.setDispatchCount( 'extend', wg );
|
|
283
294
|
km.setDispatchCount( 'shade', wg );
|
|
295
|
+
km.setDispatchCount( 'globalHist', wg );
|
|
296
|
+
km.setDispatchCount( 'globalScatter', wg );
|
|
284
297
|
km.setDispatchCount( 'compact', wg );
|
|
285
298
|
km.setDispatchCount( 'compactCopyback', wg );
|
|
286
299
|
|
|
@@ -291,11 +304,23 @@ export class PathTracer extends PathTracerStage {
|
|
|
291
304
|
km.setDispatchCount( 'extend', full );
|
|
292
305
|
km.setDispatchCount( 'shade', full );
|
|
293
306
|
km.setDispatchCount( 'compact', full );
|
|
307
|
+
km.setDispatchCount( 'globalHist', full );
|
|
308
|
+
km.setDispatchCount( 'globalScatter', full );
|
|
294
309
|
|
|
295
310
|
}
|
|
296
311
|
|
|
297
312
|
// Extend/Shade kept separate (not fused): a fused kernel's register pressure drops occupancy more than fusion saves.
|
|
298
313
|
km.dispatch( 'extend' );
|
|
314
|
+
if ( this._sortMaterials ) {
|
|
315
|
+
|
|
316
|
+
// Global material counting sort (material-pure workgroups): reset, histogram, prefix-sum, scatter.
|
|
317
|
+
km.dispatch( 'resetGlobalHist' );
|
|
318
|
+
km.dispatch( 'globalHist' );
|
|
319
|
+
km.dispatch( 'globalPrefix' );
|
|
320
|
+
km.dispatch( 'globalScatter' );
|
|
321
|
+
|
|
322
|
+
}
|
|
323
|
+
|
|
299
324
|
km.dispatch( 'shade' );
|
|
300
325
|
|
|
301
326
|
km.dispatch( 'resetActiveCounter' );
|
|
@@ -660,6 +685,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
660
685
|
displacementMaps: freshDisplacementMaps,
|
|
661
686
|
};
|
|
662
687
|
|
|
688
|
+
// Material-coherence sort gate (experiment): only worthwhile above a few materials.
|
|
689
|
+
this._sortMaterials = ( ENGINE_DEFAULTS.wavefrontSortMaterials ?? false )
|
|
690
|
+
&& ( this.materialData?.materialCount ?? 0 ) > 8;
|
|
691
|
+
|
|
663
692
|
const extFn = buildExtendKernel( {
|
|
664
693
|
bvhBuffer: freshBvh,
|
|
665
694
|
triangleBuffer: freshTri,
|
|
@@ -677,6 +706,50 @@ export class PathTracer extends PathTracerStage {
|
|
|
677
706
|
)
|
|
678
707
|
);
|
|
679
708
|
|
|
709
|
+
// Material-coherence sort: reorder the entering-ray indices by material between
|
|
710
|
+
// Extend and Shade. Histogram is workgroup-shared (patches.js §4); Shade reads the output.
|
|
711
|
+
if ( this._sortMaterials ) {
|
|
712
|
+
|
|
713
|
+
const sgHist = qm.getSortGlobalHistogram();
|
|
714
|
+
const sortBins = Math.min( SORT_GLOBAL_MAX_BINS, this.materialData?.materialCount ?? SORT_GLOBAL_MAX_BINS );
|
|
715
|
+
this._kernelManager.register( 'resetGlobalHist',
|
|
716
|
+
buildResetGlobalHistKernel( { sortGlobalHistogram: sgHist, bins: sortBins } )().compute(
|
|
717
|
+
[ 1, 1, 1 ], [ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
this._kernelManager.register( 'globalHist',
|
|
721
|
+
buildGlobalHistKernel( {
|
|
722
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
723
|
+
activeIndicesReadRO: qm.getActiveReadRO(),
|
|
724
|
+
sortGlobalHistogram: sgHist,
|
|
725
|
+
counters,
|
|
726
|
+
bins: sortBins,
|
|
727
|
+
} )().compute(
|
|
728
|
+
[ Math.ceil( maxRays / SORT_GLOBAL_WG_SIZE ), 1, 1 ],
|
|
729
|
+
[ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
730
|
+
)
|
|
731
|
+
);
|
|
732
|
+
this._kernelManager.register( 'globalPrefix',
|
|
733
|
+
buildGlobalPrefixKernel( { sortGlobalHistogram: sgHist, bins: sortBins } )().compute(
|
|
734
|
+
[ 1, 1, 1 ], [ 1, 1, 1 ]
|
|
735
|
+
)
|
|
736
|
+
);
|
|
737
|
+
this._kernelManager.register( 'globalScatter',
|
|
738
|
+
buildGlobalScatterKernel( {
|
|
739
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
740
|
+
activeIndicesReadRO: qm.getActiveReadRO(),
|
|
741
|
+
sortedIndicesRW: qm.getSortedRW(),
|
|
742
|
+
sortGlobalHistogram: sgHist,
|
|
743
|
+
counters,
|
|
744
|
+
bins: sortBins,
|
|
745
|
+
} )().compute(
|
|
746
|
+
[ Math.ceil( maxRays / SORT_GLOBAL_WG_SIZE ), 1, 1 ],
|
|
747
|
+
[ SORT_GLOBAL_WG_SIZE, 1, 1 ]
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
}
|
|
752
|
+
|
|
680
753
|
const shadeFn = buildShadeKernel( {
|
|
681
754
|
gBufferRW,
|
|
682
755
|
envCompensationDelta: this.envCompensationDelta,
|
|
@@ -689,7 +762,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
689
762
|
rngBufferRW: pb.rngBuffer.rw,
|
|
690
763
|
hitBufferRO: pb.hitBuffer.ro,
|
|
691
764
|
counters,
|
|
692
|
-
activeIndicesRO: qm.getActiveReadRO(),
|
|
765
|
+
activeIndicesRO: this._sortMaterials ? qm.getSortedRO() : qm.getActiveReadRO(),
|
|
693
766
|
albedoMaps: freshAlbedoMaps,
|
|
694
767
|
normalMaps: freshNormalMaps,
|
|
695
768
|
bumpMaps: freshBumpMaps,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SortGlobalKernels.js — GLOBAL material counting sort of the entering rays.
|
|
3
|
+
*
|
|
4
|
+
* Unlike a per-workgroup sort (locally coherent only), this orders rays by material
|
|
5
|
+
* ACROSS ALL workgroups, so each material's rays occupy one contiguous region of sortedIndices.
|
|
6
|
+
* For materials with many rays that means whole material-PURE workgroups/subgroups → real shading
|
|
7
|
+
* coherence. MEASURED −8% vs no-sort at 1024²/8b (per-WG sort was neutral).
|
|
8
|
+
*
|
|
9
|
+
* Four passes (counting sort): reset → histogram → exclusive prefix sum → scatter.
|
|
10
|
+
* - histogram uses a per-workgroup local histogram (workgroup-shared atomics, patches.js §4)
|
|
11
|
+
* flushed to the global histogram once per bin per workgroup, cutting global-atomic contention
|
|
12
|
+
* from O(rays) to O(workgroups × bins).
|
|
13
|
+
* - the prefix-summed histogram doubles as the per-bin running cursor in scatter.
|
|
14
|
+
*
|
|
15
|
+
* `bins` is chosen per-scene by the caller (= material count, capped). Materials ≥ bins clamp into
|
|
16
|
+
* the last bin (graceful coherence loss). Capped at SORT_GLOBAL_MAX_BINS so a single 256-thread
|
|
17
|
+
* workgroup can zero/flush exactly one bin per thread (no per-thread loop needed).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
Fn, uint,
|
|
22
|
+
If, Loop,
|
|
23
|
+
instanceIndex, localId,
|
|
24
|
+
workgroupBarrier,
|
|
25
|
+
atomicAdd, atomicLoad, atomicStore,
|
|
26
|
+
} from 'three/tsl';
|
|
27
|
+
|
|
28
|
+
import { workgroupAtomicArray } from './patches.js';
|
|
29
|
+
import { readHitMaterialIndex } from '../Processor/PackedRayBuffer.js';
|
|
30
|
+
import { COUNTER } from '../Processor/QueueManager.js';
|
|
31
|
+
|
|
32
|
+
const WG_SIZE = 256;
|
|
33
|
+
export const SORT_GLOBAL_MAX_BINS = 256;
|
|
34
|
+
|
|
35
|
+
// Pass 1 — zero the global histogram.
|
|
36
|
+
export function buildResetGlobalHistKernel( { sortGlobalHistogram, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
37
|
+
|
|
38
|
+
return Fn( () => {
|
|
39
|
+
|
|
40
|
+
If( instanceIndex.lessThan( uint( bins ) ), () => {
|
|
41
|
+
|
|
42
|
+
atomicStore( sortGlobalHistogram.element( instanceIndex ), uint( 0 ) );
|
|
43
|
+
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pass 2 — histogram: per-workgroup local count (on-chip), flushed to global once per bin/WG.
|
|
51
|
+
export function buildGlobalHistKernel( { hitBufferRO, activeIndicesReadRO, sortGlobalHistogram, counters, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
52
|
+
|
|
53
|
+
return Fn( () => {
|
|
54
|
+
|
|
55
|
+
const tid = instanceIndex;
|
|
56
|
+
const lid = localId.x;
|
|
57
|
+
const local = workgroupAtomicArray( 'uint', bins );
|
|
58
|
+
|
|
59
|
+
If( lid.lessThan( uint( bins ) ), () => {
|
|
60
|
+
|
|
61
|
+
atomicStore( local.element( lid ), uint( 0 ) );
|
|
62
|
+
|
|
63
|
+
} );
|
|
64
|
+
workgroupBarrier();
|
|
65
|
+
|
|
66
|
+
const entering = atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) );
|
|
67
|
+
If( tid.lessThan( entering ), () => {
|
|
68
|
+
|
|
69
|
+
const rayID = activeIndicesReadRO.element( tid );
|
|
70
|
+
const bin = uint( readHitMaterialIndex( hitBufferRO, rayID ) ).clamp( uint( 0 ), uint( bins - 1 ) );
|
|
71
|
+
atomicAdd( local.element( bin ), uint( 1 ) );
|
|
72
|
+
|
|
73
|
+
} );
|
|
74
|
+
workgroupBarrier();
|
|
75
|
+
|
|
76
|
+
If( lid.lessThan( uint( bins ) ), () => {
|
|
77
|
+
|
|
78
|
+
const c = atomicLoad( local.element( lid ) );
|
|
79
|
+
If( c.greaterThan( uint( 0 ) ), () => {
|
|
80
|
+
|
|
81
|
+
atomicAdd( sortGlobalHistogram.element( lid ), c );
|
|
82
|
+
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pass 3 — exclusive prefix sum over global bins (single thread; dispatch [1,1,1]).
|
|
92
|
+
export function buildGlobalPrefixKernel( { sortGlobalHistogram, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
93
|
+
|
|
94
|
+
return Fn( () => {
|
|
95
|
+
|
|
96
|
+
If( instanceIndex.equal( uint( 0 ) ), () => {
|
|
97
|
+
|
|
98
|
+
const running = uint( 0 ).toVar();
|
|
99
|
+
Loop( { start: uint( 0 ), end: uint( bins ), type: 'uint' }, ( { i } ) => {
|
|
100
|
+
|
|
101
|
+
const slot = sortGlobalHistogram.element( i );
|
|
102
|
+
const c = atomicLoad( slot );
|
|
103
|
+
atomicStore( slot, running );
|
|
104
|
+
running.addAssign( c );
|
|
105
|
+
|
|
106
|
+
} );
|
|
107
|
+
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Pass 4 — scatter: atomicAdd on the prefix-summed histogram gives each ray its global slot
|
|
115
|
+
// within its material's contiguous region.
|
|
116
|
+
export function buildGlobalScatterKernel( { hitBufferRO, activeIndicesReadRO, sortedIndicesRW, sortGlobalHistogram, counters, bins = SORT_GLOBAL_MAX_BINS } ) {
|
|
117
|
+
|
|
118
|
+
return Fn( () => {
|
|
119
|
+
|
|
120
|
+
const tid = instanceIndex;
|
|
121
|
+
const entering = atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) );
|
|
122
|
+
If( tid.lessThan( entering ), () => {
|
|
123
|
+
|
|
124
|
+
const rayID = activeIndicesReadRO.element( tid );
|
|
125
|
+
const bin = uint( readHitMaterialIndex( hitBufferRO, rayID ) ).clamp( uint( 0 ), uint( bins - 1 ) );
|
|
126
|
+
const pos = atomicAdd( sortGlobalHistogram.element( bin ), uint( 1 ) );
|
|
127
|
+
sortedIndicesRW.element( pos ).assign( rayID );
|
|
128
|
+
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { WG_SIZE as SORT_GLOBAL_WG_SIZE };
|
package/src/TSL/patches.js
CHANGED
|
@@ -61,6 +61,10 @@ WebGPUBackend.prototype.createNodeBuilder = function ( object, renderer ) {
|
|
|
61
61
|
configurable: true,
|
|
62
62
|
} );
|
|
63
63
|
|
|
64
|
+
// Install the workgroup-atomic-array codegen patch (section 4) lazily off the
|
|
65
|
+
// first builder, so we don't need to import WGSLNodeBuilder directly.
|
|
66
|
+
_installScopedArrayAtomicPatch( builder );
|
|
67
|
+
|
|
64
68
|
return builder;
|
|
65
69
|
|
|
66
70
|
};
|
|
@@ -220,3 +224,89 @@ export function struct( members, name = null ) {
|
|
|
220
224
|
return wrappedFactory;
|
|
221
225
|
|
|
222
226
|
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// 4. Workgroup-scoped atomic arrays — var<workgroup> array<atomic<T>, N>
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// TSL's `workgroupArray(type, count)` emits `var<workgroup> name: array<T, N>`
|
|
232
|
+
// (`WGSLNodeBuilder.getScopedArrays`, r185 ~line 1773) with a NON-atomic element
|
|
233
|
+
// type, so `atomicAdd(arr.element(i), v)` fails WGSL validation. AtomicFunctionNode
|
|
234
|
+
// is itself address-space-agnostic — it emits `atomicAdd(&name[i], v)`, valid for
|
|
235
|
+
// BOTH storage and workgroup pointers — so the ONLY missing piece is the
|
|
236
|
+
// declaration. This patch adds `workgroupAtomicArray()` (a `workgroupArray` tagged
|
|
237
|
+
// atomic) and overrides `getScopedArrays` to wrap the element type in `atomic<…>`
|
|
238
|
+
// for tagged arrays, enabling true on-chip workgroup-shared atomics (e.g. fast
|
|
239
|
+
// per-workgroup histograms) instead of the slow global-storage-atomic fallback.
|
|
240
|
+
// Access tagged arrays ONLY via atomic ops (atomicAdd/atomicLoad/atomicStore).
|
|
241
|
+
|
|
242
|
+
import { workgroupArray } from 'three/tsl';
|
|
243
|
+
|
|
244
|
+
const _WorkgroupInfoNode = workgroupArray( 'uint', 1 ).constructor;
|
|
245
|
+
|
|
246
|
+
// Tag the node so the patched getScopedArrays emits an atomic element type. The
|
|
247
|
+
// array's WGSL name is only known after generate() runs, so we mark the builder's
|
|
248
|
+
// scopedArrays entry there (idempotent across analyze/generate passes).
|
|
249
|
+
const _origWorkgroupGenerate = _WorkgroupInfoNode.prototype.generate;
|
|
250
|
+
|
|
251
|
+
_WorkgroupInfoNode.prototype.generate = function ( builder ) {
|
|
252
|
+
|
|
253
|
+
const name = _origWorkgroupGenerate.call( this, builder );
|
|
254
|
+
if ( this.isAtomicArray === true ) {
|
|
255
|
+
|
|
256
|
+
const entry = builder.scopedArrays && builder.scopedArrays.get( name );
|
|
257
|
+
if ( entry ) entry.isAtomic = true;
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return name;
|
|
262
|
+
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Like `workgroupArray(type, count)` but declares the workgroup buffer with an
|
|
267
|
+
* atomic element type: `var<workgroup> name: array<atomic<type>, count>`.
|
|
268
|
+
* Elements MUST be accessed only via atomic ops (atomicAdd/atomicLoad/atomicStore).
|
|
269
|
+
*
|
|
270
|
+
* @param {string} type - Element type (e.g. 'uint').
|
|
271
|
+
* @param {number} count - Number of elements.
|
|
272
|
+
* @returns {WorkgroupInfoNode} The tagged workgroup array node.
|
|
273
|
+
*/
|
|
274
|
+
export function workgroupAtomicArray( type, count ) {
|
|
275
|
+
|
|
276
|
+
const node = workgroupArray( type, count );
|
|
277
|
+
node.isAtomicArray = true;
|
|
278
|
+
return node;
|
|
279
|
+
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let _scopedArraysPatched = false;
|
|
283
|
+
|
|
284
|
+
// Override getScopedArrays on the builder's prototype (compute-stage WGSL
|
|
285
|
+
// assembly) to emit `atomic<T>` element types for tagged workgroup arrays.
|
|
286
|
+
// Installed once, lazily, off the first node builder created.
|
|
287
|
+
function _installScopedArrayAtomicPatch( builder ) {
|
|
288
|
+
|
|
289
|
+
if ( _scopedArraysPatched ) return;
|
|
290
|
+
const proto = Object.getPrototypeOf( builder );
|
|
291
|
+
if ( ! proto || typeof proto.getScopedArrays !== 'function' ) return;
|
|
292
|
+
|
|
293
|
+
proto.getScopedArrays = function ( shaderStage ) {
|
|
294
|
+
|
|
295
|
+
if ( shaderStage !== 'compute' ) return;
|
|
296
|
+
|
|
297
|
+
const snippets = [];
|
|
298
|
+
for ( const { name, scope, bufferType, bufferCount, isAtomic } of this.scopedArrays.values() ) {
|
|
299
|
+
|
|
300
|
+
const type = this.getType( bufferType );
|
|
301
|
+
const elementType = ( isAtomic === true ) ? `atomic< ${ type } >` : type;
|
|
302
|
+
snippets.push( `var<${ scope }> ${ name }: array< ${ elementType }, ${ bufferCount } >;` );
|
|
303
|
+
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return snippets.join( '\n' );
|
|
307
|
+
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
_scopedArraysPatched = true;
|
|
311
|
+
|
|
312
|
+
}
|