ANGLE

How ANGLE Translates Calls to Direct3D, Metal and Vulkan

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:

Printing ANGLE's translation of a GLSL fragment shaderJavaScript
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'));
Output
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).

ANGLE's back ends, with the one this browser uses highlighted and its translated shaderHTMLLive
<!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>