Tutorial 147: Load XNB from XNA and MonoGame Content

CNA Tutorials  ·  CNA snapshot 009d40f5

ℹ

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.

  1. What loads and what does not, as a table.
  2. Drop the files in and load them from a Game.
  3. The container CNA accepts: platform bytes, versions, compression, limits.
  4. Diagnose a folder of files before you run the game.
  5. Write .xnb from CNA and load it back.
  6. Convert .xnb to .cnb with the same tool.
  7. What CNA reports, and what this page can and cannot vouch for.

What loads and what does not

Content in the .xnbLoads?Notes (from the reader sources unless marked)
Texture2D, Texture3D, TextureCubeYesTexture2D 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.
SpriteFontYesA 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.
SoundEffectYes (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=…).
SongReference onlyThe .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.
VideoMetadataThe reader is registered on every build. It reads the metadata; playback needs the optional FFmpeg backend and otherwise throws NotSupportedException.
Model, VertexBuffer, IndexBuffer, VertexDeclarationYesWith 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-qualifiedRead 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, DecimalYesDecimal 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.
CollectionsClosed set11 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 typesIf you register a readerThere is no reflection. Register a ContentTypeReader<T> or a ReflectiveTypeReaderBuilder<T>.
Xbox 360 files (platform byte x)Not verified hereThe 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 calls RegisterAllBuiltInXnbReaders(), so every built-in reader exists before LoadContent() runs. A ContentManager you create yourself without a Game registers nothing: see below.
  • Load<T> returns by value. SpriteFont and SoundEffect have no default constructor, so hold them in std::optional. SoundEffect is move-only and is not cached: each call gives you a fresh, independently owned instance.
  • Catch std::exception. Most failures are ContentLoadException (a std::runtime_error), but asking for the wrong T raises std::bad_any_cast, because the .xnb tier converts the file’s root object to T with std::any_cast and nothing wraps it.
  • The root directory is relative to the working directory. On a desktop build Content is looked up under the process’s current directory, so run from (or copy Content/ 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.

FieldAcceptedOtherwise
MagicX N B… is not a valid XNB file (bad magic bytes).
Platform byteOne 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 '…'.
Version4 or 5… has an invalid XNB version (n); only 4 and 5 are supported.
Flags0x80 = 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 lengthAt least 10 and no more than the real file size… declares a totalLength (n) inconsistent with its actual file size (m).
File sizeUp to INT32_MAX bytesContentManager: '…' 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.

LimitDefault
Decompressed payload256 MiB
Single string1 MiB
Type readers in one file’s table4,096
Shared resources in one file1,000,000
Elements in one array, list or dictionary10,000,000
Object nesting depth256
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.

GroupReadersCount
PrimitivesBoolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Char, String13
MathVector2, Vector3, Vector4, Matrix, Quaternion, Color, Plane, Point, Rectangle, BoundingBox, BoundingSphere, BoundingFrustum, Ray13
Other value typesTimeSpan, DateTime, Decimal (conditional)3
CurveCurve1
TexturesTexture (the inert base), Texture2D, Texture3D, TextureCube4
Font, audio, videoSpriteFont, SoundEffect, Song, Video4
Stock effectsBasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect5
Compiled effectEffect (Effect Framework bytecode)1
Model familyVertexDeclaration, VertexBuffer, IndexBuffer, Model4
Effect materialsEffectMaterial, ExternalReference2
Closed genericsList<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
Total61

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 .xnb shows an empty xnbReaderNames. The if in 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.

OptionValuesDefault and rules
--formatcnb, xnbcnb.
--xnb-platformwindows (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-version5, 45, the XNA 4.0-era container. Version 4 is legacy and can only be written uncompressed.
--xnb-profilereach, hidefreach: a Reach asset loads in Reach and HiDef games, not the other way round.
--xnb-compressnone, lzx, lz4none. 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-namesxna40, portablexna40 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.xnb SpriteFont, and a List<string> asset built by the XNA 4.0 BuildContent task). Several are from MonoGame’s own test assets built with mgcb (uncompressed and LZX; platform bytes w and d; 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 upstream liblz4 and 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 WriteXnbAsset reproduces the XNA 4.0 List<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.md reports that on 2026-09-06 a harness loaded CNA-written files through XNA 4.0.0.0’s ContentManager: 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 with BasicEffect) 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 the cna-content help 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 seeCause 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_castThe .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 supportRaised 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 effectThe renderer lacks CompiledEffects, or the blob is MGFX or not Effect Framework bytecode. See Effects.
SoundEffect fails with unsupported SoundEffect wave format or … channel countXMA2, 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 failsThe 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 lz4LZ4 is not combinable with the three XNA 4.0 platforms. Use lzx or none, or add --xnb-platform desktopgl.

Next steps