CNB Format
Scope. This page describes the .cnb container as implemented at CNA snapshot 009d40f5 (24 September 2026, branch next). CNB did not exist in v0.1.0-alpha.1. Every layout below was read from the implementation under modules/content/{include,src}/CNA/Content/Cnb; CNA's own prose specification (docs/cnb-format.md) is cross-checked against the code by a conformance test in the CNA repository, but that test was read, not run, for this page. CNA's CNA_VERSION_STRING is still 0.1.0-alpha.1; the container itself is version 1.0.
Overview: what CNB is and is not
CNB is CNA's own compiled runtime content container. One .cnb file holds one asset — a texture, a model, a sound effect, a font, or a game-defined type — as a small header, a table of contents, and a set of checksummed chunks. It is the default output of the cna-content build tool (see Content Pipeline) and the second tier in ContentManager's load ladder, directly below .xnb.
Designed for
- One logical asset = one file; genuinely shared assets (textures, effects) stay shared and are referenced by name.
- Loading as a bounded parse of typed binary structures instead of a JSON parse plus a directory of follow-up opens.
- Rejecting malformed input with a clear message, never a crash or a silent misread.
- Deterministic output: identical inputs give byte-identical files.
- No dependence on the C++ ABI,
sizeofof a runtime type, struct padding, host endianness orstd::type_index.
Deliberately not
- Not a package or archive. One file, one asset. Bundling many assets is a different format.
- Not XNB and it shares no code with XNB: no reader table, no reflection, no platform byte, no shared-resource fixup protocol.
- Not an interchange format. glTF, PNG and WAV are how content arrives;
.cnjis how it is edited;.cnbis how it ships. - Not tamper-proof. The checksums detect accidental corruption. Anyone who can rewrite a chunk can rewrite its checksum, so a game that loads
.cnbfiles from untrusted downloads or mods needs its own signature or trusted-transport policy; the bounded, validated parse protects the loader from malformed input, not from a forged but well-formed file.
CNB, XNB and CNJ side by side
.xnb | .cnb | .cnj | |
|---|---|---|---|
| What it is | XNA's compiled content container. CNA reads it, and since this snapshot also writes it (cna-content --format xnb). | CNA's own compiled container; the default cna-content output. | CNA's editable JSON descriptor plus binary sidecars, read by the loose-file readers. An input to cna_tool_cnj_to_cnb and cna-content. |
| Files per asset | One | One (shared assets are named, not embedded) | Several: descriptor plus sidecars |
| How the type is chosen | A reader table inside the file names each reader | One u32 asset type identifier in the header | The JSON "type" string |
Tier in Load<T> | First | Second | Loose-file tier (after the literal path) |
| Compression | LZX, or a raw LZ4 block | None by default; Zstandard opt-in per chunk | None |
| Integrity | Length cross-check | CRC-32C on header, table of contents and every chunk | None |
Where to read more: ContentManager for the load ladder, XNB Loading & Interoperability for the XNB side, Content Pipeline for producing .cnb, and Tutorial 146 for a hands-on walkthrough.
The 64-byte header
Every integer in a .cnb file is little-endian, assembled byte by byte, so the file does not depend on host byte order. f32 and f64 are IEEE-754 binary32 and binary64; CNA refuses to compile on a platform whose float and double are not (four static_asserts per type in CnbFormat.hpp). A string is a u32 byte length followed by that many UTF-8 bytes, with no terminator; the reader validates the UTF-8 (no overlong forms, no surrogates, nothing above U+10FFFF).
| Offset | Size | Field | Rule |
|---|---|---|---|
| 0 | 4 | magic | 43 4E 42 1A — the ASCII letters CNB followed by 0x1A. Anything else: “not a .cnb file (bad magic bytes)”. |
| 4 | 2 | containerMajor | Must be 1. Any other major version is refused. |
| 6 | 2 | containerMinor | The writer emits 0. A higher minor is accepted on purpose: minor bumps are additive-only, and anything that matters travels in a chunk marked mandatory. |
| 8 | 4 | headerFlags | Must be 0; any set bit is refused. |
| 12 | 4 | assetTypeId | The asset type, see Asset types. 0 is invalid. |
| 16 | 4 | assetSchemaVersion | Starts at 1; must not exceed the highest version this build understands for the type. |
| 20 | 4 | chunkCount | Number of table-of-contents entries; at most 65 536 by default. |
| 24 | 8 | fileSize | Must equal the real size of the file. |
| 32 | 8 | tocOffset | At least 64. CnbWriter always writes 64. |
| 40 | 4 | tocChecksum | CRC-32C of the table-of-contents bytes (chunkCount × 48 bytes at tocOffset). |
| 44 | 4 | headerChecksum | CRC-32C of header bytes [0, 44), so it covers tocChecksum too. |
| 48 | 16 | reserved | Must be all zero. |
The reader checks the header checksum before it trusts any offset or size in the header, and the file size against the configured limit before it reads the file, so an oversized file is refused without being allocated.
Table of contents: 48 bytes per chunk
The table of contents starts at tocOffset and holds chunkCount fixed-size entries.
| Offset | Size | Field | Rule |
|---|---|---|---|
| 0 | 4 | type | Four printable ASCII bytes stored as a little-endian u32, so they read left to right in a hex dump. |
| 4 | 4 | flags | Bit 0 = Mandatory. Any other bit is refused. |
| 8 | 8 | offset | Absolute file offset of the chunk's stored bytes; a multiple of alignment. |
| 16 | 8 | storedSize | Bytes the chunk occupies in the file. |
| 24 | 8 | uncompressedSize | Logical size. Must equal storedSize when compression is 0. |
| 32 | 4 | checksum | CRC-32C of the stored bytes. |
| 36 | 4 | compression | Codec id, see Compression. |
| 40 | 4 | alignment | A power of two, at most 4096. The writer's default is 4; texture payloads use 16. |
| 44 | 4 | reserved | Must be zero. |
Layout invariants
Before any chunk byte reaches a schema decoder, CnbDocument::Parse() enforces all of the following, so a CnbDocument that exists is a structurally sound container:
- Entries are ordered by ascending offset.
- The header, the table of contents and every non-empty chunk tile the file exactly: nothing overlaps, nothing runs past the end, and every byte in an alignment gap or after the last chunk is zero. No stowaway bytes.
- A zero-length chunk is legal (an empty index buffer, a clip without tracks). It occupies no bytes and takes no part in the partition.
- All offset arithmetic is overflow-checked; the sum of every chunk's logical size is bounded (see Limits) before anything is allocated.
- Every chunk's CRC-32C matches its stored bytes.
- A compressed chunk expands to exactly its declared
uncompressedSize.
Chunk multiplicity is a schema rule, checked by the schema decoder after Parse() rather than by it: a chunk type that a schema allows only once (for example TEXH or CRVH) but that appears twice is refused (“has N ‘XXXX’ chunks, but this schema allows at most one”), and so is a required one that is missing (“is missing its required ‘XXXX’ chunk”). A custom schema gets the same checks from CnbDocument::FindSingle() and RequireSingle().
Chunks, container chunks and CRC-32C
Chunk identifiers and flags
A chunk identifier is four printable ASCII bytes (0x20–0x7E). By convention an identifier whose first byte is an uppercase letter belongs to CNA's own schemas and a game-defined schema uses a lowercase first letter; the writer does not enforce that convention. The single defined flag is Mandatory:
- An unknown chunk without the flag is skipped silently. That is what lets a newer writer add data an older reader can ignore.
- An unknown chunk with the flag makes the schema decoder refuse the whole file, naming the chunk: “is marked mandatory but is not understood by this build of CNA; the file requires a newer reader.” A decoder opts in with
CnbDocument::RequireMandatoryChunksUnderstood().
The two container-defined chunks
CnbWriter always emits these first, ahead of the schema's own chunks, each at most once, and refuses to let a schema add a chunk with either identifier. Schemas therefore address their own chunks by ordinal within a chunk type (CnbDocument::FindAll), never by table-of-contents index.
| Chunk | Flags | Contents | Role |
|---|---|---|---|
CMET | none | u32 flags (must be 0), string asset type name, string content name. | Optional for a built-in type, where the header's number already proves identity and the name is diagnostic. Required for a custom type, where it is part of loader identity. Never compressed. |
XREF | Mandatory | u32 count, then per reference: u32 flags (must be 0), u32 expected asset type id (0 = unconstrained), string logical name. | The external assets this file needs, by the exact name you would pass to Load<T>(). Lets a build script list dependencies without understanding the schema. Never compressed. |
Every XREF name is validated by one function, CnbLogicalNameProblem(), on both the write side and the read side: it must be non-empty, well-formed UTF-8, contain no backslash, not start with /, not be drive-qualified (C:), and contain no .. segment. A file can therefore never hand a path-traversal name to ContentManager's path resolution.
CRC-32C
The checksum is CRC-32C (the Castagnoli polynomial, reflected constant 0x82F63B78) over the header prefix, the table of contents, and each chunk's stored bytes. On x86 the SSE4.2 instruction is selected at run time by CPUID (no build flag, so one binary still runs on a CPU without it); on 64-bit ARM the CRC32 extension is used where the OS reports it (on Linux via HWCAP_CRC32; always on Apple silicon); everything else falls back to a table-driven implementation. All paths produce bit-identical results, and Crc32cUsesHardwareEXT() (C ABI: cna_cnb_crc32c_uses_hardware) reports which one is in use.
An annotated minimal file
The smallest instructive file is a Curve (asset type 7, schema 1) with no keys, no CMET and no XREF: 172 bytes, two chunks. The checksum values below were computed by following the writer's algorithm by hand (standard CRC-32C, checked against the well-known value 0xE3069283 for the ASCII string 123456789) and were validated by an independent re-implementation of the parse rules above; they are illustrative, not the output of a CNA run. CNA's own specification contains the same layout with the checksums elided.
offset bytes meaning
------ -------------------------- ----------------------------------------------
000000 43 4E 42 1A magic "CNB\x1A"
000004 01 00 containerMajor = 1
000006 00 00 containerMinor = 0
000008 00 00 00 00 headerFlags = 0
00000C 07 00 00 00 assetTypeId = 7 (Curve)
000010 01 00 00 00 assetSchemaVersion = 1
000014 02 00 00 00 chunkCount = 2
000018 AC 00 00 00 00 00 00 00 fileSize = 172
000020 40 00 00 00 00 00 00 00 tocOffset = 64
000028 6A 3D C5 25 tocChecksum = 0x25C53D6A (CRC-32C of [64,160))
00002C 45 60 52 D6 headerChecksum = 0xD6526045 (CRC-32C of [0,44))
000030 00 * 16 reserved
000040 43 52 56 48 chunk 0 type = "CRVH"
000044 01 00 00 00 flags = Mandatory
000048 A0 00 00 00 00 00 00 00 offset = 160
000050 0C 00 00 00 00 00 00 00 storedSize = 12
000058 0C 00 00 00 00 00 00 00 uncompressedSize = 12
000060 5D B5 60 2B checksum = 0x2B60B55D (CRC-32C of 12 zero bytes)
000064 00 00 00 00 compression = 0 (none)
000068 04 00 00 00 alignment = 4
00006C 00 00 00 00 reserved
000070 43 52 56 4B chunk 1 type = "CRVK"
000074 01 00 00 00 flags = Mandatory
000078 AC 00 00 00 00 00 00 00 offset = 172 (zero-length chunk at end of file)
000080 00 00 00 00 00 00 00 00 storedSize = 0
000088 00 00 00 00 00 00 00 00 uncompressedSize = 0
000090 00 00 00 00 checksum = 0 (CRC-32C of nothing)
000094 00 00 00 00 compression = 0
000098 04 00 00 00 alignment = 4
00009C 00 00 00 00 reserved
0000A0 00 00 00 00 CRVH: preLoop = 0 (Constant)
0000A4 00 00 00 00 CRVH: postLoop = 0 (Constant)
0000A8 00 00 00 00 CRVH: keyCount = 0
0000AC end of file
A Curve written by EncodeCurveToCnb() additionally carries a CMET chunk (the canonical name Microsoft.Xna.Framework.Curve plus the content name), so a real file is a little larger than this minimal one.
Asset types
The header's assetTypeId selects the schema and the loader. The numeric values of the built-in types are frozen. They are explicit constants, not the position of an enumerator in a runtime type, and the same holds for the other identifiers a file stores: chunk identifiers are four ASCII bytes, and texture format ids are their own CnbTextureFormat values, mapped to and from SurfaceFormat by the explicit functions CnbTextureFormatToSurfaceFormat() and SurfaceFormatToCnbTextureFormat(), because SurfaceFormat is numbered by position and inserting an enumerator would otherwise renumber every file already written. Changing a runtime enum therefore never changes the format; changing that mapping is a deliberate edit.
| Id | Asset | Runtime type returned by Load<T> | Schema |
|---|---|---|---|
0x00000001 | Texture2D | Texture2D | 1 |
0x00000002 | Texture3D | std::shared_ptr<Texture3D> (the type is move-only) | 1 |
0x00000003 | TextureCube | TextureCube | 1 |
0x00000004 | SpriteFont | SpriteFont (the glyph atlas is embedded) | 1 |
0x00000005 | Model | Model | 1 and 2 |
0x00000006 | AnimationClip | AnimationClipEXT | 1 |
0x00000007 | Curve | Curve | 1 |
0x00000008 | SoundEffect | SoundEffect | 2 (version 1 files still read) |
0x00000009 | Song | Song (metadata plus a streaming reference) | 1 |
0x0000000A | Video | Video (metadata plus a streaming reference) | 1 |
0x0000000B | Effect | Identifier reserved, no schema, by design. CNA has many renderers, so a .cnb carrying one API's shader bytecode would be useless on the others; the schema waits for the shader pipeline and renderer abstraction to settle. | |
| Range | Owner |
|---|---|
0x00000000 | Invalid; a file declaring it is refused. |
0x00000001–0x3FFFFFFF | CNA built-in types, frozen. |
0x40000000–0x7FFFFFFF | Reserved for future CNA use. |
0x80000000–0xFFFFFFFF | Game-defined types. |
Minting a custom type id
CnbAssetTypeIdFromName(name) returns FNV-1a-32(name) | 0x80000000 over the UTF-8 bytes of name (offset basis 2166136261, prime 16777619), so the same name mints the same identifier in the compiler, the runtime and any third-party tool. For example, "MyGame.Level" mints 0xF439D945 and "MyGame.LevelInfo" mints 0xA07759A5 (computed from the definition above). An empty name throws std::invalid_argument.
Only 31 bits are usable, so two unrelated names can collide. CNB defends against that by name, not by number: the CMET chunk of a custom-typed file must carry the canonical name, and the loader is resolved only when that name equals the one the loader was registered under. CnbWriter::Build() refuses to write a custom-typed file whose CMET name is missing or does not hash to the declared id, so a file that could never load cannot be produced. See Extending CNB.
Schemas: what each asset type stores
Container version and asset schema version evolve independently. Each schema defines its own chunks (all listed chunk ids are CNA-reserved uppercase-first identifiers).
| Asset | Chunks | Notes |
|---|---|---|
| Texture2D / Texture3D / TextureCube | TEXH (24 bytes: width, height, depth, faceCount, mipCount, representationCount), TEXR (24 bytes per representation), TEXD (one per level per representation, face-major then mip, 16-byte aligned) | One shared layout for the three types. Up to 16 mip levels and 8 representations. The layout can carry several encodings of the same image, but schema 1 encodes Rgba8 only: the writer refuses any other format, and a loaded file that offers only other formats is refused with “this build can upload none of them”. Bc1…Bc7 and other format ids are assigned but have no writer. |
| SpriteFont | FONT (24 bytes), GLYP, CROP, KERN, CHAR, plus an embedded TEXH/TEXR/TEXD atlas | The atlas belongs to one font, so it is embedded rather than referenced. The character map must be strictly ascending. |
| Model, schema 1 | MDLH, MSTR, MBON, MMSH, MMAT, MVTX, MIDX, MMRP, MSKL, MANM, MLIT | Bone hierarchy, meshes and parts, materials, vertex and index bytes, morph targets and weight keys, skinning skeleton, embedded animation clips, punctual lights. Textures and named effects are external: they go through XREF. The schema has no camera field, and the .cnj-to-CNB compiler refuses glTF material variants rather than compile a quietly less capable asset. The glTF import report is an authoring-time record and is not stored. |
| Model, schema 2 | M2HD, M2ST, M2BN, M2MS, M2PT, M2VD, M2VR, MVTX, M2IR, MIDX, M2FX | Preserves exact XNA vertex declarations, shared vertex and index buffers, and the five stock effects (BasicEffect, SkinnedEffect, DualTextureEffect, AlphaTestEffect, EnvironmentMapEffect) for models that originate in an XNB. The loader picks the decoder from the header's schema version. |
| SoundEffect | AUDH (28 bytes: format, sample rate, channels, frame count, loop start, loop length, reserved flags), AUDD | Headerless little-endian PCM. Schema 2 adds 8-bit unsigned PCM beside 16-bit; the header layout did not change and a version-1 file that declares 8-bit is refused. Sample rates up to 384 000 Hz, 1 or 2 channels. |
| Song / Video | SNGH (8 fixed bytes, then the display name) / VIDH (24 bytes: duration, width, height, frame rate, soundtrack type, reserved flags) | Metadata plus a reference, never the media. The streaming file is the file's single XREF entry and ships beside the .cnb, so cna_tool_cnb_info --refs shows it. Duration, frame size and frame rate are supplied by the author, because CNA has no headless multimedia decoder. |
| Curve | CRVH (pre-loop, post-loop, key count), CRVK | Keys are position, value, tangent in, tangent out, continuity. |
| AnimationClip | ACLH, ACLT, ACLK | Header, tracks, keyframes. |
Compression
Compression is a per-chunk field, off by default, and the codec ids are frozen wire format.
| Id | Codec | Status in this snapshot |
|---|---|---|
0 | None | Always available; the default. |
1 | LZ4 | Identifier assigned; no implementation. |
2 | Zstandard | Implemented when CNA is built with libzstd (CNA_CNB_ZSTD, default AUTO: on when the system library is found, silently off otherwise; ON demands it, OFF omits it). |
3 | Deflate | Identifier assigned; no implementation. |
- How to turn it on. Only through
CnbWriter::SetCompression(CnbCompression::Zstd, level)in C++ (orcna_cnb_writer_set_compressionin the C ABI). A non-test search of the source finds no other caller:cna-content,cna_tool_source_to_cnb,cna_tool_cnj_to_cnbandcna_tool_gltf_to_cnbexpose no compression option, and theEncode*ToCnb()helpers build their own writer without one. AskIsCnbCompressionSupported(CnbCompression::Zstd)rather than assuming. - Only when it helps. A chunk is stored compressed only if that made it smaller; the container chunks
CMETandXREFare never compressed. The Zstandard level is clamped to 1…the library maximum; 3 is the default. - Safe to read. The chunk checksum covers the stored bytes, so corruption is caught before a decompressor sees them. The declared
uncompressedSizeis checked against the limits before any allocation, and the stream must expand to exactly that size. - A compressed file needs a codec. A CNA built without libzstd refuses the file while reading the table of contents (“uses compression codec 2 (Zstandard), which this build does not implement”) — which also means
cna_tool_cnb_infocannot inspect it. An older CNA that predates the codec refuses any non-zero codec. Turning compression on raises that file's minimum runtime.
CNA-reported measurements, not guarantees. CNA's own notes (docs/cnb-compression-measurements.md, measured 27 August 2026 on one developer machine: AMD CPU, NVMe storage, Debian 13, gcc -O2) report Zstandard level 3 at about 51 % of the raw size for a 1024×1024 Rgba8 photograph, 57 % for a normal map, 27 % for two seconds of 16-bit audio and 15 % for float vertex data, decompressing at roughly 0.9–2 GB/s. The same notes conclude that compression saves load time only when the device reads slower than roughly 0.5–1.5 GB/s, and that on that machine's NVMe (2.5–4.4 GB/s) it makes loading slower — which is why it is opt-in. The probes are committed in the CNA repository, so the numbers are reproducible, not portable; treat them as orientation for your own measurement.
Memory mapping: measured and rejected
CnbDocument::ParseFile() reads the whole file into memory with an ifstream and verifies every chunk checksum before returning. There is no memory-mapped or lazily verified access, and CNA's own notes record that as a rejected option, not a pending one. CNA's mmap measurements (same developer machine and date, warm cache, a 32 MiB file of eight 4 MiB textures) report that reading the file took about 7 ms, table-driven CRC verification about 63 ms and SSE4.2 verification about 3 ms; memory mapping would have saved about 4.7 ms where hardware CRC saved about 59 ms. That is why CNA implemented run-time-detected hardware CRC-32C instead (reported end-to-end through cna_tool_cnb_info: 127 ms to 20 ms). The notes record what would change the answer: a target without a CRC instruction, loading only part of a file (which would trade away the every-chunk corruption check), or sharing one asset between processes. The alignment field keeps its meaning regardless.
Limits
Every count-driven read is bounded, so a corrupt or hostile count fails fast instead of triggering an enormous allocation. CnbReadLimits defaults:
| Limit | Default |
|---|---|
File size (maxFileSize) | 512 MiB |
Chunks per file (maxChunkCount) | 65 536 |
One chunk, stored or logical (maxChunkSize) | 384 MiB |
Sum of every chunk's logical size (maxTotalUncompressedSize) | 1 GiB — deliberately larger than the file limit, so compression can genuinely expand a file |
One serialized string (maxStringBytes) | 1 MiB |
Elements in one serialized array (maxArrayElementCount) | 16 Mi (16 777 216) |
Chunk alignment (maxChunkAlignment) | 4096 bytes |
Schemas add their own ceilings (16 mip levels and 8 representations per texture, 65 536 glyphs per font, 65 536-pixel video dimensions). The writer applies the same limits as the reader (CnbWriter::SetLimits(), defaulting to the reader defaults), so a file Build() returns is a file Parse() opens; a highly compressible document that would exceed the aggregate expansion budget is refused when you write it, not when a player loads it.
Determinism
Given identical inputs, CnbWriter produces byte-identical output: it reads no clock, no random source, no pointer value and no environment; chunks are emitted in the order the schema adds them; the table of contents is laid out in that same order; alignment gaps are zero-filled; and CMET holds only input-derived strings. CNA's specification records this as asserted in-process and across two separate OS processes, and against golden byte vectors produced by a separate Python implementation of the specification. Those tests exist in the repository; we did not run them.
The loading path
ContentManager::Load<T>(name) resolves in this order (the header comment names it xnb > cnb > literal > cnj > loose):
- The manager's cache, keyed by the type and the lower-case-normalised name.
<root>/<name>.xnb. An authentic compiled XNA asset always wins.<root>/<name>.cnb. The file is self-describing, so this tier needs no reader registered forT.- If
nameitself ends in.cnb, that literal file. - Otherwise the per-type loose-file reader: the literal path, then
<name>.cnj, then the reader's own extensions (.png,.wav,.gltf…).
Inside the .cnb tier:
CnbDocument::ParseFile()reads the entire file and applies every invariant listed above.CnbLoaderRegistry::ResolveForDocument()picks the loader for the header's asset type. For a built-in type the number is authoritative; for a custom type it additionally proves identity by name.- The loader decodes the chunks into a CPU-side description and builds the runtime object (creating GPU resources through the manager's
GraphicsDevice); references named inXREFare loaded through the same manager, so shared textures are cached once. - The result must be exactly the
Tyou asked for.
The built-in loaders are registered by every ContentManager constructor (the eight that need a device or the manager itself — Model, Texture2D, TextureCube, Texture3D, SpriteFont, SoundEffect, Song, Video — from ContentManager::RegisterBuiltinLoaders(), and Curve and AnimationClip from CnbLoaderRegistry::RegisterBuiltIns()), so a game never registers them. Load<SoundEffect> has its own specialisation with the same tier order; it does not cache, because SoundEffect is move-only.
What a failed load says
All of these are ContentLoadExceptions whose message names the file:
| Message (abridged) | Meaning |
|---|---|
is only N bytes; a .cnb file needs at least 64 bytes / is not a .cnb file (bad magic bytes) | Truncated or not a CNB. |
declares CNB container version M.m; this build reads major version 1 only | Written by a future container major. |
has a corrupt header: checksum 0x… does not match the computed 0x… | Damaged in transit or edited. Also raised for a corrupt table of contents or a corrupt chunk (chunk 3 (TEXD) is corrupt). |
declares a file size of A bytes but is actually B bytes | Truncation or a text-mode transfer. |
uses compression codec 2 (Zstandard), which this build does not implement | The file needs a libzstd build. |
holds a Texture2D asset, which is not the type requested for '…' | The T in Load<T> does not match the file. |
holds a custom type 0x… asset, which this build of CNA has no .cnb loader for (the file names it '…') | A custom type whose loader is not registered yet, or the reserved Effect id (printed as holds a Effect asset). |
declares custom type 'A', but asset type id 0x… is registered for 'B' | A hash collision between two game types, caught by the name check. |
The .cnb texture '…' offers only …, and this build can upload none of them | A texture with no Rgba8 representation. |
streams 'X', which was not found beside it | A Song/Video .cnb whose media file is not at its content-root-relative name. |
Two things to know. A .cnb next to a source file shadows it: the tier test is “does the file exist”, with no timestamp comparison, so a stale .cnb keeps winning after you edit the PNG next to it until you rebuild or delete it. And the .xnb tier can read platform-packaged assets (Android), whereas the .cnb tiers use plain filesystem calls; we have not verified .cnb loading from an Android package.
Extending CNB with your own asset type
CNB's extension model is deliberately much smaller than XNB's: a header carries one u32 asset type id, and the loader registered for that id decodes the file. There is no reflection, no assembly-qualified name and no per-file reader table.
- Mint the id once:
const uint32_t id = CnbAssetTypeIdFromName("MyGame.Level"); - Write files with
CnbWriter(id, schemaVersion), callingSetMetadata("MyGame.Level", contentName)(mandatory for a custom type) andAddChunk()for your data with an identifier that starts with a lowercase letter. The writer refuses a custom-typed file without the canonical name, or with a name that does not hash to the id. - Register a loader:
ContentManager::RegisterCnbLoaderEXT<T>(id, "MyGame.Level", factory), wherefactoryis astd::function<T(const CnbDocument&, ContentManager&)>andTis exactly the type you will pass toLoad<T>(). - Load it like any asset:
getContentProperty().Load<LevelInfo>("Levels/level01").
The rules the registry enforces, all verified in CnbLoaderRegistry.hpp: the id must be in the custom range (a built-in or reserved id is refused with std::invalid_argument, and there is deliberately no route at all by which a game can register one); the canonical name must not be empty and must hash to the id; registering the same id under a different name throws std::logic_error, while repeating the same registration is accepted and does nothing; registration is process-wide, guarded by a shared mutex, and outlives any ContentManager; and lookups hand back a copy of the loader, so loading from several threads is safe with respect to the registry. A working end-to-end example is in Tutorial 146; Tutorial 148 covers custom types in depth.
Tooling
All of the following are built by default with CNA (no option gates them). This section covers the CNB-specific tools; the complete command-line catalogue is on Command-Line Tools.
| Tool | Usage (from the tool's own help text) | Produces |
|---|---|---|
cna-content | cna-content build <source|dir|.contentproj> -o <output> [--format cnb|xnb] … | .cnb by default; .xnb with --format xnb. See Content Pipeline. |
cna_tool_source_to_cnb | <input> <output.cnb> [--name <logical>] [--mipmaps] [--mip-color-space linear|srgb] [--color-key R,G,B] [--as texture2d|soundeffect|song|texturecube|video] [--stream <name>] [--duration-ms <n>] [--title <text>] [--frame-size WxH] [--fps <f>] [--soundtrack 0..2] [--quiet] | Texture2D (PNG, JPEG, BMP, TGA, GIF, PSD, HDR, PIC, PNM), SoundEffect (WAV), TextureCube (DDS), Song and Video (metadata plus a reference). |
cna_tool_cnj_to_cnb | <input.cnj> [output.cnb] [--content-root <dir>] [--name <logical>] [--quiet] | From a .cnj and the sidecars it names: Curve, AnimationClip, Model, Texture2D, Texture3D, TextureCube, SpriteFont, SoundEffect. Any other "type" is refused by name. |
cna_tool_gltf_to_cnb | <input.gltf|input.glb> <outputDir> <baseName> [--unit-scale <f>] [--keep-cnj <dir>] [--quiet] | One Model .cnb per skin, using the same glTF interpretation as cna_tool_gltf_to_cnj. --unit-scale is the only place a unit scale applies (the runtime glTF path is always 1.0). |
cna_tool_cnb_info | <file.cnb> [--refs] [--chunks] [--quiet] | Nothing; validates and prints. --refs prints one external logical name per line, --chunks only the chunk table. Exit code 0 for a valid file, 1 for a malformed one (with the reason on stderr), 2 for a usage error. |
Every producer is headless: it creates no GraphicsDevice and opens no audio device, so it runs on a build machine with no display, GPU or sound card. The standalone tools publish their output through a temporary file and an atomic rename, so a failing run leaves the previous .cnb intact. Numeric options are parsed strictly (the whole token, a range check, no NaN or infinity), and an option that does not apply to the asset kind being produced — --mipmaps on a WAV, say — is an error rather than something silently ignored.
cna_tool_cnb_info prints the container fields, the metadata and a chunk table with the stored and logical size of each chunk and the codec, so a compressed chunk shows how much it saved:
Content/Textures/checker.cnb
container 1.0
asset type Texture2D (0x00000001)
schema version 1
chunks 10
type name Microsoft.Xna.Framework.Graphics.Texture2D
content name Textures/checker
chunk flags offset stored logical align codec checksum
CMET optional 544 70 70 4 none 0x…
TEXH required 616 24 24 4 none 0x…
TEXR required 640 24 24 4 none 0x…
TEXD required 672 16384 16384 16 none 0x…
… (six smaller mip levels)
external references: 0
That listing shows the shape of the output for a 64×64 Rgba8 texture with a seven-level mip chain (Tutorial 146 builds it). The offsets and sizes were derived from the writer's layout rules rather than copied from a run; the checksum values depend on your pixels and are elided.
The C ABI surface
The experimental C ABI (version 0.29.0 at this snapshot; v0.1.0-alpha.1 was 0.7.0) exports the container and every asset schema. modules/c-api/include/CNA/C/cnb.h declares 272 cna_cnb_* routes (counted from the header); none carries an _ext suffix because the header itself marks CNA-namespace surface. The family is pure functions over caller-owned bytes, plus handles for documents, cursors, writers and decoded descriptions.
| Family | Routes | Covers |
|---|---|---|
| Container identities and arithmetic | 16 | Read-limit initialisation, magic, chunk-id helpers, asset_type_id_from_name, logical-name validation, checked add and multiply. |
| Checksums | 4 | crc32c, crc32c_continue, crc32c_uses_hardware, crc32c_portable. |
| Chunk compression | 6 | Codec support query and names, compress and decompress into caller buffers. |
| Document | 28 | Parse from bytes or file, header fields, chunk table and chunk data, find-single, mandatory-chunk rule, metadata, external references, require_asset, limits. |
| Byte cursor | 24 | Bounded little-endian reads (integers, floats, strings, counts, bytes), skip, require-exhausted, UTF-8 check. |
| Byte writer | 17 | Little-endian primitive writers and take. |
| Container writer | 12 | Create, set metadata, add external reference, add chunk, set compression, set limits, build, write to file. |
| Textures | 28 | Texture formats, Texture2D, TextureCube, Texture3D data, encode and decode, embedded atlas helpers. |
| Model | 71 | A model data handle with bone, part, material, morph, skeleton, animation and light accessors; encode and decode; .cnj model build. |
| SpriteFont | 11 | Font data handle, encode and decode. |
| SoundEffect | 9 | Audio format helpers, sound data handle, encode and decode. |
| Song and Video | 10 | Encode and field-by-field decode. |
| Curve and AnimationClip | 8 | Encode and decode. |
| Loader registry | 11 | Register, remove, find, resolve for document, invoke; register built-ins. |
Importers and .cnj compile | 17 | Import an image, DDS or WAV, and compile_cnj with its result handle. |
The family counts group routes by name prefix (approximate); the total, 272, is the count of distinct cna_cnb_* routes in cnb.h. One gap, per CNA's own C-ABI notes: ContentManager::RegisterCnbLoaderEXT is not bound, so a C-registered loader is invoked directly (cna_cnb_loader_registry_resolve_for_document and cna_cnb_loader_invoke) rather than through a by-name Load. Separately, content.h declares 32 routes and content_readers.h 62 (a content manager, typed loads such as cna_content_manager_load_texture2d, which call ContentManager::Load<T> and therefore follow the same tier order, the XNB reader machinery, and the reflective reader builder). The Content Pipeline has no C ABI export. Experimental C API covers the ABI's status: it is source-level surface, and we did not build the C API library or run its tests, so treat this as the declared contract rather than a verified binary.
What is not implemented
Recorded so each boundary is a decision rather than a surprise:
- An
Effectschema. The id is reserved and frozen; the layout is deliberately undesigned (see Asset types). Effects travel as.xnb(compiled Effect Framework bytecode) or through the loose.cnjdescriptor, andcna-contentwrites an Effect only to XNB. - Embedded audio or video streams.
SongandVideostore a reference by design. - Block-compressed texture payloads. Format ids and the multi-representation layout exist, but no writer produces a
Bc*payload and the loader uploadsRgba8only. Askingcna-contentfor a DXT texture format while writing.cnbkeeps the uncompressed pixels and prints a warning; use--format xnbto get the compressed texture. - Memory-mapped or lazily verified access. Measured and rejected (see above).
- LZ4 and Deflate chunk codecs. Ids only.
- Compression through any CLI. Library and C ABI only.
- A package format bundling many assets. A different format and a different project.
- Authenticity. CRC-32C is not a signature.
Sources
Everything on this page was read from CNA at commit 009d40f5dd085c4e674d3479675fac84b12b3e0a: the container constants and identifiers in CnbFormat.hpp, the parse rules in CnbDocument.cpp, the writer in CnbWriter.cpp, the registry in CnbLoaderRegistry.hpp, the tools under tools/cnb_info, tools/source_to_cnb, tools/cnj_to_cnb and tools/gltf_to_cnb, and the C header cnb.h. Measurements and design rationale are CNA's own prose and are attributed as such above. Nothing here was built or run: CNA was not compiled for this page.
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.
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.