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 and of read-side XNB support.
- 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 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,DrawableGameComponentlifecycle (Initialize, LoadContent, Update, Draw, UnloadContent)GraphicsDevice: Clear, Present, DrawPrimitives, DrawIndexedPrimitives, SetVertexBuffer, SetIndexBufferSpriteBatch: 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 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(); }