Skip to main content

Module renderer

Module renderer 

Source
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 encode returns, so the caller’s next begin_render_pass on the same encoder is legal; and
  • the engine issues no queue.submit for scene work. It does issue queue.write_texture/write_buffer uploads, 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.

  1. 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: a FramePass::Clear pass drawing nothing and a FramePass::Opaque pass loading it, ahead of every round, and then every surface round in a pass of its own.

  2. 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 order Schedule::build listed 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 (see FilterResources). 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.

  3. The hole punch (FramePass::Punch), destination-out, when the frame recorded a ClearRect — see crate::compile::clear for 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, a ClearRect recorded 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:

  1. residency is settled for the whole frame: the frame’s LUT requests are serviced through the GradientCache so 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];
  2. each encoded paint is lowered into the GpuEncodedPaint record the fragment shader samples, carrying that residency; and
  3. 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§

EngineRenderer
One surface’s 2D render engine: a scene in, recorded passes out.
FilterPassPlan
One filter pass’s full recording state.
FilterResources
The GPU resources a frame’s filter rounds are executed with, beyond the two pooled pages they ping-pong between.
RangeCut
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.
RangeOptions
How project_command_range treats the commands it is asked to carry into a range — the one thing that differs between the renderer a range is projected for.

Enums§

FramePass
One render pass a frame recorded into the caller’s encoder — what EngineRenderer::last_frame_passes lists, in recording order.
RegionCopyMode
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 PushRetained bracket still open just before command index of commands — its command index — or None when no retained bracket is open there.
project_command_range
Writes the commands of scene in range into out (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_range over several ranges at once: writes the projection of each range of ranges, in the order given, one after the other into out (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).