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 is dead, sample-only reference and never runs.) 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.
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 — tracked (backlog 17-VS) |
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 (Phase 30 §16).