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.
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.
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.
// Half the warp sits idle on each branch
if (instanceID % 2 == 0) {
color = expensiveFunc(); // even active
} else {
color = cheapFunc(); // odd active
}
// Cost: expensiveFunc + cheapFunc
// 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);
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/THREAD | ACTIVE WARPS (NVIDIA SM) | OCCUPANCY | LATENCY HIDING |
|---|---|---|---|
| 16 | 128 | 100% | Excellent |
| 32 | 64 | 50% | Good |
| 64 | 32 | 25% | Marginal |
| 96+ | 16 | 12% | 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.
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
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);
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 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.
| PLATFORM | UNIT | WARP/WAVE | ALU WIDTH | KEY CONCERN |
|---|---|---|---|---|
| PC NVIDIA | SM | 32 threads | 32 FP32/SM | Register pressure, L2 reuse |
| PC AMD RDNA 3 | CU | 64 (wave64) / 32 | 64 FP32/CU | Wave size selection, occupancy |
| PS5 | CU (RDNA2) | 64 | 64 FP32/CU | ACE async queues, unified memory |
| Xbox Series X | CU (RDNA2) | 64 | 64 FP32/CU | Same as PS5, 52 CUs vs 36 |
| Mali Valhall | Shader Core | 16 | 16 FP32 | Divergence cost, mediump precision |
| Adreno 7xx | SP cluster | 128 | 128 FP32 | Native mediump ALU speed |
| Apple A17/M3 | GPU Core | 32 | 32 FP32 | Unified memory, half precision |
| VR (Quest 3) | Adreno 740 | 128 | 128 FP32 | 11ms/frame hard deadline |
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.
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.
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
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 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.
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.
| PLATFORM | MODEL | BANDWIDTH | TILE SRAM | KEY RULE |
|---|---|---|---|---|
| PC NVIDIA | Discrete VRAM | ~1 TB/s GDDR6X | No | Staging buffers; minimise PCIe; use ReBAR if available |
| PC AMD | Discrete + Infinity Cache | ~432–960 GB/s | No | Same as NVIDIA; Infinity Cache reduces effective VRAM BW requirement |
| PS5 | Unified GDDR6 | 448 GB/s (fast pool) | No | Allocate from correct pool; no upload needed — fence only |
| Xbox Series X | Unified GDDR6 | 560 GB/s (fast 10 GB) | No | Same; fast pool for GPU resources, slow pool for CPU data |
| Mali Valhall | Shared LPDDR5 + Tile | 51–68 GB/s total | ~1 MB | DONT_CARE storeOp depth; design passes to avoid tile flush |
| Adreno 7xx | Shared LPDDR5 + Tile | ~77 GB/s total | ~1 MB | Same; binning cost — minimise overdraw in vertex-heavy scenes |
| Apple A17/M3 | Unified HBM/LPDDR + Tile | 100–300 GB/s | ~1 MB | Tile Shaders (Metal); subpass equivalent for free G-Buffer reads |
| Quest 3 (XR) | Shared LPDDR5 + Tile | ~77 GB/s | ~1 MB | Single-Pass Instanced; DONT_CARE depth; 11ms hard limit |