playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
220 lines (219 loc) • 7.64 kB
TypeScript
/**
* A frame pass that performs GPU-based radix sort using mipmap-based prefix sums.
*
* This implementation is based on:
* - VRChat Gaussian Splatting by MichaelMoroz: https://github.com/MichaelMoroz/VRChatGaussianSplatting
* - Mipmap prefix sum trick by d4rkpl4y3r: https://github.com/d4rkc0d3r/CompactSparseTextureDemo
*
* ## Algorithm Overview
*
* The sort uses a 4-bit radix (16 buckets) and processes keys in multiple passes,
* one pass per 4-bit chunk. Each pass consists of:
*
* 1. **Count Pass**: For each digit (0-15), count how many keys in each group have that digit.
* Output is an R32F texture where each pixel stores a count. Groups are 16 elements.
*
* 2. **Mipmap Generation**: Generate mipmaps for the count texture using hardware mipmap
* generation. This creates a quadtree of counts that enables efficient binary search.
*
* 3. **Reorder Pass**: For each output position, binary search through the mipmap hierarchy
* to find which source element maps to it. The mipmap structure enables O(log N) lookup
* per element instead of O(N) linear scan.
*
* ## Mipmap Prefix Sum Trick
*
* The key insight is that mipmaps naturally form a quadtree of averages. By writing counts
* (e.g., 1.0 for active pixels) into an R32F texture with auto-generated mipmaps:
*
* - Each mip level stores the average of the 4 pixels below it
* - To reconstruct actual counts, multiply by 4^level (i.e., `1 << (level * 2)`)
* - This gives us a hierarchical prefix sum structure
*
* Binary search traversal:
* - Start at maxMipLevel and work down to level 0
* - At each level, check 3 quadrants (can skip 4th - if not in first 3, must be in 4th)
* - Order: bottom-left → bottom-right → top-left → top-right (Z-order/Morton curve)
* - Accumulate prefix sums while descending to find the target element
*
* The Z-order traversal ensures stable sorting: if element A comes before B in the input,
* it remains before B in the output.
*
* ## Internal Data Layout
*
* - Internal keys/indices use Morton order (Z-order curve) for better texture cache locality
* - Source keys texture uses linear (row-major) layout
* - Output sorted indices use linear layout for simple consumer access
*
* ## Complexity
*
* - Time: O(N log N) per pass due to mipmap binary search
* - Passes: ceil(numBits / 4) passes for numBits-bit keys
* - Memory: 2x keys textures + 2x indices textures + 1x prefix sums texture (all power-of-2)
*
* @category Graphics
* @ignore
*/
export class FramePassRadixSort extends FramePass {
/**
* The current sorted indices texture (R32U). Access sorted indices using Morton lookup.
*
* @type {Texture|null}
*/
_currentIndices: Texture | null;
/**
* Current number of radix passes.
*/
_numPasses: number;
/**
* Current internal texture size (power of 2).
*/
_internalSize: number;
/**
* Internal keys texture 0 (ping-pong buffer).
*
* @type {Texture|null}
*/
_keys0: Texture | null;
/**
* Internal keys texture 1 (ping-pong buffer).
*
* @type {Texture|null}
*/
_keys1: Texture | null;
/**
* Internal indices texture 0 (ping-pong buffer).
*
* @type {Texture|null}
*/
_indices0: Texture | null;
/**
* Internal indices texture 1 (ping-pong buffer).
*
* @type {Texture|null}
*/
_indices1: Texture | null;
/**
* Prefix sums texture (R32F with mipmaps).
*
* @type {Texture|null}
*/
_prefixSums: Texture | null;
/**
* Sort render target 0 (MRT for keys + indices).
*
* @type {RenderTarget|null}
*/
_sortRT0: RenderTarget | null;
/**
* Sort render target 1 (MRT for keys + indices).
*
* @type {RenderTarget|null}
*/
_sortRT1: RenderTarget | null;
/**
* Prefix sums render target.
*
* @type {RenderTarget|null}
*/
_prefixSumsRT: RenderTarget | null;
/**
* Count passes for each radix iteration.
*
* @type {RenderPassRadixSortCount[]}
*/
_countPasses: RenderPassRadixSortCount[];
/**
* Reorder passes for each radix iteration.
*
* @type {RenderPassRadixSortReorder[]}
*/
_reorderPasses: RenderPassRadixSortReorder[];
/**
* Number of elements to sort (set by setup()).
*/
_elementCount: number;
/**
* The source keys texture (set by setup()).
*
* @type {Texture|null}
*/
_keysTexture: Texture | null;
/**
* Gets the sorted indices texture (R32U, linear layout). Use `.width` for texture dimensions.
* Access with: `texelFetch(texture, ivec2(index % width, index / width), 0).r`
*
* @type {Texture|null}
*/
get sortedIndices(): Texture | null;
/**
* Sets up the sort for the current frame.
*
* Note: The source keys texture is read-only and can be any size.
* The sorted indices will be in a separate power-of-2 texture.
*
* @param {Texture} keysTexture - R32U texture containing sort keys (linear layout, any size).
* @param {number} elementCount - Number of elements to sort.
* @param {number} [numBits] - Number of bits to sort (1-24). More bits = more passes.
*/
setup(keysTexture: Texture, elementCount: number, numBits?: number): void;
/**
* Calculates the required power-of-2 texture size for the given element count.
*
* @param {number} elementCount - Number of elements.
* @returns {number} Power-of-2 size.
* @private
*/
private _calculateInternalSize;
/**
* Creates or resizes internal textures.
*
* @param {number} size - Power-of-2 size for textures.
* @private
*/
private _resizeInternalTextures;
/**
* Creates a texture for radix sort.
*
* @param {string} name - Texture name.
* @param {number} size - Texture size.
* @param {number} format - Pixel format (PIXELFORMAT_R32U or PIXELFORMAT_R32F).
* @param {boolean} [mipmaps] - Whether to generate mipmaps. Defaults to false.
* @returns {Texture} The created texture.
* @private
*/
private _createTexture;
/**
* Destroys internal textures and render targets.
*
* @private
*/
private _destroyInternalTextures;
/**
* Creates the sort passes based on numBits.
* Sets up beforePasses with the complete pass sequence (count, mipmap, reorder for each iteration).
*
* @private
*/
private _createPasses;
/**
* Destroys all sort passes.
*
* @private
*/
private _destroyPasses;
/**
* Executes the GPU radix sort. This is a convenience method that combines setup, frameUpdate,
* and rendering all passes in one call.
*
* @param {Texture} keysTexture - R32U texture containing sort keys (linear layout, any size).
* @param {number} elementCount - Number of elements to sort.
* @param {number} [numBits] - Number of bits to sort (1-24). More bits = more passes. Defaults to 16.
* @returns {Texture} The sorted indices texture (R32U, linear layout).
*/
sort(keysTexture: Texture, elementCount: number, numBits?: number): Texture;
}
import { FramePass } from '../../platform/graphics/frame-pass.js';
import { Texture } from '../../platform/graphics/texture.js';
import { RenderTarget } from '../../platform/graphics/render-target.js';
import { RenderPassRadixSortCount } from './render-pass-radix-sort-count.js';
import { RenderPassRadixSortReorder } from './render-pass-radix-sort-reorder.js';