From File to Pixel · Part 05: The Engine

pavelzosim:~/atlas_SYS.ONLINE / UTC+3

11 Forward vs Deferred Rendering

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.

Δ1 // FORWARD RENDERING

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.

Δ2 // DEFERRED RENDERING

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.

Rendering Path Decision Matrix

[ RENDERING_PATHS // FULL COMPARISON ]
CRITERIONForwardForward+DeferredDeferred+
Light scalabilityPoor O(n×l)Medium tiledGood O(pixels×l)Best clustered
G-Buffer VRAMNoneDepth only30–120 MB30–120 MB
MSAANativeNativeExpensiveTAA instead
TransparencyNativeNativeSeparate forward passSeparate forward pass
Mobile / TBDRIdealGoodSubpass onlyAvoid
VR (foveated)NativeGoodLimitedLimited
Custom shadingFull controlFull controlRequires G-Buffer slotRequires G-Buffer slot
Sub-surface scatterPer-object passPer-object passScreen-spaceScreen-space
PlatformsAllPC/Console/MobilePC/ConsolePC/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.

12 Unity URP — Render Graph Architecture

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.

Pass Structure · ScriptableRendererFeature

// 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.

[ URP_PASSES // EXECUTION ORDER Unity 6 ]
PASSQUEUEWRITESNOTES
Shadow MapsBefore RenderingShadow atlasOne pass per cascade, reused across frame
Depth PrepassBefore RenderingCamera depthOptional; enables SSAO, depth-of-field, Early-Z
SSAOBefore RenderingOcclusion RTRequires depth prepass; async compute in URP 17
G-Buffer (Deferred)Opaque4× MRTalbedo, normalWS, specular, depth
Deferred LightingOpaqueColor bufferScreen-space light evaluation from G-Buffer
ForwardLit (Opaque)OpaqueColor bufferSRP Batcher + GPU Instancing
SkyboxOpaque → TransparentColor bufferAfter opaque, before transparent
ForwardLit (Transparent)TransparentColor bufferAlways forward, sorted back-to-front
Post-ProcessingAfter RenderingColor bufferBloom, TAA, Color Grading, DoF

13 Unity HDRP — Advanced Pipeline

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.

[ HDRP_GBUFFER // DEFERRED LAYOUT ]
RT SLOTFORMATCONTENTS
GBuffer0RGBA8 sRGBAlbedo (RGB) + Specular Occlusion (A)
GBuffer1RGBA8Normal WS (RGB, octahedral encoded) + Perceptual Roughness (A)
GBuffer2RGBA8Metallic / Coat Mask / Diffuse Profile / Anisotropy
GBuffer3RGB111110FBaked GI (RGB) — lighting accumulation buffer
DepthD32FHardware 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.

14 Unreal Engine — Nanite · Lumen · RDG

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 — Software Rasterizer for Micro-Polygons

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 — Dynamic Global Illumination

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.

MODEHW REQUIREMENTQUALITYCOST
Software Lumen (SDF)Any DX11+ GPUGood for mid-range2–3 ms on RTX 3080
Hardware Lumen (RT)DX12 + RT coresHigh — true triangle hits4–6 ms on RTX 3080
Screen Space onlyAnyCurrent frame only<1 ms — mobile fallback

Render Dependency Graph (RDG)

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;
}

15 API Decision Matrix

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.

[ API_MATRIX // Vulkan · DX12 · DX11 · Metal · GL ES ]
CRITERIONVulkan 1.3DX12DX11Metal 3GL ES 3.2
PlatformsPC/Android/ConsolePC/XboxPC/XboxiOS/macOS/tvOSAndroid/older iOS
CPU overheadMinimalMinimalMediumMinimalHigh
Async ComputeYesYesNoYesNo
Ray TracingKHR extensionDXR nativeNoNo (TBDR)No
Mesh ShadersEXT extensionSM 6.5 nativeNoNoNo
TBDR subpassVkSubpassNoNoTile ShadersGL_EXT_shader_framebuffer_fetch
BindlessYes (descriptor sets)Yes (descriptor heaps)LimitedArgument BuffersNo
Shader languageGLSL/HLSL→SPIR-VHLSL (DXC)HLSL (FXC)MSLGLSL ES
Driver complexityYou manageYou manageDriver managesDriver + hintsDriver manages
Development speedSlow (verbose)Slow (verbose)FastMediumFast
PSO cache built-inVkPipelineCacheID3D12PipelineLibraryNoMTLBinaryArchiveNo

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.

16 Profiling Tools Reference

[ PROFILING_TOOLS // PLATFORM MATRIX ]
TOOLPLATFORMAPIKEY CAPABILITIES
NVIDIA NSight GraphicsPCDX11/12/Vulkan/GLFrame debugger, shader profiler, occupancy analysis, warp divergence heatmap, memory access patterns
AMD RGP
Radeon GPU Profiler
PCConsoleVulkan/DX12Wave occupancy, cache hit rates, async compute overlap, barrier visualisation, CU utilisation timeline
PIX for WindowsPCXboxDX11/12Frame capture, GPU timing, shader debugging, memory profiler, GPU-based validation
RenderDocAllVulkan/DX11/12/GLFrame capture and replay, draw call inspection, texture viewer, pipeline state, shader debugger
Apple Instruments / Metal DebuggeriOS/macOSMetalGPU timeline, shader profiler, memory graph, tile memory visualisation, dependency viewer
Arm Mobile StudioAndroidVulkan/GL ESMali GPU counters, bandwidth analysis, fragment shader invocations vs Early-Z rate, thermal monitoring
Snapdragon ProfilerAndroidVulkan/GL ESAdreno GPU counters, SP utilisation, tile rendering analysis, GPU-bound vs CPU-bound indicator
Meta RenderDoc + OVR MetricsQuestVulkanFrame capture on-device, GPU timing, OVR performance overlay, FFR visualisation
Unity GPU ProfilerAllUnityPass timing, render target memory, SRP Batcher efficiency, shader compilation warnings
Unreal InsightsAllUE5GPU frame timeline, RDG pass visualisation, Nanite stats, Lumen probes, memory tracking

What to Profile First — Decision Tree

17 Performance Rules — Quick Reference

[ PERF_RULES // UNIVERSAL ]
RULEPLATFORMSIMPACT
Never branch on per-pixel data in an opaque shader — use branchless arithmetic or precomputed masksAllWarp divergence → 2× fragment cost
Avoid discard / clip() in opaque shaders — disables Early-Z / HSR / FPKAllFragment shader runs on all hidden pixels
Use half for non-position data on mobile — genuine 2× ALU speedupMobileQuest2× ALU throughput on Adreno/Mali
DONT_CARE for depth storeOp when depth is not read by a later passMobileQuestEliminates full-frame LPDDR5 write
Pack struct members largest → smallest to minimise paddingAllConstant buffer waste → cache misses
Replace managed array uploads with NativeArray + BurstUnityEliminates GC stalls on hot paths
Pre-create all PSOs at load time, never mid-framePCConsoleDriver JIT compile = visible stutter
Use Single-Pass Instanced for VR — one draw call per mesh, both eyesVR50% reduction in draw calls
Maximise async compute overlap — target 30–40% ACE utilisation on consoleConsoleFree GPU time during bandwidth-limited passes
Keep group shared memory < 32 KB per group to avoid occupancy penaltyComputeExceeding limit halves active groups per SM
Prefer wave intrinsics over groupshared reductions when operating within one wavePC/ConsoleZero sync cost vs GroupMemoryBarrier
Use DrawMeshInstancedIndirect with GPU culling for >1K instancesUnityNear-zero CPU cost regardless of instance count

18 Series Summary

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.

[ SERIES_RECAP // FROM_FILE_TO_PIXEL ]
PARTCORE INSIGHT
Part 01 — The MachineA 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 PipelineIMR 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 DataStruct 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 ShaderPSO 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 EngineForward 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.
// FROM_FILE_TO_PIXEL // PART 05 OF 05 // CHAPTERS 11–18 // SERIES COMPLETE // EOF