Amplification Shader Mesh Dispatch in DirectX

In DirectX 12, the Amplification Shader acts as an optional, programmable pre-pass to the Mesh Shader that determines how many downstream mesh threadgroups to execute. By evaluating scene data directly on the GPU—such as frustum visibility, occlusion tests, or distance-based Level of Detail (LOD)—the amplification shader dynamically dictates the execution grid of the mesh shader. This article explains the intrinsic mechanisms, execution logic, and data flow used to launch mesh shader threadgroups.

The DispatchMesh Intrinsic

The amplification shader controls mesh shader execution through a built-in HLSL function called DispatchMesh. This function is the sole mechanism by which an amplification shader creates mesh shader work.

The signature of the function is:

void DispatchMesh(
    uint ThreadGroupCountX,
    uint ThreadGroupCountY,
    uint ThreadGroupCountZ,
    in payload_t Payload
);

The arguments ThreadGroupCountX, ThreadGroupCountY, and ThreadGroupCountZ define the three-dimensional dimensions of the mesh shader threadgroup grid. The total number of mesh shader threadgroups launched is the product of these three dimensions (\(X \times Y \times Z\)).

Determining Grid Dimensions

Because the amplification shader runs per-threadgroup computation, it can calculate the dispatch dimensions dynamically at runtime rather than relying on static values from the CPU.

Common methods for calculating these counts include:

  1. Frustum and Occlusion Culling: The amplification shader tests coarse bounding volumes (such as bounding spheres or oriented bounding boxes) against the camera frustum or a hierarchical Z-buffer. If a meshlet or object is invisible, the shader sets the dispatch counts to (0, 0, 0). This completely culls the geometry and prevents any mesh shader threadgroups from being allocated.
  2. Dynamic LOD Selection: By computing the distance between a bounding sphere center and the camera, the shader selects an appropriate level of detail. Coarser LODs require fewer meshlets, resulting in lower dispatch dimensions (e.g., DispatchMesh(2, 1, 1, payload)), whereas finer LODs request more threadgroups (e.g., DispatchMesh(16, 1, 1, payload)).
  3. Subdivision and Dynamic Expansion: When tessellating or procedurally generating geometry, the amplification shader computes how many meshlets are required to represent the expanded surface and outputs the corresponding threadgroup count.

The Payload Structure

Along with the dispatch dimensions, DispatchMesh accepts a user-defined Payload parameter. This payload is stored in shared memory and passed directly to all mesh shader threadgroups instantiated by that specific call.

The payload typically contains:

  • An array of meshlet indices that survived culling.
  • Calculated LOD offsets or transformation matrices.
  • Dynamic material or instance IDs.

Downstream mesh shaders access this data using the [[vk::payload]] or HLSL in payload attribute, enabling each generated mesh threadgroup to identify which specific piece of geometry it is responsible for processing.

Execution Constraints

  • Single Invocation per Threadgroup: While multiple threads within an amplification shader threadgroup can participate in culling tests and calculating the final counts using group shared memory or wave intrinsics, DispatchMesh must be called uniformly or by a single designated thread (often the first thread, using an if (GroupThreadID == 0) check) after all calculations and data synchronization are complete.
  • Dispatch Limits: The maximum number of total mesh shader threadgroups that can be dispatched by a single amplification shader group is subject to hardware and API limits, typically up to \(2^{16} - 1\) threadgroups across all dimensions.