Tutorial 147: Load XNB from XNA and MonoGame Content
What you’ll learn: What the 61 built-in .xnb readers cover, how the container is checked, how to find the reader names a content folder needs, and how to write and re-read .xnb with cna-content --format xnb.
Before you start — Tutorial 45: ContentManager for the Load<T> basics. Everything here describes CNA snapshot 009d40f5. The C++ on this page was syntax-checked against that snapshot’s headers; it has not been linked or run, and the shell commands were not executed for this page. Where a statement rests on CNA’s own documentation or test claims rather than on the code, the text says CNA reports.
What this tutorial covers
An .xnb is the compiled container XNA’s Content Pipeline wrote and that MonoGame and FNA read. If you are porting a game, the Content/ folder you already have is full of them. At this snapshot CNA can read them (61 built-in type readers, LZX and LZ4 decompression, shared resources), and it can also write them: cna-content build --format xnb runs CNA’s importer, processor and writer chain and emits an .xnb instead of the default .cnb. This tutorial goes through both directions and is careful about which parts are verified in the source and which rest on CNA’s own reports.
- What loads and what does not, as a table.
- Drop the files in and load them from a
Game. - The container CNA accepts: platform bytes, versions, compression, limits.
- Diagnose a folder of files before you run the game.
- Write
.xnbfrom CNA and load it back. - Convert
.xnbto.cnbwith the same tool. - What CNA reports, and what this page can and cannot vouch for.
What loads and what does not
Content in the .xnb | Loads? | Notes (from the reader sources unless marked) |
|---|---|---|
| Texture2D, Texture3D, TextureCube | Yes | Texture2D and TextureCube: DXT1/3/5 are decompressed to Color on the CPU unless the active renderer opts in to keeping the blocks; the Texture2D reader refuses a texture larger than the device’s maximum dimension. Texture3D: only the uncompressed HiDef volume formats are accepted (DXT is refused and the data are uploaded as stored); a Reach device refuses volume textures, and HiDef caps each axis, when the texture is created. |
| SpriteFont | Yes | A genuine XNA 4.0 Hud.xnb with a 128×132 DXT3 atlas is committed as a fixture, and the Texture2D path has a special case for block-aligned non-power-of-two atlases under the Reach profile. |
| SoundEffect | Yes (most formats) | Mono and stereo. 8- and 16-bit PCM, 32-bit float, and 4-bit MS and IMA ADPCM are decoded to 16-bit PCM. XMA2 and unknown codecs are refused with unsupported SoundEffect wave format (formatTag=…). |
| Song | Reference only | The .xnb stores a path to a media file, not the audio; the path must be relative and stay inside the content root. The reader strips the last four characters and probes .ogg, .oga, .qoa; if none exists it keeps the stored path, which must exist. Put a real audio file beside the .xnb. |
| Video | Metadata | The reader is registered on every build. It reads the metadata; playback needs the optional FFmpeg backend and otherwise throws NotSupportedException. |
| Model, VertexBuffer, IndexBuffer, VertexDeclaration | Yes | With the five stock effects (BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect) and EffectMaterial/ExternalReference entries. A Model.Tag that holds a game type (skinning data, custom animation) needs a reader you register: Tutorial 148. |
| Compiled Effect (Effect Framework bytecode) | Renderer-qualified | Read only when the active renderer reports GraphicsCapability::CompiledEffects. MGFX (MonoGame’s effect container) is refused, and HLSL .fx source is never accepted at run time. Details below. |
Curve, primitives, math structs, TimeSpan, DateTime, Decimal | Yes | Decimal is registered only on toolchains that define SHARP_RUNTIME_HAS_NATIVE_INT128, which is why the count is 61 on some builds and 60 on others. |
| Collections | Closed set | 11 closed generics are pre-registered (for example List<string>, List<int>, Dictionary<string,int>, Dictionary<string,string>). A collection of your own type needs its closed reader registered: Tutorial 148. |
| Your own types | If you register a reader | There is no reflection. Register a ContentTypeReader<T> or a ReflectiveTypeReaderBuilder<T>. |
Xbox 360 files (platform byte x) | Not verified here | The header check accepts the byte. This page did not verify Xbox payloads, and CNA refuses to write the platform unless you add --xnb-allow-unverified-xbox. |
Drop the files in and load them
Copy the .xnb files, unchanged, into your content root and ask for them by logical name: the path relative to the root, no extension, either slash. ContentManager tries <name>.xnb before anything else, ahead of a literal path, a .cnb, a .cnj and any registered loose-file reader. So a compiled asset always wins over a same-named PNG beside it, which makes migrating one asset at a time safe. The lookup walks directories case-insensitively, so Textures/Wall finds textures/wall.xnb.
void LoadContent() override
{
// A Game constructor already registered the built-in .xnb readers; nothing to call.
ContentManager& content = getContentProperty();
content.setRootDirectoryProperty("Content");
try
{
// Names have no extension. Content/Textures/wall.xnb wins over a loose wall.png.
wall_ = content.Load<Texture2D>("Textures/wall");
hudFont_ = content.Load<SpriteFont>("Fonts/Hud"); // no default constructor
ship_ = content.Load<Model>("Models/ship");
boom_ = content.Load<SoundEffect>("Audio/explosion"); // move-only, never cached
theme_ = content.Load<Song>("Music/theme");
}
catch (const std::exception& error)
{
// ContentLoadException for most problems; std::bad_any_cast if T does not match
// the asset's root type. Catching std::exception covers both.
std::cerr << "content: " << error.what() << "\n";
Exit();
}
}
Notes on those five lines:
- No registration call.
Game’s constructor callsRegisterAllBuiltInXnbReaders(), so every built-in reader exists beforeLoadContent()runs. AContentManageryou create yourself without aGameregisters nothing: see below. Load<T>returns by value.SpriteFontandSoundEffecthave no default constructor, so hold them instd::optional.SoundEffectis move-only and is not cached: each call gives you a fresh, independently owned instance.- Catch
std::exception. Most failures areContentLoadException(astd::runtime_error), but asking for the wrongTraisesstd::bad_any_cast, because the.xnbtier converts the file’s root object toTwithstd::any_castand nothing wraps it. - The root directory is relative to the working directory. On a desktop build
Contentis looked up under the process’s current directory, so run from (or copyContent/next to) where you start the game.
private:
GraphicsDeviceManager graphics_;
Texture2D wall_;
std::optional<SpriteFont> hudFont_;
Model ship_;
std::optional<SoundEffect> boom_;
std::optional<Song> theme_;
};
The premultiplied-alpha convention deserves a line. XNA 4.0’s texture processor premultiplies alpha, and so do the .xnb files it produced. CNA’s own TextureProcessor defaults premultiplyAlpha to true to match, so an .xnb you build with cna-content and one built by XNA draw the same way under the same blend state.
The container CNA accepts
An .xnb starts with a 10-byte header, then (for compressed files) a 4-byte decompressed size, then the body. Everything below is what ParseXnbHeader and ContentManager::LoadXnbAsset<T> check.
| Field | Accepted | Otherwise |
|---|---|---|
| Magic | X N B | … is not a valid XNB file (bad magic bytes). |
| Platform byte | One of 16 values: w x m i a d X W n u p M r P g l (the set FNA accepts) | … has an unrecognized XNB platform identifier '…'. |
| Version | 4 or 5 | … has an invalid XNB version (n); only 4 and 5 are supported. |
| Flags | 0x80 = LZX (XNA 4.0, MonoGame), 0x40 = one raw LZ4 block (MonoGame). The graphics-profile bit 0x01 is not acted on by the reader. | Both compression bits set: … has an unrecognized compression flag combination. |
| Total length | At least 10 and no more than the real file size | … declares a totalLength (n) inconsistent with its actual file size (m). |
| File size | Up to INT32_MAX bytes | ContentManager: '…' is too large to load. |
The LZX decoder is a port of FNA’s; the LZ4 decoder is CNA’s own, with no external library. Both check the decompressed size the header declares, but differently: the LZ4 decoder bounds every literal run and match against it as it writes, while the LZX decoder grows its output freely and compares the total with the declared size only at the end (both first reject a declared size above the configured limit).
The read limits are a fixed set of sanity ceilings in XnbReadLimits. They are generous compared with real content and exist so that one corrupted count cannot make the reader allocate gigabytes before it notices.
| Limit | Default |
|---|---|
| Decompressed payload | 256 MiB |
| Single string | 1 MiB |
| Type readers in one file’s table | 4,096 |
| Shared resources in one file | 1,000,000 |
| Elements in one array, list or dictionary | 10,000,000 |
| Object nesting depth | 256 |
File size (maxFileSize) | 64 MiB — checked when an .xnb is read as a cna-content source; ContentManager::Load itself only refuses files above INT32_MAX |
Two failure modes are worth knowing by heart. A file whose reader table names a reader that is not registered fails with '…' references an unregistered .xnb content type reader '…'., and the whole file fails, because the table must resolve in full before any object is read. And a reader whose stored version differs from the registered reader’s fails with … uses reader '…' at an unsupported version (n).
The 61 built-in readers
Counted from the registration calls in modules/content/src (one name is registered from two files and counts once): 61 canonical names, or 60 when DecimalReader is compiled out. The count is not a promise about which content loads, only about which reader names resolve.
| Group | Readers | Count |
|---|---|---|
| Primitives | Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Char, String | 13 |
| Math | Vector2, Vector3, Vector4, Matrix, Quaternion, Color, Plane, Point, Rectangle, BoundingBox, BoundingSphere, BoundingFrustum, Ray | 13 |
| Other value types | TimeSpan, DateTime, Decimal (conditional) | 3 |
| Curve | Curve | 1 |
| Textures | Texture (the inert base), Texture2D, Texture3D, TextureCube | 4 |
| Font, audio, video | SpriteFont, SoundEffect, Song, Video | 4 |
| Stock effects | BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect | 5 |
| Compiled effect | Effect (Effect Framework bytecode) | 1 |
| Model family | VertexDeclaration, VertexBuffer, IndexBuffer, Model | 4 |
| Effect materials | EffectMaterial, ExternalReference | 2 |
| Closed generics | List<String>, List<Int32>, List<Char>, List<Rectangle>, List<Vector3>, List<Matrix>, Array<Vector3>, Dictionary<String,Int32>, Dictionary<String,String>, Dictionary<String,List<Vector3>>, Dictionary<String,Object> | 11 |
| Total | 61 |
Diagnose a folder before you run the game
A ContentManager can scan its root and tell you which reader names your .xnb files reference and whether each one is registered. GetXnbReaderUsageSummary() returns one row per distinct canonical reader name (readerName, fileCount, isRegistered), and GetContentManifest() returns one entry per logical asset (relativePath, hasXnb, hasCnj, nativeExtensions, xnbReaderNames). This program is a build-machine check you can run in CI; it registers the built-ins itself because it has no Game.
// diag.cpp -- which reader names does a content folder need, and which are missing?
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include "CNA/Internal/Xnb/XnbBuiltInReaders.hpp"
#include "Microsoft/Xna/Framework/Content/ContentManager.hpp"
#include "Microsoft/Xna/Framework/Content/ContentManifestEntry.hpp"
using Microsoft::Xna::Framework::Content::ContentManager;
using Microsoft::Xna::Framework::Content::ContentManifestReaderUsage;
int main()
{
// A standalone ContentManager (no Game) registers nothing on its own.
CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders();
// ... plus your own readers, if you have any (tutorial 148).
ContentManager content; // root directory "Content"
std::vector<ContentManifestReaderUsage> usage = content.GetXnbReaderUsageSummary();
std::sort(usage.begin(), usage.end(),
[](const auto& a, const auto& b) { return a.readerName < b.readerName; });
int missing = 0;
for (const ContentManifestReaderUsage& row : usage)
{
std::cout << (row.isRegistered ? "ok " : "MISSING ") << row.fileCount
<< " file(s) " << row.readerName << "\n";
if (!row.isRegistered) ++missing;
}
// Per file: which assets have an .xnb, and which reader names does it reference?
for (const auto& entry : content.GetContentManifest())
{
if (entry.hasXnb && entry.xnbReaderNames.empty())
std::cout << entry.relativePath << ".xnb: no reader inventory (compressed or malformed)\n";
}
return missing == 0 ? 0 : 1;
}
The scan is a point-in-time snapshot: RefreshContentManifest() rebuilds it, and nothing watches the folder. Two limits matter:
- Compressed files have no inventory. The scanner only reads the reader table of uncompressed files, and it swallows any error while doing so, so an LZX- or LZ4-compressed or malformed
.xnbshows an emptyxnbReaderNames. Theifin the program above prints exactly those. - The inventory is names, not payloads. A registered reader can still refuse the content.
For a compressed file, the CNA repository has an independent checker, tools/xnb/xnb_conformance.py: python3 tools/xnb/xnb_conformance.py Content/Fonts/Hud.xnb prints the platform byte, version, compression and root reader, and --json gives the full report. It contains its own LZX and LZ4 decoders and shares no code with CNA, but it only knows the built-in payloads, so it stops with “no decoder for root reader” on a custom type. It was not run for this page.
Effects
The stock-effect readers restore native CNA effect state and do not need the compiled-effects capability. The general Effect reader is different: it hands the stored Direct3D 9 Effect Framework bytecode to the renderer, which succeeds only when that renderer reports GraphicsCapability::CompiledEffects. A default configure of CNA reports it on 1 of the 25 renderer identities (FNA3D); nine more (EasyGL, Vulkan, WebGPU, Software, DirectX 9/11/12, SDL_GPU, OpenGL 4) have opt-in CMake options that default to off. Ask the device before you load:
if (getGraphicsDeviceProperty().SupportsCapability(CNA::GraphicsCapability::CompiledEffects))
{
water_ = getContentProperty().Load<std::shared_ptr<Effect>>("Effects/water");
}
else
{
// Fall back to a stock effect or a .cnj Effect descriptor (see the Shader Effects page).
}
(CNA::GraphicsCapability comes from CNA/GraphicsCapability.hpp; the std::shared_ptr<Effect> is the type the Effect reader produces.)
When the effect cannot be created, the reader reports '…': EffectReader could not create the compiled effect as a ContentLoadException, with the underlying reason (an empty or oversized blob, not a Direct3D 9 Effect Framework binary, an MGFX container, or a missing capability) folded into the message.
Write .xnb from CNA
The build tool is cna-content (the cna_content_tool target of the CNA project). It reads source content (images, WAV, glTF, .spritefont, .fbx, effect bytecode and more; see the Content Pipeline route table) and by default writes CNA’s own .cnb; --format xnb switches the writer and nothing else, so every source route reaches both containers. A directory build preserves relative names. For a ContentSource/Textures/wall.png:
cna-content build ContentSource -o Content --format xnb \
--xnb-platform windows --xnb-version 5 --xnb-profile reach --xnb-compress lzx --explain
The tool prints one line per asset, [BUILD] Textures/wall -> …/Content/Textures/wall.xnb (1 output(s), … bytes; … -> … -> …), or [SKIP] … when the content-hashed manifest says nothing changed, and ends with Built: n Skipped: n Failed: n. A single-file build needs an output path whose extension matches the format (-o Content/wall.xnb). Then Load<Texture2D>("Textures/wall") in the game finds Content/Textures/wall.xnb like any other .xnb.
| Option | Values | Default and rules |
|---|---|---|
--format | cnb, xnb | cnb. |
--xnb-platform | windows (w), windowsphone (m), xbox360 (x), desktopgl (d), linux (l), ios (i), android (a), windowsgl (g) | windows. Only the first three are XNA 4.0 targets; the rest are extended-ecosystem identifiers XNA 4.0 never produced or consumed. xbox360 is refused unless you add --xnb-allow-unverified-xbox. |
--xnb-version | 5, 4 | 5, the XNA 4.0-era container. Version 4 is legacy and can only be written uncompressed. |
--xnb-profile | reach, hidef | reach: a Reach asset loads in Reach and HiDef games, not the other way round. |
--xnb-compress | none, lzx, lz4 | none. lzx is what XNA 4.0 produced. lz4 (a MonoGame-era extension) is refused on the three XNA 4.0 platforms, so it needs --xnb-platform desktopgl or another extended value. |
--xnb-reader-names | xna40, portable | xna40 writes the assembly-qualified spelling XNA wrote. portable drops the qualifiers: CNA loads it, but a genuine XNA 4.0 runtime is not known to accept it. |
From CMake, the same thing is cna_add_content(TARGET GameContent SOURCE_DIR ContentSource OUTPUT_DIR Content FORMAT xnb XNB_PLATFORM windows XNB_PROFILE reach XNB_COMPRESS lzx); the XNB_* options require FORMAT xnb. It creates a custom target that runs cna-content whenever it is built, so add add_dependencies(MyGame GameContent). An existing XNA .contentproj builds too (cna-content build MyGame.contentproj -o Content) and then carries its own platform, profile and compression.
What the writers cover: Texture2D, Texture3D, TextureCube, SpriteFont, SoundEffect, Song, Video, Model, and a compiled Effect (from .fxb bytecode, or from .fx source only when an external fxc-compatible compiler is configured). There is no built-in HLSL compiler and no XMA encoder. A glTF model built to .xnb is written with named warnings: skeleton, animation clips, morph targets and lights are dropped, and PBR materials are downgraded to the corresponding stock effect. See Content Pipeline for the full route table and Tutorial 145 for an end-to-end build.
Convert .xnb to .cnb
An .xnb is also an input to cna-content. The XnbImporter decodes a supported root through the same code the runtime readers use and hands it to the normal processors, so a folder of legacy assets can become CNB without going back to the sources:
cna-content build LegacyContent -o Content/Native
The supported roots are Texture2D, Texture3D, TextureCube, SpriteFont, SoundEffect, Curve, Song, Video and Model; anything else is refused. Textures come out as CNB’s Rgba8 (DXT sources are decoded), and a Model becomes CNB schema 1 when every XNB semantic fits exactly and schema 2 otherwise. A Song or Video .xnb also needs the media file it points to, resolved relative to the .xnb; if the exact path is missing the importer tries the same stem with .ogg, .oga or .qoa (Song) or .ogv or .ogg (Video). The importer does not premultiply texture alpha a second time, because an .xnb texture has already been through XNA’s processor. CNB is CNA-only, so keep the originals if you also ship the game elsewhere.
What CNA reports, and what this page can vouch for
“It loads XNA content” and “it writes XNA content” are two claims with different evidence.
Reading files other tools produced
- Verified by reading the source: the header checks, the LZX and LZ4 branches, the read limits, the 61 registered names, the ladder order, and every message quoted above.
- Fixtures in the repository: CNA commits externally produced files with manifests of the values a correct read must give. One is unchanged Microsoft XNA Game Studio 4.0 output (the Platformer
Hud.xnbSpriteFont, and aList<string>asset built by the XNA 4.0BuildContenttask). Several are from MonoGame’s own test assets built withmgcb(uncompressed and LZX; platform byteswandd; texture, fonts, a model, audio in several PCM and ADPCM formats, a song). The LZ4 fixture is not straight from MonoGame: CNA’s manifest says its body is a real MonoGame file re-compressed with upstreamliblz4and wrapped the way MonoGame’s writer wraps it. - Tests exist for these (reader, container and LZX differential tests reference the fixtures). The tests were read, not run.
Writing files XNA is meant to read
- Byte identity, per CNA’s tests: CNA’s writer tests state that
WriteXnbAssetreproduces the XNA 4.0List<string>fixture byte for byte, and MonoGame’s smallest Texture2D and a SoundEffect byte for byte. That is a narrow claim about three small assets. - A real XNA 4.0 runtime, per CNA’s README:
tests/interop/xna40/README.mdreports that on 2026-09-06 a harness loaded CNA-written files through XNA 4.0.0.0’sContentManager: six of six uncompressed fixtures (a texture with mips, a PCM16 sound, a two-glyph SpriteFont, a curve, a list of strings, a one-triangle model withBasicEffect) matched their expected values, and after the LZX encoder was fixed to end each payload with the five-byte trailer XNA requires, the six LZX versions loaded too, plus a nine-frame 256×256 texture built from a PNG with--xnb-compress lzx. It stresses that this ran on Debian under Wine with Direct3D 9 through DXVK, not on Windows, and covers only those roots and only values XNA’s public API exposes. - The documents disagree. CNA’s
docs/xnb-interoperability.md(last changed 2026-09-03) still says nothing carries the “loaded by a genuine XNA 4.0 runtime” label and that the writer “has never been run against a real XNA runtime”. The README (2026-09-06) and thecna-contenthelp text are later and say it has. No machine-readable result log was found in the tree and the harness was not run for this page, so the runtime result rests on CNA’s report alone. - Not covered: loading CNA-written files in MonoGame or FNA (nothing in CNA reports it beyond the MonoGame byte-identity fixtures above), Xbox 360 payloads beyond the sound format block, and Windows Phone output.
The safe reading. Reading real XNA and MonoGame files is exercised against genuine external files. Writing files for a real XNA runtime is reported by CNA to work for six small root types and one larger texture on a Wine host. Treat anything beyond that as untested until you test it with your own content.
Troubleshooting
| What you see | Cause and fix |
|---|---|
… references an unregistered .xnb content type reader '…' | The file needs a reader that is not registered. Run the diagnostic program. Built-ins are missing only in a standalone ContentManager (call RegisterAllBuiltInXnbReaders()); a game type needs a reader you register. Compressed files are invisible to the scan: check them with the Python script. |
ContentManager::Load<T>(): No reader registered for type, asset '…' | No .xnb or .cnb was found under that name and T has no loose-file reader. Check the logical name, the root directory and the working directory. |
std::bad_any_cast | The .xnb loaded but its root is not a T. Match the type: Texture2D for a texture, std::shared_ptr<Effect> for an effect, std::shared_ptr<T> for a reference-shaped custom root. |
… uses MonoGame's Lz4 compression, which CNA does not yet support | Raised only when an asset is reached as an external reference from another asset (for example an effect material’s texture) and is LZ4-compressed. A direct Load<T>() of an LZ4 file works. Rebuild the referenced asset uncompressed or with LZX. |
… EffectReader could not create the compiled effect | The renderer lacks CompiledEffects, or the blob is MGFX or not Effect Framework bytecode. See Effects. |
SoundEffect fails with unsupported SoundEffect wave format or … channel count | XMA2, other unknown codecs and more than two channels are refused. Re-encode as PCM or ADPCM; CNA’s own .wav route writes PCM. |
| Song load fails | The media file the .xnb points to is missing. Put an .ogg, .oga or .qoa with the same stem beside it. |
Texture2DReader: WxH exceeds this device's maximum texture dimension of N. | The asset is larger than the active renderer allows. |
cna-content refuses --xnb-compress lz4 | LZ4 is not combinable with the three XNA 4.0 platforms. Use lzx or none, or add --xnb-platform desktopgl. |
Next steps
- Tutorial 148: Custom Content Types — register readers for the game types in your
.xnbfiles. - Tutorial 145: Build Content with the Pipeline — author with
cna-contentandcna_add_content(). - Tutorial 146: Write and Load CNB Files — CNA’s native container.
- Tutorial 46: Custom Content Readers — the loose-file route.
- Reference: XNB Loading and Interoperability, Content Pipeline, CNB Format, ContentManager.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- The XNB container and the ContentReader object graph — Byte-level and session-level rules for reading .xnb files in CNA: header, compression, the type-reader table, one-based object indices, shared resources, limits and custom-reader rules.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-057: RegisterAllBuiltInXnbReaders() documentation still calls the general EffectReader a known-unsupported placeholder — XnbBuiltInReaders.hpp says the function registers "the known-unsupported placeholder (the general EffectReader, XNB-32A)", but it registers the implemented EffectReader and the placeholder hook registers nothing.