Skip to main content

GPUGridIndexQuery

Overview

GPUGridIndexQuery selects stable object IDs from cells intersecting a GPU-resident point, bounds, or radius query. It publishes a capacity-bounded candidate list, the full stored-index candidate count, an overflow flag, and an optional source-ID-addressed mask. The result feeds visibility, compaction, picking policy, or an exact application predicate without CPU readback.

The word candidate is essential. A uniform grid indexes cells, not exact object geometry. A point query returns every object in the containing cell; bounds and radius queries return every object in an intersecting cell. This deliberately permits false positives so the index remains independent of whether an ID represents a point, label, particle, span, polygon, or object with its own bounds.

Concepts

Narrow first, test exactly second

Spatial acceleration is usually a two-stage operation:

grid cell query → conservative candidate IDs → exact object predicate → visibility or picking

The grid stage should be cheap and reusable. An exact stage can then fetch application-owned data for only those candidates: point distance for a radius, object bounds for an intersection, or screen-space geometry for picking. Keeping those contracts separate prevents GPUGridIndexQuery from embedding one object representation or claiming that cell overlap is an exact hit.

This matters most for large, selective queries. If a query covers most cells—or runs only once—an unindexed GPU scan may do less total work because it avoids index construction and candidate materialization.

Query kinds

The packed float32 query view is mutable between graph encodings:

KindQuery layoutCell ruleTypical use
point2D: [x, y]; 3D: [x, y, z]The one cell containing the pointNearby labels, cursor candidates, simulation lookup
boundsMinima followed by maximaEvery cell touching the query boundsRectangle selection, viewport candidates, box neighborhoods
radiusCenter followed by radiusEvery cell whose bounds intersect the circle or sphereProximity, collision broad phase, local influence

Point coordinates use the same boundary rule as construction: a point on an internal boundary selects the upper cell, while the domain maximum selects the final cell. Invalid point, reversed bounds, non-finite values, and negative radii produce no candidates.

Lists, masks, and overflow

The candidate output preserves stable IDs from GPUGridIndex, but atomic append order is unspecified. count reports how many candidates exist in the stored index prefix even if the output list is smaller. overflow becomes 1 when either the source index was truncated or the candidate output lacks capacity. Source-index overflow is propagated even when the stored prefix contains no match, because that apparently empty result may be incomplete.

An optional outputMask is cleared on every encoding and sets row objectId to 1 when that ID fits the mask length. This form composes directly with GPUMask and GPUVisibilityWorkflow when IDs address source rows. Sparse or application-global IDs may exceed the mask; they remain present in the candidate list but do not write outside it.

When to use it

Use candidate lists when the next pass should visit only spatially plausible IDs. Use the mask when the next workflow already operates source-aligned over every row. A list is usually better for a selective exact test; a mask is convenient for intersecting spatial membership with time, LOD, hierarchy, or selection decisions.

Cell size remains the dominant tradeoff. Very small cells increase index offsets, clearing, and the number of cells touched by broad queries. Very large cells return many false positives. The right size depends on object density, query radius, update rate, and how many queries amortize one build; the API does not pretend one grid is universally optimal.

Usage

const index = new GPUGridIndex({
positions,
gridSize: [64, 64],
bounds: [-180, -90, 180, 90],
cellOffsets,
objectIds: indexedObjectIds,
count: indexCount,
overflow: indexOverflow
});
index.addToGraph(graph);

new GPUGridIndexQuery({
index,
kind: 'radius',
query: centerAndRadius, // packed float32 [x, y, radius]
output: candidateIds,
count: candidateCount,
overflow: candidateOverflow,
outputMask: candidateMask
}).addToGraph(graph);

For three dimensions, a radius query contains [x, y, z, radius]; bounds contain [minX, minY, minZ, maxX, maxY, maxZ]. Updating the query buffer and encoding the compiled graph again changes the candidates without rebuilding graph structure.

The primitive neither builds the index nor applies exact object tests. It does not submit, grow capacity, sort or deduplicate IDs, or download results. Queries over an overflowed source index are explicitly marked incomplete.