Tutorial 83: Migrating from MonoGame/FNA to CNA

CNA — C++ XNA 4.0 reimplementation

What you’ll learn

  • The namespace and type mappings from C# MonoGame to C++ CNA.
  • What changes about memory management once there is no GC.
  • Content loading and audio differences.
  • Which APIs have no CNA equivalent yet.

Before you startTutorial 01: Introduction to CNA — it covers the C++-versus-C# differences this builds on. Prior MonoGame or FNA experience is assumed; no earlier tutorial is strictly required.

Why migrate?

CNA offers C++23 performance, no garbage collector pauses, and the same XNA 4.0 API surface you already know from MonoGame or FNA. Games ported from MonoGame/FNA keep their architecture intact — the Game loop, SpriteBatch calls, Effect hierarchy, and math types are all preserved. Only the language changes from C# to C++.

Namespace changes

CNA uses the same Microsoft::Xna::Framework namespace hierarchy as MonoGame, expressed in C++ :: notation instead of C# . notation.

C# NamespaceC++ Namespace
Microsoft.Xna.FrameworkMicrosoft::Xna::Framework
Microsoft.Xna.Framework.GraphicsMicrosoft::Xna::Framework::Graphics
Microsoft.Xna.Framework.AudioMicrosoft::Xna::Framework::Audio
Microsoft.Xna.Framework.InputMicrosoft::Xna::Framework::Input
Microsoft.Xna.Framework.MediaMicrosoft::Xna::Framework::Media

C# to C++ type mapping

C# TypeC++ CNA TypeNotes
intintcs (= int32_t)sharp-runtime alias
floatSingle (= float)or just use float
doubledoublesame
boolboolsame
bytebytecs (= uint8_t)sharp-runtime alias
stringstd::stringno implicit conversions
List<T>System::Collections::Generic::List<T>from sharp-runtime, or std::vector<T>. Count is getCountProperty()
nullnullptrpointer null
? nullablestd::optional<T>or raw pointer

Memory management

MonoGame relies on the .NET GC. In CNA you manage lifetimes explicitly. Use std::unique_ptr<T> for owned resources (most game objects), std::shared_ptr<T> when shared ownership is genuinely needed, and pass by reference to avoid unnecessary copying. All CNA resource classes (Texture2D, VertexBuffer, etc.) implement RAII — they release GPU resources in their destructor so you never need to call a separate Dispose().

Content loading differences

MonoGame uses .xnb binary files produced by the XNA content pipeline. CNA reads those too. It implements the XNB container format, LZX decompression, and 50 built-in content type readers (49 when FFmpeg is absent, which drops VideoReader), so an existing ContentManager::Load<T>() call over pre-built .xnb assets carries across.

Note the direction: this is read-side XNB support, not a content pipeline. CNA has no ContentCompiler, ContentImporter or ContentProcessor and never will — authoring stays on the XNA/MonoGame/FNA side, or moves to CNA's own formats.

Readers are not auto-registered. Call RegisterAllBuiltInXnbReaders() once at startup or every .xnb load fails. It currently lives in an Internal header, which is a known ergonomics gap.

Compiled effects need a format and renderer check. Alpha.1's EffectReader accepts XNA/FNA D3D9 Effect Framework binaries on FNA3D, or on SDL_GPU, EasyGL-family and Vulkan builds whose renderer-specific compiled-effects option is enabled. It does not compile HLSL .fx source and does not accept DXBC or MGFX. On other renderers, migrate the effect to ShaderEffect in that renderer's native language. The stock effects remain a separate working path.

CNA additionally lets you skip .xnb entirely and load assets straight from source files, which is usually simpler for a new project. Load<T> tries .xnb first, then CNA's own .cnj JSON format, then the older loose-file .model.json:

  • Textures: Texture2D("assets/logo.png", gd) — PNG/JPG loaded via SDL3_image
  • Models: Model::Load("assets/house.obj", gd) via the model loader — and, newer and more useful for a migration, runtime glTF 2.0: drop a .gltf or .glb in the content root and Content.Load<Model>("name") loads it with no tooling step, including skeletal animation and morph targets
  • Fonts: a JSON-based SpriteFont descriptor, as an alternative to .spritefont XML compiled to XNB

Audio (SoundEffect same, XACT now real)

SoundEffect and SoundEffectInstance APIs are identical to MonoGame. SoundEffect("sound.wav") (the CNAEXT assetName constructor) loads OGG/WAV via SDL3_mixer. Song/ MediaPlayer streaming is also fully implemented via SDL3_mixer. XACT (.xap-authored AudioEngine/SoundBank/WaveBank/Cue) is now a real, largely functional runtime with a genuine .xgs/.xsb/.xwb parser — MonoGame games that use XACT do not need to be rewritten to use SoundEffect directly, though it remains the simpler option for one-off sounds.

Two audio gaps to check your assets 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.) And .m4a/.aac are unplayable: SDL3_mixer has no AAC decoder.

Missing APIs

A few MonoGame extensions are not present in CNA:

  • VideoPlayer — partial (see the Video Playback tutorial)

Two things that are not missing

Storage works. Microsoft::Xna::Framework::Storage is fully functional: StorageDevice, StorageContainer, and StorageDeviceNotConnectedException are all implemented. StorageContainer gives you CreateFile(), OpenFile(), FileExists(), DeleteFile(), GetFileNames(), and the matching directory operations, with file access returning a std::unique_ptr<System::IO::Stream>. Save-game code written against XNA's storage API does not need rewriting to raw C++ file I/O.

Message boxes work. CNA provides CNA::Devices::MessageBox, which pops a real native modal dialog through SDL. You do not need to call SDL_ShowMessageBox yourself:

#include "CNA/Devices/MessageBox.hpp"
#include "CNA/Devices/MessageBoxType.hpp"

using CNA::Devices::MessageBox;
using CNA::Devices::MessageBoxType;

// Simple notification.
MessageBox::ShowSimple(MessageBoxType::Error,
                       "Save failed",
                       "The save file could not be written.");

// Multi-button prompt: returns the index of the clicked button,
// or -1 if the dialog could not be shown.
int choice = MessageBox::Show(MessageBoxType::Warning,
                              "Unsaved changes",
                              "Quit without saving?",
                              {"Cancel", "Quit"});

MessageBoxType is Error, Warning, or Information. Check MessageBox::getIsSupportedProperty() first if you target a platform where a native dialog may not be available.

Side-by-side: C# MonoGame vs C++ CNA

// ===== C# MonoGame =====
// using Microsoft.Xna.Framework;
// using Microsoft.Xna.Framework.Graphics;
//
// public class Player {
//     private Texture2D sprite;
//     private Vector2 position;
//
//     public Player(ContentManager content) {
//         sprite = content.Load<Texture2D>("player");
//         position = new Vector2(100, 200);
//     }
//
//     public void Update(GameTime gt) {
//         float dt = (float)gt.getElapsedGameTimeProperty().getTotalSecondsProperty();
//         position.X += 100.0f * dt;
//     }
//
//     public void Draw(SpriteBatch sb) {
//         sb.Draw(sprite, position, Color.White);
//     }
// }

// ===== C++ CNA equivalent =====
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"

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

class Player {
public:
    Player(GraphicsDevice& gd)
        : sprite_("assets/player.png", gd)   // direct load, no ContentManager
        , position_(100.0f, 200.0f)
    {}

    void Update(GameTime& gt) {
        float dt = static_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty());
        position_.X += 100.0f * dt;
    }

    void Draw(SpriteBatch& sb) {
        sb.Draw(sprite_, position_, Color::White);
    }

private:
    Texture2D sprite_;   // owns GPU resource, released in destructor
    Vector2   position_;
};