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, and a shader's initializer defaults (matrix and array included) read back and render unset exactly asfxc'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
mgfxc3.8.5 golden in a real MonoGame 3.8.5 DesktopVKEffectand match exactly (max Δ 0), covering a VS-driven non-identity transform, explicit registers that do not start att0or leave a gap (a loneregister(t1),t0+t2, aTexture2DArrayont1, and one texture read through a different sampler per technique or per stage, drawn by SpriteBatch alone:VsDrivenVulkan -- regslots), 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. Its "param-names" arm binds a bare legacysampler2D MaskA;only througheffect.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'sTests/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 realmgfxcbuild 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 indocs/validation-matrix.md.OpenGL vertex-input semantics and matrix interpolants —
VertexSemanticsGl(MonoGame DesktopGL) andVertexSemanticsKniGl(KNI SDL2.GL, MGFX v10 and KNIFX) draw through a customVertexDeclarationwith one element per semantic (BINORMAL,TANGENT,BLENDINDICES,BLENDWEIGHT0 and 1,PSIZE,FOG,DEPTH,TEXCOORD3, ...) and afloat3x3interpolant, plus the matrix-interpolant packing fixtures, each against themgfxcOpenGL 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 —
DxModernFeaturesandKniWinFormsDXdraw each legacy-sampler shape (the XNABloomCombine.fxlayout among them) with SpriteBatch's sprite on texture slot 0 and pixel-diff it against the committedmgfxcDirectX_11golden (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—InitializerDefaultsKniDxloads a KNIFX effect written atvs_2_0/ps_2_0(HiDef and Reach) andvs_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 noSetcall, with a no-initializer control that must render black (and, on GL, the realmgfxcbuild, which stores zeros);FnaValidation -- defaultsproves the same againstfxc /T fx_2_0on both FNA3D drivers.Non-square matrix parameters —
NonSquareMatrixDx(MonoGame WindowsDX) andNonSquareMatrixVulkan(MonoGame DesktopVK) setfloat4x3,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 realmgfxc3.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/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 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 invocationSlangCompileruses internally, fed straight to DXC with no wrapping; and all 21 shaders load into a real MonoGameWindowsDXEffectand render (21/21).validation/SlangFullCorpusDx12,validation/SlangFullCorpusVulkanandvalidation/FnaValidation -- slang(issue #230) render the same 21 in the real engine next to the reference compiler's build of the identical assembled.fx(realmgfxc3.8.5 for DirectX_12 and Vulkan,fxc /T fx_2_0for 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.mgfxsampler table, and compares against the shader's own math on the CPU (andInvertagainst themgfxcgolden of the equivalent.fx, max Δ 0); itsInvertCombrow (issue #302) sets a second texture through a combinedSampler2D Combby the author's name,effect.Parameters["Comb"], in the real engine (max Δ 1). Same rule as the subset frontend:mgfxccannot read Slang, so the claim is about the.fxthe route assembles, never Slang input itself beingmgfxc-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.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, andShadowDusk_Resolutionhalved. 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, nevermgfxc-equivalence. Slang input is judged in real SkiaSharp too:SkslSlangSubsetRouteTests(built-in subset) andSlangSkslRouteTests(real slangc,Category=Integration) render 7.slangtwins of.fxfixtures against the.fx-sourced SkSL at maxd 0, each with a mutated-twin positive control. - raylib converter (Raylib-cs) —
validation/RaylibRouterenders 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.fxbuilt 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 fixturesSamplerLegacyInclude.fxandSamplerLegacyMacroDecl.fx, whose samplers an#includeor a macro supplies) at max Δ 0, tolerance ±2/255. Three positive controls must diverge or the gate fails: the emission with itsfragColortint 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), nevermgfxc-equivalence.RaylibConverterTestspins the emitted interface and every refusal. Twelve.slangarms (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.fxcaseTwoTextureOrder) render in real Raylib-cs against the.fxcase'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, andVertexSemanticsGl/VertexSemanticsKniGl(every GL vertex-input semantic and afloat3x3interpolant through a customVertexDeclaration, in real MonoGame and real KNI, vs themgfxcbuild). - 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. Since issue #254 the same job also runsSlangFullCorpusgates 1 and 3 (the real-slangc compile sweep and the real WindowsDXEffectload) and all fourVsDrivenDxmodes (the DX11 VS rig, the Apos.Shapes gallery, and the texture-array and sampler-array rows), each comparing ShadowDusk's build with the committedmgfxcgolden rendered in the same process on the same WARP device, andShaderToyRouteDx(the ShaderToy route's converted.fxvs the committedmgfxcDirectX_11golden, drawn the same way). Since 2026-10-28 it also runsInitializerDefaultsDx,InitializerDefaultsKniDxandInitializerDefaultsDx12(a global initializer is the parameter's default with noSetcall, with a no-initializer control that must render black) andNonSquareMatrixDx(non-square matrix parameters set from C#, issue #423, against MonoGame's DirectX semantics and the committed realmgfxc3.8.5DirectX_11builds).DxModernFeaturesandKniWinFormsDXalso carry the legacy-sampler texture-slot rows: SpriteBatch puts the sprite on texture slot 0, so each shape (the XNABloomCombine.fxsampler layout among them) must draw exactly like the committedmgfxcDirectX_11golden, with a swapped-slot positive control, compiled with both DXBC backends; the DX11 corpus also runs a second time through the publicd3dcompiler_47backend option (CandidateDx -- d3dcompiler). 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 Vulkan gates run in CI too (since 2026-10-01), in a third job on ubuntu:
VsDrivenVulkan(the VS-driven fixture vs themgfxcgolden, then the Apos.Shapes gallery, then the explicit-register-slot row, plus thewaveandtexarrrows that must be refused withSD0218andSD0221) and theCandidateVulkancorpus, and since 2026-10-28InitializerDefaultsVulkan(global-initializer defaults with noSetcall),NonSquareMatrixVulkan(issue #423's non-squareSetcases against MonoGame's DirectX semantics) andKniEffectTestVulkan(below), all render on real MonoGame DesktopVK through Mesa lavapipe (a CPU Vulkan driver) under xvfb. The job raisesvm.max_map_countfirst: 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.shforces 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 theregslotsrow'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 runsVsDrivenVulkan -- legacy-lod:tex2Dlod(vertex and pixel stage),tex2Dbiasandtex2Dprojin legacy sampler syntax, plus MonoGame's ownVertexTextureEffect.fx, againstmgfxc3.8.5's builds, with four built-in positive controls;DxModernFeaturescarries the DirectX 11 arm of the same scene in the WARP job, andVsDrivenDx12 -- legacy-lodthe 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'sTests/Framework.Graphics/EffectTest.cs(written and checked against XNA by KNI's author) to a real engine, on KNI's test shaders vendored undertests/fixtures/shaders/third-party/KNI.KniEffectTestKniGl(real KNI SDL2.GL, built against both KNI 4.2.9001 and 4.3.9001) andKniEffectTestGlrun on llvmpipe,KniEffectTestKniDx(both KNI lines),KniEffectTestDxandKniEffectTestDx12on WARP, andKniEffectTestVulkanon 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) andKniVsDriven, and browser-ANGLE gates still have no headless CI driver (real KNI SDL2.GL itself is in the llvmpipe job throughKniEffectTestKniGl,InitializerDefaultsKniGl,VertexSemanticsKniGlandSkiaVsKni) — they run fromvalidation/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. - Cross-host DXC output (Vulkan and DirectX 12):
DxcTargetsCrossHostByteIdentityTests(integration lane, every OS) compiles the whole fixture corpus for both targets, release andDebug, 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 labelrun-win-arm64, manual dispatch, weekly) compiles every fixture.fxfor OpenGL, Vulkan and DirectX 12 onwindows-latest, then reruns the same tests as a native arm64 process onwindows-11-armand requires identical results (ArchitectureByteIdentityTests). It also asserts DirectX 11 and FNA areSD0211there (no win-arm64 vkd3d-shader) and thatShadowDusk.SlangisSD0620(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.ymlboots an API-34 x86_64 emulator on ubuntu (KVM) and runsvalidation/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 liveEffect; a foreign or missing DXC must be refused withSD0219, a foreign or missing SPIRV-Cross withSD0103. On a PR it is opt-in via therun-androidlabel; it also runs weekly and on relevant pushes tomain. - 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 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.xnbwhen the same effect is built from a second directory, the same.xnbunder/config:Debugwith the defaultDebugMode=Auto(as stock MGCB, whose own/config:Debugbuild is checked too), and the CLI's own.xnbpayload equal to the plugin's — on three MGCB versions (the pinned 3.8.4.1, a real 3.8.5, which renumberedTargetPlatform, and a real 3.8.6; the 3.8.5 and 3.8.6 arms coverWeb,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. - 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$newerMonoGamelist (3.8.6 today) beside the pinned arms, including MonoGame 3.8.6's native OpenGL 4.1 runtime DesktopGL4 (-p:MonoGameDesktopGL4=true);-NewerMonoGameOnlyruns just that block. CI's render-gate jobs (validation-render.yml) each end with the core of that block on 3.8.6.ContentPipelineApiBindingTests(underdotnet test) checks that every MonoGame member the content plugins call exists in each supported content-pipeline release. - 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 (with the same MGFX v11 source-file checks on itsDesktopVKandWindowsDX12passes, including a second Builder run from a different source directory), 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. 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 onwindows-latestpinned 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.mgfxtruncated to half, with its effect-cache key changed, must be rejected by the realEffectloader.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 bypack-consume.yml) is the separate cold-NuGet-consumer proof for the package on all three OSes; on macOS the same workflow also runstools/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.mjsruns every corpus.slangfile, 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-Epass that finds author-written registers);tests/ShadowDusk.BrowserTests/browser-slang-gate.mjsboots the sample in headless Chromium, compiles the corpus in the page and checks the bytes againstslang-manifest.json, then renders every shipped Slang shader through both Slang front ends in KNI WebGL. Both run inwasm.yml'sbrowser-smokejob (labelrun-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.mjscaptures 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.mjscompiles 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 inwasm.yml'sbrowser-smokejob. 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#linedirective line blanked byVkd3dCompileContract.PrepareSource), requires that pass to print no#line directivefixme 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.fxdoes, vkd3d'sW5300); the browser gate compares theCompiledShader.Warningsthe real compiler returns, relocated onto the author's line, against the committedwarnings-manifest.jsonthatCrossHostByteIdentityTestsregenerates and asserts beside the byte manifest on every CI OS. - WASM stack depth (issue #271):
tests/ShadowDusk.BrowserTests/node-test-wasm-depth.mjscaptures the desktop output for deeply nested shaders (long expression chains, nested parentheses, ternaries and ifs, longelse ifchains, 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.mjschecks the same through the real compiler in Chromium (SD1907, then a correct compile after the reload). Runs inwasm.yml'sbrowser-smokejob. Measurements:.wasm-build/WASM-STACK-DEPTH.md.
See the test shader corpus for the inputs.