Skip to main content

GPUSceneDrawGeneration

Overview

GPUSceneDrawGeneration turns active, visible GPUScene records into bounded indirect draw commands. It is the bridge between GPU visibility decisions and rendering: the CPU can encode the same graph and the same fixed set of indirect draws each frame while compute updates which command slots actually draw an instance.

This is useful when visibility changes much more often than geometry or pipeline setup. Frustum culling, spatial queries, hierarchy expansion, and selection masks can remain GPU-resident instead of forcing a readback, rebuilding a CPU draw list, and uploading counts again. Geometry arguments remain stable; the workflow changes only instanceCount and firstInstance.

GitHub

Concepts

Explicit slots separate selection from renderer policy

Each scene row carries a commandSlot. A visible active row requests that slot, while GPU_SCENE_INVALID_REFERENCE means that the row intentionally has no draw command. The command buffer is initialized by the renderer with stable geometry arguments such as vertex or index count and starting offsets. Draw generation does not infer materials, pipelines, or resource bindings.

This division lets applications choose their own slot assignment and issue indirect calls in the order required by their renderer. It also keeps this primitive useful before pipeline/resource grouping is standardized.

The live scene-graph example records every renderer-owned slot once in a reusable render bundle. Changing a group checkbox alters only the GPU visibility mask; moving or removing a selected object updates its ordinary scene record. The same compiled graph then republishes counts and source-indexed indirect commands without the application constructing a CPU-selected draw list.

Fixed capacity is observable

The workflow publishes three GPU-resident scalar results:

ResultMeaning
requiredCountActive, visible rows that request a command slot, including invalid capacity requests and collisions
publishedCountUnique in-range command slots that received a draw
overflowNonzero when a requested slot is out of range or more than one row requests the same slot

These values distinguish an empty result from an incomplete result without requiring hidden allocation. A renderer can size conservatively, inspect diagnostics asynchronously, or treat overflow as a signal to rebuild its slot assignment at a deliberate boundary.

Collisions are deterministic, not silently racy

When multiple visible rows request one slot, the lowest scene-record index owns it. The command is therefore repeatable across runs, while overflow records that some requested work was not published. Inactive, invisible, invalid-reference, and losing rows cannot leave a stale draw: every encoding first clears all command instance counts and first-instance fields.

The winner's scene-record index becomes firstInstance. Shaders can use that index to fetch the record's transform, bounds, stable object ID, or renderer-owned references from the scene buffer. Because scene rows beyond zero produce nonzero firstInstance values, the device must expose the optional WebGPU indirect-first-instance feature. addToGraph() rejects unsupported devices before adding passes; applications should request the feature when creating their device.

Visibility is optional and parameter-only

Without a visibility view, every active row participates. With one, each packed uint32 row is a source-aligned flag and nonzero means visible. The view must cover the scene's entire reserved capacity, including currently inactive rows. The visibility contents may change between graph encodings; the graph does not need to be rebuilt or recompiled.

Dispatch also covers the full reserved capacity rather than just the active prefix captured when the scene was imported. Records inserted later with scene.mutate() therefore participate in the same compiled graph immediately, while inactive spare slots remain suppressed by their scene flags.

The scene and command capacities remain compile-time topology. Changing either requires building a new workflow so allocation, dispatch, and overflow behavior remain inspectable.

Submission and draw recording stay with the application

addToGraph() adds initialization, eligibility, ownership, and publication passes but does not compile, encode, submit, or read back. After the graph runs, the application still records one indirect draw for each renderer-owned command slot. Slots with instanceCount === 0 do no visible work.

WebGPU does not provide a portable bindless multi-draw contract that would let this primitive choose arbitrary pipelines and bindings. GPUSceneResourceGroups therefore adds explicit renderer-owned binding windows as a separate workflow rather than hiding pipeline policy inside draw generation.

Usage

const graph = new GPUCommandGraph(device);
const sceneView = scene.importToGraph(graph);
const commandView = commands.importToGraph(graph);

const generation = new GPUSceneDrawGeneration({
scene: sceneView,
visibility,
commands: commandView,
requiredCount,
publishedCount,
overflow
});

generation.addToGraph(graph);

// The application compiles, encodes, and submits the graph, then records its stable slots.
for (let slot = 0; slot < commands.capacity; slot++) {
commands.draw(renderPass, slot);
}

commands may contain either draw or draw-indexed records. Its backing buffer must have both storage and indirect usage. The three diagnostic outputs are distinct packed uint32 graph views and may not overlap the scene, visibility, command, or one another.

Methods

addToGraph(graph)

Adds the fixed-capacity draw-generation passes to the target graph. Every supplied view must belong to that graph.

Properties

stats reports the imported scene row count, reserved scene capacity, command capacity, record size, transient ownership storage, and total output bytes without GPU readback.

Current scope

This implements roadmap Tranche 6.2a: deterministic explicit-slot publication and overflow. It does not allocate slots, issue render calls, or mutate scene records. Resource grouping remains a separate renderer-owned contract implemented by GPUSceneResourceGroups.

See also GPUScene, GPUVisibilityWorkflow, and DrawCommandBuffer.