In-Memory Quickstart
This is the product in its purest form: add the ShadowDusk.Compiler package, call CompileAsync, and get .mgfx bytes back in memory — no temp files, no child process, no mgfxc.
1. Add the package
dotnet add package ShadowDusk.Compiler
2. Compile a shader
using ShadowDusk.Compiler;
using ShadowDusk.Core;
string hlsl = File.ReadAllText("MyShader.fx"); // or any HLSL/.fx string
var compiler = new EffectCompiler();
Result<CompiledShader, ShaderError[]> result = await compiler.CompileAsync(
hlsl,
new CompilerOptions
{
Target = PlatformTarget.OpenGL, // or PlatformTarget.DirectX / PlatformTarget.Fna
SourceFileName = "MyShader.fx", // optional — improves error messages
});
if (result.IsSuccess)
{
byte[] mgfx = result.Value.Data; // the .mgfx binary, ready to load
File.WriteAllBytes("MyShader.mgfx", mgfx);
}
else
{
foreach (ShaderError error in result.Error)
Console.Error.WriteLine(error.FxcFormattedMessage);
}
The result is a Result<CompiledShader, ShaderError[]> — a discriminated union. On success, Data is the .mgfx byte array (and Target echoes the platform). On failure you get an array of ShaderError with the file, line, column, code, and message exactly as the underlying compiler emitted them — the first entry is the fatal error; when earlier passes of the same effect had already compiled with warnings, those ride along after it so nothing is lost.
A successful compile can still have things worth knowing: Warnings carries the underlying compiler's own warnings verbatim, plus ShadowDusk's GL portability findings (SD0400–SD0499) — constructs that compile fine but are known to fail or silently misbehave at runtime on narrower GL stacks (WebGL1 / KNI Reach, ANGLE Direct3D11 in Windows browsers, strict Mesa), where the engine's only signal is a generic draw-time exception. It also carries reflection findings such as SD0104, an unrecognized vertex-input semantic that defaulted to TextureCoordinate — correct behaviour (mgfxc defaults the same way) but worth seeing, because a typo like TEXCORD0 mints an attribute your vertex declaration then has to supply. Warnings never gate output; the bytes are valid regardless.
Shader not working? Validate it in one call
When a shader compiles for one target but fails for another (the classic report: works on DirectX, fails on OpenGL), ValidateAsync compiles it for OpenGL and DirectX and reports every error and every warning, per target, with the underlying compiler's complete verbatim text. Printing the report is the whole story:
ShaderValidationReport report = await compiler.ValidateAsync(hlsl);
Console.WriteLine(report); // per-target status, every error with its source
// location, every warning, verbatim compiler text
if (!report.IsValid)
{
// structured access: report.Targets[i].Target / .Succeeded / .Errors / .Warnings
}
Validate/ValidateAsync run the exact same pipeline as Compile/CompileAsync per target (never a fork), so what validates is precisely what compiles. Need FNA or Vulkan in the sweep? Pass an explicit target list via the overload — they are not in the default pair because FNA's SM2–3 dialect would false-alarm MonoGame/KNI shaders.
3. Load it into your game
The call is the same new Effect(graphicsDevice, bytes) for all three runtimes — only which bytes you pass differs by target.
For MonoGame and KNI, the bytes are a standard .mgfx blob (KNI reads the identical MGFX v10 container):
var effect = new Effect(graphicsDevice, mgfx); // MonoGame / KNI — .mgfx
It renders the same image mgfxc's output would.
Newer runtimes (optional). The default v10 container loads on MonoGame 3.8.1.263+ (the measured floor) and KNI, so you usually do nothing. To target a newer runtime, set
CompilerOptions.MgfxVersion = 11(MonoGame 3.8.5+) orCompilerOptions.Container = EffectContainer.Knifx(KNI v4.02+) — both render identically to v10. See Parameters & Caveats.
Preprocessor macros. If your
.fxhas#ifdef-guarded branches, define the macros with Defines — the library equivalent ofmgfxc's/Defines:flag:var options = new CompilerOptions { Target = PlatformTarget.OpenGL, Defines = [new UserDefine("HIGH_QUALITY"), new UserDefine("SAMPLE_COUNT", "8")], };A bare
UserDefine("NAME")defines1. The macros apply on every target (including FNA), riding through both the#defineprepend and the DXC-Dflags.
For FNA, you pass the .fxb produced by PlatformTarget.Fna (see below); FNA loads it through MojoShader:
var effect = new Effect(graphicsDevice, fxb); // FNA — .fxb
It renders the same image fxc /T fx_2_0's output would.
Library vs CLI defaults
The library and the CLI default to different targets:
| Surface | Default target |
|---|---|
| Library — Target | OpenGL |
CLI — mgfxc /Profile |
DirectX_11 |
So the code above (no explicit Target) compiles for OpenGL, while mgfxc MyShader.fx out.mgfx (no /Profile) compiles for DirectX_11. Always set the target explicitly to avoid surprises. See the CLI Reference.
Choosing the DirectX backend
When Target = PlatformTarget.DirectX, ShadowDusk emits DXBC (SM5) via a backend selected by DxbcBackend:
DxbcBackend.Vkd3d(default) — the cross-platformvkd3d-shaderbackend; works on Linux/macOS/Windows and emits the same bytes on every OS. The vkd3d natives for all four desktop RIDs ship inside the NuGet package — consumers install nothing (self-contained; the repo's restore script is only for building ShadowDusk itself from source).DxbcBackend.D3DCompiler— the Windows-onlyd3dcompiler_47correctness oracle (opt-in; hard-fails off Windows).
You only set the property to opt in to the oracle:
var options = new CompilerOptions
{
Target = PlatformTarget.DirectX,
DxbcBackend = DxbcBackend.D3DCompiler, // opt in to the Windows-only oracle
};
See DirectX DXBC (vkd3d) Path for why DXC is not used here (it emits SM6 DXIL, which MonoGame's DX11 runtime cannot load).
Compiling for FNA
FNA doesn't read the .mgfx container — it loads the legacy D3D9 fx_2_0 .fxb through MojoShader at runtime. Select it with PlatformTarget.Fna; everything else is the same call:
var result = await compiler.CompileAsync(hlsl, new CompilerOptions
{
Target = PlatformTarget.Fna,
SourceFileName = "MyShader.fx",
});
// on success:
byte[] fxb = result.Value.Data; // the .fxb bytes (fx_2_0, SM <= 3)
var effect = new Effect(graphicsDevice, fxb); // FNA's Effect, loaded via MojoShader
Notes specific to the FNA target:
- Same package, no FNA-specific flag.
ShadowDusk.Compilerserves every target; onlyTargetchanges. FNA itself is added to your project as a project reference, not a NuGet — see Installation → Targeting FNA. - Shader Model ≤ 3. fx_2_0 caps at SM3; a shader needing SM4+ features fails loudly with a diagnostic instead of miscompiling.
- Validated. The output loads and renders pixel-equivalent (max Δ ≤ 1/255) to
fxc /T fx_2_0in real FNA across the pixel-shader-only and vertex-shader-driven corpora — multi-pass effects and in-pass render states included.
Reusing the compiler
EffectCompiler is cheap to construct and safe to reuse across many CompileAsync calls. Pass a CancellationToken to bound long compiles.