Expand description
EngineRenderer: the public seam a host drives one surface’s 2D frames
through.
§The encode contract
EngineRenderer::encode records into a wgpu::CommandEncoder the
caller owns and never submits it. That is the whole point of the seam: a
host compositing 2D over its own 3D content records its passes before and
after the engine’s into one encoder and submits once, and a submit hidden
inside the engine would split that into two command buffers with a pipeline
flush between them. Two obligations follow, and both are contract rather
than preference:
- every pass the engine begins is ended before
encodereturns, so the caller’s nextbegin_render_passon the same encoder is legal; and - the engine issues no
queue.submitfor scene work. It does issuequeue.write_texture/write_bufferuploads, which are ordered ahead of the command buffers submitted after them and so land before the passes that read them.
One thing the engine does submit, and it is worth naming precisely because
the rule above is otherwise absolute: growing the image atlas array submits a
command buffer of its own, holding one texture copy and nothing else, on the
rare frame that grows it. That is maintenance, not scene work — it never
touches the caller’s encoder, records no pass and no draw, and exists because
the queued writes it has to precede would otherwise be flushed ahead of it
(see crate::gpu::atlas’s Why growth submits a command buffer of its
own). The caller’s encoder is still never submitted by the engine, and the
frame’s own passes still reach the queue only when the caller submits it.
§Sharing the depth attachment
A caller recording its own depth-writing passes into that encoder hands the
same attachment in as EngineTarget::depth, and the two renderers then
occlude each other correctly in either order. Three rules make that work,
and crate::gpu::depth is where they are stated in full: the shared
comparison and which end of the range is near
(DEPTH_COMPARE over a buffer whose far
plane is DEPTH_CLEAR); the depth
attachment’s extent matching the colour target’s; and who owns the clear —
whichever pass runs first in the encoder, which the caller states through
EngineRenderer::set_depth_pre_cleared.
What that contract does not extend to is colour. The frame’s opening
below clears the frame’s colour target unconditionally, so content painted
into that target before encode keeps its depth and loses its pixels: a
host compositing over its own content records that content after the frame,
or into a target of its own the frame composites.
§The frame’s passes
A frame records into that encoder, in this order.
-
The opening (
FramePass::Opening), the first pass over the surface, which does three things in one render pass:- clears the colour target to the frame’s base colour, and the depth
attachment to the far plane unless the caller stated it is already
populated (see
crate::gpu::depth) — the pass’s own load operations, so a frame with no draws at all still resolves to a clean surface; - draws the opaque strips, depth-tested and depth-writing, unblended: only the fully-covered interior spans of opaque draws targeting the surface — an anti-aliased edge is by definition not opaque, and a page carries no depth attachment for the split to be sound against. The depth they establish is what lets every blended strip after them reject fragments an opaque draw in front of it already covered;
- then draws the first surface round’s own strips (pass 2 below), in order, the pipeline switching between the two inside the pass.
It ends exactly where that round’s own pass would have ended — at a punch cut, at a page or filter round, or at the end of the frame. Page and filter rounds scheduled ahead of the first surface round run ahead of the opening: they write pooled pages and read neither the surface nor its depth, so the opening waits past them. A punch issued before any surface round draws takes the opening as a pass of its own, ahead of the erase, and so does the end of a frame whose surface rounds drew nothing blended.
The opaque strips are drawn once, not once per surface round, and that is a correctness requirement: the surface can take several rounds (a cut round is how a wide sibling fan is served), and re-drawing them ahead of each would re-draw opaque coverage at equal stored depth over composites the round before it had already blended.
One pass rather than three is what a tile-based GPU is paid for: every pass over the surface is a whole-framebuffer load into tile memory and a store back out, and the clear and the opaque strips were two such passes that drew little or nothing of their own. The pixels are identical — the same draws in the same order against the same attachments, only the pass boundaries gone.
FRUST_ENGINE_NO_PASS_MERGE(EngineRenderer::set_pass_merge) records them apart again: aFramePass::Clearpass drawing nothing and aFramePass::Opaquepass loading it, ahead of every round, and then every surface round in a pass of its own. - clears the colour target to the frame’s base colour, and the depth
attachment to the far plane unless the caller stated it is already
populated (see
-
One pass per round, the first surface round that draws excepted — it is drawn inside the opening. A page round (
FramePass::Page) renders one isolated layer into a pooled intermediate page it clears to transparent, at the page’s own origin — so every instance is shifted by the page’s tile-aligned bounds, and clipped to them, which is what makes a banded layer’s column pages tile their layer instead of each holding a clamped copy of it (see [PageWindow]) — and through the page’s own viewport uniform; a round continuing a page an earlier round of the same layer opened loads it instead. A surface round (FramePass::Surface) draws alpha strips, premultiplied-blended, in painter order, loading the surface: depth-tested but not depth-writing when a depth attachment is in play, a plain painter’s-algorithm pass when it is not. A surface round with nothing blended to draw records no pass. A round’s ops run in the orderSchedule::buildlisted them, so a finished child page composites into its parent exactly where the recording entered it.A filter round is the one round that draws no strip at all: it runs one pass of a filter’s sequence, one instanced quad through
EnginePipeline::Filter, reading the layer’s other pooled page through the engine’s only sampler and clearing the page it writes (seeFilterResources). It is an ordinary round of this walk in every other respect — recorded into the caller’s own encoder, in the order the scheduler listed it, with its pages handed back the moment its pass ends. It is not an own-encoder exception; the atlas replay remains the only one of those.A region-copy round (
FramePass::Copy) is a backdrop blur’s first: it reads a region of the frame’s own target back into a pooled page (RegionCopyMode) for the blur’s filter rounds to run over. It is the one page round that follows the opening rather than waiting past it — it reads the surface, so the clear, the opaque strips and every surface round ahead of it in painter order must have landed (the scheduler cuts the surface round there; a punch recorded ahead of it is erased first) — and, for the same reason, no draw at or past the first backdrop’s depth is routed into the opaque strips. The backdrop’s own rectangle is then drawn in the next surface round as strip instances reading the blurred page through the composite’s layer input, so a clip open around it masks it like any fill. -
The hole punch (
FramePass::Punch), destination-out, when the frame recorded aClearRect— seecrate::compile::clearfor the whole contract this pass implements. It is issued at the punch’s own painter-order position, not at the end of the frame: a surface round is cut where the punch was recorded, the punch pass goes into that cut, and the round’s remaining ops resume in a pass of their own after it. Everything drawn over the slot is therefore recorded after the erase and survives it, whether or not it wrote depth. A punch past every op of the frame — the ordinary case, aClearRectrecorded last — cuts nothing and lands after the last round exactly as it always did.
With depth unavailable — no attachment, or FRUST_ENGINE_NO_DEPTH set —
the opening draws no opaque strips (and, unmerged, the opaque pass
disappears) and every instance travels through the surface rounds, blended
in painter order. That is a correctness requirement rather than a
fallback detail: routing the opaque spans into a separate, earlier pass is
only sound because the depth buffer re-establishes their ordering against the
blended ones.
§Compositing a layer
A finished page reaches its parent as ONE instanced quad through the strip
program every draw goes through (its PaintClass::LayerComposite
specialisation — see Paint-class runs), flagged as a whole rectangle and
naming the layer colour source: the fragment stage then reads the page
bound as layer_input_texture at the quad’s own texel and scales it by the
opacity packed into the instance’s low byte. The page is bound through a
bind group of its own rather than the frame’s shared one, because group 0
carries both the pass’s viewport uniform and that layer input, and a page
round’s viewport is its own.
A retained page (crate::cache::retained) is composited by exactly the
same quad, bound the same way; only the texture differs — the store’s own,
named by the retained bracket rather than by a page group. A round
rendering a body into one is a page round in every respect above (cleared on
its first round, loaded on a continued one, through its own viewport
uniform, never taking the surface opening) and is listed as a
FramePass::Page. A DrawRetained’s composite carries the painter-order
index the command itself was recorded at.
A composite carries the deepest painter’s-order index of everything inside the layer it composites, nested layers included. That is what keeps a translucent layer correctly ordered against the root round’s own draws without giving the scheduler a depth model: every draw recorded before the layer sits behind that index and every draw recorded after it sits in front.
§Paint-class runs
Every strip pass is drawn in runs, each through the pipeline of one
PaintClass — the strip program specialised to the paints the run holds
(see crate::gpu::pipelines’s The paint-class axis). The runs are cut
when a segment is planned (Scratch::push_run, by
merge_class_runs): contiguous, in painter order, covering the segment
exactly, so a pass draws the same instances in the same order it always did
and only the program shading each run changes. A run shorter than the
renderer’s threshold (EngineRenderer::run_split_min,
crate::gpu::strips::RUN_SPLIT_MIN by default) is merged and drawn with
PaintClass::Uber rather than paying a pipeline switch of its own; a
composite always takes PaintClass::LayerComposite, the opaque pass its
own runs, and a punch PaintClass::SolidCoverage. A pass sets a pipeline
only where the class changes between runs.
FRUST_ENGINE_UBER_PROGRAM (EngineRenderer::set_uber_program) turns all
of it off: no segment is split, every pass draws through its state’s one
Uber pipeline, and no class program is warmed or built.
FRUST_ENGINE_IMAGE_SAMPLER (EngineRenderer::with_image_sampler) picks
the program family the classes are compiled from, not a class: in the
sampler family — the default — every program that reads atlas images (the
image classes and Uber) filters a bilinear image through one hardware
sampler, which every strip bind group then carries beside the atlas array;
its kill switch (0) compiles the manual four-tap family instead. The
runs, and the pipelines a frame resolves, are the same in both families.
§Paint resolution
A solid colour travels inside the strip instance itself. Anything else the compiler encoded — a gradient, an image, a blurred rounded rectangle — is resolved once per frame before a single instance is built, in three steps that have to happen in this order:
- residency is settled for the whole frame: the frame’s LUT requests are
serviced through the
GradientCacheso every gradient’s colour ramp has an offset into the packed LUT buffer, and every image paint’s atlas rectangle is looked up (or learned, the first time it is drawn) in the renderer’s own image registry — see [FrameResources::resolve_paints]; - each encoded paint is lowered into the
GpuEncodedPaintrecord the fragment shader samples, carrying that residency; and - the records are serialized back to back, which fixes the texel each one starts at — the index a strip instance names its paint by.
Ramp offsets are only valid within the frame that took them: the cache
compacts and rewrites them in EngineRenderer::end_frame, which is why
residency is decided here rather than at compile time. An image’s atlas
rectangle, by contrast, is stable for as long as the image stays resident
(the compiler’s crate::cache::images::ImageResidency does not move a
live image), so the renderer’s own registry only ever forgets an entry
when the frame that compiled it reports the entry’s region evicted.
A paint that still cannot be resolved — an image the atlas has no room for, a gradient whose ramp could not be baked, an external texture (nothing binds one yet) — leaves its draw skipped rather than stamped in a wrong colour: the same “a frame draws less, never wrong” rule the compiler follows for the commands it does not lower.
An image the atlas holds a minified copy of is the one paint whose lowered
record needs a correction here. The compiler composed its natural-to-device
transform against the source’s declared extent, before residency was
consulted and so before the fit was known; the shader samples the resident
rectangle. ResidentImage::minify_scale is the ratio between the two, and
folding it into the lowered record’s transform is what keeps a downsampled
image landing on the destination rectangle the display list asked for.
Structs§
- Engine
Renderer - One surface’s 2D render engine: a scene in, recorded passes out.
- Filter
Pass Plan - One filter pass’s full recording state.
- Filter
Resources - The GPU resources a frame’s filter rounds are executed with, beyond the two pooled pages they ping-pong between.
- Range
Cut - What cutting a command range out of a scene did — the bracket rule of the overlay spike, reported so a caller (and a test) can see what a cut cost.
- Range
Options - How
project_command_rangetreats the commands it is asked to carry into a range — the one thing that differs between the renderer a range is projected for.
Enums§
- Frame
Pass - One render pass a frame recorded into the caller’s encoder — what
EngineRenderer::last_frame_passeslists, in recording order. - Region
Copy Mode - How a region of a frame’s own target is read back into a pooled page —
the mechanism every effect sampling what the frame has already painted
shares (a backdrop blur today; see
crate::filters’ The backdrop blur).
Functions§
- open_
retained_ bracket - The outermost
PushRetainedbracket still open just before commandindexofcommands— its command index — orNonewhen no retained bracket is open there. - project_
command_ range - Writes the commands of
sceneinrangeintoout(reset first) as a self-contained display list, and reports what the cut did — the overlay spike’s range projection (render-model Phase 4, decision D10). - project_
command_ ranges project_command_rangeover several ranges at once: writes the projection of each range ofranges, in the order given, one after the other intoout(reset first), and reports the cuts summed — the segmented frame’s way of drawing the commands between overlay cuts on the main surface (render-model Phase 4, decision D10).