getExtension(name) does two jobs: it returns null when the extension is missing, and it enables the extension on that context. Listing it in getSupportedExtensions() is not enough, as this probe shows:
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, 4, 4); // a float render target
gl.bindFramebuffer(gl.FRAMEBUFFER, gl.createFramebuffer());
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
const status = () => gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
console.log('listed:', gl.getSupportedExtensions().includes('EXT_color_buffer_float'),
'| complete:', status());
const ext = gl.getExtension('EXT_color_buffer_float'); // enables it
console.log('enabled:', ext !== null, '| complete:', status(), '| same object again:',
gl.getExtension('ext_color_buffer_float') === ext, '| unknown:', gl.getExtension('WEBGL_x'));listed: true | complete: false enabled: true | complete: true | same object again: true | unknown: null
The float framebuffer was incomplete (FRAMEBUFFER_INCOMPLETE_ATTACHMENT) until the extension was requested; likewise, texParameterf() with the anisotropy constant gave INVALID_ENUM, "EXT_texture_filter_anisotropic not enabled". Names are case-insensitive, and repeated calls return the same object. The defensive pattern: request every extension you use once, in the init() that also runs after a restore (Context Restore), keep the objects, and give each feature a fallback, such as RGBA8 targets or no anisotropy, rather than refusing to run.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
.names { display: flex; max-width: 600px; text-align: center; font: 11px monospace; }
.names div { flex: 1; padding: 4px 2px; }
pre { max-width: 600px; margin: 4px 0; font: 11px monospace; white-space: pre-wrap; }
</style>
<canvas id="c" width="600" height="200"></canvas>
<div class="names"><div>fallback: RGBA8 target</div><div>after getExtension('EXT_color_buffer_float'): RGBA32F</div></div>
<pre id="out"></pre>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const log = [];
function target(format) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texStorage2D(gl.TEXTURE_2D, 1, format, 300, 200);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
const complete = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { texture, fbo, complete };
}
// Listed is not enabled: the float target stays incomplete until getExtension() is called.
const listed = gl.getSupportedExtensions().includes('EXT_color_buffer_float');
const early = target(gl.RGBA32F);
log.push(`listed: ${listed} | RGBA32F target complete before getExtension: ${early.complete}`);
const ext = gl.getExtension('EXT_color_buffer_float'); // enables it (and returns null if missing)
const late = target(gl.RGBA32F);
log.push(`enabled: ${ext !== null} | complete now: ${late.complete} | same object again (any case): ${gl.getExtension('ext_color_buffer_float') === ext} | unknown name: ${gl.getExtension('WEBGL_x')}`);
const fallback = target(gl.RGBA8); // what a defensive page uses when the extension is absent
function program(fs) {
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
out vec2 vUV; void main() { vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0; gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, fs]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(p, s);
}
gl.linkProgram(p);
return p;
}
// Write a very dark, smooth gradient (0 to 0.03), then show it brightened 30x: storage precision becomes visible.
const write = program(`#version 300 es
precision highp float; in vec2 vUV; out vec4 c; void main() { c = vec4(vec3(0.03 * vUV.x, 0.02 * vUV.x, 0.01 * vUV.y + 0.01 * vUV.x), 1.0); }`);
const show = program(`#version 300 es
precision highp float; in vec2 vUV; uniform sampler2D uImage; out vec4 c; void main() { c = vec4(texture(uImage, vUV).rgb * 30.0, 1.0); }`);
gl.bindVertexArray(gl.createVertexArray());
const pick = late.complete ? late : fallback; // feature with a fallback, never refuse to run
[[fallback, 0], [pick, 300]].forEach(([t, x]) => {
gl.bindFramebuffer(gl.FRAMEBUFFER, t.fbo); gl.viewport(0, 0, 300, 200);
gl.useProgram(write); gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(x, 0, 300, 200);
gl.useProgram(show); gl.bindTexture(gl.TEXTURE_2D, t.texture); gl.drawArrays(gl.TRIANGLES, 0, 3);
});
document.getElementById('out').textContent = log.join('\n');
</script>