XNB Loading & Interoperability
This snapshot includes a real read-side XNB loader. ContentManager loads .xnb through a real loader: RegisterAllBuiltInXnbReaders() installs 61 built-in readers (60 on toolchains without a native 128-bit integer, which drops DecimalReader), including a real compiled-effect EffectReader, and both LZX and LZ4 compressed files are decompressed. New since alpha.1: CNA also writes XNB. It no longer only reads the format. The build-time cna-content Content Pipeline can write .xnb (--format xnb) as well as CNA’s own .cnb. CNA still does not discover arbitrary game types through .NET reflection; games register explicit C++ readers, or declare one field by field with ReflectiveTypeReaderBuilder<T>.
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 needs a custom reader, or has to go through CNA's other paths — raw images and audio, JSON descriptors, the CNB container, or the offline gltf_to_cnj converter for models. The opposite direction is new: CNA can produce .xnb from source files.
| Capability | Detail |
|---|---|
| Reader size | Module-owned implementation under modules/content, including parser, decompression and built-in readers |
| Registered type readers | 61 canonical reader names; 60 when the toolchain has no native 128-bit integer (DecimalReader is conditional). VideoReader is no longer FFmpeg-conditional: it reads the video’s metadata everywhere, and only playback needs a backend |
| Decompression | A real 31 KB LZX decompressor (a port of FNA’s) and a bounds-checked in-tree LZ4 decoder for MonoGame’s LZ4-flagged files — not stubs, 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 | Hard read limits (decompressed size, string, reader-table, shared-resource, collection and nesting caps) plus fuzz-style tests over the container, the LZX decoder and the writer’s inputs |
| Writing | cna-content build ... --format xnb: XNA 4.0 container version 5 (or legacy 4, uncompressed only), Reach or HiDef profile, no compression, real LZX, or LZ4 for non-XNA targets; deterministic output |
ContentManager integration |
Prefers a .xnb when one is present; then a .cnb; falls back to loose files otherwise |
Registering the built-in readers
The reader registry is empty by default outside a Game. A Game registers the built-in readers for you: at alpha.1 you had to call RegisterAllBuiltInXnbReaders() yourself before the first load. In this snapshot Game’s constructor makes that call, so every Game subclass can load built-in .xnb types from LoadContent() with no setup. A ContentManager used without a Game — a content tool, a unit test, a C API host — is still deliberately neutral: its registry starts empty and you must call the function once.
#include "CNA/Internal/Xnb/XnbBuiltInReaders.hpp"
#include "Microsoft/Xna/Framework/Content/ContentManager.hpp"
using Microsoft::Xna::Framework::Content::ContentManager;
// A tool or test with no Game: register once, then use a ContentManager directly.
int main() {
CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders(); // idempotent
ContentManager content(nullptr, "Content"); // no Game, no GraphicsDevice
// Types that need no GPU load fine: primitives, math, collections, ...
auto names = content.Load<std::vector<std::string>>("data/names");
return static_cast<int>(names.size());
}
The header is CNA/Internal/Xnb/XnbBuiltInReaders.hpp (it still lives under Internal, which is a known ergonomics gap). Registration is global and idempotent: a repeat registration of the same canonical name is silently ignored, so calling it twice, or calling it in a Game that has already done so, is harmless. Assets that create GPU resources (Texture2D, Model, …) additionally need a graphics device from setGraphicsDevice() or an IGraphicsDeviceService — which a Game provides.
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, then a .cnb, and otherwise falls back to the loose-file path (raw images and audio, or a JSON descriptor) documented on the ContentManager page. The .xnb is dispatched from the file’s own type-reader table — no reader for the requested T has to be registered on the manager.
// Content/textures/player.xnb exists -> loaded through the XNB reader.
// Content/textures/player.cnb exists (no .xnb) -> loaded through the CNB loader.
// Content/textures/player.png exists but neither -> decoded from the image file.
// Either way, the call site is identical, and the result is returned by value:
Texture2D tex = getContentProperty().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. On platforms with packaged assets (Android) the .xnb tier reads through the platform’s asset route; the .cnb tiers use the ordinary filesystem, and whether a .cnb loads from an APK has not been verified.
The 61 built-in type readers
RegisterAllBuiltInXnbReaders() installs 61 readers. They cover the asset categories a real XNA game actually ships:
| Category | Covered | Count | Status |
|---|---|---|---|
| Primitives | All the primitive value types the pipeline emits (Boolean, Byte, SByte, Int16/32/64, UInt16/32/64, Single, Double, Char, String) | 13 | Implemented |
| Math types | All of them — Vector2/3/4, Matrix, Quaternion, Color, Plane, Point, Rectangle, BoundingBox, BoundingSphere, BoundingFrustum, Ray | 13 | Implemented |
| Other value types | TimeSpan, DateTime and (with a native 128-bit integer) Decimal |
3 | Implemented |
| Curve | Curve |
1 | Implemented |
| Textures | Texture2D, Texture3D, TextureCube, and the legacy TextureReader alias |
4 | Implemented |
| Fonts, audio, video | SpriteFont, SoundEffect, Song, Video |
4 | Implemented |
| Stock effects | The five stock effects the content pipeline can emit: Basic, AlphaTest, DualTexture, EnvironmentMap, Skinned | 5 | Implemented |
Compiled Effect bytecode |
EffectReader reads XNA/FNA Effect Framework bytecode |
1 | Implemented; renderer capability required |
| Model family | The full model reader family: VertexDeclaration, VertexBuffer, IndexBuffer and Model (meshes, mesh parts, bones) |
4 | Implemented |
| Effect materials | EffectMaterialReader and ExternalReferenceReader (typed external references) |
2 | Implemented |
| Closed collection readers | Eleven pre-registered closed generics: List<String>, List<Int32>, List<Char>, List<Rectangle>, List<Vector3>, List<Matrix>, Vector3[], Dictionary<String,Int32>, Dictionary<String,String>, Dictionary<String,List<Vector3>> and Dictionary<String,Object> |
11 | Implemented |
Counts: 13 + 13 + 3 + 1 + 4 + 4 + 5 + 1 + 4 + 2 + 11 = 61 (60 without Decimal). Eleven of these readers are new since alpha.1’s 50: the legacy TextureReader alias, EffectMaterialReader, ExternalReferenceReader, and eight closed collection readers.
Stock-effect readers restore native CNA effect state. The general EffectReader instead reads the already-compiled Direct3D 9 Effect Framework payload and builds a reflected runtime effect. That succeeds only when the active renderer reports GraphicsCapability::CompiledEffects; it is not a universal renderer feature (FNA3D always provides it; eight other renderer families offer it through opt-in build options that default to off). See Effects System.
Container format support
Four parts of the container format are worth calling out because they are where naive XNB readers usually stop short:
- LZX and LZ4 decompression. XNA compressed most shipped
.xnbbodies 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. The header’s LZX flag (0x80) and the LZ4 flag MonoGame uses (0x40, a raw single block decoded by an in-tree, bounds-checked decoder) are both honoured for an asset you load directly; a header with both bits set is refused, and an LZ4 file reached as an untyped external reference from another.xnb(thestd::any-returningReadExternalReference()path used byExternalReferenceReader) is refused at this snapshot; the typed external references (Texture2D,TextureCubeand compiled effects) load throughContentManager::Load<T>, whose loader does decode LZ4. - The header. Magic
XNB, a platform byte (sixteen accepted values, including the deprecated ones FNA still accepts), container version 4 or 5, the flags byte and the file’s declared total length, which is cross-checked against the bytes actually read before any pointer arithmetic uses it. - 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.
- Hard limits and fuzz hardening. 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. By default the reader refuses a file of 2 GiB or more, a decompressed payload over 256 MiB, a string over 1 MiB, more than 4,096 type readers, more than 1,000,000 shared resources, a collection of more than 10,000,000 elements, or an object graph nested more than 256 deep; and it is exercised by fuzz-style tests over the container, the LZX decoder and the writer’s inputs.
Writing XNB from CNA
At alpha.1 this page said CNA does not author XNB files. That is no longer true. The build-time cna-content tool (see Content Pipeline) runs source files through importer → processor → writer, and the writer can be either CNA’s native .cnb or an XNA-compatible .xnb. Importers and processors are identical either way; only the writer differs, so every source route can reach both containers except where a container has no representation (for example, a compiled Effect has no CNB schema, but does have an XNB writer).
# Build a content folder into XNA-compatible .xnb files (XNA 4.0 container, LZX-compressed)
cna-content build ContentSource -o Content --format xnb \
--xnb-platform windows --xnb-version 5 --xnb-profile reach --xnb-compress lzx
| Option | Values (default in bold) | Notes |
|---|---|---|
--xnb-platform | windows, windowsphone, xbox360 (the XNA 4.0 targets); desktopgl, linux, ios, android, windowsgl (extended identifiers Microsoft XNA 4.0 never produced or consumed) | xbox360 is refused unless --xnb-allow-unverified-xbox, which produces candidate files that CNA itself labels unverified. |
--xnb-version | 5 (XNA 4.0 era), 4 (legacy) | Version 4 output is offered uncompressed only. |
--xnb-profile | reach, hidef | A Reach asset loads under both profiles. |
--xnb-compress | none, lzx, lz4 | lzx is what XNA 4.0 itself produced and the only compressed form an XNA 4.0 runtime loads. lz4 is a later-ecosystem extension and is refused on an XNA 4.0 target platform. Compression is deterministic: the same payload always produces the same bytes. |
--xnb-reader-names | xna40, portable | The spelling style used for type-reader names in the file’s reader table. |
The XNB writers cover the same families as the readers: textures (2D, 3D, cube), SpriteFont, SoundEffect, Song, Video, models together with their vertex and index buffers and the five stock effects, compiled Effect bytecode, effect materials and external references, plus the primitive, math and Curve value types and a closed set of collections. The LZX compressor is a real encoder (one verbatim block per 32 KiB frame with Huffman-coded literals and matches), not a pass-through. Two build-time details matter to porters:
- glTF to XNB drops data, with named warnings. An XNB
Modelbuilt from glTF has no place for the skeleton, animation clips, lights or morph targets, so they are dropped with a warning; PBR materials are downgraded to the corresponding stock effect; generated child assets and external-effect parts are refused. The.cnbroute keeps skeleton, animation, morph targets and lights. Choose.cnbfor animated glTF. - The XNA-shaped API is here too.
cna-contentalso builds a real XNA.contentproj, and CNA carries an implementation of theMicrosoft::Xna::Framework::Content::Pipelinenamespace (importers, processors,ContentCompiler,IntermediateSerializer); the Content Pipeline page states exactly what that claim covers.
An .xnb is also a valid source for cna-content: a supported built-in root type is transcoded to a native .cnb (an .xnb is not written back as .xnb from this route). For a step-by-step walk-through see Tutorial 147.
Custom XNB readers
XNA deserialised user-defined types by .NET reflection. CNA has no reflection, so every reader is registered explicitly, keyed by the canonical reader name that appears in the .xnb file’s reader table. Three mechanisms are available:
| Mechanism | Use it for |
|---|---|
ContentTypeReader<T> plus ContentTypeReaderManager::AddTypeCreator(canonicalName, factory) | Full control: implement Read(ContentReader&, existingInstance) yourself. Registration is process-wide and the first registration of a name wins; RemoveTypeCreatorEXT() withdraws one. |
ReflectiveTypeReaderBuilder<T>(name).Field(&T::a).Field(&T::b)….Register() | Types that XNA serialised field by field (FNA’s ReflectiveReader<T>). You declare the field list once; .RegisterShared() for reference types, .SharedResourceField(), .EnumField(), .Base(), .Custom() and .RegisterAbstract() cover the other shapes. This is a declared-field model, not run-time reflection. |
EnumTypeReader<TEnum> | Enum-typed fields. Register() on a builder registers the enum readers its EnumField() calls need. |
None of these is part of the 61 built-ins; they are yours to register before the first load that needs them. Tutorial 148 takes one game type through all three routes, including the CNB equivalent.
Diagnosing XNB problems
A failed load throws ContentLoadException, with the underlying cause attached when there is one. The common causes are a bad magic or platform byte, a version other than 4 or 5, a declared length that disagrees with the file, a decompression failure, or a reader-table entry with no registered reader. Before you load anything, ask the manager which reader names your content root needs:
for (const auto& usage : getContentProperty().GetXnbReaderUsageSummary()) {
if (!usage.isRegistered) {
// usage.readerName is the canonical name, usage.fileCount how many .xnb files need it
std::printf("no reader for %s (%d file(s))\n",
usage.readerName.c_str(), usage.fileCount);
}
}
The manifest behind that call (GetContentManifest(), rebuilt with RefreshContentManifest()) is a point-in-time scan of the content root: one entry per logical asset name, recording whether an .xnb or .cnj exists, any other extensions found, and the reader names read from an .xnb’s type-reader table. The table is read only from uncompressed, well-formed files, so a compressed file appears with an empty reader list rather than as a scan failure.
What CNA reports about XNA 4.0 compatibility
Whether CNA’s output loads in a genuine XNA runtime is a claim that this site cannot check for itself, so it is stated as CNA reports it, with dates and hosts:
- CNA’s own interoperability notes report a harness that loads CNA-written files through a genuine Microsoft XNA 4.0
ContentManageron a Debian host under Wine with DXVK: six of six uncompressed fixtures (aTexture2Dwith mip levels, aSoundEffect, aSpriteFont, aCurve, aList<string>and aModelwith aBasicEffect) and, after CNA added a five-byte trailer that genuine XNA requires after the final LZX block, the same six LZX-compressed, dated 6 September 2026. CNA’s older interoperability document (3 September 2026) still says nothing had been run against a real XNA runtime; the later dated harness notes and the tool’s own help text say it was, so treat the older text as stale. No committed machine-readable result log was found, and the run is not part of any CI workflow. - CNA reports byte-identical output to genuine XNA 4.0 for a
List<string>and to MonoGame for aTexture2Dand aSoundEffect, pinned by golden tests in its own suite. - An independent conformance parser,
tools/xnb/xnb_conformance.py, shares no code with CNA’s reader, andcna_tool_xnb_interop_fixturesregenerates the committed CNA-written corpus.
None of this measures MonoGame or FNA loading CNA’s output beyond what CNA reports, and none of it says every asset type has been checked: it is evidence for those six fixture roots, not a general guarantee. Xbox 360 output is refused by default for that reason.
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.
| Gap | Consequence |
|---|---|
| No automatic reader discovery | XNA used reflection to discover arbitrary user-defined readers. CNA cannot do that automatically. A game registers ContentTypeReader<T> factories through ContentTypeReaderManager::AddTypeCreator(), or declares a reader with ReflectiveTypeReaderBuilder<T> (changed since alpha.1, which had no reflective reader at all), but every canonical reader name must be registered explicitly. |
EnumReader is opt-in per enum |
EnumTypeReader<TEnum> exists (changed since alpha.1), but it is not one of the 61 built-ins: each enum type used in an XNB graph needs its reader registered, which a builder’s EnumField() does for you. |
| External references are typed | ContentReader::ReadExternalReference<T>() is implemented for the explicit types CNA instantiates; there is no reflective catch-all for arbitrary game types. |
VideoReader reads metadata only |
It is registered on every build (changed since alpha.1, when it followed FFmpeg availability). Playback of the loaded Video still needs the optional FFmpeg backend and throws NotSupportedException without one. |
EffectReader is renderer-qualified |
The reader is real, but construction throws on a renderer/build that does not expose CompiledEffects. HLSL .fx source and MGFX are still not accepted by the run-time reader. |
| New closed collection types still need registration | Eleven closed List, Array and Dictionary combinations are pre-registered, and Nullable exists in the codebase but is not. Without reflection each further closed generic instantiation (a list of your own type, say) has to be hand-registered, so there is no way to register them all up front. |
Effect and arbitrary .xml graphs have no CNB form |
They can be built to .xnb only. See Content Pipeline limits. |
Two adjacent Content namespace notes belong here as well. ResourceContentManager, which was a stub at alpha.1, is implemented in this snapshot: it reads binary resources from a System::Resources::ResourceManager through the protected OpenStream() seam (the public Load<T>() ladder still searches files). And ContentManager::Unload() clears the manager’s cache of loaded assets; a copy you already hold stays valid.
Testing
At this snapshot the Content module’s test tree holds 170 C++ test sources with 1,872 statically discoverable GoogleTest-family definitions (alpha.1: 112 sources and 978), including XNB container, reader-registration, writer, LZX encoder/decoder, real fixture, effect, audio, texture and model cases; the new content-pipeline module adds 47 sources with 487 definitions. These are counts of test definitions found by scanning the source, not of tests executed: the executable subset remains configuration-dependent. Tests against real pipeline output are especially valuable because they check compatibility beyond hand-authored byte arrays.
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.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Content input boundaries and hostile-input behaviour — Where CNA enforces size, count, nesting and path limits across XNB, CNB, CNJ and the build importer, how the loaders are fuzzed, which exceptions hostile files produce, and which gaps remain.
- 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.
- XNB type readers: wire contracts and validation — What each built-in XNB type reader reads, validates and refuses at this snapshot, from primitives and textures to SpriteFont, Model, effects and audio, with the evidence behind each.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-017: XnbReadLimits::maxFileSize is not applied to ContentManager's whole-file .xnb reads — The 64 MiB maxFileSize ceiling is enforced by the decompressors and the build-time canonical read, but ContentManager reads a run-time .xnb of up to INT32_MAX bytes and the manifest scan reads any size.
- CNA-BUG-022: ReflectiveTypeReader has no TypeVersion, so an XNA .xnb for a [ContentSerializerTypeVersion] type is rejected — Reader versions must match exactly, as in XNA, but a reflective reader always reports version 0 and ReflectiveTypeReaderBuilder cannot declare the version XNA's ReflectiveReader takes from the attribute.
- 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.
- CNA-BUG-120: ContentManager::LoadXnbAssetUntyped lacks LoadXnbAsset<T>'s compressed-header guard and refuses LZ4 — The untyped decoder behind EffectMaterial external references reads the 4-byte size at offset 10 without the 14-byte check, over-reading a 10-13-byte compressed file, and refuses LZ4 that the typed load path decodes.
- CNA-BUG-121: ContentManager::Load<T> never calls the virtual OpenStream, so ResourceContentManager::Load<T> ignores its resource family — OpenStream is documented as the seam every .xnb load goes through, but the public Load template reads files with TryReadAssetBytes; only the protected ReadAsset calls it, so an OpenStream override is bypassed.
- CNA-BUG-122: ContentManager::Load<T> leaks non-XNA exception types: unwrapped Load<SoundEffect> errors and std::runtime_error when disposed — ContentManager::Load wraps loose-reader failures in ContentLoadException, but the SoundEffect specialization lets System::NotSupportedException escape from its loose tier, and a disposed manager throws std::runtime_error
- CNA-BUG-228: xnb.md still says 'Status: planning document only. Nothing described here is implemented yet.' directly beneath a banner that records the XNB reader phases complete — The root design document for CNA's .xnb reader records the reader MVP, LZX and the SpriteFont/effect/audio readers as done, then states that nothing it describes is implemented; its banner also calls the Model phase unst
- CNA-VGAP-028: Several XNB runtime readers are never exercised with externally produced files, although genuine XNA 4.0 outputs for some of them are already in the tree — Texture3D, Curve, Video, DXT1/DXT5 Texture2D, container-version-4 textures and four of the five stock effects are read in tests only from CNA-written or hand-built bytes; the XNA-built differential corpus is used only by