MGCB Content Pipeline

ShadowDusk plugs into the MonoGame Content Builder as a content-processor plugin. Add the ShadowDusk.MgcbPlugin package, point one /reference: line at it, and MGCB compiles your .fx to .xnb through ShadowDusk, inside its own process — no mgfxc, no fxc.exe, no Wine, no Windows SDK, on Linux, macOS, and Windows alike.

Important

Do not try to route MGCB through a PATH override. This page used to document one, and it does not work. Measured on 2026-07-28 against dotnet mgcb 3.8.2.1105, 3.8.4.1, and 3.8.5: with a real mgfxc executable placed first on PATH, a .mgcb content build never invoked it once and still produced a valid .xnb. MGCB compiles .fx in-process — there is nothing for a PATH alias to intercept. MonoGame 3.8.5's code-centric Content Builder is a C# project over MonoGame.Framework.Content.Pipeline and has no external-tool seam either. The plugin below is the real integration.

MonoGame 3.8.6: use ShadowDusk 0.24.0 or later. MonoGame 3.8.6 removed a constructor (ContentIdentity(string, string)) that the 0.23.0 importer calls, so with 0.23.0 every .fx fails with MissingMethodException. Until you can update, MonoGame's own EffectImporter with the ShadowDusk processor works on 3.8.6. Newer releases are checked against every MonoGame content pipeline from 3.8.2.1105 to 3.8.6.

Setup

dotnet add package ShadowDusk.MgcbPlugin

It is a build-time (DevelopmentDependency) package: nothing from it reaches your shipped game assembly.

Then in your .mgcb, add the reference once and select the ShadowDusk importer and processor on each effect:

/reference:$(NuGetPackageRoot)shadowdusk.mgcbplugin/<version>/tools/net8.0/any/ShadowDusk.MgcbPlugin.dll

#begin MyEffect.fx
/importer:ShadowDuskEffectImporter
/processor:ShadowDuskEffectProcessor
/build:MyEffect.fx

MGCB does not expand MSBuild properties inside a .mgcb, so spell the path out (or copy the package's tools/net8.0/any directory somewhere stable in your repo and point at that). Everything the plugin needs — the ShadowDusk assemblies and the pinned DXC, SPIRV-Cross, and vkd3d-shader natives for every RID — lives in that one directory, because MGCB resolves a referenced plugin's dependencies from the plugin's own folder.

That is the whole setup. The target comes from the content project's own /platform: line, and the output is the backwards-compatible MGFX v10 container every MonoGame 3.8.1.263+ and KNI runtime loads. You never pick a version, a format, or a flag to get correct output.

Platform → target mapping

.mgcb /platform: ShadowDusk target
Windows DirectX 11 (DXBC SM5)
DesktopGL, MacOSX, iOS, Android, RaspberryPi, Web, NativeClient OpenGL (GLSL)
DesktopVK (MonoGame 3.8.5+) Vulkan (SPIR-V)
WindowsDX12 (MonoGame 3.8.5+) DirectX 12 (DXIL SM6), signed on every host
PlayStation4, PlayStation5, XboxOne, XboxSeries, Switch, Xbox360, Stadia not supported — the build fails loudly with SD0501 rather than emitting an artifact those runtimes cannot load

The map keys on the platform's name, never on the enum's number. MonoGame renumbered TargetPlatform in 3.8.5 (Stadia=12, Web=13 became Web=12, DesktopVK=13, WindowsDX12=14, XboxSeries=15), and the plugin is compiled against the 3.8.2.1105 contract so it loads into every MGCB from 3.8.2 up. Until Phase 63 (issue #203) the map switched on the compiled-in members, so on dotnet-mgcb 3.8.5 /platform:DesktopVK silently produced an OpenGL effect under the Vulkan platform byte and /platform:Web was refused; both are fixed, and validation/MgcbPlugin now runs a real 3.8.5 arm.

On MGCB before 3.8.5, WindowsDX12 and DesktopVK have no member in the platform list at all, so they are reached through the ShaderProfile processor parameter below.

Tip

On MonoGame 3.8.5 the template-default content story is a Content Builder project (C# you own) rather than a .mgcb. The same importer and processor ship for that shape as the library-shaped ShadowDusk.ContentPipeline package — see MonoGame 3.8.5 Content Builder. This tools-only package cannot be referenced from C#.

Processor parameters

Every one is optional. The defaults are the correct path — a parameter here is an escape hatch, never a step you must take to get working output.

/processorParam: Default What it does
DebugMode Auto Only Debug turns shader debug information on. Auto and Optimize optimize, whatever the content build configuration (/config:Debug included), exactly like MonoGame's stock EffectProcessor, which never reads the configuration.
Defines (empty) Preprocessor macros in mgfxc's /Defines: spelling: NAME=VALUE entries separated by ; or ,; a bare NAME defines it as 1. Same property name and format as the stock processor, so an existing /processorParam:Defines=… carries over unchanged.
IncludeDirs (empty) Extra #include search directories, ;-separated. The including file's own directory is always searched first and needs no entry. Equivalent to the CLI's /I.
ShaderProfile (empty) Overrides the target derived from /platform:. DirectX_11, DirectX_12, OpenGL, Vulkan — needed only on MGCB before 3.8.5, whose platform list cannot name DirectX 12 or Vulkan; from 3.8.5 WindowsDX12 and DesktopVK derive their target directly. DX12 DXIL is signed on every host (the retired SD0214 warned about the old unsigned output).
MgfxVersion 10 11 opts into the newer MGFX container (MonoGame 3.8.5+).
DxbcBackend vkd3d d3dcompiler opts into the Windows-only correctness oracle for the DirectX target.

Diagnostics

Shader errors surface through MGCB in the file(line,col-col): error CODE: message form fxc/mgfxc use and MSBuild and IDEs parse, with the underlying compiler's own words verbatim beneath — the same text the ShadowDusk CLI prints, from the same formatter. The build fails; nothing is silently swallowed.

C:\game\Content\Broken.fx(49,32-32): error X0000: use of undeclared identifier 'notADeclaredThing'
    C:\game\Content\Broken.fx:49:32: error: use of undeclared identifier 'notADeclaredThing'
        col.rgb = (col.r + col.g + notADeclaredThing) / 3.0f;
                                   ^

#included files are registered as build dependencies, so editing an .fxh rebuilds the effects that use it.

Is it really ShadowDusk?

Yes, and it is checked rather than claimed. The .mgfx bytes inside the .xnb are byte-for-byte what the ShadowDusk CLI writes for the same source and target — because the plugin is an adapter onto the same EffectCompiler, adding no compilation logic of its own. Two gates hold that: MgcbPluginByteIdentityTests drives the processor and compares against the real CLI binary on every dotnet test, and validation/MgcbPlugin runs an actual dotnet mgcb build and additionally checks that the .xnb envelope matches MGCB's own stock output while the payload differs from it. Verified green on dotnet mgcb 3.8.2.1105, 3.8.3, 3.8.4, 3.8.4.1, 3.8.5, and 3.8.6 — the gate runs the pinned 3.8.4.1 and real 3.8.5 and 3.8.6 builds (Web, DesktopVK, WindowsDX12 included), and it runs the plugin-arm MGCB with a decoy dxcompiler.dll first on PATH, so a build that ever fell back to an OS-search-path DXC (a Vulkan SDK install puts one there) instead of the pinned one fails loudly.

Your build path stays out of the content

There is one place the plugin's bytes deliberately differ from the CLI's. The MGFX v11 container, which WindowsDX12 and DesktopVK always use, stores a source-file string per shader (the runtime shows it only in shader error messages). MGCB hands a processor your effect's absolute path. The CLI's .mgfx output records the path it is given, as mgfxc does; the plugin writes <unknown>, which is exactly what MonoGame's stock EffectProcessor writes (and so does the CLI when it writes an .xnb, since MGCB, not mgfxc, is the reference for content files). So a DirectX 12 or Vulkan .xnb never carries your build machine's directory or user name, and it is the same bytes wherever the project is checked out. Everything else in the payload is the CLI's, byte for byte, and build errors and warnings still name the real file, line and column. The gate checks all of it on a real dotnet mgcb 3.8.5: the string against the stock build's, the payload against the CLI's with only that string replaced, and the same effect built from a second directory for byte-identical output. OpenGL and DirectX 11 use MGFX v10, which has no such string.

A /config:Debug build with the default DebugMode=Auto is the same bytes as a release build, as it is with the stock processor (the gate checks both). Only an explicit DebugMode=Debug turns debug information on, and that is different by design: the compiler's own debug information names the source file so a graphics debugger can find it, so those DirectX 12 and Vulkan effects do record the path.

The other routes (still supported)

The plugin is the native integration for teams who want MGCB in their build, but none of these went away:

0. Skip MGCB entirely and let ShadowDusk write the .xnb itself. If effects are the only reason MGCB is in your build, you no longer need it there:

ShadowDuskCLI Content/MyEffect.fx Content/MyEffect.xnb /Profile:OpenGL

Drop the file where the pipeline-built one used to go and Content.Load<Effect>("MyEffect") keeps working unchanged — verified against a real dotnet-mgcb build, loaded through a real ContentManager, rendered pixel-identically, on MonoGame (WindowsDX and DesktopGL), KNI (4.2.9001 and 4.3.9001) and FNA. Name the profile: with no /Profile: the CLI defaults to DirectX_11, which a DesktopGL game rejects (the CLI warns with SD0029). From the library it is result.Value.ToXnb(). See Drop-in mgfxc.

Note

On KNI 4.2.9001 this direct route is the only one that works: KNI 4.2's reader-name resolver rejects the type-reader manifest MonoGame's MGCB writes (stock or through the plugin) with FileLoadException: The given assembly name was invalid.; ShadowDusk's own .xnb carries the XNA-4.0 name KNI accepts. KNI 4.3.9001 loads both.

1. Compile with the CLI and let MGCB copy the result. Build .fx → .mgfx with the ShadowDusk CLI as a pre-build step, then /copy: it:

ShadowDuskCLI Content/MyEffect.fx Content/MyEffect.mgfx /Profile:OpenGL
#begin MyEffect.mgfx
/copy:MyEffect.mgfx

2. Skip the content pipeline for shaders entirely — compile at runtime and hand the bytes to Effect, which is what the library is for:

var result = await new EffectCompiler().CompileAsync(File.ReadAllText("MyEffect.fx"),
    new CompilerOptions { Target = PlatformTarget.OpenGL });
var effect = new Effect(GraphicsDevice, result.Value.Data);

The /Profile: names in route 1 are the same names the ShaderProfile processor parameter takes.

A worked sample

The repository's samples/mgcb sample carries two content projects over the same shader corpus: Content.mgcb (MonoGame's own compiler) and Content.ShadowDusk.mgcb (this plugin), so you can build both and compare.

Known limits

  • Macro-defined techniques on the OpenGL target fail with SD0010 "Effect source contains no techniques". That is a compiler-library gap, not a plugin one — the same shaders fail identically through the CLI. It affects MonoGame's own BasicEffect-family stock effects and Penumbra's.
  • The package is large (~81 MB) because it carries the pinned DXC for every RID. That is what "add the package and point at it" costs; nothing has to be installed separately.