XNB Content Pipeline

CNA::Internal::Xnb — reading Microsoft XNA Content Pipeline .xnb binaries

This is new. CNA can now read .xnb files produced by the XNA Content Pipeline. Roughly 4,000 lines of reader code under src/CNA/Internal/Xnb/ and include/CNA/Internal/Xnb/ are wired directly into ContentManager, with 49 registered type readers, a real LZX decompressor, and the full FNA ContentReader surface. Earlier versions of this site stated that CNA had no XNB support and no plans for it; that is no longer true. The gaps that do remain are listed honestly in Known gaps — the largest is that there is no ReflectiveReader, so custom/user-defined types still cannot be loaded from XNB.

Overview

The XNA Content Pipeline compiled every asset — textures, fonts, sounds, models, effects — into a binary .xnb container. An .xnb file carries a header, an optional compressed body, a table of the type readers needed to deserialise its contents, and then the object graph itself, written by the reader for the asset's root type.

CNA implements that format in the CNA::Internal::Xnb namespace. The reader is not a shim or a converter: it decompresses the body, walks the type-reader table, and deserialises the object graph into live CNA objects — Texture2D, SpriteFont, SoundEffect, Model and so on — using the same ContentReader API surface FNA exposes, so reader code written against FNA's shape ports across directly.

The practical consequence: content already built by the original Microsoft tooling (or by MonoGame's mgcb) can be loaded as-is, without an offline conversion step, for every type in the registered reader set. Content that falls outside that set still has to go through CNA's loose-file path — raw images and audio, JSON descriptors, or the offline gltf_to_cnj converter for models.

CapabilityDetail
Reader size ~4,000 LOC across src/CNA/Internal/Xnb/ and include/CNA/Internal/Xnb/
Registered type readers 49
Decompression A real 31 KB LZX decompressor — not a stub, not a shell-out to an external tool
Reader API The full FNA ContentReader surface
Shared resources Two-pass resolution, so assets that reference each other deserialise correctly
Robustness Fuzz-hardened against malformed and adversarial files
ContentManager integration Prefers a .xnb when one is present; falls back to loose files otherwise

Registering the built-in readers

The reader registry is empty by default. You must call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() exactly once at startup, before the first Load<T>() that should resolve an .xnb. Without it, no type reader is known and XNB loading will not work. This is a deliberate design decision documented in the header — not an oversight and not a bug: registration is explicit so that an application which never touches XNB does not pay for 49 readers it will not use.

#include <CNA/Internal/Xnb/XnbReaderRegistry.hpp>

// Call once at startup — e.g. at the top of main(), or in your
// Game subclass's Initialize(), before any content is loaded.
int main() {
    CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders();

    MyGame game;
    game.Run();
    return 0;
}

Registration is global and idempotent in intent: do it once for the lifetime of the process. After that, ContentManager needs no further configuration — the XNB path is transparent from the caller's point of view.

How ContentManager resolves an asset

ContentManager does not require you to choose between pipelines. For a given asset name it prefers a .xnb when one exists, and otherwise falls back to the loose-file path (raw images and audio, or a JSON descriptor) documented on the ContentManager page.

// Content/textures/player.xnb exists → loaded through the XNB reader.
// Content/textures/player.png exists but no .xnb → loaded via SDL3_image.
// Either way, the call site is identical:
auto& tex = Content.Load<Texture2D>("textures/player");

This makes incremental migration practical in both directions. You can drop a folder of pipeline-built .xnb assets into an existing loose-file project and they will take precedence, or delete an .xnb and let the loose file take over, without touching any calling code.

The 49 registered type readers

RegisterAllBuiltInXnbReaders() installs 49 readers. They cover the asset categories a real XNA game actually ships:

CategoryCoveredStatus
Primitives All the primitive value types the pipeline emits Implemented
Math types All of them — vectors, matrices, quaternions, rectangles, bounding volumes, colour Implemented
Textures Texture2D, Texture3D, TextureCube Implemented
Fonts SpriteFont Implemented
Audio SoundEffect, Song Implemented
Stock effects The five stock effects the content pipeline can emit Implemented
Model family The full model reader family (meshes, mesh parts, bones, buffers) Implemented
Compiled Effect bytecode EffectReader is registered as an always-throwing placeholder Not supported

Note the distinction in the last two effect rows. The stock effects (BasicEffect and friends) load because CNA implements them natively in C++ and the reader only has to restore their parameter state. A compiled .fx shader is a different matter entirely: CNA has no bytecode translator, so its reader is a registered placeholder that throws. See Effects System for the full picture.

Container format support

Three parts of the container format are worth calling out because they are where naive XNB readers usually stop short:

  • LZX decompression. XNA compressed most shipped .xnb bodies with LZX. CNA carries a genuine 31 KB LZX decompressor, so compressed assets from real games load directly rather than requiring a decompression pass through external tooling.
  • Two-pass shared-resource resolution. XNB object graphs can contain shared resources referenced from multiple points — a material referenced by several mesh parts, for instance. CNA reads the graph in two passes so those references resolve to the same object instead of being duplicated or left dangling.
  • Fuzz hardening. The reader has been fuzzed against malformed and adversarial input. Content files are attacker-controlled data in any game that loads mods or user downloads, and a binary deserialiser is exactly the wrong place to trust your input.

Known gaps

These are real limitations, not roadmap language. If your content depends on any of them, the XNB path will not load it today.

GapConsequence
No ReflectiveReader The biggest one. XNA used reflection to deserialise arbitrary user-defined types marked up for the content pipeline. C++ has no equivalent, so no custom or user-defined type can be loaded from an .xnb. Only the 49 built-in readers apply.
No EnumReader Enum-typed fields in an XNB graph cannot be read.
No ExternalReferenceReader Assets that reference another .xnb by external reference cannot be resolved.
No VideoReader Video assets cannot be loaded from XNB. See Video Playback for the FFmpeg path and its platform limits.
EffectReader is a placeholder It is registered, but throws unconditionally. Compiled .fx bytecode is unsupported throughout CNA, not just here.
Collection readers implemented but unregistered Array, List, Dictionary and Nullable readers exist in the codebase but are not registered by default. Without reflection each closed generic instantiation has to be hand-registered, so there is no way to register them all up front.

Two adjacent Content namespace caveats belong here as well: ResourceContentManager is a pure stub — the only one of its kind in the repository — and ContentManager::Unload() clears its internal maps without disposing the assets they held.

Testing

The XNB reader is covered by 24 test files containing 134 test cases, run against binary MonoGame fixtures checked in under tests/assets/xnb/. Testing against real pipeline output rather than hand-authored byte arrays is the point: it is what makes the container-format handling credible rather than merely self-consistent.

Those fixtures sit inside CNA's broader Content test area. For how the project verifies itself overall — including the pixel-exact XNA oracle corpus and differential testing against a real FNA build — see Verification & Known Issues.