HLSL Working Group

[0047] - DirectX Semantic Signature Metadata

StatusAccepted
Author

Introduction

This proposal defines the LLVM IR module-level metadata schema for HLSL shader input and output signatures when targeting DirectX, and describes how that metadata is generated by Clang’s HLSL code generation.

Metadata Schema

Named Signature Table

!dx.semantic.signatures = !{ !1, !2 }

A named metadata node, dx.semantic.signatures, is used to identify the table of per-entry-point semantic signatures. The table itself is a list of references to function/signature triples. If no entry point in the module has any signature, the named metadata node may be omitted entirely.

Function/Signature Triple

!1 = !{ ptr @vs_main, !3, !4 }

The function/signature triple associates an entry-point function with its input and output signatures.

#TypeDescription
0ptrThe entry-point function these signatures belong to.
1metadata nodeReference to input signature element list, or null if absent.
2metadata nodeReference to output signature element list, or null if absent.

An entry function pointer can appear at most once in !dx.semantic.signatures.

Input/Output Signature Element List

!3 = !{ !5, !6 }

A signature element list consists of a list of references to signature element nodes.

Signature Element

!5 = !{ i32 0, !"TEXCOORD", i32 9, i32 0, !50, i32 0, i32 1, i8 4, i32 0, i8 0, i8 0, i8 0, i32 0 }

A signature element describes a single packed range of signature rows with its semantic name and indices, register placement, component masks, and stage-specific attributes. Each element must retain all relevant information to serialize into ISG1, OSG1 and PSV0.

#TypeDescription
0i32dense 0-based index within the entry function signature list; matches the operand of llvm.dx.load.input / llvm.dx.store.output
1metadata stringthe semantic name (e.g. !"TEXCOORD", !"SV_Position")
2i32component type; see llvm::dxil::ElementType.
3i32semantic kind; Arbitrary (0) for user-defined semantics, the corresponding SV_* value otherwise. See SEMANTIC_KIND
4metadata nodereference to a semantic indices node
5i32interpolation mode; see INTERPOLATION_MODE
6i32number of consecutive register rows occupied
7i8number of components per row (1–4)
8i32starting register row; -1 (0xFFFFFFFF) if unallocated
9i8starting component column; -1 (0xFF) if unallocated, otherwise 0–3
10i84-bit bitmask of components that are always read (input) or may be written (output).
11i84-bit bitmask of components that are dynamically indexed
12i32GS output stream index; 0 for non-GS stages

The following downstream container fields are derived from the operands above rather than carried in separate metadata operands:

  • Allocated status: A signature element is allocated iff field 8 (StartRow) is not -1 (0xFFFFFFFF) and field 9 (StartCol) is not -1 (0xFF). The two sentinels are always set together: StartRow == -1 iff StartCol == -1.

  • Declared component mask (DeclaredMask): Computed from field 7 (Cols) and field 9 (StartCol) as ((1 << Cols) - 1) << StartCol.

  • AlwaysReads / NeverWrites Masks: Field 10 is written directly to the container’s AlwaysReads mask for input signatures; for output signatures the container’s NeverWrites = ~UsageMask & DeclaredMask.

  • MinPrecision: Computed from field 2 (CompType) plus the UseMinPrecision module flag.

Semantic Indices

!50 = !{ i32 0 }
!51 = !{ i32 0, i32 1 }

A metadata node containing one or more semantic indices. The length of this list must equal the Rows field of the containing signature element.

Example

float4 vs_main(float4 pos   : POSITION,
               float4 uv[2] : TEXCOORD0) : SV_Position {
  return pos + uv[0] + uv[1];
}

struct PSOut {
  float4 color : SV_Target0;
  float4 extra : SV_Target1;
};

PSOut ps_main(float4 pos : SV_Position,
              float4 uv0 : TEXCOORD0,
              float4 uv1 : TEXCOORD1) {
  PSOut o;
  o.color = pos + uv0;
  o.extra = float4(uv1.xyz, 1);
  return o;
}
!dx.semantic.signatures = !{!10, !20}

; vs_main
!10  = !{ ptr @vs_main, !100, !110 }
!100 = !{ !1, !2 }            ; inputs
!110 = !{ !3 }                ; outputs

; ps_main
!20  = !{ ptr @ps_main, !120, !130 }
!120 = !{ !4, !5, !6 }        ; inputs
!130 = !{ !7, !8 }            ; outputs

; vs_main inputs
!1 = !{i32 0, !"POSITION", i32 9, i32 0, !50, i32 0, i32 1, i8 4, i32 0, i8 0, i8 0, i8 0, i32 0}
!2 = !{i32 1, !"TEXCOORD", i32 9, i32 0, !51, i32 0, i32 2, i8 4, i32 1, i8 0, i8 0, i8 0, i32 0}

; vs_main outputs
!3 = !{i32 0, !"SV_Position", i32 9, i32 3, !50, i32 0, i32 1, i8 4, i32 0, i8 0, i8 0, i8 0, i32 0}

; ps_main inputs
!4 = !{i32 0, !"SV_Position", i32 9, i32 3, !50, i32 2, i32 1, i8 4, i32 0, i8 0, i8 0, i8 0, i32 0}
!5 = !{i32 1, !"TEXCOORD",    i32 9, i32 0, !50, i32 2, i32 1, i8 4, i32 1, i8 0, i8 0, i8 0, i32 0}
!6 = !{i32 2, !"TEXCOORD",    i32 9, i32 0, !52, i32 2, i32 1, i8 4, i32 2, i8 0, i8 7, i8 0, i32 0}

; ps_main outputs
!7 = !{i32 0, !"SV_Target", i32 9, i32 16, !50, i32 0, i32 1, i8 4, i32 0, i8 0, i8 0, i8 0, i32 0}
!8 = !{i32 1, !"SV_Target", i32 9, i32 16, !52, i32 0, i32 1, i8 4, i32 1, i8 0, i8 0, i8 0, i32 0}

!50 = !{i32 0}          ; single semantic index {0}
!51 = !{i32 0, i32 1}   ; array form: one element spanning two rows, indices {0, 1}
!52 = !{i32 1}          ; single semantic index {1}

Generating Metadata

The dx.semantic.signatures table is produced entirely from within CGHLSLRuntime::emitEntryFunction in clang/lib/CodeGen/CGHLSLRuntime.cpp. Briefly, this is done so by traversing through each of the entry function’s input parameters and return outputs, accumulating an in-memory representation of the signature elements and emitting the load/store intrinsic. Signature IDs are assigned in declaration order giving each semantic a stable ID that appears in both the intrinsic call and the metadata table. Once these have been traversed, the DXIL packing algorithm is run on the in-memory signature elements. Finally we can emit the in-memory representation to the metadata nodes as described above. This is described in more detail below.

In-Memory Representation

A SemanticSignatureElement struct, defined in llvm/Frontend/HLSL/SemanticSignatures.h, will contain all the operands as above as well as helpers to determine the extra container field information (eg., is allocated). The struct will also define functions to convert to and from the metadata representation and the packing algorithm is implemented in this library.

Accumulate SemanticSignatureElements

emitEntryFunction will then have a vector of SemanticSignatureElements for both input and output signatures. The existing traversal visits each leaf semantic in declaration order as it generates llvm.dx.load.input or llvm.dx.store.output calls. At each leaf:

  1. The next available SigId is taken from the input or output counter and incremented. This index is the element’s declaration-order position in its signature table.
  2. The SigId is passed as operand 0 of the emitted llvm.dx.load.input / llvm.dx.store.output call. The remaining operands are element-relative as defined by the intrinsic (row/col offset within the element, vertex axis, dynamic index expression).
  3. A SemanticElement is appended to the corresponding vector, populated from information already in scope:
    • SemanticName, SemanticKind, and SemanticIndices from the HLSLAppliedSemanticAttr on the parameter or field;
    • CompType from the leaf LLVM type;
    • Rows from the array extent of the declaration (1 for scalars and vectors), Cols from the vector width (1 for scalars);
    • InterpMode from a specified interpolation qualifier (or stage default);
    • GSStream from the relevant stream attribute, or 0;
    • StartRow = -1, StartCol = -1, UsageMask = 0, DynIndexMask = 0.

Packing

Once the traversal of both the parameters and the output completes, the two vectors describe every signature element for the entry, with layout fields left unallocated. emitEntryFunction then invokes the packing algorithm on the in-memory representation.

Note that:

  • Inputs and outputs are packed independently (and outputs are further partitioned by GSStream for geometry shaders).
  • The packer only mutates StartRow, StartCol, UsageMask, and DynIndexMask on each SemanticElement. It never reorders, inserts, or removes entries, so every previously emitted call site remains valid: the SigId operand still names the same element, and the row/column operands are element-relative and therefore packing-agnostic.
  • UsageMask and DynIndexMask are computed from the recorded uses observed during the traversal (which columns were read/written, and which accesses used a non-poison dynamic index operand).

Emitting the Metadata Table

At the end of emitEntryFunction, with the two vectors fully populated and packed, the metadata is materialized in one pass:

  1. For each SemanticSignatureElement, build the semantic-indices node (!{ i32 ... }) and the signature element node described in Signature Element.
  2. Build the input and output element-list nodes referencing those element nodes (or null if the corresponding vector is empty).
  3. Build the function/signature triple { ptr @entry, !inputs, !outputs } and append it as an operand of the module’s !dx.semantic.signatures named metadata, creating the named node on first use.

Interpolation Modifiers

A signature element may carry an interpolation modifier that specifies how the rasterizer interpolates its value across a primitive for the pixel shader. The recognized modifiers are nointerpolation, linear, centroid, noperspective, sample and center.

The modifier keywords are independent of semantics, but they are only meaningful on declarations that contribute to a signature element. Practically, this means the same set of declarations on which a semantic may appear on:

  • a function parameter declaration,
  • a struct or class field declaration, or
  • a function declaration (applied to its return value).

When applied to an aggregate type, the modifier propagates recursively to every scalar or vector of the type. A modifier on an inner field overrides one inherited from an enclosing declaration.

For example:

struct Material {
   float3                albedo : COLOR0;     // inherits nointerpolation from the parameter
   nointerpolation float gloss  : COLOR1;     // already nointerpolation
   linear float2         uv     : TEXCOORD0;  // inner linear overrides the outer
};

float4 ps_main(nointerpolation Material m) : SV_Target {
   ...
}

Parse and Sema

An HLSLInterpolationModeAttr is defined with a spelling for each unique modifier and carries a single Modifiers bitmask argument with one bit per modifier. The attr has ParmVarDecl, FieldDecl, or FunctionDecl as subjects. At parse time a new HLSLInterpolationModeAttr is created with the respective modifier set and then the attribute is consolidated through a dedicated merge step, SemaHLSL::mergeInterpolationModeAttr. The merge runs at declaration time and guarantees that a declaration ends up with at most one HLSLInterpolationModeAttr whose Modifiers mask is the union of the keywords written on it.

mergeInterpolationModeAttr is invoked once per keyword occurrence and:

  1. If the declaration has no HLSLInterpolationModeAttr yet, it creates one whose Modifiers mask has only this keyword’s bit set.
  2. Otherwise it creates a new HLSLInterpolationModeAttr carrying the OR-ed mask and drops the existing attr.
  3. It will then perform diagnostics of the constructed attr:
    • Error: nointerpolation with any of linear, centroid, noperspective, sample, center
    • Error: nointerpolation on SV_Position
    • Error: Any non-nointerpolation modifier on integer, boolean and 64-bit floating point
    • Warning: Duplicate of the same modifier on one declaration
    • Warning: center < centroid < sample overrides in ascending order

Note: A modifier on a declaration that does not reach an entry-point signature parses and is preserved on the AST, but produces no signature metadata.

CodeGen

During entry-function emission (the emitEntryFunction flow described in Generating Metadata), each leaf signature element determines its set of interpolation modes by reading the Modifiers bitmask from the leaf declaration’s HLSLInterpolationModeAttr. If the declaration has no such attribute, walk the parent declarations to find the first enclosing declaration that has one. Then map the collected set of modifiers to their corresponding InterpolationMode using the table below:

Modifier attribute setInterpolationMode
{}Undefined (0)
{nointerpolation}Constant (1)
{linear}, {center}, {linear, center}Linear (2)
{centroid}, {linear, centroid}LinearCentroid (3)
{noperspective}, {linear, noperspective}, {linear, noperspective, center}LinearNoperspective (4)
{noperspective, centroid}, {linear, noperspective, centroid}LinearNoperspectiveCentroid (5)
{sample}, {linear, sample}LinearSample (6)
{noperspective, sample}, {linear, noperspective, sample}LinearNoperspectiveSample (7)

Note: center does not appear in the InterpolationMode as it is the default sampling location and is used when centroid/sample is not present.

Then the interpolation mode is “normalized” as follows:

  1. If the mode is Undefined, set it based on the component type: Linear for 32-bit-or-smaller floating-point leaves, Constant otherwise.
  2. Promote an SV_Position interpolation mode into the corresponding noperspective variant of its current mode (Linear → LinearNoperspective, LinearCentroid → LinearNoperspectiveCentroid, LinearSample → LinearNoperspectiveSample).
  3. Finally, if the entry function is not a pixel shader then it is set to Undefined

Metadata

The normalized InterpolationMode is written into operand 5 of the Signature Element metadata node. In the SemanticElement population step described in Accumulate SemanticSignatureElements, the InterpMode field is set to the value produced by normalization.

No additional metadata operands are required, the modifier is fully captured by the existing InterpMode slot and is propogated to the DXContainer accordingly.