---
title: "The SPIR-V backend"
url: https://recursiveai.org/oneshader/docs/spirv-backend/
description: "How OneShader maps HLSL to SPIR-V 1.5 for Vulkan 1.2: matrices, layouts, bindings, stage variables, precise, wave intrinsics, 16-bit and 64-bit types, specialization constants, and the errors it reports instead of wrong code."
version: "1.2.0"
source: https://github.com/recursive-ai-oss/oneshader-csharp/blob/8896e63be3a4f246295427451f15e4fcfb5535cf/docs/spirv-backend.md
---

# The SPIR-V backend

`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 `floatRxC` is a SPIR-V matrix with R columns of C components, so
  `m[i]` is column `i`, and `mul(a, b)` is `OpMatrixTimesMatrix(b, a)`, `mul(v, m)` is `OpMatrixTimesVector(m, v)`
  and `mul(m, v)` is `OpVectorTimesMatrix(v, m)`. In buffers, HLSL `column_major` becomes `RowMajor` and vice
  versa; `MatrixStride` comes 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 a `Block` struct in the `PushConstant` storage class with no
  descriptor decorations. Its offsets follow the vector-relaxed std430 rules of a structured-buffer element (what DXC emits,
  checked against it by `PushConstantTests`), not the constant-buffer rules.
- **Resources.** A constant buffer is a `Block` struct in `Uniform`; a structured or byte-address buffer is a
  `Block` struct holding one runtime array in `StorageBuffer` (`NonWritable` when read-only). Access chains into
  a storage buffer get the leading `0` for that struct. Textures and samplers are separate variables combined
  with `OpSampledImage` at 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 add `StorageImageExtendedFormat`) and `Unknown` otherwise. Atomics on a
  texel go through `OpImageTexelPointer`; atomic scopes and barrier semantics match what DXC emits.
  A resource array is one variable of `OpTypeArray` (set and binding as for a single resource); a run-time index
  adds the matching `*ArrayDynamicIndexing` capability, and `NonUniformResourceIndex` adds `ShaderNonUniform`, the
  `*ArrayNonUniformIndexing` capability and `NonUniform` on 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) are `Flat`. Pixel shaders get `OriginUpperLeft`, plus `DepthReplacing` when 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_CullDistance` variable of an entry point is a slice of one `ClipDistance` /
  `CullDistance` built-in array, ordered by semantic index (an array's elements in order), as DXC lays them out.
- **Booleans in memory.** `OpTypeBool` is not allowed in uniform or storage memory, so `bool` members 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 a `float3`).
- **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; `OpTerminateInvocation` needs a newer environment.
- **Shifts** mask the count to the operand width, as HLSL does. `Ne` on floats is unordered (true for NaN).
  `precise` becomes `NoContraction`.

## 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.
