Cost & precision
What makes a shader slow, what makes it wrong, and how to read the cost line on every card.
How it works
A fragment shaderThe GPU program that runs once for every pixel a triangle covers and decides its color. Fragment shader The GPU program that runs once for every pixel a triangle covers and decides its color.
What costs what
- Arithmetic is the cheapest work: additions, multiplications,
mix,dot,clamp. GPUs are built to do a lot of it in parallel. Functions likesin,exp,powandsqrtcost a few times more, and are still cheap compared with memory. - Texture reads fetch memory. The GPU hides much of the wait by working on other pixels, but every read costs bandwidth, and many reads per pixel (a wide blur, a large kernel) is the most common reason an effect is slow. A dependent read, whose coordinates come from an earlier read or from heavy math, can’t start early, and is slower on some mobile GPUs.
- Branches are cheap when neighboring pixels take the same side. GPUs run pixels in groups that execute in
lockstep, so when the pixels of a group disagree, the group runs both sides and keeps the results it needs. An
ifon a uniform is effectively free; anifon noise may cost both branches. - Loops cost every iteration. A loop of a fixed count is easy for the compiler; a loop that runs for a value computed per pixel behaves like a divergent branch.
Pixels and passes
How many pixels you shade often matters more than how you shade them.
- OverdrawShading the same pixel more than once in a frame, as overlapping transparent layers do. Each layer runs the fragment shader again.Fundamental
Overdraw
Shading the same pixel more than once in a frame, as overlapping transparent layers do. Each layer runs the fragment shader again.
. Each layer of transparent geometry runs its fragment shader again on the same pixels. Ten overlapping particles cost ten times one. Opaque geometry drawn front to back lets the depth test skip hidden pixels before shading, butdiscardand writinggl_FragDepthcan disable that early test. - Passes. A post-processing pass reads and writes a full-screen image. Each extra pass adds that bandwidth, even if its shader is trivial. Rendering a pass at half resolution cuts its pixel count to a quarter.
- Separable filters. A blur that weights a 9×9 neighborhood reads 81 texels per pixel. Splitting it into a horizontal pass of 9 and a vertical pass of 9 reads 18, at the price of one more pass. For anything wider than a few texels the split wins, which is why the Separable Gaussian blur uses two passes.
Measure, don’t guess
Compilers rearrange code, and GPUs differ, so reasoning about cost only gets you so far. A quick test: shrink the canvas
or the render resolution. If the frame time drops with it, the fragment shader is the bottleneck. Engines have
profilers (Godot’s Visual Profiler shows GPU time per stage), and WebGL 2 can time GPU work with the
EXT_disjoint_timer_query_webgl2 extension where the browser exposes it.
Each card on this site has a cost line: math operations, texture reads and passes. They are counted from the effect code, not timed, so read them as a comparison between cards, not as milliseconds. Texture reads and passes weigh more than math operations.
Precision
GLSL floats come in three precisions. highp is a 32-bit float on most hardware. mediump only guarantees a relative
precision of about 1 part in 1000 and a range of about ±16,000. Desktop GPUs compute everything at full precision
whatever you declare, so a mediump problem often appears only on phones. Declare precision highp float; unless you
have measured a reason not to; GLSL ES 3.00 requires fragment shaders to support it.
Even highp runs out. A 32-bit float has 24 bits of mantissa, so the gap between neighboring values grows with the
size of the number. A time uniform counting seconds is fine for minutes, but after a day (86,400 s) the gap is about
0.008 s, and sin(time * 10.0) visibly steps. Large world coordinates have the same problem in space. Wrap time on the
CPU into a range your animation repeats over, and keep coordinates near the origin.
NaN and infinity
Some operations have no real answer, and the GPU returns NaN (not a number) or infinity instead of failing:
sqrt(x)andlog(x)of a negative number,pow(x, y)with a negativex- division by zero, including
normalizeof a zero vector asinandacosoutside -1..1
NaN spreads through every operation it touches. In a single pixel it shows as a black or white speck; feed that pixel
into a blur or a bloom and it grows into a block. Clamp inputs where they can go out of range, for example
Output values as color Writing an intermediate value to the output color to see it, remapped to 0–1 first when needed. A shader's main debugging tool.sqrt(max(x, 0.0)) and pow(max(x, 0.0), k), and add a small epsilon before dividing by something that can reach
zero. To find where one starts, paint it a visible color (see output values as colorWriting an intermediate value to the output color to see it, remapped to 0–1 first when needed. A shader's main debugging tool.color = v × 0.5 + 0.5 (v ∈ −1..1)