Skip to main content

GPUCompaction

Overview

GPUCompaction stably selects packed uint32 values using packed uint32 flags.

At a glance

QuestionAnswer
ProblemTurn a sparse keep/discard decision into a stable dense work list.
Reads / writesReads source IDs and flags; writes packed IDs plus one valid-row count.
OwnershipInputs, output capacity, and count are caller-owned; offsets are graph-owned transients.
Output contractBounded; only the count-named prefix is valid, with source order preserved.
Expected workOne hierarchical exclusive scan plus scatter/count work over the input domain.
ChunksPreserved as one logical sequence; matching vector topology is required.
Conditions / budgetsCan sit inside a conditioned branch; standalone compaction has no resumable plan.
Neighborhoodmask and IDs → GPUCompaction → indirect compute, drawing, or dense analysis.

Concepts

Compaction converts a sparse keep/discard decision into a dense output. An exclusive scan assigns each selected row its destination index, a scatter copies selected values in source order, and one count reports the valid output prefix. For input [8, 3, 5, 2] and flags [1, 0, 1, 0], the valid output is [8, 5] with count 2; capacity beyond that prefix is unspecified.

When to use it

Use compaction when a later stage needs a dense work list rather than one flag per source row. It can turn a frustum mask into visible object IDs, a brush mask into selected record IDs, or a validity mask into jobs for a follow-up compute pass. Writing count into a DrawCommandBuffer also turns the same result directly into an indirect instance list.

Keep the mask un-compacted when downstream shaders already visit every source row or need random source-aligned membership tests. Compaction adds scan and scatter work, and only the prefix selected by count is meaningful; it does not shrink the caller-owned output allocation.

graph.add(new GPUCompaction({
id: 'visible-ids',
input: sourceIds,
flags: visibilityFlags,
output: visibleIds,
count: visibleCount
}));

Flags should contain 0 or 1. Nonzero values are clamped to one by the scatter pass. Selected values retain their source order. count must provide at least one packed uint32 row.

input, flags, and output may each be a packed GraphDataView<'uint32'> or GraphVectorView<'uint32'>. Input and flag lengths must match, but their atomic/vector boundaries may differ. The output only needs enough logical capacity and may use an independent atomic or vector topology. Scan and compaction align source rows by logical position, preserve caller-owned buffers and output chunk boundaries, and report one vector-wide total without concatenating data.

The algorithm composes GPUScan, allocates graph-owned offset scratch with the flags' chunking when needed, scatters selected values, and writes the final count. The count view may point at the instanceCount field of a DrawCommandBuffer, enabling compute-to-indirect-render dataflow without readback. Alignment borrows subviews from the original buffers; it does not pack source, flag, or output chunks, or require one scratch allocation for a streamed vector.

The initial implementation compacts IDs rather than arbitrary records. Renderers and subsequent kernels use those IDs to fetch source data.

Performance notes

GPUCompaction inherits the optional subgroup acceleration of its unsegmented GPUScan. GPUIndexedRangeCompaction and GPUPartitionedIndexedRangeCompaction also use subgroup prefix collectives for their dedicated per-range scans, reducing each local scan from 17 workgroup barriers to two. This path is used by the GPU Trace Viewer for stable span and dependency lists; CORE devices retain the original workgroup implementation.