MonoGame Content Pipeline: When to Build .xnb and When to Load Raw
MGCB compiles assets into platform-optimized .xnb files at build time. Use it for static content; reserve Texture2D.FromStream for user-generated or post-launch assets. Covers trade-offs, CI/CD, and verification.
27 Jul 2026, 03:02 UTC

The asset decision that bites you at 2 AM
You're shipping a MonoGame project. The artist drops a 4K PNG for a background. The designer wants players to import custom sprites. The CI pipeline runs on Linux, but you develop on Windows. Do you run everything through dotnet mgcb and ship .xnb files, or do you call Texture2D.FromStream at runtime and move on?
The answer isn't "always use the pipeline." It's "use the pipeline for everything static, and only reach for FromStream when the content genuinely isn't known at build time." This post walks through why that rule holds, where it breaks, and how to verify the trade-offs yourself.
What MGCB actually does at build time
The MonoGame Content Builder (MGCB) is an offline processor. It takes source assets — PNG, FBX, WAV, .spritefont, .mgfxo — and emits .xnb files tuned for each target platform. On Windows DesktopGL you get DXT/BC compressed textures. On iOS you get PVRTC. On Android you get ETC2. The same source PNG becomes different GPU-ready bytecode without you writing platform branches.
Beyond compression, the pipeline handles premultiplied-alpha conversion, mipmap generation, texture atlasing via custom processors, and effect compilation. HLSL shaders become DirectX bytecode on Windows, GLSL or SPIR-V via MojoShader/MoltenVK elsewhere. A .spritefont file turns into a pre-rasterized glyph atlas at a fixed size. All of this happens once, on the build machine, not on the player's device.
The engineering decision framework
Treat Content.Load<T>("asset") as the default path for any asset that ships with your game: UI textures, level geometry, character models, audio cues, shader bytecode, and any custom data types you serialize through ContentTypeWriter/ContentTypeReader pairs. You get deterministic memory layout, optimal GPU formats, mipmaps for free, and versioned serialization.
Reserve Texture2D.FromStream (or raw model/audio parsing) for three scenarios only:
- User-generated content — players importing their own sprites.
- Modding support — external creators dropping files into a mods folder.
- Assets downloaded post-launch — seasonal events, DLC packs delivered without an app update.
Everything else pays a measurable penalty when loaded raw: uncompressed RGBA32 on CPU then uploaded to GPU, no mipmaps (so aliasing and bandwidth spikes on mobile), no premultiplied-alpha handling, no cross-platform format translation. A 2048×2048 PNG loaded via FromStream consumes roughly 16 MB VRAM; the same texture through MGCB with BC7 compression drops to ~4 MB.
Worked example: CI/CD integration with platform-specific outputs
You want deterministic asset builds on Windows, macOS, and Linux runners. The dotnet mgcb tool (installed via dotnet tool install -g dotnet-mgcb) runs headless. A minimal Content.mgcb file lists your assets and processors. In your .github/workflows/build.yml:
- name: Build content for Windows
run: dotnet mgcb /platform:Windows /outputDir:Content/Windows /quiet
- name: Build content for iOS
run: dotnet mgcb /platform:iOS /outputDir:Content/iOS /quiet
- name: Build content for Android
run: dotnet mgcb /platform:Android /outputDir:Content/Android /quiet
Each invocation produces a platform-tuned .xnb tree. On the macOS runner, the iOS build emits PVRTC data; on the Ubuntu runner, Android emits ETC2. Your game's Content.RootDirectory switches at runtime based on DeviceInfo.Platform (or your own platform detection). This avoids the classic pitfall: a single .xnb built on Windows that only contains DXT data, then fails to render efficiently on mobile.
To verify the output, open any generated .xnb in a hex editor. The first three bytes are XNB. The fourth byte is the format version. The platform-specific texture data follows. On iOS you'll see PVRTC headers; on Android, ETC2 markers. You can also run PVRTexToolCLI -i Content/iOS/background.xnb -info on a macOS box to confirm PVRTC compression.
Where the pipeline bites back
Three practical limitations deserve your attention before you commit.
Version sensitivity
.xnb is MonoGame-specific and version-sensitive. A pipeline version mismatch between build and runtime causes load failures with opaque errors. Pin the MonoGame NuGet version in both your game project and the MGCB tool. In global.json or your CI script, lock dotnet-mgcb to the same version as your MonoGame.Framework.DesktopGL package (for example, dotnet tool install -g dotnet-mgcb --version <version>).
Shader profile traps
Effect compilation requires the correct shader profile. Targeting ps_4_0_level_9_1 for older mobile GPUs is safe; ps_5_0 will silently fall back to software rendering or crash on constrained devices. Set the profile in your .mgfxo source or via /profile: flag in Content.mgcb. Test on the lowest-spec device you support.
Font scaling is static
.spritefont rasterizes at build time at a fixed point size. Dynamic UI scaling means either shipping multiple pre-built sizes (e.g., Font12.spritefont, Font24.spritefont) or integrating a signed-distance-field (SDF) pipeline extension — not included by default. Plan your UI font strategy before you hit the scaling wall.
Verify before you ship
Run these checks in your next sprint:
- Memory comparison: Load the same 2048×2048 PNG via
Content.Load<Texture2D>andTexture2D.FromStream. Profile withdotnet-traceor Visual Studio Diagnostics. Expect ~4× VRAM difference on mobile GPUs. - Cross-platform build test: On a macOS runner, run
dotnet mgcb /platform:iOS /outputDir:Content/iOSand inspect the output withPVRTexToolCLI. Confirm PVRTC data exists. - Custom type round-trip: Write a
ContentTypeWriterfor a simpleLevelDatastruct, rebuild, load viaContent.Load<LevelData>, and verify the same bytes deserialize on Windows and Linux builds.
If all three pass, your pipeline is doing its job. If any fails, you've caught a platform mismatch before it reaches players.
Closing thought
The pipeline isn't ceremony — it's the only path that guarantees the texture on disk matches the texture the GPU expects, on every platform you ship. Use FromStream for the genuinely dynamic. Build everything else offline. Your future self, debugging a 3 AM OOM on a low-end Android, will thank you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.