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:
- Duplicate that attribute across all three vertices of the triangle
and apply the
flatinterpolation qualifier. - 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
In the Mesh Shader (
mesh): Outputs marked withperprimitiveEXTare 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 } }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.