From File to Pixel · Part 02: The Pipeline

pavelzosim:~/atlas_SYS.ONLINE / UTC+3

03 Render Pipeline Stages

The render pipeline is the sequence of hardware and software stages that transforms geometry and material data into pixel colours in a framebuffer. Understanding which stages are programmable, which are fixed-function, and which behave differently between architectures determines how you write and optimise shader code.

Universal Stage Sequence

Input Assembly
VB/IB → triangles
Vertex Shader
programmable
Tessellation
optional
Rasterization
fixed-function
Early-Z / HSR
depth cull
Fragment Shader
programmable
Output Merger
blend · depth

Programmable stages (Vertex, Fragment, Compute) run your HLSL/GLSL/MSL code on the GPU. Fixed-function stages (Rasterization, Output Merger) execute at hardware speed with no programmable code — only state configuration via API calls.

IMR vs TBDR — The Core Architectural Split

Δ1 // IMR — Immediate Mode Rendering

Each draw call is processed and written to VRAM immediately. Fragments are shaded as they arrive. No ordering guarantee between draw calls — depth test eliminates hidden fragments after the fact.

Used by: PC NVIDIA/AMD, PS5, Xbox Series X.

Advantage: simple model, unlimited render targets, no tile budget constraints.

Disadvantage: hidden surfaces consume fragment shader invocations before depth test eliminates them (without depth prepass).

Δ2 // TBDR — Tile-Based Deferred Rendering

Screen is divided into tiles (~16×16 px). All geometry in a frame is rasterized and binned per-tile first (Binning Pass). Then each tile is rendered entirely in on-chip SRAM — including depth test — before writing the final colour to system RAM.

Used by: Mali, Adreno, Apple GPU, Quest 3.

Advantage: intermediate buffers (depth, G-Buffer) stay on-chip. Bandwidth to LPDDR5 = final colour only.

Disadvantage: binning overhead, tile memory size limit, framebuffer breaks are catastrophically expensive.

Early-Z · HSR · FPK — Depth Elimination

The most important fixed-function performance feature in the pipeline. Eliminates fragments before the fragment shader runs — saving ALU, bandwidth, and power.

[ DEPTH_ELIMINATION // MECHANISMS BY PLATFORM ]
MECHANISMPLATFORMHOW IT WORKSWHAT BREAKS IT
Early-Z PCConsole Hardware depth test runs before fragment shader. Fragments that fail depth are discarded without invoking the PS. discard / clip() in fragment shader. Manual SV_Depth write. Alpha test. Any operation that may change whether the fragment writes depth.
HSR
Hidden Surface Removal
Apple GPU After binning, the GPU sorts fragments per-tile and eliminates hidden ones before any fragment shader runs. Stronger than Early-Z — eliminates even within a single draw call. Same as Early-Z. Discard breaks HSR analysis.
FPK
Forward Pixel Kill
ARM Mali During fragment execution, if a later fragment covers an earlier one and the earlier one hasn't written yet, Mali kills the earlier fragment mid-execution. Alpha blend (write order matters), discard, late depth write.
Depth Prepass IMR Explicit first pass rendering only depth with minimal PS. Main pass uses ZTest Equal → only visible fragments shade. Doubles draw calls. Only worth it when PS is expensive and overdraw is high.

The single most common Early-Z killer: clip() or discard in an opaque fragment shader. This includes alpha cutout vegetation shaders. If your foliage shader uses clip(albedo.a - _Cutoff), Early-Z is disabled for every draw using that shader. Solution: use a dedicated depth prepass with alpha test, then the main pass with ZTest Equal.

Platform Pipeline Differences

PC Modern extensions — Mesh Shaders, Ray Tracing

DX12 SM6.5 / Vulkan 1.3 introduced Mesh Shaders: replace Input Assembly + Vertex + (optionally) Tessellation with two programmable stages — Amplification Shader (per-meshlet culling) and Mesh Shader (per-meshlet geometry). Nanite in Unreal 5 uses this on PC/Console for software rasterization of micro-polygons.

// DX12 Mesh Shader — per-meshlet dispatch
[numthreads(128, 1, 1)]
[OutputTopology("triangle")]
void MeshMain(
    uint3  gid  : SV_GroupID,
    uint   gtid : SV_GroupIndex,
    out vertices Vertex  verts[252],
    out indices  uint3   tris[126])
{
    Meshlet m = _Meshlets[gid.x];
    SetMeshOutputCounts(m.vertexCount, m.triangleCount);
    if (gtid < m.vertexCount)
        verts[gtid] = LoadVertex(m, gtid);
    if (gtid < m.triangleCount)
        tris[gtid]  = LoadTriangle(m, gtid);
}

Console Async Compute + ICB

PS5 and Xbox support Indirect Command Buffers (ICB) — a GPU writes draw commands into a buffer, which the GPU then executes without any CPU involvement. Combined with an Async Compute pre-pass for culling, this is how modern console engines achieve GPU-driven rendering with near-zero CPU overhead.

Mobile TBDR render pass design

On TBDR, the render pass boundary is the unit of execution. Incorrect loadOp / storeOp causes unnecessary LPDDR5 traffic. The rules are simple and absolute:

ATTACHMENTloadOpstoreOpRULE
Color (final)CLEARSTOREAlways store final color to RAM
Depth (not read later)CLEARDONT_CARENever store depth if not needed downstream — saves full frame of LPDDR5 writes
Depth (read by next pass)CLEARSTOREMust store if shadow map or depth-of-field reads it
G-Buffer (same-pass subpass)DONT_CAREDONT_CARENever leaves tile SRAM — free read via subpassInput / framebuffer fetch
// Vulkan — correct mobile render pass configuration
VkRenderPassCreateInfo rpInfo = {};
// Depth attachment — DONT_CARE storeOp saves bandwidth
VkAttachmentDescription depthAttach = {
    .format         = VK_FORMAT_D32_SFLOAT,
    .loadOp         = VK_ATTACHMENT_LOAD_OP_CLEAR,
    .storeOp        = VK_ATTACHMENT_STORE_OP_DONT_CARE, // never hits LPDDR5
    .initialLayout  = VK_IMAGE_LAYOUT_UNDEFINED,
    .finalLayout    = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
};

VR Single-Pass Instanced Stereo

Standard stereo rendering submits every draw call twice — once per eye. Single-Pass Instanced renders both eyes in one draw: the vertex shader receives an instance ID that encodes the eye index, selects the correct VP matrix, and writes to the correct render target array layer. One draw call, two eyes, halved CPU submission cost.

Pipeline Matrix — All Platforms

[ PIPELINE_MATRIX // PLATFORM_COMPARISON ]
PLATFORMRASTER MODELDEPTH ELIMRT MEMORYKEY RECOMMENDATION
PC (NVIDIA/AMD)IMREarly-Z (HW)VRAMAvoid discard in opaque shaders; use depth prepass for complex scenes
PS5 / XboxIMR + Mesh ShaderEarly-ZVRAM (unified)Use async compute queues; leverage ICB for GPU-driven rendering
Mali ValhallTBDRFPK (HW)Tile SRAM → LPDDR5DONT_CARE depth storeOp; design render passes; avoid discard
Adreno 7xxTBDRHSR (Adreno)Tile SRAM → LPDDR5Minimise overdraw; DONT_CARE depth; avoid mid-frame RT reads
Apple GPUTBDRHSR (HW)Tile SRAM → unifiedTile Shaders; Subpass / Framebuffer Fetch for G-Buffer; DONT_CARE
VR PC (PCVR)IMR + stereoEarly-ZVRAM (eye textures)Single-Pass Instanced; store depth for ATW; minimal post-processing
VR Standalone (Quest)TBDR + stereoHSRTile SRAM → LPDDR5FFR; alpha-to-coverage; Single-Pass Instanced; 11 ms hard limit

04 Buffer Taxonomy

A GPU buffer is a typed allocation in memory with specific access rules. The type determines where data physically lives, who can read and write it, how the hardware caches it, and at what cost. Choosing the wrong buffer type is one of the most common sources of invisible performance loss — no compiler warning, no crash, just slower rendering.

Every Buffer Type — Where It Lives · When To Use

[ BUFFER_TAXONOMY // ALL TYPES ]
BUFFER TYPELIVES INACCESSCACHEMAX SIZEUSE CASE
Vertex BufferVRAMVS readL1/L24 GBPer-vertex geometry: position, normal, UV, color
Index BufferVRAMFixed funcL14 GBTriangle topology — reuse vertices without duplication
Constant / Uniform BufferVRAM → L1All stages, readCB cache (aggressive)64 KBPer-frame / per-material shared data: matrices, lights, params
Push Constants / Root ConstantsCmd BufferAll stages, readRegister-like128–256 BPer-draw hot data: MVP matrix, draw ID
Structured Buffer / SSBOVRAMAny stage, readL2 generalVRAM limitInstance arrays, bone matrices, large dynamic data
RW Buffer / UAV / SSBO (rw)VRAMCS read+writeL2 (coherent)VRAM limitCompute output: particles, physics, GPU culling results
Texture 2D (SRV)VRAMPS/CS, samplerTexture cache (2D spatial)16K×16KAlbedo, normals, LUTs — with mip + filtering
RW Texture / UAVVRAMCS read+writeL2Post-process, procedural texture generation
Render TargetVRAM / Tile SRAMPS write, blendROP / tileG-Buffer, shadow maps, final color output
Depth BufferVRAM / Tile SRAMHW depth testDepth cacheOcclusion, shadow depth, reprojection (VR)
Shared / Group MemoryOn-chip SMCS only, R+WNo cache — direct64–100 KBIntra-group communication, reductions, tiled algorithms
Indirect BufferVRAMFixed func argsL2GPU-written draw arguments for GPU-driven rendering

Access Speed Order

Push / Root Constants
registers · ~1 cy
Shared / Group Mem
~20 cy (no conflict)
Constant Buffer (L1 hit)
~30 cy
Texture (L2 hit)
~100 cy
Structured Buffer (L2 hit)
~150 cy
VRAM miss
~400–600 cy

Decision rule: per-draw data under 128B → Push Constants. Per-frame shared data under 64 KB → Constant Buffer. Large arrays / dynamic data → Structured Buffer. Compute output → UAV / RWBuffer. Inter-thread communication → Shared Memory.

API Code — Vulkan / DX12 / DX11 / Metal

Vulkan

In Vulkan every buffer is a VkBuffer with usage flags. The "type" is determined by how you bind it in a descriptor set.

// Vulkan GLSL — all buffer types declared in shader
layout(set=0, binding=0) uniform FrameUBO {    // Constant Buffer
    mat4 viewProj;
    vec4 sunDir;
} frame;

layout(set=1, binding=0) readonly buffer InstanceSSBO { // Structured Buffer
    mat4 matrices[];
} instances;

layout(set=1, binding=1) buffer ParticleSSBO { // RW Structured Buffer (UAV)
    struct { vec3 pos; float life; } data[];
} particles;

layout(set=2, binding=0) uniform sampler2D albedoMap;      // Texture SRV
layout(set=2, binding=1, rgba8) uniform image2D outputTex; // RW Texture UAV

DX12

DX12 uses Descriptor Heaps — large arrays of resource descriptors (CBV, SRV, UAV, Sampler) in GPU-visible memory. Shaders index into them via the Root Signature. Bindless rendering stores thousands of texture descriptors in one heap and indexes with a root constant.

// HLSL DX12 — bindless textures via heap indexing
Texture2D textures[] : register(t0, space0); // unbounded array

uint texIndex; // passed as root constant
float4 albedo = textures[texIndex].Sample(samp, uv);

// Structured buffer — typed array
StructuredBuffer _Instances : register(t1);

// RW buffer for compute output
RWStructuredBuffer _Particles : register(u0);

DX11 / Unity Built-in

Slot-based: textures bind to t0–t127, constant buffers to b0–b13, UAVs to u0–u7. No explicit descriptor management — the driver handles it.

// HLSL DX11 / Unity — explicit register slots
Texture2D        _BaseMap    : register(t0);
SamplerState     sampler_BaseMap : register(s0);
StructuredBuffer _Instances : register(t1);

CBUFFER_START(UnityPerMaterial)
    float4 _BaseColor;
    float  _Roughness;
CBUFFER_END

// Unity NativeArray — zero-GC update
var native = new NativeArray(count, Allocator.TempJob);
// fill via Burst-compiled Job
computeBuffer.SetData(native); // memcpy native → VRAM, no GC alloc
native.Dispose();

Metal Argument Buffers — bindless as a first-class feature

Metal's Argument Buffers pack all resources for a draw — textures, samplers, constant pointers — into a single struct buffer. On Apple Silicon, the struct written by CPU is read by GPU without any copy.

// MSL — Argument Buffer
struct MaterialArgs {
    texture2d    albedo    [[id(0)]];
    texture2d    normalMap [[id(1)]];
    constant float4*    params    [[id(2)]];
    sampler             samp      [[id(3)]];
};

fragment float4 fragMain(
    Varyings in [[stage_in]],
    device MaterialArgs& mat [[buffer(0)]])
{
    float4 col = mat.albedo.sample(mat.samp, in.uv);
    return col * mat.params[0];
}

// Mobile subpass input — free tile memory read on TBDR
layout(input_attachment_index=0, set=0, binding=0)
    uniform subpassInput colorInput;
vec4 prev = subpassLoad(colorInput); // FREE on Mali/Adreno — on-chip read

Buffer Names Cross-API Reference

[ BUFFER_NAMES // CROSS-API REFERENCE ]
CONCEPTVulkan GLSLDX12 HLSLDX11 HLSLMetal MSLGL ES
Constant Bufferuniform block (UBO)cbuffer / CBVcbufferconstant T& [[buffer(N)]]uniform block
Per-draw fast datapush_constant blockRoot ConstantssetVertexBytes()glUniform*()
Read-only arrayreadonly buffer (SSBO)StructuredBuffer<T>StructuredBuffer<T>device const T* [[buffer(N)]]readonly SSBO
Read-write arraybuffer (SSBO)RWStructuredBuffer<T>RWStructuredBuffer<T>device T* (rw)SSBO
Texture readsampler2DTexture2D + SamplerStateTexture2D + SamplerStatetexture2d<T> + samplersampler2D
Texture writeimage2D (imageStore)RWTexture2D<T>RWTexture2D<T>texture2d<T, access::write>image2D
Group sharedshared T[]groupshared T[]groupshared T[]threadgroup T[]shared T[]
Tile memory readsubpassInput[[color(N)]] inoutframebuffer_fetch
Indirect argsVkDrawIndexedIndirectCommandD3D12_DRAW_INDEXED_ARGUMENTSMTLDrawIndexedPrimitivesIndirectArguments
// FROM_FILE_TO_PIXEL // PART 02 OF 05 // CHAPTERS 03–04 // EOF