UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

336 lines (335 loc) 15.4 kB
import { Vec2 } from "../../../core/math/vec2.js"; import { Compute } from "../../../platform/graphics/compute.js"; import { Shader } from "../../../platform/graphics/shader.js"; import { StorageBuffer } from "../../../platform/graphics/storage-buffer.js"; import { BindGroupFormat, BindStorageBufferFormat, BindUniformBufferFormat } from "../../../platform/graphics/bind-group-format.js"; import { UniformBufferFormat, UniformFormat } from "../../../platform/graphics/uniform-buffer-format.js"; import { BUFFERUSAGE_COPY_DST, SHADERLANGUAGE_WGSL, SHADERSTAGE_COMPUTE, UNIFORMTYPE_UINT } from "../../../platform/graphics/constants.js"; import { onesweepGlobalHistSource } from "../../shader-lib/wgsl/chunks/radix-sort/onesweep-global-hist.js"; import { onesweepScanSource } from "../../shader-lib/wgsl/chunks/radix-sort/onesweep-scan.js"; import { onesweepBinningSource } from "../../shader-lib/wgsl/chunks/radix-sort/onesweep-binning.js"; import { ComputeRadixSortBase } from "./compute-radix-sort-base.js"; const D_DIM = 256; const KEYS_PER_THREAD = 15; const PART_SIZE = D_DIM * KEYS_PER_THREAD; const G_HIST_DIM = 128; const G_HIST_PART_SIZE = 32768; const RADIX = 256; const MAX_PASSES = 4; class ComputeRadixSortOneSweep extends ComputeRadixSortBase { _threadBlocks = 0; _allocatedThreadBlocks = 0; _globalHist = null; _passHist = null; _index = null; _binningDispatchSize = new Vec2(1, 1); _globalHistDispatchSize = new Vec2(1, 1); _globalHistBindGroupFormat = null; _scanBindGroupFormat = null; _binningBindGroupFormat = null; _globalHistUniformFormat = null; _scanUniformFormat = null; _binningUniformFormat = null; _globalHistShader = null; _scanShader = null; _binningShader = null; _globalHistCompute = null; _scanCompute = null; _binningComputes = []; constructor(device, indirect = false) { super(device, indirect); const info = this._indirectInfo; info[0] = 2; info[1] = PART_SIZE; info[2] = G_HIST_PART_SIZE; this._globalHistUniformFormat = new UniformBufferFormat(device, [ new UniformFormat("numKeys", UNIFORMTYPE_UINT), new UniformFormat("threadBlocks", UNIFORMTYPE_UINT), new UniformFormat("numPasses", UNIFORMTYPE_UINT), new UniformFormat("_pad", UNIFORMTYPE_UINT) ]); this._scanUniformFormat = new UniformBufferFormat(device, [ new UniformFormat("threadBlocks", UNIFORMTYPE_UINT), new UniformFormat("_pad0", UNIFORMTYPE_UINT), new UniformFormat("_pad1", UNIFORMTYPE_UINT), new UniformFormat("_pad2", UNIFORMTYPE_UINT) ]); this._binningUniformFormat = new UniformBufferFormat(device, [ new UniformFormat("numKeys", UNIFORMTYPE_UINT), new UniformFormat("threadBlocks", UNIFORMTYPE_UINT), new UniformFormat("pass_", UNIFORMTYPE_UINT), new UniformFormat("flags", UNIFORMTYPE_UINT) ]); const minSubgroupSize = device.minSubgroupSize || device.maxSubgroupSize || 32; const maxSubgroups = Math.max(1, Math.ceil(256 / minSubgroupSize)); const suffix = indirect ? "Indirect" : ""; const histGroupEntries = [ new BindStorageBufferFormat("b_sort", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("b_globalHist", SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE) ]; const scanGroupEntries = [ new BindStorageBufferFormat("b_globalHist", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("b_passHist", SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE) ]; const binGroupEntries = [ new BindStorageBufferFormat("inputKeys", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("outputKeys", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("inputValues", SHADERSTAGE_COMPUTE, true), new BindStorageBufferFormat("outputValues", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("b_passHist", SHADERSTAGE_COMPUTE, false), new BindStorageBufferFormat("b_index", SHADERSTAGE_COMPUTE, false), new BindUniformBufferFormat("uniforms", SHADERSTAGE_COMPUTE) ]; if (indirect) { histGroupEntries.push(new BindStorageBufferFormat("b_sortElementCount", SHADERSTAGE_COMPUTE, true)); scanGroupEntries.push(new BindStorageBufferFormat("b_sortElementCount", SHADERSTAGE_COMPUTE, true)); binGroupEntries.push(new BindStorageBufferFormat("b_sortElementCount", SHADERSTAGE_COMPUTE, true)); } this._globalHistBindGroupFormat = new BindGroupFormat(device, histGroupEntries); this._scanBindGroupFormat = new BindGroupFormat(device, scanGroupEntries); this._binningBindGroupFormat = new BindGroupFormat(device, binGroupEntries); const histDefines = /* @__PURE__ */ new Map(); histDefines.set("{G_HIST_DIM}", G_HIST_DIM); histDefines.set("{G_HIST_PART_SIZE}", G_HIST_PART_SIZE); if (indirect) histDefines.set("USE_INDIRECT_SORT", ""); this._globalHistShader = new Shader(device, { name: `OneSweepGlobalHist${suffix}`, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: onesweepGlobalHistSource, cdefines: histDefines, computeBindGroupFormat: this._globalHistBindGroupFormat, computeUniformBufferFormats: { uniforms: this._globalHistUniformFormat } }); const scanDefines = /* @__PURE__ */ new Map(); scanDefines.set("{MAX_SUBGROUPS}", maxSubgroups); scanDefines.set("{PART_SIZE}", PART_SIZE); if (indirect) scanDefines.set("USE_INDIRECT_SORT", ""); this._scanShader = new Shader(device, { name: `OneSweepScan${suffix}`, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: onesweepScanSource, cdefines: scanDefines, computeBindGroupFormat: this._scanBindGroupFormat, computeUniformBufferFormats: { uniforms: this._scanUniformFormat } }); const binDefines = /* @__PURE__ */ new Map(); binDefines.set("{D_DIM}", D_DIM); binDefines.set("{KEYS_PER_THREAD}", KEYS_PER_THREAD); binDefines.set("{MAX_SUBGROUPS}", maxSubgroups); if (indirect) binDefines.set("USE_INDIRECT_SORT", ""); this._binningShader = new Shader(device, { name: `OneSweepBinning${suffix}`, shaderLanguage: SHADERLANGUAGE_WGSL, cshader: onesweepBinningSource, cdefines: binDefines, computeBindGroupFormat: this._binningBindGroupFormat, computeUniformBufferFormats: { uniforms: this._binningUniformFormat } }); this._globalHistCompute = new Compute(device, this._globalHistShader, this._globalHistShader.name); this._scanCompute = new Compute(device, this._scanShader, this._scanShader.name); } destroy() { this._destroyBuffers(); this._globalHistShader?.destroy(); this._scanShader?.destroy(); this._binningShader?.destroy(); this._globalHistBindGroupFormat?.destroy(); this._scanBindGroupFormat?.destroy(); this._binningBindGroupFormat?.destroy(); this._globalHistShader = null; this._scanShader = null; this._binningShader = null; this._globalHistCompute = null; this._scanCompute = null; this._binningComputes.length = 0; this._globalHistBindGroupFormat = null; this._scanBindGroupFormat = null; this._binningBindGroupFormat = null; this._globalHistUniformFormat = null; this._scanUniformFormat = null; this._binningUniformFormat = null; super.destroy(); } _destroyBuffers() { this._destroyPingPongBuffers(); this._globalHist?.destroy(); this._passHist?.destroy(); this._index?.destroy(); this._globalHist = null; this._passHist = null; this._index = null; this._allocatedThreadBlocks = 0; this._threadBlocks = 0; } get radixBits() { return 8; } _ensureBinningComputes(numPasses) { while (this._binningComputes.length < numPasses) { this._binningComputes.push(new Compute(this.device, this._binningShader, `OneSweepBinning-${this._binningComputes.length}`)); } } _allocateBuffers(elementCount, forceRealloc = false) { const effectiveCount = Math.max(elementCount, this.capacity); const allocThreadBlocks = Math.max(1, Math.ceil(effectiveCount / PART_SIZE)); const currentThreadBlocks = Math.max(1, Math.ceil(elementCount / PART_SIZE)); const needRealloc = forceRealloc || allocThreadBlocks !== this._allocatedThreadBlocks || !this._keys0; if (needRealloc) { this._destroyBuffers(); this._allocatedThreadBlocks = allocThreadBlocks; this.capacity = effectiveCount; const device = this.device; this._allocatePingPongElementBuffers(effectiveCount); this._globalHist = new StorageBuffer(device, MAX_PASSES * RADIX * 4, BUFFERUSAGE_COPY_DST); this._passHist = new StorageBuffer(device, MAX_PASSES * allocThreadBlocks * RADIX * 4, BUFFERUSAGE_COPY_DST); this._index = new StorageBuffer(device, MAX_PASSES * 4, BUFFERUSAGE_COPY_DST); } this._threadBlocks = currentThreadBlocks; const maxPerDim = this.device.limits.maxComputeWorkgroupsPerDimension || 65535; Compute.calcDispatchSize(currentThreadBlocks, this._binningDispatchSize, maxPerDim); const histBlocks = Math.max(1, Math.ceil(elementCount / G_HIST_PART_SIZE)); Compute.calcDispatchSize(histBlocks, this._globalHistDispatchSize, maxPerDim); } sort(keysBuffer, elementCount, numBits = 16, initialValues, skipLastPassKeyWrite = false, destructiveKeys = false) { const numPasses = numBits / 8; const hasInitialValues = !!initialValues; this._elementCount = elementCount; this._numBits = numBits; this._hasInitialValues = hasInitialValues; this._skipLastPassKeyWrite = skipLastPassKeyWrite; const prevDestructiveKeys = this._destructiveKeys; this._destructiveKeys = destructiveKeys; this._allocateBuffers(elementCount, destructiveKeys !== prevDestructiveKeys); this._ensureBinningComputes(numPasses); if (destructiveKeys) { this._keys1 = keysBuffer; } const device = this.device; this._globalHist.clear(); this._passHist.clear(0, MAX_PASSES * this._threadBlocks * RADIX * 4); this._index.clear(); const histCompute = this._globalHistCompute; histCompute.setParameter("b_sort", keysBuffer); histCompute.setParameter("b_globalHist", this._globalHist); histCompute.setParameter("numKeys", elementCount); histCompute.setParameter("threadBlocks", this._threadBlocks); histCompute.setParameter("numPasses", numPasses); histCompute.setParameter("_pad", 0); histCompute.setupDispatch(this._globalHistDispatchSize.x, this._globalHistDispatchSize.y, 1); device.computeDispatch([histCompute], "OneSweep-GlobalHist"); const scanCompute = this._scanCompute; scanCompute.setParameter("b_globalHist", this._globalHist); scanCompute.setParameter("b_passHist", this._passHist); scanCompute.setParameter("threadBlocks", this._threadBlocks); scanCompute.setParameter("_pad0", 0); scanCompute.setParameter("_pad1", 0); scanCompute.setParameter("_pad2", 0); scanCompute.setupDispatch(numPasses, 1, 1); device.computeDispatch([scanCompute], "OneSweep-Scan"); let currentKeys = keysBuffer; let currentValues = initialValues ?? this._values0; let nextKeys = this._keys0; let nextValues = this._values1; for (let pass = 0; pass < numPasses; pass++) { const isFirstPass = pass === 0 && !hasInitialValues; const isLastPass = pass === numPasses - 1; const flags = (isFirstPass ? 1 : 0) | (isLastPass && skipLastPassKeyWrite ? 2 : 0); const binCompute = this._binningComputes[pass]; binCompute.setParameter("inputKeys", currentKeys); binCompute.setParameter("outputKeys", nextKeys); binCompute.setParameter("inputValues", currentValues); binCompute.setParameter("outputValues", nextValues); binCompute.setParameter("b_passHist", this._passHist); binCompute.setParameter("b_index", this._index); binCompute.setParameter("numKeys", elementCount); binCompute.setParameter("threadBlocks", this._threadBlocks); binCompute.setParameter("pass_", pass); binCompute.setParameter("flags", flags); binCompute.setupDispatch(this._binningDispatchSize.x, this._binningDispatchSize.y, 1); device.computeDispatch([binCompute], `OneSweep-Binning-${pass}`); if (!isLastPass) { currentKeys = nextKeys; nextKeys = currentKeys === this._keys0 ? this._keys1 : this._keys0; const t = currentValues; currentValues = nextValues; nextValues = t; } } return this.sortedIndices; } sortIndirect(keysBuffer, maxElementCount, numBits, sortSlotBase, sortElementCountBuffer, initialValues, skipLastPassKeyWrite = false, destructiveKeys = false) { const numPasses = numBits / 8; const hasInitialValues = !!initialValues; this._elementCount = maxElementCount; this._numBits = numBits; this._hasInitialValues = hasInitialValues; this._skipLastPassKeyWrite = skipLastPassKeyWrite; const prevDestructiveKeys = this._destructiveKeys; this._destructiveKeys = destructiveKeys; this._allocateBuffers(maxElementCount, destructiveKeys !== prevDestructiveKeys); this._ensureBinningComputes(numPasses); if (destructiveKeys) { this._keys1 = keysBuffer; } const device = this.device; this._globalHist.clear(); this._passHist.clear(0, MAX_PASSES * this._allocatedThreadBlocks * RADIX * 4); this._index.clear(); const histCompute = this._globalHistCompute; histCompute.setParameter("b_sort", keysBuffer); histCompute.setParameter("b_globalHist", this._globalHist); histCompute.setParameter("b_sortElementCount", sortElementCountBuffer); histCompute.setParameter("numKeys", 0); histCompute.setParameter("threadBlocks", 0); histCompute.setParameter("numPasses", numPasses); histCompute.setParameter("_pad", 0); histCompute.setupIndirectDispatch(sortSlotBase + 1); device.computeDispatch([histCompute], "OneSweep-GlobalHistIndirect"); const scanCompute = this._scanCompute; scanCompute.setParameter("b_globalHist", this._globalHist); scanCompute.setParameter("b_passHist", this._passHist); scanCompute.setParameter("b_sortElementCount", sortElementCountBuffer); scanCompute.setParameter("threadBlocks", 0); scanCompute.setParameter("_pad0", 0); scanCompute.setParameter("_pad1", 0); scanCompute.setParameter("_pad2", 0); scanCompute.setupDispatch(numPasses, 1, 1); device.computeDispatch([scanCompute], "OneSweep-ScanIndirect"); let currentKeys = keysBuffer; let currentValues = initialValues ?? this._values0; let nextKeys = this._keys0; let nextValues = this._values1; for (let pass = 0; pass < numPasses; pass++) { const isFirstPass = pass === 0 && !hasInitialValues; const isLastPass = pass === numPasses - 1; const flags = (isFirstPass ? 1 : 0) | (isLastPass && skipLastPassKeyWrite ? 2 : 0); const binCompute = this._binningComputes[pass]; binCompute.setParameter("inputKeys", currentKeys); binCompute.setParameter("outputKeys", nextKeys); binCompute.setParameter("inputValues", currentValues); binCompute.setParameter("outputValues", nextValues); binCompute.setParameter("b_passHist", this._passHist); binCompute.setParameter("b_index", this._index); binCompute.setParameter("b_sortElementCount", sortElementCountBuffer); binCompute.setParameter("numKeys", 0); binCompute.setParameter("threadBlocks", 0); binCompute.setParameter("pass_", pass); binCompute.setParameter("flags", flags); binCompute.setupIndirectDispatch(sortSlotBase); device.computeDispatch([binCompute], `OneSweep-BinningIndirect-${pass}`); if (!isLastPass) { currentKeys = nextKeys; nextKeys = currentKeys === this._keys0 ? this._keys1 : this._keys0; const t = currentValues; currentValues = nextValues; nextValues = t; } } return this.sortedIndices; } } export { ComputeRadixSortOneSweep };