GLSL 16-Bit and 32-Bit Type Promotion Rules

This article outlines the rules governing implicit type promotion in GLSL when combining 16-bit and 32-bit arithmetic operands. It details how the OpenGL Shading Language resolves bit-width disparities, handles signed and unsigned integer interactions, balances floating-point and integer conversions, and applies these rules across scalar and vector operations under modern explicit type extensions.

The Explicit Type Hierarchy

In core GLSL, precision qualifiers such as lowp, mediump, and highp do not define distinct data types; they represent storage precision hints without triggering standard implicit type conversions. True 16-bit arithmetic is introduced via extensions such as GL_EXT_shader_explicit_arithmetic_types (or GL_AMD_gpu_shader_half_float), which provide concrete types including float16_t, int16_t, and uint16_t.

When these extensions are enabled, GLSL establishes a strict type conversion hierarchy based on rank:

  1. double (64-bit float)
  2. float / float32_t (32-bit float)
  3. float16_t (16-bit float)
  4. uint64_t / int64_t (64-bit integers)
  5. uint32_t / int32_t (32-bit integers)
  6. uint16_t / int16_t (16-bit integers)
  7. uint8_t / int8_t (8-bit integers)

Bit-Width Widening

When an arithmetic operation involves a 16-bit type and a 32-bit type of the same category (e.g., both floating-point or both signed integers), the operand with the smaller bit width is widened to match the 32-bit operand.

  • Floating-Point Widening: A binary operation between a float16_t and a float promotes the float16_t operand to float. The operation executes at 32-bit precision, and the resulting expression evaluates to float.
  • Same-Sign Integer Widening: An operation between int16_t and int32_t promotes the int16_t operand to int32_t. Similarly, uint16_t mixed with uint32_t promotes to uint32_t.

Mixing Signed and Unsigned Integers

When mixing signed and unsigned integer types across 16-bit and 32-bit widths, the conversion behavior depends on whether the larger type can safely represent all values of the smaller type:

  • Unsigned 16-Bit and Signed 32-Bit: A uint16_t operand mixed with an int32_t promotes directly to int32_t. Because a 32-bit signed integer can represent all possible values of a 16-bit unsigned integer (\(0\) to \(65,535\)), preserving signedness of the wider type introduces no value loss.
  • Signed 16-Bit and Unsigned 32-Bit: An int16_t operand mixed with a uint32_t promotes to uint32_t. The signed 16-bit integer is converted to a 32-bit unsigned integer, which reinterprets negative values according to standard two's complement rules.

Mixing Integers and Floating-Point Types

Floating-point types consistently take precedence over integer types regardless of bit-width differences, following the rank hierarchy:

  • 16-Bit Float and 32-Bit Integer: An operation mixing float16_t and int32_t (or uint32_t) promotes the 16-bit float to a 32-bit float (float), and converts the 32-bit integer to a 32-bit float. The operation evaluates using standard single-precision floating-point arithmetic.
  • 16-Bit Integer and 32-Bit Float: An operation mixing int16_t (or uint16_t) and float implicitly converts the 16-bit integer directly to float.

Vector and Matrix Promotion

Implicit promotions apply component-wise to vectors and matrices. If an operation occurs between a 16-bit vector (such as f16vec3) and a 32-bit vector of the same dimension (vec3), the 16-bit vector components are promoted to 32-bit floats, yielding a vec3 result.

In scalar-to-vector operations—such as adding a float scalar to an f16vec4 vector—the scalar does not demote to 16 bits. Instead, the 16-bit vector components widen to 32 bits, producing a standard 32-bit vector result (vec4).