ContentManager
Implementation status: The Content namespace is roughly 80% functional: core Load<T>() and Unload() work for all built-in types, and CNA now has a real .xnb reader wired straight into ContentManager — see XNB Content Pipeline. IServiceProvider wiring is simplified; Game::Services is available but not all XNA services are pre-registered. Two specific caveats: ResourceContentManager is a pure stub, and Unload() clears the manager's internal maps without disposing the assets they held.
Overview
ContentManager lives in the Microsoft::Xna::Framework::Content namespace and is the standard mechanism for loading typed game assets from disk. It mirrors the XNA 4.0 API: call Load<T>(assetName) with a path relative to RootDirectory, and the manager returns a reference to the loaded asset. Assets are cached internally; repeated calls with the same name return the same object without re-reading the file.
CNA supports both asset paths. It reads the XNA Content Pipeline binary format (.xnb) through a real reader, and it also loads loose files directly: complex asset types such as fonts, models, and shaders can be described by small JSON descriptor files, while simple image and audio formats are read straight from their native container (PNG, WAV, OGG, etc.) with no intermediate build step.
A ContentManager is constructed with an IServiceProvider& and a rootDirectory string. In a typical Game subclass the instance is accessed through this->Content, which is pre-configured with the game's service provider and a default root of "Content".
// Typical construction (done for you inside Game)
ContentManager content(services, "Content");
// Or construct manually
ContentManager myContent(game.Services, "Assets/Levels/Level1");
Two asset paths: .xnb first, loose files second
For any given asset name, ContentManager prefers a .xnb when one exists and falls back to the loose-file path otherwise. Call sites do not change: Load<Texture2D>("textures/player") resolves to textures/player.xnb if that file is present, and to textures/player.png if it is not.
This means content built by the original Microsoft tooling (or by MonoGame's mgcb) can be dropped into a project as-is, and mixed freely with loose assets in the same content tree. Migration can go either way and one asset at a time.
One required startup call. The XNB type-reader registry is empty by default. Call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() once before loading XNB content. The tag provides 50 built-in readers with FFmpeg and 49 without video; custom readers can be registered explicitly even though reflection-based discovery is absent. See XNB Content Pipeline.
Core API
| Member | Signature | Description |
|---|---|---|
Load<T>() |
T& Load<T>(const std::string& assetName) |
Loads and caches an asset of type T. Returns a reference to the cached object. Subsequent calls with the same name return the same instance without re-reading the file. |
Unload() |
void Unload() |
Disposes all cached assets and frees their GPU/audio resources. The manager can be reused after calling Unload(). |
RootDirectory |
std::string RootDirectory |
Read/write property. The base path prepended to every assetName passed to Load<T>(). |
ServiceProvider |
IServiceProvider& ServiceProvider |
Read-only. The service provider supplied at construction time. Used by built-in readers to obtain the GraphicsDevice and audio services. |
Load<T> resolution order
When you call content.Load<T>("path/to/asset"), the manager:
- Checks the internal cache; returns the existing instance if found.
- Constructs the full file path as
RootDirectory + "/" + assetName. - Looks up the registered
ContentTypeReader<T>for the typeT. - Delegates to
ReadAsset(fullPath)on the reader. - Stores the result in the cache and returns a reference to it.
For types that map directly to a file container (e.g. Texture2D from a PNG), no extension is required in the asset name — the reader appends the appropriate extension automatically. For descriptor-described types the asset name must resolve to the .cnj file, with or without the extension.
Built-in content type readers
Type T |
File format(s) | Notes | Status |
|---|---|---|---|
Texture2D |
.png .jpg .jpeg .bmp .gif .tga .tif .tiff .qoi |
Decoded via SDL3_image. Textures are cached by weak reference, so GPU memory is released once your last copy goes out of scope. | Implemented |
TextureCube |
.dds |
Real DDS parser for cube maps. DDS is the only accepted container. | Implemented |
SpriteFont |
.cnj |
A .cnj descriptor naming a glyph-atlas texture, which is itself resolved through ContentManager. See format below. |
Implemented |
Model |
.cnj, .xnb |
CNA's primary model path is .cnj, produced offline from glTF 2.0 by the gltf_to_cnj tool. The XNB model reader family is also registered. See Model Loading. |
Implemented |
SkinnedModelEXT |
.cnj |
CNA extension (not an XNA type) for skinned meshes, carrying a skeleton and AnimationClipEXT data. |
Implemented |
Effect / ShaderEffect |
.cnj |
A .cnj descriptor naming renderer-native vertex and fragment sources, loaded as a ShaderEffect. This is separate from XNA/FNA D3D9 Effect Framework binaries (.fxb or XNB Effect payloads), which require a renderer build advertising CompiledEffects. CNA does not compile HLSL .fx source. See Effects System. |
Implemented |
SoundEffect |
.wav only |
Short clips loaded fully into memory via SDL3_mixer. Note the asymmetry: unlike Song, the SoundEffect loader accepts WAV and nothing else. Compressed formats must be routed through Song/MediaPlayer or decoded yourself. |
Implemented |
Song |
.mp3 .ogg .wav .flac .opus .aac .wma |
Streamed music via SDL3_mixer. Only one Song plays at a time through MediaPlayer. |
Implemented |
Video |
MP4, OGV, WEBM, MKV, AVI, MOV | Decoded via FFmpeg and played through VideoPlayer. FFmpeg is unavailable on Emscripten, Android and every Windows target — effectively native Linux only. There is no XNB VideoReader. |
Linux only |
JSON descriptor formats
Alongside the .xnb reader, CNA supports a descriptor for the loose-file path. The types that use one all share CNA's single .cnj document format — a plain JSON file that lives alongside your assets, carries a cnjVersion and a type that must match the C++ type you ask for, and references the actual data files by name. There is no offline build step.
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. There is no .texture.json and no .sound.json. Sampling and wrapping are device state you set at draw time, and volume, pitch and pan are arguments to Play.
.cnj — SpriteFont
Describes a pre-rendered glyph atlas. texture names the 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 entry in the glyphs array maps a code point to its position and metrics within the atlas.
{
"cnjVersion": 1,
"type": "SpriteFont",
"texture": "fonts/arial32_atlas",
"lineSpacing": 36,
"spacing": 0.0,
"defaultCharacter": "?",
"glyphs": [
{ "char": 32, "source": [0, 0, 6, 30], "crop": [0, 0, 6, 30], "kerning": [0.0, 6.0, 0.0] },
{ "char": 65, "source": [10, 0, 22, 30], "crop": [0, 0, 22, 30], "kerning": [0.0, 23.0, 0.0] }
]
}
Field reference for each glyph entry:
| Field | Type | Description |
|---|---|---|
char | int | The numeric code point this glyph represents. |
source | [x, y, w, h] | The glyph's rectangle inside the atlas. |
crop | [x, y, w, h] | The offset and size used when drawing. |
kerning | [float, float, float] | XNA's three floats: left bearing, advance width, right bearing. |
See Tutorial 09: SpriteFont for a worked example.
.cnj — Model
CNA's actual model content path is the offline gltf_to_cnj tool (tools/gltf_to_cnj/, CMake target cna_tool_gltf_to_cnj), which converts glTF 2.0 into a .cnj descriptor plus binary vertex/index sidecars, carrying the Model and its AnimationClip data. .gltf and .glb files can also be loaded directly, and pipeline-built models read straight from .xnb. See Model Loading and Tutorial 35.
.cnj — Effect / ShaderEffect
The Effect descriptor has exactly two shader fields, vertex and fragment, each naming a GLSL source file relative to the descriptor. Missing either raises a ContentLoadException. There is no uniform declaration list — ShaderEffect has no Parameters collection; uniforms are set by name with SetUniformMat4/Vec4/Vec3/Vec2/Float/Int/FloatArray. Load it as Effect — the type the reader is registered for — then downcast to ShaderEffect.
{
"cnjVersion": 1,
"type": "Effect",
"vertex": "my_effect.vert",
"fragment": "my_effect.frag"
}
See Tutorial 52: Writing Custom Shaders.
Custom ContentTypeReader<T>
You can extend ContentManager to load any custom type by implementing a ContentTypeReader<T> and registering it. This matches the extensibility model in XNA 4.0, though the registration mechanism is simpler in CNA (no reflection or attribute discovery).
1 — Implement the reader
#include <CNA/Content/ContentTypeReader.hpp>
struct LevelData {
std::string name;
int width, height;
std::vector<int> tiles;
};
class LevelDataReader : public ContentTypeReader<LevelData> {
public:
std::unique_ptr<LevelData> ReadAsset(const std::string& assetPath) override {
// Parse your custom format however you like
auto level = std::make_unique<LevelData>();
// ... read assetPath, fill level fields ...
return level;
}
};
2 — Register the reader
// Register before the first Load<LevelData>() call
ContentManager::RegisterReader<LevelData>(std::make_shared<LevelDataReader>());
3 — Load as usual
LevelData& level = content.Load<LevelData>("levels/level01.lvl");
Registered readers are global per type; you only need to register once per application lifetime. Built-in readers for the types listed in the table above are pre-registered by CNA and cannot be overridden (attempting to register a reader for a built-in type throws std::runtime_error).
Custom types are loose-file only. A ContentTypeReader<T> registered as above serves the loose-file path. It does not extend the .xnb reader: XNA deserialised user-defined types by reflection, and CNA has no ReflectiveReader equivalent, so custom types cannot be loaded from an .xnb at all. Keep custom asset types as raw files or JSON descriptors, or convert them at build time. See Known gaps.
Code examples
Example 1 — Loading Texture2D and SoundEffect in LoadContent()
void MyGame::LoadContent() {
// Texture: loads Content/textures/player.png
Texture2D& playerTex = Content.Load<Texture2D>("textures/player");
// Sound effect: loads Content/audio/jump.wav
SoundEffect& jumpSound = Content.Load<SoundEffect>("audio/jump");
// Song for background music: loads Content/music/theme.ogg
Song& bgMusic = Content.Load<Song>("music/theme");
MediaPlayer::Play(bgMusic);
MediaPlayer::IsRepeating = true;
}
Example 2 — Loading a Model
// Resolves Content/models/house.xnb if present,
// otherwise the .cnj produced by gltf_to_cnj
Model& houseModel = Content.Load<Model>("models/house");
// Draw all meshes with their associated BasicEffect
houseModel.Draw(worldMatrix, camera.View(), camera.Projection());
Example 3 — Custom ContentTypeReader<T> (full round-trip)
#include <CNA/Content/ContentTypeReader.hpp>
#include <fstream>
#include <nlohmann/json.hpp>
struct TileMap { int width, height; std::vector<int> tiles; };
class TileMapReader : public ContentTypeReader<TileMap> {
public:
std::unique_ptr<TileMap> ReadAsset(const std::string& path) override {
std::ifstream f(path);
auto j = nlohmann::json::parse(f);
auto m = std::make_unique<TileMap>();
m->width = j["width"];
m->height = j["height"];
m->tiles = j["tiles"].get<std::vector<int>>();
return m;
}
};
// Registration — call once, e.g. in Game constructor or Initialize()
ContentManager::RegisterReader<TileMap>(std::make_shared<TileMapReader>());
// Usage in LoadContent()
TileMap& map = Content.Load<TileMap>("maps/world1.tilemap.json");
Example 4 — Example SpriteFont .cnj (full file)
{
"cnjVersion": 1,
"type": "SpriteFont",
"texture": "fonts/ui_16_atlas",
"lineSpacing": 20,
"spacing": 0.0,
"defaultCharacter": "?",
"glyphs": [
{ "char": 32, "source": [0, 0, 0, 0], "crop": [0, 0, 0, 0], "kerning": [0.0, 5.0, 0.0] },
{ "char": 33, "source": [0, 0, 4, 14], "crop": [0, 3, 4, 14], "kerning": [0.0, 5.0, 0.0] },
{ "char": 65, "source": [4, 0, 12, 14], "crop": [0, 3, 12, 14], "kerning": [0.0, 13.0, 0.0] },
{ "char": 66, "source": [16, 0, 11, 14],"crop": [1, 3, 11, 14], "kerning": [1.0, 12.0, 0.0] },
{ "char": 97, "source": [27, 0, 10, 11],"crop": [0, 6, 10, 11], "kerning": [0.0, 11.0, 0.0] },
{ "char": 98, "source": [37, 0, 10, 14],"crop": [1, 3, 10, 14], "kerning": [1.0, 11.0, 0.0] },
{ "char": 48, "source": [47, 0, 11, 14],"crop": [0, 3, 11, 14], "kerning": [0.0, 12.0, 0.0] }
]
}
ServiceProvider note
In XNA 4.0, ContentManager accepted an IServiceProvider so that content readers could resolve services (graphics device, audio engine, etc.) at load time. CNA retains this constructor signature for source compatibility, but the service locator implementation is simplified. Built-in readers obtain the GraphicsDevice through a direct reference rather than a service lookup. Game::Services is available and accepts user-registered services, but not all XNA services (IGraphicsDeviceService, IGraphicsDeviceManager) are automatically pre-populated.