§65 batching, or null (the default) to batch nothing — the opt-in seam
R-9 recorded for the GL backend, restated here byte for byte: the field is
read once per frame, the whole no-batching cost is one comparison per
item, and this class names the uploader import type only, so a bundle
that never calls createWgpuBatching never links it.
import { createWgpuBatching } from "@fourjs/render-webgpu";
renderer.batching = createWgpuBatching();
ReadonlyeventsThe §6b channel required by Renderer — contextlost (see the module header).
§9 render time in seconds, read by §60 node graphs containing a
time node (RFC 0001) — and by nothing else in this backend. 0 by
default, so a time-less application pays nothing; the application (or
Application's frame loop) writes it before render. Never simulation
time (§42/§43) — WebglRenderer.renderTime's contract, verbatim.
§84's render counters (A-1). null until an application assigns a record;
the frame reads this once and every call site is guarded by that one
comparison, so a renderer with statistics off issues exactly the commands
it always did.
What this backend can do (§62), read off the device at initialization.
Whether the device has been lost (§61). A lost renderer draws nothing.
Whether WebgpuRenderer.dispose has run. Disposal is terminal.
Last completed GPU-frame duration in seconds (A-1). Reading this
getter is what arms timestamp-query; a renderer that never reads
it issues not one extra GPU command (R-30b).
Runs one §82 ComputePass (WP-R1.8): compiles the kernel on first use
(cached on the source — wgpu-compute.ts), binds the storage buffers at
@group(0) in array order, and submits the dispatch.
This is Renderer.compute?() — the R-1 plan's Q3 promotion,
executed 2026-08-29: the descriptor now lives in @fourjs/render
(compute.ts there owns the story) and this method implements the
optional interface member. Presence is the capability, the
statistics/renderEffect pattern: WebGL 2 has no compute and never
grows this member, and §62's computeShaders capability is how an
application asks before reaching for it. The frame path never calls it —
§82's "basic graphics … must not require compute support", held
structurally. A buffer another backend minted is refused per binding
(the promoted handle is structural; wgpu-compute.ts).
Throws a FourError: INVALID_APPLICATION_STATE on a disposed or
never-initialized renderer, for non-integer workgroup counts, or for a
disposed buffer in bindings; DEVICE_LOST while lost;
UNSUPPORTED_GPU_FEATURE on a device surface without the compute entry
points. A kernel that fails to compile does not throw — WebGPU surfaces
compilation failure through the device's error scopes and the dispatch
does nothing, the pipeline cache's recorded reading of the error model.
Allocates one §82 storage buffer (WP-R1.8, wgpu-compute.ts) — the
application owns it and owes its dispose() (§83).
Throws a FourError: INVALID_APPLICATION_STATE on a renderer that is
disposed or was never initialized, or for an option record naming both
size and data, neither, or a size that is not a positive multiple of
4; DEVICE_LOST while the device is lost (§89).
Creates the device residency of one §36 simulation: "gpu" particle
system (R-31 wiring, 2026-08-29) — see wgpu-particle-simulation.ts's
header for the design: flat-lane storage buffers, the WP-R1.8 integrator
kernel, moveSlot compaction mirroring, and the position buffer doubling
as the draw's instance stream.
The application binds the result to its emitter
(emitter.bindGpuSimulation(...)) and owns its disposal (§83, the
createPickingService ownership rule). This renderer registers it under
options.systemId — the emitting node's id — and the §36 draw arm joins
on that key: a frame drawing a particle item whose id has a live
registered simulation binds the simulation's position buffer as the
per-instance position stream (gpuInstances pipeline variant) instead
of the interleaved CPU lanes; every other particle item, and every
pre-existing scene, draws the landed CPU path byte-identically.
One simulation per system id: a duplicate registration is refused — two residencies for one node would make the draw's join ambiguous — dispose the old one first.
Throws a FourError: INVALID_APPLICATION_STATE on a disposed or
never-initialized renderer, a duplicate systemId, or a non-positive
capacity; DEVICE_LOST while lost; UNSUPPORTED_GPU_FEATURE on a
device surface without the §82 compute entry points or
copyBufferToBuffer — which is how a backendless §36 GPU emitter fails
at authoring time rather than drawing a wrong picture (§85; §62's
computeShaders is the capability to ask first).
Builds a PickingService over this renderer — §71's "gpu" tier
(RFC 0005), gated on registerPickingPipeline() exactly as node-material
draws are gated on registerWebgpuNodeMaterialPipeline(): this method
resolves the registry slot and refuses (§85) when nothing registered, so
the id pipeline, the service, and its mapAsync read-back live only in
bundles that opted in (the pipeline-cost law; wgpu-picking-registry.ts).
What the service receives is a live window onto exactly the renderer
state an id pass needs — device, the three shared caches (geometry,
particles, render targets), the surface size, and the two lifecycle
flags — as accessors, so a §61 loss's dropped caches are seen rather
than captured stale (PickingRendererHost). Each call builds an
independent service; the caller owns and disposes it (§83). No GPU call
is issued here — the id pipeline compiles on the service's first pass.
Releases every GPU resource this renderer owns (§83).
Idempotent and terminal, and succeeds while the device is lost — the
caches drop their records without calling into a device that is gone. Every
other method throws INVALID_APPLICATION_STATE afterwards, as the other
two backends do.
Acquires an adapter, a device and the canvas's "webgpu" context (§61,
§45).
Rejects with a FourError carrying RENDERER_INITIALIZATION_FAILED (§89)
when there is no canvas, no navigator.gpu, no adapter (which is what a
browser without the WebGPU flag reports — navigator.gpu exists and
requestAdapter() resolves null; see register.ts), no device, or when
the canvas will not give up a WebGPU context. Calling it twice rejects with
INVALID_APPLICATION_STATE.
Nothing is compiled here. Pipelines are created lazily on first use, which
is a deliberate departure from the WebGL backend's compile-at-init —
wgpu-pipeline-cache.ts carries the measured argument.
Optionaloptions: RendererOptionsUpload a CPU texture and wait until its queued GPU upload has completed.
Reads a compute buffer back as a tightly packed ArrayBuffer (§82) —
copyBufferToBuffer + mapAsync, readPixels' honestly-asynchronous
contract for a buffer: rejects rather than skips, with the same codes
(INVALID_APPLICATION_STATE for a disposed renderer, one never
initialized, or a disposed buffer; DEVICE_LOST while lost;
UNSUPPORTED_GPU_FEATURE on a device surface without the entry points).
Reads back target's colour attachment — or the region rectangle of it
— as tightly packed RGBA8 bytes: §61's readPixels (WP-R1.6 shipped the
whole-target form; the region form arrived 2026-08-29 with Rectangle2
in @fourjs/math, RFC 0005's named prerequisite, cleared).
Asynchronous, honestly and permanently. WebGPU has no synchronous
readback — copyTextureToBuffer + mapAsync is the only path (probe-
verified; wgpu-readback.ts) — and §61's own sketch types the member
Promise<ArrayBuffer>, which is the RFC 0005 argument this method is the
standing evidence for. The result is width * height * 4 bytes (the
region's, when given; the target's otherwise), rows bottom-to-top
(§7a's Y-up; wgpu-readback.ts records the decision). region is
measured in target texels from the bottom-left corner, the same
space the rows are defined in; a region copies only its own rectangle.
A target that was never rendered into reads back its zero-filled
allocation — transparent black, the same defined answer sampling one
gives. Unlike the frame methods this one rejects rather than skips:
it is not called inside a frame, its caller is awaiting a value, and a
silently empty buffer would be undefined content by another name.
Rejections carry a FourError: INVALID_APPLICATION_STATE for a
disposed renderer, one never initialized, or a disposed target;
DEVICE_LOST while the device is lost (§89's "a caller asks for
something that cannot be satisfied while lost");
UNSUPPORTED_GPU_FEATURE on a device double without the readback entry
points (their presence is the capability — webgpu-device.ts). A
malformed region — fractional, empty, or hanging off the target — rejects
with validateReadbackRegion's RangeError (§85, @fourjs/render's
shared check, so both backends refuse with the same words).
Optionalregion: Rectangle2Draws root's subtree once per viewport, in array order (§61, §48, §64).
The per-view sequence is: viewport rectangle, scissor rectangle, the clear
draw (wgpu-unlit.ts explains why a clear is a draw here), then this
view's items. §61's shared clear semantics hold unchanged — colour is
cleared only where the view carries a clearColor, depth is cleared for
every view, and both are confined to the rectangle.
Items this tier has no pipeline for are skipped (see the module header),
as is a draw whose geometry will not upload. Returns without drawing
while the device is lost, when views is empty, and when target is a
disposed render target (§83's "disposed resources still in use" — the
frame must not draw into a released surface). An off-screen frame
(WP-R1.6) attaches the target's own colour and depth views, resolves its
viewport rectangles against the target's size, and bakes the target's
formats into its pipelines; everything else — clears, culling, the draw
arms — is the identical code path, which is what makes "the same scene
draws the same way off screen" true by construction.
Optionalinterpolation: RenderInterpolationOptionaltarget: RenderTarget | nullDraws one §70 full-screen effect (WP-R1.6) — pass.source's colour
attachment over the whole of pass.target, or of the swap chain, through
pass.effect. The normative contract is on @fourjs/render's
Renderer.renderEffect; the GL backend's method documents the shared
readings and this one differs in exactly two ways, both structural:
try/finally;
here the effect is its own render pass in its own command encoder, and
a WebGPU pass has no ambient state to corrupt — the one place this
backend is structurally safer, restated for §70. No scissor or viewport
is set: a pass's defaults are the whole attachment, which is exactly
what "an effect covers its destination surface" means.wgpu-effect.ts carries the inverted
R-19 argument), so a chain that only ever copies compiles one module
and uploads no uniforms at all; only a grade touches the 16-byte block.Everything, on render's terms — a lost device, a source that is not a
render-target texture (a caller that bypassed validateEffectRenderPass),
a disposed source or destination, an effect kind this backend does not
draw (RFC 0001's "graph" kind is drawn since WP-R1.9, through the
registered node store; unregistered, it skips — the closed-union rule
either way), and the feedback loop: a pass
whose destination is the very surface it samples, refused here exactly as
R-4 refuses it per draw, with RenderGraph.validate reporting it
statically as "feedback" so the mistake is normally caught at setup.
Resizes the swap chain to width × height logical pixels at
resolution device pixels per logical pixel (§61, §45).
The depth attachment is not reallocated here: it is allocated lazily by the next frame that needs one, so a burst of resize events during a window drag costs one allocation rather than one per event. Returns quietly while the device is lost, recording the size for whenever a frame runs again.
Optionalresolution: numberUploads data into a compute buffer at byteOffset (§82) — the per-step
params refresh the integrator tier needs, without reallocating.
The error contract is WebgpuRenderer.createComputeBuffer's, plus
INVALID_APPLICATION_STATE for a disposed buffer or an out-of-range or
misaligned write.
OptionalbyteOffset: number
The WebGPU backend (§62 backend 1).
Uniforms: one buffer, one bind group, a dynamic offset per draw
See
wgpu-bindings.tsfor the layout and the argument. The consequence here is the shape of a frame: the whole frame's uniform blocks are packed into one CPU staging array while the command encoder records, uploaded with a singlequeue.writeBufferbeforesubmit, and read back by the recorded draws through their dynamic offsets. Queue ordering makes that safe — awriteBufferenqueued before asubmitis visible to it — and it means a frame issues exactly one buffer upload however many objects it draws.The staging array and the GPU buffer are sized before recording, from an upper bound (one block per view, plus one per view per item), so no reallocation can happen mid-frame and invalidate the bind group the pass has already been told to use. They only ever grow.
One render pass per frame
WebGPU's scissor and viewport are pass commands, not ambient device state, so all the views of a frame are recorded into a single render pass with
setViewport/setScissorRectbetween them. This is the one place the WebGPU backend is structurally safer than the GL one: there is no state mirror to keep, nothing to restore in afinally, and a draw that throws cannot leave state behind for the next frame — the pass is simply never submitted.