SpirvBackend.Emit(IrModule, entryPointName, SpirvOptions?, DiagnosticBag) turns one entry point of an IR
module, and everything it reaches, into a SPIR-V 1.5 binary for the Vulkan 1.2 environment (Shader capability,
logical addressing, GLSL450 memory model, GLSL.std.450 for math). It returns null and reports OSH50xx
errors when something cannot be expressed; it does not throw for unsupported input. The same module and
options always give the same bytes. Code is in src/OneShader/SpirV/.
Mapping decisions
- Matrices are transposed. An HLSL
floatRxCis a SPIR-V matrix with R columns of C components, som[i]is columni, andmul(a, b)isOpMatrixTimesMatrix(b, a),mul(v, m)isOpMatrixTimesVector(m, v)andmul(m, v)isOpVectorTimesMatrix(v, m). In buffers, HLSLcolumn_majorbecomesRowMajorand vice versa;MatrixStridecomes from the buffer layout. One-row or one-column matrices and one-component vectors have no SPIR-V matrix type: they are vectors and scalars, and their indices disappear. - Buffer layout types (offset-decorated structs, strided arrays) are separate SPIR-V types from the plain types used in registers. Loading or storing a whole struct or array to a buffer converts member by member.
- Push constants. A
[[vk::push_constant]]block is aBlockstruct in thePushConstantstorage class with no descriptor decorations. Its offsets follow the vector-relaxed std430 rules of a structured-buffer element (what DXC emits, checked against it byPushConstantTests), not the constant-buffer rules. - Resources. A constant buffer is a
Blockstruct inUniform; a structured or byte-address buffer is aBlockstruct holding one runtime array inStorageBuffer(NonWritablewhen read-only). Access chains into a storage buffer get the leading0for that struct. Textures and samplers are separate variables combined withOpSampledImageat each use. Storage images use the format from[[vk::image_format]]when the shader gives one (no read/write-without-format capability then; formats outside the core set addStorageImageExtendedFormat) andUnknownotherwise. Atomics on a texel go throughOpImageTexelPointer; atomic scopes and barrier semantics match what DXC emits. A resource array is one variable ofOpTypeArray(set and binding as for a single resource); a run-time index adds the matching*ArrayDynamicIndexingcapability, andNonUniformResourceIndexaddsShaderNonUniform, the*ArrayNonUniformIndexingcapability andNonUniformon the index, access chain, loaded handle and sampled image. An append or consume buffer's counter is a second storage buffer variable (counter.var.<name>) in the buffer's set. - Interface. Every global variable the entry point references is listed on
OpEntryPoint, as SPIR-V 1.4+ requires, inputs first, then outputs, then the rest in module order. Integer and double fragment inputs (and the vertex outputs that feed them) areFlat. Pixel shaders getOriginUpperLeft, plusDepthReplacingwhen they write depth. - Stage variables. Arrays and matrices (at least 2x2) become one variable per element or row, each with its own
location, which occupies the same locations as DXC's single array-typed variable. Clip and cull distances are the
exception: every
SV_ClipDistance/SV_CullDistancevariable of an entry point is a slice of oneClipDistance/CullDistancebuilt-in array, ordered by semantic index (an array's elements in order), as DXC lays them out. - Booleans in memory.
OpTypeBoolis not allowed in uniform or storage memory, soboolmembers of buffers are 32-bit unsigned integers (0 or 1), converted to bool on load and back on store. A structured buffer's element stride is its size rounded up to its alignment (16 bytes for afloat3). - Control flow. Blocks are emitted in reverse post-order of a walk that visits a header's merge and continue
blocks first, which puts every merge after the blocks inside its construct. Unreachable blocks are dropped; a
block without a terminator becomes
OpUnreachable. - Discard is
OpKill, valid in SPIR-V 1.5;OpTerminateInvocationneeds a newer environment. - Shifts mask the count to the operand width, as HLSL does.
Neon floats is unordered (true for NaN).precisebecomesNoContraction.
Wave and quad intrinsics
GroupNonUniform* instructions with Subgroup scope, chosen as DXC's output chooses them: WaveReadLaneAt is OpGroupNonUniformShuffle,
WaveActiveCountBits/WavePrefixCountBits are OpGroupNonUniformBallotBitCount (Reduce / ExclusiveScan) of a ballot, the prefix functions are
exclusive scans, QuadReadAcross* is OpGroupNonUniformQuadSwap and QuadReadLaneAt is OpGroupNonUniformQuadBroadcast. The lane index and
count are the SubgroupLocalInvocationId and SubgroupSize input variables. The capabilities (GroupNonUniform, Vote, Arithmetic, Ballot,
Shuffle, Quad) are added only for what the entry point uses; reflection lists the same classes (EntryPointInfo.SubgroupOperations, values equal
to Vulkan's VkSubgroupFeatureFlagBits) so a host can check subgroupSupportedOperations before creating the pipeline.
WaveMatch and WaveMultiPrefix* are front-end errors (OSH0350): partitioned operations exist in Vulkan only as a vendor extension.
Specialization constants
[[vk::constant_id(N)]] const bool|int|uint|float Name = default; is OpSpecConstantTrue/OpSpecConstantFalse (bool) or OpSpecConstant (the default's
bits) with OpDecorate SpecId N and an OpName, written where it is first used, so an entry point carries only the constants it reaches. The ids are DXC's
(a test compares the decorations with DXC's output for the corpus shaders). Expressions on constants are ordinary instructions; DXC emits
OpSpecConstantOp for the initializer of a static const that depends on one, and the result is the same for every pipeline. The optimizer treats a
constant as an unknown value, so branches on it stay. Pass the values in VkSpecializationInfo (SpecializationValues.ToVulkan()): every constant is a
4-byte value, a bool is a VkBool32.
16-bit and 64-bit types
With Enable16BitTypes, half/min16float/min10float are OpTypeFloat 16 and min16int/min16uint and the int16_t family are 16-bit
integers (capabilities Float16, Int16, and for buffer members StorageBuffer16BitAccess, UniformAndStorageBuffer16BitAccess,
StoragePushConstant16, StorageInputOutput16). double is Float64, int64_t/uint64_t are Int64, and 64-bit Interlocked* adds Int64Atomics.
Buffer offsets, array strides and matrix strides equal DXC's (checked with spirv-dis on the corpus types_* cases). Without the switch, min-precision
types are 32-bit and carry RelaxedPrecision on members and results; a min16int constant is cut to 16 bits while the arithmetic stays 32-bit
(observed in DXC's output). Observed DXC choices followed: double arguments to float-only intrinsics are converted to float (we warn, OSH0361);
unsuffixed float literals stay double until they meet a concrete type. One DXC behaviour is not copied: (float)int64_t goes through a 32-bit
SConvert in DXC and truncates; we convert directly. EntryPointInfo.RequiredFeatures lists the device features the capabilities need.
Note: on the RTX 4070 (driver 595) a 16-bit signed remainder with a negative dividend returns a different value from RADV, llvmpipe and Metal;
the corpus case types_int16 agrees between DXC and this compiler on every device because both emit OpSRem.
Not supported yet (reported as OSH5002)
Unbounded resource arrays (rejected by the front end), comparison or bit-casting of whole matrices, bool matrices, products involving one-row/column matrices.