From File to Pixel · Part 01: The Machine

pavelzosim:~/atlas_SYS.ONLINE / UTC+3

01 GPU Execution Model

Understanding what a GPU actually is — at the hardware level — is the prerequisite for every performance decision in shader code. The execution model explains why divergence is expensive, why occupancy matters, and why the same code behaves differently on different platforms.

SIMT, Warps, Wavefronts — Universal Foundation

A GPU is not a fast CPU. It is a massively parallel SIMT machine — Single Instruction, Multiple Threads. Instead of one powerful core executing a sequence of instructions, a GPU runs thousands of lightweight threads simultaneously, all executing the same instruction stream on different data.

The execution unit is called a Streaming Multiprocessor (SM) on NVIDIA, a Compute Unit (CU) on AMD, and a Shader Core on mobile/Apple GPUs. Each SM/CU executes threads in groups called warps (NVIDIA, 32 threads) or wavefronts (AMD, 64 or 32 threads). All threads in a warp execute the same instruction at the same clock cycle — on different data.

Warp Divergence

This has one critical consequence: branching is expensive when threads disagree. If half a warp takes the if path and half takes else, the hardware executes both paths sequentially with inactive threads masked. You pay for both branches.

Δ1 // DIVERGENT — expensive
// Half the warp sits idle on each branch
if (instanceID % 2 == 0) {
    color = expensiveFunc(); // even active
} else {
    color = cheapFunc();     // odd active
}
// Cost: expensiveFunc + cheapFunc
Δ2 // COHERENT — preferred
// All threads take the same branch
// → sort draws by type, batch by material
// Or: branchless select
float t = (float)(instanceID % 2);
color = lerp(cheapFunc(),
             expensiveFunc(), t);

Occupancy

Each SM has a fixed register pool (~65 536 × 32-bit on NVIDIA Ampere). The number of threads that can be active simultaneously is determined by register usage per thread. Low occupancy means the GPU cannot hide memory latency by switching to another warp while one waits for data.

REGISTERS/THREADACTIVE WARPS (NVIDIA SM)OCCUPANCYLATENCY HIDING
16128100%Excellent
326450%Good
643225%Marginal
96+1612%Poor — latency visible

Occupancy is not always the bottleneck. A shader with high instruction-level parallelism (ILP) can hide latency at 25% occupancy. Always measure with NSight / RGP / Snapdragon Profiler before reducing register count at the cost of code clarity.

Platform Differences

PC NVIDIA vs AMD — warp size differences

NVIDIA Turing+: Warp = 32 threads. Independent Thread Scheduling — each thread has its own program counter. Divergent warps can interleave execution, reducing stalls. SM count: 84 (RTX 4090) at ~2.5 GHz.

AMD RDNA 2/3: Wavefront = 64 threads by default (wave64), but wave32 is selectable per-shader at compile time. Wave32 gives better occupancy for small workgroups but halves SIMD utilisation per wavefront. For compute shaders doing bitwise reductions with WaveActiveBitOr(), the wave size directly affects the reduction algorithm.

// HLSL DXC — force wave32 on AMD (DX12/Vulkan)
[WaveSize(32)]
[numthreads(32, 1, 1)]
void CSReduce(uint3 tid : SV_DispatchThreadID) { ... }

// Query wave size at runtime
uint lane = WaveGetLaneIndex(); // 0..WaveGetLaneCount()-1

Console PS5 / Xbox — RDNA 2, ACE queues

Both consoles use AMD RDNA 2 CUs with wave64. PS5: 36 CUs at 2.23 GHz. Xbox Series X: 52 CUs at 1.825 GHz. The key console advantage is Asynchronous Compute Engines (ACE) — dedicated hardware queues that run compute workloads in parallel with the main graphics queue, filling CUs that would otherwise idle during bandwidth-limited raster passes.

// PS5 PSSL — async compute dispatch
// Main queue: rendering G-Buffer
// ACE queue (simultaneously): particle simulation
// No CPU involvement after initial submission
sce::Gnmx::ComputeContext asyncCtx;
asyncCtx.dispatch(particles.groupsX, 1, 1);

Mobile Mali / Adreno / Apple — smaller, power-constrained

ARM Mali Valhall: Uses SIMD width of 16. Divergence in small SIMDs is proportionally more expensive than on desktop. Qualcomm Adreno: SP clusters with 128 ALUs per SP, natively accelerates mediump arithmetic. Apple A/M-series: 32-thread SIMD groups; half in MSL is genuinely faster, not a hint — the compiler promotes to float16 on ALUs.

Mobile rule: avoid divergence more aggressively than on desktop. With fewer ALUs and tighter thermal budgets, divergence cost is disproportionately high. Prefer branchless arithmetic and uniform control flow.

VR / XR Two eyes — doubled thread count, halved time budget

VR does not change the GPU execution model — warps still execute the same way. What changes is the time budget: at 90 Hz you have 11.1 ms per frame total, at 120 Hz just 8.3 ms. Every occupancy and divergence concern is amplified.

Execution Model — Platform Comparison

[ EXECUTION_MODEL // PLATFORM_MATRIX ]
PLATFORMUNITWARP/WAVEALU WIDTHKEY CONCERN
PC NVIDIASM32 threads32 FP32/SMRegister pressure, L2 reuse
PC AMD RDNA 3CU64 (wave64) / 3264 FP32/CUWave size selection, occupancy
PS5CU (RDNA2)6464 FP32/CUACE async queues, unified memory
Xbox Series XCU (RDNA2)6464 FP32/CUSame as PS5, 52 CUs vs 36
Mali ValhallShader Core1616 FP32Divergence cost, mediump precision
Adreno 7xxSP cluster128128 FP32Native mediump ALU speed
Apple A17/M3GPU Core3232 FP32Unified memory, half precision
VR (Quest 3)Adreno 740128128 FP3211ms/frame hard deadline

02 Memory Hierarchy

Every GPU architecture organises memory in the same conceptual hierarchy. The physics is identical: SRAM is fast and small, DRAM is slow and large. Your job as a shader engineer is to keep hot data as high in this hierarchy as possible — every step down costs an order of magnitude in latency.

Universal Hierarchy: Registers → VRAM → PCIe

Registers
~20 TB/s · 1 cycle
Shared / Tile Mem
~10 TB/s · 20 cy
L1 / Tex Cache
~5 TB/s · 30 cy
L2 Cache
~4 TB/s · 100 cy
VRAM / GDDR6X
~1 TB/s · 400 cy
System RAM (PCIe)
~16 GB/s · huge

Registers are private per-thread. Every local variable in your shader becomes a register — or spills to L1 if you run out. Shared / Group memory is explicitly managed on-chip SRAM, shared between threads in the same workgroup. L1 is the texture cache (2D-spatial) + general data cache. L2 is shared across all SMs. VRAM is where your buffers and textures live permanently. System RAM connects via PCIe — the bottleneck for streaming data.

Platform Memory Models

PC Discrete GPU — separate VRAM, PCIe bottleneck

VRAM is physically separate from system RAM. Every resource must be uploaded across PCIe before the GPU can use it. PCIe 4.0 x16 delivers ~32 GB/s theoretical, ~16–24 GB/s practical. GDDR6X VRAM delivers ~1 TB/s. This 56× gap is the primary architecture constraint for streaming and instancing.

NVIDIA Ada Lovelace (RTX 40xx) expanded L2 to 96 MB — large enough to hold G-Buffer attachments in cache, significantly reducing VRAM bandwidth in deferred pipelines. RDNA 3 uses Infinity Cache (up to 96 MB on 7900 XTX) as L3-equivalent.

Resizable BAR (ReBAR) / Smart Access Memory (SAM): when enabled, the entire VRAM is mapped into CPU address space. CPU can write directly to any VRAM address — eliminates the staging buffer pattern for per-frame data.

// Without ReBAR: two-copy upload
memcpy(stagingBuffer.mappedPtr, data, size); // CPU RAM
vkCmdCopyBuffer(cmd, staging, deviceLocal, ...); // staging → VRAM via DMA

// With ReBAR: one copy, direct to VRAM
memcpy(barMappedVRAM, data, size); // CPU writes directly into VRAM

Console PS5 / Xbox — unified 448–560 GB/s, no PCIe

CPU and GPU share one physical GDDR6 pool. There is no PCIe bus, no staging buffer, no upload pass. An allocation made by the CPU is immediately accessible by the GPU at the same address. The only synchronisation needed is a timeline fence.

PS5 splits its 16 GB pool: 5.5 GB at 448 GB/s (fast, GPU-preferred) and 3.5 GB slower for CPU-heavy data. Xbox Series X: 10 GB at 560 GB/s + 6 GB at 336 GB/s. Allocating the wrong pool type is a common console-specific performance bug.

// PS5 GNM — allocate from fast GPU pool ("garlic")
sce::Gnm::SizeAlign sa = sce::Gnm::computeDataBufferSizeAlign(count, stride);
void* ptr = garlic.allocate(sa); // fast GDDR6 pool
// CPU writes, GPU reads — same pointer, no copy
memcpy(ptr, instanceData, size);

On console, the "how do I minimise uploads?" question disappears — there is no PCIe. The bottleneck shifts to GPU execution and memory bandwidth within the unified pool. Tight packing and cache-friendly access patterns matter more than upload size.

Mobile Shared system RAM + on-chip tile SRAM — TBDR

Mobile GPUs have no dedicated VRAM. They share LPDDR5 system RAM with the CPU — typically 51–77 GB/s bandwidth vs 1 TB/s on desktop discrete. Every VRAM access is an LPDDR5 access.

The architectural response is Tile-Based Deferred Rendering (TBDR). The screen is divided into tiles (~16×16 px). Each tile is rendered entirely within fast on-chip tile SRAM (~256 KB–1 MB) before being written to system memory. Depth, stencil, and intermediate colour data never leave the chip during tile rendering. This makes framebuffer reads within a render pass free — and framebuffer breaks between passes catastrophically expensive.

The single most important mobile performance rule: design your render passes so that intermediate attachments (depth, G-Buffer components) stay on-chip and are never stored to system RAM unless explicitly needed downstream. A single unexpected storeOp = STORE on depth adds measurable frame time.

VR / XR PC VR vs standalone — two very different memory models

PC VR (PCVR): Same discrete VRAM model as desktop — the headset is a display, not a compute target. Eye textures (~2064×2096 per eye on Quest Link) multiply VRAM pressure: two render targets, two depth buffers, two full G-Buffers if deferred. Use Single-Pass Instanced rendering to halve draw calls.

Standalone VR (Quest 3): Adreno 740 with LPDDR5 shared memory. Full TBDR architecture applies. 11 ms/frame at 90 Hz. Eye textures stored in shared LPDDR5 — framebuffer breaks between eyes require two LPDDR5 writes instead of zero if Single-Pass Instanced is used correctly.

Memory Comparison

[ MEMORY_MODEL // PLATFORM_COMPARISON ]
PLATFORMMODELBANDWIDTHTILE SRAMKEY RULE
PC NVIDIADiscrete VRAM~1 TB/s GDDR6XNoStaging buffers; minimise PCIe; use ReBAR if available
PC AMDDiscrete + Infinity Cache~432–960 GB/sNoSame as NVIDIA; Infinity Cache reduces effective VRAM BW requirement
PS5Unified GDDR6448 GB/s (fast pool)NoAllocate from correct pool; no upload needed — fence only
Xbox Series XUnified GDDR6560 GB/s (fast 10 GB)NoSame; fast pool for GPU resources, slow pool for CPU data
Mali ValhallShared LPDDR5 + Tile51–68 GB/s total~1 MBDONT_CARE storeOp depth; design passes to avoid tile flush
Adreno 7xxShared LPDDR5 + Tile~77 GB/s total~1 MBSame; binning cost — minimise overdraw in vertex-heavy scenes
Apple A17/M3Unified HBM/LPDDR + Tile100–300 GB/s~1 MBTile Shaders (Metal); subpass equivalent for free G-Buffer reads
Quest 3 (XR)Shared LPDDR5 + Tile~77 GB/s~1 MBSingle-Pass Instanced; DONT_CARE depth; 11ms hard limit
// FROM_FILE_TO_PIXEL // PART 01 OF 05 // CHAPTERS 01–02 // EOF