MonoGame Content Pipeline: Cross-Platform Asset Compilation Explained
MonoGame's Content Pipeline compiles raw assets into platform-specific .xnb files at build time, enabling identical runtime loading across all targets. Covers MGCB integration, texture compression, shader translation, custom processors, and async loading.
10 Apr 2026, 04:04 UTC

The Problem and the Takeaway
MonoGame games ship on Windows, macOS, Linux, iOS, Android, and consoles. Each platform expects different texture compression formats, shader bytecode, and audio containers. Writing platform-specific asset loaders for every target is impractical. The MonoGame Content Pipeline solves this by compiling raw assets (.png, .fbx, .wav, .fx) into optimized, platform-specific .xnb files at build time. The runtime ContentManager.Load<T> API then works identically everywhere because the heavy lifting—format conversion, shader translation, compression—already happened during the build.
How the Pipeline Fits Into Your Build
The pipeline is driven by MGCB (MonoGame Content Builder), invoked automatically via the MonoGame.Content.Builder.Task NuGet package when you build a .csproj that contains <MonoGameContentReference> items. A .mgcb file (XML) lists every asset, its importer, processor, and per-platform parameters. MGCB reads this file, hashes each source file, and writes .xnb outputs to bin/$(Configuration)/$(TargetFramework)/Content/ matching the current target platform.
Worked Example: Platform-Specific Texture Compression
Suppose you have a 2048×2048 UI atlas Content/Textures/ui-atlas.png. You want DXT5 (BC3) on DesktopGL, ASTC 4x4 on iOS, and ETC2 on Android. Open the .mgcb file in the MGCB Editor (dotnet mgcb-editor) or edit the XML directly. For each platform, set the TextureFormat processor parameter:
<Item>
<Name>ui-atlas</Name>
<Importer>TextureImporter</Importer>
<Processor>TextureProcessor</Processor>
<ProcessorParams>
<Parameter Name="TextureFormat" Value="Compressed" />
<Parameter Name="PremultiplyAlpha" Value="True" />
<Parameter Name="GenerateMipmaps" Value="False" />
<Parameter Name="ResizeToPowerOfTwo" Value="False" />
<Parameter Name="ColorKeyEnabled" Value="False" />
<Parameter Name="ColorKeyColor" Value="255,0,255,255" />
</ProcessorParams>
<Platforms>
<Platform Name="Windows" TextureFormat="Dxt5" />
<Platform Name="WindowsGL" TextureFormat="Dxt5" />
<Platform Name="MacOSX" TextureFormat="Dxt5" />
<Platform Name="Linux" TextureFormat="Dxt5" />
<Platform Name="iOS" TextureFormat="Astc4x4" />
<Platform Name="Android" TextureFormat="Etc2" />
</Platforms>
</Item>
Build for net8.0-android and the output ui-atlas.xnb will contain ETC2-compressed data. Build for net8.0-ios and you get ASTC. The same Content.Load<Texture2D>("Textures/ui-atlas") call works at runtime on both platforms. Verify the format at runtime:
var tex = Content.Load<Texture2D>("Textures/ui-atlas");
Console.WriteLine(tex.Format); // Shows SurfaceFormat.Dxt5, Astc4x4, or Etc2
Shader Compilation: HLSL to GLSL/Metal/MSL at Build Time
Effect files (.fx written in HLSL) are compiled by the pipeline. On Windows DirectX targets, FXC produces .fxb bytecode. On DesktopGL, iOS, Android, and Mac, MojoShader translates HLSL to GLSL, Metal Shading Language, or SPIR-V at build time. The translated bytecode is stored inside the .xnb, so the game never compiles shaders at runtime. This eliminates first-frame stalls and removes the need for a shader compiler on the device.
To use a custom effect, add Content/Effects/basic.fx to the .mgcb file with EffectImporter and EffectProcessor. The processor parameter Effect points to the entry point technique. No extra code is required.
Custom Content Processors
When built-in processors aren't enough—e.g., you need to bake procedural mesh data, convert a custom model format, or generate sprite font atlases from a texture atlas—you write a custom processor:
- Create a class library targeting
netstandard2.0(or compatible TFM). - Reference
MonoGame.Framework.Content.PipelineNuGet. - Implement
ContentProcessor<TInput, TOutput>. - Build the library, then add
/reference:path/to/YourProcessor.dllto the.mgcbfile (or use the MGCB Editor's References tab). - Assign the processor to specific assets in the
.mgcbfile.
public class AtlasProcessor : ContentProcessor<Texture2DContent, Texture2DContent> {
public override Texture2DContent Process(Texture2DContent input, ContentProcessorContext context) {
// Example: force premultiplied alpha on all mip levels
foreach (var face in input.Faces) {
foreach (var mip in face) {
BitmapUtils.PremultiplyAlpha(mip);
}
}
return input;
}
}
The processor runs on the build machine (inside MGCB), not at runtime. The output .xnb contains the processed data ready for Content.Load.
Incremental Builds and the Cache
MGCB maintains a .mgcb.cache file (JSON) mapping source file paths to content hashes. On subsequent builds, only assets whose hash changed are reprocessed. For a project with thousands of textures, this reduces clean-build times from minutes to seconds. The cache lives next to the .mgcb file and is checked into source control by default—safe to share across the team.
Async Loading in MonoGame 3.8+
ContentManager.LoadAsync<T>(assetName) returns a Task<T>. Internally it uses Task.Run to read and deserialize the .xnb on a background thread, then marshals the final object back to the calling thread. This prevents UI thread stalls when loading large textures or models during gameplay. Example:
var loadTask = Content.LoadAsync<Texture2D>("Textures/large-background");
// ... do other work ...
var background = await loadTask;
// background is ready to draw
Note: the final deserialization step still runs on the calling thread. For extremely large assets (hundreds of MB), consider streaming via TitleContainer.OpenStream and custom deserialization to avoid a single-frame hitch.
Common Mistakes and How to Avoid Them
1. Wrong Item Type in .csproj
Assets must be included as <MonoGameContentReference Include="Content/**/*" />. Using <None> or <Content> skips the pipeline entirely—the raw files copy to output but are not converted to .xnb, causing ContentLoadException at runtime.
2. Unsupported Compression for Target Platform
The .mgcb file does not validate compression choices at build time. If you set TextureFormat="Astc4x4" for a WindowsGL build, the build succeeds but Content.Load throws at runtime. Always test each target platform.
3. Custom Processor Assembly Mismatch
Custom processor DLLs must target netstandard2.0 (or a TFM compatible with the MGCB runtime, typically .NET 6/8). A processor built for .NET Framework 4.8 will fail with Could not load file or assembly when MGCB runs on .NET 8.
4. Changing RootDirectory After First Load
ContentManager.RootDirectory defaults to "Content". Set it before any Load call. Changing it later invalidates the manager's internal path cache, causing missing-asset exceptions for previously loaded content.
5. MGCB Not Rebuilding on .mgcb Changes
In some IDE configurations (especially VS Code with the C# Dev Kit), editing the .mgcb file doesn't trigger a rebuild. Run dotnet clean && dotnet build or invoke dotnet mgcb manually after adding assets or changing processor parameters.
Verification Checklist
- Create a DesktopGL project, add a PNG, build, confirm
.xnbappears inbin/Debug/net8.0/Content/. - Open the
.mgcbin MGCB Editor, inspect per-platform compression settings. - Add a custom processor library, reference it, assign to an asset, rebuild, check build logs for processor execution.
- Switch target to Android, rebuild, load the texture at runtime, assert
Texture2D.Format == SurfaceFormat.Etc2. - Modify one source asset, rebuild, verify only that asset's output timestamp changes.
- Profile
LoadAsyncfor a large texture; confirm main thread frame time stays under budget.
Limitations
- The pipeline runs at build time only; no hot-reloading of assets in shipped builds.
- Platform-specific settings live in the
.mgcbXML—no per-configuration (Debug/Release) overrides without multiple.mgcbfiles. - Audio compression options are limited to what the underlying platform supports (XACT on Windows, native codecs elsewhere).
- Custom processors cannot access game runtime types; they only see pipeline content types (
*Contentclasses).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.