UNPKG

three

Version:

JavaScript 3D library

272 lines (204 loc) 8.39 kB
import { StorageBufferAttribute, DynamicDrawUsage } from 'three/webgpu'; import { Fn, Loop, atomicAdd, atomicLoad, atomicStore, instanceIndex, storage, uint } from 'three/tsl'; /** * A reusable GPU counting sort. * * This computes a stable-ish permutation of the integers `[0, count)` that orders them by an * arbitrary, user supplied `uint` key ("bin") in the range `[0, binCount)`. It is a good fit for * approximate ordering of large element counts (hundreds of thousands to millions) where an exact * comparison sort such as a bitonic sort (see {@link BitonicSort}) would be too slow: a counting * sort only requires a fixed number of passes (reset, histogram, prefix sum, scatter) regardless of * `count`, at the cost of only being accurate to the resolution of `binCount` - elements that land * in the same bin end up in an unspecified relative order. * * This class does not compute the sort key itself. Instead, a TSL function is supplied via * {@link CountingSort#setBinNode} that maps the current `instanceIndex` to a bin, and an equivalent * plain JavaScript function can be supplied to {@link CountingSort#computeCPU} for platforms without * compute shader support (e.g. the WebGL backend of {@link WebGPURenderer}). * * ```js * const sort = new CountingSort( count, { binCount: 4096 } ); * sort.setBinNode( () => { * * // return a `Node<uint>` bin index for `instanceIndex`, e.g. derived from a depth value. * * } ); * * sort.compute( renderer ); * * // `sort.orderRead` now holds a storage buffer of `count` indices, ordered by bin. * ``` * * @three_import import { CountingSort } from 'three/addons/gpgpu/CountingSort.js'; */ class CountingSort { /** * Constructs a new counting sort. * * @param {number} count - The number of elements to sort. * @param {Object} [options={}] - Options that modify the counting sort. * @param {number} [options.binCount=4096] - The number of bins/buckets the sort key is quantized into. Larger values improve sort accuracy at the cost of a longer (but still single-pass) prefix sum. * @param {number} [options.workgroupSize=256] - The workgroup size of the compute shaders executed during the sort. */ constructor( count, { binCount = 4096, workgroupSize = 256 } = {} ) { /** * The number of elements to sort. * * @type {number} */ this.count = count; /** * The number of bins/buckets the sort key is quantized into. * * @type {number} */ this.binCount = binCount; /** * The workgroup size of the compute shaders executed during the sort. * * @type {number} */ this.workgroupSize = workgroupSize; const orderData = new Uint32Array( count ); for ( let i = 0; i < count; i ++ ) orderData[ i ] = i; /** * The buffer attribute holding the sorted order (a permutation of `[0, count)`). This is * also the attribute that is kept up to date by {@link CountingSort#computeCPU}. * * @type {StorageBufferAttribute} */ this.orderAttribute = new StorageBufferAttribute( orderData, 1, Uint32Array ); const binAttribute = new StorageBufferAttribute( new Uint32Array( count ), 1, Uint32Array ); const histogramAttribute = new StorageBufferAttribute( new Uint32Array( binCount ), 1, Uint32Array ); const offsetAttribute = new StorageBufferAttribute( new Uint32Array( binCount ), 1, Uint32Array ); /** * A read-only storage node for the sorted order buffer. * * @type {StorageBufferNode} */ this.orderRead = storage( this.orderAttribute, 'uint', count ).toReadOnly(); /** * A writable storage node for the sorted order buffer. * * @type {StorageBufferNode} */ this.orderWrite = storage( this.orderAttribute, 'uint', count ); /** * A read-only storage node holding each element's bin, computed during the histogram pass. * * @type {StorageBufferNode} */ this.binRead = storage( binAttribute, 'uint', count ).toReadOnly(); /** * A writable storage node holding each element's bin. * * @type {StorageBufferNode} */ this.binWrite = storage( binAttribute, 'uint', count ); /** * An atomic storage node used to accumulate the per-bin histogram. * * @type {StorageBufferNode} */ this.histogramAtomic = storage( histogramAttribute, 'uint', binCount ).toAtomic(); /** * An atomic storage node used both for the exclusive prefix sum of the histogram and, during * the scatter pass, as a per-bin write cursor. * * @type {StorageBufferNode} */ this.offsetAtomic = storage( offsetAttribute, 'uint', binCount ).toAtomic(); this._webGLBuffersEnabled = false; this._cpuBins = new Uint32Array( count ); this._cpuCounts = new Uint32Array( binCount ); this._cpuOffsets = new Uint32Array( binCount ); this._resetNode = null; this._histogramNode = null; this._prefixNode = null; this._scatterNode = null; } /** * Sets the TSL function used to compute the bin of the element currently referenced by * `instanceIndex`, and (re)builds the compute nodes used by {@link CountingSort#compute}. * * @param {Function} binNode - A parameterless function returning a `Node<uint>` in `[0, binCount)`. */ setBinNode( binNode ) { const { binCount, workgroupSize, count } = this; this._resetNode = Fn( () => { atomicStore( this.histogramAtomic.element( instanceIndex ), uint( 0 ) ); atomicStore( this.offsetAtomic.element( instanceIndex ), uint( 0 ) ); } )().compute( binCount, [ workgroupSize ] ).setName( 'CountingSortReset' ); this._histogramNode = Fn( () => { const bin = binNode().toVar( 'bin' ); this.binWrite.element( instanceIndex ).assign( bin ); atomicAdd( this.histogramAtomic.element( bin ), uint( 1 ) ); } )().compute( count, [ workgroupSize ] ).setName( 'CountingSortHistogram' ); this._prefixNode = Fn( () => { const sum = uint( 0 ).toVar( 'sum' ); Loop( { start: 0, end: binCount, type: 'uint', name: 'bin', condition: '<' }, ( { bin } ) => { const binCountValue = atomicLoad( this.histogramAtomic.element( bin ) ).toVar( 'count' ); atomicStore( this.offsetAtomic.element( bin ), sum ); sum.addAssign( binCountValue ); } ); } )().compute( 1 ).setName( 'CountingSortPrefix' ); this._scatterNode = Fn( () => { const bin = this.binRead.element( instanceIndex ).toVar( 'bin' ); const targetIndex = atomicAdd( this.offsetAtomic.element( bin ), uint( 1 ) ).toVar( 'targetIndex' ); this.orderWrite.element( targetIndex ).assign( instanceIndex ); } )().compute( count, [ workgroupSize ] ).setName( 'CountingSortScatter' ); } /** * Executes a complete counting sort on the GPU, updating {@link CountingSort#orderRead}. * * @param {Renderer} renderer - The current scene's renderer. */ compute( renderer ) { renderer.compute( this._resetNode ); renderer.compute( this._histogramNode ); renderer.compute( this._prefixNode ); renderer.compute( this._scatterNode ); } /** * Executes a complete counting sort on the CPU, updating {@link CountingSort#orderAttribute}. * Intended as a fallback for backends without compute shader support. * * @param {Function} binFn - A function taking an element index and returning its bin (a plain number in `[0, binCount)`). */ computeCPU( binFn ) { const { count, binCount } = this; const order = this.orderAttribute.array; const bins = this._cpuBins; const counts = this._cpuCounts; const offsets = this._cpuOffsets; counts.fill( 0 ); for ( let i = 0; i < count; i ++ ) { const bin = binFn( i ); bins[ i ] = bin; counts[ bin ] ++; } let sum = 0; for ( let i = 0; i < binCount; i ++ ) { offsets[ i ] = sum; sum += counts[ i ]; } for ( let i = 0; i < count; i ++ ) { order[ offsets[ bins[ i ] ] ++ ] = i; } this.orderAttribute.needsUpdate = true; if ( this.orderAttribute.pbo !== undefined ) { this.orderAttribute.pbo.needsUpdate = true; } } /** * Enables the WebGL-specific storage buffer path (PBO + dynamic draw usage) for the order buffer. * Only needed when {@link CountingSort#computeCPU} is used with the WebGL backend of {@link WebGPURenderer}. */ enableWebGLBuffers() { if ( this._webGLBuffersEnabled === true ) return; this.orderAttribute.setUsage( DynamicDrawUsage ); this.orderRead.setPBO( true ); this._webGLBuffersEnabled = true; } } export { CountingSort };