GL_NV_mesh_shader vs GL_EXT_mesh_shader in GLSL

This article compares NVIDIA’s vendor-specific GL_NV_mesh_shader extension with the cross-vendor GL_EXT_mesh_shader extension in GLSL. While both extensions replace the traditional vertex, geometry, and tessellation pipeline with a cooperative, compute-like programming model using task (amplification) and mesh stages, the EXT version introduces standardized syntax, more explicit memory management, dynamic primitive sizing, and multidimensional workgroup spawning.

Task Dispatch and Workgroup Spawning

In GL_NV_mesh_shader, the task shader triggers subsequent mesh shader workgroups using the built-in variable gl_TaskCountNV. This restricts invocation generation to a one-dimensional dispatch count:

// GL_NV_mesh_shader
gl_TaskCountNV = 16;

In GL_EXT_mesh_shader, workgroup generation is handled functionally via the built-in function EmitMeshTasksEXT(), which supports three-dimensional workgroup dispatch grids:

// GL_EXT_mesh_shader
EmitMeshTasksEXT(taskCountX, taskCountY, taskCountZ);

Once EmitMeshTasksEXT() executes, control flow terminates or ignores subsequent emissions, making task dispatch explicit and aligned with compute dispatch conventions.

Task Payload Communication

Both specifications pass data from the task shader to the mesh shader through a shared memory payload, but the declarations and storage qualifiers differ:

  • NV Extension: The payload is defined using the taskNV storage qualifier applied to an output block in the task shader, and as an input block in the mesh shader:

    // In task shader (NV)
    taskNV out TaskData {
        uint offset;
    } taskOut;
  • EXT Extension: The payload uses the taskPayloadSharedEXT storage qualifier, which functions similarly to shared workgroup memory. Both the task and mesh shaders declare the same variable layout:

    // In task and mesh shaders (EXT)
    struct TaskData {
        uint offset;
    };
    taskPayloadSharedEXT TaskData taskPayload;

Dynamic Primitive and Vertex Output Allocation

Under GL_NV_mesh_shader, the number of vertices and primitives written by a mesh workgroup is implicitly determined by the maximum bounds declared in the layout qualifiers (max_vertices and max_primitives), or manually capped by what indices are populated:

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

GL_EXT_mesh_shader retains maximum bounds in the layout declaration for hardware allocation purposes, but mandates an explicit runtime call to SetMeshOutputCountsEXT() before writing per-primitive or per-vertex data:

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

void main() {
    SetMeshOutputCountsEXT(actualNumVertices, actualNumPrimitives);
    // Writes to vertices and primitives follow
}

Omitting SetMeshOutputCountsEXT() or writing outputs beyond the specified counts produces undefined behavior.

Primitive Index Representation

The two extensions structure index output buffers differently:

  • NV Extension: Uses a flat output array, gl_PrimitiveIndicesNV[]. For a triangle topology, every three consecutive indices correspond to a single primitive:

    gl_PrimitiveIndicesNV[gl_LocalInvocationIndex * 3 + 0] = idx0;
    gl_PrimitiveIndicesNV[gl_LocalInvocationIndex * 3 + 1] = idx1;
    gl_PrimitiveIndicesNV[gl_LocalInvocationIndex * 3 + 2] = idx2;
  • EXT Extension: Uses strongly-typed vector arrays matching the output topology. For triangles, indices are assigned as a uvec3 to gl_PrimitiveTriangleIndicesEXT[] (or uvec2 to gl_PrimitiveLineIndicesEXT[] for lines, and uint to gl_PrimitivePointIndicesEXT[] for points):

    gl_PrimitiveTriangleIndicesEXT[gl_LocalInvocationIndex] = uvec3(idx0, idx1, idx2);

Built-in Output Structures and Per-Primitive Qualifiers

GL_NV_mesh_shader provides a built-in per-vertex array gl_MeshVerticesNV[] containing traditional vertex outputs like gl_Position and gl_PointSize. Custom per-primitive attributes are denoted using the perprimitiveNV qualifier on output blocks or variables.

GL_EXT_mesh_shader standardizes this using gl_MeshVerticesEXT[] for standard vertex attributes and introduces gl_MeshPrimitivesEXT[] for per-primitive built-ins (such as gl_PrimitiveID, gl_Layer, and gl_CullPrimitiveEXT). Custom outputs are divided using the explicit qualifiers perprimitiveEXT or vertex-scoped array blocks:

// GL_EXT_mesh_shader custom outputs
layout(location = 0) out vec4 outColor[];                     // Per-vertex attribute
layout(location = 1) perprimitiveEXT out uint outMaterialID[]; // Per-primitive attribute

Summary of Key Syntax Differences

Feature GL_NV_mesh_shader GL_EXT_mesh_shader
Workgroup Generation gl_TaskCountNV = N; EmitMeshTasksEXT(x, y, z);
Payload Storage taskNV out / in taskPayloadSharedEXT
Output Count Definition Implicit / maximum bound Explicit via SetMeshOutputCountsEXT(v, p)
Triangle Index Output Flat uint gl_PrimitiveIndicesNV[] Vector uvec3 gl_PrimitiveTriangleIndicesEXT[]
Per-Primitive Qualifier perprimitiveNV perprimitiveEXT
Built-in Arrays gl_MeshVerticesNV[] gl_MeshVerticesEXT[], gl_MeshPrimitivesEXT[]