GLSL ES 2.0 highp: Vertex vs Fragment Stage

In legacy GLSL ES 2.0, the primary difference between the vertex and fragment stages regarding highp precision is that highp support is mandatory in the vertex stage but entirely optional in the fragment stage. This architectural distinction was introduced to accommodate early mobile GPUs with limited silicon area, requiring developers to write conditional code when targeting hardware that could not process 32-bit floating-point values during rasterization.

Vertex Shader Precision Requirements

In the vertex stage, hardware implementations are strictly required to support highp floating-point and integer types. Vertex transformations rely heavily on high numerical precision to calculate accurate coordinate projections and prevent geometry jitter, vertex snapping, or matrix drift.

Because highp is guaranteed:

  • Compilers provide built-in default precisions: both float and int default to highp.
  • Explicit precision declarations are optional for standard types in vertex shaders.
  • Numerical operations typically conform to standard 32-bit IEEE 754 floating-point ranges (a minimum relative precision of \(2^{-62}\) and a dynamic range spanning at least \(2^{-62}\) to \(2^{62}\)).

Fragment Shader Precision Requirements

In the fragment stage, highp floating-point support is optional. Many early OpenGL ES 2.0 mobile GPUs saved power and die space by providing only 16-bit half-precision (mediump) arithmetic units in their fragment pipelines, reserving full 32-bit operations exclusively for the vertex pipeline.

Key behaviors in the fragment stage include:

  • No Default Float Precision: Unlike the vertex stage, the fragment stage does not specify a default precision for floating-point values. Omitting a precision declaration (such as precision mediump float;) causes a compile-time error.
  • Optional Hardware Support: An implementation can legally support only lowp and mediump for floating-point calculations within fragment code.
  • Integer Precision: highp integers are also optional in the fragment stage, defaulting to mediump if an integer precision is not declared.

Checking for Support: GL_FRAGMENT_PRECISION_HIGH

To write portable shaders that run across all compliant OpenGL ES 2.0 devices, GLSL ES 2.0 defines a preprocessor macro: GL_FRAGMENT_PRECISION_HIGH.

If an implementation supports highp in the fragment stage, the compiler sets this macro to 1. If the implementation does not support it, the macro remains undefined. Developers handle this disparity using preprocessor directives:

#ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
#else
    precision mediump float;
#endif

Practical Implications

Relying on mediump fallbacks on hardware lacking fragment highp creates several rendering challenges:

  • Banding and Truncation: Calculations involving world-space coordinates, large UV domains, or high-frequency procedural noise often display severe visual banding or precision artifacts when clamped to 16-bit floating-point limits.
  • Non-Linear Depth: Computing or reconstructing depth values inside the fragment shader often breaks down under mediump, as the 10-bit mantissa lacks the resolution needed for non-linear depth buffers.
  • Cross-Platform Portability: Desktop shaders ported directly to mobile GLSL ES 2.0 frequently fail to compile if they declare precision highp float; on devices where GL_FRAGMENT_PRECISION_HIGH is absent.