Tutorial 84: Migrating from XNA to CNA

CNA — C++ XNA 4.0 reimplementation

ℹ

What you’ll learn

  • Where CNA's surface is identical to XNA 4.0 and where C++ forces a difference.
  • What the CNAEXT marker tells you when porting.
  • The state of XACT, of XNB loading and of the build-time content pipeline.
  • Using CNA's own test suite to check a port.

Before you start — Tutorial 01: Introduction to CNA — it introduces the CNAEXT marker and the C++ semantics this depends on. Prior XNA experience is assumed.

XNA historical context

Microsoft XNA Game Studio 4.0 was released in 2010 and discontinued in 2014. CNA's primary goal is to be a C++ drop-in replacement for XNA 4.0 — the same namespaces, class names, method signatures, and behavior. If you have an XNA 4.0 game in C#, CNA gives you a migration path to a maintained, cross-platform C++ codebase that runs on Linux, Windows, macOS, Android, and the web.

Identical API surface goal

CNA represents every public type and every documented member of the XNA 4.0 runtime assemblies: the repository’s audit script (tools/audit_xna_runtime_surface.py, checked against Microsoft’s XML documentation and the assembly metadata) counts 331 of 331 public types and 3,627 of 3,627 documented runtime members as present. That is a statement about representation — the symbol exists with the XNA shape — not a claim that every member behaves like XNA; behaviour is what the tests and the oracle corpus below are for. Microsoft.Xna.Framework.Design (the 13 TypeConverter classes) is included too, as the opt-in CNA::Design module: link it explicitly, it is not part of the CNA umbrella target, and it is a tooling facility (there is no Windows Forms property grid in C++). Rather than publish a blended coverage percentage of behaviour — no reproducible one exists — CNA offers a CNA_STRICT_XNA_API purity mode that turns any use of a CNA extension into a compile error. It is a compile definition you set on the target you want checked (a CMake -D option of that name does nothing), and only the Input namespace additionally has signature-freeze tests. Every class in Microsoft.Xna.Framework has a C++ counterpart in Microsoft::Xna::Framework. Class and method names use the same capitalization (XNA's PascalCase is preserved), with one systematic difference: C++ has no properties, so an XNA property Foo is exposed as getFooProperty() / setFooProperty(value). Enum values are identical. The goal is that the XNA 4.0 documentation at learn.microsoft.com applies directly to CNA once you apply that one rule.

What's the same

  • Game, GameComponent, DrawableGameComponent lifecycle (Initialize, LoadContent, Update, Draw, UnloadContent)
  • GraphicsDevice: Clear, Present, DrawPrimitives, DrawIndexedPrimitives, SetVertexBuffer, and the index buffer through setIndicesProperty() (XNA’s Indices; a SetIndexBuffer() convenience also exists but is a CNA extension)
  • SpriteBatch: Begin, End, Draw (all overloads), DrawString
  • Texture2D, RenderTarget2D, Texture3D, TextureCube
  • All Effect subclasses: BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect
  • Math types: Vector2, Vector3, Vector4, Matrix, Quaternion, Color, Rectangle, BoundingBox, BoundingSphere, Ray, Plane
  • Input: Keyboard, Mouse, GamePad, TouchPanel
  • Audio: SoundEffect, SoundEffectInstance
  • ContentManager (reads .xnb binaries as well as CNA’s .cnb and loose files)

What's different (C++ semantics, no GC, CNAEXT markers)

C++ requires explicit resource management. Any XNA class that implements IDisposable in C# has a destructor in CNA that releases GPU resources. Use std::unique_ptr where you would have used using blocks in C#.

APIs that are not part of XNA 4.0 — CNA additions — are marked CNAEXT in the CNA headers (in the XNA-shaped namespaces; the opt-in CNA::Graphics extensions — retro post-process effects and DebugDraw — are instead gated by CNA_CNAEXT). Examples: Texture2D(assetName, device) (load an image file directly), StorageDevice::SetAppNameEXT, and TouchPanel::setMouseTouchEmulationEnabledEXT. A few XNA APIs are honest, documented approximations rather than extensions: GraphicsAdapter::IsProfileSupported answers true on renderers that have no capability structure to consult, and the Guide is CNA’s own console-style system UI, drawn by CNA, rather than the Xbox one. GamerServices (Gamer, SignedInGamer, GamerProfile, leaderboards, Guide, achievements, and the Avatar subsystem) is a complete API port, and it is not Xbox LIVE. With no service configured, gamers are local offline profiles: nobody is signed in until Guide::ShowSignIn or the auto-sign-in setting signs a profile in, and achievements and leaderboards are kept locally. Local and SystemLink sessions work offline. NetworkSessionType::PlayerMatch and Ranked, and JoinInvited, go through the optional CNA Gamer Services server and its relay, and throw GamerServicesNotAvailableException when none is configured. See Gamer Services.

Two changes since alpha.1 bring CNA closer to the real runtime and matter for a port. The game clock is now XNA’s: the first Update runs with ElapsedGameTime == 0 and TotalGameTime advances after Update returns (see Tutorial 48). And GraphicsDeviceManager defaults to the Reach profile, enforced on every renderer, so code that used HiDef-only features in XNA (multiple render targets, occlusion queries, 32-bit indices, float render targets) must request GraphicsProfile::HiDef explicitly, for example with graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef).

XACT (now real)

The Microsoft::Xna::Framework::Audio::AudioEngine/WaveBank/SoundBank/Cue (XACT) classes implement a real .xgs/.xsb/.xwb parser with mixer-backed playback (SDL3_mixer by default, CNA’s own mixer with the ALSA audio implementation) — category/lifecycle/3D positional audio/instance-limit enforcement with fade in/out and continuous RPC volume/pitch curves are all real. XNA games that use XACT can carry their existing .xap-authored audio projects over rather than rewriting to SoundEffect’s own constructors. A couple of narrow, documented deviations remain (no HRTF/elevation, no AttackTime/ReleaseTime envelope tracking). One behaviour to check your banks against: a WaveBank containing XMA or WMA content does not throw — it logs to stderr and returns nullptr, so the sound is simply missing. The XNB SoundEffectReader path does throw. 3D audio accepts any number of AudioListeners as in XNA (the nearest one decides attenuation, pan and Doppler); only a zero or negative count is an error. Ordering matters, as it does in XNA: calling Apply3D after Play() on an instance that is in pan mode throws InvalidOperationException, so set 3D positions before you play.

XNB support and the content pipeline

XNA's content pipeline produces .xnb binary files, and CNA reads them. A real XNB reader is wired into ContentManager with 61 built-in type readers (60 where the platform has no native 128-bit integer), a real LZX decompressor (and LZ4 for MonoGame-style files) and two-pass shared-resource resolution, so getContentProperty().Load<Texture2D>("name") can stay exactly as written (note that Load returns the asset by value). A Game registers the readers when it is constructed; only a stand-alone ContentManager still needs CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() once at startup.

The other direction now exists as well. Since alpha.1 CNA has a build-time content pipeline that a shipped game never links: the cna-content tool builds .cnb (CNA’s own container, the default) or .xnb from source assets, behind an XNA-shaped Microsoft::Xna::Framework::Content::Pipeline layer (all 128 public types and 705 members of XNA 4.0’s pipeline API are represented, with 10 importers and 12 processors). So the old advice “keep authoring in XNA Game Studio” is now optional: you can keep your built .xnb files, or rebuild content from its sources with cna-content build (or cna_add_content(...) in CMake).

Two boundaries shape what you can bring across. There is no reflection-based reader discovery, but custom readers can be registered explicitly through ContentTypeReader<T>, AddTypeCreator or the CNAEXT ReflectiveTypeReaderBuilder<T>, which reads custom types from .xnb given a declared field list. The XNB EffectReader accepts XNA/FNA D3D9 Effect Framework bytecode only on FNA3D or on a renderer whose compiled-effects build option you enabled (eight default-OFF options: EasyGL, Vulkan, WebGPU, Software, DirectX 9, DirectX 11, SDL_GPU and Metal); it does not accept MGFX or DXBC, and HLSL .fx source is never compiled at run time. cna-content can build .fx source into an effect .xnb, but only through an external legacy fxc compiler that you provide (CNA does not itself check that output against a genuine fxc). Where you prefer to drop the pipeline, construct assets directly from loose files instead — Texture2D("assets/name.png", gd) — or use the cna_tool_gltf_to_cnj converter. Better still for models: CNA loads glTF 2.0 at runtime — drop a .gltf/.glb in the content root and getContentProperty().Load<Model>("name") works with no tooling step at all, including skeletal animation and morph targets. The offline converter is only needed for multi-mesh-group files (the runtime emits one asset per group) or non-metre unit scaling. See the XNB Loading & Interoperability reference.

Testing against CNA's own suite

At this snapshot, CNA has 813 C++ test source files and 11,380 statically discoverable GoogleTest-family definitions (counted from the TEST/TEST_F/TEST_P/TYPED_TEST macros; 781 of the files contain such a macro) covering math, geometry, game-loop semantics, content, renderers and more, plus 781 standalone examples/**/*_test.cpp pixel programs that are not part of those figures. The tests actually compiled and registered with CTest vary with renderer, platform, audio implementation and options. Run ctest --test-dir build after migrating: a green run over the math and geometry tests is strong evidence that your numerical behaviour matches XNA.

The sharpest correctness gate CNA has is its XNA oracle corpus: 39 scenes (256×256, HiDef) with reference images captured from the genuine Microsoft XNA 4.0 runtime, run under Wine + DXVK on Linux (so the reference is XNA through DXVK, not through a Windows GPU driver). The DIRECTX9 renderer was recorded in the repository as matching all 39 at tolerance 0 through that same Wine + DXVK path; the check is not part of CI, and the last consolidated dated report covers 31 scenes (later per-scene notes record the rest). Other renderers are gated on far less — EasyGL and SOFTWARE on two line scenes only (an EasyGL difference fails the test; a SOFTWARE difference does not, only a failure to render), FNA3D only on its scenes rendering — and SOFTWARE was measured byte-exact on 18 of the 39. If your migration goal is bit-identical output, DIRECTX9 is the renderer to target (it runs on Windows, or under Wine), and no other one substitutes for it. Separately, the same repository holds a set of 32 cross-renderer parity fixtures (registered by EasyGL, WebGPU, SDL_GPU and Direct3D 11) whose expected values are the fixtures’ own assertions, not real-XNA output.

XNA C# class to CNA C++ migration example

// ===== Original XNA 4.0 C# =====
// using Microsoft.Xna.Framework;
// using Microsoft.Xna.Framework.Graphics;
//
// public class MyXnaGame : Game {
//     GraphicsDeviceManager graphics;
//     SpriteBatch spriteBatch;
//     Texture2D texture;
//
//     public MyXnaGame() {
//         graphics = new GraphicsDeviceManager(this);
//         Content.RootDirectory = "Content";
//     }
//
//     protected override void LoadContent() {
//         spriteBatch = new SpriteBatch(GraphicsDevice);
//         texture = Content.Load<Texture2D>("logo");
//     }
//
//     protected override void Update(GameTime gameTime) {
//         if (Keyboard.GetState().IsKeyDown(Keys.Escape))
//             Exit();
//         base.Update(gameTime);
//     }
//
//     protected override void Draw(GameTime gameTime) {
//         GraphicsDevice.Clear(Color.CornflowerBlue);
//         spriteBatch.Begin();
//         spriteBatch.Draw(texture, Vector2.Zero, Color.White);
//         spriteBatch.End();
//         base.Draw(gameTime);
//     }
// }

// ===== Equivalent CNA C++ =====
#include <memory>
#include <optional>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Content/ContentManager.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;

class MyGame final : public Game {
public:
    MyGame() : graphics_(this) {
        getContentProperty().setRootDirectoryProperty("Content");
    }

protected:
    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
        // The XNA route: Load<T> returns the Texture2D by value and finds
        // Content/logo.xnb, logo.cnb or logo.png.
        // (CNAEXT alternative: Texture2D("assets/logo.png", gd) reads a file directly.)
        texture_ = getContentProperty().Load<Texture2D>("logo");
    }

    void Update(GameTime& gameTime) override {
        if (Keyboard::GetState().IsKeyDown(Keys::Escape))
            Exit();
        Game::Update(gameTime);    // base.Update: components and FrameworkDispatcher
    }

    void Draw(const GameTime& gameTime) override {
        getGraphicsDeviceProperty().Clear(Color::CornflowerBlue);
        spriteBatch_->Begin();
        spriteBatch_->Draw(*texture_, Vector2::Zero, Color::White);
        spriteBatch_->End();
        Game::Draw(gameTime);      // base.Draw; Game presents the frame afterwards, as in XNA
    }

private:
    GraphicsDeviceManager           graphics_;   // declared first: destroyed last
    std::unique_ptr<SpriteBatch>    spriteBatch_;
    std::optional<Texture2D>        texture_;
};

int main() { MyGame game; game.Run(); }