Skip to main content

GPU Core Cookbook

Overview

This guide starts with application outcomes and maps them to small compositions of GPU Core operations. Use it after the first-graph tutorial and before reading individual API contracts. Each recipe names its durable inputs, bounded outputs, likely costs, and common mistake.

The arrows below describe GPU-resident dataflow, not CPU readbacks. Unless a recipe explicitly says otherwise, the application compiles topology once, updates parameters or imported data, and encodes the same graph again.

Choose a recipe

OutcomePipeline neighborhoodGPU-resident resultRoutine CPU readback
Render a selected subsetmask → scan → compaction → indirect drawstable IDs plus draw countNone
Summarize a selectionmask → reduction, histogram, or group aggregationbounded bins or group rowsOnly small chart output, if needed
Rank or order rowskeys → sort or segmented sort → downstream consumerordered IDs or key/value pairsNone
Query nearby objectsreusable index → bounded query → compactioncandidate or exact IDs plus countNone
Pick and highlightinteger picking target → bounded readback → highlight parametercanonical object IDOne small asynchronous result
Skip optional workCPU predicate or GPU indirect condition → dependent branchretained or newly published outputNone for GPU conditions
Spread analysis across framesimmutable graph → execution plan → bounded slicescontinuation state plus eventual resultProgress only, if desired
Package a workflowcontributor → resources and nodes → caller-owned graphreusable declared subgraphNone

Try execution policy safely

Toggle an optional branch, change the per-frame work budget, and deliberately undersize a bounded output. The graph topology stays fixed while the execution plan and validation result remain visible.

Interactive execution policy

Skip, slice, and reject work before it surprises the frame

These controls change parameters and a legal execution plan; they do not rebuild graph topology.

Conditionanalysis branch encoded
Execution4 frame slices
Validationready to encode

Select, compact, and render

Pipeline neighborhood: GPUMaskGPUScanGPUCompactionDrawCommandBuffer

Use a source-aligned mask when several predicates must combine or downstream work needs random membership tests. Compact it when rendering or later compute should visit only accepted rows. GPUCompaction owns its transient offsets; the application owns the source IDs, flags, output capacity, and count destination. Point that count at an indirect command's instanceCount to avoid waiting for JavaScript before drawing.

  • Cost to watch: the mask visits the source capacity; scan and scatter visit the selected input domain even when the final output is small.
  • Common mistake: treating every output slot as valid. Only the prefix named by the GPU-written count is published.
  • Shortcut: GPUVisibilityWorkflow packages common predicate-mask composition, stable IDs, compaction, and count publication.

Aggregate a selection

Pipeline neighborhood: source-aligned mask → GPUReduction, GPUHistogram, or GPUGroupAggregation → bounded chart/stat output

Keep the selection as a mask when an aggregation already examines each source row. Compact first only when several expensive downstream operations will reuse a much smaller selected set. The application owns input columns and output capacity; contributors may allocate scratch reductions or grouping state.

  • Cost to watch: distinguish input rows, candidate groups, output capacity, and actual published groups. They are different workload measures.
  • Common mistake: downloading source rows to build a chart. Keep large intermediate tables on the GPU and read back only the small bounded result that the UI needs.

Sort or select top rows

Pipeline neighborhood: keys and optional values → GPUSort or GPUSegmentedSort → ordered GPU consumer

Use GPUSort for one logical sequence and segmented sort when independent partitions should remain independent. Preserve batch or chunk boundaries when they carry streaming or ownership meaning; choose an explicitly packed output only when a global order requires it.

  • Cost to watch: sort work depends on capacity, key width, pass count, and whether a global or segmented order is required.
  • Common mistake: assuming that sorted chunks form one globally sorted sequence.

Query a reusable spatial index

Pipeline neighborhood: positions → GPUGridIndex or GPUBVHGPUGridIndexQuery or GPUBVHQuery → bounded IDs

Build an index when many changing queries reuse mostly static source data. A grid is a strong fit for roughly uniform neighborhoods; a BVH better follows irregular spatial extent. Query output remains bounded and should feed compaction, rendering, or analysis without returning the source dataset to JavaScript.

  • Cost to watch: separate index-build cost from per-query cost, and candidates from exact hits.
  • Common mistake: rebuilding an unchanged index for every view update.

Pick and highlight

Pipeline neighborhood: rendered canonical IDs → GPUIndexPickingTargetGPUReadbackRing → hover parameter → normal render

Render integer IDs into a small picking attachment, copy the requested pixel or region into a bounded readback slot, and publish only the newest completed generation. Use the returned canonical ID as a render parameter so highlighting does not require rebuilding source data.

  • Cost to watch: picking latency includes rendering, copy, queue completion, and mapping—not just the byte count.
  • Common mistake: allowing an older asynchronous pick to overwrite a newer pointer position.

Condition optional work

Pipeline neighborhood: CPU-known state or GPU-written indirect record → conditioned graph node → explicitly conditioned consumers

Use a CPU condition when parameters already say that work is unnecessary. Use a GPU indirect condition when the decision is produced on the GPU and a readback would introduce a stall. See Conditional execution for the exact ownership rules.

type Parameters = {analysisEnabled: boolean};

graph.addComputePass({
id: 'optional-analysis',
condition: {
id: 'analysis-enabled',
source: 'cpu',
evaluate: parameters => parameters.analysisEnabled
},
resources: [
{buffer: source, usage: 'storage-read'},
{buffer: analysis, usage: 'storage-write'}
],
compile: compileAnalysis
});
  • Cost to watch: a GPU condition avoids shader invocations for a zero dispatch, but upstream work that computes the condition still runs.
  • Common mistake: conditioning a writer but allowing its dependent readers to consume retained, stale contents unintentionally.

Spread analysis across frames

Pipeline neighborhood: compiled immutable topology → planExecution() → bounded execution slices → generation-checked publication

Use resumable execution for work that is useful but cannot monopolize an interactive frame. The operation exposes legal steps; the application selects a bounded set using an explicit budget and retains continuation state. Data batches remain data batches—an execution slice only controls how much scheduled work advances now.

const budget = {
maximumInvocationCount: 262_144,
maximumReadByteLength: 16 * 1024 * 1024
};
const plan = compiled.getExecutionPlan(budget, {latencyPriority: 'background'});
const execution = compiled.createExecution(budget, {latencyPriority: 'background'});

function encodeNextSlice(): void {
const commandEncoder = device.createCommandEncoder();
const step = execution.encodeNext(commandEncoder, {parameters});
device.submit(commandEncoder.finish());
if (!step.completed) requestAnimationFrame(encodeNextSlice);
}

Inspect plan.oversizedStepCount before starting. A nonzero value means at least one indivisible node exceeds the requested budget and must run alone for the execution to make progress.

  • Cost to watch: record actual queue time, invocations, bytes, and completion fraction per slice.
  • Common mistake: publishing a partial or superseded generation as if it were a complete result.

See Resumable execution and work budgets.

Package a reusable operation

Pipeline neighborhood: typed props → composite getNodes() → leaf getCommandNodes(graph) → command nodes → caller compilation and encoding

A contributor validates its fixed contract, declares every resource use, creates any bounded transients, and constructs nodes with stable identifiers. Composites can return their child primitives from getNodes() without receiving a graph; graph.add() recursively expands them in order. It does not submit commands, own the frame loop, or map application data. Expose ordinary graph views, masks, counts, and indirect commands so the next operation can compose without CPU translation.

class VisibleItems {
constructor(readonly props: VisibleItemsProps) {}

getNodes() {
return [
new GPUMask({
inputs: this.props.predicateMasks,
output: this.props.visibleMask,
operation: 'and'
}),
new GPUCompaction({
input: this.props.sourceIds,
flags: this.props.visibleMask,
output: this.props.visibleIds,
count: this.props.visibleCount
})
];
}
}

graph.add(new VisibleItems(props));
  • Cost to watch: attach estimates for invocations, bytes, dispatches, draws, and whether each estimate is exact or an upper bound.
  • Common mistake: hiding allocation, readback, submission, or mutable topology inside a helper that appears to be a declarative contributor.

See Composition levels and the GPUCommandGraph extension contracts.