Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 74 Migrating an XNA or FNA Game to CNA

CNA preserves much of XNA’s public shape in C++; it does not make a C# game binary- or source-compatible. A successful migration is a controlled translation with explicit asset, renderer, service, and verification decisions. The steps below use the implementation and evidence boundaries established in the preceding Parts rather than CNA’s stale migration prose.

Porting begins by freezing the original game's behavioral and asset baseline. Translate API shape mechanically and compile against CNA's strict XNA surface. Inventory every asset and select a concrete XNB, CNJ, native, or glTF route. Choose renderer and shader strategies, adapt ownership and services, then compare behavior and pixels against the baseline. A mismatch loops back to the smallest responsible boundary before deployment across the intended platform matrix.
Figure 74.1: A porting workflow that preserves attribution. Translation, asset conversion, renderer choice, and service adaptation are verified as separable changes.

74.1 Freeze a behavioral baseline first

Before changing language or framework, record the original game at a named commit and capture inputs that matter: deterministic simulation traces, representative screenshots, save files, network packets, audio events, and asset inventories. Record resolution, fixed-step interval, random seeds, locale, and platform. A port without a baseline can become internally consistent while drifting from the game it was meant to preserve.

Classify the starting point. A C# XNA or FNA game requires a language translation; an existing C++ reconstruction may need only its platform-service boundary replaced. Keep these risks separate. Translating game logic and redesigning rendering in one pass destroys the ability to attribute a mismatch.

74.2 Translate the API shape mechanically

Namespaces and type/member names generally map directly from dots to C++ scopes. The largest systematic difference is properties: a C# read of obj.Value becomes obj.getValueProperty(), and an assignment becomes obj.setValueProperty(value). Events, nullable values, collections, ownership, and ref/out arguments also need explicit C++ idioms.

Do not redesign names while translating. First reach compiling, behaviorally comparable code; then isolate genuinely game-owned abstractions. Compiler errors are useful inventory signals when the translation remains mechanical. Broad wrapper layers that hide every mismatch too early can make an unimplemented CNA behavior look like a game bug.

74.3 Inventory content by actual reader route

CNA can read real XNB files. Its binary stack handles the container, LZX, a registered reader table, shared resources, stock effects, graphics assets, models, and several audio encodings, with 30 checked-in fixtures at the audited revision. Old statements that XNB is entirely unsupported are false.

The boundary is still narrower than XNA’s build pipeline. CNA does not produce XNB files, the reader registry is explicit rather than reflective, several reader forms remain unsupported, and LZ4 is refused. The general EffectReader accepts XNA/FNA D3D9 Effect Framework bytecode only when the active renderer reports CompiledEffects; the five stock-effect XNB readers remain separate. Test every concrete asset against the selected route rather than reasoning from its extension alone.

Loose assets use CNJ envelopes, native image/audio/video formats, or direct glTF/GLB model loading. Runtime glTF and offline gltf_to_cnj share one import core but retain material, animation, and multi-group limitations described in Part VI. A migration manifest should map every source asset to one of these routes, its expected reader, sidecars, and verification oracle.

74.4 Treat custom shaders as renderer contracts

Effect(GraphicsDevice&, byte[]) and the general XNB Effect reader now accept bounded XNA/FNA D3D9 Effect Framework binaries on FNA3D. SDL_GPU, EasyGL and Vulkan expose the same capability only behind their opt-in compiled-effects switches. Inventory the exact byte format and target configuration: .fx/HLSL source, DXBC and MonoGame MGFX remain different inputs.

ShaderEffect is the separate source/program route, and accepted language varies by renderer: GLSL on several GL paths, HLSL on Direct3D paths, SPIR-V bytes on Vulkan, marker-gated SkSL on Skia, and explicit refusal or null on others. Choose the target renderer before choosing a shader migration strategy. A portable game may need renderer-specific programs or may need to stay within stock effects.

74.5 Audit vertex layouts and model assumptions

The glTF importer recognizes seven packed strides, while the upload path has eight cases because one semantic form expands the dispatch. A custom XNA VertexDeclaration is not guaranteed to fit every renderer or importer route. Record element offsets, formats, usages, stride, index width, primitive topology, and whether data comes through XNB, glTF, CNJ, or direct buffers.

Models have four routes with different evidence: hand-built runtime objects, binary XNB, runtime glTF/GLB, and CNJ/offline import. Skinning also has two ordering systems. Preserve joint, scene-node, and palette index spaces explicitly; do not infer that a model drawing on one renderer proves every route or renderer.

74.6 Select a renderer by evidence, not by name

Renderer selection is a CMake-time choice from 50 public identities backed by 46 families. First filter by platform admissibility and required capabilities: 2D/3D, custom effects, MRT, render targets, texture forms, instancing, readback, and occlusion queries. Then inspect the evidence tier for the exact identity and host.

A useful progression begins with one well-verified desktop renderer, adds a structurally independent control, and only then crosses to Web or mobile. Compile the same game against each identity; do not add renderer-specific conditionals to game logic unless the capability contract requires them. A colored inherited draw fallback or a headless skip is not a parity result.

74.7 Make service differences explicit

List every use of input, audio, media, storage, GamerServices, networking, sensors, and platform dialogs. CNA’s implementations are not equally deep: local GamerServices identities are synthetic, Storage containment differs between selectors and files, network Local delivery is not a normal transport, Web discovery and persistence diverge, Android lifecycle coverage is partial, and media depends on the FFmpeg configuration.

Timing deserves its own line item. Set TargetElapsedTime from the original simulation instead of accepting CNA’s default. Decide how pause, background, slow frames, variable-step mode, and shutdown map on every host; the Emscripten loop does not yet mirror all native semantics.

74.8 Build a verification ladder for the port

Use increasingly independent checks:

  1. 1.

    compile-time surface and negative-compilation checks;

  2. 2.

    deterministic logic tests with no renderer;

  3. 3.

    asset-load tests for every reader form and malformed boundary;

  4. 4.

    single-pixel and small golden checks on the primary renderer;

  5. 5.

    differential screenshots or numeric traces against the frozen original;

  6. 6.

    a second renderer as a localization control;

  7. 7.

    browser, compatibility-layer, emulator, and physical-host engagement evidence.

Record skips and runtime identity. A Wine result must prove DXVK/vkd3d/DirectDraw engagement; a browser result must reach a frame through a window verdict; a mobile result must retain its APK, logs, screenshot, and lifecycle trace. “Built everywhere” and “behaves everywhere” are separate milestones.

74.9 Troubleshoot from the boundary inward

Configure failures usually indicate a wrong public renderer name, a missing sibling dependency, an inadmissible platform/renderer pair, or a feature option that changes the source closure. Link failures often expose module ownership or missing external runtime libraries. Keep the exact configure command and generated renderer definition.

At runtime, classify the first failure before editing: asset resolution, reader registration, window/device creation, frame lifetime, renderer engagement, or behavioral oracle. Log working directory and content root; preserve the original exception; test the smallest asset or draw that reproduces it. The fastest migration is not the one with the fewest red results, but the one in which every red result names the boundary that actually failed.

Type at least three characters. Results are ranked by how often and where the words occur.