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 and of read-side XNB support.
  • Using CNA's own test suite to check a port.

Before you startTutorial 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 covers the public Microsoft.Xna.Framework namespace surface except Framework.Design, a Windows Forms designer namespace with no C++ equivalent. Rather than publish a blended coverage percentage — no reproducible one exists — CNA pins its public API with compile-time signature-freeze tests and a CNA_STRICT_XNA_API purity mode that turns any use of a CNA extension into a compile error. Every class in Microsoft.Xna.Framework has a C++ counterpart in Microsoft::Xna::Framework. Method names use the same capitalization (XNA's PascalCase is preserved). Enum values are identical. The goal is that the XNA 4.0 documentation at learn.microsoft.com applies directly to CNA without translation.

What's the same

  • Game, GameComponent, DrawableGameComponent lifecycle (Initialize, LoadContent, Update, Draw, UnloadContent)
  • GraphicsDevice: Clear, Present, DrawPrimitives, DrawIndexedPrimitives, SetVertexBuffer, SetIndexBuffer
  • 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 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 cannot be implemented due to platform or design constraints are marked // CNAEXT in the CNA headers. Examples: GraphicsAdapter.IsProfileSupported (always returns true), StorageDevice.BeginShowSelector (UI-based storage selection). GamerServices (Gamer, SignedInGamer, GamerProfile, leaderboards, Guide, achievements, and the Avatar subsystem) has a complete API port with local/synthetic semantics — the same approach FNA itself takes for this namespace, since it's not binary-compatible with real Xbox Live.

XACT (now real)

The Microsoft::Xna::Framework::Audio::AudioEngine/WaveBank/SoundBank/Cue (XACT) classes implement a real .xgs/.xsb/.xwb parser with SDL3_mixer-backed playback — 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. Also note 3D audio supports exactly one AudioListener; a second throws.

Read-side XNB support

XNA's content pipeline produces .xnb binary files, and CNA reads them. Read-side only: CNA has no ContentCompiler, ContentImporter or ContentProcessor, and never will — that is explicitly out of scope. A real XNB reader is wired into ContentManager with 50 built-in type readers (49 without FFmpeg, which drops VideoReader), a real LZX decompressor and two-pass shared-resource resolution, so content.Load<Texture2D>("name") can stay exactly as written. Call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() once at startup — the registry is deliberately empty by default.

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> and AddTypeCreator. The XNB EffectReader accepts XNA/FNA D3D9 Effect Framework bytecode only on FNA3D or an opted-in SDL_GPU, EasyGL-family or Vulkan build; it does not accept HLSL .fx source, DXBC or MGFX. 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 Content.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 Content Pipeline reference.

Testing against CNA's own suite

At alpha.1, CNA has 568 C++ test source files and 8,263 statically discoverable GoogleTest-family definitions covering math, geometry, game-loop semantics, content, renderers and more. 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 captured from the genuine Microsoft XNA 4.0 runtime. The DIRECTX9 renderer matches all 39 at tolerance 0. Other renderers match far fewer — the EasyGL family 10, FNA3D 10, OPENGLES1 11 — so if your migration goal is bit-identical output, DIRECTX9 is the renderer to target and no other one substitutes for it.

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 = getContentProperty().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 "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"

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) {}

protected:
    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
        // Direct file load — bypasses ContentManager entirely.
        // ContentManager can also read .xnb; see the read-side XNB docs.
        texture_ = std::make_unique<Texture2D>("assets/logo.png",
                                               getGraphicsDeviceProperty());
    }

    void Update(GameTime& gameTime) override {
        if (Keyboard::GetState().IsKeyDown(Keys::Escape))
            Exit();
    }

    void Draw(const GameTime& gameTime) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::CornflowerBlue);
        spriteBatch_->Begin();
        spriteBatch_->Draw(*texture_, Vector2::Zero, Color::White);
        spriteBatch_->End();
        gd.Present();
    }

private:
    GraphicsDeviceManager           graphics_;
    std::unique_ptr<SpriteBatch>    spriteBatch_;
    std::unique_ptr<Texture2D>      texture_;
};

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