GLSL perprimitiveEXT Explained: Per-Face Attributes

The perprimitiveEXT storage qualifier in GLSL is an output and input qualifier introduced with the mesh shading pipeline (GL_EXT_mesh_shader) that enables shaders to associate data directly with individual primitives, such as triangles or lines, rather than with individual vertices. This article details the purpose of perprimitiveEXT, how it functions during the mesh and fragment shading stages, and why it is more efficient for handling per-face attributes compared to traditional vertex interpolation.

The Role of perprimitiveEXT

In modern GPU pipelines utilizing mesh shaders, geometry generation is decoupled into two distinct output categories: vertex attributes (such as positions, normals, and texture coordinates) and primitive attributes (such as material indices, primitive IDs, or face normals).

The primary purpose of the perprimitiveEXT qualifier is to mark an output variable in a mesh shader—or an input variable in a fragment shader—as applying to the entire primitive rather than an individual vertex.

The Problem It Solves

In traditional vertex and geometry shading pipelines, all user-defined varying data sent to the fragment shader originates as per-vertex attributes. If an attribute applies uniformly across a single triangle (a per-face attribute, such as a face normal or a sub-mesh material index), developers historically had to:

  1. Duplicate that attribute across all three vertices of the triangle and apply the flat interpolation qualifier.
  2. Manually look up primitive data from a storage buffer (SSBO) or texture in the fragment shader using gl_PrimitiveID.

Duplicating data across vertices wastes vertex output memory, increases register pressure, and consumes valuable cache bandwidth. Looking up data dynamically in the fragment shader introduces memory latency.

The perprimitiveEXT qualifier solves this by allowing a mesh shader to output a single value per primitive, allocating storage strictly on a per-face basis.

How It Functions

  1. In the Mesh Shader (mesh): Outputs marked with perprimitiveEXT are indexed per primitive rather than per vertex. The array size corresponds to the maximum number of primitives declared in the mesh shader layout:

    #extension GL_EXT_mesh_shader : require
    
    layout(triangles, max_vertices = 64, max_primitives = 126) out;
    
    // Per-vertex output
    layout(location = 0) out vec2 v_TexCoord[];
    
    // Per-primitive (per-face) output
    perprimitiveEXT layout(location = 1) out uint v_MaterialID[];
    
    void main() {
        uint primIndex = gl_LocalInvocationIndex;
        if (primIndex < 126) {
            v_MaterialID[primIndex] = 2u; // Set per-face attribute directly
        }
    }
  2. In the Fragment Shader (frag): The fragment shader receives the per-face attribute directly as a non-array variable, requiring no interpolation:

    #extension GL_EXT_mesh_shader : require
    
    perprimitiveEXT layout(location = 1) in flat uint v_MaterialID;
    
    void main() {
        // v_MaterialID contains the value assigned to the primitive this fragment belongs to
    }

Key Advantages

  • Memory and Bandwidth Optimization: Mesh shaders write the attribute only once per primitive (e.g., once per triangle instead of three times), significantly reducing the payload size stored in on-chip memory between the mesh and fragment stages.
  • No Redundant Interpolation: The rasterizer does not perform barycentric interpolation on these attributes; the hardware routes the value directly to all fragments generated by that specific primitive.
  • Native Built-ins Support: Crucial built-in per-primitive control signals, such as gl_CullPrimitiveEXT (used for hardware primitive culling inside the mesh shader), rely on the same per-primitive execution paradigm.