Tutorial 83: Migrating from MonoGame/FNA to CNA
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# Namespace | C++ Namespace |
|---|---|
Microsoft.Xna.Framework | Microsoft::Xna::Framework |
Microsoft.Xna.Framework.Graphics | Microsoft::Xna::Framework::Graphics |
Microsoft.Xna.Framework.Audio | Microsoft::Xna::Framework::Audio |
Microsoft.Xna.Framework.Input | Microsoft::Xna::Framework::Input |
Microsoft.Xna.Framework.Media | Microsoft::Xna::Framework::Media |
C# to C++ type mapping
| C# Type | C++ CNA Type | Notes |
|---|---|---|
int | intcs (= int32_t) | sharp-runtime alias, SharpRuntime::intcs |
float | SharpRuntime::Single (= float) | or just use float |
double | double | same |
bool | bool | same |
byte | bytecs (= uint8_t) | sharp-runtime alias, SharpRuntime::bytecs |
string | std::string | no implicit conversions |
List<T> | System::Collections::Generic::List<T> | from sharp-runtime, or std::vector<T>. Count is getCountProperty() |
null | nullptr | pointer null |
? nullable | std::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, Components | getGraphicsDeviceProperty(), 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")findslogo.png(also.jpg,.bmp,.gif,.tga,.tif,.qoi), decoded by CNA’s own image loader. The CNAEXT constructorTexture2D("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.gltfor.glbin the content root andgetContentProperty().Load<Model>("name")loads it with no tooling step, including skeletal animation and morph targets..fbxand.xmodels go throughcna-content. - Fonts: a JSON-based
SpriteFontdescriptor (.cnj), as an alternative to.spritefontXML 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— theVideoandVideoPlayertypes 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 throwsSystem::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());
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.