Tutorial 84: Migrating from XNA to CNA
What you’ll learn
- Where CNA's surface is identical to XNA 4.0 and where C++ forces a difference.
- What the
CNAEXTmarker 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,DrawableGameComponentlifecycle (Initialize, LoadContent, Update, Draw, UnloadContent)GraphicsDevice: Clear, Present, DrawPrimitives, DrawIndexedPrimitives, SetVertexBuffer, and the index buffer throughsetIndicesProperty()(XNA’sIndices; aSetIndexBuffer()convenience also exists but is a CNA extension)SpriteBatch: Begin, End, Draw (all overloads), DrawStringTexture2D,RenderTarget2D,Texture3D,TextureCube- All
Effectsubclasses: 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
.xnbbinaries as well as CNA’s.cnband 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(); }
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Migrating an XNA game to CNA: a process that keeps every mismatch attributable — A porting process for XNA and FNA games at this snapshot: freeze a baseline, translate mechanically, map assets and shaders to real routes, pick renderers by evidence and verify in rungs.