GLSL debugging and live recovery: gotchas, error reading, and the panic shader
Learning objectives
- learner can port a Shadertoy shader to glslViewer by replacing all incompatible uniform names and the entry-point signature
- learner can identify and fix GLSL type-system and syntax errors — int/float mixing, integer division truncation, wrong vector constructor arity, undeclared uniforms, uniform-bounded loops on ES 1.00, mismatched #version syntax, and missing main()/gl_FragColor — from the compiler's error line before a set
- learner can diagnose a black, blown-out, or visually-degraded frame by tracing the likely cause (NaN from undefined math, clamped channel, unbounded/mediump time precision loss, feedback gain ≥ 1, an unfilled feedback buffer, or step-edge aliasing) and apply the targeted fix (smoothstep+fwidth, highp/bounded time, #ifdef guard, or gain < 1)
- learner can recover from any GLSL compile or runtime failure mid-set using the error-line/bisect procedure and the canonical panic shader
Capstone — one whole task that evidences the objectives
Port and repair a deliberately broken glslViewer shader, then recover from it live. The shader ships with every fault class seeded at once: Shadertoy uniforms and entry-point (iTime/iResolution/iMouse/mainImage) that must be replaced with rig names and a u_mouse pixel-to-0..1 normalization; a missing precision block plus mediump banding in large u_time / fine gradients; the full strict-typing set — an int/float mixed expression, an integer-division truncation, a wrong-arity vector constructor, an undeclared uniform, a for-loop bounded by a uniform on ES 1.00, mixed #version syntax, and an absent void main()/gl_FragColor; a hard step() edge that aliases; and the runtime failures — a NaN-producing pow()/normalize call that renders black, a feedback pass whose render code lacks its #ifdef guard so the buffer never fills, and a feedback gain ≥ 1 that blows to white. Diagnose and fix each fault reading only the compiler error line (bisecting to a known-good body when the message is ambiguous) and the visual frame — relying on the fact that glslViewer keeps the last good frame while you work — replacing the step edge with smoothstep+fwidth and bounding u_time so precision holds over a long set. Confirm the rig has no audio bridge, so all motion is driven by u_time, then swap in the canonical cosine-palette panic shader from memory in under thirty seconds: it must compile cleanly, define main()/gl_FragColor, output a clamped colour, and loop correctly over unbounded u_time.
Prerequisite modules
Live visual coding with glslViewer in this rig means one thing above all else: you are one bad save away from a compile error, and the audience sees the last good frame while you race to fix it. That safety net is the first thing to internalise — glslViewer never swaps in a broken shader — and the second thing is the repair workflow: read the line number, fix the named token, and bisect if the message is unclear. This module builds the reflex for both halves: the preventive knowledge that keeps you from landing in trouble, and the recovery procedures that get you out when you do.
The preventive layer covers two clusters of failure. The first is the rig-incompatibility cluster: Shadertoy shaders look runnable but every uniform name is wrong (iTime → u_time, iResolution → u_resolution, iMouse → u_mouse, mainImage → main() writing gl_FragColor), the mouse uniform is in pixels rather than 0..1, and the rig has no audio bridge — a.fft simply does not exist, so any reactivity must come from u_time. The second is the GLSL type-system cluster: the compiler is strict in ways JavaScript is not — every float literal needs a decimal point, integers cannot appear in float expressions without a cast, integer division silently truncates, vector constructors require the exact right number of components, uniforms must be declared before use, every shader must assign gl_FragColor from a void main(), and #version syntax must be internally consistent.
The runtime failure layer is about pixels that compile but look wrong: NaN from pow(negative, x), 1.0/0.0, or normalize(vec3(0)) spreads black clusters; feedback buffers without their #ifdef guard never fill; gain ≥ 1 in a double-buffer loop saturates to white within frames; unbounded u_time in float32 loses precision over minutes, creating jitter in periodic animations; and hard step() edges alias while smoothstep with fwidth is the per-pixel-correct alternative.
The arc runs from gotcha inventory to drill to live repair. Work through each failure category on a scaffold shader: first introduce and fix the Shadertoy porting errors on a known-good template, then mutate the type system one error at a time and read the compiler messages, then exercise the runtime failures (feedback, NaN, precision) and read the frame. The L5 atoms arrive in the second half: practice the error-line/bisect procedure on an ambiguous message, then memorise and type the panic shader from scratch — not copy-paste, from memory — until it takes under thirty seconds. The capstone is a gauntlet that presents all five fault classes at once: you diagnose from error messages and visual evidence alone, fix each in situ, then execute the panic-shader swap as if the set were live.
Supporting atoms for grounding: glsl-standard-uniforms is the authoritative list of what glslViewer provides without declaration; glsl-float-decimal-requirement and glsl-coordinate-normalization are from the prerequisite module but recur constantly in debugging sessions — having them here as supporting gives you fast look-up context without re-gating the capstone on material already drilled.
Walkthrough
Porting a shader from Shadertoy into a GLSL ES 1.00 rig fails in a dozen predictable ways — most are compile errors, so nothing renders. This lesson walks the fault classes as fixes: each fence is the corrected shader that compiles and renders. Paste each into The Book of Shaders editor (WebGL1 / GLSL ES 1.00). The rig provides u_time, u_resolution, u_mouse.
1 — the reset shader (memorise it). Your panic patch: a shader that always compiles and always shows a gradient. When a port won’t compile mid-set, paste this and rebuild from a known-good body ([[glsl-bisect-to-known-good-body]]).
precision mediump float;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = vec4(uv.x, uv.y, 0.5, 1.0); // always compiles, always shows
}
2 — port the Shadertoy names. Shadertoy uses iTime/iResolution/mainImage(out vec4 fragColor, in vec2 fragCoord); the rig uses u_time/u_resolution, a plain main(), and writes gl_FragColor. Rename them and it renders ([[glsl-shadertoy-uniform-names-incompatible]], [[glsl-mouse-uniform-in-pixels]]).
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution; // was fragCoord/iResolution
float v = 0.5 + 0.5 * sin(u_time + uv.x * 6.0); // was iTime
gl_FragColor = vec4(vec3(v), 1.0); // was fragColor
}
3 — strict typing. ES 1.00 will not implicitly convert: float literals need a decimal (1.0, not 1), you can’t mix int and float in an expression, and vector constructors need exact arity. All correct here — one missing dot is a compile failure ([[glsl-int-float-strict-typing]], [[glsl-vector-constructors-exact]]).
precision mediump float;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float d = length(uv - 0.5); // 0.5 is a float literal, not 0
vec3 col = vec3(1.0 - d); // vec3(float) fills all three components
gl_FragColor = vec4(col, 1.0);
}
4 — constant loop bounds. ES 1.00 forbids looping to a uniform or variable limit — the bound must be a compile-time constant. Use a constant max and break early for dynamic behaviour ([[glsl-loop-constant-bounds-es1]]).
precision mediump float;
uniform vec2 u_resolution;
uniform float u_time;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float sum = 0.0;
for (int i = 0; i < 16; i++) { // constant bound
if (float(i) > 8.0 + 4.0 * sin(u_time)) break; // dynamic early-out instead
sum += 0.06;
}
gl_FragColor = vec4(vec3(sum), 1.0);
}
5 — the debug move: visualise the value. No breakpoints on a GPU — so output the suspect value as colour and look at it. Here we render the distance field d directly to confirm it’s in 0..1 before we shape it. Seeing the intermediate is how you find the bug ([[glsl-nan-from-undefined-math]]).
precision mediump float;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float d = length(uv - 0.5);
gl_FragColor = vec4(vec3(d), 1.0); // render `d` as brightness to SEE its range
}
6 — the repaired, debuggable shader (the capstone). All faults fixed: precision block present, rig uniforms, mouse normalised from pixels, strict types, smoothstep (not aliased step) for the edge. A clean port that compiles, renders, and grades ([[glsl-mediump-banding]], [[glsl-step-aliasing-smoothstep]]):
precision mediump float; // present → no compile fail
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec2 m = u_mouse / u_resolution; // normalise mouse: pixels → 0..1
float d = length(uv - 0.5);
float ring = smoothstep(0.02, 0.0, abs(d - 0.3 - 0.1 * sin(u_time))); // anti-aliased edge
vec3 col = mix(vec3(0.1, 0.2, 0.4), vec3(1.0, 0.8, 0.3), ring);
gl_FragColor = vec4(col, 1.0);
}
What good looks like. A calm GLSL recovery reads the compile error line, maps it to a fault class (type, arity, loop bound, uniform name), and fixes in place — or bisects to the reset shader and rebuilds. The tell of a struggling coder is randomly adding dots and casts hoping it compiles. Read the error, know the class. And when the maths goes NaN (black or garbage), visualise the suspect value as colour. (Skill map: live-visualist Domain E — debug a shader live by outputting an intermediate value; keep a reset patch ready.)
Now make it yours. Break each fix on purpose (drop the precision line, use step for the ring, loop to a uniform) and read the exact compile error each produces. Build a mix-based cosine palette as a second reset shader. Practise bisecting: comment the body down to gl_FragColor = vec4(uv, 0.5, 1.0) and add back until it breaks.
Atoms in this module
Required — these gate the capstone
Supporting — enrichment, not gating
Part of curricula
- Generative & AI AV Artist — real-time machine-driven performance — Orient the machine collaborator & ship a first ML AV artefact recommended
- Live Visualist — zero to performing live-coded & generative visuals — Perform the set — live-coded, generative, audio-reactive visuals for an audience required
- Shader Artist — real-time GPU craft to a demoscene-grade visual — The demoscene-grade piece: pipeline, reactivity, and release recommended