GLSL perprimitiveEXT Qualifier Explained

The perprimitiveEXT qualifier in GLSL designates output variables in mesh shaders as per-primitive rather than per-vertex attributes. Introduced primarily through the GL_EXT_mesh_shader extension for Vulkan, this qualifier enables developers to associate unique data—such as primitive IDs, face normals, or material indices—directly with an entire primitive (e.g., a triangle, line, or point). Instead of interpolating values across vertices or relying on flat-shaded provoking vertices, the GPU natively assigns these attributes to the specified primitive, which can then be read directly by the downstream fragment shader.

Syntax and Declaration

In a mesh shader, attributes are per-vertex by default. To assign data to a primitive as a whole, the perprimitiveEXT layout or type qualifier must precede the out declaration.

#version 450
#extension GL_EXT_mesh_shader : require

layout(local_size_x = 32) in;
layout(triangles, max_vertices = 64, max_primitives = 126) out;

// Per-vertex output
layout(location = 0) out vec3 v_Position[];

// Per-primitive output
perprimitiveEXT layout(location = 1) out uint p_MaterialID[];
perprimitiveEXT layout(location = 2) out vec3 p_FaceNormal[];

perprimitiveEXT can also be applied to output interface blocks:

perprimitiveEXT out PrimitiveData {
    uint materialID;
    vec3 faceNormal;
} primOut[];

Array Sizing and Indexing

Outputs declared with perprimitiveEXT must be arrays sized according to the max_primitives layout qualifier specified in the mesh shader.

During execution, each element of the array corresponds to the primitive at that specific index. For example, if a mesh shader thread generates a triangle and writes its indices to gl_PrimitiveTriangleIndicesEXT[primIndex], the thread can assign corresponding primitive data by indexing into the per-primitive array using the same primIndex:

uint primIndex = gl_LocalInvocationIndex;
if (primIndex < maxPrimitivesCount) {
    p_MaterialID[primIndex] = 2u;
    p_FaceNormal[primIndex] = vec3(0.0, 1.0, 0.0);
}

Fragment Shader Consumption

When data tagged with perprimitiveEXT reaches the fragment shader, it is received without perspective division or attribute interpolation across the primitive's surface.

In the fragment shader, the corresponding input must be declared with the perprimitiveEXT qualifier (or implicitly read as a flat primitive value, depending on the shading language environment and driver support):

#version 450
#extension GL_EXT_mesh_shader : require

perprimitiveEXT layout(location = 1) in uint p_MaterialID;
perprimitiveEXT layout(location = 2) in vec3 p_FaceNormal;

layout(location = 0) out vec4 fragColor;

void main() {
    // Both variables evaluate directly to the values assigned to the current primitive
    fragColor = vec4(p_FaceNormal, 1.0);
}

Unlike the mesh shader where per-primitive outputs are arrays matching the primitive count, the fragment shader receives the data as a scalar or single vector/struct evaluated for the specific primitive generating that fragment.

Key Functional Benefits

  1. Bandwidth Efficiency: Traditional pipelines require duplicating primitive-level constants across all vertices forming that primitive or passing them through uniform buffers. perprimitiveEXT eliminates this redundancy in the shader export stage.
  2. Deterministic Attribution: Legacy pipelines rely on the "provoking vertex" convention to handle flat attributes, which can complicate geometry algorithms when vertices are shared among multiple primitives. perprimitiveEXT decouples primitive data from vertex sharing entirely, ensuring that each polygon owns its specific state.