compileShader() and linkProgram() return at once; the work runs in the GPU process, and the first call that needs the result (getProgramParameter(LINK_STATUS), getUniformLocation()) blocks until it is done. With KHR_parallel_shader_compile, you can instead poll COMPLETION_STATUS_KHR, which never blocks:
const parallel = gl.getExtension('KHR_parallel_shader_compile'); // absent on SwiftShader
const programs = sources.map(([vs, fs]) => startLinking(vs, fs)); // compile + link only
const done = (p) => gl.getProgramParameter(p, parallel.COMPLETION_STATUS_KHR);
while (parallel && !programs.every(done)) {
await new Promise(requestAnimationFrame); // keep animating meanwhile
}
for (const p of programs) { // no stall left now
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
}A test linked 24 physically based shaders (Normal Maps and PBR's BRDF in an 8-light loop, each with a different constant, so no cache helped). Asking for LINK_STATUS at once froze the main thread for 370 to 378 ms; polling kept it busy for about 2 ms over 10 to 21 frames, in similar wall time. Compile at load time, not on the frame that first needs a shader, and share programs between materials through uniforms.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
pre { max-width: 600px; margin: 4px 0; font: 11px monospace; }
</style>
<canvas id="c" width="600" height="240"></canvas>
<pre id="out"></pre>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const vs = `#version 300 es
uniform vec4 uRect; out vec2 vUV;
void main() { vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1); gl_Position = vec4(uRect.xy + vUV * uRect.zw, 0.0, 1.0); }`;
// Twelve different heavy fragment shaders: each has its own constant, so no compile cache helps.
const sources = Array.from({ length: 12 }, (_, k) => `#version 300 es
precision highp float; in vec2 vUV; out vec4 c;
void main() {
vec3 color = vec3(0.0);
for (int i = 0; i < 8; i++) { // eight "lights", as a PBR loop would have
vec2 p = vec2(0.5) + 0.35 * vec2(cos(float(i) * ${(0.7 + k * 0.13).toFixed(2)}), sin(float(i) * 1.3));
color += vec3(0.5 + 0.5 * cos(float(i + ${k}) + vec3(0.0, 2.0, 4.0))) * 0.02 / (0.01 + dot(vUV - p, vUV - p));
}
c = vec4(color / (1.0 + color), 1.0);
}`);
function startLinking(fsSource) { // compile + link only: no status queries yet
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fsSource]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(p, s);
}
gl.linkProgram(p);
return p;
}
const spinner = startLinking(`#version 300 es
precision mediump float; in vec2 vUV; uniform float uAngle; out vec4 c;
void main() { vec2 d = vUV - 0.5; float a = mod(atan(d.y, d.x) - uAngle, 6.2832);
if (abs(length(d) - 0.35) > 0.08) discard; c = vec4(vec3(0.12, 0.37, 0.55) * (0.3 + a / 6.2832), 1.0); }`);
const parallel = gl.getExtension('KHR_parallel_shader_compile');
const t0 = performance.now();
const programs = sources.map(startLinking);
let ready = [], frames = 0, longest = 0, lastFrame = t0, reported = false;
gl.bindVertexArray(gl.createVertexArray());
function rect(p, x, y, w, h) { gl.uniform4f(gl.getUniformLocation(p, 'uRect'), x, y, w, h); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }
function frame(now) {
longest = Math.max(longest, now - lastFrame); lastFrame = now;
// Poll without blocking; without the extension every program counts as done (and the first query stalls).
ready = programs.map((p) => !parallel || gl.getProgramParameter(p, parallel.COMPLETION_STATUS_KHR));
frames++;
gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(spinner);
gl.uniform1f(gl.getUniformLocation(spinner, 'uAngle'), now * 0.006);
rect(spinner, -0.98, -0.5, 0.4, 1.0); // keeps turning while shaders compile
programs.forEach((p, i) => {
const x = -0.52 + (i % 6) * 0.25, y = i < 6 ? 0.05 : -0.9;
if (!ready[i]) return;
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p)); // no stall left now
gl.useProgram(p); rect(p, x, y, 0.22, 0.8);
});
const done = ready.filter(Boolean).length;
if (done === programs.length && !reported) {
reported = true;
document.getElementById('out').textContent = `${parallel ? 'KHR_parallel_shader_compile: polled' : 'no KHR_parallel_shader_compile: linked on first use'}; ` +
`all ${done} programs ready after ${frames} frame(s), ${(now - t0).toFixed(0)} ms; longest frame ${longest.toFixed(0)} ms`;
} else if (!reported) document.getElementById('out').textContent = `${done} / ${programs.length} programs ready, frame ${frames}`;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>