Home / Features / Render pipeline
There is no frame graph and no pass scheduler. The order of a frame is written down once, in
Rendering/RenderPass.cs, and every backend follows it. That
costs some scheduling generality; it buys a pipeline you can read in an afternoon and
compare pixel for pixel across four graphics APIs.
| Phase | What runs there |
|---|---|
| FrameStart Compute | Sky and atmosphere LUTs, cloud noise, SDF voxelisation, DDGI probe tracing — anything the scene pass will sample. |
| Shadow | Cascaded sun shadows plus one punctual slot, rendered into a single depth atlas. |
| Scene | Opaque and transparent geometry, sky, 3D sprites. Writes scene colour, depth and motion vectors. |
| AfterScene Compute | GTAO, TAA resolve, bloom downsample chain, debug view generation. |
| Post | Tonemap and composite of the post chain into the final colour target. |
| FinalBlit | Resolve to the swapchain, including design-resolution scaling. |
| Overlay | 2D sprites, shapes and MSDF text, drawn after everything else in screen space. |
One pass is missing from that strip because it is conditional:
OutlineMask sits between the AfterScene compute phase and
Post, and it renders the objects that asked for a screen-space outline into a mask that the
composite step then dilates. All four backends implement it, and it costs nothing on frames
where nothing requested an outline — the backend early-outs before allocating the mask
target.
Passes are named by an enum rather than by string, which means a backend that forgets one fails to compile rather than failing to draw.
public enum RenderPassId
{
Shadow,
Scene,
Post,
OutlineMask,
FinalBlit,
Overlay
}
The two compute phases are not pass identifiers. They are points in the schedule where registered compute work is dispatched, described further down.
Each pass is described by a PassDesc carrying its colour,
depth and velocity targets. The value of a fixed schedule is not the schedule itself —
it is that the rules around each pass can be stated in three lines and then relied on.
Resource states are transitioned at pass boundaries, never inside a pass body. No bindings survive from one pass to the next; every pass rebinds what it needs. Viewport and scissor are set by the pass framework, and the Shadow pass is the one place that overrides the viewport itself, because it renders four tiles into one atlas.
That contract is why the four backends stay comparable. A Vulkan barrier, a D3D12 resource transition and a Metal encoder boundary all land in the same place, so a divergence between two backends is a bug in one pass rather than a difference in scheduling philosophy.
Passes do not look each other up. They read and write a small set of static slots on
FrameSchedule, which is the whole of the inter-pass
communication surface.
| Slot | Type | Meaning |
|---|---|---|
SceneColor | RenderTarget? | HDR scene colour written by the Scene pass. |
SceneDepth | RenderTarget? | Scene depth, read by GTAO, TAA and the debug views. |
SceneVelocity | RenderTarget? | Motion vectors; null when motion vectors are off. |
ShadowMap | RenderTarget? | The shared cascade atlas. |
PostColor | RenderTarget? | Target the Post pass composites into. |
RenderShadow | Action<IGraphics>? | Callback the Shadow pass invokes to draw casters. |
RenderPost | Action<IGraphics, RenderTarget>? | Callback the Post pass invokes to composite. |
BloomTexture | string? | Name of the bloom result, or null when bloom is off. |
AoTexture | string? | Name of the AO result, or null. |
SceneColorOverride | string? | Lets an effect substitute its own output for scene colour. |
SkyViewTexture | string? | Sky-view LUT published by the atmosphere effect. |
CloudNoiseTexture | string? | Cloud noise volume name. |
AerialLutTexture | string? | Aerial-perspective LUT name. |
TaaActive | bool | True when TAA is resolving, so the camera applies jitter. |
The string? slots are deliberate. An effect publishes a
texture under a compute:// name and writes that name into the
slot; a consumer that finds null simply takes its unlit path. Switching off an effect and
failing to initialise it therefore produce the same, correct, frame.
Compute work is not hard-wired into the schedule. Effects register themselves against a phase, and the phase decides only whether they see the scene or precede it.
public enum ComputePhase
{
FrameStart,
AfterScene
}
// Returns false when the backend cannot
// run compute, or Initialize failed.
// Either way nothing is left behind.
bool ok = FrameSchedule.RegisterCompute(
graphics, new MyEffect());
Contract. A compute effect registers itself into one of the two compute phases and must leave no residue when a backend cannot run it. That is why the same scene code produces a correct frame with GI, AO and TAA all switched off.
Post-processing and compute effects walks the effects that ship in the box; Writing a compute effect is the how-to.
Worth stating plainly, because it is the central design trade of the renderer.