Storing Per-Vertex Attributes in gl_MeshVerticesEXT

This article explains how the gl_MeshVerticesEXT output array operates in GLSL mesh shaders under the GL_EXT_mesh_shader extension. It details the structural definition of built-in per-vertex outputs, how custom user attributes correlate with this array, and how mesh shader workgroups cooperatively write and index per-vertex data in GPU memory before primitive assembly.

Definition and Structure of gl_MeshVerticesEXT

In GLSL mesh shaders (GL_EXT_mesh_shader), gl_MeshVerticesEXT is a built-in output array of interface blocks designed specifically to handle standard per-vertex outputs for the rasterizer. It is defined implicitly in the shader stage as:

out gl_MeshPerVertexEXT {
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
    float gl_CullDistance[];
} gl_MeshVerticesEXT[];

The array size corresponds to the maximum vertex capacity declared in the layout qualifier at the top of the shader, such as layout(max_vertices = N) out;. Each element in the array represents a single vertex in the emitted meshlet.

Built-in vs. Custom Per-Vertex Attributes

gl_MeshVerticesEXT strictly holds Vulkan's built-in vertex attributes, most notably gl_Position. Custom user attributes—such as texture coordinates, normals, or colors—are not nested inside gl_MeshVerticesEXT. Instead, custom per-vertex attributes are declared as separate, explicitly sized or unsized output arrays or interface blocks matching the vertex capacity:

layout(location = 0) out vec3 outNormal[];
layout(location = 1) out vec2 outTexCoord[];

Alternatively, grouped attributes can be written using custom output blocks:

layout(location = 0) out VertexOutput {
    vec3 normal;
    vec2 uv;
} v_out[];

Even though custom outputs are declared independently from gl_MeshVerticesEXT, they share the exact same indexing scheme. The vertex located at index i is defined by the combination of gl_MeshVerticesEXT[i] and all corresponding custom attributes at index i.

Output Allocation with SetMeshOutputsEXT

Before writing to gl_MeshVerticesEXT or custom vertex output arrays, the mesh shader must define the actual number of primitives and vertices being emitted for the current workgroup. This is accomplished using the built-in function:

SetMeshOutputsEXT(vertexCount, primitiveCount);

Once SetMeshOutputsEXT executes, the hardware allocates backing memory for the specified number of vertices. Attempting to write to indices greater than or equal to vertexCount results in undefined behavior.

Indexing and Workgroup Memory Access

Writing to gl_MeshVerticesEXT is fundamentally parallel. Unlike legacy vertex shaders, which process a single vertex per thread invocation, a mesh shader workgroup processes an entire meshlet containing multiple vertices and primitives concurrently.

Attributes in gl_MeshVerticesEXT are stored via direct array indexing. Threads within the workgroup typically map their local thread ID (gl_LocalInvocationIndex) to vertex indices:

uint vID = gl_LocalInvocationIndex;
if (vID < vertexCount) {
    gl_MeshVerticesEXT[vID].gl_Position = transform * vec4(inPos[vID], 1.0);
    outTexCoord[vID] = inUV[vID];
}

If the number of vertices exceeds the workgroup size, threads can iterate using a strided loop until all elements of gl_MeshVerticesEXT are populated.

Memory Layout Under the Hood

Under the hood, the GPU driver allocates on-chip scratch memory (such as Shared Memory or dedicated Primitive/Vertex parameter cache) to stage the gl_MeshVerticesEXT array and custom per-vertex output arrays.

Because gl_MeshVerticesEXT is structured as an Array of Structures (AoS) while individual custom arrays are often structured as Structure of Arrays (SoA), modern compilers optimize access patterns to ensure coalesced writes across SIMD lanes. After the workgroup finishes execution and synchronizes, the rasterizer reads the populated storage slots directly according to the indices defined in gl_PrimitiveTriangleIndicesEXT or gl_PrimitiveLineIndicesEXT.