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.

Parameters lists the effect's parameters (name, shape, type, and annotations on MGFX and KNIFX) together with the default each one's HLSL declares, so a tool can show float Scale = 2.0; without loading the effect into a runtime. DefaultValue is the same default the compiled file carries, in logical row-major order (see DefaultValue), and null when the declaration has no initializer or it could not be evaluated.

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+) or CompilerOptions.Container = EffectContainer.Knifx (KNI v4.02+) — both render identically to v10. See Parameters & Caveats.

Preprocessor macros. If your .fx has #ifdef-guarded branches, define the macros with Defines — the library equivalent of mgfxc's /Defines: flag:

var options = new CompilerOptions
{
    Target  = PlatformTarget.OpenGL,
    Defines = [new UserDefine("HIGH_QUALITY"), new UserDefine("SAMPLE_COUNT", "8")],
};

A bare UserDefine("NAME") defines 1. The macros apply on every target (including FNA), riding through both the #define prepend and the DXC -D flags.

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-platform vkd3d-shader backend; 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-only d3dcompiler_47 correctness 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.Compiler serves every target; only Target changes. 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_0 in 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.

Shaders without a file extension

When a shader arrives from a stream, a byte buffer, or a package whose files lost their extensions, let ShadowDusk work out the format from the text. ShaderConverter has one auto-detecting method per output kind: Compile / CompileAsync (MonoGame, KNI, FNA), ConvertToSksl (SkiaSharp) and ConvertToRaylib (raylib). Each reads an .fx effect (a technique block) or HLSL-compatible Slang (a [shader("...")] entry attribute) and routes it exactly as the format-specific entry point would.

using ShadowDusk.Compiler;
using ShadowDusk.Core;

var result = ShaderConverter.Compile(new EffectCompiler(), source,
    new CompilerOptions { Target = PlatformTarget.OpenGL }, nameHint: "Outline");

if (result.IsFailure && result.Error.Any(e => e.Kind == ShaderErrorKind.UnsupportedInput))
{
    // Not a format ShadowDusk reads (code SD0650). Nothing was compiled: try another loader.
}
else if (result.IsFailure)
{
    // A real error in a recognized shader: report it.
}
  • "Not mine" is a typed result, not an exception. ShaderErrorKind.UnsupportedInput (SD0650) means the input was not recognized and nothing ran. Every other failure, including ShaderErrorKind.AmbiguousInput (SD0005), is a real error to surface.
  • Cheap check first. ShaderConverter.CanConvert(source) answers with a text scan and no compile. ShaderFormatDetector.Detect(source) (in ShadowDusk.Core) reports the format itself.
  • The name is only a hint. The content decides. An optional nameHint is read only for its extension, and only when the content is ambiguous or carries no signal at all.
  • Valid as both Slang and .fx? A file with a technique block is reported and compiled as an .fx effect.
  • GLSL is reported as unsupported here. ShaderToy-style and plain void main() GLSL are recognized, but their converter is the separate ShadowDusk.ShaderToy package: call ShaderToyConverter.Convert first and pass its .fx output in.