Mobile GPU architecture
Every demo above targets the same hardware model: a tile-based renderer on a narrow, shared memory bus. That architecture, not the shading model, dictates how the shader is written.
Tile-based rendering
A desktop GPU is an immediate-mode renderer: fragments go straight to colour and depth buffers in VRAM behind a 300–1000 GB/s bus. A phone has 15–60 GB/s of LPDDR shared with the CPU, ISP and display controller, so it inverts the pipeline. Vertex processing runs first across the whole frame and bins primitives into screen tiles — 16×16 on Mali, 32×32 on PowerVR and Apple. Fragment processing then runs tile by tile with colour, depth and stencil resident in on-chip tile memory: hundreds of kilobytes of GMEM on Adreno, tens of kilobytes per tile elsewhere. The tile is resolved to main memory once.
- Blending, depth and stencil test and the MSAA resolve all happen in tile memory. 4× MSAA costs bandwidth only at resolve time — cheaper than any post-process AA.
- Hidden-surface removal (PowerVR HSR, Adreno LRZ, Mali FPK) kills occluded fragments before shading, but only for opaque draws with depth writes enabled.
- An FBO switch is a tile flush: store the current tile, load the next target. Render passes are the unit of cost, not pixels.
invalidateFramebufferon an attachment you will not read again skips its store entirely — the same thing Vulkan calls aDONT_CAREstore op.- WebGL2 exposes none of the tiler-specific extensions — no framebuffer fetch, no pixel local storage, no subpasses — so these demos optimise only what is portable: pass count, formats, precision.
Bandwidth arithmetic
1080p RGBA8 is 8.3 MB. One full-screen pass reading and writing it at 60 Hz costs 1 GB/s; the same target as RGBA32F costs 4 GB/s, out of a budget shared with the whole SoC. Hence R8 and RG8 wherever the range allows, RGBA16F only for HDR, ASTC for sampled textures, simulation targets at a fixed resolution decoupled from device pixel ratio, and analytic evaluation instead of LUT textures whenever ALU is cheaper than the fetch.
Precision and occupancy
mediump is fp16: double FMA throughput on Valhall and Adreno, half the register file per invocation. Register pressure sets occupancy — how many warps (16 threads on Valhall, 64 or 128 on Adreno, 32-wide SIMD groups on Apple) the core keeps resident to hide texture latency. Precision is a scheduling decision, not a quality one. highp goes to positions, hash inputs and texture addressing; lowp samplers are avoided outright, since a sampler is permitted to truncate the values it returns.
What breaks a tiler
discardand alpha test: coverage becomes unknown at raster time, so early-Z and HSR are disabled for the entire draw.- Transparency: blended layers cannot be culled by HSR, so overdraw is fill rate paid per layer.
- FBO ping-pong and mid-pass readback: a tile flush, plus a full pipeline stall for
readPixels.
- Dependent texture reads: coordinates derived from another fetch defeat prefetch and expose the full latency.
- Divergent control flow: a warp executes the union of its threads' branches, so an uncapped raymarcher bills its worst pixel to all of them.
- Per-frame buffer and uniform uploads: driver validation and CPU/GPU synchronisation return to the critical path.
How the demos above comply
- Paint — two passes per frame; MRT writes state and pigment together; simulation fixed at 640 px wide, independent of DPR; RGBA16F, never 32F.
- Snow — the trail is RG8 accumulated with
MAXblending: no readback, no ping-pong, no CPU-side copy of the height field. - Light — cost bounded by a 768×480 scene texture rather than the framebuffer; 32-step cap per ray to bound divergence; bloom at half resolution.
- Metal — zero texture fetches: three flake layers are integer hashes and dot products, ALU traded against bandwidth.
- Motion — no vertex or instance buffers; geometry comes from
gl_VertexIDandgl_InstanceID, particles are closed-form functions of time, so the per-frame upload is one 16-entry uniform array. - Cloth — nine 72×72 solver passes per frame with no readback; positions in RGBA32F because the integrator accumulates error, everything else in half precision.
- Clouds — half-resolution march into RGBA16F, transmittance early-out, baked 3D noise instead of per-sample hashing, and history reused only while the frame is static.