UNPKG

playcanvas

Version:

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

266 lines (265 loc) 10.4 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug, DebugHelper } from "../../../core/debug.js"; import { BUFFERUSAGE_COPY_DST, BUFFERUSAGE_COPY_SRC } from "../../../platform/graphics/constants.js"; import { StorageBuffer } from "../../../platform/graphics/storage-buffer.js"; class ComputeRadixSortBase { /** * @param {GraphicsDevice} device - The graphics device. * @param {boolean} [indirect] - Whether the instance is for indirect dispatch only. */ constructor(device, indirect = false) { /** * The graphics device. * * @type {GraphicsDevice} */ __publicField(this, "device"); /** * Whether this sorter instance was created for indirect-dispatch use only. When `true`, * only indirect-mode shaders are compiled and {@link sort} is unavailable. * * @type {boolean} */ __publicField(this, "_indirect", false); /** * Minimum element capacity for internal buffers. Set by the caller as a high-water mark to * avoid reallocation churn when the workload shrinks; can be lowered to request shrinkage at * the next sort call. Concrete backends size allocations using max(element count for the sort, * `capacity`); reallocation is deferred until the next sort when that effective size changes. * Updated by implementations after allocation. * * @type {number} */ __publicField(this, "capacity", 0); /** * Current element count for the last or in-progress sort. * * @type {number} * @protected */ __publicField(this, "_elementCount", 0); /** * Number of key bits the current passes are built for. * * @type {number} * @protected */ __publicField(this, "_numBits", 0); /** * Whether the current sort uses caller-supplied initial values on pass 0. * * @type {boolean} * @protected */ __publicField(this, "_hasInitialValues", false); /** * When true, the last pass skips writing sorted keys (values only); {@link sortedKeys} may be stale. * * @type {boolean} * @protected */ __publicField(this, "_skipLastPassKeyWrite", false); /** * When true, the caller permits the sort to overwrite `keysBuffer` after pass 0 reads it. * `_keys1` is not owned by the sorter in this mode — it is assigned to `keysBuffer` before * each pass loop and must not be destroyed on realloc/destroy. * * @type {boolean} * @protected */ __publicField(this, "_destructiveKeys", false); /** * Internal keys buffer 0 (ping-pong). * * @type {StorageBuffer|null} * @protected */ __publicField(this, "_keys0", null); /** * Internal keys buffer 1 (ping-pong). When `_destructiveKeys` is true this is borrowed from * the caller's `keysBuffer` and must not be destroyed by the sorter. * * @type {StorageBuffer|null} * @protected */ __publicField(this, "_keys1", null); /** * Internal values/indices buffer 0 (ping-pong). * * @type {StorageBuffer|null} * @protected */ __publicField(this, "_values0", null); /** * Internal values/indices buffer 1 (ping-pong). * * @type {StorageBuffer|null} * @protected */ __publicField(this, "_values1", null); /** * Stable metadata buffer returned by {@link prepareIndirect}. Preallocated as four `u32` * entries; concrete backends assign `[slotCount, g0, g1, g2]` after `super(device)`. The caller * uploads its contents into a GPU uniform unchanged. * * @type {Uint32Array} * @protected */ __publicField(this, "_indirectInfo", new Uint32Array(4)); this.device = device; this._indirect = indirect; } /** * Returns the sorted indices (or values, when `initialValues` was passed) buffer of the last * completed sort. The result lives in whichever ping-pong values buffer was written last, * determined by pass-count parity — no separate output buffer is allocated. * * @type {StorageBuffer|null} */ get sortedIndices() { if (!this._values0) return null; const numPasses = this._numBits / this.radixBits; return numPasses % 2 === 1 ? this._values1 : this._values0; } /** * Returns the sorted keys buffer after the last sort. Keys live in one of the internal * ping-pong buffers depending on pass count and {@link radixBits}. * * @type {StorageBuffer|null} */ get sortedKeys() { if (!this._keys0) { return null; } const radix = this.radixBits; const numPasses = this._numBits / radix; return numPasses % 2 === 0 ? this._keys1 : this._keys0; } /** * Radix width in bits for this backend. Callers can align their key bit counts to the radix * boundary generically without knowing which backend is active. * * @type {number} * @abstract */ get radixBits() { Debug.error("ComputeRadixSortBase.radixBits must be implemented by a subclass"); return 0; } /** * Executes a direct-dispatch radix sort. See subclass docs for argument semantics. * * @param {StorageBuffer} keysBuffer - Input keys buffer. * @param {number} elementCount - Number of elements to sort. * @param {number} [numBits] - Number of bits to sort. * @param {StorageBuffer} [initialValues] - Optional initial values buffer for pass 0. * @param {boolean} [skipLastPassKeyWrite] - Skip writing keys on the last pass. * @param {boolean} [destructiveKeys] - When true, the sort may overwrite `keysBuffer` after * the first pass reads it (saves one internal key buffer). The caller must not read * `keysBuffer` after the sort returns. * @returns {StorageBuffer} Sorted values buffer. * @abstract */ sort(keysBuffer, elementCount, numBits, initialValues, skipLastPassKeyWrite, destructiveKeys) { Debug.error("ComputeRadixSortBase.sort must be implemented by a subclass"); return ( /** @type {any} */ null ); } /** * Executes an indirect-dispatch radix sort using workgroup counts pre-written into * `device.indirectDispatchBuffer` (typically by a shader that included the * `sortIndirectArgsCS` WGSL chunk). See subclass docs for argument semantics. * * @param {StorageBuffer} keysBuffer - Input keys buffer. * @param {number} maxElementCount - Maximum elements (allocation size). * @param {number} numBits - Number of bits to sort. * @param {number} sortSlotBase - Base indirect dispatch slot index. The backend uses * `slotCount` consecutive slots starting at this index; see {@link prepareIndirect}. * @param {StorageBuffer} sortElementCountBuffer - GPU-written element count buffer. * @param {StorageBuffer} [initialValues] - Optional initial values buffer for pass 0. * @param {boolean} [skipLastPassKeyWrite] - Skip writing keys on the last pass. * @param {boolean} [destructiveKeys] - When true, the sort may overwrite `keysBuffer` after * the first pass reads it (saves one internal key buffer). The caller must not read * `keysBuffer` after the sort returns. * @returns {StorageBuffer} Sorted values buffer. * @abstract */ sortIndirect(keysBuffer, maxElementCount, numBits, sortSlotBase, sortElementCountBuffer, initialValues, skipLastPassKeyWrite, destructiveKeys) { Debug.error("ComputeRadixSortBase.sortIndirect must be implemented by a subclass"); return ( /** @type {any} */ null ); } /** * Returns stable sort metadata describing how many indirect dispatch slots this backend uses * and the elements-per-workgroup granularity of each slot. The returned 4-element Uint32 * array is sorter-owned and never reallocated across calls, so it can be uploaded directly as * a uniform `vec4<u32>` and reused as the `slotInfo` argument to the `writeSortIndirectArgs` * WGSL helper (see `sortIndirectArgsCS` chunk): * * ``` * [slotCount, g0, g1, g2] // g_i = elements-per-workgroup for slot i; unused entries = 0 * ``` * * The caller must reserve `slotCount` consecutive slots in `device.indirectDispatchBuffer` * via {@link GraphicsDevice#getIndirectDispatchSlot} and pass the resulting base to * {@link sortIndirect}. * * @returns {Uint32Array} Sorter-owned 4-element Uint32 array (stable across calls). */ prepareIndirect() { return this._indirectInfo; } /** * Allocates ping-pong key/value buffers (`u32` per element). When `_destructiveKeys` is true, * `_keys1` is left null — the caller's `keysBuffer` will be borrowed into it before each pass * loop instead. * * @param {number} effectiveCount - Element high-water count (same units as `capacity` sizing). * @protected */ _allocatePingPongElementBuffers(effectiveCount) { const elementSize = effectiveCount * 4; const usage = BUFFERUSAGE_COPY_SRC | BUFFERUSAGE_COPY_DST; const device = this.device; this._keys0 = new StorageBuffer(device, elementSize, usage); DebugHelper.setName(this._keys0, "ComputeRadixSort.keys0"); if (!this._destructiveKeys) { this._keys1 = new StorageBuffer(device, elementSize, usage); DebugHelper.setName(this._keys1, "ComputeRadixSort.keys1"); } this._values0 = new StorageBuffer(device, elementSize, usage); this._values1 = new StorageBuffer(device, elementSize, usage); DebugHelper.setName(this._values0, "ComputeRadixSort.values0"); DebugHelper.setName(this._values1, "ComputeRadixSort.values1"); } /** * Destroys ping-pong keys/values buffers owned by the base. When `_destructiveKeys` is true, * `_keys1` is borrowed from the caller and is not destroyed. * * @protected */ _destroyPingPongBuffers() { this._keys0?.destroy(); if (!this._destructiveKeys) this._keys1?.destroy(); this._values0?.destroy(); this._values1?.destroy(); this._keys0 = null; this._keys1 = null; this._values0 = null; this._values1 = null; } /** * Releases resources owned by this backend. */ destroy() { } } export { ComputeRadixSortBase };