SwiftShader 2,546 is Google's open-source Vulkan and OpenGL ES implementation that runs entirely on the CPU, JIT-compiling shaders with its Subzero back end. ANGLE 2,592 targets it like any Vulkan driver, which gives WebGL to servers and CI machines without a GPU. Launched with --use-angle=swiftshader --enable-unsafe-swiftshader, the probe of Getting a Context reports the renderer ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver), vendor Google Inc. (Google), and a MAX_TEXTURE_SIZE of 8192 instead of the GPU's 16384.
The second flag is now required: Chromium 4,389 has deprecated automatic WebGL fallback to SwiftShader, whose JIT weakens the GPU process's sandbox, so expect null contexts on GPU-less machines. Browsers may mask the renderer string, a fingerprinting signal, so use it for bug reports, never to block users.
<!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%; }
.lbl { position: absolute; left: 8px; transform: translateY(-50%); font-weight: 600; }
.val { position: absolute; transform: translateY(-50%); color: #444; }
#badge { max-width: 600px; margin: 0 0 6px; padding: 6px 8px; border-radius: 4px; color: #fff; }
</style>
<div id="badge"></div>
<div class="wrap"><canvas id="view" width="600" height="200"></canvas></div>
<script>
const canvas = document.getElementById('view');
const gl = canvas.getContext('webgl2');
const info = gl.getExtension('WEBGL_debug_renderer_info'); // may be masked or absent
const renderer = info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
const software = /SwiftShader|llvmpipe|Software/i.test(renderer);
const maxTexture = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const badge = document.getElementById('badge');
badge.style.background = software ? '#b5452f' : '#3f7d3a';
badge.textContent = (software ? 'Software rendering (CPU): ' : 'Hardware GPU: ') + renderer;
// Bars are drawn with scissored clears: the scissor box limits gl.clear to a rectangle.
const bars = [
['Typical desktop GPU', 16384, [0.12, 0.37, 0.55]],
['SwiftShader (CPU)', 8192, [0.71, 0.27, 0.18]],
['This browser', maxTexture, [0.87, 0.47, 0.13]],
];
const wrap = document.querySelector('.wrap');
const scale = 400 / 16384;
function frame(ms) {
const grow = Math.min(1, ms / 900); // bars grow in over the first second
gl.disable(gl.SCISSOR_TEST);
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.SCISSOR_TEST);
for (let k = 0; k <= 4; k++) { // vertical guides every 4096 texels
gl.scissor(170 + k * 4096 * scale * 0.95, 10, 1, 180);
gl.clearColor(0.8, 0.78, 0.73, 1); gl.clear(gl.COLOR_BUFFER_BIT);
}
bars.forEach(([, size, rgb], i) => {
const y = 200 - (i + 1) * 60; // scissor y is measured from the bottom
gl.scissor(170, y, Math.min(size, 16384) * scale * 0.95 * grow, 36);
gl.clearColor(...rgb, 1); gl.clear(gl.COLOR_BUFFER_BIT);
});
if (grow < 1) requestAnimationFrame(frame);
}
bars.forEach(([name, size], i) => {
const top = ((i + 1) * 60 - 18) / 2 + '%';
wrap.insertAdjacentHTML('beforeend',
`<div class="lbl" style="top:${top}">${name}</div>` +
`<div class="val" style="top:${top};left:${(176 + Math.min(size, 16384) * scale * 0.95) / 6}%">${size}</div>`);
});
requestAnimationFrame(frame);
</script>