Tutorial 146: Write and Load CNB Files
What you’ll learn: How to produce .cnb assets with cna_tool_source_to_cnb and cna_tool_cnj_to_cnb, inspect and validate them with cna_tool_cnb_info, write your own with CnbWriter (including a game-defined asset type), and load them with getContentProperty().Load<T>() — and which file wins when .xnb, .cnb and loose files coexist.
Before you start — Tutorial 03: Your First CNA Window (a CMake project that links CNA) and Tutorial 45: ContentManager and Asset Pipeline (Load<T> and the content root). Read the CNB Format reference alongside this tutorial if you want the byte-level detail behind each step.
Use CNA's next branch. .cnb and every tool in this tutorial exist at CNA snapshot 009d40f5 only. A plain git clone https://github.com/libcna/cna.git gives v0.1.0-alpha.1, which has none of them; clone with git clone -b next https://github.com/libcna/cna.git (or check out 009d40f5dd085c4e674d3479675fac84b12b3e0a), and use sharp-runtime's next branch as well. The commands below were checked against that snapshot's source, but CNA itself was not built for this tutorial, so the sample console output is written from the tools' print statements, not pasted from a run.
What you will build
A content folder in which every asset is a single compiled file, produced three different ways, then loaded by a game:
Source/ your original files (PNG, WAV, OGG, ...)
Content/
Textures/wall.cnb Texture2D, built by cna_tool_source_to_cnb (with a mip chain)
Textures/wall_keyed.cnb Texture2D, built from a .cnj by cna_tool_cnj_to_cnb (with a colour key)
Textures/checker.cnb Texture2D, written from C++ with the CNB codec
Audio/jump.cnb SoundEffect, built from a WAV
Music/theme.cnb Song: metadata plus a reference to Content/Music/theme.ogg
Levels/level01.cnb "MyGame.LevelInfo": a game-defined asset type, written with CnbWriter
You will inspect the results with cna_tool_cnb_info and load them through getContentProperty().Load<T>(). The producers are headless: they create no window, GraphicsDevice or audio device, so they run on a build server. Only the final game needs a renderer.
1. Build the tools
The CNB tools are ordinary CMake targets that CNA builds by default (there is no option that gates them). Build just the ones you need from your existing CNA build directory:
cmake --build <your-build-dir> --target cna_tool_source_to_cnb cna_tool_cnj_to_cnb cna_tool_cnb_info
# optional, for glTF models:
cmake --build <your-build-dir> --target cna_tool_gltf_to_cnb
Each target produces an executable of the same name in your build tree (find them with find <your-build-dir> -name 'cna_tool_*' -type f). The examples below assume they are on your PATH. For the general build workflow see Building.
2. Compile source files with cna_tool_source_to_cnb
One front end handles every source kind; the extension decides what comes out. A texture with a full mip chain:
cna_tool_source_to_cnb Source/wall.png Content/Textures/wall.cnb --mipmaps --mip-color-space srgb --name Textures/wall
On success it prints one line of the form <output>: <what it built>, <bytes> bytes, for example:
Content/Textures/wall.cnb: Texture2D 256x256 Rgba8, 9 levels, <bytes> bytes
(The dimensions and byte count are those of your PNG; a 256×256 image has 9 levels.) The options you will use most:
| Option | Applies to | Effect |
|---|---|---|
--mipmaps | Texture2D | Generates a full box-filtered mip chain (floor(log2(max(w,h)))+1 levels). Without it the file has a single level. |
--mip-color-space linear|srgb | Texture2D | How the averaging is done. Use srgb for colour maps; leave the default linear for normal, roughness and mask maps. |
--color-key R,G,B | Texture2D | Pixels of exactly this colour get alpha 0 (colour kept). Never applied unless you ask. |
--name <logical> | all | The content name recorded in the file's CMET chunk. Provenance only; defaults to the input file's stem. |
--as texture2d|soundeffect|song|texturecube|video | all | Forces the output kind when the extension is not enough (required for .ogg, .mp3 and other media). |
A sound effect and a song:
cna_tool_source_to_cnb Source/jump.wav Content/Audio/jump.cnb
cna_tool_source_to_cnb Source/theme.ogg Content/Music/theme.cnb \\
--as song --stream Music/theme.ogg --title "Theme" --duration-ms 95000
cp Source/theme.ogg Content/Music/theme.ogg # the media itself is NOT inside the .cnb
A Song (or Video) .cnb holds metadata plus a reference to the media file, never the audio. --stream is a name relative to the content root, and you must ship that file yourself. Duration (and, for video, --frame-size WxH and --fps) are arguments because CNA has no headless multimedia decoder to read them from the file. The tool refuses options that do not fit the kind, so a mistake is loud:
$ cna_tool_source_to_cnb Source/jump.wav Content/Audio/jump.cnb --mipmaps
error: --mipmaps does not apply to a soundeffect asset.
Two tools, two alpha policies. cna_tool_source_to_cnb stores the pixels CNA's loose-file image loader would decode from the same PNG, with no alpha premultiplication. The cna-content build tool (Tutorial 145) runs a TextureProcessor whose premultiplyAlpha parameter defaults to true. If you mix the two on textures that draw with the same blend state, decide which convention you want and pass the parameter explicitly.
3. Compile a .cnj descriptor with cna_tool_cnj_to_cnb
.cnj is CNA's editable JSON descriptor format (see Tutorial 111). cna_tool_cnj_to_cnb compiles a .cnj and the files it names into one .cnb. It supports eight types — Curve, AnimationClip, Model, Texture2D, Texture3D, TextureCube, SpriteFont and SoundEffect — and refuses any other "type" by name. A Texture2D descriptor names an image with sourceFile (relative to the .cnj) and can carry a colour key:
// Content/Textures/wall_keyed.cnj
{
"cnjVersion": 1,
"type": "Texture2D",
"sourceFile": "wall.png",
"colorKey": [255, 0, 255]
}
cna_tool_cnj_to_cnb Content/Textures/wall_keyed.cnj Content/Textures/wall_keyed.cnb
Wrote Content/Textures/wall_keyed.cnb (Microsoft.Xna.Framework.Graphics.Texture2D, <bytes> bytes)
absorbed 2 source file(s):
wall_keyed.cnj
wall.png
“Absorbed” files are the ones whose contents are now inside the .cnb and no longer need to ship. Assets that can be shared, such as the textures of a model, are not absorbed: they stay outside and the tool lists them as external references. If wall.png lives elsewhere, pass --content-root <dir>; a sourceFile may not be absolute or escape that root. The colour key must be exactly three integers in 0–255 or the compile fails (a malformed key is an error, not something silently clamped).
For glTF models there is a direct route, cna_tool_gltf_to_cnb <input.gltf|glb> <outputDir> <baseName> [--unit-scale <f>], which writes one Model .cnb per skin and prints <file>: <asset type>, <bytes> bytes, <n> file(s) absorbed. It is the only place a non-1.0 unit scale can be applied (the runtime glTF path is fixed at 1.0). Textures a model uses are recorded by name in its XREF table — the next step shows how to list them. See Model Loading for what a model .cnb does and does not carry.
4. Inspect and validate with cna_tool_cnb_info
cna_tool_cnb_info knows nothing about any asset schema, so it can describe a texture, a model or a type your own game defines equally well. Reading a file applies every structural check the runtime would, so it doubles as a validator.
cna_tool_cnb_info Content/Textures/checker.cnb
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...
TEXD required 17056 4096 4096 16 none 0x...
... (five smaller mip levels)
external references: 0
That listing is the expected shape for the 64×64, seven-level checker.cnb you write in step 5 (offsets and sizes derived from the writer's layout rules; the checksum column shows values that depend on your pixels). Other modes:
| Command | Prints |
|---|---|
cna_tool_cnb_info a.cnb --chunks | Only the chunk table. |
cna_tool_cnb_info a.cnb --refs | Only the external logical names, one per line — a build script can turn that into a dependency list without knowing the schema, e.g. cna_tool_cnb_info Content/Music/theme.cnb --refs prints Music/theme.ogg. |
cna_tool_cnb_info a.cnb --quiet | Nothing. Use the exit code: 0 valid, 1 malformed (the reason goes to stderr), 2 usage error. |
# A content gate for a build script:
for f in $(find Content -name '*.cnb'); do
cna_tool_cnb_info "$f" --quiet || { echo "bad asset: $f"; exit 1; }
done
5. Write CNB from C++
The tools are thin shells over a library, so your own tool can do the same. The codec headers live under CNA/Content/Cnb/. This program writes two files with no source-format importer involved: a built-in Texture2D (through the ready-made EncodeTexture2DToCnb()) and a game-defined asset type (through CnbWriter, the general container writer). The level file names the checker texture as an external reference instead of embedding it, and, when your CNA build has Zstandard, compresses its chunks.
// MakeAssets.cpp - writes two .cnb files without any source-format importer:
// Content/Textures/checker.cnb a built-in Texture2D, with a generated mip chain
// Content/Levels/level01.cnb a game-defined asset type, "MyGame.LevelInfo"
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "CNA/Content/Cnb/CnbByteWriter.hpp"
#include "CNA/Content/Cnb/CnbChunkCompression.hpp"
#include "CNA/Content/Cnb/CnbFormat.hpp"
#include "CNA/Content/Cnb/CnbTextureCodec.hpp"
#include "CNA/Content/Cnb/CnbWriter.hpp"
using namespace CNA::Content::Cnb;
namespace
{
void WriteFile(const std::filesystem::path& path, const std::vector<std::uint8_t>& bytes)
{
// CnbWriter::WriteToFile() does not create directories, and the Encode*ToCnb() helpers
// return bytes only, so both routes need the folder to exist first.
std::filesystem::create_directories(path.parent_path());
std::ofstream file(path, std::ios::binary | std::ios::trunc);
file.write(reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
if (!file) { throw std::runtime_error("cannot write " + path.string()); }
std::cout << "wrote " << path.string() << " (" << bytes.size() << " bytes)\n";
}
// 64x64 RGBA8 checkerboard: 8x8-pixel squares, red and white.
std::vector<std::uint8_t> CheckerPixels(std::uint32_t size)
{
std::vector<std::uint8_t> rgba(static_cast<std::size_t>(size) * size * 4u);
for (std::uint32_t y = 0; y < size; ++y)
{
for (std::uint32_t x = 0; x < size; ++x)
{
const bool red = (((x / 8u) + (y / 8u)) & 1u) == 0u;
std::uint8_t* p = &rgba[(static_cast<std::size_t>(y) * size + x) * 4u];
p[0] = 255; p[1] = red ? 40 : 255; p[2] = red ? 40 : 255; p[3] = 255;
}
}
return rgba;
}
}
int main()
{
try
{
// ---- 1. A built-in type: Texture2D -------------------------------------------------
CnbTextureData texture = MakeRgba8Texture2DData(64, 64, CheckerPixels(64));
GenerateRgba8MipChain(texture, CnbMipColorSpace::Srgb); // 7 levels: 64 ... 1
WriteFile("Content/Textures/checker.cnb",
EncodeTexture2DToCnb(texture, "Textures/checker"));
// ---- 2. A game-defined type -------------------------------------------------------
const std::string typeName = "MyGame.LevelInfo";
const std::uint32_t typeId = CnbAssetTypeIdFromName(typeName); // FNV-1a-32 | 0x80000000
CnbWriter writer(typeId, /*assetSchemaVersion=*/1);
writer.SetMetadata(typeName, "Levels/level01"); // required for custom types
// The sky texture stays a separate, shared asset: the file only names it.
writer.SetExternalReferences({
CnbExternalReference{0u, CnbAssetTypeId::Texture2D, "Textures/checker"}});
CnbByteWriter header; // chunk "lvlH"
header.WriteString("Ice Cavern");
header.WriteU32(64); // width
header.WriteU32(64); // height
header.WriteU32(0); // sky = XREF entry 0
writer.AddChunk(MakeChunkId('l', 'v', 'l', 'H'), header.Take(),
CnbChunkFlags::Mandatory);
std::vector<std::uint8_t> tiles(64u * 64u, 0u); // chunk "lvlT": 4096 tiles
for (std::size_t i = 0; i < tiles.size(); ++i) { tiles[i] = static_cast<std::uint8_t>((i / 64u) % 3u); }
writer.AddChunk(MakeChunkId('l', 'v', 'l', 'T'), std::move(tiles),
CnbChunkFlags::Mandatory);
// Optional and OFF by default. Only a libzstd build implements the codec; a file that
// uses it cannot be opened by a CNA built without libzstd.
if (IsCnbCompressionSupported(CnbCompression::Zstd))
{
writer.SetCompression(CnbCompression::Zstd, 3);
}
std::filesystem::create_directories("Content/Levels");
writer.WriteToFile("Content/Levels/level01.cnb");
std::cout << "wrote Content/Levels/level01.cnb\n";
return 0;
}
catch (const std::exception& e)
{
std::cerr << "MakeAssets: " << e.what() << "\n";
return 1;
}
}
Build it as one more executable in a project that adds CNA with add_subdirectory, as in Tutorial 03:
add_executable(MakeAssets MakeAssets.cpp)
target_link_libraries(MakeAssets PRIVATE CNA)
(CNA's own tools, such as cna_tool_cnb_info, are built the same way and additionally call the repository helper cna_link_sharp_runtime(<target> PRIVATE).) Run it from the folder that contains Content/, then inspect the level file:
./MakeAssets
cna_tool_cnb_info Content/Levels/level01.cnb
You should see the asset type printed as custom type 0xA07759A5 (that id is what the name MyGame.LevelInfo mints, computed from the FNV-1a definition on the CNB Format page), the type name and content name from CMET, one external reference (Textures/checker, expected type Texture2D), and the two lvl* chunks.
What the writer checks for you
- Custom types need
SetMetadata().Build()throws if a custom-typed file has no canonical type name, or if the name does not hash to the id you constructed the writer with. A file that could never load cannot be produced. - Container chunks are reserved.
AddChunk()refusesCMETandXREF; you set them throughSetMetadata()andSetExternalReferences(). External names must be relative,/-separated and free of... - The writer applies the reader's limits (512 MiB file, 384 MiB chunk, 1 GiB total expansion by default), so what
Build()returns is what a default reader opens. - Output is deterministic. Run
MakeAssetstwice and the files are byte-identical. WriteToFile()and theEncode*ToCnb()helpers do not create directories, which is why the program callscreate_directoriesfirst.
About the optional Zstandard step
Compression is off by default and can be requested only through CnbWriter::SetCompression() (or cna_cnb_writer_set_compression in the C ABI). None of the command-line tools — cna-content included — has a compression option, and the Encode*ToCnb() helpers do not take a codec, so for a built-in asset type you would assemble the chunks yourself. Zstandard exists only when CNA was built with libzstd (CNA_CNB_ZSTD, default AUTO), which is why the program asks IsCnbCompressionSupported() first; SetCompression() throws std::invalid_argument otherwise. A chunk is stored compressed only if that made it smaller. When it does, cna_tool_cnb_info --chunks shows a Zstandard codec and separate stored and logical sizes for the tile chunk, plus a summary line of the total.
A compressed file needs a codec at the other end. A CNA built without libzstd refuses it while reading the table of contents: “uses compression codec 2 (Zstandard), which this build does not implement” — and cna_tool_cnb_info cannot open it either. CNA's own measurements, taken on one NVMe developer machine, report that compression makes loading slower on fast storage even though it shrinks the file, which is why it is opt-in. Treat it as a distribution-size tool and measure on your target hardware.
6. Load the assets
ContentManager loads a .cnb without any registration for the built-in types: every ContentManager constructor installs the built-in loaders. Only your own type needs a loader, registered once with ContentManager::RegisterCnbLoaderEXT<T>(). Load<T> returns T by value and throws ContentLoadException on failure. The name you pass is the logical name relative to the content root, with no extension.
// PackDemo.cpp - loads compiled .cnb assets through ContentManager.
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>
#include "CNA/Content/Cnb/CnbDocument.hpp"
#include "CNA/Content/Cnb/CnbFormat.hpp"
#include "Microsoft/Xna/Framework/Content/ContentManager.hpp"
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Content;
using namespace Microsoft::Xna::Framework::Graphics;
namespace Cnb = CNA::Content::Cnb;
// The runtime type a "MyGame.LevelInfo" .cnb decodes to. It must be copy-constructible,
// because ContentManager caches loaded assets by value.
struct LevelInfo
{
std::string name;
int width = 0;
int height = 0;
std::vector<std::uint8_t> tiles;
Texture2D sky;
};
class PackDemo final : public Game
{
public:
PackDemo() : graphics_(this) {}
protected:
void LoadContent() override
{
ContentManager& content = getContentProperty();
content.setRootDirectoryProperty("Content");
// Registration is process-wide and idempotent for the same (id, name) pair.
const std::string typeName = "MyGame.LevelInfo";
const std::uint32_t typeId = Cnb::CnbAssetTypeIdFromName(typeName);
ContentManager::RegisterCnbLoaderEXT<LevelInfo>(
typeId, typeName,
[typeId](const Cnb::CnbDocument& document, ContentManager& cm) -> LevelInfo
{
const Cnb::CnbChunkId headerId = Cnb::MakeChunkId('l', 'v', 'l', 'H');
const Cnb::CnbChunkId tilesId = Cnb::MakeChunkId('l', 'v', 'l', 'T');
document.RequireAsset(typeId, /*maxSchemaVersion=*/1);
const Cnb::CnbChunkId known[] = {headerId, tilesId};
document.RequireMandatoryChunksUnderstood(known);
Cnb::CnbByteReader header = document.OpenChunk(document.RequireSingle(headerId));
LevelInfo info;
info.name = header.ReadString();
info.width = static_cast<int>(header.ReadU32());
info.height = static_cast<int>(header.ReadU32());
const std::uint32_t sky = header.ReadU32();
header.RequireExhausted();
Cnb::CnbByteReader tiles = document.OpenChunk(document.RequireSingle(tilesId));
const auto bytes = tiles.ReadBytes(tiles.Remaining());
info.tiles.assign(bytes.begin(), bytes.end());
// Resolve the reference through the same ContentManager, so the texture is
// cached and shared like any other Load<Texture2D>() call.
info.sky = cm.Load<Texture2D>(
document.ExternalReferenceAt(sky, "the level's sky").logicalName);
return info;
});
// .xnb first, then .cnb, then the loose PNG/WAV/.cnj files - see the ladder below.
Texture2D checker = content.Load<Texture2D>("Textures/checker");
std::cout << "checker: " << checker.getWidthProperty() << "x"
<< checker.getHeightProperty() << ", "
<< checker.getLevelCountProperty() << " mip level(s)\n";
LevelInfo level = content.Load<LevelInfo>("Levels/level01");
std::cout << level.name << ": " << level.width << "x" << level.height << ", "
<< level.tiles.size() << " tiles, sky "
<< level.sky.getWidthProperty() << "px wide\n";
}
void Update(GameTime&) override { Exit(); }
private:
GraphicsDeviceManager graphics_;
};
int main()
{
PackDemo game;
game.Run();
return 0;
}
Run it from the folder that contains Content/. Because checker.cnb was written with a seven-level mip chain, you should see:
checker: 64x64, 7 mip level(s)
Ice Cavern: 64x64, 4096 tiles, sky 64px wide
Things this example shows:
- No reader is registered for
LevelInfoas a loose file. The.cnbtier is self-describing: it dispatches on the asset type id in the file header, before any per-type reader is consulted. - The factory's
Tmust be exactly the type you load. Asking for a different type is aContentLoadException(“holds a … asset, which is not the type requested”). RequireMandatoryChunksUnderstood()makes an older loader refuse a file from a newer writer that added a mandatory chunk, instead of loading half of it.- The external reference goes through the same manager (
cm.Load<Texture2D>()), so the sky texture is cached and shared with any other asset that names it. - Registration is process-wide. It outlives the
ContentManagerand repeating the same(id, name)pair is harmless; registering the same id under a different name throwsstd::logic_error.
The other built-in types load the same way, for example content.Load<SoundEffect>("Audio/jump") and content.Load<Song>("Music/theme") (which needs Content/Music/theme.ogg in place, or you get “streams 'Music/theme.ogg', which was not found beside it”). Model loading and Model details are in Tutorial 35; writing loaders for your own types is the subject of Tutorial 148.
7. Which file wins? The load ladder
For one logical name, Load<T> tries these in order and uses the first that exists:
| # | Candidate for Load<Texture2D>("Textures/wall") | Notes |
|---|---|---|
| 0 | The manager's cache | Keyed by type and lower-cased name. |
| 1 | Content/Textures/wall.xnb | An authentic compiled XNA asset always wins. |
| 2 | Content/Textures/wall.cnb | CNA's compiled container. |
| 3 | the literal name, when you spell it "Textures/wall.cnb" | Handled explicitly. |
| 4 | the loose-file reader: the literal path, then wall.cnj, then wall.png, .jpg, … | A .cnj descriptor beats a native file of the same name. |
To watch it happen, put both wall.png and the mipmapped wall.cnb from step 2 in Content/Textures/ and print getLevelCountProperty() after the load. The .cnb wins, and you get its mip chain (9 levels for a 256×256 source). Delete the .cnb and the same call decodes the PNG through the loose-file reader, which produces a single-level texture.
A stale .cnb silently shadows the source next to it. The tier test is “does the file exist”; there is no timestamp comparison. If you edit wall.png and forget to rebuild, the game keeps loading the old .cnb. Rebuild as part of your build (Tutorial 145 shows how cna-content does that incrementally), and ship only the compiled output. Also note that the .xnb tier can read platform-packaged assets on Android whereas the .cnb tiers use ordinary filesystem calls; loading .cnb from an Android package has not been verified.
8. The same thing from C
The experimental C ABI (0.29.0 at this snapshot) declares 272 cna_cnb_* routes in CNA/C/cnb.h: parse and inspect documents, read and write chunks, every asset schema, the loader registry, the source importers and a .cnj compile route, including cna_cnb_writer_set_compression. It also exports ContentManager loads such as cna_content_manager_load_texture2d, which go through the same Load<T> and therefore the same tier order. One asymmetry: ContentManager::RegisterCnbLoaderEXT itself is not bound, so a C-registered loader is invoked directly through cna_cnb_loader_registry_resolve_for_document and cna_cnb_loader_invoke. This is source-level surface; we did not build the C API library. See Experimental C API and the route table in CNB Format.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
error: '.ogg' does not name a kind this compiler knows. Use --as texture2d|soundeffect|song|video. | cna_tool_source_to_cnb only auto-detects images, .wav and .dds. | Add --as song --stream <name>. |
error: --stream is required for song | A Song/Video .cnb must say which media file to stream. | Pass --stream Music/theme.ogg and ship that file. |
error: --mipmaps does not apply to a soundeffect asset. | Options that do not fit the output kind are refused. | Remove the option or fix the input. |
cnj_to_cnb: … has no 'sourceFile' naming an image | A Texture2D .cnj must name its image. | Add "sourceFile". |
cnb_info: '…' is not a .cnb file (bad magic bytes). | Not a CNB (or a text-mode/renamed file). | Rebuild the asset; transfer binaries in binary mode. |
… has a corrupt header: checksum 0x… does not match the computed 0x… or chunk 3 (TEXD) is corrupt | Damaged or edited after writing. | Rebuild; check your copy step. |
… declares a file size of A bytes but is actually B bytes | Truncated copy. | Re-copy the file. |
… uses compression codec 2 (Zstandard), which this build does not implement | The reading CNA has no libzstd. | Build CNA with libzstd (CNA_CNB_ZSTD=ON fails configure if it is missing) or write the asset uncompressed. |
CnbWriter: asset type 0x… is a custom type, so the file must carry its canonical type name. | Custom asset written without SetMetadata(). | Call SetMetadata(name, contentName) with the same name you passed to CnbAssetTypeIdFromName(). |
CnbWriter: 'CMET' is a container-defined chunk and cannot be added as a schema chunk. | AddChunk() with a reserved id. | Use SetMetadata() / SetExternalReferences(). |
CnbWriter: external reference '…' contains a '..' segment (or is absolute, or has a backslash) | Illegal XREF name. | Use a relative, /-separated logical name. |
… holds a custom type 0x… asset, which this build of CNA has no .cnb loader for (the file names it 'MyGame.LevelInfo'). | Your custom loader was not registered before Load, or the id is not the one you registered. | Call RegisterCnbLoaderEXT before the first load; check the name and id match. |
… holds a Texture2D asset, which is not the type requested for '…' | The T in Load<T> does not match the file. | Load it as the type it holds. |
ContentManager::Load<T>(): No reader registered for type, asset '…' | No .xnb/.cnb file for that name, and no loose-file reader for T. | Check the name, the content root and that the file exists. |
| The game loads the old picture after you changed the PNG | A stale .cnb shadows it. | Rebuild the .cnb, or delete it during development. |
Next steps
- Tutorial 145: Build Content with cna-content and cna_add_content() — produce and rebuild whole content trees incrementally instead of one file at a time.
- Tutorial 147: XNB Interoperability — the other compiled container, and what loads from real XNA and MonoGame pipelines.
- Tutorial 148: Custom Content Types — XNB readers, reflective readers and CNB loaders for your own types.
- CNB Format — the byte-level reference, limits, schemas and the full C ABI table.
- Content Pipeline — importers, processors, writers and the
cna-contentCLI.