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.
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 DirectX / DX12 / FNA / KNI / Vulkan / browser-ANGLE gates 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. It needs no GPU, but it does needdotnet tool restore(CI has nodotnet-mgcb), so it runs from the same Windows gate script. - The direct-
.xnbgate:validation/XnbContentLoadproves the "replace your content pipeline, change no code" claim end to end. It builds each 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. Needsdotnet tool restoreand a GPU; default-ON in the Windows gate script.
See the test shader corpus for the inputs.