highp, mediump and lowp state the minimum range and precision a variable needs, letting a GPU use cheaper 16-bit arithmetic. The GLSL ES 3.00 specification sets the floors:
| Qualifier | Float range | Float precision | Signed int range |
|---|---|---|---|
| highp | IEEE 754 32-bit | relative 2^-24 | -2^31 to 2^31-1 |
| mediump | at least (-2^14, 2^14) | relative 2^-10 | -2^15 to 2^15-1 |
| lowp | at least (-2, 2) | absolute 2^-8 | -2^8 to 2^8-1 |
precision mediump float; sets the default for unqualified floats. The vertex shader defaults to highp; the fragment shader has no default float precision, so omitting the statement fails with "No precision specified for (float)". Ask the driver what it really provides:
for (const stage of ['VERTEX_SHADER', 'FRAGMENT_SHADER']) {
const row = ['LOW_FLOAT', 'MEDIUM_FLOAT', 'HIGH_FLOAT'].map((type) => {
const f = gl.getShaderPrecisionFormat(gl[stage], gl[type]);
return `${type} 2^${f.rangeMax} ${f.precision} bits`;
});
console.log(stage.padEnd(15), row.join(' | '));
}VERTEX_SHADER LOW_FLOAT 2^127 23 bits | MEDIUM_FLOAT 2^127 23 bits | HIGH_FLOAT 2^127 23 bits FRAGMENT_SHADER LOW_FLOAT 2^127 23 bits | MEDIUM_FLOAT 2^127 23 bits | HIGH_FLOAT 2^127 23 bits
On the GTX 1650 everything is 32-bit, so precision bugs cannot show. SwiftShader 2,546 reports mediump and lowp as 2^15 with 10 bits, a half float's shape, and phone GPUs such as Arm Mali really compute mediump in 16 bits. Use highp for positions and texture coordinates, mediump for colors, and test on a phone.
<!doctype html>
<style>
body { margin: 0; font: 12px 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; }
.names div { flex: 1; padding: 3px 0; }
table { max-width: 600px; width: 100%; border-collapse: collapse; margin-top: 4px; font-size: 11px; }
td, th { padding: 3px 5px; border-bottom: 1px solid #e3ddd0; text-align: left; }
</style>
<canvas id="view" width="600" height="200"></canvas>
<div class="names"><div>highp: 32-bit float (24-bit precision)</div><div>mediump on a phone: 16-bit float (10-bit precision)</div></div>
<table id="formats"></table>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const vs = `#version 300 es
void main() { gl_Position = vec4(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0, 0.0, 1.0); }`;
const fs = `#version 300 es
precision highp float; // the fragment shader has no default float precision
uniform bool uHalf; uniform float uTime;
out vec4 outColor;
// Round x to a 10-bit mantissa, the way a 16-bit mediump register would store it.
float asHalf(float x) {
if (!uHalf || x == 0.0) return x;
float step = exp2(floor(log2(abs(x))) - 10.0);
return round(x / step) * step;
}
void main() {
vec2 local = mod(gl_FragCoord.xy, vec2(300.0, 200.0)) - vec2(150.0, 100.0);
// A "world" coordinate far from the origin, like a texture coordinate on a big atlas.
float world = asHalf(1500.0 + length(local) * 0.25 + uTime);
float rings = 0.5 + 0.5 * sin(world * 3.0);
vec3 color = mix(vec3(0.12, 0.37, 0.55), vec3(0.93, 0.9, 0.84), rings);
outColor = vec4(color, 1.0);
}`;
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
gl.enable(gl.SCISSOR_TEST);
const uHalf = gl.getUniformLocation(program, 'uHalf');
const uTime = gl.getUniformLocation(program, 'uTime');
function frame(ms) {
gl.uniform1f(uTime, ms * 0.002);
[false, true].forEach((half, i) => {
gl.scissor(i * 300 + (i ? 4 : 0), 0, 296, 200); // left and right halves
gl.uniform1i(uHalf, half);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// Ask the driver what each qualifier really provides in each stage.
let rows = '<tr><th>stage</th><th>lowp</th><th>mediump</th><th>highp</th></tr>';
for (const stage of ['VERTEX_SHADER', 'FRAGMENT_SHADER']) {
rows += `<tr><td>${stage}</td>` + ['LOW_FLOAT', 'MEDIUM_FLOAT', 'HIGH_FLOAT'].map(type => {
const f = gl.getShaderPrecisionFormat(gl[stage], gl[type]);
return `<td>range 2^${f.rangeMax}, ${f.precision} bits</td>`;
}).join('') + '</tr>';
}
document.getElementById('formats').innerHTML = rows;
</script>