In-Browser Compilation (KNI / Blazor WASM)
The same faithful pipeline runs inside .NET WebAssembly via the ShadowDusk.Wasm package (WasmShaderCompiler : IShaderCompiler), so a KNI / Blazor WebAssembly game can compile .fx → .mgfx at runtime, in the browser, with no server roundtrip and no native toolchain on the user's machine.
The in-browser frontend is the faithful pinned DirectXShaderCompiler compiled to WebAssembly (matching the desktop Vortice.Dxc commit), so its SPIR-V is byte-identical to the desktop pipeline — one faithful compiler everywhere, no substitute frontend. (The older Slang-WASM frontend in the sample — Slang in place of DXC — is dead, sample-only reference and never runs; note this is a different thing from ShadowDusk's support for HLSL-compatible .slang as an input language, a pure text transform upstream of the pipeline that works wherever the pipeline does, this host included.) See WASM In-Browser Frontend for the architecture.
Beyond the OpenGL/WebGL path this guide covers, the same package also compiles DirectX .mgfx and FNA .fxb in the browser (the pinned vkd3d-shader compiled to WASM) — as export targets, byte-identical to the desktop output; a browser cannot render DXBC/D3D9 bytecode. See DirectX & FNA in the Browser.
Full Slang input in the browser (genuine Slang: import, generics, interfaces) works too, through ShadowDusk.Slang.Wasm's WasmSlangCompiler (issue #257). A browser cannot spawn slangc, so the same pinned slangc v2026.14.1 runs inside the page as WebAssembly, receives the identical command line the desktop route passes, and hands its HLSL to the faithful DXC/vkd3d modules above. Slang is only the input here: it never compiles HLSL and never stands in for DXC. Measured: the module's output is byte-identical to native slangc's for every corpus shader, entry point and target, and a real headless browser compiles the Slang corpus to the same OpenGL and DirectX bytes as the desktop. It is not published as a NuGet package yet (Phase 68); the sample uses it from source.
The ShaderFiddle.Web sample is a working demonstration of this reach — itself only a sample, not the product.
The complete walkthrough (setup, package wiring, KNI specifics, gotchas) is maintained in the repository and reproduced below as the single source of truth:
How to use ShadowDusk in a KNI WebAssembly (Blazor) app
Compile .fx shaders to .mgfx in the browser at runtime and load them with
new Effect(graphicsDevice, bytes) — no server, no mgfxc, no native toolchain on
the user's machine. The faithful pinned-DXC→WASM pipeline is packaged so a consumer
adds one package reference and wires nothing.
1. What works (and what doesn't, yet)
| Status | |
|---|---|
| KNI (nkast's MonoGame fork) Blazor WebAssembly + WebGL, target OpenGL | ✅ supported — this is the path below |
Output loads in real KNI WebGL Effect and renders like mgfxc |
✅ proven (10/10 corpus, headless) |
| MonoGame proper in the browser | ❌ MonoGame has no mature browser-WASM runtime; use KNI for WASM |
DirectX/DXBC compiled in the browser (PlatformTarget.DirectX) |
✅ supported — as an export target: the .mgfx is byte-identical to the desktop compile, but a browser cannot render DXBC; it renders in your MonoGame WindowsDX game |
FNA fx_2_0 .fxb compiled in the browser (PlatformTarget.Fna) |
✅ supported — likewise export-only (no D3D9 in a browser); renders in your FNA game |
| Vertex-shader-driven effects (the corpus is pixel-shader-only) | ⚠️ untested in WASM |
So: KNI + Blazor WASM + OpenGL/WebGL, pixel-shader effects — solid; that's the
render-in-the-browser path this guide targets. The DirectX/FNA rows compile through
the same pinned vkd3d-shader compiled to WebAssembly (never a substitute compiler);
use them to export artifacts from a browser tool — see
samples/ShaderFiddle.Web for a working export station.
2. Prerequisites
- .NET 8 SDK (the browser runtime is pinned to .NET 8 / emscripten 3.1.34).
- The WASM tools workload:
dotnet workload install wasm-tools - KNI Blazor templates / packages. This guide uses
nkast.Kni.Platform.Blazor.GLversion4.2.9001.*(the version this repo's working sample uses). Install KNI's templates per nkast's instructions (see https://github.com/nkast/MonoGame), or just crib from the working sample in this repo:samples/ShaderFiddle.Web/is a complete KNI Blazor-WASM app that already uses the package exactly as below — copy itsindex.html,Program.cs, andShaderFiddleGame.csfor the KNI host plumbing (canvas + JS shims + the tick loop), which is the same for any KNI Blazor app.
3. Get the ShadowDusk packages
Add the ShadowDusk.Wasm package to your app — that single reference is all you need
(§4 shows the .csproj). It brings the compiler and the native DXC + SPIRV-Cross +
vkd3d-shader WASM modules transitively; there's nothing else to install and nothing to
copy into wwwroot.
Consuming an unreleased / source build (local feed)
If you're building against a source checkout ahead of the published version, pack a local
NuGet feed from this repo. The faithful
DXC→WASM module (dxcompiler.wasm, ~17 MB) is gitignored in the package wwwroot, but
its source-of-truth is committed at .wasm-build/dxc-wasm-out/, so restore just
copies it (no rebuild); the same script also downloads the pinned
vkd3d-shader.{js,wasm} module (SHA-256-verified) into the package wwwroot:
# from the repo root
pwsh -File tools/restore.ps1 # or: ./tools/restore.sh (copies dxcompiler.wasm into the package wwwroot)
# pack the 5 packages in the dependency chain (Wasm -> Compiler -> Core/HLSL/GLSL),
# all at one shared version — the repo's Directory.Build.props <Version>
$feed = "$PWD/local-feed"
dotnet pack src/ShadowDusk.Core/ShadowDusk.Core.csproj -c Release -o $feed
dotnet pack src/ShadowDusk.HLSL/ShadowDusk.HLSL.csproj -c Release -o $feed
dotnet pack src/ShadowDusk.GLSL/ShadowDusk.GLSL.csproj -c Release -o $feed
dotnet pack src/ShadowDusk.Compiler/ShadowDusk.Compiler.csproj -c Release -o $feed
dotnet pack src/ShadowDusk.Wasm/ShadowDusk.Wasm.csproj -c Release -o $feed
In your consumer app, add a nuget.config next to the .csproj pointing at that
feed (use the absolute path to the local-feed folder you packed into above):
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="shadowdusk-local" value="/path/to/ShadowDusk/local-feed" />
</packageSources>
</configuration>
4. Create the KNI Blazor WASM app
Create a KNI Blazor-WASM project from the KNI template (or copy samples/ShaderFiddle.Web/).
The project must target net8.0-browser (required for [JSImport]):
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0-browser</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- the [JSImport] generator needs /unsafe -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nkast.Kni.Platform.Blazor.GL" Version="4.2.9001.*" />
<PackageReference Include="ShadowDusk.Wasm" Version="*" /> <!-- the ONLY ShadowDusk line you add -->
</ItemGroup>
</Project>
That single ShadowDusk.Wasm reference brings ShadowDusk.Compiler/Core/HLSL/GLSL
transitively and the native DXC + SPIRV-Cross + vkd3d-shader WASM modules (as Blazor
static web assets). You do not add anything to wwwroot, and you do not call
JSHost.ImportAsync — the library self-registers its modules on the first compile.
5. Compile a shader and render it
The API is IShaderCompiler.CompileAsync(hlsl, options) → Result<CompiledShader, ShaderError[]>.
In your KNI Game (or a Blazor component that holds one):
using ShadowDusk.Core; // CompilerOptions, PlatformTarget, Result, CompiledShader, ShaderError
using ShadowDusk.Wasm; // WasmShaderCompiler
using Microsoft.Xna.Framework.Graphics;
private readonly IShaderCompiler _compiler = new WasmShaderCompiler();
// Call this when the user wants to (re)compile. First call lazily downloads the
// ~17 MB dxcompiler.wasm, so show a "compiling…" state.
public async Task<Effect?> CompileEffectAsync(string fxSource)
{
var options = new CompilerOptions
{
Target = PlatformTarget.OpenGL, // WebGL path
SourceFileName = "myshader.fx", // shows up in error messages
// MgfxVersion = 10 (default; what KNI's MGFXReader10 loads)
};
Result<CompiledShader, ShaderError[]> result = await _compiler.CompileAsync(fxSource, options);
if (result.IsFailure)
{
foreach (ShaderError e in result.Error)
Console.Error.WriteLine(e.FxcFormattedMessage); // file(line,col): error CODE: message
return null;
}
byte[] mgfx = result.Value.Data; // the .mgfx bytes
return new Effect(GraphicsDevice, mgfx); // load into the real KNI WebGL runtime
}
Then draw with it, exactly like a mgfxc-compiled effect — e.g. via SpriteBatch:
// set parameters by name (null-safe), then draw
effect.Parameters["SpriteTexture"]?.SetValue(myTexture);
_spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.LinearClamp,
null, null, effect);
_spriteBatch.Draw(myTexture, destRect, Color.White);
_spriteBatch.End();
Multi-texture pixel shaders: if your effect samples a second texture (a separate sampler slot), pin that slot's state before the draw, e.g.
GraphicsDevice.SamplerStates[1] = SamplerState.LinearClamp;—SpriteBatchonly sets slot 0, and WebGL vs desktop GL resolve an unset slot differently for non-power-of-two textures. (This is what the corpus "Dissolve" shader needs.)
That's the whole integration. The full working version is
samples/ShaderFiddle.Web/{Pages/Index.razor.cs, ShaderFiddleGame.cs}.
6. How it works (so you can debug it)
WasmShaderCompilerruns the same faithful pipeline as the desktop CLI: HLSL → DXC (compiled to WASM) → SPIR-V → SPIRV-Cross (WASM) → managed reflect + MojoShader-dialect rewrite + MGFX writer →.mgfx. The in-browser SPIR-V is byte-identical to desktop DXC.- The native modules ride inside the package and are served at
_content/ShadowDusk.Wasm/(Blazor static web assets).ShadowDusk.WasmcallsJSHost.ImportAsyncitself (inWasmModuleRegistration) against../_content/ShadowDusk.Wasm/<file>— so it works whether your app is hosted at the site root or a sub-path, with no consumer wiring. - Each WASM module is lazy-loaded by the first
CompileAsyncthat needs it, not at page boot — so app startup stays fast: the ~17 MBdxcompiler.wasmon the first OpenGL compile, the ~1.3 MBvkd3d-shader.wasmon the first DirectX/FNA compile. Serve your site with HTTP compression (brotli/gzip): on the wire they compress to ~6 MB and ~0.4 MB respectively.
7. Troubleshooting
| Symptom | Fix |
|---|---|
Build error: dxcompiler.wasm is missing or vkd3d-shader.{js,wasm} is missing |
Run tools/restore.ps1/.sh before pack (copies/downloads the modules into the package wwwroot). |
SD1902 on a DirectX/FNA compile |
The vkd3d-shader.{js,wasm} module couldn't be loaded (e.g. not restored in a source build, or not served). Run tools/restore.ps1/.sh and check the browser fetched _content/ShadowDusk.Wasm/vkd3d/vkd3d-shader.wasm. |
Restore can't find ShadowDusk.Compiler etc. |
You only packed Wasm — pack all five projects (§3) into the local feed. |
new Effect(...) throws on load |
You're not on KNI (MonoGame proper has no v10 WebGL reader), or the .mgfx is for the wrong target. Use KNI + PlatformTarget.OpenGL. |
| First compile hangs/slow | That's the one-time ~17 MB dxcompiler.wasm download. Show a loading state; enable server compression. |
| Effect renders wrong only in the browser | Check sampler-slot state for multi-texture shaders (§5 note); compare against the desktop render of the same bytes. |
[JSImport]/unsafe build error |
Add <AllowUnsafeBlocks>true</AllowUnsafeBlocks> and target net8.0-browser. |
8. Rebuilding the DXC→WASM module (only if you bump the DXC pin)
You don't need to — the built module is committed. But the full reproducible recipe is
.wasm-build/DXC-WASM-BUILD.md + Invoke-DxcWasmBuild.ps1 (pinned DXC e043f4a1 ==
Vortice.Dxc 3.3.4, emscripten 3.1.34). It's a multi-hour LLVM-fork build and is
Windows/MSVC-only today; a Linux/macOS rebuild + CI is planned future work.