Validation & Evidence Ladder

ShadowDusk earns its existence on two axes, both required:

  1. Reach mgfxc can't — compile .fx on Linux/macOS (no Wine, no Windows SDK) and at runtime / in-browser via WASM.
  2. Output the reference compiler would — the compiled effect, loaded into the real runtime, renders the same image as the reference-compiled version (mgfxc for MonoGame/KNI .mgfx; fxc /T fx_2_0 for FNA .fxb).

The product is the combination: the same result mgfxc gives, produced where mgfxc can't run.

The bar: in-engine behavioral equivalence

The measure is what a player sees in a real MonoGame game, not "ShadowDusk's own tests pass." Unit tests, structural .mgfx tests, and images from ShadowDusk's own renderer are necessary proxies, not the bar — a proxy can be green while the real goal is unmet.

Evidence ladder (weakest → strongest)

  1. Compiles without error.
  2. The .mgfx is structurally well-formed.
  3. ShadowDusk's GLSL matches mgfxc's GLSL in our own renderer.
  4. ShadowDusk's .mgfx loads in MonoGame's Effect and renders like mgfxc's in the real runtime. ← only this proves the promise.

Rung 4 is proven for:

  • the OpenGL SM3 PS-only corpus — 10/10 render pixel-equivalent in real MonoGame DesktopGL; Apos.Shapes (Gum's SDF shape renderer) also render-proven on real MonoGame DesktopGL (max Δ 2/255 — documented transcendental-math GLSL-dialect drift on the shader's OkLab round-trip), via apos-shapes.fx — a different, older vendored revision than the DX/Vulkan Apos.Shapes proof uses, because the later revision's real mgfxc GL compile is confirmed to render solid black (a MojoShader/fxc codegen bug, not a ShadowDusk defect). The full 30-cell ShapeBatch shape gallery also renders through ShadowDusk's GL compile as a candidate-only visibility check — all 30 shapes render visible content; no trustworthy mgfxc GL oracle exists for that revision;

  • the DirectX SM5 PS-only corpus — 10/10 DX .mgfx load in real MonoGame WindowsDX and render pixel-equivalent to mgfxc, via both the d3dcompiler_47 oracle and the cross-platform vkd3d-shader backend; the same is now also true for the real-world Apos.Shapes SDF shape renderer, driven through the real package's own ShapeBatch across its full 30-cell shape gallery (max Δ 0, both DXBC backends — the d3dcompiler_47 arm against the real mgfxc golden, the vkd3d-shader arm against the package's own vkd3d-compiled embedded effect);

  • the KNI WebGL path — render-equivalent in a real headless KNI WebGL run;

  • the FNA target — the PS-only and custom-vertex-shader corpora render pixel-equivalent (max Δ ≤ 1/255, an imperceptible difference) to fxc /T fx_2_0 in real FNA, including multi-pass effects and in-pass render states, and a shader's initializer defaults (matrix and array included) read back and render unset exactly as fxc's build does, on both the D3D11 and OpenGL FNA3D drivers;

  • the Vulkan SPIR-V path — on two arms. Effects with explicit registers are pixel-diffed against the mgfxc 3.8.5 golden in a real MonoGame 3.8.5 DesktopVK Effect and match exactly (max Δ 0), covering a VS-driven non-identity transform, explicit registers that do not start at t0 or leave a gap (a lone register(t1), t0 + t2, a Texture2DArray on t1, and one texture read through a different sampler per technique or per stage, drawn by SpriteBatch alone: VsDrivenVulkan -- regslots), and the upstream Apos.Shapes effect — since expanded to the package's full 30-cell ShapeBatch shape gallery (max Δ 0 against the package's own DXC-family embedded effect). The 10/10 PS corpus (profile byte 80) is instead in-engine render-proven on its own output, because mgfxc's Vulkan output crashes in real DesktopVK for auto-numbered (non-explicit-register) resources — a confirmed MonoGame-side SlotOffset bug — so no reference render exists for that corpus to diff against;

  • the DirectX 12 DXIL path — pixel-diffed against a real mgfxc /platform:WindowsDX12 golden (MonoGame 3.8.5's own content pipeline) in a real MonoGame WindowsDX12 Effect and matches exactly (max Δ 0) across the 10/10 PS/SpriteBatch corpus, a VS-driven rig, and the Apos.Shapes SDF shape renderer (custom vertex shader); the full 30-cell ShapeBatch shape gallery lands within 1/255 against the same golden, differing on 11 pixels out of 402,984 — root-caused to the pinned DXC build, not a ShadowDusk defect: ShadowDusk's DXIL comes from dxcoob 1.7.2212.40 (the Vortice.Dxc pin) and the golden's from MonoGame 3.8.5's bundled dxcoob 1.8.2505.32, and putting ShadowDusk's own preprocessed HLSL and own flags through a DXC 1.8 build reproduces the golden's DXIL instruction-for-instruction and renders at max Δ 0;

  • multiple render targets — DeferredSpriteMrtGl draws a two-output shader on real MonoGame DesktopGL with two targets bound, reads both attachments back, and pixel-diffs each against the mgfxc OpenGL golden (max Δ 0 on both). Every other GL gate binds a single target, so none of them (and no structural check — that output lives in the emitted GLSL, not the .mgfx record tables) could distinguish "the second output reached attachment 1" from "the second output went nowhere";

  • the ShaderToy / .glsl frontend route — ShaderToyRouteGl converts GradientToy.glsl in process with the real converter and pixel-diffs ShadowDusk's OpenGL build against mgfxc's build of the same converted .fx on real MonoGame DesktopGL (max Δ 0). An mgfxc oracle exists here because "no oracle" is true only of ShaderToy input: the converter's output is ordinary HLSL. The gate asserts the converter still emits the committed .fx the golden was built from before it renders, so converter drift turns it red. A DirectX arm (ShaderToyRouteDx, real MonoGame WindowsDX) exists since 2026-07-31 and is default-ON in the Windows gate script; it became possible only once the converter started emitting a DirectX-valid profile header, because mgfxc had been refusing the converter's own output for /Profile:DirectX_11;

  • per-(texture, sampler)-pair sampler records — SamplerPairsGl renders an asymmetric function of two samplers (a symmetric diffuse * light would render identically under a swap and prove nothing) so a mis-binding changes the picture, covering both the shared-SamplerState and the reverse-first-use shapes that Phase 51 A7 fixed.

  • OpenGL sampler slot allocation — SamplerRegisterOrderGl (issue #189) proves texture units are allocated in HLSL declaration order like fxc/mgfxc, and that an explicit register(sN) on a legacy sampler pins the unit. It is the only GL gate that does not bind every texture through effect.Parameters[...]: it leaves unit 0 to SpriteBatch, which is what makes the numbering visible in the rendered picture at all. Both arms pixel-diff against real mgfxc OpenGL goldens (max Δ 0; each measured max Δ 255 before the fix). Every other GL gate was green throughout the defect, because a first-use-numbered table is internally consistent when the effect binds every unit itself. Its "param-names" arm binds a bare legacy sampler2D MaskA; only through effect.Parameters["MaskA"], mgfxc's name for it, and fails if ShadowDusk names the parameter anything else.

  • KNI's own effect test suite, on eight runtimes — the validation/KniEffectTest* drivers run every test of KNI's Tests/Framework.Graphics/EffectTest.cs (checked against XNA by KNI's author) on KNI's vendored test shaders plus five companions, in real KNI 4.2.9001 and 4.3.9001 SDL2.GL and WinForms.DX11 (MGFX v10 and KNIFX) and real MonoGame DesktopGL, WindowsDX, WindowsDX12 and DesktopVK, beside the real mgfxc build and, on KNI 4.3.9001, KNI's own prebuilt .xnb. Every gated row passes and every positive control is caught; the rows still known-failing are ShadowDusk bugs with open issues (none today: the last two, #446 and #453, were fixed 2026-10-31). Per-runtime counts are in docs/validation-matrix.md.

  • OpenGL vertex-input semantics and matrix interpolants — VertexSemanticsGl (MonoGame DesktopGL) and VertexSemanticsKniGl (KNI SDL2.GL, MGFX v10 and KNIFX) draw through a custom VertexDeclaration with one element per semantic (BINORMAL, TANGENT, BLENDINDICES, BLENDWEIGHT 0 and 1, PSIZE, FOG, DEPTH, TEXCOORD3, ...) and a float3x3 interpolant, plus the matrix-interpolant packing fixtures, each against the mgfxc OpenGL build drawn in the same process (max Δ 0) and the element's CPU-known value, with positive controls that must diverge.

  • DirectX 11 legacy-sampler texture slots — DxModernFeatures and KniWinFormsDX draw each legacy-sampler shape (the XNA BloomCombine.fx layout among them) with SpriteBatch's sprite on texture slot 0 and pixel-diff it against the committed mgfxc DirectX_11 golden (max Δ 0) on both DXBC backends, in real MonoGame WindowsDX and in real KNI WinForms.DX11 (MGFX and KNIFX), each with a swapped-slot positive control.

  • KNIFX DirectX ps_2_0 / ps_3_0 — InitializerDefaultsKniDx loads a KNIFX effect written at vs_2_0/ps_2_0 (HiDef and Reach) and vs_3_0/ps_3_0 (HiDef) in real KNI WinForms.DX11 and renders it pixel-identical to KNI's own build of the same effect.

  • Global-initializer defaults — the six InitializerDefaults* drivers (MonoGame DesktopGL, WindowsDX, WindowsDX12, DesktopVK; KNI SDL2.GL and WinForms.DX11, MGFX v10 and KNIFX) prove a global's initializer is the parameter's default with no Set call, with a no-initializer control that must render black (and, on GL, the real mgfxc build, which stores zeros); FnaValidation -- defaults proves the same against fxc /T fx_2_0 on both FNA3D drivers.

  • Non-square matrix parameters — NonSquareMatrixDx (MonoGame WindowsDX) and NonSquareMatrixVulkan (MonoGame DesktopVK) set float4x3, float3x2, float2x3, float4x3[2] and trailing-matrix parameters from C# and check every gated element lands where MonoGame's own DirectX semantics put it, with the committed real mgfxc 3.8.5 builds rendered alongside (issue #423).

Additive, distinct axes — Slang input, the SkSL converter, and the raylib converter

Slang input, the SkiaSharp/SkSL converter, and the raylib converter are not .mgfx backends, so none of them sits on the rung-4 ladder above; each earns its own evidence bar, because there is no mgfxc oracle for either.

  • Slang input, HLSL-compatible subset (ShadowDusk.Compiler, free) — validation/SlangCorpus cross-validates a 17-shader corpus (tests/fixtures/shaders/slang/) against the real, pinned slangc compiler: every shader is accepted by slangc as genuine Slang (proving the corpus isn't HLSL wearing a .slang extension), and an 8-shader uniform-free procedural subset renders pixel-identical (max Δ 0) through ShadowDusk's route vs through slangc's own HLSL emission, both fed to the same DXC + SPIRV-Cross. All 17 also convert and compile on OpenGL and DirectX in-suite (SlangCorpusCompileTests). There is no mgfxc oracle for Slang input, so no route through it is ever called mgfxc-equivalent.
  • Slang input, full language (ShadowDusk.Slang, opt-in) — the separate, real-slangc-backed package (genuine Slang: import, generics, interface conformances). validation/SlangFullCorpus is a distinct driver from the row above: a 21-shader corpus (the 17 shipped + 4 Phase 65 Gum/generics-probe shaders) compiles through the real SlangCompiler on all four MGFX targets (84/84, OpenGL/DirectX_11/DirectX_12/Vulkan); the 8-shader uniform-free procedural subset renders pixel-identical (max Δ 0) on OpenGL between ShadowDusk's .fx-wrapped route and the exact same slangc invocation SlangCompiler uses internally, fed straight to DXC with no wrapping; and all 21 shaders load into a real MonoGame WindowsDX Effect and render (21/21). validation/SlangFullCorpusDx12, validation/SlangFullCorpusVulkan and validation/FnaValidation -- slang (issue #230) render the same 21 in the real engine next to the reference compiler's build of the identical assembled .fx (real mgfxc 3.8.5 for DirectX_12 and Vulkan, fxc /T fx_2_0 for FNA): 21/21 on each, max Δ 0 / 1 / 1, with two positive controls per arm that must diverge. validation/SlangTexturedGl (issue #252) renders the six textured pixel-only corpus shaders in real MonoGame DesktopGL with the texture left to SpriteBatch on unit 0, checks each .mgfx sampler table, and compares against the shader's own math on the CPU (and Invert against the mgfxc golden of the equivalent .fx, max Δ 0); its InvertComb row (issue #302) sets a second texture through a combined Sampler2D Comb by the author's name, effect.Parameters["Comb"], in the real engine (max Δ 1). Same rule as the subset frontend: mgfxc cannot read Slang, so the claim is about the .fx the route assembles, never Slang input itself being mgfxc-equivalent.
  • SkSL converter (SkiaSharp) — SkslConverterTests and SkslSkiaEvidenceTests render the converter's SkSL emissions in real SkiaSharp (CPU raster, a test-only dependency) and assert the result matches the original HLSL's analytically computed math at ±2/255 (SkSL half precision), with a positive control proving a known silent-loss failure mode is measurably absent. validation/SkiaVsKni (issue #369) is the cross-renderer form: XnaFiddle's texture-only examples (Fading, Grayscale, Invert, Pixelated, Tint, and Mask with a second texture) go through the converter into real SkiaSharp (the texture as the child shader, ShadowDusk_Color = the tint, ShadowDusk_Resolution = the child size) and through the OpenGL backend into real KNI SDL2.GL (SpriteBatch, the tint as the vertex color), same texels, untinted and tinted: 12/12 within ±2/255 (max Δ 0-1). Three positive controls must diverge or the gate fails: the tint left white on Skia only, the UV's V flipped, and ShadowDusk_Resolution halved. It runs in the Linux GL CI lane and in the Windows gate script. Skia has no reference compiler, so the evidence model is rendered-image fidelity against the original HLSL and against the proven GL backend, never mgfxc-equivalence. Slang input is judged in real SkiaSharp too: SkslSlangSubsetRouteTests (built-in subset) and SlangSkslRouteTests (real slangc, Category=Integration) render 7 .slang twins of .fx fixtures against the .fx-sourced SkSL at maxd 0, each with a mutated-twin positive control.
  • raylib converter (Raylib-cs) — validation/RaylibRoute renders each conversion in real Raylib-cs (raylib 6.0, LoadShaderFromMemory(null, fs) behind raylib's own vertex shader) and pixel-diffs it against the same .fx built for OpenGL and rendered in real MonoGame DesktopGL (both OpenGL, so this stays a same-backend comparison): 15 shaders (the 10-shader GL corpus, a CRT and a handheld-LCD effect, Gum's Grayscale, and since issue #327 the two legacy-sampler fixtures SamplerLegacyInclude.fx and SamplerLegacyMacroDecl.fx, whose samplers an #include or a macro supplies) at max Δ 0, tolerance ±2/255. Three positive controls must diverge or the gate fails: the emission with its fragColor tint dropped, with V flipped, and with the CRT curvature nudged by 0.01. A raylib shader-load failure (raylib silently falls back to its default shader) is a hard failure. Runs in the Linux GL CI lane. raylib has no reference compiler, so the evidence model is rendered-image fidelity (owner decision 2026-10-01), never mgfxc-equivalence. RaylibConverterTests pins the emitted interface and every refusal. Twelve .slang arms (six Slang twins, each through the built-in subset and real slangc, one of them sampling its second-declared texture first, beside its own 16th .fx case TwoTextureOrder) render in real Raylib-cs against the .fx case's MonoGame image, with a mutated-twin control (issue #253).

Compare same-backend, never cross-backend

Validation always compares ShadowDusk vs mgfxc on the same target (GL↔GL, DX↔DX) — never OpenGL output against DirectX output. Each backend is a separate emitted artifact (OpenGL = GLSL text; DirectX = GPU bytecode) loaded by a different runtime path, so a green OpenGL result says nothing about DirectX. A shipped game runs exactly one backend; each must be produced and validated on its own.

"Same .mgfx" ≠ byte-identical to mgfxc

"Same .mgfx output" means behaviorally equivalent and Effect-loadable. ShadowDusk and mgfxc are different compilers; byte-equality with mgfxc is neither expected nor a goal. The "deterministic / byte-identical" constraint refers only to ShadowDusk's own reproducibility: same ShadowDusk version + same source + same target → same bytes.

One carve-out: DirectX12. The DXIL bitcode embeds the compiling DXC's own identity string, so DX12 raw bytes differ per host (the container is signed on every host since issue #478, by dxil.dll on Windows and a byte-identical managed hash elsewhere). The byte-identity manifest covers DirectX_Vkd3d, FNA, and OpenGL only.

Where the harnesses live

  • The render-validation harness is under validation/ in the repository.
  • Fourteen in-process OpenGL render gates run in CI on Mesa llvmpipe (.github/workflows/validation-render.yml): StateFidelity, CbufferModel, TextureBreadthValidation, ReservedWordGl, SamplerPairsGl, SamplerRegisterOrderGl, DeferredSpriteMrtGl, ShaderToyRouteGl, RaylibRoute, SkiaVsKni, InitializerDefaultsGl, InitializerDefaultsKniGl, and VertexSemanticsGl / VertexSemanticsKniGl (every GL vertex-input semantic and a float3x3 interpolant through a custom VertexDeclaration, in real MonoGame and real KNI, vs the mgfxc build).
  • The 10-shader GL corpus also runs there (Baseline + Candidate + compare.py, added 2026-09-10): the oracle-backed pixel diff of ShadowDusk's OpenGL output against the committed mgfxc goldens, on a second rasterizer. That breadth matters for the shaders whose divergence is precision-shaped — see the Dots.fx row in docs/validation-matrix.md §7.
  • Four DX render gates run in CI too (issue #204, extended by issue #209), in the same workflow's windows-latest job, pinned to WARP (Windows' bundled software D3D rasterizer — no GPU needed): DxModernFeatures, KniWinFormsDX, the BaselineDx+CandidateDx DX11 corpus, and the BaselineDx12+CandidateDx12 DX12 corpus. Since issue #254 the same job also runs SlangFullCorpus gates 1 and 3 (the real-slangc compile sweep and the real WindowsDX Effect load) and all four VsDrivenDx modes (the DX11 VS rig, the Apos.Shapes gallery, and the texture-array and sampler-array rows), each comparing ShadowDusk's build with the committed mgfxc golden rendered in the same process on the same WARP device, and ShaderToyRouteDx (the ShaderToy route's converted .fx vs the committed mgfxc DirectX_11 golden, drawn the same way). Since 2026-10-28 it also runs InitializerDefaultsDx, InitializerDefaultsKniDx and InitializerDefaultsDx12 (a global initializer is the parameter's default with no Set call, with a no-initializer control that must render black) and NonSquareMatrixDx (non-square matrix parameters set from C#, issue #423, against MonoGame's DirectX semantics and the committed real mgfxc 3.8.5 DirectX_11 builds). DxModernFeatures and KniWinFormsDX also carry the legacy-sampler texture-slot rows: SpriteBatch puts the sprite on texture slot 0, so each shape (the XNA BloomCombine.fx sampler layout among them) must draw exactly like the committed mgfxc DirectX_11 golden, with a swapped-slot positive control, compiled with both DXBC backends; the DX11 corpus also runs a second time through the public d3dcompiler_47 backend option (CandidateDx -- d3dcompiler). Opt-in via SHADOWDUSK_DX_WARP=1 (see validation/SharedDx/DxHeadlessRasterizer.cs), so a local dotnet run still renders on your real GPU.
  • The DX12 corpus needed one more piece: MonoGame.Framework.Native compiles a Vulkan window-surface probe into any DX12 build's SDL2 window creation, independent of the WARP pin, and windows-latest ships neither a Vulkan loader nor an ICD. The job installs Google SwiftShader (a CPU Vulkan ICD + loader) via jakoch/install-vulkan-sdk-action to satisfy that probe — WARP still does the actual D3D12 rendering (issue #209).
  • The Vulkan gates run in CI too (since 2026-10-01), in a third job on ubuntu: VsDrivenVulkan (the VS-driven fixture vs the mgfxc golden, then the Apos.Shapes gallery, then the explicit-register-slot row, plus the wave and texarr rows that must be refused with SD0218 and SD0221) and the CandidateVulkan corpus, and since 2026-10-28 InitializerDefaultsVulkan (global-initializer defaults with no Set call), NonSquareMatrixVulkan (issue #423's non-square Set cases against MonoGame's DirectX semantics) and KniEffectTestVulkan (below), all render on real MonoGame DesktopVK through Mesa lavapipe (a CPU Vulkan driver) under xvfb. The job raises vm.max_map_count first: MonoGame 3.8.5 DesktopVK holds about 13,500 lavapipe mappings per shader, and the image's default crashes the driver after about 19 shaders. validation/run-with-vk-validation.sh forces the Khronos validation layer on for every run and fails the gate on any layer error, because a CPU driver can accept what a GPU driver rejects. Six positive controls in the same job must each turn it red: a wrong shader (the pixel diff must catch it), an out-of-spec SPIR-V version (the validation layer must catch it), and four defects planted in the regslots row's candidate (texture slots rewritten to the old ordinal numbering, an explicit register space kept, a dead sampler's register reserved, and the old whole-file sampler pairing). The same job also runs VsDrivenVulkan -- legacy-lod: tex2Dlod (vertex and pixel stage), tex2Dbias and tex2Dproj in legacy sampler syntax, plus MonoGame's own VertexTextureEffect.fx, against mgfxc 3.8.5's builds, with four built-in positive controls; DxModernFeatures carries the DirectX 11 arm of the same scene in the WARP job, and VsDrivenDx12 -- legacy-lod the DirectX 12 arm in the Windows gate script.
  • KNI's own effect test suite is wired into all three CI jobs (2026-10-28; green in CI since 2026-10-29, run 40967278410 on the merge of PR #450, all three jobs): the validation/KniEffectTest* drivers port every test of KNI's Tests/Framework.Graphics/EffectTest.cs (written and checked against XNA by KNI's author) to a real engine, on KNI's test shaders vendored under tests/fixtures/shaders/third-party/KNI. KniEffectTestKniGl (real KNI SDL2.GL, built against both KNI 4.2.9001 and 4.3.9001) and KniEffectTestGl run on llvmpipe, KniEffectTestKniDx (both KNI lines), KniEffectTestDx and KniEffectTestDx12 on WARP, and KniEffectTestVulkan on lavapipe under the validation layer. Each prints a table of GATE rows (must pass), KNOWN-FAIL rows (a listed ShadowDusk bug, which must fail for that bug's reason: the run turns red if one starts passing, so the PR that fixes it must flip the row, and red if it fails some other way) and INFO rows (runtime-only tests, runtime bugs, inherent MGFX limits, reference arms), each with its reason, and positive controls that must be caught. All of them are also in the Windows gate script.
  • The DX12 VS-driven/Apos.Shapes gallery, FNA, the real-KNI-desktop-GL corpus (KniDesktopGL) and KniVsDriven, and browser-ANGLE gates still have no headless CI driver (real KNI SDL2.GL itself is in the llvmpipe job through KniEffectTestKniGl, InitializerDefaultsKniGl, VertexSemanticsKniGl and SkiaVsKni) — they run from validation/run-windows-render-gates.ps1 on a Windows box with a GPU, which is the pre-merge and pre-release bar for anything touching shader output.
  • Cross-platform compile reach is exercised by CI (.github/workflows/ci.yml) on Linux, macOS, and Windows.
  • Cross-host DXC output (Vulkan and DirectX 12): DxcTargetsCrossHostByteIdentityTests (integration lane, every OS) compiles the whole fixture corpus for both targets, release and Debug, against one win-x64 manifest (tests/fixtures/golden/byte-identity/dxc-targets-manifest.json). Vulkan must match byte for byte; DirectX 12 byte for byte on Windows and, elsewhere, apart from the signature, DXC's identity string and the disassembler's locale-dependent non-ASCII escaping. The desktop hosts load different DXC builds, so this is what proves their outputs agree.
  • The forward-compatibility version matrix (v10 across MonoGame versions) lives under validation/ForwardCompat/.
  • Windows on Arm is proven by byte identity, in CI (issue #286): .github/workflows/win-arm64.yml (PR label run-win-arm64, manual dispatch, weekly) compiles every fixture .fx for OpenGL, Vulkan and DirectX 12 on windows-latest, then reruns the same tests as a native arm64 process on windows-11-arm and requires identical results (ArchitectureByteIdentityTests). It also asserts DirectX 11 and FNA are SD0211 there (no win-arm64 vkd3d-shader) and that ShadowDusk.Slang is SD0620 (no win-arm64 slangc is bundled). No game is rendered on Windows on Arm; the win-x64 render proofs carry over because the bytes are the same.
  • The Android on-device checks run in CI (issue #304): .github/workflows/android-emulator.yml boots an API-34 x86_64 emulator on ubuntu (KVM) and runs validation/AndroidGl/run-dxc-identity-checks.ps1, which builds the real .NET-for-Android MonoGame app five times. The pinned natives compile an HLSL string on the device into a live Effect; a foreign or missing DXC must be refused with SD0219, a foreign or missing SPIRV-Cross with SD0103. On a PR it is opt-in via the run-android label; it also runs weekly and on relevant pushes to main.
  • One driver there is not a render gate: validation/MgcbPlugin runs a real dotnet mgcb content build through the MGCB content-processor plugin and asserts the .mgfx inside the produced .xnb is byte-for-byte the CLI's, that the .xnb envelope matches MGCB's own stock build, and that the payload differs from stock. On the MGFX v11 targets (DesktopVK, WindowsDX12) it also pins the per-shader source-file string: <unknown> like the stock build, never the build machine's path, the rest of the payload equal to the CLI's, a byte-identical .xnb when the same effect is built from a second directory, the same .xnb under /config:Debug with the default DebugMode=Auto (as stock MGCB, whose own /config:Debug build is checked too), and the CLI's own .xnb payload equal to the plugin's — on three MGCB versions (the pinned 3.8.4.1, a real 3.8.5, which renumbered TargetPlatform, and a real 3.8.6; the 3.8.5 and 3.8.6 arms cover Web, DesktopVK, WindowsDX12), with a decoy dxcompiler.dll first on the child MGCB's PATH so a fallback to an OS-search-path DXC fails loudly. It needs no GPU, but it does need dotnet tool restore (CI has no dotnet-mgcb), so it runs from the same Windows gate script.
  • Newer MonoGame releases: every MonoGame driver takes -p:MonoGameVersion=<v> (validation/MonoGameVersion.targets), building it against that release with its own lock file and a guard that fails the run unless that release actually loaded. The Windows render gate re-runs the MonoGame gates on each release in its $newerMonoGame list (3.8.6 today) beside the pinned arms, including MonoGame 3.8.6's native OpenGL 4.1 runtime DesktopGL4 (-p:MonoGameDesktopGL4=true); -NewerMonoGameOnly runs just that block. CI's render-gate jobs (validation-render.yml) each end with the core of that block on 3.8.6. ContentPipelineApiBindingTests (under dotnet test) checks that every MonoGame member the content plugins call exists in each supported content-pipeline release.
  • The Content Builder gate: validation/ContentBuilder runs a real MonoGame 3.8.5 ContentBuilder subclass over the fixtures with two content roots — MonoGame's stock pair and ShadowDusk.ContentPipeline's — asserts the ShadowDusk payload is byte-for-byte the CLI's and the .xnb envelope byte-for-byte the stock build's (with the same MGFX v11 source-file checks on its DesktopVK and WindowsDX12 passes, including a second Builder run from a different source directory), then loads both through a real ContentManager.Load<Effect> on MonoGame 3.8.5 and requires pixel-identical renders. It is also the only run of the Builder's unguarded Assembly.GetTypes() scan over ShadowDusk's real dependency graph. Needs a GPU and the 3.8.5 runtime; default-ON in the Windows gate script. The packed package is additionally consumed cold by a scratch Builder in pack-consume.yml on all three OSes.
  • The direct-.xnb gates: validation/XnbContentLoad (MonoGame WindowsDX) and validation/XnbContentLoadGl (MonoGame DesktopGL, same driver source) prove the "replace your content pipeline, change no code" claim end to end. Each builds every fixture twice — once through stock dotnet mgcb, once through ShadowDusk's own XNB writer with MGCB never involved — loads both through a real ContentManager.Load<Effect>(assetName), and requires the renders to be pixel-identical. validation/KniXnbContentLoad does the same on real KNI, built against both 4.2.9001 and 4.3.9001 (v10 and KNIFX payloads), and pins that a stock mgcb .xnb is rejected on 4.2 — the KNI fact behind the writer's XNA-4.0 type-reader name. validation/FnaValidation loads every gate shader's .fxb through a real FNA ContentManager as a third arm. All need dotnet tool restore and a GPU; the MonoGame and KNI ones are default-ON in the Windows gate script, the FNA one rides -IncludeFna.
  • The Slang cross-validation driver: validation/SlangCorpus downloads and SHA-256-verifies the real slangc compiler as a test-time oracle (never shipped, never invoked by the product), then runs the two gates described above. Needs no GPU beyond the ordinary GL 3.3 context the ImageTests recipe already uses; default-ON in the Windows gate script. The in-suite half (SlangCorpusCompileTests, no oracle needed) runs in CI.
  • The Slang FULL-corpus driver (ShadowDusk.Slang): validation/SlangFullCorpus uses the same restored native slangc the product package ships (not a downloaded oracle — for this package slangc IS the route), and runs the three gates described above. Gates 1-2 run on every host with a bundled slangc (dotnet run -c Release --project validation/SlangFullCorpus -f net8.0; CI runs them on ubuntu under llvmpipe); gate 3 needs Windows (-f net8.0-windows, default-ON in the Windows gate script) and prints NOT RUN elsewhere. CI runs gates 1 and 3 on windows-latest pinned to WARP with -- --skip-gl-gate, which prints gate 2 as NOT RUN because that runner has no OpenGL 3.3 driver. Gate 3 carries its own positive control: a real .mgfx truncated to half, with its effect-cache key changed, must be rejected by the real Effect loader. validation/SlangTexturedGl (dotnet run -c Release --project validation/SlangTexturedGl) runs in the same CI lane and in the Windows gate script. tools/verify-slang-packaging.sh (run by pack-consume.yml) is the separate cold-NuGet-consumer proof for the package on all three OSes; on macOS the same workflow also runs tools/verify-slang-osx-x64-rosetta.sh, which executes the packaged osx-x64 slangc under Rosetta 2 and requires its HLSL to be byte-identical to the osx-arm64 slangc's (CI's macOS runners are arm64, so nothing else runs the x64 binary).
  • Full Slang in the browser (issue #257): .wasm-build/slang-wasm/node-test-slangc-wasm.mjs runs every corpus .slang file, entry point and target through native slangc and through the in-process slangc WebAssembly module with the identical command line and requires identical exit code, output and diagnostics (the per-entry compile and the preprocess-only -E pass that finds author-written registers); tests/ShadowDusk.BrowserTests/browser-slang-gate.mjs boots the sample in headless Chromium, compiles the corpus in the page and checks the bytes against slang-manifest.json, then renders every shipped Slang shader through both Slang front ends in KNI WebGL. Both run in wasm.yml's browser-smoke job (label run-browser), which first builds the module with .wasm-build/slang-wasm/build-slangc-wasm.ps1.
  • Browser vkd3d byte-identity (DirectX and FNA export, issue #295): tests/ShadowDusk.BrowserTests/node-test-vkd3d-wasm.mjs captures every vkd3d compile the desktop pipeline issues over the DirectX and FNA corpus, including the compile options the desktop really passed, replays each through the browser module and requires identical bytes; browser-vkd3d-gate.mjs compiles the same corpus through the real compiler in headless Chromium and checks every artifact against the cross-host byte-identity manifest. Sm3SemanticStructs.fx (Shader Model 3 semantics on struct fields) is the fixture whose output depends on those options, so a host that passes vkd3d a different list fails on it. Both run in wasm.yml's browser-smoke job. Every fixture is enforced; the browser shim refuses to load a vkd3d module that cannot take compile options. Both also hold the source text (issue #319): the node gate replays the bytes the desktop really handed vkd3d (read back from the native call, every #line directive line blanked by Vkd3dCompileContract.PrepareSource), requires that pass to print no #line directive fixme line on an intercepted stderr, and replays the directive-carrying text as a control that must give the same bytes and one fixme per directive; the browser gate requires the page console to carry no such line over the corpus, after a control message is seen to reach its listener. And both hold the warnings (issue #335): the node gate records the verbatim message text vkd3d returned to the desktop for every corpus compile and requires the browser module to hand back the identical text beside identical bytes (with a control that fails when no corpus compile carries one; ImplicitTruncationWarning.fx does, vkd3d's W5300); the browser gate compares the CompiledShader.Warnings the real compiler returns, relocated onto the author's line, against the committed warnings-manifest.json that CrossHostByteIdentityTests regenerates and asserts beside the byte manifest on every CI OS.
  • WASM stack depth (issue #271): tests/ShadowDusk.BrowserTests/node-test-wasm-depth.mjs captures the desktop output for deeply nested shaders (long expression chains, nested parentheses, ternaries and ifs, long else if chains, deep call chains) and requires the browser DXC, SPIRV-Cross and vkd3d modules to produce the same bytes, each case in its own process so a hang shows up as a timeout. It then makes each module trap on far deeper source and checks the trapped instance is discarded and reloads correctly; browser-vkd3d-gate.mjs checks the same through the real compiler in Chromium (SD1907, then a correct compile after the reload). Runs in wasm.yml's browser-smoke job. Measurements: .wasm-build/WASM-STACK-DEPTH.md.

See the test shader corpus for the inputs.