Content Pipeline
Status at this snapshot. CNA now has a build-time Content Pipeline: the cna-content tool (CMake target cna_content_tool) turns PNG, WAV, glTF, .spritefont, .cnj and other sources into CNA's own .cnb container (the default) or into XNA-compatible .xnb files. The v0.1.0-alpha.1 tag shipped only the read side (the loaders described on XNB Loading); everything on this page is new in CNA snapshot 009d40f5. The tool is built with CNA by default; no CMake option gates it. The custom-component C++ API is explicitly marked experimental, and there is no C ABI export of the pipeline.
What it is
The pipeline is the offline half of content handling. A source file goes through three stages, and the same importers and processors feed both output containers: only the last stage, the writer, differs.
source file --> Importer --> Processor --> Content type writer --> .cnb (default)
(PNG, WAV, glTF, ...) or .xnb (--format xnb)
| Item | At this snapshot |
|---|---|
| Command-line tool | cna-content, built from the CMake target cna_content_tool; two commands, build and clean. The executable is a 42-line main over a static library, cna_content_compiler (alias CNA::ContentCompiler), which carries the whole coordinator. |
| Default output | .cnb, CNA's native compiled container (CNB Format). --format xnb writes XNA-compatible .xnb instead. |
| Engine module | modules/content (cna_content): ContentManager, the XNB reader and writer, the CNB codecs, and the canonical pipeline engine (ContentImporter, ContentProcessor, ContentTypeWriter, ContentPipelineRegistry, the incremental manifest). A game that only loads content links this module. |
| Build-time module | modules/content-pipeline (cna_content_pipeline): the FreeType-backed .spritefont route, the block-compression encoder, the .x and .fbx readers, the XNA Content.Pipeline façade, and the effect-compiler and XMA-encoder services. Only the compiler links it, so FreeType and FFmpeg never enter a runtime game's link closure. About 28,000 lines of C++ in src/. |
| Project formats | CNA's own optional .cna-content.json (per-asset overrides), and XNA Game Studio .contentproj files, read as plain XML without MSBuild. There is no MonoGame .mgcb reader. |
| Incremental | A content-hashed manifest in the output directory; an unchanged asset is skipped. Output bytes are deterministic for the same inputs. |
| CMake integration | The cna_add_content() function, see below. |
Terminology. XNB Loading is the reader page: what ContentManager can load at run time. This page is the authoring side: what cna-content can produce. CNB is an output of the pipeline, not the pipeline itself. For the other command-line tools (cna_tool_cnb_info, cna_tool_gltf_to_cnb and so on) see Command-Line Tools.
Quick start
Build the tool once from your CNA build tree, then point it at a directory of sources. The tool targets set no output-directory override, so with a single-configuration generator the executable lands at the top of the build tree (for example build/cna-content; a multi-configuration generator adds a configuration folder, and Windows adds .exe). See Building for configuring CNA itself.
# 1. build the tool (CMake target cna_content_tool)
cmake --build build --target cna_content_tool
# 2. compile every recognised source under ContentSource/ into Content/
./build/cna-content build ContentSource -o Content
# 3. ask why each asset was built or skipped
./build/cna-content build ContentSource -o Content --explain
# 4. compile a single file (the output extension must match --format)
./build/cna-content build ContentSource/Textures/wall.png -o Content/Textures/wall.cnb
# 5. remove what the pipeline wrote, and only that
./build/cna-content clean Content
A directory build keeps relative paths. Every output takes the source's path below the source root with the last extension replaced, and that path without an extension is the asset's logical name, the string you pass to Load<T>():
ContentSource/ Content/
Textures/wall.png --> Textures/wall.cnb Load<Texture2D>("Textures/wall")
Sounds/explosion.wav --> Sounds/explosion.cnb Load<SoundEffect>("Sounds/explosion")
Models/robot.glb --> Models/robot.cnb Load<Model>("Models/robot")
.cna-content-manifest.json
.cna-content.lock
Discovery is by extension, and it is silent. A directory build walks the source tree recursively and takes every regular file whose extension some registered importer claims; any other file is ignored without a message. Two sources that resolve to the same logical name (wall.png and wall.wav in one folder) are an error. The output directory must not be inside the source directory. For a hands-on walk-through with real commands, see Tutorial 145: Build Content with cna-content.
Source-to-output routes
These are the routes the stock cna-content registers, read from each importer's declared source extensions. In the two container columns, “Yes” means the container can hold the result and a dash means it cannot; the build reports why.
| Source | Becomes | .cnb |
.xnb |
Needs at build time |
|---|---|---|---|---|
.png .jpg .jpeg .bmp .tga .gif .psd .hdr .pic .pnm .dds .dib .pfm .ppm |
Texture2D through CNA.TextureProcessor. A .dds can also carry a cube or volume texture, as with XNA's TextureImporter. |
Yes, Rgba8 only (texture schema 1); optional mip chain | Yes, including textureFormat DXT1/3/5 |
Nothing extra |
.wav |
SoundEffect. Only 8-bit and 16-bit PCM is accepted by this route. | Yes | Yes | Nothing extra |
.mp3 .ogg .oga .qoa .flac .opus .aac .wma |
Song: metadata plus a streaming reference. The media file itself is copied byte for byte beside the output as a deployment file. | Yes | Yes | Duration is read by a build-time media probe when one is present; otherwise the Song carries duration zero |
.mp3 .wma asked for as a sound effect (importer CNA.CompressedSoundImporter, chosen by name) |
SoundEffect decoded to PCM | Yes | Yes | The build-time media pipeline (FFmpeg libraries) |
.wmv .mp4 .ogv .webm .mkv .avi .mov |
Video: metadata plus a streaming reference; the media file is deployed beside the output | Yes | Yes | Frame width, height and rate are required parameters unless a build-time probe can read them |
.gltf .glb |
Model through CNA.GltfImporter and CNA.ModelProcessor; unit scale is always 1.0 |
Yes (model schema 1 or 2). One primary Model per source; a file that produces several Model documents needs generateChildAssets |
Yes, but skeleton, animation clips, lights and morph targets are dropped with warnings, and PBR materials are downgraded to stock effects | Nothing extra (Draco-compressed meshes need CNA_ENABLE_DRACO) |
.cnj |
Typed by the document: Texture2D, Texture3D, TextureCube, SpriteFont, SoundEffect, Curve, AnimationClip or Model | Yes | Where an XNB writer exists; AnimationClip is CNB-only by design | Nothing extra |
.xnb (a supported built-in root) |
Transcoded: Texture2D, Texture3D, TextureCube, SpriteFont, SoundEffect, Song, Video, Curve or Model roots become native assets | Yes | — (an .xnb is a source here) |
Nothing extra |
.spritefont |
SpriteFont, rasterised from an installed font family | Yes (8-bit atlas) | Yes (CNA reports a DXT3 atlas for .xnb, matching XNA's processor) |
FreeType (CNA_ENABLE_FONT_PIPELINE) and the font itself; see --font-directory |
.fxb (already-compiled effect) |
Effect | — (CNB has no Effect schema, by design) | Yes | Nothing extra |
.fx (HLSL source) |
Effect, compiled by an external fxc-compatible program |
— | Yes, only when an external compiler is found | An external compiler (--fx-compiler, CNA_FXC, CNA_FXC_EXECUTABLE or fxc on PATH) |
.x, .fbx |
Model through XNA's ModelProcessor; the materials' textures start nested texture builds |
Yes for models whose materials map to stock effects; a model whose material names a game-specific effect is refused with a message pointing to XNB | Yes | zlib for compressed arrays in binary FBX; a text FBX needs nothing |
.xml (XNA intermediate XML) |
Any type registered with the XNA ContentCompiler, read by the intermediate serializer |
— (no CNB schema for arbitrary object graphs) | Yes | Nothing extra |
.contentproj |
An XNA content project: its own platform, profile, compression and per-asset importer/processor/parameters | — (a project build runs the coordinator with --format xnb) |
Yes | Whatever its assets need |
Several importers, one default. Some extensions have more than one importer: .wav is a SoundEffect by default and a Song when you name CNA.WavSongImporter; .mp3 and .wma are Songs by default and SoundEffects when you name CNA.CompressedSoundImporter. Name a non-default importer, processor or writer per asset in .cna-content.json. Where a route has no writer for a container, the pipeline records a reason and the build reports it rather than failing silently; a test enforces that every processed type either has a writer or a recorded reason.
Command line
cna-content build <source-file | source-directory | .contentproj> -o|--output <output>
[--format cnb|xnb] [--config <file>] [--workers <1..64>]
[--xnb-platform <name>] [--xnb-version 4|5] [--xnb-profile reach|hidef]
[--xnb-compress none|lzx|lz4] [--xnb-reader-names xna40|portable]
[--xnb-allow-unverified-xbox]
[--fx-compiler <path>] [--fx-compiler-launcher <program>]
[--xma-encoder <path>] [--xma-encoder-launcher <program>] [--xma-encoder-arg <arg>]...
[--font-directory <dir>]... [--build-configuration <name>]
[--explain] [--quiet] [--xna-compatible] [--only-configured-assets]
cna-content clean <output-directory> [--quiet]
| Option | Meaning |
|---|---|
-o, --output | Required. For a directory build, the output directory (relative paths and logical names are preserved). For a single file, an output file whose extension matches --format. |
--format cnb|xnb | The compiled container. Default cnb. Importers and processors are the same either way. |
--config <file> | A configuration file. Without it, .cna-content.json in the source root is used when it exists. |
--workers <1..64> | Bounded concurrent execution of independent build nodes. 1, serial, is the default. The output bytes are meant to be identical whatever the count. |
--explain | Print a reason: line under every asset saying why it was built or skipped. |
--quiet | Suppress the per-asset lines and the summary; errors still go to standard error. |
--xna-compatible | Turn three refusals into XNA-style warnings: a processor parameter the processor does not recognise, a parameter value it cannot convert, and a character a font has no glyph for. The tool's own default is to refuse all three. |
--only-configured-assets | Make the configuration the asset list instead of a set of overrides: discovered sources it does not name are left alone. A .contentproj build selects this itself. |
--xnb-platform, --xnb-version, --xnb-profile, --xnb-compress, --xnb-reader-names, --xnb-allow-unverified-xbox | Container options for .xnb output, described in XNB output. |
--fx-compiler, --fx-compiler-launcher | Choose the external fxc-compatible compiler for .fx sources and a program to run it through, such as wine. Precedence: the option, then CNA_FXC / CNA_FXC_LAUNCHER in the environment, then the path baked in by CMake, then fxc on PATH. |
--xma-encoder, --xma-encoder-launcher, --xma-encoder-arg | Attach your own XMA encoder (a program handed a RIFF WAVE that writes a .xma). cna-content ships no XMA encoder; asking for one without attaching yours is refused with XMA ENCODER EXTERNALLY UNAVAILABLE. Environment forms: CNA_XMA_ENCODER, CNA_XMA_ENCODER_LAUNCHER, CNA_XMA_ENCODER_ARGS. |
--font-directory <dir> | Add a directory to the .spritefont font search, ahead of the platform's own; repeat per directory. CNA_FONT_PATH in the environment is read after these. |
--build-configuration <name> | MSBuild's $(Configuration), Release unless given. It decides whether an effect that names no debug mode is compiled with debug information and without optimisation (Debug). |
clean <dir> | Remove the files the output manifest proves the pipeline owns, then the manifest. Files it did not write are left alone. |
Exit codes: 0 on success, 1 when discovery or any build item fails, 2 for invalid command-line syntax.
Per-asset configuration
Convention-only builds need no configuration. When you want to rename an asset, choose a non-default importer, processor or writer, set processor parameters, or pick a container for one asset, add a strict JSON file named .cna-content.json in the source root (or pass --config <file>). Unknown fields, duplicate entries, unsafe paths and badly typed values are refused when the file is parsed.
{
"format": "CNA.ContentPipeline.Config",
"version": 1,
"outputFormat": "cnb",
"assets": {
"Textures/wall.png": {
"logicalName": "Textures/wall",
"parameters": {
"colorKey": { "type": "string", "value": "255,0,255" },
"generateMipmaps": { "type": "bool", "value": true },
"premultiplyAlpha": { "type": "bool", "value": true }
}
},
"Models/level.glb": {
"parameters": {
"generateChildAssets": { "type": "bool", "value": true }
}
},
"Music/theme.wav": {
"importer": "CNA.WavSongImporter"
}
}
}
The example assumes those three source files exist; a configuration entry that names a missing file, or a file with no importer, fails the build. The Music/theme.wav entry asks for the Song reading of a WAV instead of the default SoundEffect.
| Key | Where | Meaning |
|---|---|---|
format | root, required | Must be "CNA.ContentPipeline.Config". |
version | root, required | The configuration format version, 1. |
outputFormat | root or asset | "cnb" or "xnb". Narrows from the command line's --format, to this project-wide default, to the one asset. |
sourceRoots | root | Read-only external source roots, by alias, for sources that live outside the primary source directory. |
assets | root, required | An object keyed by the source path relative to the source root, using /, without ... Each named file must exist and have an importer. |
logicalName, importer, processor, writer, outputFormat, parameters | per asset | Optional overrides. Importer, processor and writer take the stable component names, such as CNA.ImageImporter, CNA.TextureProcessor, CNA.Texture2DContentWriter. |
Each parameter is an object with a type and a value. The types are bool, i64, u64, f64 and string; the numeric types carry their value as a JSON string ({ "type": "u64", "value": "1200" }) so the persisted value is exact. Parameters take part in the build fingerprint, so changing one rebuilds that asset.
Texture parameters
| Parameter | Type | Default | Effect |
|---|---|---|---|
colorKey | string | none | "R,G,B" or "R,G,B,A" in decimal. Texels matching all four channels (alpha is 255 when you give three components) become fully transparent. |
textureFormat | string | NoChange | NoChange, Color, DxtCompressed, Dxt1, Dxt3 or Dxt5, case-insensitive. A .cnb build that asks for DXT keeps the uncompressed Rgba8 pixels and warns that CNB texture schema 1 stores Rgba8 only; build with --format xnb to get the compressed texture. |
generateMipmaps | bool | false | Generate a full mip chain. For .xnb output on the Reach profile every dimension must be a power of two. |
premultiplyAlpha | bool | true | Multiply colour by alpha, as XNA 4.0's TextureProcessor does by default. SpriteBatch::Begin()'s default blend state is the premultiplied AlphaBlend. Set false for straight alpha, for example when you select BlendState::NonPremultiplied. |
resizeToPowerOfTwo | bool | false | Round level zero up to powers of two. |
passThrough | bool | false | XNA's PassThroughProcessor behaviour: no colour key, resize or premultiply, and a block-compressed .dds keeps its own blocks. |
The order is fixed: colour key, resize, premultiply, mip chain, block compression. We found no premultiplication in the loose-file image decode path that ContentManager uses for a plain PNG, whereas a pipeline-built texture is premultiplied unless you turn that off, so decide on one alpha convention per project and check how an asset looks when it comes from each route.
XNA defaults versus native defaults. A native directory build applies no colour key unless you configure one. When a .contentproj (or an XNA-shaped component) names XNA's TextureProcessor, CNA maps XNA's own defaults onto the same parameters, including the magenta colour key 255,0,255 that XNA enables by default. If you are reproducing an existing XNA project, use its .contentproj rather than re-deriving the settings.
Other processor parameters
- Model (
.gltf,.glb):generateChildAssets(bool). Without it, a source that yields more than one Model document is refused with a message naming the parameter. With it, the extra Models, animation clips and extracted textures are published as deterministic<logical name>_…siblings. Not accepted for.xnboutput. Only the default glTF scene is imported. - Song:
streamReference(string, the root-relative media name),name(string),durationMs(u64). - Video:
streamReference,durationMs,width,height(u64),framesPerSecond(f64),soundtrackType(u64 0–2). - Effect source (
.fx):profile(reach/hidef),defines(NAME=VALUE;NAME),debug(bool).
Incremental builds
Every build reads and rewrites a manifest, .cna-content-manifest.json, in the output directory (manifest format version 9 at this snapshot, format identity CNA.ContentPipeline.Manifest). It records, per asset, the source and dependency hashes, the importer, processor and writer identities and versions, the parameters, and the SHA-256 of each published output. An asset is skipped only when all of those and the published output are unchanged.
[BUILD] <logical name> -> <output path> (<n> output(s), <bytes> bytes; <importer> -> <processor> -> <writer>)
reason: manifest unavailable (only with --explain)
[SKIP] <logical name> -> <output path>
reason: fingerprint and published output digests unchanged
Built: 1 Skipped: 1 Failed: 0
--explain reasons include manifest unavailable, new asset, primary source bytes changed, importer / processor / writer identity/version changed, processor parameters changed, source dependency bytes changed, content-build dependency effective fingerprint changed, compiled output missing and compiled output digest mismatch. Because component versions are part of the fingerprint, upgrading CNA rebuilds assets whose writers changed.
- Atomic publication. Each output is written to a temporary sibling and renamed over the destination (a replacing rename on POSIX,
MoveFileExWwith replace-existing on Windows), so a build whose process is killed or fails leaves the old file or the new file, never a prefix of the new one. Nothing is fsynced, so power loss or an operating-system crash is outside that guarantee. Staging happens in a private directory under the system temporary directory, and abandoned staging directories older than a day are scavenged. - One writer at a time. A lock file,
.cna-content.lock, in the output directory serialises builds and cleans; a second run reports that another content build or clean operation is active for the output root. - The manifest is written only when every item succeeded. A failed item is counted in
Failed:and the run exits with 1. - Nested builds. A model whose materials name textures starts nested texture builds, and a processor can declare content-build dependencies, so dependent assets rebuild when what they depend on changes.
- Clean.
cna-content cleandeletes only files the manifest proves it owns and that are unchanged, then the manifest, and printsCleaned: <n> Failed: <n>. It refuses a symlinked output directory.
CMake: cna_add_content()
CNA defines a CMake function that creates a build target running the same cna-content executable you would run by hand. It is available to any project that brings CNA in with add_subdirectory().
cna_add_content(
TARGET MyGameContent
SOURCE_DIR ContentSource # relative to the calling CMakeLists.txt
OUTPUT_DIR Content # relative to the calling binary directory
# optional:
# CONFIG_FILE ContentSource/pipeline.json
# WORKERS 4
# FORMAT xnb XNB_PLATFORM windows XNB_PROFILE reach XNB_COMPRESS lzx
# XNA_COMPATIBLE
# QUIET
# CONTENT_EXECUTABLE /path/to/host/cna-content
)
add_dependencies(MyGame MyGameContent)
| Argument | Meaning |
|---|---|
TARGET | Required. The new target's name; it must not already exist. |
SOURCE_DIR or CONTENT_PROJECT | Exactly one. SOURCE_DIR must be a directory; CONTENT_PROJECT must be an existing file ending in .contentproj. |
OUTPUT_DIR | Required. Resolved against the calling directory's binary directory. |
CONFIG_FILE | Optional. Must exist at configure time; passed as --config. |
WORKERS | Optional, 1 to 64, default 1. |
FORMAT | Optional, cnb or xnb; anything else is a configure error. |
XNB_PLATFORM, XNB_PROFILE, XNB_COMPRESS | Forwarded to the CLI, which is the one authority on valid names. They need FORMAT xnb. |
XNA_COMPATIBLE, QUIET | Flags forwarded as --xna-compatible and --quiet. |
CONTENT_EXECUTABLE | A host cna-content to run instead of the tree's own. Required when cross-compiling, because a target-platform executable cannot run on the build host. |
- The target is not part of
ALL. It is a custom target, so make your game depend on it withadd_dependencies()as shown, or build it by name. It depends oncna_content_tool, so the tool is built first. - It runs every time it is requested. The helper deliberately teaches CMake no second dependency model; the manifest makes an unchanged run a cheap per-asset no-op.
- A project file carries its own settings.
CONTENT_PROJECTrefusesFORMAT,XNB_*,CONFIG_FILEandXNA_COMPATIBLE, because accepting an option the file would silently override is worse than refusing it. - Where the output lands matters at run time. A relative
RootDirectoryis resolved like any relative path, against the process's working directory; run the game from the directory that containsContent/, or set an absolute root.
Optional dependencies
The pipeline itself is always compiled. What varies is which inputs the build machine can read. Each switch below only affects the build-time tool; none of them changes what a game links.
| Option | Default | Enables | Without it |
|---|---|---|---|
CNA_ENABLE_FONT_PIPELINE | AUTO (OFF/AUTO/ON) | FreeType, for the .spritefont route | A .spritefont build fails with a message saying so |
CNA_ENABLE_MEDIA_PIPELINE | AUTO | FFmpeg libraries, for build-time .mp3/.wma/.wmv reading and decoding. The libraries are found through the same probe as CNA_ENABLE_VIDEO, so that switch being OFF, or a Windows, Emscripten, Android or iOS target, leaves it unavailable. | Those sources report that the build has no media decoder |
| zlib | found automatically | Compressed arrays in binary FBX | A compressed FBX array is refused with a sentence saying so; text FBX is unaffected |
CNA_CNB_ZSTD | AUTO | Zstandard chunks in CNB, through the library and C ABI only | A compressed chunk is refused; see CNB Format |
CNA_ENABLE_DRACO | ON, OFF under Emscripten | KHR_draco_mesh_compression decoding in the glTF importer | Draco primitives throw at import time |
CNA_FXC_EXECUTABLE, CNA_FXC_LAUNCHER | empty cache strings | Bake a default external HLSL compiler (and launcher such as wine) into the tool | Discovery falls through to CNA_FXC and then PATH |
The XNA Content.Pipeline façade
For XNA porters the pipeline also exposes the XNA-shaped API, Microsoft::Xna::Framework::Content::Pipeline, as a view over the canonical engine (not a second engine). It contains the importer, processor, ContentCompiler, intermediate-serializer and build-task types XNA Game Studio 4.0 had, implemented in C++ under modules/content-pipeline.
| Part | What is there |
|---|---|
| Importers (10) | TextureImporter, WavImporter, Mp3Importer, WmaImporter, WmvImporter, EffectImporter, FontDescriptionImporter, XImporter, FbxImporter, XmlImporter |
| Processors (12) | TextureProcessor, SpriteTextureProcessor, ModelTextureProcessor, MaterialProcessor, ModelProcessor, EffectProcessor, FontDescriptionProcessor, FontTextureProcessor, SongProcessor, SoundEffectProcessor, VideoProcessor, PassThroughProcessor |
| Serialisation | IntermediateSerializer, IntermediateReader, IntermediateWriter (XNA intermediate XML) and ContentCompiler, ContentWriter, ContentTypeWriter |
| Tasks | BuildContent, CleanContent and ContentProject (the last reads a .contentproj); BuildXact is declared. CNA's report marks BuildContent, CleanContent and BuildXact as host substitutions, since MSBuild is not involved |
Representation, not behaviour. CNA's generated parity report (produced by tools/xna-pipeline-oracle/parity_report.py from a census of the XNA Game Studio 4.0 Refresh assemblies) counts 128 of 128 public and protected types and 705 of 705 members as represented, with 10/10 importers, 12/12 processors, 47/47 processor properties, 27/27 enum values and 18/18 source extensions marked implemented and tested, and zero missing. That is a statement that the symbols exist and are mapped, not that every output matches Microsoft's byte for byte. The report itself uses a status vocabulary (exact, semantic equivalent, host substitution) and says where a measurement could not be made; see Verification. Build behaviour is only as verified as the routes you actually use.
A .contentproj is read as MSBuild-free XML: <Compile> items are built, <Content> and <None> items are copied (honouring CopyToOutputDirectory and Link), and <Importer>, <Processor>, <ProcessorParameters_*>, platform, profile and compression are honoured. A conditional item group the reader cannot decide is refused by name rather than guessed at, and a project that names components this build lacks is refused with all of them listed while the rest are built. A <Reference> or <ProjectReference> to a game’s own pipeline assembly (anything other than Microsoft.Xna.Framework.*) is refused as well, because C++ cannot load a managed assembly: the build reports that it cannot load pipeline assemblies, still writes every asset whose importer and processor are built in, and then exits with status 1. Port such importers and processors to C++ and register them with RegisterXnaImporter<T> / RegisterXnaProcessor<T> in your own compiler (see Extending). Passing --xnb-platform, --xnb-profile or --xnb-compress overrides the project's own, the way an MSBuild property on the command line would.
./build/cna-content build MyGame/Content/Content.contentproj -o Content --build-configuration Release
To register your own XNA-shaped importer or processor with a registry, the bridge header CNA/Content/Pipeline/XnaPipelineBridge.hpp declares RegisterXnaImporter<T>(registry, className, attribute, version), RegisterXnaProcessor<T>(…) and RegisterXnaXnbOutput(registry, compiler, options). There is no assembly scanning in C++; PipelineComponentScanner enumerates what a registry holds. See Extending.
XNB output
--format xnb writes the container XNA's content pipeline produced. Every option below has a default chosen for maximum XNA 4.0 compatibility: Windows, container version 5, Reach profile, no compression, XNA-4.0 reader-name spelling.
./build/cna-content build ContentSource -o Content --format xnb \
--xnb-platform windows --xnb-version 5 --xnb-profile reach --xnb-compress lzx
| Option | Values | Notes |
|---|---|---|
--xnb-platform | windows (header byte w), windowsphone (m), xbox360 (x); extended identifiers desktopgl (d), linux (l), ios (i), android (a), windowsgl (g) | The first three are XNA 4.0 targets. The rest are identifiers used by other XNB ecosystems that XNA 4.0 itself never produced or consumed. xbox360 is refused unless --xnb-allow-unverified-xbox is given, which produces candidate files for testing on real hardware. |
--xnb-version | 5 (default), 4 | 5 is the XNA 4.0-era container; 4 is earlier legacy XNB and is offered only uncompressed. |
--xnb-profile | reach (default), hidef | Recorded in the header. Also enforced while building an .xnb: Reach limits a Texture2D to 2048 texels a side, HiDef to 4096, and Reach requires power-of-two sizes for mipmapped textures. A .cnb has no target profile and is not limited this way. |
--xnb-compress | none (default), lzx, lz4 | LZX is the compression XNA 4.0 itself wrote and the only compressed form an XNA 4.0 runtime loads; CNA's encoder is deterministic, one verbatim block per 32 KiB frame. LZ4 is a single raw block from a later ecosystem and is refused on an XNA 4.0 platform. |
--xnb-reader-names | xna40 (default), portable | xna40 spells reader names the way XNA 4.0 did, with assembly qualification. portable writes bare Microsoft.Xna.Framework.Content.*Reader names that CNA loads; a genuine XNA 4.0 runtime is not known to accept them. |
Writers exist for Texture2D, Texture3D, TextureCube, SpriteFont, SoundEffect, Song, Video, the vertex and index buffers, the five stock effects, compiled Effect, effect materials and external references, and Model, plus primitives, math types, Curve and a set of closed collections. Reading the same files back needs no registration inside a Game; see XNB Loading.
glTF to XNB is lossy, and says so. An XNA Model has no skeleton, animation, light or morph storage. Building a glTF to .xnb drops the skinning skeleton, animation clips, punctual lights and morph targets with a named warning each, downgrades a physically based material to the closest stock effect (base colour, emissive factor, specular colour factor and alpha survive), and refuses generated child assets. The CNB output of the same source keeps all of it. Warnings appear as indented warning (…) lines under the asset in the build output.
CNB output
The default output is one asset per .cnb file: a deterministic, little-endian container with a 64-byte header, a table of contents, CRC-32C checksums and typed payload chunks, plus two container chunks, metadata and a list of external asset names the file depends on (which is how a build script can ask what a Model needs, with cna_tool_cnb_info file.cnb --refs). It shares no code with XNB and has no reader tables. The byte layout, asset type identifiers, schema versions, read limits and the loading path are documented on CNB Format; Tutorial 146 writes and loads one from code.
Loading the result
Call sites do not change when the container does. ContentManager::Load<T>() tries, in order: the cache; <name>.xnb; <name>.cnb; a name that itself ends in .cnb; and then the type's loose-file reader (the literal path, a .cnj sidecar, then the reader's own extensions such as .png). So a compiled asset wins over the loose source it was built from, and a genuine .xnb wins over both.
void MyGame::LoadContent()
{
getContentProperty().setRootDirectoryProperty("Content"); // relative to the working directory
Texture2D wall = getContentProperty().Load<Texture2D>("Textures/wall");
Model robot = getContentProperty().Load<Model>("Models/robot");
SoundEffect boom = getContentProperty().Load<SoundEffect>("Sounds/explosion");
}
Load<T>returnsTby value; a failure throwsContentLoadException. From aGamethe manager isgetContentProperty(); there is noContentpointer.- The
.cnbloaders are built intoContentManager; nothing needs registering. AGamealso registers the built-in XNB readers in its constructor. - A
.cnbdeclares its own asset type, so requesting the wrong C++ type is aContentLoadExceptionnaming what the file holds. - Whether a
.cnbloads from a packaged-asset platform such as an Android APK is not something this page verifies: the.xnbtier reads through the platform's packaged-asset route, while the.cnbtiers use ordinary filesystem calls.
Extending the pipeline
You can add importers, processors and writers of your own by building your own copy of the compiler. The mechanism is source and toolchain compatibility, not a plugin ABI: nothing is loaded dynamically, and CNA marks the API experimental (ContentPipelineExtensionApiIsExperimental is true in CNA/Content/Pipeline/ContentPipeline.hpp). Registries are configured explicitly and frozen before discovery begins.
#include <filesystem>
#include <memory>
#include <vector>
#include "CNA/Content/Pipeline/ContentCompiler.hpp"
namespace Pipeline = CNA::Content::Pipeline;
// GreetingImporter : Pipeline::ContentImporter (Identity, SourceExtensions, OutputTypes, Import)
// GreetingProcessor: Pipeline::ContentProcessor (Identity, InputType, OutputType, ValidateParameters, Process)
// GreetingWriter : Pipeline::ContentTypeWriter (Identity, OutputSchemaIdentities, InputType, Write)
int main(int argc, char** argv) // use wmain on Windows, as the example does
{
std::vector<std::filesystem::path> arguments(argv + 1, argv + argc);
auto registry = std::make_shared<Pipeline::ContentPipelineRegistry>();
Pipeline::RegisterBuiltInContentPipeline(*registry); // every stock route
registry->RegisterImporter(std::make_shared<GreetingImporter>());
registry->RegisterProcessor(std::make_shared<GreetingProcessor>());
registry->RegisterWriter(std::make_shared<GreetingWriter>());
// Same coordinator as cna-content: discovery, configuration, fingerprints,
// incremental manifest, atomic publication, clean.
return Pipeline::RunContentCompiler(arguments, std::move(registry));
}
- Component identities are
{name, version}pairs; the version enters the incremental fingerprint, so bump it when a component's output changes. - A processor receives typed parameters (
bool,i64,u64,f64,string) and can add source dependencies, content-build dependencies, runtime references and deployment files through its context. - To honour
--fx-compilerand--fx-compiler-launcher, use theRunContentCompileroverload that takes a registry factory: the factory runs after the command line is parsed. Passing those options with a pre-built registry is refused rather than silently ignored. - A custom
.cnbasset type also needs a runtime loader:ContentManager::RegisterCnbLoaderEXT<T>(), with the identifier minted byCnbAssetTypeIdFromName(). Runnable examples:custom-content-compiler.cpp(the canonical API, targetcna_custom_content_compiler_example) andxna-custom-pipeline.cpp(the XNA-shaped API, targetcna_xna_custom_pipeline_example). Both examples build only underCNA_BUILD_EXAMPLESorCNA_BUILD_TESTS. Tutorial 148 covers custom types end to end.
Limits and what is not covered
| Limit | Detail |
|---|---|
| No Effect in CNB | CNB has no Effect schema, by design: a .cnb carrying Direct3D 9 bytecode would be unloadable on every renderer that is not Direct3D 9. Build .fx and .fxb with --format xnb. Loading compiled effects at run time also needs a renderer build that reports CompiledEffects; see Effects System. |
| No embedded HLSL compiler | .fx needs an external fxc-compatible program (Microsoft's legacy compiler, typically through wine off Windows). CNA does not ship one. |
Arbitrary .xml | XNA intermediate XML has no CNB form; it reaches .xnb only. A stray .xml file in a source directory is picked up as a source, so keep unrelated XML outside the source root or use --only-configured-assets. |
| CNB texture formats | Schema 1 encodes Rgba8 only. A request for DXT keeps Rgba8 and warns. |
| No CNB compression switch | Zstandard chunk compression exists in the CNB writer library and C ABI (when built with libzstd). cna-content has no option that enables it and no shipped tool does. |
| Song and Video are references | The .cnb holds metadata and a streaming reference; the media file is copied beside it, not embedded. Playback of Video still needs a media backend at run time. |
| glTF limits | Unit scale is always 1.0 (use cna_tool_gltf_to_cnb --unit-scale from Command-Line Tools when you need scaling). A multi-group source needs generateChildAssets. CNB model schema 1 has no fields for cameras or the import report, and a glTF that uses material variants (KHR_materials_variants) is refused by name when compiled to CNB rather than silently reduced; direct runtime glTF loading keeps all three. CNA's own notes list two further limits of this route, which we did not re-verify: only the default scene is imported, and named source roots are not honoured for glTF URI loading. |
| XMA | No XMA encoder ships; the Xbox 360 target is refused by default. Do not treat Xbox 360 output as verified. |
| No C ABI | The Content Pipeline is deliberately outside the C ABI (an owner decision recorded in CNA's C-API coverage report): cna_c_api does not link cna_content_pipeline. The CNB container itself does have C ABI functions. |
| Single-host, developer-build numbers | CNA's own benchmark notes for the pipeline are single-machine, Debug-build developer evidence and are not reproduced here as performance guarantees. |
Verification: what CNA reports
The statements in this section describe evidence CNA publishes about itself at this snapshot. The site did not re-run the oracle or the interop harness; treat them as CNA's claims with the scope stated.
Build success is not runtime proof. A produced asset must still be loaded, and graphics or audio content must be engaged on the renderer or device route you ship: an Effect, for instance, is written to .xnb on any host but loads only where the active renderer reports the CompiledEffects capability.
| Evidence | What it is, and what it is not |
|---|---|
| Test sources | By the site's counting method (macros TEST, TEST_F, TEST_P, TYPED_TEST, TYPED_TEST_P): modules/content/tests has 170 C++ test sources with 1,872 static definitions, and the build-time module modules/content-pipeline/tests has 47 sources with 487. Static definitions, not executed-and-passed counts; which of them run depends on the configuration. |
| XNA 4.0 interop harness | CNA's tests/interop/xna40/README.md reports that on 2026-09-06 six uncompressed fixtures, and after a fix six LZX-compressed fixtures, loaded through a genuine Microsoft XNA 4.0 ContentManager with every declared value matching. The host was Debian with the XNA 4.0 Refresh runtime in a Wine prefix and Direct3D 9 through DXVK, not Windows. Only six root types were exercised. CNA's own docs/xnb-interoperability.md, last edited three days earlier, still says the files had never been run against a real XNA runtime; the two documents disagree and we have not resolved it. No machine-readable log of the run is committed. |
| Independent checks without XNA | A Python conformance parser that shares no code with CNA (tools/xnb/xnb_conformance.py) validates the generated fixtures, and CNA reports a byte-identical match against genuine XNA 4.0 output for a List<string> and against MonoGame output for a Texture2D and a SoundEffect. |
| Parity report | The generated XNA Content Pipeline parity report gives the 128/705 representation counts and a per-route table of Windows, Windows Phone and Xbox 360 target legs. Its own caveats: the .wma and .wmv importers could not be measured against the genuine runtime (no Windows Media runtime under Wine), .mp3 was measured on Windows only, the .fx route was measured with Microsoft's legacy June 2010 fxc through Wine, several Windows Phone and Xbox 360 target legs are marked UNVERIFIED, and .wav for Xbox 360 reports XMA ENCODER EXTERNALLY UNAVAILABLE. |
| Final audit | CNA's generated docs/xna-content-pipeline-final-audit.md lists twenty-six completion conditions (API counts, importers, processors, product routes, differential cases, genuine-runtime tests, fuzzing, determinism, provenance, documentation) and reports “26 of 26 conditions hold”. tools/xna-pipeline-oracle/final_audit.py generates it and the CTest XnaPipelineFinalAuditIsGreen runs it. It is an existence audit: each row checks that its evidence file, named test or CTest registration is really there (the processor-defaults row instead runs another gate script and requires exit status 0), and the document itself says that whether a test passes is the test suite's question. Green therefore means every condition has something behind it, not that every condition passed (read at 009d40f5; not executed). |
Windows CI
One workflow, content-pipeline-windows-ci.yml, builds and tests the pipeline on a native Windows runner. What it does and does not do at this snapshot:
- Trigger. Manual (
workflow_dispatch) and pushes to a branch namedcontent-pipeline-final. It has no trigger onnext, so it is not part of ordinary automatic CI. - Environment.
windows-latest, native MSVC x64 with Ninja, Debug,CNA_PLATFORM=HEADLESSandCNA_GRAPHICS_RENDERER=HEADLESS, video and Draco off. It buildscna_content_toolandCnaContentTestsonly. - Test gate. A GoogleTest filter over the configuration, manifest, pipeline, registry, Texture2D, SoundEffect, CNJ, Model, Song, Video, CNB codec, golden-vector and XNB-pipeline suites, excluding runtime-loading cases.
- CLI gate. Copies a small fixture to a path containing non-ASCII characters, runs
cna-content build --workers 4 --explainand checks the[BUILD]line and the manifest unavailable reason; runs a second build and checks[SKIP]; runsclean; rebuilds and checks the output is byte-identical to the first build. Its last assertion greps the manifest for"version": 8, while the manifest version constant in the source at this snapshot is 9, so as written that step looks stale. We did not run the workflow. - Not covered. No graphics, audio device, media playback or GPU work; no D3D or GDI renderer; no
.spritefont(no FreeType) or FFmpeg route.
On Linux, the automatic platform-ci.yml workflow builds the content test executables in its SDL-free native X11 cell but runs only WAV and SoundEffect filters from them (*Adpcm*:*Wav* and *Wav*:*SoundEffect*). We did not trace which content-pipeline tests the unfiltered general-tests-ci.yml run registers, so this page makes no claim about them. See Verification & Known Issues for what CI gates across the project.
Where to go next. Tutorial 145: Build Content with cna-content (end to end), CNB Format, Tutorial 146: CNB packs, Tutorial 147: XNB interoperability, ContentManager, Model Loading, and Command-Line Tools for the other content tools. CNA's own long-form notes live in docs/content-pipeline.md; they are hand-written and were not checked line by line here (for example, they still mention manifest version 8).
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- sharp-runtime streams, UTF-8 text and tasks — Exact semantics of sharp-runtime's Stream and MemoryStream (as CNA's content streams use them), UTF8Encoding's validation and U+FFFD fallback, and the Task, WhenAll, WhenAny, TaskCompletionSource and Thread model.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-221: RunContentCompiler documentation says XNB output writers are registered only for --format xnb; they are registered on every run — ContentCompiler.hpp ties the coordinator's XNB writer registration to '--format xnb', but tools/content/content.cpp registers them unconditionally before freezing the registry.