Skip to content

Add MonoGame 3.8.5 Content Builder export mode (closes DX12/Vulkan shader gate)#120

Merged
vchelaru merged 7 commits into
mainfrom
feat/monogame-content-builder-export
Jul 19, 2026
Merged

Add MonoGame 3.8.5 Content Builder export mode (closes DX12/Vulkan shader gate)#120
vchelaru merged 7 commits into
mainfrom
feat/monogame-content-builder-export

Conversation

@vchelaru

@vchelaru vchelaru commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds an opt-in ContentBuildMode.ContentBuilder export choice, available for every MonoGame target. It routes both assets and shipped .fx shaders through MonoGame 3.8.5 GA's new code-first Content Builder (a generated {ProjectName}.Content console project + Builder.cs + BuildContent.targets) instead of raw/ShadowDusk/MGCB, superseding ShaderCompileMode for that export.
  • Unlike the legacy mgcb.exe, the new Content Builder isn't limited to a fixed platform list — so this is the mechanism that finally closes the DX12/Vulkan shader gate from issue Export shaders to gated targets: iOS, MonoGame DX12/Vulkan #52 (CompilesShippedShaders now returns true there under Content Builder mode).
  • MonoGame.Content.Builder.Task stays installed on classic MonoGame targets even in Content Builder mode: Apos.Shapes/Gum ship their own .fx + Content.mgcb via buildTransitive, invisible to the new pipeline (which only walks the fiddle's own Assets/ folder). Both pipelines deliberately coexist there.
  • New export-dialog "Content pipeline" radio (Raw / Content Builder), gated to ExportRuntime.MonoGame; picking Content Builder hides the now-superseded shader-compilation radio and updates the status banner.

Stacks on #119 (chore/monogame-3.8.5-ga, not yet merged) — this branch is based on that PR's HEAD, so the diff includes its commit until #119 merges.

Implementation notes / deviations from the plan

  • Asset exclusion list widened from 2 to 13 extensions. The plan named only .achx/.fnt as needing to stay raw (no pipeline importer). A grep of Index.razor.cs's SupportedAssetExtensions (broadened by Broaden asset allowlist to include tilemap/level and text-data formats #115, the commit right before this stack) turned up the full set that XnaFiddle ships raw via TitleContainer.OpenStream/dedicated loaders rather than ContentManager.Load<T>: .achx .fnt .ttf .ember .xnb .tmx .tsx .world .ldtk .ogmo .json .txt .xml. Only .png/.wav are actually ContentManager-loadable, so those are the only ones routed into {ProjectName}.Content/Assets/; everything else keeps shipping into the head's own Content/ folder as before.
  • Fixed a real bug in the plan's own predicate spec. The plan's verbatim UsesShadowDuskShaders/UsesMgcbShaders guarded on contentMode != ContentBuildMode.ContentBuilder (global), which would have gated all shaders — including KNI/FNA — to "unsupported" the moment ContentBuildMode.ContentBuilder was passed to Export(), even for a non-MonoGame target. Changed the guard to !UsesContentBuilder(target, contentMode) (target-aware) so KNI/FNA stay a true no-op, matching the plan's own required "KNI/FNA no-op" test.
  • GenerateCsproj's raw-copy suppression is conditional, not unconditional. The plan said to unconditionally suppress the <None Include>/<AndroidAsset Include> raw-copy block under Content Builder mode. That would silently drop the .achx/.fnt-style excluded files from the build output entirely (they'd still be zipped into the head's Content/ folder, but never copied to the output dir). Instead the block now stays gated on whether there's any Content-Builder-excluded content left (hasRawContentBuilderAssets), so those files still copy correctly.

Verification

  • dotnet test — 337/337 passing (7 new facts + 5 new Theory rows for Content Builder mode).
  • dotnet buildXnaFiddle.Core, XnaFiddle.Tests, XnaFiddle.BlazorGL all clean.
  • Real end-to-end build attempted (network/NuGet was reachable in this sandbox, including the brand-new MonoGame.Tool.Dxc etc. packages): generated a real Content-Builder-mode zip for a classic target (MonoGameDesktopGL) and a next-gen target (MonoGameWindowsDX12), dotnet restore + dotnet build both.
    • Classic target: asset (.png) and shader (.fx) both correctly picked up by WildcardRule("*") and compiled to .xnb. Confirms the asset-routing/exclusion logic is correct and there's no MonoGame.Content.Builder.Task vs. Content Builder target-name collision.
    • DX12 target: asset compiled fine; the shader failed with a legitimate, expected error (Invalid DirectX 12 pixel profile 'ps_3_0'! Requires ps_6_0) — my throwaway test .fx used a legacy SM3 profile, which DX12's real DXC-based compiler correctly rejects. This confirms the DX12 plumbing (packages, Import, WildcardRule routing) works end-to-end; it's not an exporter bug, just a reminder that a real shipped .fx needs an SM6-compatible technique to build on DX12.
    • Found a real, reproducible issue (not a target-name collision, but the same category of risk): running dotnet build on the whole .slnx (default multi-core parallelism) races — the Content project is both a normal top-level .slnx member and invoked via a nested <MSBuild Projects="...Content.csproj"> inside BuildContent.targets, and the two concurrent builds of the same project collide on the output DLL (CS2012: ... being used by another process). Verified this is not something this PR introduced — MonoGame's own official 3.8.5 GA template (fetched from MonoGame/MonoGame.Templates) has the exact same structure (byte-for-byte matching BuildContent.targets/{Name}.Content.csproj, confirmed by direct comparison) and would hit the identical race. Two verified workarounds: dotnet build *.slnx -maxcpucount:1, or dotnet restore *.slnx once followed by dotnet build on just the single head .csproj. A ProjectReference ReferenceOutputAssembly="false" fix doesn't work as-is (cross-TFM error: head is net8.0, Content project is net9.0 per the reference template). Flagging this for follow-up rather than fixing here, since it's an upstream MonoGame template characteristic, not specific to this PR's logic.

Test plan

  • dotnet test (337/337)
  • dotnet build across XnaFiddle.Core/XnaFiddle.Tests/XnaFiddle.BlazorGL
  • Real dotnet restore/dotnet build of a generated Content-Builder-mode export (classic + DX12 targets)
  • Manual verification in the running app (export dialog UI)

Update: reworked into two independent axes

The design above conflated "how assets build" and "how shaders compile" into one 3-way choice — picking Content Builder unconditionally forced shaders through its native DXC pipeline and hid ShadowDusk entirely. Reworked into two orthogonal enums:

  • ContentBuildMode: Raw / ClassicMgcb (new) / ContentBuilder — how non-shader assets build.
  • ShaderCompileMode: ShadowDusk / Native (renamed from ContentPipeline) — how .fx compiles, independent of asset strategy.

ShadowDusk is now the default shader compiler under every content strategy, not just Raw. ClassicMgcb is a real asset pipeline for the first time (previously it only ever touched .fx) — .png/.wav now compile via TextureImporter/WavImporter, with ColorKeyEnabled=False forced (MonoGame's TextureProcessor defaults to color-keying pure magenta to transparent, which would otherwise silently punch holes in shipped textures — confirmed with a real end-to-end build, see Verification below).

DX12 + ShadowDusk: per the plan, checked ShadowDusk's NuGet feed and PlatformTarget.cs for a DX12 backend before wiring MonoGameWindowsDX12 into GetShaderExportInfo. Still at 0.12.0, no DirectX12 enum member, README lists DX12 as "Not yet". Left the existing _ => default fallthrough as-is; CompilesShippedShaders(DX12, ShadowDusk, ContentBuilder) stays false. Test DX12_ContentBuilder_ShadowDusk_StaysGated_UntilShadowDuskAddsDx12 pins this and documents the flip: once ShadowDusk ships DX12, add the MonoGameWindowsDX12 case and this assertion flips to true with no other code change needed.

Bug found during implementation (not called out in the plan): decoupling the axes means a ShadowDusk-mode .fx can now stay in the head's own Content/ folder even under ContentBuildMode.ContentBuilder (previously Content Builder always redirected shaders too, so this case never existed). The csproj's wholesale-copy-to-output gate assumed nothing was ever left in Content/ under Content Builder mode, so that .fx would ship in the zip but never get CopyToOutputDirectory — build succeeds, but the shader fails to load at runtime. Fixed by broadening hasNonPipelineContentBuilderAssets's computation to also cover hasShaders && !useContentBuilderShaders; regression-tested by SinglePlatform_ContentBuilder_ShadowDuskShaders_HeadContentShaderStillWiredForCopyToOutput.

Verification (this update):

  • dotnet test XnaFiddle.Tests — 350/350 passing (76 tests in ProjectExporterTests).
  • dotnet build XnaFiddle.sln — clean.
  • Manual end-to-end: exported the new default combo (ClassicMgcb + ShadowDusk, one magenta .png asset) via a throwaway console driver calling ProjectExporter.Export directly, extracted the zip, ran a real dotnet restore/dotnet build, then parsed the compiled magenta.xnb's raw pixel bytes directly — all 4 texels stayed opaque magenta (alpha=255), confirming ColorKeyEnabled=False works end-to-end, not just structurally in the generated Content.mgcb text.

Scope: this update covers the core enum/predicate rework, GenerateContentMgcb/asset-routing/GenerateCsproj, the UI (two independent radio groups), and the ProjectExporterTests.cs rewrite. The automated build-verification test suite (XnaFiddle.ExportBuild.Tests) remains a separate, deferred follow-up.

🤖 Generated with Claude Code

vchelaru and others added 7 commits July 18, 2026 19:40
3.8.5 shipped stable on nuget.org, replacing the preview line. Removes
the now-dead preview/stable duality (version props, UI selector,
.Native version-gating, the monoGameVersion override) — WindowsDX12
and DesktopVK are now regular, always-available MonoGame targets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ader gate)

Opt-in ContentBuildMode.ContentBuilder routes assets and shaders through
MonoGame's new code-first Content Builder (Builder.cs + BuildContent.targets)
instead of raw/ShadowDusk/MGCB, superseding ShaderCompileMode when chosen.
Unlike mgcb.exe it isn't limited to a fixed platform list, so it also closes
the DX12/Vulkan shader gate from issue #52. MonoGame.Content.Builder.Task
stays installed on classic targets for buildTransitive-only libraries
(Apos.Shapes/Gum) that the new pipeline can't see.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MonoGame's DX12/Vulkan Content Builder compiles via DXC, which requires
Shader Model 6 (vs_6_0/ps_6_0), rejects the legacy sampler2D/tex2D combo,
and requires SV_Target0 instead of COLOR/COLOR0 for the pixel output.
None of the example shaders had an SM6 branch, so ContentBuilder exports
to MonoGameWindowsDX12/MonoGameDesktopVK failed to compile every shader.

Adds an `#elif defined(SM6) || defined(VULKAN)` branch to each file's
shader-model defines, a matching Texture2D<float4>/SamplerState
declaration for the texture(s), and a fully separate SM6 pixel-shader
entry point (ShadowDusk's OpenGL translator requires the classic
`: COLOR` / `tex2D(...)` entry point to stay literal and unsplit, so the
two shader models get independent function bodies rather than sharing
one via macros or a split signature). The `#if OPENGL` / classic branches
are untouched.

Verified: ShadowDuskCLI /Profile:OpenGL produces byte-identical .mgfx
output before/after for all 16 files; a real `dotnet build` of a
ContentBuildMode.ContentBuilder export targeting MonoGameWindowsDX12
compiles all 16 shaders with zero errors; full XnaFiddle.Tests suite
(337 tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… Vulkan backend

ShadowDusk 0.12.0 adds a real Vulkan backend that compiles the same plain
.fx source as every other target (no SM6 source rewrite needed, verified
against the pre-SM6-branch form of Grayscale.fx via ShadowDuskCLI and a
real dotnet build). GetShaderExportInfo now wires MonoGameDesktopVK through
ShadowDusk.Compiler/PlatformTarget.Vulkan, closing the DesktopVK half of
issue #52; MonoGameWindowsDX12 is unchanged (still gated, still needs
ContentBuildMode.ContentBuilder for shaders).

Verified the 0.11.0 -> 0.12.0 GL codegen fidelity change (pow/division
strength-reduction) produces byte-identical .mgfx for all 16 bundled
example shaders, so no rendering regression risk there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Shader compilation:" and "Content pipeline:" silently overrode each
other and both used the words "Content Pipeline"/"MGCB" for different
things. Replaced with a single "Content strategy:" group (ShadowDusk /
Classic MGCB / MonoGame Content Builder) backed by one UI-only
ContentStrategy enum that maps to ProjectExporter's existing
(ShaderCompileMode, ContentBuildMode) pair. No changes to
ProjectExporter's public API.
Splits the 3-way content strategy choice into two independent enums:
ContentBuildMode (Raw/ClassicMgcb/ContentBuilder) for non-shader assets,
and ShaderCompileMode (ShadowDusk/Native, renamed from ContentPipeline)
for .fx compilation. ShadowDusk is now the default shader compiler under
every content strategy, not just Raw - previously Content Builder mode
silently forced shaders through its own native pipeline.

ClassicMgcb becomes a real asset pipeline for the first time: .png/.wav
compile via TextureImporter/WavImporter, with ColorKeyEnabled=False
forced to stop MonoGame's default magenta color-keying from punching
holes in shipped textures.

DX12 was NOT wired into GetShaderExportInfo - checked ShadowDusk's NuGet
feed and PlatformTarget.cs; still 0.12.0 with no DirectX12 backend
(README lists it "Not yet"). CompilesShippedShaders(DX12, ShadowDusk,
ContentBuilder) stays gated until ShadowDusk ships one.

Fixed a bug surfaced by the axis decoupling: a ShadowDusk-mode .fx can
now stay in the head's own Content/ under ContentBuildMode.ContentBuilder,
but the csproj's copy-to-output gate assumed nothing ever did, so the
shader shipped in the zip but never reached the build output.

UI: two independent radio groups replace the single 3-way choice; Raw
shows a fixed "ShadowDusk (fixed)" label since it has no native pipeline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Multi-platform exports listed the shared Common library and Content Builder
console project before any actual game project — Visual Studio picks the
first .slnx entry as the default startup project, so users had to manually
retarget it every time. Platform-head projects now come first.

Also refreshes the two now-stale skill docs (project-export, shaders) and
CLAUDE.md's skill-maintenance policy for the orthogonal content/shader axes
rework, and removes "(default)" from radio labels that snuck back in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vchelaru
vchelaru merged commit a54a963 into main Jul 19, 2026
1 check passed
@vchelaru
vchelaru deleted the feat/monogame-content-builder-export branch July 19, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant