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.
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.
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).
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.
The most important fixed-function performance feature in the pipeline. Eliminates fragments before the fragment shader runs — saving ALU, bandwidth, and power.
| MECHANISM | PLATFORM | HOW IT WORKS | WHAT 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.
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);
}
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.
On TBDR, the render pass boundary is the unit of execution. Incorrect loadOp / storeOp causes unnecessary LPDDR5 traffic. The rules are simple and absolute:
| ATTACHMENT | loadOp | storeOp | RULE |
|---|---|---|---|
| Color (final) | CLEAR | STORE | Always store final color to RAM |
| Depth (not read later) | CLEAR | DONT_CARE | Never store depth if not needed downstream — saves full frame of LPDDR5 writes |
| Depth (read by next pass) | CLEAR | STORE | Must store if shadow map or depth-of-field reads it |
| G-Buffer (same-pass subpass) | DONT_CARE | DONT_CARE | Never 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,
};
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.
| PLATFORM | RASTER MODEL | DEPTH ELIM | RT MEMORY | KEY RECOMMENDATION |
|---|---|---|---|---|
| PC (NVIDIA/AMD) | IMR | Early-Z (HW) | VRAM | Avoid discard in opaque shaders; use depth prepass for complex scenes |
| PS5 / Xbox | IMR + Mesh Shader | Early-Z | VRAM (unified) | Use async compute queues; leverage ICB for GPU-driven rendering |
| Mali Valhall | TBDR | FPK (HW) | Tile SRAM → LPDDR5 | DONT_CARE depth storeOp; design render passes; avoid discard |
| Adreno 7xx | TBDR | HSR (Adreno) | Tile SRAM → LPDDR5 | Minimise overdraw; DONT_CARE depth; avoid mid-frame RT reads |
| Apple GPU | TBDR | HSR (HW) | Tile SRAM → unified | Tile Shaders; Subpass / Framebuffer Fetch for G-Buffer; DONT_CARE |
| VR PC (PCVR) | IMR + stereo | Early-Z | VRAM (eye textures) | Single-Pass Instanced; store depth for ATW; minimal post-processing |
| VR Standalone (Quest) | TBDR + stereo | HSR | Tile SRAM → LPDDR5 | FFR; alpha-to-coverage; Single-Pass Instanced; 11 ms hard limit |
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.
| BUFFER TYPE | LIVES IN | ACCESS | CACHE | MAX SIZE | USE CASE |
|---|---|---|---|---|---|
| Vertex Buffer | VRAM | VS read | L1/L2 | 4 GB | Per-vertex geometry: position, normal, UV, color |
| Index Buffer | VRAM | Fixed func | L1 | 4 GB | Triangle topology — reuse vertices without duplication |
| Constant / Uniform Buffer | VRAM → L1 | All stages, read | CB cache (aggressive) | 64 KB | Per-frame / per-material shared data: matrices, lights, params |
| Push Constants / Root Constants | Cmd Buffer | All stages, read | Register-like | 128–256 B | Per-draw hot data: MVP matrix, draw ID |
| Structured Buffer / SSBO | VRAM | Any stage, read | L2 general | VRAM limit | Instance arrays, bone matrices, large dynamic data |
| RW Buffer / UAV / SSBO (rw) | VRAM | CS read+write | L2 (coherent) | VRAM limit | Compute output: particles, physics, GPU culling results |
| Texture 2D (SRV) | VRAM | PS/CS, sampler | Texture cache (2D spatial) | 16K×16K | Albedo, normals, LUTs — with mip + filtering |
| RW Texture / UAV | VRAM | CS read+write | L2 | — | Post-process, procedural texture generation |
| Render Target | VRAM / Tile SRAM | PS write, blend | ROP / tile | — | G-Buffer, shadow maps, final color output |
| Depth Buffer | VRAM / Tile SRAM | HW depth test | Depth cache | — | Occlusion, shadow depth, reprojection (VR) |
| Shared / Group Memory | On-chip SM | CS only, R+W | No cache — direct | 64–100 KB | Intra-group communication, reductions, tiled algorithms |
| Indirect Buffer | VRAM | Fixed func args | L2 | — | GPU-written draw arguments for GPU-driven rendering |
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.
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 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);
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'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
| CONCEPT | Vulkan GLSL | DX12 HLSL | DX11 HLSL | Metal MSL | GL ES |
|---|---|---|---|---|---|
| Constant Buffer | uniform block (UBO) | cbuffer / CBV | cbuffer | constant T& [[buffer(N)]] | uniform block |
| Per-draw fast data | push_constant block | Root Constants | — | setVertexBytes() | glUniform*() |
| Read-only array | readonly buffer (SSBO) | StructuredBuffer<T> | StructuredBuffer<T> | device const T* [[buffer(N)]] | readonly SSBO |
| Read-write array | buffer (SSBO) | RWStructuredBuffer<T> | RWStructuredBuffer<T> | device T* (rw) | SSBO |
| Texture read | sampler2D | Texture2D + SamplerState | Texture2D + SamplerState | texture2d<T> + sampler | sampler2D |
| Texture write | image2D (imageStore) | RWTexture2D<T> | RWTexture2D<T> | texture2d<T, access::write> | image2D |
| Group shared | shared T[] | groupshared T[] | groupshared T[] | threadgroup T[] | shared T[] |
| Tile memory read | subpassInput | — | — | [[color(N)]] inout | framebuffer_fetch |
| Indirect args | VkDrawIndexedIndirectCommand | D3D12_DRAW_INDEXED_ARGUMENTS | — | MTLDrawIndexedPrimitivesIndirectArguments | — |