CNA implements the XNA 4.0 avatar API (AvatarDescription, AvatarAnimation,
AvatarRenderer) with real behaviour on original CNA avatars. It is not an Xbox LIVE avatar
system: it neither reads nor produces Xbox avatar data or assets, and every avatar asset is
original CNA work generated by tools/avatar_builder/. On Windows, XNA's avatar classes were stubs;
CNA implements the behaviour XNA's documentation describes for the Xbox 360 (never traced on a
console), with avatars that resemble that era in original art. What remains limited (baked shading, catalog edge cases,
likeness) is in gamer-services-known-limitations.md.
Who owns what. The CNA service owns each account's AvatarDescription and its revision; the CNA
runtime owns the standard, immutable avatar catalogs (meshes, textures, animations), compiled into
every release. AvatarDescription.BeginGetFromGamer retrieves only the description, and
AvatarRenderer draws it with the locally installed catalog of exactly the version it names.
Normal rendering never downloads a per-user model or asset. The service's catalog packs are only an
optional, versioned update path for a catalog a client does not have (below).
AvatarDescription keeps the XNA contract: an opaque buffer of exactly 1021 bytes, IsValid
true when the first byte is non-zero, Height in meters from the feet to the top of the head,
and BodyType. Inside the buffer CNA stores its own versioned encoding. There are two formats,
and no newer one: format 1 and format 2 share bytes 0–42; format 2 adds individuality format 1
cannot express:
| Bytes | Field |
|---|---|
| 0 | format: 1, or 2 when facial hair or a face shape is set — non-zero, so XNA's IsValid rule holds |
| 1–3 | C N A |
| 4 | body type (0 female, 1 male) |
| 5–6 | height in millimeters, little endian (1450–2050) |
| 7 | build, 0 (slim) to 255 (heavy); 128 is the authored body |
| 8–9 | asset catalog version the item ids refer to |
| 10–30 | RGB colors: skin, hair, eyes, top, bottom, shoes, accessory |
| 31–42 | catalog item ids, 16-bit: hair, top, bottom, shoes, glasses, hat (0 = none, accessories only) |
| 43–44 | format 2: facial-hair feature item id (0 = none) |
| 45–60 | format 2: face shape, one byte each, 128 = as authored: head width, head height, jaw width, chin size, cheek fullness, eye size, eye spacing, eye height, eye tilt, brow height, nose size, nose width, nose height, mouth width, mouth height, ear size |
| up to 1016 | reserved, zero (from byte 43 in format 1, from 61 in format 2) |
| 1017–1020 | CRC-32 (IEEE) of bytes 0–1016, little endian |
CreateRandom() and CreateRandom(bodyType) produce valid descriptions from the newest catalog
compiled in, with a random face and, for male bodies, sometimes facial hair; passing a
description's Description bytes to the constructor reproduces the same avatar. A description
that uses no format 2 field is written as format 1, which every CNA build reads. A buffer that is
not a CNA description (another format, a damaged checksum, non-zero reserved bytes) keeps XNA's
IsValid answer, which only looks at the first byte, but reports height 0 and the female body
type, and a renderer given it is Unavailable. CNA builds older than format 2 read format 2
descriptions that way; the service hands them a format 1 copy instead (below).
AvatarDescription.BeginGetFromGamer/EndGetFromGamer read a gamer's avatar from the configured
CNA service (see docs/gamer-services-server.md): a signed-in gamer, a gamer returned by
Gamer.GetFromGamertag, or a remote NetworkGamer met in an online session all carry a service
identity. The operation completes during GamerServicesDispatcher.Update (never synchronously)
and EndGetFromGamer returns the stored description. A gamer without an avatar, without a service
identity (a remote System Link gamer), or an unreachable service yields an
invalid, all-zero description, so games take their usual "no avatar" path.
A signed-in local offline profile (no service configured, see docs/gamer-services-server.md) has
the avatar stored with the profile, created at random when the profile is, and the operation
completes synchronously with it. The service stores one
avatar per account and accepts only CNA descriptions (formats 1 and 2) whose items exist in a
catalog it has imported; players change theirs with the CNA avatar editor (below) and
administration can assign one (cna-gamer-services-admin <db> avatar <user> random).
Games that share avatars in a session the XNA way (sending Description bytes to other machines
and constructing an AvatarDescription from them) need nothing from the service at all.
As in XNA, each signed-in player index keeps the description EndGetFromGamer returned for it
until that gamer's avatar changes; other gamers get a fresh description on every read.
GamerServicesDispatcher.Update reads a kept avatar again every 10 seconds (the service's copy, or
the local profile's store, which the CNA avatar editor writes) and, when it differs, empties the
slot and raises that description's Changed on the dispatcher thread with the SignedInGamer as
sender. The C++ description is a value, so its Changed is shared by every copy of it (subscribing
or unsubscribing through any copy is subscribing to the one event). An unchanged avatar, a new
catalog, or an unreachable service raises nothing; signing out empties the slot without an event.
The CNA avatar editor is a system screen of the Guide: Edit avatar on the Guide's Home opens it
over any game for a signed-in account or local profile, and cna_avatar_editor is the same screen
as a program of its own, as the console's editor was. No game contains customization code and no
XNA API is added for it. Opening it over a running game rather than leaving the game is CNA policy.
The avatar stands large on a lit stage, idle-animated (a wave on opening, a small gesture now and then, a cheer on saving). Beside it are twelve categories -- Body, Skin, Face, Eyes, Nose & mouth, Hair, Facial hair, Tops, Bottoms, Shoes, Glasses, Headwear -- and each category's choices:
- cards rendered by the standard
AvatarRendererwearing that item (styles, body type, and the face, eye and nose-and-mouth shapes, which move several face features together: Balanced resets them); - swatches for skin, hair, eye, clothing and accessory colors;
- sliders for height (1.45-2.05 m), build and each face feature, labelled in words ("A little wide", "Very broad") rather than numbers.
What the focus rests on is tried on at once; Accept keeps it, and leaving the row without it goes back. The camera eases to what the category is about -- the whole avatar, head and shoulders, the top, the bottoms, the shoes -- and the player turns it (right stick, Z/C, dragging) and zooms (triggers, +/-, the wheel). X shuffles the category shown, Y randomizes the whole avatar coherently (natural hair and eye colors mostly, clothes in colors that go together, a face within its usual range, accessories now and then) and keeps the body type. Catalog 1 offers no face shapes or facial hair, and says so.
It edits against the newest catalog the avatar's owner accepts -- the service's newest imported
catalog for an account (the one it validates a save against; a catalog this computer lacks is
installed as a pack first), the newest compiled-in one for a local profile -- and carries an older
catalog's avatar into it item by item. Saving is transactional: B asks to save, discard or keep
editing when anything changed; the service's avatars.set (one change per account every two
seconds) or the local profile store either holds the new avatar, or the editor explains why not and
keeps the edits. When the stored avatar changes elsewhere while the editor is open, an untouched
editor shows the new one and an edited one says that saving replaces it. Running games see a saved
avatar through AvatarDescription.Changed.
cna_avatar_editor is built with the examples of a 3D renderer (CNA_BUILD_EXAMPLES).
--capture DIR walks the categories, tries on and keeps choices, randomizes and saves, writing a
PNG of each step; with CNA_GAMER_SERVICES_AUTO_SIGN_IN naming a profile and
CNA_GAMER_SERVICES_PROFILES_DIR a scratch store it runs unattended on a private display.
AvatarRenderer needs nothing beyond the XNA API. A game that creates a
GraphicsDeviceManager, adds a GamerServicesComponent (or calls
GamerServicesDispatcher.Initialize), and constructs a renderer from a description gets:
- States. A description without an avatar, or one CNA cannot read, is
Unavailableand draws nothing. A valid one isLoadingwhile one shared background thread assembles the body, wardrobe and face from the asset catalog, thenReady. Identical descriptions share the assembled model. - Bind pose and skeleton.
ParentBonesis XNA's 71-entry table. Once ready,BindPoseholds 71 local transforms relative to the parent, scaled to the description's height (bind rotations are identity, so they are pure translations). - Draw. Every bone must be decomposable (
InvalidOperationExceptionotherwise). Each bone's rotation and scale are applied relative to its parent. The root also takes the supplied translation, and every other bone keeps the avatar's own bind offset, so one standard animation fits every height and build. Lighting is exactlyLightDirection/LightColorplusAmbientLightColor(defaults 0.65 key and 0.35 ambient from the front). The face features are decals chosen by theAvatarExpression(left and right eyes and eyebrows independently). The model always renders solid, with clockwise front faces as in XNA, and the blend, depth, rasterizer and first sampler states are restored afterwards. There is no baked ambient occlusion or self-shadowing: a visual-quality limit, not a missing API (shadows a game draws itself, as in the XNA AvatarShadows sample, work). - Loading effect. While loading, a renderer constructed with
useLoadingEffect(the one-argument constructor's default) draws a softly pulsing translucent silhouette the height of the avatar. If loading completes during aDrawcall, that call remains on the loading path and the next update observesReadybefore the next draw consumes animation transforms. - Device. The graphics device comes from the
IGraphicsDeviceServiceof the service provider given to the dispatcher (XNA's rule); drawing before gamer services are initialized throwsInvalidOperationException.
Each of the 31 AvatarAnimationPreset values is an original CNA clip on the 71-bone skeleton,
2.5 to 10 seconds long, with a keyed facial expression (idles blink). BoneTransforms holds 71
local animation deltas at CurrentPosition: non-root translations are zero, because the
renderer's BindPose supplies the joint offsets. Thus BoneTransforms[i] * BindPose[i] * parentWorld computes the same joint as drawing that pose, including attachments. Update follows the reference exactly: negative time
plays backward, looping wraps by the remainder of Length (a position equal to Length is kept),
a non-looping animation clamps, and an overflowing step clamps to the nearer end. An undefined
preset value is a zero-length bind pose.
All geometry, textures and motion are original CNA work generated by
tools/avatar_builder/generate_avatar_catalog.py (pure Python, deterministic, no downloaded or
third-party content). Every catalog version lives in its own directory,
modules/gamer-services/assets/avatars/v<N>/, and all of them are compiled into the library by
cmake/EmbedBinaryFiles.cmake as v<N>/<file>. A released catalog never changes: catalog v1 is
frozen data (the generator that wrote it is in Git history), catalog v2 likewise (generator at
commit d1730d4f3), and catalog v3, the current one, was released on 2026-09-30 and is frozen from
then on: new art goes into a new catalog version.
A description is drawn from exactly the catalog version it names — the compiled-in manifest of that version, else an installed pack of it (below) — never from another. Files are found by size and SHA-256, so two versions may use one file name for different contents and identical contents are shared. If the named catalog cannot be obtained at all, the newest compiled-in body is drawn with every item replaced by its slot default (its ids are not read against another version). Preset animations are system data, not part of a description, and come from the newest compiled-in catalog.
Catalog v3 (the newest) has:
- a body per body type in toy-like console proportions: a large round head (about a fifth larger than v2's, a softer jaw and fuller cheeks) on a short neck, lowered shoulders, shorter legs, sturdier limbs and larger hands (a rounded palm as wide as its finger row, four separate fingers and a thumb, closing on the forearm without a seam); a rounded nose, ears and the face-feature decal patches;
- 39 wardrobe items fitted to both bodies, with exactly catalog v2's ids and slots: 10 hair styles (as broader clumps than v2's, so each reads as one shaped mass), 9 tops, 6 bottoms, 5 shoes, 4 glasses and 5 hats, with per-body CreateRandom weights;
featureItems: 4 facial-hair styles a format 2 description can name;faceControlsper body type: what each face-shape byte does, as scale/move/rotate deformers with an ellipsoidal falloff on Head-weighted vertices — the face-only ones move the head, face decals and facial hair, the whole-head ones (width, height) also hair, hats and glasses;- for every hair style,
hatAssets: the style with what a hat hides removed and the rest flattened under one shared hat envelope, used whenever the hat itemcoversHair; face_features.png, an atlas of every eye (almond openings under a bold lid line with a small outer wing; a separate iris layer tinted by the eye color), eyebrow and mouth state (fuller upper and lower lips, a gentle resting smile) at 128 pixels per tile; lips are translucent so they suit any skin tone; face controls reach about half again as far as v2's;animations.glbwith the 31 presets as glTF cubic-spline rotation curves (the standing idle sways from foot to foot with the head following);catalog.json, listing every file with its SHA-256.
Every GLB uses the same canonical 71-joint rig in XNA's exact topology: meters, Y up, facing +Z,
feet on y = 0, the avatar's left on +X. This is the asset space, not the standard API's
space: XNA avatars face -Z and their left is -X. AvatarAnimation.BoneTransforms and
AvatarRenderer.BindPose expose the half-turn about Y of the catalog transforms. The renderer
converts caller bones back to asset space for skinning and applies the half-turn before the
caller's World, so attachments computed from the public bones and bind pose agree with the
drawn body. Released GLBs, manifests, assembled-model digests and descriptions stay unchanged.
The Guide editor and GPU art-review utility orient their own camera view accordingly.
Joints XNA leaves unnamed are helpers named SlotNN,
never skinned or animated. Materials are a base color factor, an optional embedded base-color PNG
(a near-white multiplier carrying shading and fabric detail) and a description color tint. The
reader refuses anything outside that contract: external URIs, other rigs, non-identity bind
rotations, out-of-range indices or weights, and oversized data. Catalogs v2 and v3 keep to what
the format 1 reader accepts (normals and texture coordinates are normalized 16-bit accessors), so
CNA builds from before catalog packs, which fetched a missing catalog's items one by one, can draw
them; they ignore facial hair, face controls and hat variants. GamerServices_AvatarCatalogUpToDate
regenerates the newest catalog and compares it byte for byte (on a C library other than glibc, every
byte except catalog v3's six FemaleShocked arm rotations, which a non-convergent head solve froze
from glibc's libm and are checked structurally there; plans/plan_apple_m4.md AM4-166); AvatarCatalogTest.CatalogV1IsFrozen,
V1DescriptionsAssembleExactlyAsReleased, CatalogV2IsFrozen and CatalogV3IsFrozen pin the
released ones, and EmbeddedAssetsResolveByContentNotByName and
AnUnavailableCatalogNeverReadsItsIdsAgainstAnotherVersion keep each version's files and ids its own. A v2
avatar keeps drawing from v2; the avatar editor carries it into v3 item by item when it is edited. tools/avatar_builder/avatar_review.py renders
review sheets through the real renderer (cna_avatar_review) and a CPU preview.
The avatar model is the console's: the service owns each account's description (1,021 bytes and a
revision); the avatar catalogs are client content. A description on a catalog of this release
renders offline, and BeginGetFromGamer + AvatarRenderer transfer nothing but the description.
A pack is a whole immutable catalog version, installed once per client, never a per-user download.
When a description names a catalog this build does not compile in, the renderer's loader installs
that exact catalog as one pack, if the configuration allows it (avatarCatalogUpdates, default on;
CNA_AVATAR_CATALOG_UPDATES=0 turns it off; maxAvatarCatalogBytes, default 64 MiB):
avatars.catalogPackdescribes it: version, pack format, contract level (reader), description formats, the manifest's SHA-256 and size, and the total size. A pack this client cannot read or is not allowed to keep is refused before anything is downloaded.- The manifest and each file the client lacks are downloaded by hash from the service's file route; files this build compiles in (or another installed pack holds) are copied, not downloaded. Every file is verified into a staging directory.
- The whole catalog is validated as the renderer will use it: every model against the 71-bone contract and bounds, every texture decoded, every item fitted to its body, the face atlas against its layout, all 31 animation presets.
- One rename activates it. An interrupted install leaves only staging, which a retry reuses after verifying each file again; an invalid pack is discarded; nothing half-installed is ever read.
Installed packs are kept, not cached: CNA_GAMER_SERVICES_CATALOGS_DIR, else
$XDG_DATA_HOME/cna/avatar-catalogs (%LOCALAPPDATA%\cna\avatar-catalogs on Windows), one
self-contained v<N>/ per version. Catalog versions are CNA-wide identities: the pack installed
first is that version, and a different catalog offered under the same number is refused. A version
that failed to install is not asked for again for a minute.
The service negotiates what each client receives (avatars.get states the formats it reads, the
catalogs it has, whether and how large it installs packs). A client that can draw the stored avatar,
or will install its catalog, receives it; otherwise the service sends a projection onto the newest
catalog the client has, marked as such, and leaves the stored avatar unchanged. Games see an
ordinary description either way, and AvatarDescription.Changed follows the stored avatar's
revision, so installing a catalog or receiving a projection is never a change.
The fallback order for a description is: (1) its catalog is installed; (2) its pack is installed
now; (3) the service's projection (when it came from the service); (4) the deterministic default
avatar -- the newest installed body with the description's body type, height, build and colours,
every item its slot default (its ids are never read against another catalog); (5) Unavailable
only when not even a body can be drawn. Descriptions exchanged directly between games (bytes over a
NetworkSession) take (1), (2) and (4).