[0047] - DirectX Semantic Signature Metadata
| Status | Accepted |
|---|---|
| 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.
| # | Type | Description |
|---|---|---|
| 0 | ptr | The entry-point function these signatures belong to. |
| 1 | metadata node | Reference to input signature element list, or null if absent. |
| 2 | metadata node | Reference 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.
| # | Type | Description |
|---|---|---|
| 0 | i32 | dense 0-based index within the entry function signature list; matches the operand of llvm.dx.load.input / llvm.dx.store.output |
| 1 | metadata string | the semantic name (e.g. !"TEXCOORD", !"SV_Position") |
| 2 | i32 | component type; see llvm::dxil::ElementType. |
| 3 | i32 | semantic kind; Arbitrary (0) for user-defined semantics, the corresponding SV_* value otherwise. See SEMANTIC_KIND |
| 4 | metadata node | reference to a semantic indices node |
| 5 | i32 | interpolation mode; see INTERPOLATION_MODE |
| 6 | i32 | number of consecutive register rows occupied |
| 7 | i8 | number of components per row (1–4) |
| 8 | i32 | starting register row; -1 (0xFFFFFFFF) if unallocated |
| 9 | i8 | starting component column; -1 (0xFF) if unallocated, otherwise 0–3 |
| 10 | i8 | 4-bit bitmask of components that are always read (input) or may be written (output). |
| 11 | i8 | 4-bit bitmask of components that are dynamically indexed |
| 12 | i32 | GS 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 == -1iffStartCol == -1.Declared component mask (
DeclaredMask): Computed from field 7 (Cols) and field 9 (StartCol) as((1 << Cols) - 1) << StartCol.AlwaysReads/NeverWritesMasks: Field 10 is written directly to the container’sAlwaysReadsmask for input signatures; for output signatures the container’sNeverWrites = ~UsageMask & DeclaredMask.MinPrecision: Computed from field 2 (CompType) plus theUseMinPrecisionmodule 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:
- The next available
SigIdis taken from the input or output counter and incremented. This index is the element’s declaration-order position in its signature table. - The
SigIdis passed as operand 0 of the emittedllvm.dx.load.input/llvm.dx.store.outputcall. The remaining operands are element-relative as defined by the intrinsic (row/col offset within the element, vertex axis, dynamic index expression). - A
SemanticElementis appended to the corresponding vector, populated from information already in scope:SemanticName,SemanticKind, andSemanticIndicesfrom theHLSLAppliedSemanticAttron the parameter or field;CompTypefrom the leaf LLVM type;Rowsfrom the array extent of the declaration (1 for scalars and vectors),Colsfrom the vector width (1 for scalars);InterpModefrom a specified interpolation qualifier (or stage default);GSStreamfrom 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
GSStreamfor geometry shaders). - The packer only mutates
StartRow,StartCol,UsageMask, andDynIndexMaskon eachSemanticElement. It never reorders, inserts, or removes entries, so every previously emitted call site remains valid: theSigIdoperand still names the same element, and the row/column operands are element-relative and therefore packing-agnostic. UsageMaskandDynIndexMaskare computed from the recorded uses observed during the traversal (which columns were read/written, and which accesses used a non-poisondynamic 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:
- For each
SemanticSignatureElement, build the semantic-indices node (!{ i32 ... }) and the signature element node described in Signature Element. - Build the input and output element-list nodes referencing those element
nodes (or
nullif the corresponding vector is empty). - Build the function/signature triple
{ ptr @entry, !inputs, !outputs }and append it as an operand of the module’s!dx.semantic.signaturesnamed 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:
- If the declaration has no
HLSLInterpolationModeAttryet, it creates one whoseModifiersmask has only this keyword’s bit set. - Otherwise it creates a new
HLSLInterpolationModeAttrcarrying the OR-ed mask and drops the existing attr. - It will then perform diagnostics of the constructed attr:
- Error:
nointerpolationwith any oflinear,centroid,noperspective,sample,center - Error:
nointerpolationonSV_Position - Error: Any non-
nointerpolationmodifier on integer, boolean and 64-bit floating point - Warning: Duplicate of the same modifier on one declaration
- Warning:
center<centroid<sampleoverrides in ascending order
- Error:
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 set | InterpolationMode |
|---|---|
{} | 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:
- If the mode is
Undefined, set it based on the component type:Linearfor 32-bit-or-smaller floating-point leaves,Constantotherwise. - Promote an
SV_Positioninterpolation mode into the correspondingnoperspectivevariant of its current mode (Linear→LinearNoperspective,LinearCentroid→LinearNoperspectiveCentroid,LinearSample→LinearNoperspectiveSample). - 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.