The rendering path determines how the engine evaluates lighting: in a single pass per object (Forward) or in two passes where geometry and lighting are fully separated (Deferred). The choice drives G-Buffer memory usage, light count scalability, transparency handling, and MSAA compatibility.
Each object rendered once. Fragment shader evaluates all lights that affect it. No G-Buffer. MSAA native. Transparency straightforward.
Cost per pixel: O(objects × lights). With 100 lights and 10 000 objects: 1 000 000 light evaluations per pixel.
Best for: mobile (TBDR), VR, scenes with few dynamic lights, transparent-heavy scenes.
Pass 1: Geometry writes surface data (normals, albedo, roughness, depth) into a G-Buffer. Pass 2: Lighting reads G-Buffer and evaluates all lights in screen space.
Cost per pixel: O(pixels × lights). With 100 lights: 100 evaluations per visible pixel, regardless of object count.
Best for: PC/Console, many dynamic lights, complex lighting environments.
| CRITERION | Forward | Forward+ | Deferred | Deferred+ |
|---|---|---|---|---|
| Light scalability | Poor O(n×l) | Medium tiled | Good O(pixels×l) | Best clustered |
| G-Buffer VRAM | None | Depth only | 30–120 MB | 30–120 MB |
| MSAA | Native | Native | Expensive | TAA instead |
| Transparency | Native | Native | Separate forward pass | Separate forward pass |
| Mobile / TBDR | Ideal | Good | Subpass only | Avoid |
| VR (foveated) | Native | Good | Limited | Limited |
| Custom shading | Full control | Full control | Requires G-Buffer slot | Requires G-Buffer slot |
| Sub-surface scatter | Per-object pass | Per-object pass | Screen-space | Screen-space |
| Platforms | All | PC/Console/Mobile | PC/Console | PC/Console (Unity 6+) |
Forward+ (Unity URP Deferred, UE Forward Shading): a hybrid — tiled/clustered light culling in a prepass compute shader outputs per-tile or per-cluster light lists. The forward shading pass then iterates only the lights relevant to each tile. Scales to hundreds of lights without a full G-Buffer. The best mobile path for lit scenes.
Unity 6 unified URP around a Render Graph system — a frame-graph abstraction that tracks resource lifetimes, automatically manages transient allocations, and enables native async compute scheduling. Custom passes now live in RecordRenderGraph instead of the old Execute pattern.
// Unity 6 URP — Render Graph custom pass
public class MyBlurPass : ScriptableRenderPass
{
// Declare pass data struct — passed between Record and Execute
class PassData
{
public TextureHandle source;
public TextureHandle dest;
public Material blurMaterial;
public int iterations;
}
public override void RecordRenderGraph(
RenderGraph renderGraph,
ContextContainer frameData)
{
var resourceData = frameData.Get<UniversalResourceData>();
// Describe transient texture — allocated/freed by RenderGraph
var desc = renderGraph.GetTextureDesc(resourceData.activeColorTexture);
desc.name = "_BlurTemp";
TextureHandle tempTex = renderGraph.CreateTexture(desc);
// Add a raster render pass
using var builder = renderGraph.AddRasterRenderPass<PassData>(
"My Blur Pass", out var passData);
passData.source = resourceData.activeColorTexture;
passData.dest = tempTex;
passData.blurMaterial = m_BlurMaterial;
passData.iterations = m_Iterations;
// Declare resource access — RenderGraph validates and schedules
builder.UseTexture(passData.source, AccessFlags.Read);
builder.SetRenderAttachment(passData.dest, 0, AccessFlags.Write);
// Execution lambda — runs on render thread
builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
{
for (int i = 0; i < data.iterations; i++)
Blitter.BlitTexture(ctx.cmd, data.source,
new Vector4(1,1,0,0), data.blurMaterial, 0);
});
}
}
// URP ForwardLit pass — shader setup
// SRP Batcher requires ALL per-material data in UnityPerMaterial CBUFFER:
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST;
float _Roughness;
float _Metallic;
float _OcclusionStrength;
CBUFFER_END
Render Graph benefits: automatic aliasing of transient textures (two passes that don't overlap can share the same memory), automatic culling of passes whose outputs are never read, explicit resource lifetime tracking that prevents use-after-free hazards in async compute.
| PASS | QUEUE | WRITES | NOTES |
|---|---|---|---|
| Shadow Maps | Before Rendering | Shadow atlas | One pass per cascade, reused across frame |
| Depth Prepass | Before Rendering | Camera depth | Optional; enables SSAO, depth-of-field, Early-Z |
| SSAO | Before Rendering | Occlusion RT | Requires depth prepass; async compute in URP 17 |
| G-Buffer (Deferred) | Opaque | 4× MRT | albedo, normalWS, specular, depth |
| Deferred Lighting | Opaque | Color buffer | Screen-space light evaluation from G-Buffer |
| ForwardLit (Opaque) | Opaque | Color buffer | SRP Batcher + GPU Instancing |
| Skybox | Opaque → Transparent | Color buffer | After opaque, before transparent |
| ForwardLit (Transparent) | Transparent | Color buffer | Always forward, sorted back-to-front |
| Post-Processing | After Rendering | Color buffer | Bloom, TAA, Color Grading, DoF |
HDRP is a physically-based pipeline targeting high-end PC and current-gen console. From 2026 it enters maintenance mode — no new features, bug fixes only. All novel HDRP features (PLU, Physical Sky, SSR, real-time GI) are being ported to URP. This section documents the current stable HDRP architecture for existing projects.
| RT SLOT | FORMAT | CONTENTS |
|---|---|---|
| GBuffer0 | RGBA8 sRGB | Albedo (RGB) + Specular Occlusion (A) |
| GBuffer1 | RGBA8 | Normal WS (RGB, octahedral encoded) + Perceptual Roughness (A) |
| GBuffer2 | RGBA8 | Metallic / Coat Mask / Diffuse Profile / Anisotropy |
| GBuffer3 | RGB111110F | Baked GI (RGB) — lighting accumulation buffer |
| Depth | D32F | Hardware depth — reconstructs world position |
// HDRP custom shader — uses SurfaceDescription struct
// ShaderGraph API exposed as HLSL via CustomFunction node
void MySurface_float(
float2 uv,
out float3 Albedo,
out float Smoothness,
out float Metallic,
out float3 Normal,
out float3 Emission)
{
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
Albedo = baseMap.rgb * _BaseColor.rgb;
Smoothness = baseMap.a * _Smoothness;
Metallic = _Metallic;
Normal = UnpackNormalScale(
SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv), _NormalScale);
Emission = _EmissionColor.rgb * _EmissionIntensity;
}
// HDRP stacking layers for SSS (Skin, Foliage)
// SubsurfaceScattering material type writes a separate
// thickness map into GBuffer — lighting pass reads it for
// screen-space transmission evaluation
HDRP in 2026: maintenance mode. If starting a new project, use URP — it will receive all the features HDRP had, with better mobile/VR compatibility. Migrate existing HDRP projects to URP before Unity 6.7 LTS to stay on a supported path.
Unreal 5's three architectural pillars — Nanite, Lumen, and the Render Dependency Graph — represent a shift toward fully GPU-driven, software-defined rendering. Understanding how they interact is essential for custom shader work in UE5.
Nanite replaces the traditional mesh LOD system. Geometry is stored as a hierarchical cluster DAG. At runtime, a compute shader determines the visible cluster set for the current view, rasterizes clusters in software using hardware rasterization only as a fallback for large triangles. Result: unlimited polygon budgets — the engine automatically selects cluster detail matching approximately one cluster per screen pixel.
Nanite limitations (UE 5.4): no transparency, no World Position Offset animation (moving foliage, cloth), no masked materials, no vertex animation. These use the traditional pipeline. Check these constraints before committing assets to Nanite in production.
Lumen maintains a multi-level scene representation: Screen-Space Radiance Cache (fastest, this frame), Mesh Distance Fields (surface hits at distance), and Lumen Scene Cards (albedo/emissive/normal precomputed into atlas). Ray hits are accumulated via Software Ray Tracing against SDF, or Hardware Ray Tracing against actual triangles on supported hardware.
| MODE | HW REQUIREMENT | QUALITY | COST |
|---|---|---|---|
| Software Lumen (SDF) | Any DX11+ GPU | Good for mid-range | 2–3 ms on RTX 3080 |
| Hardware Lumen (RT) | DX12 + RT cores | High — true triangle hits | 4–6 ms on RTX 3080 |
| Screen Space only | Any | Current frame only | <1 ms — mobile fallback |
UE5's RDG is a frame-graph system equivalent to Unity's Render Graph. All rendering passes declare their resource accesses. The RDG compiles the dependency graph, inserts automatic barriers, aliases transient resource memory, and schedules async compute passes to overlap with graphics.
// UE5 RDG — custom compute pass
void FMyPass::AddPass(
FRDGBuilder& GraphBuilder,
FRDGTextureRef InputTexture,
FRDGTextureRef OutputTexture)
{
// Declare shader
TShaderMapRef<FMyComputeShader> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));
// Set shader parameters — RDG tracks resource access
FMyComputeShader::FParameters* PassParameters =
GraphBuilder.AllocParameters<FMyComputeShader::FParameters>();
PassParameters->InputTexture = InputTexture;
PassParameters->OutputTexture = GraphBuilder.CreateUAV(OutputTexture);
PassParameters->ViewUniformBuffer = View.ViewUniformBuffer;
// Add pass — RDG inserts barriers automatically
FIntVector GroupCount = FComputeShaderUtils::GetGroupCount(
FIntPoint(OutputTexture->Desc.Extent), 8);
GraphBuilder.AddPass(
RDG_EVENT_NAME("MyComputePass"),
PassParameters,
ERDGPassFlags::Compute,
[ComputeShader, PassParameters, GroupCount](FRHIComputeCommandList& RHICmdList)
{
FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader,
*PassParameters, GroupCount);
});
}
// USF shader (UE5 custom shader file)
// Must go in Engine/Shaders/Private/ or plugin Shaders/ folder
#include "/Engine/Public/Platform.ush"
RWTexture2D<float4> OutputTexture;
Texture2D InputTexture;
[numthreads(8, 8, 1)]
void MainCS(uint3 ThreadId : SV_DispatchThreadID)
{
float4 col = InputTexture.Load(int3(ThreadId.xy, 0));
OutputTexture[ThreadId.xy] = col * 0.5f;
}
Choosing a graphics API is a platform, toolchain, and team decision — not a performance one. Modern engines abstract the API layer. This matrix captures the real-world constraints that drive the choice.
| CRITERION | Vulkan 1.3 | DX12 | DX11 | Metal 3 | GL ES 3.2 |
|---|---|---|---|---|---|
| Platforms | PC/Android/Console | PC/Xbox | PC/Xbox | iOS/macOS/tvOS | Android/older iOS |
| CPU overhead | Minimal | Minimal | Medium | Minimal | High |
| Async Compute | Yes | Yes | No | Yes | No |
| Ray Tracing | KHR extension | DXR native | No | No (TBDR) | No |
| Mesh Shaders | EXT extension | SM 6.5 native | No | No | No |
| TBDR subpass | VkSubpass | No | No | Tile Shaders | GL_EXT_shader_framebuffer_fetch |
| Bindless | Yes (descriptor sets) | Yes (descriptor heaps) | Limited | Argument Buffers | No |
| Shader language | GLSL/HLSL→SPIR-V | HLSL (DXC) | HLSL (FXC) | MSL | GLSL ES |
| Driver complexity | You manage | You manage | Driver manages | Driver + hints | Driver manages |
| Development speed | Slow (verbose) | Slow (verbose) | Fast | Medium | Fast |
| PSO cache built-in | VkPipelineCache | ID3D12PipelineLibrary | No | MTLBinaryArchive | No |
Engine decision: Unity and Unreal both abstract the API layer — you write HLSL, the engine generates Vulkan/Metal/DX12/GL ES calls. You rarely choose the API directly unless writing a custom renderer or plugin. The matrix matters most for engine engineers and low-level graphics programmers.
| TOOL | PLATFORM | API | KEY CAPABILITIES |
|---|---|---|---|
| NVIDIA NSight Graphics | PC | DX11/12/Vulkan/GL | Frame debugger, shader profiler, occupancy analysis, warp divergence heatmap, memory access patterns |
| AMD RGP Radeon GPU Profiler | PCConsole | Vulkan/DX12 | Wave occupancy, cache hit rates, async compute overlap, barrier visualisation, CU utilisation timeline |
| PIX for Windows | PCXbox | DX11/12 | Frame capture, GPU timing, shader debugging, memory profiler, GPU-based validation |
| RenderDoc | All | Vulkan/DX11/12/GL | Frame capture and replay, draw call inspection, texture viewer, pipeline state, shader debugger |
| Apple Instruments / Metal Debugger | iOS/macOS | Metal | GPU timeline, shader profiler, memory graph, tile memory visualisation, dependency viewer |
| Arm Mobile Studio | Android | Vulkan/GL ES | Mali GPU counters, bandwidth analysis, fragment shader invocations vs Early-Z rate, thermal monitoring |
| Snapdragon Profiler | Android | Vulkan/GL ES | Adreno GPU counters, SP utilisation, tile rendering analysis, GPU-bound vs CPU-bound indicator |
| Meta RenderDoc + OVR Metrics | Quest | Vulkan | Frame capture on-device, GPU timing, OVR performance overlay, FFR visualisation |
| Unity GPU Profiler | All | Unity | Pass timing, render target memory, SRP Batcher efficiency, shader compilation warnings |
| Unreal Insights | All | UE5 | GPU frame timeline, RDG pass visualisation, Nanite stats, Lumen probes, memory tracking |
| RULE | PLATFORMS | IMPACT |
|---|---|---|
| Never branch on per-pixel data in an opaque shader — use branchless arithmetic or precomputed masks | All | Warp divergence → 2× fragment cost |
| Avoid discard / clip() in opaque shaders — disables Early-Z / HSR / FPK | All | Fragment shader runs on all hidden pixels |
| Use half for non-position data on mobile — genuine 2× ALU speedup | MobileQuest | 2× ALU throughput on Adreno/Mali |
| DONT_CARE for depth storeOp when depth is not read by a later pass | MobileQuest | Eliminates full-frame LPDDR5 write |
| Pack struct members largest → smallest to minimise padding | All | Constant buffer waste → cache misses |
| Replace managed array uploads with NativeArray + Burst | Unity | Eliminates GC stalls on hot paths |
| Pre-create all PSOs at load time, never mid-frame | PCConsole | Driver JIT compile = visible stutter |
| Use Single-Pass Instanced for VR — one draw call per mesh, both eyes | VR | 50% reduction in draw calls |
| Maximise async compute overlap — target 30–40% ACE utilisation on console | Console | Free GPU time during bandwidth-limited passes |
| Keep group shared memory < 32 KB per group to avoid occupancy penalty | Compute | Exceeding limit halves active groups per SM |
| Prefer wave intrinsics over groupshared reductions when operating within one wave | PC/Console | Zero sync cost vs GroupMemoryBarrier |
| Use DrawMeshInstancedIndirect with GPU culling for >1K instances | Unity | Near-zero CPU cost regardless of instance count |
Five parts, 18 chapters, one consistent theme: the hardware determines the tradeoffs, the architecture determines the cost, and the code is just the expression of that understanding.
| PART | CORE INSIGHT |
|---|---|
| Part 01 — The Machine | A GPU is a SIMT machine. Divergence costs exactly as much as the slower branch. Occupancy determines whether the hardware can hide memory latency. Different platforms have fundamentally different memory models. |
| Part 02 — The Pipeline | IMR and TBDR are not just different architectures — they require different code. Early-Z, HSR, and FPK eliminate fragments before the shader runs. Render pass design is a first-class performance concern on mobile. |
| Part 03 — The Data | Struct misalignment is silent data corruption. bool is 4 bytes on GPU. NativeArray eliminates GC. Upload patterns differ completely between PC and console — on console there is no upload, only a fence. |
| Part 04 — The Shader | PSO stutter is compilation happening at the wrong time. Variant bloat is keywords multiplying. HLSL/GLSL/MSL are the same concepts with different syntax. Compute is not a special case — it is the main case in modern GPU-driven rendering. |
| Part 05 — The Engine | Forward vs Deferred is a light scalability tradeoff. Render Graph is the correct abstraction for managing frame resources. Nanite and Lumen are specific solutions with specific constraints. Profile first, optimise second. |