Tutorial 157: A Tour of the Content Tools
What you’ll learn: Build and use CNA's command-line content tools: convert a single file to .cnb, validate and inspect it, build a directory incrementally with cna-content, write XNB with LZX compression, and clean the output safely.
Development snapshot only. These tools do not exist in the v0.1.0-alpha.1 tag (only cna_tool_gltf_to_cnj does). Everything here targets CNA snapshot 009d40f5 on the next branch, which still reports 0.1.0-alpha.1. Commands and options are taken from each tool’s own usage text and CMake target; we did not run them for this tutorial, so what a tool prints is described, not quoted. The asset generator below was run and checked by us.
Before you start
You need a CNA checkout of the next branch that you can configure (Tutorial 02, with sharp-runtime also on next). None of these tools needs a window, GPU or sound card. Build just the three you need in a tree you already have:
cd cna
cmake -S . -B build
cmake --build build --target cna_content_tool cna_tool_source_to_cnb cna_tool_cnb_info -j"$(nproc)"
ls build/cna-content build/cna_tool_source_to_cnb build/cna_tool_cnb_info
The content tools are defined in every configuration; cna_content_tool produces the executable cna-content. For the complete list see Command-Line Tools, and for the pipeline itself Content Pipeline.
Make two tiny assets
Any PNG and WAV will do. This script needs only Python 3’s standard library and writes an 8×8 checkerboard and a quarter-second 440 Hz beep into assets/:
# make_assets.py -- writes assets/checker.png (8x8 checkerboard) and assets/beep.wav (0.25 s, 440 Hz)
import math, os, struct, wave, zlib
os.makedirs("assets", exist_ok=True)
def write_png(path, width, height, pixel):
rows = b"".join(
b"\x00" + b"".join(bytes(pixel(x, y)) for x in range(width)) for y in range(height))
def chunk(kind, data):
body = kind + data
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)
with open(path, "wb") as out:
out.write(b"\x89PNG\r\n\x1a\n")
out.write(chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0))) # 8-bit RGBA
out.write(chunk(b"IDAT", zlib.compress(rows)))
out.write(chunk(b"IEND", b""))
write_png("assets/checker.png", 8, 8,
lambda x, y: (255, 255, 255, 255) if (x + y) % 2 == 0 else (30, 30, 30, 255))
with wave.open("assets/beep.wav", "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2) # 16-bit PCM
wav.setframerate(22050)
wav.writeframes(b"".join(
struct.pack("<h", int(12000 * math.sin(2 * math.pi * 440 * n / 22050)))
for n in range(22050 // 4)))
python3 make_assets.py && ls assets
Compile one file with cna_tool_source_to_cnb
The focused converter picks the asset type from the extension and never touches a device or a clock, so its output is deterministic. Run it from your cna directory with the assets path adjusted:
mkdir -p out
build/cna_tool_source_to_cnb assets/checker.png out/checker.cnb --mipmaps --mip-color-space srgb
build/cna_tool_source_to_cnb assets/beep.wav out/beep.cnb
--mipmaps generates a full box-filtered mip chain for a Texture2D, and --mip-color-space srgb averages in sRGB terms, which is right for colour maps (leave the default linear for normal, roughness and mask maps). An option that does not apply to the input kind is refused rather than ignored: try --color-key 255,0,255 on the WAV and the tool will tell you why it will not.
Inspect and validate with cna_tool_cnb_info
build/cna_tool_cnb_info out/checker.cnb # header, chunks and references
build/cna_tool_cnb_info out/checker.cnb --chunks # only the chunk table
build/cna_tool_cnb_info out/checker.cnb --refs # only external asset names, one per line
build/cna_tool_cnb_info out/checker.cnb --quiet; echo "exit $?"
The tool checks every structural invariant while reading, so a non-zero exit means the file is malformed and the message says how. --refs prints the external assets a file depends on, one per line (a model, for example, lists its textures), which is what a build script wants; --quiet reports through the exit code alone. Prove the validation works by damaging a copy:
head -c 40 out/checker.cnb > out/broken.cnb
build/cna_tool_cnb_info out/broken.cnb; echo "exit $?" # non-zero: truncated file, with a reason
Build a whole directory with cna-content
cna-content compiles every source it recognises through Importer → Processor → Writer, and records what it built in a content-hashed manifest so that a second identical run does no work. Ask it to explain itself:
build/cna-content build assets -o out/content --explain # first run: every asset is built, with the reason
build/cna-content build assets -o out/content --explain # second run: assets are skipped, unchanged
# now change the PNG (edit the two colours in make_assets.py and run it again), then:
build/cna-content build assets -o out/content --explain # only the changed asset is rebuilt
The output directory keeps the source layout and gains the manifest and lock files (.cna-content-manifest.json, .cna-content.lock). List it with find out/content -type f and inspect any produced .cnb with cna_tool_cnb_info. Two front ends now exist for the same idea, so do not assume they agree byte for byte: cna-content applies its processor defaults (for example, premultiplied alpha on textures), while cna_tool_source_to_cnb does not rewrite pixels unless you ask (for example with --color-key). Compare the outputs rather than guess.
A single file is also valid input, if the output path’s extension matches the format:
build/cna-content build assets/checker.png -o out/single/checker.cnb
The same content as XNB
Importers and processors are identical for both containers; only the writer differs. Ask for XNB with LZX compression, the compression XNA 4.0 itself produced:
build/cna-content build assets -o out/content-xnb --format xnb --xnb-platform windows --xnb-version 5 --xnb-profile reach --xnb-compress lzx
The command names the XNA 4.0 values explicitly: platform windows (windows, windowsphone and xbox360 are the XNA 4.0 platforms; xbox360 is refused unless you add --xnb-allow-unverified-xbox, and other platform names are extended identifiers XNA 4.0 never produced), container version 5 (the XNA 4.0-era container) and the Reach profile (the alternative is hidef). CNA reports that its LZX output loads through a genuine XNA 4.0 ContentManager in its own interoperability tests; we have not reproduced that.
Clean up
build/cna-content clean out/content
build/cna-content clean out/content-xnb
clean removes only unchanged files that a valid output manifest proves the pipeline itself created, so it will not delete files you put in that directory yourself.
Which tool for which job
| You want to… | Use |
|---|---|
| Build a whole content tree incrementally, from CMake or a script | cna-content build (and cna_add_content() in CMake) |
| Convert one image, WAV, DDS cube or song/video description right now | cna_tool_source_to_cnb |
Validate a .cnb in CI or list its dependencies | cna_tool_cnb_info --quiet / --refs |
Compile a glTF file to .cnb without a .cnj left behind | cna_tool_gltf_to_cnb <in> <outDir> <baseName> |
Keep the intermediate .cnj to read or edit it | cna_tool_gltf_to_cnj, then cna_tool_cnj_to_cnb (see Tutorial 111) |
| Produce XNB for an XNA-shaped consumer | cna-content build ... --format xnb |
Next steps
- Tutorial 145: Build content with the pipeline, end to end — load the results in a game.
- Command-Line Tools — every option of every tool.
- CNB Format — what is inside the files you just made.