Mesh Shader Culling with gl_CullPrimitiveEXT
In modern GPU pipelines using the GL_EXT_mesh_shader
extension, the gl_CullPrimitiveEXT built-in output array
provides a direct, hardware-supported mechanism to discard individual
primitives prior to rasterization. By setting a boolean flag per
primitive directly inside the mesh shader, developers can dynamically
cull back-facing, degenerate, or frustum-clipped geometry without
needing to reorganize indices or compact output buffers, significantly
improving rendering efficiency.
Declaration and Nature of the Built-In
When writing a GLSL mesh shader, enabling the
GL_EXT_mesh_shader extension implicitly provides the
per-primitive output array:
perprimitiveEXT out bool gl_CullPrimitiveEXT[];The array size corresponds to the maximum number of output primitives
declared in the shader's layout qualifier (e.g.,
layout(max_primitives = N) out;). Because it is declared
with the perprimitiveEXT qualifier, each element in the
array maps directly to a specific primitive generated by the mesh shader
invocation.
How Primitive Discarding Operates
To discard a primitive, a thread in the mesh shader workgroup writes
a boolean value to the gl_CullPrimitiveEXT array using that
primitive's local index:
gl_CullPrimitiveEXT[primIndex] = true;: Marks the primitive for discarding. The fixed-function hardware drops the primitive immediately after the mesh shader completes, preventing it from proceeding to clipping, setup, or the fragment shader.gl_CullPrimitiveEXT[primIndex] = false;: Keeps the primitive active for standard rasterization and fragment processing.
This evaluation happens on a per-primitive basis independently of
vertex assembly. Even though the vertices belonging to a culled
primitive may have been processed and output by the mesh shader, the
primitive assembly stage bypasses any triangle or line marked with
true.
Compaction Versus Individual Culling
Prior to primitive culling flags, eliminating invisible geometry
within a mesh workgroup required manual thread compaction. Developers
had to dynamically decrement the output count passed to
SetMeshOutputsEXT() and re-index active primitives into
contiguous memory slots to avoid rendering empty or degenerate
triangles.
gl_CullPrimitiveEXT removes the need for workgroup-level
stream compaction. The mesh shader can retain a static topology and
assign fixed primitive index mappings per thread. When a thread computes
that its assigned primitive is facing away from the camera, off-screen,
or zero-area, it simply writes true to its index in
gl_CullPrimitiveEXT. The underlying hardware pipeline
handles the sparse primitive stream natively, reducing register pressure
and avoiding expensive intra-workgroup barriers.