Tutorial 45: ContentManager and Asset Pipeline
What you’ll learn
- The generic
ContentManager::Load<T>, which asset types it accepts, and the.xnb,.cnb, loose-file ladder it walks. RootDirectoryand how asset paths resolve.- Freeing assets with
Unload()on a state change. - The JSON descriptor formats CNA uses, and where compiled content comes from.
Before you start — Tutorial 08: Loading and Drawing Textures and Tutorial 09: Drawing Text with SpriteFont — this is the reference pass over the loader those two used informally.
CNA's ContentManager provides the same Load<T> interface as XNA 4.0, and it reads three kinds of content: compiled .xnb binaries, compiled .cnb containers (CNA's own format), and open file formats such as PNG, glTF, WAV, OGG and .cnj descriptors. The ladder is fixed: an .xnb wins over a .cnb, which wins over the loose files.
CNA is both an XNB/CNB loader and, at build time, a content pipeline. At run time ContentManager reads .xnb files that XNA, MonoGame or FNA produced — 61 built-in type readers (60 on toolchains without a native 128-bit integer), real LZX and LZ4 decompression, and two-pass shared-resource resolution — and its own .cnb files. New since alpha.1, the separate cna-content tool is a real importer, processor and writer pipeline (with the XNA Content.Pipeline classes behind it) that builds .cnb or .xnb from source files; nothing in a shipped game links it. See Content Pipeline and Tutorial 145.
A Game registers the built-in XNB readers for you. Its constructor calls CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders(), so LoadContent() can load built-in .xnb types with no setup. Only a ContentManager used outside a Game (a tool or a test) must make that call itself. Custom XNB types are supported: register a ContentTypeReader<T>, or declare one with ReflectiveTypeReaderBuilder<T> (there is still no run-time reflection). See Tutorial 148 and the XNB Loading & Interoperability reference.
ContentManager::Load<T> generic
#include "Microsoft/Xna/Framework/Content/ContentManager.hpp"
using namespace Microsoft::Xna::Framework::Content;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Audio;
// Inside Game::LoadContent():
auto& content = getContentProperty(); // returns ContentManager&
content.setRootDirectoryProperty("Content"); // under the title folder, else the working directory
// Load<T> returns T by value — not a pointer. A failed load throws
// ContentLoadException; there is no null to check.
// Texture2D — loads PNG/JPG/BMP from Content/textures/player.png
Texture2D tex = content.Load<Texture2D>("textures/player");
// Model — resolves Content/models/ship.xnb, then .cnb, then .cnj, then .gltf/.glb
Model model = content.Load<Model>("models/ship");
// SoundEffect — loads from Content/audio/explosion.wav
auto sfx = content.Load<SoundEffect>("audio/explosion");
// Song (streaming) — loads from Content/music/theme.ogg
auto song = content.Load<Song>("music/theme");
// SpriteFont — loads from Content/fonts/arial20.cnj
SpriteFont font = content.Load<SpriteFont>("fonts/arial20");
Supported asset types
| Type | File extension(s) | Notes |
|---|---|---|
Texture2D | .png .jpg .jpeg .bmp .gif .tga .tif .tiff .qoi | The image file is loaded directly and needs no descriptor. (An optional .cnj wrapper with a sourceFile and a colorKey exists; see Tutorial 111.) Decoded as straight, non-premultiplied alpha; cna-content’s texture processor premultiplies by default. |
Model | .xnb .cnb .cnj .gltf .glb | Tried in that order. .cnj is a JSON descriptor plus binary vertex/index sidecars, produced by the gltf_to_cnj tool; .cnb is the compiled form built by cna-content. There is no OBJ loader and no run-time FBX or .x importer (cna-content compiles those two at build time). |
SoundEffect | .wav | The WAV file is loaded directly into memory and needs no descriptor. Use for short clips. Never cached: each Load decodes a fresh instance. |
Song | .mp3 .ogg .wav .flac .opus .aac .wma | Streaming; only one Song plays at a time. The loader accepts each extension listed, but whether a given file plays depends on the decoders of the audio platform you built with (for example, .aac is not decoded everywhere). |
SpriteFont | .cnj | A .cnj descriptor naming a glyph-atlas texture, which is itself loaded through ContentManager. |
std::shared_ptr<Effect> | .xnb, .cnj | XNB EffectReader loads XNA/FNA D3D9 Effect Framework bytecode when the active renderer supports compiled effects. A .cnj descriptor can instead name renderer-native sources for ShaderEffect (load as std::shared_ptr<Effect>, then downcast). HLSL .fx source, DXBC, and MGFX are not interchangeable with the supported Effect Framework binary; cna-content can compile .fx to an .xnb only through an external fxc-compatible compiler, and there is no .cnb form of an Effect. |
TextureCube | .dds | Compressed cube maps are limited to DXT1, DXT3 and DXT5. |
std::shared_ptr<Texture3D> | .cnj | Move-only type, so it comes back through a shared pointer. .xnb and .cnb volumes load too. |
std::shared_ptr<SkinnedModelEXT> | .skinnedmodel.json | CNAEXT type; see Tutorial 112 for the Model-based path most games use. |
Video | .mp4 .ogv .webm .mkv .avi .mov | The types (Video, VideoPlayer, the XNB VideoReader) exist in every build. Decoding needs the optional FFmpeg backend (CNA_ENABLE_VIDEO), which is never built on Windows, Emscripten, Android or iOS; without it, playback throws NotSupportedException rather than failing to link. |
ContentManager::RootDirectory
// The manager inside a Game (and ContentManager()) starts at "Content".
// ContentManager(IServiceProvider*) alone starts with an empty root, as XNA does.
// A relative root is looked up under the game's title folder (TitleLocation.Path)
// first, as in XNA, and otherwise relative to the process's working directory.
content.setRootDirectoryProperty("Content");
// Nested managers per state (one content manager per game screen)
ContentManager levelContent(&getServicesProperty(), "Content/Level1");
ContentManager::Unload()
Unload() clears the cache of every asset loaded through that ContentManager instance. Call it when transitioning between game states so the manager stops holding the assets. Because Load<T> returns copies whose GPU state is shared-owned, memory is actually reclaimed once the manager’s cache and every copy you kept are gone — reset your own holders too.
// On screen exit:
levelContent.Unload();
// The manager no longer caches anything it loaded for this screen.
// Copies you still hold remain valid until you drop them; pointers you took
// into them (for example a Texture2D* given to an effect) are yours to clear.
JSON descriptor formats
Only some content types have a descriptor. Texture2D and SoundEffect do not — the image or WAV file is read directly, and everything you might expect a descriptor to configure is set on the object in C++ instead. The types that do use one all share CNA's single .cnj document format, which carries a cnjVersion and a type that must match the C++ type you ask for.
Texture2D — no descriptor
There is no .texture.json. Load<Texture2D> resolves the asset name against .png, .jpg, .jpeg, .bmp, .gif, .tga, .tif, .tiff and .qoi, and decodes whichever it finds. Sampling and wrapping are not asset properties in XNA at all — they are device state you set at draw time:
Texture2D tex = content.Load<Texture2D>("textures/terrain");
// Wrapping and filtering are sampler state, not file metadata.
gd.getSamplerStatesProperty()[0] = SamplerState::LinearWrap;
SoundEffect — no descriptor
There is no .sound.json. Load<SoundEffect> reads a .wav file and nothing else. Volume, pitch and pan are per-playback values, so they are arguments to Play or properties of a SoundEffectInstance:
SoundEffect sfx = content.Load<SoundEffect>("audio/explosion");
// volume, pitch, pan — set at the call site, not in a file.
sfx.Play(0.8f, 0.0f, 0.0f);
.cnj (Model)
Geometry lives in the binary sidecars the descriptor names; the JSON just wires them together. Generate it with gltf_to_cnj rather than writing it by hand — see Tutorial 35.
// Content/models/ship.cnj
{
"cnjVersion": 1,
"type": "Model",
"meshes": [
{ "name": "Hull", "vertices": "ship_body_verts.bin", "indices": "ship_body_idx.bin", "vertexStride": 32, "effect": "BasicEffect" },
{ "name": "Engine", "vertices": "ship_engine_verts.bin", "indices": "ship_engine_idx.bin", "vertexStride": 32, "effect": "BasicEffect" }
]
}
.cnj (SpriteFont)
texture names the glyph atlas and is required — a descriptor without it raises a ContentLoadException. That name goes back through ContentManager, so the atlas is resolved and cached like any other texture. Each glyph carries source (its rectangle in the atlas), crop (the offset and size used when drawing), and kerning as XNA's three floats: left bearing, advance width, right bearing.
// Content/fonts/arial20.cnj
{
"cnjVersion": 1,
"type": "SpriteFont",
"texture": "fonts/arial20_atlas",
"lineSpacing": 24,
"spacing": 0.0,
"defaultCharacter": "?",
"glyphs": [
{ "char": 32, "source": [0, 0, 6, 20], "crop": [0, 0, 6, 20], "kerning": [0.0, 6.0, 0.0] },
{ "char": 65, "source": [10, 0, 14, 20], "crop": [0, 0, 14, 20], "kerning": [0.0, 14.0, 0.0] }
]
}
.cnj (Effect)
The Effect descriptor has exactly two shader fields, vertex and fragment, each naming a GLSL source file relative to the content root, not to the descriptor's own folder. Missing either raises a ContentLoadException. Load it as Effect — the type the reader is registered for — then downcast to ShaderEffect. See Tutorial 52.
// Content/effects/wave.cnj (the shader paths are written from Content/, not from Content/effects/)
{
"cnjVersion": 1,
"type": "Effect",
"vertex": "effects/wave.vert",
"fragment": "effects/wave.frag"
}
Compiled content: .cnb and .xnb
Every loose format above can also be compiled ahead of time. The cna-content build tool turns a folder of PNGs, WAVs, glTF models, .cnj descriptors and more into .cnb files (the default) or .xnb files, keeping the same logical names, so the code in this tutorial does not change — the manager simply finds the compiled file first:
cna-content build ContentSource -o Content # .cnb, one per asset
cna-content build ContentSource -o Content --format xnb # XNA-compatible .xnb
Because compiled files outrank loose ones, a stale .cnb beside an edited .png keeps winning until you rebuild it. Tutorial 145 covers the workflow, incremental rebuilds and the CMake helper; Tutorial 146 covers the .cnb container itself.
Code example: load multiple assets, unload on state change
class MultiAssetGame final : public Game {
public:
MultiAssetGame() : graphics_(this) {}
protected:
void Initialize() override {
Game::Initialize();
state_ = GameState::Menu;
}
void LoadContent() override {
// Menu assets use the global ContentManager
auto& shared = getContentProperty();
shared.setRootDirectoryProperty("Content");
menuFont_ = shared.Load<SpriteFont>("fonts/title");
menuBg_ = shared.Load<Texture2D>("ui/menu_bg");
// Game-level assets in a separate manager so they can be unloaded
gameContent_ = std::make_unique<ContentManager>(
&getServicesProperty(), "Content/Game");
playerTex_ = gameContent_->Load<Texture2D>("player");
enemyTex_ = gameContent_->Load<Texture2D>("enemy");
shootSfx_ = gameContent_->Load<SoundEffect>("shoot");
levelModel_ = gameContent_->Load<Model>("level01");
}
void Update(GameTime& gameTime) override {
auto kb = Keyboard::GetState();
if (state_ == GameState::Menu && kb.IsKeyDown(Keys::Enter)) {
state_ = GameState::Playing;
}
if (state_ == GameState::Playing && kb.IsKeyDown(Keys::Escape)) {
// Free all in-game assets; menu assets remain loaded
gameContent_->Unload();
playerTex_ = {};
enemyTex_ = {};
shootSfx_.reset();
levelModel_ = {};
state_ = GameState::Menu;
}
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::Black);
// ... draw based on state_ ...
// No gd.Present(): Game presents in EndDraw, after Draw() returns.
}
private:
enum class GameState { Menu, Playing };
GameState state_ = GameState::Menu;
GraphicsDeviceManager graphics_;
std::unique_ptr<ContentManager> gameContent_;
Texture2D menuBg_;
Texture2D playerTex_;
Texture2D enemyTex_;
Model levelModel_;
std::optional<SpriteFont> menuFont_; // no default ctor
std::optional<SoundEffect> shootSfx_; // no default ctor
};
Load<T> returns the asset by value, so your member is your copy — there is no shared handle to keep alive. How you declare that member depends on the type: Texture2D and Model are default-constructible, so a plain value member works and is what CNA's own examples use. SpriteFont, Song and SoundEffect are not, so wrap those in std::optional<T> and take the address with &*font_ when an API wants a pointer.
After Unload() the manager's cached copy is gone; reset your holders as shown above so the next state change reloads cleanly.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- ContentManager resolution, caching and failure rules — Which file ContentManager::Load<T> actually reads, what RootDirectory does and does not confine, what the cache keeps, and which exception each tier throws at this snapshot.