Vulkan
Vulkan consumes SPIR-V directly, which is convenient because the faithful pipeline already produces SPIR-V as its intermediate:
HLSL → DXC → SPIR-V (consumed directly by Vulkan)
Current state
The CLI and PlatformTarget accept a Vulkan profile. Compiling targets MonoGame's DesktopVK platform: the .mgfx uses the real Vulkan container (profile byte 80), with each shader's SPIR-V wrapped in the descriptor-layout header MonoGame's native Vulkan pipeline creation reads directly. ShadowDusk's own Vulkan output is validated end-to-end — it loads and renders correctly in a real MonoGame DesktopVK Effect, across the standard post-process corpus.
One caveat, now narrower than it used to be: a pixel comparison against MonoGame's own mgfxc /Profile:Vulkan output is possible only for effects that declare explicit registers. mgfxc's compiled Vulkan output crashes on load in real DesktopVK for shaders using auto-numbered (non-explicit-register) resources — a MonoGame-side bug, independent of ShadowDusk, unrelated to anything this library emits. ShadowDusk's own output sidesteps it by construction. Where the comparison can run, it matches exactly (max Δ 0, including the real-world Apos.Shapes effect); where it can't, ShadowDusk's output is verified by rendering it in the real engine. Track MonoGame's DesktopVK runtime maturity if you rely on this target in production.
KNI does not ship a Vulkan platform, so this target is MonoGame-only.
HLSL wave and quad intrinsics (WaveActiveSum, QuadReadAcrossX, ...) are not supported on this target and are rejected at compile time with SD0218, naming the intrinsic. MonoGame's DesktopVK creates a Vulkan 1.0 instance with no subgroup support, and the SPIR-V 1.3 module these intrinsics need was measured out of spec there by the Khronos validation layer (issue #229): a CPU driver rendered it, but a GPU driver is not required to. Keep them out of the Vulkan build (for example behind #if !VULKAN), or target DirectX 12, which supports them.
An array of textures or samplers (Texture2D Tex[N], SamplerState S[N]) is rejected at compile time with SD0221, located at the declaration (issue #324). MonoGame's Vulkan effect format binds one texture per descriptor slot and names it by one effect parameter, so the array has no representation: mgfxc 3.8.5's own /Profile:Vulkan output for it carries no parameter, no sampler record and no descriptor binding at all (measured for 1, 2 and 4 elements, with and without an explicit register), and that effect loads into real DesktopVK with nothing a game could set and draws nothing. ShadowDusk refuses the shape instead. Declare each element as its own texture and sample each by name, or keep the array out of the Vulkan build behind #if !VULKAN. The same applies to a Slang Sampler2D T[N] on the ShadowDusk.Slang route.
A Vulkan .mgfx requires at most one constant buffer per shader stage — the same limit mgfxc's own Vulkan writer enforces. ShadowDusk fails loudly (rather than mis-emitting) if a shader declares more than one (SD0026).
Texture and sampler registers are assigned for you
Vulkan binds a texture and the sampler it is used with as one combined descriptor, so the two halves of a pair must sit on the same binding. A pair left to automatic numbering does not get that, and the mismatch crashes MonoGame's native Vulkan draw path rather than failing at compile time. On the Vulkan target only, ShadowDusk therefore assigns matching register(tN)/register(sN) to each texture/sampler pair it sees used together.
Registers you write yourself are kept wherever they can be. They are re-assigned only when honouring them would break the rules above — splitting a pair across two bindings, or putting two textures on one binding. The texture's register decides the pair: a sampler moves onto its texture's register, and a texture without a register takes the one DirectX would give it (Texture2D A; SamplerState S : register(s2); is slot 0). A kept register is the texture's slot, exactly as on DirectX and in mgfxc: Texture2D Tex1 : register(t1) is slot 1 even with nothing at t0, so a texture you set through effect.Parameters, or directly through GraphicsDevice.Textures[1], reaches the shader. (Before 2026-10-28 ShadowDusk numbered Vulkan slots by the texture's position instead, and an effect whose registers did not start at t0 or had a gap drew nothing in DesktopVK.) The rewrite applies to the Vulkan output alone: DirectX, OpenGL, and FNA bytes are untouched.
An explicit register space (register(t1, space0)) is dropped on this target: MonoGame's DesktopVK binds one descriptor set per shader stage, so a space can only send a resource where the runtime never looks (mgfxc's own build of a pixel-shader space0 draws nothing). A resource that would still land outside its stage's set is rejected with SD0229.
A declaration that comes out of a macro is handled: ShadowDusk compiles the macro-expanded source in that case. A texture or sampler declared in a form the rewrite does not recognise even then, such as two names in one declaration (Texture2D A, B;), would be numbered by DXC below the range MonoGame's Vulkan runtime binds, and is rejected with SD0228 naming it. Declare each in its own statement.
A texture and its sampler are paired from Tex.Sample(Sampler, ...) calls, also through helper functions you pass both to by name and through macros. Pairing is done for each compiled shader on its own, because each is its own descriptor layout: only the code its entry point reaches counts, and a call inside an inactive #if (#if 0, or the branch your macros do not take) counts for nothing (unless a conditional in the file tests a macro only the compiler defines, such as __HLSL_VERSION, the SD0009 warning: then every line counts, and a dead call that comes first moves the live one to a second descriptor with SD0235). So a texture read through a point sampler in one technique and a linear sampler in another, or through one sampler in the vertex shader and another in the pixel shader, is one descriptor at its own slot in each, where SpriteBatch finds it. A sampler whose texture ShadowDusk still cannot tell (an expression argument, a sampler chosen at runtime, an overloaded helper) is rejected with SD0232 rather than shipped as a descriptor the runtime cannot bind. A texture read only with Load works as with mgfxc. One texture sampled through two samplers in the same shader (linear and point, say) works too: ShadowDusk gives the second pairing its own descriptor bound to the same effect parameter and warns with SD0235, so set that texture through effect.Parameters (SpriteBatch fills only the first slot). That works for direct calls only; a second sampler reached inside a helper function is SD0232. The second descriptor stays inside the stage's texture slots (MonoGame's DesktopVK has 16 for a pixel shader and 8 for a vertex shader): it takes the next free slot above the texture's, else the lowest free one below it, never slot 0 (SpriteBatch binds its sprite there, over whatever the effect parameter set), and when no slot but slot 0 is free the shader is rejected with SD0237. The suffix ShadowDusk names that descriptor with, _SDVkAlias, is reserved on this target (SD0234). Storage resources (read-write textures, Buffer<T>, structured and byte-address buffers) are not supported by MonoGame's Vulkan effect format and are rejected with SD0233.
One shape is refused: a single sampler shared by two textures in the same live code path. Vulkan's combined-image-sampler model needs a distinct descriptor per texture, and ShadowDusk does not split that shape, so it is rejected at compile time with SD0028, located at the offending Sample call (judged per compiled shader, like the pairing above: a call its entry point never reaches, or on a line an inactive #if drops, does not count). SD0213 is the post-compile reflection backstop for shared-sampler shapes the source scan cannot see. Declare one sampler per texture.
A legacy POSITION vertex output works
A vertex shader that writes its clip-space position to the legacy : POSITION0 output (struct VOut { float4 Pos : POSITION0; ... };, the XNA spelling) is mapped to SV_Position on this target, as DirectX 11 does (issue #446, fixed 2026-10-31). Before that, DXC's SPIR-V path left it an ordinary output and DesktopVK silently drew nothing (mgfxc refuses the same file for Vulkan). Not mapped: POSITION1 and up, a semantic spelled through a macro, and a struct the vertex shader also takes as input; for those, write : SV_POSITION.
Additive by policy
Like all backends, targeting Vulkan is opt-in per compile (PlatformTarget.Vulkan / /Profile:Vulkan) and does not change OpenGL/DX11/v10 output for consumers who don't ask for it.