playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
273 lines (272 loc) • 12.1 kB
TypeScript
/**
* Renders splats using a tiled compute pipeline with per-tile binning and local sorting.
* Receives a compacted splat ID list from {@link GSplatIntervalCompaction}, projects each
* splat into a projection cache, bins them into screen-space tiles via a fused
* count+pair-write pass, classifies tiles by size, sorts each tile by depth (with bucket
* pre-sort for large tiles), then rasterizes front-to-back. Pipeline:
*
* 1. Tile count: project each visible splat, write projection cache (screen pos, conic,
* color, depth). Iterate overlapping tiles twice: first to count intersections and
* build a 6x5 bitmask, then after a workgroup prefix sum + single global atomicAdd,
* to perform capped atomicAdd on per-tile counters and write (tileIdx, localOffset)
* pairs into a contiguous pair buffer. Large splats (AABB > 64 tiles) are deferred
* to a cooperative pass — their IDs are appended to a largeSplatIds buffer.
* 1b. Large tile count: one workgroup (256 threads) per deferred large splat reads
* projCache, recomputes the AABB, and cooperatively iterates tiles. Same pair-buffer
* and tileSplatCounts writes as pass 1. Sets the high bit of splatPairCount to flag
* these splats for the cooperative place-entries pass.
* 2. Prefix sum: exclusive prefix sum over per-tile counts produces offsets + total.
* 3. Place entries: each thread reads its (tileIdx, localOffset) pairs from the pair
* buffer and writes its splat index into tileEntries at deterministic positions
* (prefix-summed offset + localOffset). No atomics, no projCache reads. Skips large
* splats (high bit of splatPairCount).
* 3b. Large place entries: cooperative pass — one workgroup (256 threads) per large
* splat, same dispatch as 1b. Reads pairs and writes tileEntries in parallel.
* 3.5. Classify: scan tiles, build small/large/rasterize tile lists, assign compact
* overflow scratch offsets for large tiles, write indirect args.
* 4b. Bucket pre-sort: logarithmic-depth bucket histogram + scatter for large tiles
* (>4096 entries), using overflow scratch in the unified tileEntries buffer, packs
* whole buckets into <=4096 chunks (indirect dispatch).
* 4b.5. Copy chunk sort indirect dispatch args (separate pass for inter-pass barrier).
* 4a. Small tile sort: bitonic sort for tiles with 1..4096 entries (indirect dispatch).
* 4c. Chunk sort: bitonic sort on each chunk from bucket pre-sort (indirect dispatch).
* 5. Rasterize: one workgroup per non-empty tile reads its sorted entry range, loads
* from the projection cache via shared memory, and blends front-to-back with
* early-out (indirect dispatch).
*
* The tileEntries buffer is unified: main tile entry lists occupy [0, totalEntries),
* overflow scratch for bucket sort occupies [totalEntries, totalEntries + overflowUsed).
* Buffer capacity adapts dynamically via async GPU readback of actual usage.
*
* Supports both color and pick dispatch via two {@link GSplatLocalDispatchSet} instances.
* Splat/entry-dependent buffers (projCache, tileEntries) are shared between dispatch sets
* with a submitVersion guard to prevent resizing within the same command encoder.
*
* @ignore
*/
export class GSplatComputeLocalRenderer extends GSplatRenderer {
/** @type {GSplatLocalDispatchSet} */
_mainSet: GSplatLocalDispatchSet;
/** @type {GSplatLocalDispatchSet|null} */
_pickSet: GSplatLocalDispatchSet | null;
/** @type {FramePassGSplatComputeLocal} */
framePass: FramePassGSplatComputeLocal;
/** @type {GSplatTileComposite} */
tileComposite: GSplatTileComposite;
/** @type {boolean} */
_needsFramePassRegister: boolean;
/** @type {number} */
_textureSize: number;
/** @type {number} */
_minPixelSize: number;
/** @type {number} */
_minContribution: number;
/** @type {number} */
_alphaClip: number;
/** @type {number} */
_alphaClipForward: number;
/** @type {number} */
_exposure: number;
/** @type {number} */
_numSplats: number;
/** @type {StorageBuffer|null} */
_compactedSplatIds: StorageBuffer | null;
/** @type {StorageBuffer|null} */
_sortElementCountBuffer: StorageBuffer | null;
/** @type {StorageBuffer|null} */
_projCacheBuffer: StorageBuffer | null;
/** @type {StorageBuffer|null} */
_tileEntriesBuffer: StorageBuffer | null;
/** @type {StorageBuffer|null} */
_pairBuffer: StorageBuffer | null;
/**
* Packed atomic counters: [0] = global pair counter, [1] = large splat count.
*
* @type {StorageBuffer|null}
*/
_countersBuffer: StorageBuffer | null;
/** @type {StorageBuffer|null} */
_splatPairStartBuffer: StorageBuffer | null;
/** @type {StorageBuffer|null} */
_splatPairCountBuffer: StorageBuffer | null;
/** @type {StorageBuffer|null} */
_largeSplatIdsBuffer: StorageBuffer | null;
/** @type {number} */
_largeSplatIdsCapacity: number;
/** @type {number} */
_allocatedLargeSplatCapacity: number;
/** @type {number} */
_allocatedSplatCapacity: number;
/** @type {number} */
_allocatedEntryCapacity: number;
/** @type {number} */
_tileEntryMultiplier: number;
/** @type {number} Last device.submitVersion when shared buffers were resized */
_lastBufferSubmitVersion: number;
/** @type {number} Last readback entry count (-1 = consumed / no fresh data) */
_lastReadbackEntryCount: number;
/** @type {number} Consecutive frames where usage < half capacity */
_shrinkFrameCount: number;
/** @type {number} */
_fisheye: number;
/**
* Active data source providing format and texture access. When set via {@link setDataSource},
* the renderer reads format and textures from this object instead of the inherited workBuffer.
* Defaults to the workBuffer passed to the constructor.
*
* @type {{ format: GSplatFormat, getTexture: (name: string) => Texture }}
* @private
*/
private _dataSource;
/** @type {Shader} */
_placeEntriesShader: Shader;
/** @type {BindGroupFormat} */
_placeEntriesBindGroupFormat: BindGroupFormat;
/** @type {Shader} */
_placeEntryPrepShader: Shader;
/** @type {BindGroupFormat} */
_placeEntryPrepBindGroupFormat: BindGroupFormat;
/** @type {StorageBuffer|null} */
_placeEntryPrepDispatchBuffer: StorageBuffer | null;
/** @type {Compute|null} */
_placeEntryPrepCompute: Compute | null;
/** @type {Shader} */
_largeSplatShader: Shader;
/** @type {BindGroupFormat} */
_largeSplatBindGroupFormat: BindGroupFormat;
/** @type {Shader} */
_largeSplatPrepShader: Shader;
/** @type {BindGroupFormat} */
_largeSplatPrepBindGroupFormat: BindGroupFormat;
/** @type {StorageBuffer|null} */
_largeSplatDispatchBuffer: StorageBuffer | null;
/** @type {Compute|null} */
_largeSplatPrepCompute: Compute | null;
/** @type {Shader} */
_largePlaceEntriesShader: Shader;
/** @type {BindGroupFormat} */
_largePlaceEntriesBindGroupFormat: BindGroupFormat;
/** @type {Shader} */
_classifyShader: Shader;
/** @type {BindGroupFormat} */
_classifyBindGroupFormat: BindGroupFormat;
/** @type {Shader} */
_sortShader: Shader;
/** @type {BindGroupFormat} */
_sortBindGroupFormat: BindGroupFormat;
/** @type {Shader} */
_bucketSortShader: Shader;
/** @type {BindGroupFormat} */
_bucketSortBindGroupFormat: BindGroupFormat;
/** @type {Shader} */
_copyShader: Shader;
/** @type {BindGroupFormat} */
_copyBindGroupFormat: BindGroupFormat;
/** @type {Shader} */
_chunkSortShader: Shader;
/** @type {BindGroupFormat} */
_chunkSortBindGroupFormat: BindGroupFormat;
setRenderMode(renderMode: any): void;
frameUpdate(gsplat: any, exposure: any, fogParams: any): void;
_fogParams: any;
_debugMode: any;
_formatHash: any;
/**
* @param {StorageBuffer} compactedSplatIds - Dense buffer of visible splat IDs.
* @param {StorageBuffer} sortElementCountBuffer - Single-u32 buffer with visible count.
* @param {number} textureSize - The work buffer texture size.
* @param {number} numSplats - Upper bound on visible splats.
*/
setCompactedData(compactedSplatIds: StorageBuffer, sortElementCountBuffer: StorageBuffer, textureSize: number, numSplats: number): void;
/** @private */
private _registerFramePass;
/** @private */
private _unregisterFramePass;
resizeOutputTexture(width: any, height: any): void;
/**
* Ensure shared splat/entry buffers are large enough. Only resizes when
* device.submitVersion has changed (preventing mid-encoder buffer destruction).
*
* @param {number} numSplats - Upper bound on visible splats.
* @private
*/
private _ensureSharedBuffers;
_depthBuffer: StorageBuffer;
/**
* Main color dispatch.
*/
dispatch(): void;
/**
* Pick dispatch: runs the compute pick pipeline and returns the configured pick mesh instance.
*
* @param {object} cam - The camera.
* @param {number} width - Pick target width.
* @param {number} height - Pick target height.
* @returns {MeshInstance|null} The pick mesh instance ready for the picker's render list.
*/
dispatchPick(cam: object, width: number, height: number): MeshInstance | null;
/** @type {number} */
_lastDrawSlot: number;
/** @type {number} */
_lastNumTilesX: number;
/**
* Unified dispatch pipeline used by both color and pick paths.
*
* @param {GSplatLocalDispatchSet} set - The dispatch set to use.
* @param {number} width - Render target width.
* @param {number} height - Render target height.
* @param {boolean} pickMode - Whether this is a pick dispatch.
* @private
*/
private _dispatchPipeline;
/**
* @param {number} numSplats - Splat count at dispatch time.
* @param {number} numTiles - Tile count at dispatch time.
* @param {GSplatLocalDispatchSet} set - The dispatch set to readback from.
* @private
*/
private _scheduleReadback;
/**
* Invalidates all cached count resources on both dispatch sets. Called when the
* work buffer format changes, making all compiled count shaders invalid.
*
* @private
*/
private _invalidateCountCompute;
/** @private */
private _createCommonIncludes;
/** @private */
private _createBitonicIncludes;
/**
* Creates all shared shaders and bind group formats (called once in constructor).
*
* @private
*/
private _createSharedShaders;
/**
* Creates the count shader + shared bind group format.
*
* @param {boolean} pickMode - Whether to create the pick variant.
* @param {boolean} fisheyeEnabled - Whether to include the GSPLAT_FISHEYE define and fisheye uniforms.
* @returns {{ shader: Shader, bindGroupFormat: BindGroupFormat }} The shader and format.
* @private
*/
private _createCountShaderAndFormat;
/**
* Creates a dispatch set with its 8 Compute instances.
*
* @param {boolean} pickMode - Whether this set is for picking.
* @returns {GSplatLocalDispatchSet} The populated dispatch set.
* @private
*/
private _createDispatchSet;
}
import { GSplatRenderer } from './gsplat-renderer.js';
import { GSplatLocalDispatchSet } from './gsplat-local-dispatch-set.js';
import { FramePassGSplatComputeLocal } from './frame-pass-gsplat-compute-local.js';
import { GSplatTileComposite } from './gsplat-tile-composite.js';
import { StorageBuffer } from '../../platform/graphics/storage-buffer.js';
import { Shader } from '../../platform/graphics/shader.js';
import { BindGroupFormat } from '../../platform/graphics/bind-group-format.js';
import { Compute } from '../../platform/graphics/compute.js';
import type { MeshInstance } from '../mesh-instance.js';