GLSL float precision
On this page

Rendered from AUTHORING.md at commit 26de7be8. The package is imported here by its 0.2.0 name, typeshade.

GLSL float precision

After this page you know when to emit a GLSL stage at mediump, what that one option changes in the emitted source, and which parts of the header it leaves alone.

GLSL ES 3.00 has no implicit precision for floats, so the emitted source declares one. The backend writes precision highp float; at the top of every stage, and every float in that stage takes it. A mobile GPU pays real bandwidth and power for highp arithmetic and highp varyings, and the precision qualifier is the only lever the language gives for that, so the GLSL emit options carry a knob for it.

The floatPrecision option

emitGlslModule and emitGlslStages accept a floatPrecision value of 'highp' or 'mediump' in their options bag:

import { emitGlslModule } from 'typeshade'
const fs = emitGlslModule(m, 'fragment', { floatPrecision: 'mediump' })

'highp' is the default and it is byte-neutral: omitting the option gives the same bytes as passing it. 'mediump' moves exactly one token in the whole emit, the qualifier on the float line.

The choice happens at build time. It is an emit option, so it is not a runtime probe of the device the program will run on. If you cache emitted source, put the precision into the cache key, because a key without it can hand a mediump program back to a caller that asked for highp.

What a whole-stage default covers

mediump is roughly fp16: about three decimal digits of significand over a range of about ±65504. The default applies to the whole stage, so it covers positions, values in world units, varyings and every intermediate value in the stage, including the ones you were not thinking about when you reached for it. A value that needs more than three significant digits, a position in world units for one, does not survive. f32 itself already collapses once a value grows past its seven digits, which is why the fp64 emulation exists at all.

Use mediump for a stage whose output is a bounded, low dynamic range colour. Keep highp on a stage that computes a position, a value in world units, or an f64 value. Since the option is per emit call, a program can take one qualifier in its vertex stage and another in its fragment stage:

import { emitGlslModule } from 'typeshade'
const vertex = emitGlslModule(m, 'vertex') // positions stay highp
const fragment = emitGlslModule(m, 'fragment', { floatPrecision: 'mediump' })

emitGlslStages(m, opts) takes the same options bag and applies it to both stages, so use it when both stages want the same qualifier and you want the shared lowering it pays for once.

What stays highp

The option spells the float line and nothing else. Two other precision lines in the header are load-bearing and stay at highp under either setting.

precision highp int; is one of them. A GLSL ES 3.00 fragment shader has no default int precision at all, so the line has to be there, and both the index math that reads a storage buffer through a data texture and the integer half of a bitcast need the full int range. Lowering it would turn a bandwidth choice into a wrong result.

The sampler lines are the other. GLSL ES 3.00 predeclares a default precision for sampler2D and samplerCube only, so a module that declares a sampler2DArray, a usampler2D or an isampler2DArray gets its own precision highp <type>; line for each shape it uses, and precision highp float; does not cover them. Those lines are a compile requirement, so they keep their qualifier. A fragment stage that samples one sampler2DArray, emitted at mediump, opens with:

#version 300 es
precision mediump float;
precision highp int;
precision highp sampler2DArray;

What a CI run cannot tell you

A build can check two things about this option. The header shape is pinned in the backend’s unit tests. Whether the source compiles and links is a question for a real driver, and both settings pass it identically, apart from the one token.

The numeric effect is a different question, and a desktop rasterizer cannot answer it. One such stack reports mediump as a 10-bit format through getShaderPrecisionFormat and then computes a mediump shader at f32, so a probe that should lose a bit returns the highp answer. A GPU stack is free to do that, and a shader compiler is free to reassociate the arithmetic a precision probe uses, because GLSL ES 3.00 has no qualifier that forbids reassociation, so the two cases look the same from outside the driver. What follows is the part to carry with you: no pixel comparison in CI can distinguish a mediump emit from a highp one, so the bandwidth win, the banding mediump can introduce and the range clipping are all verifiable only on real mobile hardware.

What a build can assert is that the option changed one line and left the rest of the emit alone:

import { emitGlslModule } from 'typeshade'
const highp = emitGlslModule(m, 'fragment')
const mediump = emitGlslModule(m, 'fragment', { floatPrecision: 'mediump' })
mediump.replace('precision mediump float;', 'precision highp float;') === highp // true

So treat the option as a decision about a device you have in your hand. Emit mediump for a colour stage, look at it on the phone you are shipping to, and keep highp everywhere a coordinate flows.

Edit this page Report a problem