Migrating from MonoGame / XNA

Porting a C# XNA 4.0 or MonoGame game to CNA (C++23)

ⓘ

Snapshot status: the XNA-shaped namespaces and core types make structural translation largely mechanical, and every documented type and member of the XNA 4.0 runtime is represented (331/331 types, 3,627/3,627 members — representation, not behaviour; see XNA Compatibility). CNA reads built-in XNB types, lets games register explicit C++ type readers, and now has a build-time content pipeline (cna-content) that can write .xnb and .cnb. Already-compiled XNA/FNA Effect Framework bytecode loads on FNA3D and on builds that opt in to a compiled-effect option (11 of the 14 renderer identities); HLSL source and MonoGame MGFX still need a separate porting strategy.

➤

Keeping your game in C#? This guide is for rewriting a game in C++. If you would rather keep the C# source, CNA.NET compiles an unchanged XNA 4.0 C# game against CNA through its C ABI; see Migrating C# games to CNA.NET.

Overview

CNA is a C++23 reimplementation of Microsoft XNA 4.0, with SDL3 as its windowing platform layer (it reaches Windows, X11, Wayland and macOS through SDL’s own video drivers). Its API is intentionally faithful to XNA: the same namespace hierarchy, the same class names, the same method names. For a developer coming from MonoGame or XNA the conceptual model is identical — Game, SpriteBatch, ContentManager, GraphicsDevice, BasicEffect — all exist and behave as expected.

The differences are almost entirely syntactic: C++ does not have garbage collection, properties, delegates, or using-disposal blocks. Each of these maps cleanly to a C++ equivalent. Once you have internalised the mechanical substitutions in this guide, most XNA code ports line-by-line.

1. Namespace mapping

XNA uses dot-separated C# namespaces; CNA uses the identical names with :: separators. The using directive works the same way.

C# (XNA / MonoGame) C++ (CNA)
Microsoft.Xna.Framework Microsoft::Xna::Framework
Microsoft.Xna.Framework.Graphics Microsoft::Xna::Framework::Graphics
Microsoft.Xna.Framework.Input Microsoft::Xna::Framework::Input
Microsoft.Xna.Framework.Audio Microsoft::Xna::Framework::Audio
Microsoft.Xna.Framework.Content Microsoft::Xna::Framework::Content
using Microsoft.Xna.Framework; using namespace Microsoft::Xna::Framework;

2. C++ vs C# quick reference

The table below covers the most common syntactic substitutions you will encounter during a port.

Concept C# (XNA) C++ (CNA)
Property read game.IsActive game.getIsActiveProperty()
Property write graphics.PreferredBackBufferWidth = 800 graphics.setPreferredBackBufferWidthProperty(800)
Struct fields (vectors, matrices, rectangles) v.X, m.M11, rect.Width Plain public fields, the same spelling (v.X); only C# properties become get/setXProperty() accessors (for example color.getRProperty())
Content access Content.Load<Texture2D>("a") getContentProperty().Load<Texture2D>("a") — returns Texture2D by value; there is no Content member or pointer
Graphics device GraphicsDevice.Clear(...) getGraphicsDeviceProperty().Clear(...) (a reference)
Async operation results IAsyncResult returned by Begin* std::unique_ptr<IAsyncResult>; pass result.get() to End*
Inheritance class MyGame : Game class MyGame final : public Game
Object creation new SpriteBatch(gd) std::make_unique<SpriteBatch>(gd)
Null reference null nullptr
String type string std::string or String (sharp-runtime alias)
Boolean bool (true/false) bool (true/false) — same
Delegates / events EventHandler handler = ... Virtual method override or EventHandler<T>
using resource disposal using (var x = ...) {} RAII / std::unique_ptr<>
Abstract override override void Draw(...) void Draw(...) override
Integer types int, byte, short intcs, bytecs, shortcs (sharp-runtime aliases)
Float type float Single or float
Array T[] std::vector<T> or std::array<T, N>
Optional / nullable T? std::optional<T>
foreach foreach (var x in col) Range-for: for (auto& x : col)

3. Game class migration

The Game subclass is the entry point for any XNA game. The lifecycle methods — Initialize, LoadContent, Update, and Draw — exist identically in CNA (they are protected virtual; Update takes GameTime& and Draw takes const GameTime&). The differences are C++ syntax: override comes after the signature, member variables are declared with their types (no implicit nullability), and resources are owned by std::unique_ptr or held by value rather than being garbage-collected. Two behavioural points matter when you port timing-sensitive code: with the default fixed time step the first Update sees an elapsed time of zero (XNA's clock; a variable step gets the time since the loop started), and a Game registers the built-in XNB readers for you. See Game Loop.

C# (MonoGame) C++ (CNA)
public class MyGame : Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Texture2D playerTexture;

    public MyGame()
    {
        graphics =
          new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void LoadContent()
    {
        spriteBatch =
          new SpriteBatch(GraphicsDevice);
        playerTexture =
          Content.Load<Texture2D>("player");
    }

    protected override void Update(
        GameTime gameTime)
    {
        if (GamePad.GetState(
              PlayerIndex.One).Buttons.Back
            == ButtonState.Pressed)
            Exit();
        base.Update(gameTime);
    }

    protected override void Draw(
        GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.Black);
        spriteBatch.Begin();
        spriteBatch.Draw(
            playerTexture,
            new Vector2(100, 100),
            Color.White);
        spriteBatch.End();
        base.Draw(gameTime);
    }
}
class MyGame final : public Game
{
    GraphicsDeviceManager graphics;
    std::unique_ptr<SpriteBatch> spriteBatch;
    std::optional<Texture2D> playerTexture;

public:
    MyGame()
        : graphics(this)
    {
        getContentProperty()
          .setRootDirectoryProperty("Content");
    }

protected:
    void LoadContent() override
    {
        spriteBatch =
          std::make_unique<SpriteBatch>(
            getGraphicsDeviceProperty());
        playerTexture.emplace(
          getContentProperty()
            .Load<Texture2D>("player"));
    }

    void Update(
        GameTime& gameTime) override
    {
        if (GamePad::GetState(
              PlayerIndex::One)
            .getButtonsProperty().getBackProperty()
            == ButtonState::Pressed)
            Exit();
        Game::Update(gameTime);
    }

    void Draw(
        const GameTime& gameTime) override
    {
        getGraphicsDeviceProperty()
          .Clear(Color::Black);
        spriteBatch->Begin();
        spriteBatch->Draw(
            *playerTexture,
            Vector2(100, 100),
            Color::White);
        spriteBatch->End();
        Game::Draw(gameTime);
    }
};

4. Memory management

XNA relies on the .NET garbage collector: objects are allocated with new and reclaimed automatically. C++ has no garbage collector; you manage object lifetimes explicitly. CNA follows standard modern C++ ownership conventions:

  • std::unique_ptr<T> — single-owner resource. Use this for game components, renderers, and anything your Game class owns exclusively. The resource is released automatically when the owning pointer goes out of scope or is reset.
  • std::shared_ptr<T> — shared ownership, for objects that several owners really do share. Content assets are not the usual case: ContentManager::Load<T> returns each asset (Texture2D, SoundEffect, SpriteFont, Model) by value, and the GPU state behind a texture is shared-owned, so a copy you hold stays valid even after the manager unloads. Keep assets as value or std::optional members; types without a default constructor (SpriteFont, Video) need std::optional or a unique_ptr.
  • Stack allocation — use for temporaries such as Vector2, Color, Rectangle, and GameTime. These are small value types and incur no heap allocation.
  • ContentManager::Unload() — releases all assets currently held by the content manager's cache. Call this when transitioning between scenes to free GPU and CPU memory. Copies you already hold remain valid.
  • Game components — add drawable/updatable components to the getComponentsProperty() collection (identical to XNA; a std::shared_ptr overload keeps the component alive for you), or hold them as std::unique_ptr members on your Game subclass.

The key rule: if you would write new Foo(...) in C#, write std::make_unique<Foo>(...) in C++ and store the result in a std::unique_ptr<Foo> member. The destructor will run automatically when your game object is destroyed.

5. Content pipeline

XNA and MonoGame pre-compile source assets into .xnb binary files. CNA reads XNB directly. Its 61 built-in readers (60 in a build without native 128-bit integer support) cover primitives, math, textures, fonts, audio, video, stock and compiled effects, and models, with LZX and shared-resource resolution, and ContentManager can migrate asset by asset between XNB, CNA's own .cnb container and supported loose formats. New in this snapshot: CNA can also write content at build time with the cna-content tool (.xnb or .cnb output, including .fbx, .x, .spritefont and XNA .contentproj inputs), so a project no longer has to keep MGCB or the XNA pipeline around just to produce assets. It is not an MGCB replacement: there is no .mgcb route.

ⓘ

A Game registers the built-in XNB readers in its constructor, so a game subclass needs no setup. A stand-alone ContentManager used without a Game (a tool, a test) still starts with an empty registry and must call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() itself. See XNB Loading for the full reader list and the known gaps.

XNA / MonoGame CNA
Pre-compile assets with MGCB / XNA Content Pipeline Tool Existing .xnb output can be copied across as-is; loose source files also work with no build step; or build with cna-content (XNB or CNB output)
.xnb binary files at runtime .xnb binary files at runtime, or raw asset files (PNG, WAV, OGG) / JSON descriptors
Content.Load<Texture2D>("player") loads player.xnb getContentProperty().Load<Texture2D>("player") prefers player.xnb, then player.cnb, else falls back to a loose player.png (or another supported image extension)
Fonts: .spritefont + MGCB Fonts: a compiled SpriteFont .xnb, a .cnj descriptor naming a pre-rendered glyph-atlas texture, or a .spritefont built with cna-content (FreeType, when CNA_ENABLE_FONT_PIPELINE is available)
Models: .fbx / .obj compiled to .xnb Models: the XNB model readers, a .gltf/.glb file read directly, a .cnj from the gltf_to_cnj converter, or .fbx/.x built to .xnb/.cnb with cna-content. There is no .obj route
Custom shaders: .fx HLSL compiled by pipeline XNA/FNA D3D9 Effect Framework binaries load on FNA3D or on builds that opt in to a compiled-effect option (11 of the 14 renderer identities; see Effects). Otherwise migrate to renderer-native ShaderEffect. HLSL .fx source and MGFX are not accepted at run time; cna-content can compile .fx at build time through an external legacy fxc (unverified against a genuine Microsoft fxc).
Custom / user-defined content types No reflection-based discovery. Implement ContentTypeReader<T> and register its creator explicitly (ContentTypeReaderManager::AddTypeCreator), declare the field list once with ReflectiveTypeReaderBuilder<T>, or move the data to a loose format.

For a typical 2D game the migration is straightforward: copy your existing content directory across, and the Load<T> calls are identical apart from the accessor spelling and the by-value result. Where you would rather drop the pre-compilation step entirely, place PNG and WAV files in the content directory instead and point getContentProperty().setRootDirectoryProperty() at it (a Game's content root already defaults to "Content").

6. Audio

CNA implements the main XNA audio API in the Microsoft::Xna::Framework::Audio namespace. The common classes work as expected:

  • SoundEffect — loads a WAV file (or a compiled .xnb) and plays it as a fire-and-forget sound. API identical to XNA; Load<SoundEffect> returns it by value and CreateInstance() returns the instance by value.
  • SoundEffectInstance — a controllable instance of a SoundEffect: pause, resume, loop, and volume control all work through setVolumeProperty, setPitchProperty, setPanProperty and setIsLoopedProperty. As in XNA, Apply3D after Play() on a pan-mode instance throws InvalidOperationException.
  • MediaPlayer / Song — background music playback. API identical to XNA.

The XACT subsystem (AudioEngine, SoundBank, WaveBank, Cue) is a real parser and player rather than a facade, with FACT-shaped volume/RPC behavior and 3D calculations, playing through the SDL3 or ALSA audio implementation (the Null choice compiles it without audible output). Important caveats: only the first PlayWave per track is honoured; XMA/WMA wave-bank entries log and return a null sound rather than throwing; reverb is a no-op; and the XNB SoundEffectReader has different failure behavior. SoundEffect and MediaPlayer remain simpler alternatives.

7. Known gaps and limitations

⚠

Before porting, review these known gaps. The API surface is largely there, but the items below are absent, partial, or need hardware validation. See Verification & Known Issues for the complete current list.

  • Compiled effects have a renderer boundary. XNA/FNA D3D9 Effect Framework binaries and XNB Effect payloads load on FNA3D always and on 10 more renderer identities when their default-off CNA_*_COMPILED_EFFECTS build option is enabled (a default configure reports the capability on FNA3D only). HLSL .fx source and MonoGame MGFX are not accepted at run time. Query GraphicsCapability::CompiledEffects before depending on the path.
  • Surface formats are profile- and renderer-dependent — the default Reach profile now refuses the wider formats (and multiple render targets, occlusion queries, 32-bit indices, float targets and large cubes) on every renderer unless HiDef is requested with graphics.setGraphicsProfileProperty(GraphicsProfile::HiDef); on top of that, eight renderer families (ten of the 14 identities) classify the broader public texture formats themselves, each accepting its own subset, while the other four identities (DIRECTX9, SDL_RENDERER, HEADLESS and STUB) have no classifier and accept only SurfaceFormat::Color for a public Texture2D. Verify the active renderer.
  • XNB is supported, with explicit registration for custom types — built-in video, typed external-reference and renderer-qualified effect readers exist, and a Game registers the built-ins. Custom types can register a reader/creator, but there is no reflection-based catch-all. See XNB Loading.
  • GamerServices is offline by default, with an optional CNA server — all 52 types are present with complete signatures, and unlike FNA's no-op shim CNA backs them with a real local implementation. Achievements and leaderboard entries are genuinely written as JSON under <StorageDevice root>/GamerServices/ (the storage root, not SDL_GetPrefPath()) and reload in a later process, and LeaderboardWriter/LeaderboardReader do real sorting, ranking and paging. Guide::BeginShowMessageBox and BeginShowKeyboardInput are complete implementations with overlay rendering. The Guide is a CNA-drawn system UI whose standard pages open, and AvatarRenderer::Draw() draws CNA’s own avatars. Accounts, social features and PlayerMatch/Ranked/JoinInvited sessions need a self-hosted CNA Gamer Services server, configured outside game code (not Xbox LIVE; no Steam integration); without one those session types throw GamerServicesNotAvailableException.
  • Media is build-dependent — the catalogue and playback layers are real, while FFmpeg-backed video decodes only where the optional backend was built (CNA_ENABLE_VIDEO; Linux and macOS with FFmpeg found). On Windows, Emscripten, Android and iOS the Video types link but file-backed playback throws NotSupportedException. See Video Playback.
  • Touch input — the TouchPanel API and gesture pipeline are fully wired, with all 10 XNA gesture types genuinely detected. One bug: TouchCollection reports IsReadOnly == true but its mutators mutate rather than throwing.
  • Apple targets are scoped — macOS/Metal has automatic CI. iOS is experimental and limited to SDL_RENDERER final-link plus a one-frame simulator smoke path; tvOS is unsupported.
  • Renderer-dependent state — FillMode::WireFrame and some other state are not honoured on every renderer, and state objects throw InvalidOperationException if you mutate them after binding (as in XNA). Query SupportsCapability() and the RendererCapabilityProfile, and verify visually. See Graphics State.
  • Web builds — no save persistence (the storage root lands in Emscripten's in-memory file system), no LAN discovery, and video throws. A stack-allocated Game is fine.

8. Code examples

Full Game class

The following pair shows a minimal but complete game loop: window setup, content loading, update, and draw.

C# (MonoGame) C++ (CNA)
public class MyGame : Game
{
    GraphicsDeviceManager _graphics;
    SpriteBatch _spriteBatch;
    Texture2D _background;

    public MyGame()
    {
        _graphics =
          new GraphicsDeviceManager(this);
        _graphics
          .PreferredBackBufferWidth = 1280;
        _graphics
          .PreferredBackBufferHeight = 720;
        Content.RootDirectory = "Content";
        IsMouseVisible = true;
    }

    protected override void LoadContent()
    {
        _spriteBatch =
          new SpriteBatch(GraphicsDevice);
        _background =
          Content.Load<Texture2D>("bg");
    }

    protected override void Update(
        GameTime gt)
    {
        if (Keyboard.GetState()
            .IsKeyDown(Keys.Escape))
            Exit();
        base.Update(gt);
    }

    protected override void Draw(
        GameTime gt)
    {
        GraphicsDevice.Clear(
            Color.CornflowerBlue);
        _spriteBatch.Begin();
        _spriteBatch.Draw(
            _background,
            Vector2.Zero,
            Color.White);
        _spriteBatch.End();
        base.Draw(gt);
    }
}
class MyGame final : public Game
{
    GraphicsDeviceManager _graphics;
    std::unique_ptr<SpriteBatch> _spriteBatch;
    std::optional<Texture2D> _background;

public:
    MyGame() : _graphics(this)
    {
        _graphics
          .setPreferredBackBufferWidthProperty(1280);
        _graphics
          .setPreferredBackBufferHeightProperty(720);
        getContentProperty()
          .setRootDirectoryProperty("Content");
        setIsMouseVisibleProperty(true);
    }

protected:
    void LoadContent() override
    {
        _spriteBatch =
          std::make_unique<SpriteBatch>(
            getGraphicsDeviceProperty());
        _background.emplace(
          getContentProperty()
            .Load<Texture2D>("bg"));
    }

    void Update(GameTime& gt) override
    {
        if (Keyboard::GetState()
            .IsKeyDown(Keys::Escape))
            Exit();
        Game::Update(gt);
    }

    void Draw(const GameTime& gt) override
    {
        getGraphicsDeviceProperty().Clear(
            Color::CornflowerBlue);
        _spriteBatch->Begin();
        _spriteBatch->Draw(
            *_background,
            Vector2::Zero,
            Color::White);
        _spriteBatch->End();
        Game::Draw(gt);
    }
};

SpriteBatch draw

C# (MonoGame) C++ (CNA)
spriteBatch.Begin(
    SpriteSortMode.Deferred,
    BlendState.AlphaBlend);

spriteBatch.Draw(
    texture,
    new Rectangle(10, 10, 64, 64),
    Color.White);

spriteBatch.DrawString(
    font,
    "Hello, World!",
    new Vector2(200, 100),
    Color.Yellow);

spriteBatch.End();
spriteBatch->Begin(
    SpriteSortMode::Deferred,
    BlendState::AlphaBlend);

spriteBatch->Draw(
    texture,          // a const Texture2D& (or *ptr)
    Rectangle(10, 10, 64, 64),
    Color::White);

spriteBatch->DrawString(
    font,             // a const SpriteFont&
    "Hello, World!",
    Vector2(200, 100),
    Color::Yellow);

spriteBatch->End();

ContentManager load

C# (MonoGame) C++ (CNA)
// Load a texture
Texture2D tex =
    Content.Load<Texture2D>("sprites/hero");

// Load a sound
SoundEffect boom =
    Content.Load<SoundEffect>("sfx/boom");

// Load a font
SpriteFont font =
    Content.Load<SpriteFont>("fonts/ui");

// Unload all assets
Content.Unload();
// Load a texture (PNG loaded directly);
// Load<T> returns the asset by value
Texture2D tex =
    getContentProperty()
      .Load<Texture2D>("sprites/hero");

// Load a sound (WAV as a loose file)
SoundEffect boom =
    getContentProperty()
      .Load<SoundEffect>("sfx/boom");

// Load a font (.cnj descriptor or .xnb)
SpriteFont font =
    getContentProperty()
      .Load<SpriteFont>("fonts/ui");

// Unload all assets (copies you
// hold stay valid)
getContentProperty().Unload();