Content architecture

CNA snapshot 3b43bd86  ·  Development › Architecture Maps  ·  source links pinned to 3b43bd86

✓

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 3b43bd86; 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). The file is opened through the manager's virtual OpenStream, so a derived manager that overrides it supplies the bytes. 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> (the built-ins, for 2D, cube and 3D textures, SoundEffect, effects, SpriteFont, models, skinned models, animation clips, curves, songs and videos, are installed on a manager when their type is first loaded, or all at once by RegisterBuiltinLoaders()). 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. A relative RootDirectory is resolved under the title folder (TitleLocation::Path) when that directory exists there, as XNA opens content through TitleContainer, and otherwise relative to the working directory (ContentRootTests). As in XNA, setting RootDirectory throws InvalidOperationException while anything is cached, even to the same value, so the root cannot change under a cached entry until Unload() empties the cache. 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, which the XNB and CNB tiers read). 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 live in a process-wide registry. The Game constructor in Game.cpp declares them registered (DeclareBuiltinXnbReaders()), so a real Game can load built-in types before Initialize runs and the registry answers queries as if every reader were there (GameTest.ConstructionRegistersBuiltInXnbReadersBeforeLoadContent); each reader is installed when its type is first loaded, or when the registry is asked for it by name, so a game links only the readers of the types it loads. A bare ContentManager declares nothing and installs its type’s family on first load; ContentManager::RegisterBuiltinLoaders() installs everything at once for hosts such as the C API.

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() follows XNA: it disposes every disposable object the readers recorded while loading (textures, effects, model vertex and index buffers, sound effects and songs), in the order they were read, and then clears the records and the cache; objects a game created itself are not touched. Dispose(true) calls it, Dispose(false) only drops the cache and the records, and a disposed manager refuses Load, ReadAsset and Unload with ObjectDisposedException. Because a returned value shares the underlying resource, a copy a caller kept from before Unload() now holds disposed resources: ContentOwnershipCompatibilityTest.UnloadDisposesMaterializedObjectsAndClearsCache and the Unload cases in the content ownership suites pin the disposal, and ContentManagerTexture2DXnbTest.UnloadClearsTheTextureCache that a load after Unload() produces a different renderer texture. 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