Tutorial 157: A Tour of the Content Tools

CNA Tutorials  ·  CNA snapshot 009d40f5

ℹ

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 scriptcna-content build (and cna_add_content() in CMake)
Convert one image, WAV, DDS cube or song/video description right nowcna_tool_source_to_cnb
Validate a .cnb in CI or list its dependenciescna_tool_cnb_info --quiet / --refs
Compile a glTF file to .cnb without a .cnj left behindcna_tool_gltf_to_cnb <in> <outDir> <baseName>
Keep the intermediate .cnj to read or edit itcna_tool_gltf_to_cnj, then cna_tool_cnj_to_cnb (see Tutorial 111)
Produce XNB for an XNA-shaped consumercna-content build ... --format xnb

Next steps