Chrome 1 , Edge and Firefox 555 on Windows route WebGL through ANGLE 2,592 (Almost Native Graphics Layer Engine), Google's open-source layer (chromium.googlesource.com/angle/angle (https://chromium.googlesource.com/angle/angle 2,592 )); WebGL on other platforms uses at least its shader translator. ANGLE validates every call and translates GLSL for its back ends: Direct3D 11 (Chrome's default on Windows), Direct3D 9, Metal, Vulkan and OpenGL. WEBGL_debug_shaders shows it:
const gl = document.createElement('canvas').getContext('webgl2');
const shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shader, `#version 300 es
precision highp float; uniform vec3 uColor; out vec4 outColor;
void main() { outColor = vec4(uColor * 0.5, 1.0); }`);
gl.compileShader(shader);
const translated = gl.getExtension('WEBGL_debug_shaders').getTranslatedShaderSource(shader);
console.log(translated.split('\n').filter((line) => /Color|ctor/.test(line)).join('\n'));float4 vec4_ctor(float3 x0, float x1)
uniform float3 _uColor : register(c0);
static float4 out_outColor = {0, 0, 0, 0};
(out_outColor = vec4_ctor((_uColor * 0.5), 1.0));That is HLSL for Direct3D 11: vec4 became float4, the uniform went to constant register c0, and names gained an underscore prefix. Under SwiftShader 2,546 (SwiftShader) the same call returns SPIR-V for Vulkan, hex words starting with SPIR-V's magic number 0x07230203. Validation turns mistakes into error codes, not crashes: drawing with no program makes gl.getError() return gl.INVALID_OPERATION (1282).
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.wrap { position: relative; width: 100%; max-width: 600px; }
canvas { display: block; width: 100%; }
.box { position: absolute; transform: translate(-50%, -50%); text-align: center; font-weight: 600; }
pre { max-width: 600px; margin: 6px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd;
font-size: 11px; white-space: pre-wrap; word-break: break-all; max-height: 130px; overflow: auto; }
</style>
<div class="wrap"><canvas id="view" width="600" height="230"></canvas></div>
<pre id="out"></pre>
<script>
const canvas = document.getElementById('view');
const gl = canvas.getContext('webgl2');
function compile(vsSource, fsSource) {
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vsSource], [gl.FRAGMENT_SHADER, fsSource]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src.trim()); gl.compileShader(s);
gl.attachShader(p, s);
}
gl.bindAttribLocation(p, 0, 'aPos');
gl.linkProgram(p);
return p;
}
// Before any program exists, a draw call is rejected by ANGLE's validation, not crashed.
gl.drawArrays(gl.TRIANGLES, 0, 3);
const errorCode = gl.getError(); // 1282 = INVALID_OPERATION
// The book's shader: ask ANGLE how it translated it for the native back end.
const fsSource = `#version 300 es
precision highp float; uniform vec3 uColor; out vec4 outColor;
void main() { outColor = vec4(uColor * 0.5, 1.0); }`;
const program = compile(`#version 300 es
in vec2 aPos; uniform vec4 uRect;
void main() { gl_Position = vec4(uRect.xy + aPos * uRect.zw, 0.0, 1.0); }`, fsSource);
const debugShaders = gl.getExtension('WEBGL_debug_shaders');
const renderer = (() => {
const info = gl.getExtension('WEBGL_debug_renderer_info');
return info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
})();
const backEnds = [['Direct3D 11', /D3D11|Direct3D11/i], ['Direct3D 9', /D3D9|Direct3D9/i],
['Metal', /Metal/i], ['Vulkan', /Vulkan/i], ['OpenGL', /OpenGL(?! ES)/i]];
const active = backEnds.findIndex(([, re]) => re.test(renderer));
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, 1,1, -1,-1, 1,1, -1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
const uRect = gl.getUniformLocation(program, 'uRect');
const uColor = gl.getUniformLocation(program, 'uColor');
// uColor is halved by the shader, so pass twice the colour we want to see
function box(cx, cy, w, h, rgb) { // centre and size in clip space
gl.uniform4f(uRect, cx, cy, w / 2, h / 2);
gl.uniform3f(uColor, rgb[0] * 2, rgb[1] * 2, rgb[2] * 2);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function line(x0, y0, x1, y1, rgb) { // a thin box between two points
const steps = 120;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
box(x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, 0.012, 0.03, rgb);
}
}
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
const wrap = document.querySelector('.wrap');
function label(text, cx, cy, color) {
const div = document.createElement('div');
div.className = 'box'; div.textContent = text; div.style.color = color;
div.style.left = (cx + 1) * 50 + '%'; div.style.top = (1 - cy) * 50 + '%';
wrap.appendChild(div);
}
box(0, 0.68, 0.7, 0.3, [0.27, 0.27, 0.3]); label('WebGL calls + GLSL', 0, 0.68, '#fff');
box(0, 0.12, 1.0, 0.34, [0.12, 0.37, 0.55]); label('ANGLE: validate + translate', 0, 0.12, '#fff');
line(0, 0.53, 0, 0.29, [0.4, 0.4, 0.4]);
backEnds.forEach(([name], i) => {
const x = -0.8 + i * 0.4;
const on = i === active;
line(0, -0.05, x, -0.52, on ? [0.87, 0.47, 0.13] : [0.7, 0.7, 0.7]);
box(x, -0.66, 0.34, 0.26, on ? [0.87, 0.47, 0.13] : [0.85, 0.83, 0.78]);
label(name, x, -0.66, on ? '#fff' : '#555');
});
const translated = debugShaders
? debugShaders.getTranslatedShaderSource(gl.getAttachedShaders(program)[1])
: '(WEBGL_debug_shaders is not exposed in this browser)';
document.getElementById('out').textContent =
`Renderer: ${renderer}\ndrawArrays with no program -> getError() = ${errorCode}\n\n` +
'Translated fragment shader (lines mentioning Color):\n' +
translated.split('\n').filter(l => /Color|ctor|float4|0x/.test(l)).slice(0, 12).join('\n');
</script>