The passes, in execution order (§63). A live view of the graph's own array — read it, do not retain it across an RenderGraph.addPass or RenderGraph.removePass, and never mutate it.
Appends a pass under name (§63's graph.addPass(name, pass, options))
and returns the graph, so calls chain.
Throws a FourError carrying
INVALID_APPLICATION_STATE when name is empty, when it is already taken,
or when an entry of options.inputs names a pass this graph does not have.
The last of those is what makes the graph acyclic by construction: an input
must already be in the list, and the list is the execution order.
An EffectRenderPass is additionally checked against §85 here, by
validateEffectRenderPass — setup is the one place an effect's
parameters can be rejected loudly, since a backend may not throw from
inside a frame (§61). It raises a RangeError, not a FourError: a
non-finite grading coefficient is a value violation, the same class
RenderTarget's size check reports, where everything else this method
refuses is a graph that cannot exist.
The pass object is not copied. Mutating its views array between
frames is therefore visible to the next execute, which is the same
arrangement renderer.render already has with the array it is handed.
Optionaloptions: AddPassOptionsRenders the graph as text — §63's "debug visualization" at the tier this package can honestly reach without a backend (the module header says what the full requirement needs).
RenderGraph: 4 passes, 3 enabled
1. world [scene] -> render-target-1
2. grade [effect grade] -> render-target-2 <- world
3. ui [scene, disabled] -> screen
4. composite [custom] -> screen <- grade, ui
An effect pass prints its §70 effect beside its kind, because "which
effect" is the only thing that distinguishes two otherwise identical
passes in a chain. -> screen is a pass with no target — the default
drawing buffer. The
output is deterministic (§33: no clock, no identity hashes) and is meant to
be logged or snapshotted; its exact layout is not a contract.
Draws every enabled pass in order through renderer, and returns how many
ran.
Each SceneRenderPass becomes exactly one
renderer.render(root, views, interpolation, target) call — R-4's seam,
with the frame's interpolation record threaded through unchanged — and
each EffectRenderPass becomes exactly one
renderer.renderEffect(pass) call, with the pass object forwarded
unchanged (R-6). So a graph produces the same backend calls, in the same
order, as the hand-written sequence it replaces. A
CustomRenderPass is called instead, and whatever it does with the
renderer is its own.
Nothing here interprets a disposed or unallocatable target: R-4 already defines both as "the backend skips the frame", and re-deciding that here would give the engine two policies for one situation.
An effect pass met by a renderer that does not implement
ScreenEffectRenderer throws a
FourError carrying
INVALID_APPLICATION_STATE. That is a deliberate exception to "a frame
never throws": the mismatch is a permanent property of the backend the
application selected (§62 selects one at the edge, once), not the
transient, driver-scheduled condition §61's no-throw rule protects
against — so it can only ever fail on the very first frame, loudly, where
the alternative is a post-processed image that silently never appears.
supportsScreenEffects answers the same question without a frame.
Allocates one context record per custom pass per call and nothing else; a graph of scene and effect passes allocates nothing at all.
Optionalinterpolation: RenderInterpolationThe pass added under name, or undefined when the graph has none.
Removes the pass added under name and returns whether there was one.
Refused with a FourError while another pass declares it as an input: silently dropping a producer would leave its consumers sampling a surface nothing writes, and the graph knows that now rather than at RenderGraph.validate time. Drop the consumers first, or disable the producer with RenderGraph.setPassEnabled.
Turns a pass on or off — §63's "pass enable/disable" — and returns the graph, so calls chain.
A disabled pass is skipped entirely by RenderGraph.execute: no
render call, no clear, nothing bound. Its consumers keep running and keep
sampling whatever its target last held, which RenderGraph.validate
reports as "unwritten".
Throws a FourError when the graph has no
pass under name, rather than doing nothing: a typo that silently fails to
disable a pass costs a frame's worth of work per frame, invisibly.
Checks the enabled passes against what they actually sample, and returns every finding in pass order (§63's "pass dependencies").
An empty array means: no enabled pass samples the target it writes, none samples a target only a later pass writes, none samples a target nothing writes, and none is opaque to the check. See RenderGraphIssueCode for what each finding means and RenderGraphIssue.severity for how much it matters.
const errors = graph.validate().filter((issue) => issue.severity === "error");
expect(errors).toEqual([]);
Call it at setup, not per frame. It builds a render list per scene pass to discover what that pass samples (the module header explains why the result cannot be cached), so it costs a scene traversal per pass. Every finding it reports is a static property of the graph plus the materials currently attached; re-run it when either changes.
§63's render graph at the linear-pass tier: an ordered, named, individually enableable list of passes that one RenderGraph.execute call drives through a Renderer.
Read the module header before reaching for this: it tabulates which of §63's seven requirements are shipped here and which are staged, and why.
Ownership
A graph owns nothing. Passes, roots, viewports, and render targets all belong to whoever created them, and the graph holds references — so it has no
dispose, disposing a target does not remove its pass, and two graphs may name the same target. This is the same rule a material follows towards its texture (§83), applied one level up.