Migrating an XNA game to CNA: a process that keeps every mismatch attributable
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. Checked by reading TARGET sources at 009d40f5; not built or executed. The process is a recommended method: no CNA gate enforces it for a consumer game, and the consumer examples are evidence for their own pinned revisions.
CNA keeps much of XNA 4.0's public shape in C++, but it does not make a C# game binary- or source-compatible: a port is a controlled translation in which language, assets, renderer, services and timing each change separately. This page is the process view of that translation for the snapshot 009d40f5: what to freeze before touching code, how to inventory assets and shaders by the route CNA actually takes, how to choose a renderer by evidence, and how to build a verification ladder so that every mismatch names the boundary that caused it. The API-level substitutions (namespaces, properties, memory) are in the migration guide; worked real ports are on the porting case studies.
Classify the starting point first
Two very different projects are both called "porting to CNA". A C# XNA or FNA game needs a language translation: every class is rewritten in C++ against CNA's headers. An existing C++ game or reconstruction needs only its platform-service boundary replaced: the simulation code already compiles, and the work is to swap the layer that talks to windows, pixels, sound and files. The Speedy Blupi 2013 port did the first (decompiled C#, then C++, then CNA); the Free Eggbert feasibility study plans the second (C++ that currently runs through the DirectDraw-shaped free-direct layer). Both are described on the case-studies page.
Keep the two risks apart even when a project has both. Translating game logic and redesigning rendering in one pass destroys the ability to attribute a mismatch: when the first screenshot differs, nobody can say whether the translation, the asset conversion or the renderer is wrong. The workflow below changes one layer at a time and compares after each.
freeze original mechanical API strict-XNA asset-route
baseline ---> translation ---> compile ---> manifest
(traces, frames, (C# -> C++, no (CNAEXT made (XNB / CNB / CNJ /
saves, assets) redesign) [[deprecated]]) loose / glTF)
|
v
deployment <--- behaviour + <--- services, owner- <--- renderer +
matrix pixel compare ship, lifetime, shader strategy
(hosts, renderers) vs the baseline timing adaptation (capability first)
|
| mismatch
v
localise it: return to the smallest step
(API, asset, renderer or service) that can explain it
Freeze a behavioural baseline
Before changing language or framework, pin the original at a named commit (or, for a shipped binary, a file hash) and record what the port must reproduce: deterministic simulation traces, representative screenshots or frame captures, save files, network packets, audio events and a complete asset inventory. Record the settings that make those captures reproducible: resolution, fixed-step interval, random seeds, locale and platform. A port without a baseline can become internally consistent while drifting away from the game it was meant to preserve.
The cna-samples campaign shows what a strict baseline looks like in practice (its plan at 4da98a0): the original project is built and run under real XNA, its commands, controls, captures and audio are retained beside the port, and a time hook (CNA_TIME in that repository) pins the instant both engines animate from so that frames can be compared pixel for pixel. CNA's own reference tools are the other half: tools/xna-oracle holds 39 declarative scenes with reference images captured from real XNA 4.0 under Wine and DXVK on Linux, and Tutorial 161 shows how a scene is diffed.
Translate the API shape mechanically
Namespaces and type and member names map directly from dots to C++ scopes (Microsoft.Xna.Framework.Graphics becomes Microsoft::Xna::Framework::Graphics). The largest systematic difference is properties: a C# read of obj.Value becomes obj.getValueProperty() and an assignment becomes obj.setValueProperty(value); content loads keep their shape as getContentProperty().Load<T>("name"), returning the asset by value. Events, nullable values (std::optional), collections, ownership and ref/out arguments (reference-parameter overloads) need explicit C++ idioms; the full rule set is on CNA's translation conventions.
Do not redesign names while translating. First reach compiling, behaviourally comparable code; only then isolate genuinely game-owned abstractions. While the translation stays mechanical, compiler errors are a useful inventory of what the game touches. A broad wrapper layer that hides every mismatch too early does the opposite: an unimplemented or deliberately refused CNA behaviour then looks like a game bug.
The compile step in the figure is literal. Every CNA member that is not XNA 4.0 API carries the CNAEXT marker, and building with CNA_STRICT_XNA_API turns that marker into [[deprecated]], so a warning names every place the translated game leans on a CNA extension (CNA's own harness for this, StrictXnaApiSurfaceCheck.cpp, is wired in Harnesses.cmake). A port that compiles cleanly in that mode uses only the XNA-shaped surface; one that does not has a named list of extensions to review.
Inventory content by the reader route it takes
CNA reads real XNB files: the container with LZX and LZ4 bodies on the typed Load<T> path (XnbDecompression.cpp; the untyped external-reference path behind ExternalReferenceReader still refuses LZ4 by name), a registered table of built-in readers (61, or 60 without native 128-bit integers), shared resources, stock effects, graphics assets, models and several audio encodings. Statements that XNB is unsupported, or that CNA cannot produce XNB, are false at this snapshot: the build-time cna-content tool writes both XNB and CNA's own CNB container (Tutorial 145).
The boundary is still narrower than XNA's pipeline, and it is route-specific rather than extension-specific:
- The reader registry is explicit. A custom type needs a registered creator (
ContentTypeReaderManager::AddTypeCreator) or a field list declared once withReflectiveTypeReaderBuilder<T>(ReflectiveTypeReader.hpp); there is no reflective discovery. Both routes are CNA extensions (AddTypeCreatorandReflectiveTypeReaderBuildercarry theCNAEXTmarker, as does a bareContentManager()), so a game with custom content types will always show them in the strict-XNA warning list of the compile step above. Tutorial 148 walks through it. - The general
EffectReader(EffectContentTypeReader.cpp) reads a bounded bytecode blob and hands it toEffect(GraphicsDevice&, bytes), which accepts XNA/FNA Direct3D 9 Effect Framework binaries only where the active renderer reports the compiled-effects capability; the five stock-effect readers (StockEffectContentTypeReaders.cpp) are a separate route that works everywhere a 3D pipeline exists. - Loose assets take other routes: CNJ descriptors, native image, audio and video formats, and direct
.gltf/.glbmodel loading. Runtime glTF and the offlinegltf_to_cnjconverter share one import core but keep route-specific material, animation and grouping limits (the glTF import core).
The practical consequence is a migration manifest with one row per source asset rather than per folder. An illustrative shape (not taken from any particular game):
| Source asset | Route at run time | Reader or loader | Sidecars | Verification oracle |
|---|---|---|---|---|
player.xnb (Texture2D, LZX) | XNB, first rung of the load ladder | built-in Texture2D reader | none | pixel compare against the original's first frame |
glow.xnb (compiled Effect) | XNB Effect | EffectReader then Effect(GraphicsDevice&, bytes) | none | capability check first; then a small golden scene |
level1.png with a transparent colour | a Texture2D .cnj descriptor whose sourceFile names the PNG | CNJ Texture2D reader, then the image loader | the .cnj; its colorKey is one exact RGB value | byte compare of decoded pixels, keyed texels at alpha 0 |
theme.mid | none: no Song route accepts MIDI | none | none | decide: pre-render or game-specific service |
Test every concrete asset against the selected route rather than reasoning from its extension: the load ladder (.xnb, then .cnb, then loose files) means a stale sibling file silently wins, as ContentManager resolution explains, and Tutorial 147 diagnoses a foreign XNB file.
Treat custom shaders as renderer contracts
Compiled XNA/FNA Direct3D 9 Effect Framework binaries execute on 14 of the 25 renderer identities in 10 families, but only one family has the path on by default: FNA3D always, and the other nine behind default-off build options (CNA_EASYGL_COMPILED_EFFECTS, which covers the five GL identities, and the Vulkan, WebGPU, Software, DirectX 9/11/12, SDL_GPU and OpenGL 4 equivalents). A default configure therefore reports CompiledEffects true on FNA3D only. Inventory the exact byte format of every shader asset: HLSL .fx source, DXBC and MonoGame MGFX are different inputs, none accepted at run time; cna-content can compile .fx at build time through an external legacy compiler (Tutorial 150), a route whose equivalence to Microsoft's compiler CNA has not verified.
ShaderEffect is the separate source-and-program route, and the accepted language depends on the renderer: GLSL on the GL paths, HLSL on the Direct3D paths, SPIR-V bytes on Vulkan, marker-gated renderer-specific payloads on others, and a deterministic refusal on the rest (which input each renderer takes). So the order of decisions is fixed: choose the target renderer set first, then the shader strategy. A game meant to stay portable may need one program per renderer family (Tutorial 151) or may be better served by staying within the stock effects.
Audit vertex layouts and model routes
A custom XNA VertexDeclaration is not guaranteed to fit every renderer or importer route. For each vertex format record element offsets, formats, usages, stride, index width, primitive topology and where the data comes from (XNB, glTF, CNJ or buffers the game fills). The glTF path packs into a small closed set of strides, and its upload path treats that set as an ABI (vertex packing); a game-defined layout is only guaranteed on the direct-buffer route.
Models reach the runtime through five content routes (XNB, CNB, self-contained CNJ, direct glTF/GLB and the legacy skinned-model manifest), each building a slightly different graph (the route table), plus hand-built runtime objects. Skinning adds separate index spaces for joints, scene nodes and the bone palette (skinning and animation). Preserve those spaces explicitly in the port, and do not infer that a model drawing correctly on one route and one renderer proves any other combination.
Select a renderer by evidence, not by name
A build chooses from 25 public renderer identities over 21 implementation families at configure time: CNA_GRAPHICS_RENDERER names one, and CNA_GRAPHICS_RENDERERS compiles a list from which the game selects at run time (runtime selection, Tutorial 126). Filter in this order:
- Admissibility. Some pairs are refused at configure time (
RendererSelection.cmake):OPENGLES3is refused under Emscripten, where the same EasyGL implementation is spelledWEBGL2; the Direct3D identities build only for Windows targets; theTERMINALplatform accepts only the CPU renderersSOFTWARE,PORTABLEGL,HEADLESSandSTUB. - Required capabilities. 2D or 3D (seven identities are 2D-only), compiled effects, MRT, render-target and texture formats, instancing, readback and occlusion queries. The default
GraphicsProfileis Reach, which refuses MRT, occlusion queries, 32-bit indices, float targets and large cubes on every renderer unlessHiDefis requested (Tutorial 152). QuerySupportsCapability()and theRendererCapabilityProfilereport rather than a renderer's name (Tutorial 133). - Evidence tier. Then read what has actually been observed for that identity on that host, which differs widely between families (what each kind of renderer can prove).
A useful progression starts on one well-verified desktop renderer, adds a structurally independent control, and only then crosses to the web or mobile. The cna-samples campaign fixed exactly such a reference pair: OPENGLES3 natively and its WEBGL2 spelling in the browser. Compile the same game against each identity; do not add renderer conditionals to game logic unless the capability contract requires them. A coloured fallback draw from an inherited default, or a test skipped on a headless renderer, is not a parity result.
Make service differences explicit
List every use of input, audio, media, storage, GamerServices, networking, sensors and platform dialogs, and write down what CNA does for each on every target host. The implementations are not equally deep:
| Service | What to record for the port at this snapshot | Detail |
|---|---|---|
| Audio | Four implementations (SDL3 default, SDL2, NULL, ALSA); the SOUND_ENABLED mixer exists for SDL3 and ALSA only | sound effects and streaming |
| Media | Song decodes Ogg Vorbis, MP3, WAV and FLAC through the mixer; Ogg Opus is not decoded (the vendored SDL3_mixer is built with SDLMIXER_OPUS=OFF and CNA's ALSA mixer refuses it by name), and there is no MIDI and no AAC route. Video types exist everywhere and throw NotSupportedException without the optional FFmpeg backend (CNA_ENABLE_VIDEO), which is never built for Windows, Emscripten, Android or iOS | media contract |
| Storage | The storage root is a host substitution; in a browser, saves live in memory unless the page adds persistence | web storage, Tutorial 141 |
| GamerServices | No online service; achievements and leaderboards persist locally as JSON, local identities are synthetic, the social Guide::Show* entry points are documented no-ops, and the message-box and keyboard-input dialogs are real | GamerServices contract |
| Networking | Only SystemLink uses a transport (ENet); a Local session delivers no packets; browser builds find no LAN sessions | network sessions |
| Sensors and host devices | Microsoft::Devices::Sensors types exist; what they report depends on the host | sensors and vibration |
| Input | Snapshots, logical mouse coordinates, touch and gestures follow XNA; text input and IME are host-dependent | input model |
Timing is its own line item
CNA's Game starts with a fixed step (IsFixedTimeStep true) of 166,667 ticks, about 16.67 ms or 60 Hz (Game.cpp); the setter refuses a zero or negative span with std::out_of_range, and accumulated time is clamped at 500 ms. Set TargetElapsedTime from the original's simulation instead of accepting the default: a game whose logic counts frames runs three times too fast at 60 Hz if it was written for 20 Hz. The Speedy Blupi 2013 port sets 500,000 ticks (50 ms), and the Free Eggbert study, whose original updates from a 50 ms multimedia timer, reaches the same number independently.
Speed-up features need a decision, not a shortcut. Running several fixed simulation steps per rendered frame (for example 1, 2, 4 or 8 for a four-speed control) keeps every step identical to the original; shortening TargetElapsedTime or rescaling per-frame constants changes the simulation step itself and must be verified as a new behaviour. Decide likewise how pause, loss of focus (InactiveSleepTime), slow frames (IsRunningSlowly), variable-step mode and shutdown map on every host; the exact clock semantics are on GameTime and the timestep, and the browser loop, which does not mirror every native semantic, on the web main loop.
Build a verification ladder for the port
Use increasingly independent checks, and keep the result of each rung separate:
| Rung | What it proves | CNA instruments |
|---|---|---|
| 1. Compile-time surface and negative compilation | The port uses the XNA-shaped API and nothing it should not | CNA_STRICT_XNA_API; the census of the API surface (auditing the XNA API surface) |
| 2. Deterministic logic tests with no renderer | Simulation equals the original trace | the HEADLESS or STUB renderer; a fixed TargetElapsedTime |
| 3. Asset-load tests, including malformed input | Every route in the manifest loads, and bad bytes fail cleanly | content input boundaries |
| 4. Single-pixel and small golden checks on the primary renderer | Draw calls produce the expected pixels | Tutorial 125; module *_test.cpp programs |
| 5. Differential screenshots or numeric traces against the frozen original | The port behaves like the game it replaces | the baseline captures; the oracle corpus for framework-level questions |
| 6. A second renderer as a localisation control | A mismatch is or is not renderer-specific | a structurally independent identity built from the same source |
| 7. Browser, compatibility-layer, emulator and physical-host evidence | The claimed host actually ran the claimed code path | engagement gates |
Record skips and the runtime identity with every result. A Wine result must prove that the translation layer (DXVK, vkd3d-proton or the DirectDraw path) was engaged; a browser result must reach a frame through an out-of-process verdict; a mobile result must retain its package, logs, screenshot and lifecycle trace. "Built everywhere" and "behaves everywhere" are separate milestones, and so are both from "compared with the original".
Troubleshoot from the boundary inward
Configure failures usually mean a renderer name outside the 25 public identities (a configure-time error), a missing sibling dependency (sharp-runtime must be its next branch), an inadmissible platform and renderer pair, or a feature option that changes which sources are compiled. Keep the exact configure command and the generated definitions (CNA_RENDERER_<ID>, plus CNA_MULTI_RENDERER in a multi-renderer build). Link failures usually expose module ownership (link the CNA modules whose public API the game uses, for example CNA::Runtime, CNA::Devices, CNA::GamerServices) or a missing external runtime library.
At run time, classify the first failure before editing anything:
| Boundary | Typical symptom | First evidence to collect |
|---|---|---|
| Asset resolution | ContentLoadException naming a missing file, or the wrong sibling loaded | working directory, content root, the ladder rung that answered |
| Reader registration | an unregistered reader name in the XNB reader table | the reader table of the file; the custom-type registration |
| Window and device creation | failure before the first Draw | platform, renderer, profile, driver |
| Frame lifetime | state errors, use after Dispose, SpriteBatch misuse | the HEADLESS renderer's trace mode, which names the violated rule |
| Renderer engagement | right API calls, wrong or no pixels | the same build on a second identity |
| Behavioural oracle | runs, but differs from the original | the frozen baseline; XNA or FNA for the disputed call |
Preserve the original exception (ContentLoadException wraps the inner error with the asset name) and reduce to the smallest asset or draw that reproduces it; investigating a sample failure gives the controls that each flip one variable. The fastest migration is not the one with the fewest red results but the one in which every red result names the boundary that failed.
Evidence and limits
Checked by reading the TARGET sources named above at 009d40f5 (timestep defaults and setter in Game.cpp, the XNB decompression and Effect readers, RendererSelection.cmake's refusals, the strict-API harness wiring); not built or executed. Counts of renderer identities, compiled-effect identities and built-in readers are the site's canonical TARGET figures. The cna-samples practices are that repository's own plan at 4da98a0 (2026-09-20); the two timing examples come from mobile-eggbert at d43c6a1 and the Free Eggbert feasibility document, and describe those projects, not CNA. The manifest table is illustrative, and the verification ladder is a method: no CNA gate enforces it for a consumer game.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Migrating from MonoGame / XNA · Tutorial 84: Migrating from XNA · Tutorial 83: Migrating from MonoGame/FNA
- Architecture
- Architecture overview
- Internals
- Renderer selection internals
- Maintainer workflow
- Investigate a sample failure