Parameters & Caveats
A grab-bag of the practical things to know when compiling with ShadowDusk.
The library vs. CLI default target
This bites people, so it's first:
| Surface | Default target |
|---|---|
| Library — Target | OpenGL |
CLI — mgfxc /Profile |
DirectX_11 |
Always set the target explicitly. The CLI default matches mgfxc (DirectX_11); the library default favors the cross-platform OpenGL path.
Global parameter initializers are not baked in
A global with an initializer, e.g.:
float FishEyeAmount = 0.35;
…does not carry 0.35 into the .mgfx. DXC (unlike fxc/mgfxc) drops global cbuffer initializers, so the stored default is 0. The effect renders as identity until you either:
SetValuethe parameter from code after loading theEffect, or- inline the value as a literal in the shader instead of a named global.
This is a known fidelity gap versus mgfxc for the initializer specifically (it did not affect the validated SM3 corpus). Setting parameters by name at runtime is the recommended pattern regardless.
Advanced texture intrinsics compile on Windows only (for now)
A small family of advanced OpenGL-target texture intrinsics — 3D-texture sampling (tex3D), explicit-LOD sampling (the tex2Dlod family), and gradient sampling (the tex2Dgrad family) — currently compiles on the Windows DXC native but fails to compile on the Linux/macOS DXC builds, even though all hosts pin the same DXC version. If your .fx uses these and you compile on Linux/macOS, expect a compile error (a loud diagnostic, never a miscompile); compiling the same shader on Windows works. The ordinary texture corpus (tex2D, multi-texture, samplers, render targets, …) is unaffected and compiles identically on all three OSes. This per-OS divergence is a known, tracked gap under investigation — it will be fixed in the compiler, not papered over here.
DirectX 12 must be compiled on Windows (for now)
DXC's DXIL validation-and-signing step runs through dxil.dll, which is Windows-only. A DirectX12 compile on Linux or macOS therefore emits unsigned DXIL: it loads only with Windows Developer Mode enabled, and retail D3D12 rejects it at pipeline-state creation.
Unlike the intrinsics gap above, this one does not fail at compile time — the compile succeeds and emits the SD0214 warning, so a build that ignores warnings will produce an artifact that is broken only on the consumer's machine. Build DX12 content on Windows until cross-platform signing ships. See DirectX 12 for detail. Other targets (DX11, OpenGL, FNA) are unaffected.
Vertex-stage texture fetch is rejected on the OpenGL target
Sampling a texture in a vertex shader (e.g. displacement mapping via SampleLevel/tex2Dlod in the VS) fails loudly with SD0210 for Target = OpenGL. This is deliberate: MonoGame 3.8.2's OpenGL runtime cannot bind vertex textures at all — its GL program link assigns texture units only for the pixel shader's sampler records, and there is no GL VertexTextures apply path — so any compiled output would silently sample whatever happens to sit on texture unit 0 (typically rendering black). A loud compile error is the only honest result. Move the fetch to the pixel stage, or pass the data through a uniform/vertex stream. The DirectX and FNA targets are unaffected by this MonoGame-GL-runtime limitation.
The compile <target> profile and the #if OPENGL header
A technique pass names its shaders with compile <target> Entry(), where <target> is the shader profile:
pass P0
{
VertexShader = compile VS_SHADERMODEL MainVS();
PixelShader = compile PS_SHADERMODEL MainPS();
}
VS_SHADERMODEL / PS_SHADERMODEL are not built-in — they come from the standard MonoGame cross-platform header, which picks the right profile per backend:
#if OPENGL
#define SV_POSITION POSITION
#define VS_SHADERMODEL vs_3_0
#define PS_SHADERMODEL ps_3_0
#else
#define VS_SHADERMODEL vs_4_0_level_9_1
#define PS_SHADERMODEL ps_4_0_level_9_1
#endif
The header expresses one split twice: the profile tokens (legacy SM3 on OpenGL, the feature-level-9 SM4 forms on DirectX) and the position semantic (SV_POSITION is a real Shader Model 4+ system-value on the DirectX side, so it is used as-is; on the SM3 OpenGL side it is mapped back to the D3D9-era POSITION).
ShadowDusk validates the compile target exactly as mgfxc/fxc do: after macro expansion the token must resolve to a recognized profile. If it does not, the compile fails loudly with SD0013 instead of silently falling back to SM3. The two ways to trip it:
- A typo in the profile token (
compile A MainPS();). - A
*_SHADERMODELmacro that is not defined — most often because the#if OPENGL … #else …header above was removed or never added. Add the header (or#definethe profile directly, e.g.compile ps_3_0 MainPS();).
This is the same input mgfxc rejects with unrecognized compiler target; ShadowDusk surfaces it at compile time so the shader cannot silently ship broken.
SD0015 — a real profile that is still too low for the DirectX target
A profile can be perfectly recognized and still be one the target's reference compiler refuses. MonoGame's DirectX_11 shader profile accepts only:
vs_4_0_level_9_1 vs_4_0_level_9_3 vs_4_0 vs_4_1 vs_5_0
ps_4_0_level_9_1 ps_4_0_level_9_3 ps_4_0 ps_4_1 ps_5_0
Anything else — every SM 1–3 profile, *_4_0_level_9_0, and (counter-intuitively) every SM6 profile such as ps_6_0 — makes mgfxc /Profile:DirectX_11 fail with "Invalid profile 'ps_3_0'. Pixel shader 'MainPS' must be SM 4.0 level 9.1 or higher!". ShadowDusk reports the same condition as SD0015.
This is the shape you hit when a shader written for DesktopGL or FNA — PixelShader = compile ps_3_0 MainPS(); with no cross-platform header — is built for WindowsDX. The fix is the header above: add the #if OPENGL … #else … block so the DirectX arm defines the *_4_0_level_9_1 pair. Nothing else in the shader needs to change.
The floor is per target, and only the DirectX one is enforced today: OpenGL caps at SM 3.0 and Vulkan requires exactly *_6_0, so a profile that is wrong for DirectX is often exactly right for another target. Do not "fix" an SD0015 by raising the profile for every backend.
Uniform types on the OpenGL target (cbuffers, arrays, staged limits)
Free uniforms, named cbuffers (including one shared by both shader stages, or several in one stage), and array uniforms (float4 Colors[4], float4x4 Bones[N], float/float2/float3 arrays) are fully modelled: array parameters expose their elements in Effect.Parameters — Parameters["Colors"].SetValue(Vector4[]) and Parameters["Colors"].Elements[i] work exactly as with mgfxc output, on every target.
Two uniform shapes are rejected loudly (SD0210) on the OpenGL target rather than silently miscompiled (a staged scope — these shapes are not modelled on the GL path yet):
int/bool(andintN/boolN) uniforms — MojoShader models these in separate{vs,ps}_uniforms_ivec4/_boolregister sets that ShadowDusk does not emit yet. Use afloat-typed uniform and cast inside the shader.- Non-
float4x4matrices (float3x3,float2x2, …) andstructuniforms — pad the matrix tofloat4x4or split it into vectors.
The loud error is the honest result until these shapes are modelled — far better than the silent broken GLSL they would otherwise produce (which fails only at Effect-load time inside the game).
DirectX uses vkd3d-shader, not DXC
For Target = DirectX, ShadowDusk emits DXBC (SM ≤ 5) — what MonoGame's DX11 runtime loads — via vkd3d-shader (cross-platform) or d3dcompiler_47 (Windows-only oracle), selected by DxbcBackend. DXC is not used for DX11 because it only emits SM6 DXIL. See DirectX DXBC (vkd3d) Path.
Effect container: MGFX v10 (default), and opt-in MGFX v11 / KNIFX
ShadowDusk produces MGFX v10 by default (MgfxVersion defaults to 10; CLI --mgfx-version). v10 is the one effect container that both MonoGame (3.8.x DesktopGL/WindowsDX) and KNI (Reach and HiDef) load — MonoGame reads it directly and KNI reads it as its supported migration format. It is the seamless default: you never set a flag to get correct output, and it loads on every MonoGame 3.8.1.263+ and KNI runtime. 3.8.1.263 is the measured floor: every stable MonoGame.Framework.DesktopGL release was probed and 3.8.0.1641 is the one that rejects v10 (its loader predates the format). ShadowDusk does not emit the older v9 format.
Two opt-in, experimental newer containers are available for consumers targeting newer runtimes. Both are additive — the v10 default is unchanged, and you only get them if you ask:
- MGFX v11 — set MgfxVersion
= 11(CLI--mgfx-version 11). A faithful MonoGame v11 container for MonoGame 3.8.5+. v11 adds two per-shader diagnostic strings (the source file + entry point, used only in shader error messages, MonoGame PR #8813); it renders identically to v10. Render-proven in real MonoGame 3.8.5. (v11 stays opt-in: v10 loads in every MonoGame from 3.8.1.263 onward and in KNI, whereas v11 requires 3.8.5+.) - KNIFX v11 — set Container
= EffectContainer.Knifx. KNI's newer KNIFX container for KNI v4.02+, render-proven in real KNI v4.2.9001.MgfxVersionis ignored whenContainer == Knifx;Containeris ignored for theFnatarget (always D3D9 fx_2_0).
To pick a whole target (backend + container) with one value instead of setting these separately, set CompilerOptions.Profile to a CapabilityProfile (e.g. CapabilityProfile.KniGL_4_02), or use the CLI --target-runtime flag — a profile overrides Target / Container / MgfxVersion. See Choosing a Target.
Important:
--mgfx-version 11writes a correct, full v11 body (not merely a bumped header byte over a v10 body). Still, leave the default (v10) unless you specifically target a newer runtime and want its container — v10 loads everywhere.
.mgfx vs .xnb
ShadowDusk's compiled effect is a raw .mgfx blob. The .xnb is the Content Pipeline's container that wraps a .mgfx (along with a type-reader header), and it's what Content.Load<Effect>("name") reads. The raw .mgfx is what the new Effect(graphicsDevice, mgfxBytes) constructor reads directly. ShadowDusk produces either:
- Loading effects with
new Effect(gd, bytes)→ use ShadowDusk's.mgfxas-is. - Loading via
Content.Load<Effect>→ you need an.xnb, and ShadowDusk writes it directly: callresult.Value.ToXnb(), or give the CLI an.xnboutput path (the extension is the whole switch). The platform byte is derived from your target and the payload inside is the same.mgfxbytes. No MGCB involved — though for builds that already run MGCB for other content, the MGCB Content Pipeline plugin route also works.
(mgfxc itself only ever emits the raw effect and leaves .xnb wrapping to the content pipeline — the direct route is one thing ShadowDusk does that it doesn't.) See Choosing a Target.
KNI HiDef / WebGL2
A single ShadowDusk .mgfx loads in both KNI Reach (WebGL1) and HiDef (WebGL2 / GLSL ES 3.00) — no profile flag, no separate build. KNI's runtime converts the legacy GLSL to ES 3.00 at load, and ShadowDusk emits the #define-aliased fragment output that converter expects. HiDef shader loading needs a recent KNI (the release that added the runtime converter); Reach and desktop GL have no version requirement. After upgrading ShadowDusk, recompile your .fx — an old .mgfx keeps the old output.
Includes
Pass extra include search paths with the CLI /I <path> (repeatable) or the library's AdditionalIncludePaths / a custom IIncludeResolver. Missing or circular includes fail loudly with SD0001 / SD0002 diagnostics.
Errors fail loudly
Compilation returns Result<CompiledShader, ShaderError[]>. Each ShaderError carries the file, line, column, code, and message exactly as the underlying compiler emitted them; FxcFormattedMessage renders the MGCB-parseable form. No exceptions are thrown for expected shader failures.
Validating a .fx (no render needed)
Because failures come back as data, CompileAsync doubles as a validator/linter: compile the source, ignore the .mgfx bytes on success, and read ShaderError[] on failure for the line, column, and message. No graphics device, no Effect load, no render required — handy for IDEs, build checks, and shader linters.
var result = await compiler.CompileAsync(fxSource, options);
if (result.IsFailure)
foreach (var e in result.Error)
Console.WriteLine($"{e.File}({e.Line},{e.Column}): {e.Code}: {e.Message}");
This works identically on desktop and in the browser — the WASM path reports the same Line/Column as desktop, so an in-browser editor (e.g. a KNI/Blazor shader fiddle) can highlight the offending line. It's a real compile (not a parse-only pass), so it surfaces exactly what the shipping pipeline would reject.