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 clear pass 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. Clear. 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). It draws nothing; separating it from the strip passes is what lets a frame with no draws at all still resolve to a clean surface.

  2. Opaque strips, depth-tested and depth-writing, unblended. Once per frame, ahead of every round: only the fully-covered interior spans of opaque draws targeting the surface reach it — 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 it establishes is what lets the alpha passes reject fragments an opaque draw in front of them already covered.

    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-recording this pass ahead of each would re-draw opaque coverage at equal stored depth over composites the round before it had already blended. Running it ahead of the layer rounds rather than after them changes nothing they do: a layer round writes a pooled page and reads neither the surface nor the depth attachment.

  3. One pass per round. A page round 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 draws alpha strips, premultiplied-blended, in painter order: depth-tested but not depth-writing when a depth attachment is in play, a plain painter’s-algorithm pass when it is not. 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.

  4. The hole 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 — pass 2 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 same strip program every draw goes through, 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 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 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.