gl_PerVertex Redeclaration in Separable GLSL Programs

This article explores how explicit gl_PerVertex redeclaration resolves interface mismatches in OpenGL separable shader programs. In modular rendering pipelines, shaders are compiled independently rather than as a monolithic whole, which removes the driver's ability to automatically reconcile built-in stage inputs and outputs. By enforcing explicit redeclaration of the gl_PerVertex interface block, GLSL ensures that producer and consumer stages agree on exact memory layouts, preventing silent undefined behavior, structural size discrepancies, and interface validation failures during pipeline execution.

The Role of Separable Programs

In traditional OpenGL, shaders are compiled and linked together into a single monolithic program object. During this monolithic link step, the compiler analyzes every stage simultaneously. It strips out unused built-in outputs and standardizes the memory offsets between stages such as vertex, tessellation, and geometry shaders.

Separable shader programs (introduced in OpenGL 4.1 via GL_ARB_separate_shader_objects) alter this workflow. Separable programs allow stages to be compiled and linked into isolated program objects, which are then bound together dynamically at runtime using Program Pipeline Objects (glGenProgramPipelines). While this modularity increases flexibility and avoids recompilation bottlenecks, it deprives the GLSL linker of global cross-stage visibility.

The Default gl_PerVertex Discrepancy

GLSL provides built-in attributes like gl_Position, gl_PointSize, gl_ClipDistance, and gl_CullDistance through an implicitly declared interface block named gl_PerVertex.

The implicit definition of gl_PerVertex is not identical across all shader stages. For example, a vertex shader might only write to gl_Position, while a geometry or tessellation shader might natively recognize or allocate space for clip distances and point sizes. In a monolithic program, the linker inspects consumer stages, prunes unreferenced members, and aligns the underlying struct.

In a separable program, an upstream stage (such as a vertex shader) has no knowledge of what a downstream stage (such as a geometry shader) expects. If left to implicit declarations, the vertex stage might emit a gl_PerVertex layout with four floats for gl_Position, whereas the geometry stage might expect an internal structure containing space for clipping planes and point sizes. This divergence results in a structural layout mismatch across the stage interface, corrupting vertex data fetches.

How Redeclaration Enforces Compatibility

To prevent interface linking failures, the GLSL specification mandates that if any member of gl_PerVertex is used in a separable program (or if certain features like clip distances are enabled), the application must explicitly redeclare the gl_PerVertex block in all communicating stages.

An explicit redeclaration replaces the default, stage-specific built-in block with a user-defined subset:

// In the Vertex Shader (output interface)
out gl_PerVertex {
    vec4 gl_Position;
    float gl_PointSize;
};

// In the Geometry Shader (input interface)
in gl_PerVertex {
    vec4 gl_Position;
    float gl_PointSize;
} gl_in[];

The redeclaration rule eliminates ambiguity by functioning as a strict contract between stages:

  1. Exact Structural Equivalence: The GLSL linker requires that interface blocks matching by name between separate program stages contain the exact same members, declared in the exact same order, with identical qualification.
  2. Fixed Member Offsets: By explicitly listing only the active members (such as gl_Position and gl_PointSize), both programs agree on the size, stride, and byte offset of each field within the vertex data stream.
  3. Link-Time Validation: When the pipeline object executes stage interface validation (via glValidateProgramPipeline), the driver compares the explicit block signatures. Because both stages define the identical layout, validation passes without requiring cross-stage binary patching at runtime.

By removing the driver's reliance on implicit layout assumptions, the gl_PerVertex redeclaration rule guarantees predictable, high-performance data transfer across decoupled shader stages.