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 start — Tutorial 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. The language changes from C# to C++, and one habit changes with it: C# properties become accessor functions (below).

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, SharpRuntime::intcs
floatSharpRuntime::Single (= float)or just use float
doubledoublesame
boolboolsame
bytebytecs (= uint8_t)sharp-runtime alias, SharpRuntime::bytecs
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
C# property Foo (x.Foo, x.Foo = v)x.getFooProperty(), x.setFooProperty(v)the C++ language has no properties; read-only ones have only the getter
Content (a member of Game)getContentProperty()Load<T>("name") returns the asset by value (a Texture2D is a cheap shared handle), not a reference or pointer
GraphicsDevice, Window, ComponentsgetGraphicsDeviceProperty(), getWindowProperty(), getComponentsProperty()getWindowProperty().setTitleProperty("My Game")

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() (it exists, as in XNA, for early release). Declare the GraphicsDeviceManager member first and your GPU resources after it, so they are destroyed first; Tutorial 71 explains destruction order and when UnloadContent() runs.

Content loading differences

MonoGame uses .xnb binary files produced by its content pipeline (MGCB), and XNA and FNA use the same container. CNA reads those too. It implements the XNB container format, LZX and LZ4 decompression (MonoGame’s LZ4-compressed XNBs decode), and 61 built-in content type readers (60 where the platform has no native 128-bit integer, which drops the decimal reader). The VideoReader is always registered, even when the video decoder is not built. An existing getContentProperty().Load<T>("name") call over pre-built .xnb assets carries across: the default root directory of the game’s content manager is Content, and the extension is resolved for you.

Since alpha.1 the picture is no longer “read-side only”. The runtime ContentManager is still a loader, and a shipped game links no importer, processor or compiler. But CNA now has a build-time content pipeline: the cna-content tool turns source files (or an MSBuild-free .contentproj) into .cnb (CNA’s own compiled container, the default) or .xnb, and an XNA-shaped importer/processor/compiler layer (Microsoft::Xna::Framework::Content::Pipeline) sits behind it. It reads images, .wav/song/video sources, .gltf/.glb, .fbx/.x models, .spritefont, typed .cnj descriptors and even existing .xnb files. So a migration can either keep the built .xnb assets or rebuild them from the sources with cna-content build, wired into CMake with cna_add_content(...).

⚠

Reader registration. A Game registers the built-in readers for you when it is constructed. Only a stand-alone ContentManager that is not owned by a Game (a tool, a test) still has to call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() once, and that function lives in an Internal header.

Compiled effects need a format and renderer check. CNA’s EffectReader accepts XNA/FNA D3D9 Effect Framework binaries. In this snapshot that works on FNA3D (always) and on the renderers whose compiled-effects build option you switched on (the eight default-OFF options, for EasyGL, Vulkan, WebGPU, Software, DirectX 9, DirectX 11, Metal and SDL_GPU); a default configure reports compiled-effects support on FNA3D only. It does not accept MonoGame’s MGFX effects or raw DXBC, and the runtime never compiles HLSL. cna-content can build a .fx file into an effect .xnb at build time, but only by calling an external legacy fxc compiler that you supply. 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 the compiled formats and load assets straight from source files, which is usually simpler for a new project. Load<T> tries .xnb first, then CNA's own .cnb, then the loose-file readers (.cnj JSON descriptors and each type’s native formats):

  • Textures: getContentProperty().Load<Texture2D>("logo") finds logo.png (also .jpg, .bmp, .gif, .tga, .tif, .qoi), decoded by CNA’s own image loader. The CNAEXT constructor Texture2D("assets/logo.png", gd) reads a file directly.
  • Models: there is no runtime OBJ loader and no Model::Load. Runtime glTF 2.0 is the newer and more useful route for a migration: drop a .gltf or .glb in the content root and getContentProperty().Load<Model>("name") loads it with no tooling step, including skeletal animation and morph targets. .fbx and .x models go through cna-content.
  • Fonts: a JSON-based SpriteFont descriptor (.cnj), as an alternative to .spritefont XML built to XNB by the pipeline

Audio (SoundEffect same, XACT now real)

SoundEffect and SoundEffectInstance APIs are identical to MonoGame. SoundEffect("sound.wav") (the CNAEXT assetName constructor) loads WAV and OGG (and MP3/FLAC) through the selected audio implementation: SDL3_mixer with the default CNA_AUDIO_PLATFORM=SDL3, or CNA’s own mixer with ALSA on Linux. The NULL audio platform compiles but plays nothing. Song/MediaPlayer streaming is also implemented on those two mixers. 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: neither mixer has an AAC decoder.

Missing APIs

A few MonoGame extensions are not present in CNA, or are present only in part:

  • VideoPlayer — the Video and VideoPlayer types exist in every build, but decoding needs the optional FFmpeg backend (CNA_ENABLE_VIDEO, available on Linux and macOS only). Without it, playing a file-backed video throws System::NotSupportedException (see Tutorial 121).

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, but note two differences from a desktop MonoGame game: container names and paths that escape the container (absolute or ..) are rejected, and on the web CNA keeps saves in the browser’s IndexedDB, which a threaded build only gets with -DCNA_EMSCRIPTEN_USE_WASMFS=OFF.

Message boxes work. CNA provides CNA::Devices::MessageBox, which pops a real native modal dialog through the SDL3 platform. You do not need to call SDL_ShowMessageBox yourself. It belongs to the optional CNA::Devices layer, so configure with -DCNA_DEVICES=ON (default OFF):

#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.ElapsedGameTime.TotalSeconds;
//         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/Content/ContentManager.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"

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

class Player {
public:
    // Same shape as the C# constructor: the ContentManager loads the texture.
    // Load<T> returns the Texture2D by value; it shares the GPU texture with the manager's cache.
    Player(ContentManager& content)
        : sprite_(content.Load<Texture2D>("player"))   // finds Content/player.xnb, .cnb, or player.png
        , 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_;   // a handle to the GPU texture, released when the last copy is destroyed
    Vector2   position_;
};

// In your Game subclass:  player_ = std::make_unique<Player>(getContentProperty());