Compressed Textures showed why GPU block compression matters and how Basis Universal 3,114 transcodes one file per device. The extension lists above show the other half of the story: which formats a device accepts is decided by its hardware, not by the browser. The GTX 1650, a desktop GPU, took the BC family only (s3tc, s3tc_srgb, rgtc, bptc); SwiftShader 2,546 , which decodes in software, also took ETC and ASTC, the phone formats. No renderer here offered WEBGL_compressed_texture_pvrtc, the PowerVR format of older iOS devices. Enabling matters too: gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS) stays empty until getExtension() has requested the extensions. A loader therefore enables the families it has files for, best first, and transcodes to the first that works:
const families = { astc: 'WEBGL_compressed_texture_astc', bc7: 'EXT_texture_compression_bptc',
etc2: 'WEBGL_compressed_texture_etc', bc3: 'WEBGL_compressed_texture_s3tc' }; // best first
console.log('formats before:', gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS).length);
const enabled = Object.keys(families).filter((family) => gl.getExtension(families[family]));
const count = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS).length;
const best = enabled[0] ?? 'rgba8'; // uncompressed if nothing matches
console.log(`enabled: ${enabled.join(', ')} | formats: ${count} | use ${best}`);formats before: 0 enabled: bc7, bc3 | formats: 8 | use bc7
That is the GTX 1650; SwiftShader printed enabled: astc, bc7, etc2, bc3 | formats: 46 | use astc. The KTX2 loaders of Three.js 30,815 and Babylon.js 89,603 make the same choice when they transcode Basis Universal textures (Compressed Textures).
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.wrap { position: relative; max-width: 600px; }
canvas { display: block; width: 100%; }
.lbl { position: absolute; transform: translate(-50%, -50%); font: 11px monospace; text-align: center; color: #fff; }
pre { max-width: 600px; margin: 4px 0; font: 11px monospace; }
</style>
<div class="wrap"><canvas id="c" width="600" height="220"></canvas></div>
<pre id="out"></pre>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const before = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS).length; // empty until extensions are requested
const families = [['astc', 'WEBGL_compressed_texture_astc', 'phones, Apple silicon'], ['bc7', 'EXT_texture_compression_bptc', 'desktop'],
['etc2', 'WEBGL_compressed_texture_etc', 'Android'], ['bc3', 'WEBGL_compressed_texture_s3tc', 'desktop'],
['pvrtc', 'WEBGL_compressed_texture_pvrtc', 'older iOS']]; // best first
const enabled = families.filter(([, ext]) => gl.getExtension(ext)).map(([family]) => family);
const count = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS).length;
const best = enabled[0] ?? 'rgba8'; // uncompressed if nothing matches
// One tile per family: green if this device takes it, grey if not, a gold frame round the choice.
const wrap = document.querySelector('.wrap');
gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.SCISSOR_TEST);
families.forEach(([family, ext, where], i) => {
const x = 10 + i * 118, ok = enabled.includes(family);
if (family === best) { gl.scissor(x - 5, 45, 118, 150); gl.clearColor(0.88, 0.6, 0.06, 1); gl.clear(gl.COLOR_BUFFER_BIT); }
gl.scissor(x, 50, 108, 140);
gl.clearColor(...(ok ? [0.25, 0.49, 0.23] : [0.7, 0.68, 0.64]), 1); gl.clear(gl.COLOR_BUFFER_BIT);
// a 4x4 "block" motif: the unit of block compression
for (let k = 0; k < 16; k++) {
gl.scissor(x + 22 + (k % 4) * 16, 110 + Math.floor(k / 4) * 16, 14, 14);
gl.clearColor(...(ok ? [0.35 + (k % 3) * 0.1, 0.62, 0.35] : [0.8, 0.78, 0.74]), 1); gl.clear(gl.COLOR_BUFFER_BIT);
}
wrap.insertAdjacentHTML('beforeend', `<div class="lbl" style="left:${(x + 54) / 6}%;top:${(220 - 80) / 2.2}%"><b>${family}</b><br>${ok ? 'yes' : 'no'}</div>` +
`<div class="lbl" style="left:${(x + 54) / 6}%;top:${(220 - 30) / 2.2}%;color:#333;font-size:9px">${where}</div>`);
});
document.getElementById('out').textContent =
`COMPRESSED_TEXTURE_FORMATS: ${before} before getExtension(), ${count} after\nenabled: ${enabled.join(', ') || 'none'} | use ${best}`;
</script>