Validation & Evidence Ladder
ShadowDusk earns its existence on two axes, both required:
- Reach
mgfxccan't — compile.fxon Linux/macOS (no Wine, no Windows SDK) and at runtime / in-browser via WASM. - Output the reference compiler would — the compiled effect, loaded into the real runtime, renders the same image as the reference-compiled version (
mgfxcfor MonoGame/KNI.mgfx;fxc /T fx_2_0for 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)
- Compiles without error.
- The
.mgfxis structurally well-formed. - ShadowDusk's GLSL matches
mgfxc's GLSL in our own renderer. - ShadowDusk's
.mgfxloads in MonoGame'sEffectand renders likemgfxc'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 realmgfxcGL compile is confirmed to render solid black (a MojoShader/fxc codegen bug, not a ShadowDusk defect). The full 30-cellShapeBatchshape gallery also renders through ShadowDusk's GL compile as a candidate-only visibility check — all 30 shapes render visible content; no trustworthymgfxcGL oracle exists for that revision; - the DirectX SM5 PS-only corpus — 10/10 DX
.mgfxload in real MonoGame WindowsDX and render pixel-equivalent tomgfxc, via both thed3dcompiler_47oracle and the cross-platformvkd3d-shaderbackend; the same is now also true for the real-worldApos.ShapesSDF shape renderer, driven through the real package's ownShapeBatchacross its full 30-cell shape gallery (max Δ 0, both DXBC backends — thed3dcompiler_47arm against the realmgfxcgolden, thevkd3d-shaderarm 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_0in real FNA, including multi-pass effects and in-pass render states; - the Vulkan SPIR-V path — on two arms. Effects with explicit registers are pixel-diffed against the
mgfxc3.8.5 golden in a real MonoGame 3.8.5 DesktopVKEffectand match exactly (max Δ 0), covering a VS-driven non-identity transform and the upstreamApos.Shapeseffect — since expanded to the package's full 30-cellShapeBatchshape 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, becausemgfxc's Vulkan output crashes in real DesktopVK for auto-numbered (non-explicit-register) resources — a confirmed MonoGame-sideSlotOffsetbug — so no reference render exists for that corpus to diff against; - the DirectX 12 DXIL path — pixel-diffed against a real
mgfxc /platform:WindowsDX12golden (MonoGame 3.8.5's own content pipeline) in a real MonoGame WindowsDX12Effectand matches exactly (max Δ 0) across the 10/10 PS/SpriteBatch corpus, a VS-driven rig, and theApos.ShapesSDF shape renderer (custom vertex shader); the full 30-cellShapeBatchshape 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 fromdxcoob 1.7.2212.40(theVortice.Dxcpin) and the golden's from MonoGame 3.8.5's bundleddxcoob 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 —
DeferredSpriteMrtGldraws a two-output shader on real MonoGame DesktopGL with two targets bound, reads both attachments back, and pixel-diffs each against themgfxcOpenGL 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.mgfxrecord tables) could distinguish "the second output reached attachment 1" from "the second output went nowhere"; - the ShaderToy /
.glslfrontend route —ShaderToyRouteGlconvertsGradientToy.glslin process with the real converter and pixel-diffs ShadowDusk's OpenGL build againstmgfxc's build of the same converted.fxon real MonoGame DesktopGL (max Δ 0). Anmgfxcoracle 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.fxthe golden was built from before it renders, so converter drift turns it red. A DirectX arm (ShaderToyRouteDx, real MonoGameWindowsDX) 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, becausemgfxchad been refusing the converter's own output for/Profile:DirectX_11; - per-(texture, sampler)-pair sampler records —
SamplerPairsGlrenders an asymmetric function of two samplers (a symmetricdiffuse * lightwould render identically under a swap and prove nothing) so a mis-binding changes the picture, covering both the shared-SamplerStateand 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 likefxc/mgfxc, and that an explicitregister(sN)on a legacysamplerpins the unit. It is the only GL gate that does not bind every texture througheffect.Parameters[...]: it leaves unit 0 toSpriteBatch, which is what makes the numbering visible in the rendered picture at all. Both arms pixel-diff against realmgfxcOpenGL 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.
Two additive, distinct axes — Slang input and the SkSL converter
Slang input and the SkiaSharp/SkSL converter are not .mgfx backends, so neither 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/SlangCorpuscross-validates a 17-shader corpus (tests/fixtures/shaders/slang/) against the real, pinnedslangccompiler: every shader is accepted byslangcas genuine Slang (proving the corpus isn't HLSL wearing a.slangextension), 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 nomgfxcoracle for Slang input, so no route through it is ever calledmgfxc-equivalent. - Slang input, full language (
ShadowDusk.Slang, opt-in) — the separate, real-slangc-backed package (genuine Slang:import, generics,interfaceconformances).validation/SlangFullCorpusis a distinct driver from the row above: a 21-shader corpus (the 17 shipped + 4 Phase 65 Gum/generics-probe shaders) compiles through the realSlangCompileron all four reachable targets (84/84, OpenGL/DirectX_11/DirectX_12/Vulkan; FNA excluded, matching the subset gate's own precedent); 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 invocationSlangCompileruses internally, fed straight to DXC with no wrapping; and all 21 shaders load into a real MonoGameWindowsDXEffectand render (21/21). DirectX_12/Vulkan stay at the compile+structural rung — no real-Effect-load driver exists for either yet. Same rule as the subset frontend: no route through it ismgfxc-equivalent. - SkSL converter (SkiaSharp) —
SkslConverterTestsandSkslSkiaEvidenceTestsrender 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 (SkSLhalfprecision), with a positive control proving a known silent-loss failure mode is measurably absent. Skia has no reference compiler, so the evidence model is rendered-image fidelity against the original HLSL, nevermgfxc-equivalence.
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. DXIL signing is Windows-only (dxil.dll), so DX12 output is host-dependent — a Linux/macOS compile emits unsigned DXIL that retail D3D12 rejects, warned as SD0214. 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. - Eight in-process OpenGL render gates run in CI on Mesa llvmpipe (
.github/workflows/validation-render.yml):StateFidelity,CbufferModel,TextureBreadthValidation,ReservedWordGl,SamplerPairsGl,SamplerRegisterOrderGl,DeferredSpriteMrtGl, andShaderToyRouteGl. - 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 committedmgfxcgoldens, on a second rasterizer. That breadth matters for the shaders whose divergence is precision-shaped — see theDots.fxrow indocs/validation-matrix.md§7. - Four DX render gates run in CI too (issue #204, extended by issue #209), in the same workflow's
windows-latestjob, pinned to WARP (Windows' bundled software D3D rasterizer — no GPU needed):DxModernFeatures,KniWinFormsDX, theBaselineDx+CandidateDxDX11 corpus, and theBaselineDx12+CandidateDx12DX12 corpus. Opt-in viaSHADOWDUSK_DX_WARP=1(seevalidation/SharedDx/DxHeadlessRasterizer.cs), so a localdotnet runstill renders on your real GPU. - The DX12 corpus needed one more piece:
MonoGame.Framework.Nativecompiles a Vulkan window-surface probe into any DX12 build's SDL2 window creation, independent of the WARP pin, andwindows-latestships neither a Vulkan loader nor an ICD. The job installs Google SwiftShader (a CPU Vulkan ICD + loader) viajakoch/install-vulkan-sdk-actionto satisfy that probe — WARP still does the actual D3D12 rendering (issue #209). - The DX11/DX12 Apos.Shapes gallery, the ShaderToy DX route, FNA, real-KNI-desktop-GL, Vulkan, and browser-ANGLE gates still have no headless CI driver — they run from
validation/run-windows-render-gates.ps1on 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. - The forward-compatibility version matrix (v10 across MonoGame versions) lives under
validation/ForwardCompat/. - One driver there is not a render gate:
validation/MgcbPluginruns a realdotnet mgcbcontent build through the MGCB content-processor plugin and asserts the.mgfxinside the produced.xnbis byte-for-byte the CLI's, that the.xnbenvelope matches MGCB's own stock build, and that the payload differs from stock — on two MGCB versions (the pinned 3.8.4.1 and a real 3.8.5, which renumberedTargetPlatform; the 3.8.5 arm coversWeb,DesktopVK,WindowsDX12), with a decoydxcompiler.dllfirst on the child MGCB'sPATHso a fallback to an OS-search-path DXC fails loudly. It needs no GPU, but it does needdotnet tool restore(CI has nodotnet-mgcb), so it runs from the same Windows gate script. - The Content Builder gate:
validation/ContentBuilderruns a real MonoGame 3.8.5ContentBuildersubclass over the fixtures with two content roots — MonoGame's stock pair andShadowDusk.ContentPipeline's — asserts the ShadowDusk payload is byte-for-byte the CLI's and the.xnbenvelope byte-for-byte the stock build's, then loads both through a realContentManager.Load<Effect>on MonoGame 3.8.5 and requires pixel-identical renders. It is also the only run of the Builder's unguardedAssembly.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 inpack-consume.ymlon all three OSes. - The direct-
.xnbgates:validation/XnbContentLoad(MonoGame WindowsDX) andvalidation/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 stockdotnet mgcb, once through ShadowDusk's own XNB writer with MGCB never involved — loads both through a realContentManager.Load<Effect>(assetName), and requires the renders to be pixel-identical.validation/KniXnbContentLoaddoes 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.xnbis rejected on 4.2 — the KNI fact behind the writer's XNA-4.0 type-reader name.validation/FnaValidationloads every gate shader's.fxbthrough a real FNAContentManageras a third arm. All needdotnet tool restoreand 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/SlangCorpusdownloads and SHA-256-verifies the realslangccompiler 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 theImageTestsrecipe 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/SlangFullCorpususes 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. Needs a DirectX_11 GPU for the real-Effect-load gate plus the ordinary GL 3.3 context for the pixel-equivalence gate; default-ON in the Windows gate script.
See the test shader corpus for the inputs.