GPUGridIndex
Overview
GPUGridIndex builds a flat uniform-grid index over packed two- or three-dimensional points. It
publishes exclusive cell offsets, capacity-bounded stable object IDs, the full accepted count, and
an overflow flag. The structure stays in storage buffers so later visibility, picking, simulation,
or neighborhood passes can consume candidates without first downloading them.
A grid index trades one rebuild for cheaper repeated spatial queries. It is most useful when many objects share a bounded domain, queries are local, and the same index serves several frames or several queries. Examples include screen-space label neighborhoods, map points, particle collision candidates, nearby simulation agents, and coarse 3D visibility cells.
Concepts
Flat cell storage
The index uses a compressed row-major layout analogous to CSR adjacency:
cellOffsets: [0, 2, 2, 5]
objectIds: [8, 3, 4, 9, 1]
cell 0 cell 2
Cell i owns the logical range [cellOffsets[i], cellOffsets[i + 1]). Two-dimensional cells use
row * width + column; three-dimensional cells use (layer * height + row) * width + column.
Exact maximum coordinates enter the last cell on an axis. Non-finite and out-of-domain positions
are ignored.
The final offset and count report the full required object-ID length even when objectIds is too
small. overflow then becomes 1, and only destinations below capacity are written. This preserves
an honest sizing signal and prevents out-of-bounds writes. A query must clamp every cell range to
the available object-ID capacity when consuming an overflowed build.
Stable identity, not stable cell order
By default, IDs are logical source-row indices plus firstSourceIndex. sourceIds can instead
provide application-owned stable IDs with the same atomic or vector topology as the positions.
Position vector chunks remain separate inputs and retain their logical bases; the build never packs
or rewrites them.
Atomic scatter order inside one cell is unspecified. The set of IDs and every cell boundary are stable, but two equal builds need not place IDs in the same within-cell order. Consumers that need a deterministic priority should sort candidates explicitly or apply a deterministic reduction after querying.
Build and update cost
Every graph encoding performs a complete build: clear cell counts, count accepted positions, exclusive-scan counts into offsets, and scatter IDs. Graph-owned count, cursor, and scan buffers make the allocation cost visible through command-graph statistics and reusable across encodings.
The current updatePolicy is 'rebuild'. An application may upload only a changed source range or
replace one vector chunk, but changing any position or membership rebuilds the compact index. This
is an explicit cost contract, not incremental index maintenance. A future incremental mode must
demonstrate bounded relocation or reserved per-cell capacity and compare its memory and update cost
with this compact rebuild before it can share the API.
When to use it
Use a grid when the domain and useful cell size are known, object density is reasonably distributed, and multiple selective queries can amortize construction. Cell size controls the tradeoff: smaller cells reduce query candidates but increase offset storage and clearing work; larger cells build a smaller index but leave more candidates for exact filtering.
An unindexed GPU scan is often better for one broad query, frequently changing data, or a small
population. Highly clustered or scale-varying data may favor a BVH once that contract is available.
GPUGridBinning and GPUGridAggregation are summaries rather than indexes: they return counts or
statistics per cell but intentionally discard the object IDs needed for spatial queries.
Usage
const index = new GPUGridIndex({
positions,
gridSize: [64, 64],
bounds: [-180, -90, 180, 90],
cellOffsets,
objectIds,
count,
overflow
});
index.addToGraph(graph);
Three-dimensional positions use float32x3, a three-component gridSize, and minima followed by
maxima in bounds:
new GPUGridIndex({
positions: particlePositions,
gridSize: [32, 16, 32],
bounds: [-100, -50, -100, 100, 50, 100],
cellOffsets,
objectIds,
count,
overflow
}).addToGraph(graph);
cellOffsets.length must equal width * height + 1 or width * height * depth + 1.
objectIds.length is the ID capacity. count and overflow each provide at least one packed
uint32 row. Generated IDs and the accepted population must fit in uint32.
The primitive records build work only. It does not submit commands, allocate persistent output, read results back, choose a cell size, grow capacity, or perform an exact spatial query.