EmitMeshTasksEXT Execution Syntax and Thread Model

This article provides an overview of the EmitMeshTasksEXT function in GLSL task shaders under the VK_EXT_mesh_shader extension. It details the precise execution syntax, rules governing invocation behavior and control flow, the thread allocation model from task workgroups to mesh workgroups, and payload data synchronization requirements.


Execution Syntax

The EmitMeshTasksEXT function is available exclusively within GLSL task shaders enabled by the GL_EXT_mesh_shader extension. Its function signature is defined as follows:

void EmitMeshTasksEXT(uint groupCountX, uint groupCountY, uint groupCountZ);

Parameters:

  • groupCountX: The number of mesh shader workgroups to spawn along the X dimension.
  • groupCountY: The number of mesh shader workgroups to spawn along the Y dimension.
  • groupCountZ: The number of mesh shader workgroups to spawn along the Z dimension.

Setting any dimension to 0 results in zero mesh shader workgroups being spawned, effectively culling all downstream geometry for that task workgroup.

Basic Usage Example:

#version 460
#extension GL_EXT_mesh_shader : require

layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;

struct TaskPayload {
    uint baseIndex;
    uint meshletIndices[64];
};

taskPayloadSharedEXT TaskPayload payload;

void main() {
    // Perform culling or compute meshlet distribution across invocations
    uint threadID = gl_LocalInvocationIndex;
    
    // Write shared payload data
    if (threadID == 0) {
        payload.baseIndex = 128;
    }
    
    // Ensure all payload writes are complete
    memoryBarrierShared();
    barrier();

    // Spawn 16 mesh shader workgroups across a 1D grid
    EmitMeshTasksEXT(16, 1, 1);
}

Control Flow and Execution Rules

The execution of EmitMeshTasksEXT differs significantly from standard shader functions due to specific architectural and compiler constraints:

  1. Workgroup Uniform Control Flow: All invocations within the task shader workgroup must execute EmitMeshTasksEXT in uniform control flow. It cannot be conditionally placed within a dynamic branch where only a subset of threads execute it (such as inside if (gl_LocalInvocationIndex == 0)). Divergent execution leads to undefined behavior.

  2. Workgroup Termination: Executing EmitMeshTasksEXT implicitly terminates the execution of the calling task shader workgroup. Any code placed after EmitMeshTasksEXT will either not be executed or cause compilation/validation errors depending on the driver implementation.

  3. Single Execution Limit: Exactly one dynamic execution of EmitMeshTasksEXT is permitted per task shader workgroup. Calling it multiple times in loops or separate control paths is invalid.


Thread Allocation and Scheduling Model

The task-to-mesh pipeline establishes a hierarchical compute model consisting of two linked workgroup stages:

[ DrawMeshTasksEXT Command ]
          │
          ▼
┌───────────────────────────┐
│ Task Shader Workgroup Grid│
│ (User-defined XYZ count)  │
└─────────────┬─────────────┘
              │  EmitMeshTasksEXT(gx, gy, gz)
              ▼
┌───────────────────────────┐
│ Mesh Shader Workgroup Grid│
│ (gx × gy × gz per task WG)│
└───────────────────────────┘

1. Input Workgroup Sizing

The task shader itself is dispatched via Vulkan API commands such as vkCmdDrawMeshTasksEXT. The local thread count per task workgroup is defined by the standard compute layout qualifier:

layout(local_size_x = X, local_size_y = Y, local_size_z = Z) in;

2. Downstream Mesh Workgroup Generation

When EmitMeshTasksEXT(gx, gy, gz) runs, the GPU allocates a 3D grid of mesh shader workgroups totaling gx * gy * gz.

  • Each spawned mesh workgroup inherits the execution context of the specific task workgroup that emitted it.
  • Inside the spawned mesh shader, the workgroup coordinates are read using gl_WorkGroupID, ranging from (0, 0, 0) up to (gx - 1, gy - 1, gz - 1).
  • Each mesh workgroup operates with its own independent thread count as defined by the mesh shader’s layout(local_size_x, ...) directive.

3. Payload Allocation and Synchronization

Data is passed from the task shader workgroup to the downstream mesh shader workgroups via variables marked with the taskPayloadSharedEXT storage qualifier.

  • Lifetime and Scope: The payload memory persists until all mesh shader workgroups launched by that specific task workgroup have completed.
  • Synchronization: Writes to taskPayloadSharedEXT memory must be made visible to all threads using memoryBarrierShared() and synchronized using barrier() prior to calling EmitMeshTasksEXT. Downstream mesh shader workgroups can read this data concurrently as read-only memory.