Content architecture

CNA snapshot 009d40f5  ·  Development › Architecture Maps  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. The resolution order, cache key and disposal behavior were read from ContentManager.hpp and ContentManager.cpp and the module CMake files; the build-time split is also stated in CNA's own pipeline documentation. The named tests exist but were not executed for this page.

Build-time import, process and write, and run-time load and cache, are separate systems in CNA. A content-pipeline tool must not need a game window, a graphics device or an audio device merely to convert an asset, and a shipped game must not carry the authoring dependencies (FreeType, FFmpeg importers, FBX readers, an effect compiler) that only a build needs. This page states where that boundary sits in the modules, how ContentManager resolves a request, what its cache keys and ownership mean, and how to add or change an asset type without breaking either half.

Build-time pipeline

source asset (.png, .gltf/.glb, .wav, .cnj, .spritefont, .fbx, ...)
   └─ importer → canonical content value → processor → writer
                                                     ├─ CNB   (CNA's own format, the default)
                                                     └─ XNB   (the XNA 4.0 container, selected per build)
                                                          ▼
                                                     runtime file in the Content tree

Importers decode source formats and answer “what is in this source?”; processors produce runtime-oriented data and answer “how should this become runtime content?”; writers serialize. The importer, the processor and the canonical value are shared by both output formats and only the serializer differs, so a route that works for CNB is not automatically covered for XNB: XNB writers are a separate, format-keyed registry. This keeps authoring dependencies out of runtime loading. CNA's own pipeline documentation states that no pipeline component constructs a GraphicsDevice, opens an audio device, creates a window or initializes a renderer.

The conversion code is split across three build targets, and only the first ships in a game:

  • modules/content (cna_content) holds what a running game needs to load content and also the canonical pipeline engine: src/Pipeline (build configuration, manifest, the per-type pipelines), the CNB codecs and writer (src/Cnb), the XNB readers and writers (src/Xnb) and the glTF importer.
  • modules/content-pipeline (cna_content_pipeline, alias CNA::ContentPipelineBuild) holds the build-time-only components: the FreeType-backed .spritefont route, the FFmpeg-based MP3, WMA and WMV importers, the FBX and .x readers, the external effect-compiler service and the XNA Content.Pipeline facade. Their probes are three-state switches (CNA_ENABLE_FONT_PIPELINE, CNA_ENABLE_MEDIA_PIPELINE) or optional (zlib for binary FBX arrays).
  • cna_content_compiler (CNA::ContentCompiler, defined in ToolContentPipeline.cmake) links both and implements the command-line coordinator: source discovery, cache and configuration, atomic publication. The cna-content executable is a small main over that library, and cna_add_content(...) in the same file wires a content build into a consumer's CMake.

Only cna_content_compiler links cna_content_pipeline, so the boundary is enforced by the dependency graph rather than by convention (see the module graph). The format and workflow references are Content Pipeline, CNB format and XNB and the content pipeline.

Runtime resolution order

ContentManager::Load<T>(name) resolves a request in this order:

  1. The cache, keyed by the requested C++ type and the normalized name.
  2. A compiled .xnb (<root>/<name>.xnb). This tier needs no per-type reader registered on the manager: root-object dispatch is driven by the file's own type-reader table through the process-wide ContentTypeReaderManager. A genuine external .xnb is never shadowed by a CNA convenience file.
  3. A compiled .cnb. It ranks just below .xnb and above everything it could have been compiled from. A .cnb is self-describing: its header's asset-type identifier selects the loader through the process-wide CnbLoaderRegistry, again with no per-type reader on the manager. A caller who spells out Foo.cnb is handled too. A malformed .cnb is a hard error and does not fall through.
  4. Loose-file readers, registered per T as LooseFileContentTypeReader<T> (every ContentManager constructor registers the built-ins: 2D, cube and 3D textures, SoundEffect, effects, SpriteFont, models, skinned models, animation clips, curves, songs and videos). For such a reader the literal path wins if it exists, then <name>.cnj, then each extension the reader declares, then the bare path. With no reader for T the load throws ContentLoadException.

Extension fallback and root-directory normalization are observable behavior. Backslashes are converted to slashes in the root and the asset name, and paths are resolved case-insensitively component by component (on Android, relative paths go to the platform's packaged-asset namespace). The precedence is pinned by CnbContentManagerTest.ResolutionOrderIsXnbThenCnbThenLiteralThenCnjThenNative, ContentManagerXnbTest.XnbWinsOverCnjAndNativeExtensionForTheSameName and CnbContentManagerTest.ACurveCnbOutranksASameNamedCurveCnj. Load<SoundEffect> is an explicit specialization because the type is move-only; it repeats the same tier order by hand, which is why a tier once went missing for that one type.

Built-in XNB readers are registered process-wide by RegisterAllBuiltInXnbReaders(), which Game.cpp calls in the Game constructor: a bare ContentManager stays neutral for isolated use, while a real Game is ready to load built-in types before Initialize runs (GameTest.ConstructionRegistersBuiltInXnbReadersBeforeLoadContent).

Cache and ownership

The cache key combines the requested C++ type with the normalized asset name (lower-cased, slash-separated). The same textual asset requested as two types therefore does not alias by name alone: the second request goes back down the ladder and, if that type's reader rejects the file, throws a ContentLoadException instead of an unrelated bad_any_cast (CnjAssetCacheTypeSafetyTest.DifferentTypeSameNameThrowsContentLoadExceptionNotBadAnyCast; a repeat of the same type and name returns the cached instance). SoundEffect is outside the generic cache: each Load<SoundEffect> returns an independently owned instance.

ContentManager::Unload() clears the owned cache and nothing else; Dispose(true) calls it, and Dispose(false) also drops the cache without going through Unload(). Because a returned value can share the underlying resource, code that retains objects after unload must follow that object type's ownership semantics rather than assume the cache keeps it alive or that unloading disposed it: ContentManagerTexture2DXnbTest.UnloadClearsTheTextureCache asserts that a load after Unload() produces a different renderer texture even while the caller keeps its first copy. Content readers create graphics resources, so review content lifetime together with device teardown (content cache ownership).

Adding or changing an asset type

  1. Decide which piece owns the change: a loose run-time reader (RegisterTypeReader<T>, or RegisterCnjLoader<T> for a game-defined .cnj type), a CNB loader (a game extension registers a custom identifier through ContentManager::RegisterCnbLoaderEXT; the built-in identifiers belong to CNA and are unreachable by a caller), an XNB reader (the type-reader registry), or a pipeline conversion (importer, processor, writer).
  2. Keep the importer free of run-time device creation, and keep a new authoring dependency in content-pipeline rather than widening cna_content.
  3. Register the reader or loader at the intended scope (registrations are process-wide for XNB and CNB, per manager for loose readers) and test duplicate and unknown identifiers: CnbContentManagerTest.RegisteringTwoTypesUnderOneIdentifierIsRefused, CnbContentManagerTest.AGameExtensionCannotClaimABuiltInOrReservedIdentifier and ContentTypeReaderManagerTest.RepeatRegistrationOfSameNameIsIgnoredNotReplaced show the existing shape.
  4. Test cache identity, malformed or truncated input (CnbContentManagerTest.AMalformedCnbIsAHardErrorAndDoesNotFallThroughToTheLowerTiers) and unload.
  5. If the object creates GPU or audio resources, test device and service destruction order.
  6. If a writer changed, follow the change into the matching run-time reader: the format contract and cache disposal differ from ordinary graphics allocation.

The step-by-step recipes are modify ContentManager and modify the Content Pipeline; the source tours are content runtime internals and content pipeline internals.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture
Reference
Module index