The render pipeline
Everything a GPU draws takes the same conveyor belt: vertices in one end, lit pixels out the other, each station doing one job and handing the result on. Knowing the order is how you work out where a bug or a stall lives — a wrong shape is upstream, a wrong colour is downstream.
The belt
Vertex data → vertex shader → primitive assembly → clipping → rasterization → fragment shader → depth/stencil → blending → framebuffer.
- Vertex data — positions and attributes in a buffer, plus indices saying which three make a triangle.
- Vertex shader — your code, once per vertex: into clip space, attributes passed along.
- Primitive assembly — indices group vertices into triangles.
- Clipping — anything outside the view volume is cut off; a clipped triangle comes out as more triangles.
- Rasterization — hardware finds the covered pixels and emits a fragment for each, attributes interpolated across the face.
- Fragment shader — your code, once per fragment: interpolated inputs in, colour out.
- Depth/stencil test — is this fragment behind something already drawn?
- Blending — combine with what is already in the framebuffer.
Three vertices, a million fragments
Per-vertex cost scales with model complexity; per-fragment cost scales with screen area — and area is the one that bites.
One triangle filling a 1080p screen runs the vertex shader 3 times and the fragment shader two million. So a heavy vertex shader rarely matters and a heavy fragment shader always does, resolution is the most expensive setting you own, and overdraw — shading a fragment a later one covers up — is work you paid for in full and threw away. That asymmetry explains most graphics performance advice.
Draw calls and state
The GPU is fast; the conversation with it is slow.
A draw call is "with this shader, these buffers and this state, draw N primitives". Each costs CPU time to validate and submit, and changing state between calls — shader, blend mode, render target — can force the driver to rebuild a pipeline and drain work in flight. Batching merges objects that share state into one call; sorting draws by state is worth as much as cutting their number.
The modern variant
Compute shaders run arbitrary parallel work with no triangles involved; mesh shaders replace the vertex stage with a program that emits small batches of primitives directly. Both are escapes from the fixed belt, not replacements for understanding it — rasterization, depth and blending are unchanged downstream.
Demo
One triangle stepping through every stage — auto-advance through the stages, then drag the pixel-size slider and watch the fragment counter against the stubbornly constant "3 vertices".
Gotchas
| Symptom | Cause |
|---|---|
| Frame rate collapses when the camera moves closer | Fill-bound: the same geometry now covers far more fragments |
| Halving the resolution nearly doubles the frame rate | Confirms you are fragment-bound, not vertex- or CPU-bound |
| 10k objects crawl despite trivial geometry | Draw-call bound on the CPU — batch, instance, or merge |
| Big win from drawing opaque objects front-to-back | You were paying for overdraw; early-z can now reject most of it |
| Stall right after a shader or blend-state change | Pipeline rebuild mid-frame; sort draws by state |
| Geometry disappears at the screen edge instead of being cut | Clipping is fine, your near plane or guard-band assumption is not |