ReadonlycapabilitiesWhat this backend can do (§62). Valid before Renderer.initialize only as far as the backend can know statically; a backend that queries device limits publishes the real values during initialization.
ReadonlyeventsThis renderer's event channel (§6b) — contextlost and contextrestored.
Composition rather than inheritance: see the module header. Use it exactly like any other fourJS emitter, including the returned unsubscriber:
const off = renderer.events.on("contextlost", (event) => {
pauseWhileLost(event.renderer);
});
Optional ReadonlylastSeconds the GPU spent on the most recently completed frame
(§84 gpuFrameTime, A-1), or undefined when this backend does not
measure GPU time.
Optional, and its presence is the capability — the same stance
Renderer.statistics takes. A backend that can time GPU work
declares the member (the WebGL 2 and WebGPU backends do); one that
cannot omits it, and Application then leaves stats.gpuFrameTime
as NaN rather than inventing a zero.
The value is seconds (§7a). NaN means "measuring, but no completed
sample yet" — timestamp queries are asynchronous on both GPU APIs, so
the first armed frame (and any frame whose query was disjoint or still
in flight) honestly reports unmeasured. A finite number is the last
query that landed, never the frame still on the GPU.
Reading the getter is what arms measurement. A renderer whose
lastGpuFrameTimeSeconds is never read issues not one extra GPU
command (R-30b): landed transcripts stay byte-identical. That is why
this is not gated on Renderer.statistics — A-1's draw counters
must not add, remove, or reorder a GPU call, and a timer query would.
OptionalstatisticsWhere this backend accumulates §84's drawCalls, triangles, and
instances, or null to count nothing (A-1, 2026-08-07).
Optional, and its presence is the capability. §61's interface says nothing about statistics, and §84 asks the application for the counters — but only a backend knows what it actually submitted, so the numbers have to originate here. A backend that counts declares the member (the WebGL 2 backend does); one that cannot omits it, and supportsRenderStatistics tells them apart so an application reports "not measured" rather than a confident zero.
A backend that declares it owes three things, and nothing else:
render calls
(an off-screen pass, then the on-screen one) and §84's counters are the
frame's totals. The owner of the record clears it per frame.statistics.ts).renderer.statistics = createRenderStatistics();
OptionalcomputeRecords and submits one §82 compute dispatch — the kernel in
pass.shader, pass.workgroups workgroups, pass.bindings bound in
array order at @group(0) (WP-R1.8; promoted here by the R-1 plan's Q3,
2026-08-29).
Optional, and its presence is the capability — the fourth instance of
the Renderer.statistics / Renderer.renderEffect /
Renderer.createPickingService stance, for the same two reasons: a
required member would break every implementor, and a backend with no
compute stage (WebGL 2, Canvas 2D, SVG, the null tier) must say so by
omission rather than by emulating on the CPU — §82's own closing sentence
makes compute an advanced optional capability, and §62's
computeShaders capability is how an application asks before reaching
for it. supportsCompute is the type-level test.
A backend that declares it owes:
render never calls this and this never draws;INVALID_APPLICATION_STATE), never as a
silent no-op (§85);UNSUPPORTED_GPU_FEATURE when the live device turns out to lack the
compute entry points the backend expected (readPixels' contract).Buffer allocation stays a backend API — see compute.ts's module header
for what deliberately did not move.
OptionalcreateBuilds a PickingService over this backend — §71's "gpu"/"pixel"
picking tier (RFC 0005, 2026-08-28).
Optional, and its presence is the capability — the
Renderer.statistics / Renderer.renderEffect stance, for
the same two reasons: a required member would break every implementor,
and a backend with no readable pixels should say so by omission rather
than by emulating. supportsPicking is the runtime test. Absent on
NullRenderer (no pixels at all), and deliberately absent on the
future Canvas 2D / SVG tiers even though both could answer "pixel"
natively (isPointInPath, SVG hit testing): emulation would make §71's
result quality vary by backend, and the owner's decision on RFC 0005 Q6
is that the tier is declared absent there instead.
The WebGL 2 and WebGPU backends declare it, gated on each package's
registerPickingPipeline() (the skinning seam's shape): the member says
what the backend can do, registration is the application opting in to
paying for it, and calling this without registering is refused with
INVALID_APPLICATION_STATE (§85) naming the fix. Each call builds an
independent service with its own id buffer; the caller owns it and
disposes it (§83). WebGL draws particle systems (one id per emitter)
and skinned meshes (deformed silhouette via SkinnedIdProgram).
WebGPU draws particle systems the same way (CPU 8-float billboard)
and skinned meshes through a private skinned id pipeline (never bind
pose).
Releases every GPU resource this renderer owns (§83).
Declared by Disposable, which this interface extends, and restated here to carry the renderer-specific contract. Implementations must make it idempotent (a second call is a no-op) and terminal, must succeed while the context is lost, and must leave no listeners attached to Renderer.events (§83: teardown may not retain listeners). Resources the renderer did not create — geometries, materials, textures handed in by the application — are not disposed here; whoever created them owns them.
Acquires the backend's context or device (§61, §45).
Asynchronous because WebGPU adapter/device acquisition is (§45's
await app.initialize()), and because a backend may compile pipelines
up front. Rejects with a FourError carrying
RENDERER_INITIALIZATION_FAILED (§62, §89) when the backend cannot start —
explicitly, rather than silently downgrading; backend selection and its
"auto" fallback are the application's job (§62), not an individual
backend's.
Calling it twice is an implementation-defined error, not a second acquisition.
Optionaloptions: RendererOptionsOptionalpreparePre-upload CPU texture bytes and wait for GPU completion. False means disposed, lost, or superseded while waiting; unavailable completion primitives reject.
OptionalreadReads back target's colour attachment — or the region rectangle of it
— as tightly packed RGBA8 bytes (§61, §92; the member the module header's
typed TODO carried until Rectangle2 landed, 2026-08-29).
Optional, and its presence is the capability — the Renderer.statistics / Renderer.renderEffect stance, for the same two reasons: a required member would break every implementor, and a backend with no readable pixels (NullRenderer, §62's SVG tier) says so by omission rather than by fabricating bytes.
Asynchronous, honestly and permanently (RFC 0005's commitment, and
§61's own signature). WebGPU has no synchronous readback at all —
copyTextureToBuffer + mapAsync is the only path — so the Promise is
the floor of what every backend can promise, and a backend whose native
read happens to be synchronous (GL's readPixels) still resolves rather
than returning bytes directly: the shape never varies by backend (§62).
Callers must not assume resolution within any particular frame.
The contract every backend that declares it owes:
width × height × 4 bytes (the region's, when given;
the target's otherwise) of tightly packed, straight-alpha RGBA8 — no
row padding, regardless of the backend's own alignment rules.region is measured in target texels from the bottom-left corner,
+Y up — the same space the result's rows are defined in. It must be
non-empty, integer-valued, and lie inside the target
(validateReadbackRegion is the shared §85 check; a violation
rejects with its RangeError). Omitted means the whole target.DEVICE_LOST /
CONTEXT_LOST, INVALID_APPLICATION_STATE,
UNSUPPORTED_GPU_FEATURE; §89) — a silently empty buffer would be
undefined content by another name.§92's visual regression tier is the first consumer. The pixel-picking
service (RFC 0005) does not read through this member: it owns its
own one-texel fence / mapAsync path.
Optionalregion: Rectangle2Draws root's subtree once for every viewport in views, in order (§61,
§48).
root is usually the Scene. Nothing in the scene is mutated: rendering
reads world transforms (or §43 interpolated render poses) and writes
pixels — §42's transform authority and §43's "render poses never feed back"
rule both depend on that, and it is what lets a renderer be swapped, run
twice, or omitted entirely without changing simulation results.
views array draws nothing — and, importantly, clears nothing.x/y/width/height in pixels, or
fractions of the current drawing-buffer size when normalized is true
(§48). Fractions are resolved against the size last given to
Renderer.resize, multiplied by the resolution — so a normalized
full-surface view stays correct across resizes with no per-frame work by
the application.Viewport.clearColor present means clear that rectangle to that colour (RGBA in
[0, 1], straight alpha, linear-light per §60a) before drawing it;
absent means do not clear colour, which is how a minimap or
picture-in-picture view composites over what an earlier view drew.clearDepth is deferred; the cleared value is the far plane.With interpolation present the backend draws each node at its §43 render
pose — the previous and current fixed-step poses blended at
interpolation.alpha, positions lerped and rotations slerped — instead of
at its resolved world transform. In @fourjs/render terms that is
buildInterpolatedRenderList rather than buildRenderList,
and it is what makes motion smooth when the display rate and the fixed
simulation rate disagree (§10). Nodes the buffer does not track are drawn
from their live transforms at every alpha, so a mixed scene needs no
bookkeeping.
With interpolation omitted the backend draws the resolved world
transforms, which requires the caller to have run resolveWorldTransforms
for the frame (§7, §64) — the interpolated path derives its matrices itself
and does not.
With target present the frame is drawn into that off-screen surface
instead of the backend's default drawing buffer, and
target.colorTexture can then be sampled by any material — which is the
whole of render-to-texture, and what §48's minimaps, mirrors, portals and
previews, §63's transient resources, and §70's effect chain are all built
out of. Everything else is unchanged: the same views, the same clears, the
same interpolation.
Three rules bind every backend:
{ x: 0, y: 0, width: 1, height: 1, normalized: true } it is on screen.
Pixel rectangles are target pixels, and are not scaled by the
Renderer.resize resolution — the target has a size of its own.A disposed target draws nothing at all, and a target the backend cannot allocate (an incomplete framebuffer, a device out of memory) skips the frame rather than throwing — same reasoning as the lost-context rule.
Returns immediately if the context is lost (see the context-loss contract above): never throws for that reason.
Optionalinterpolation: RenderInterpolationOptionaltarget: RenderTarget | nullOptionalrenderDraws one §70 full-screen effect: pass.source's texels over the whole of
pass.target (or of the drawing buffer) through pass.effect (R-6,
2026-08-07).
Optional, and its presence is the capability — the same stance Renderer.statistics takes, for the same two reasons: adding a required member to a published interface breaks every implementor, and a backend with no fragment stage to run an effect in (§62's SVG tier draws DOM nodes) should say so by omission rather than by silently copying. supportsScreenEffects is the runtime test; RenderGraph.execute uses it before forwarding an EffectRenderPass.
A backend that declares it owes exactly what Renderer.render owes, restated because the two are separate entry points into the same device:
See effect-pass.ts for which of §70's effects this tier ships and what
each staged one is waiting on.
Resizes the drawing surface to width × height logical pixels at
resolution device pixels per logical pixel (§61, §45).
The drawing buffer becomes width * resolution × height * resolution.
resolution defaults to 1 (see the module header); pass
devicePixelRatio in a browser.
Cameras are not updated: a camera's aspect is the application's to
set, because only the application knows which camera belongs to which
viewport and whether a resize should change the field of view, the visible
extent, or neither (§47's explicit-recomputation rule).
Optionalresolution: number
A backend that turns a scene into pixels (§61).
Swapping the backend: one import, one constructor — and the surface type
§62's promise is that changing backend is an import and a constructor. That holds, with one measured exception a reader meets immediately: this interface has no surface member. Each backend takes its own surface in its own constructor, so the swap costs a third line whenever the two backends' surface types differ.
HTMLCanvasElement, so only the import and the constructor change.Canvas2dRenderertakes anHTMLCanvasElementandSvgRendereranSVGSVGElement, so the surface handle changes too.The seam held in both cases — nothing above the surface moved, and the per-frame calls are identical. The cost is one line, not a redesign, and it is by design rather than omission: a surface member on this interface would have to name a union of every backend's surface type, which is exactly the backend coupling §62 keeps out.
docs/guides/custom-renderer-backends.mdshows the diff.Device and context loss is a first-class event (§61, spec rev 1.3)
§61: "Device and context loss (WebGL context loss, WebGPU device loss) is a first-class event, not an error case." Every implementation of this interface owes the following, and the §92 integration test ("renderer context loss and restore") checks it:
contextloston Renderer.events when the context is lost, andcontextrestoredwhen it comes back. Loss is reported, never thrown — theCONTEXT_LOST/DEVICE_LOSTcodes of §89 exist for the case where a caller asks for something that cannot be satisfied while lost, not to turn the loss itself into an exception.contextrestored, and re-upload user resources that retain CPU-side sources. Resources with no retained source expose a documented re-upload hook (that hook arrives with the texture API; see the deferred members below).dispose()during a lost context must succeed, and the renderer's own state (size, resolution, capabilities) survives loss — only GPU-side objects are rebuilt.Deferred §61 members (typed TODO)
These are part of §61 and are not part of this interface. The list is shorter again —
readPixelsjoined the interface whenRectangle2landed in@fourjs/math(2026-08-29; RFC 0005's recorded prerequisite, cleared) — but the two factories stay deferred by decision, not by absence (R-4, 2026-08-07):