Migrating from MonoGame / XNA

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

Alpha.1 status: the XNA-shaped namespaces and core types make structural translation largely mechanical. CNA reads built-in XNB types and lets games register explicit C++ type readers. Already-compiled XNA/FNA Effect Framework bytecode can now load on FNA3D and opt-in EasyGL, SDL_GPU or Vulkan builds; HLSL source and MonoGame MGFX still need a separate porting strategy.

Overview

CNA is a C++23 reimplementation of Microsoft XNA 4.0, built on SDL3. 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)
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. The only 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 rather than being garbage-collected.

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::shared_ptr<Texture2D> playerTexture;

public:
    MyGame()
        : graphics(this)
    {
        Content->setRootDirectoryProperty("Content");
    }

    void LoadContent() override
    {
        spriteBatch =
          std::make_unique<SpriteBatch>(
            GraphicsDevice);
        playerTexture =
          Content->Load<Texture2D>("player");
    }

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

    void Draw(
        GameTime gameTime) override
    {
        GraphicsDevice->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. Use this for assets (textures, sounds, models) that multiple objects may reference simultaneously.
  • 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. Call this when transitioning between scenes to free GPU and CPU memory.
  • Game components — add drawable/updatable components to the Components collection (identical to XNA), 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 50 built-in readers with FFmpeg (49 without video) cover primitives, math, textures, fonts, audio, video, stock and compiled effects, and models, with LZX and shared-resource resolution. Built-ins must be registered explicitly; ContentManager can then migrate asset by asset between XNB and supported loose formats.

You must call CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders() once at startup. The registry is deliberately empty by default, so an .xnb load will fail until you do. See XNB Content Pipeline 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
.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 Content->Load<Texture2D>("player") prefers player.xnb, else falls back to player.png
Fonts: .spritefont + MGCB Fonts: a compiled SpriteFont .xnb, or a .cnj descriptor naming a pre-rendered glyph-atlas texture
Models: .fbx / .obj compiled to .xnb Models: the XNB model readers, a .gltf/.glb file read directly, or a .cnj from the gltf_to_cnj converter (CNA's own model path)
Custom shaders: .fx HLSL compiled by pipeline XNA/FNA D3D9 Effect Framework binaries load on FNA3D or opted-in EasyGL-family, SDL_GPU and Vulkan builds. Otherwise migrate to renderer-native ShaderEffect. HLSL .fx source and MGFX are not accepted as the binary format.
Custom / user-defined content types No reflection-based discovery. Implement ContentTypeReader<T> and register its creator explicitly, or move the data to a loose format.

For a typical 2D game the migration is straightforward: copy your existing content directory across, register the built-in readers at startup, and the Load<T> calls are identical. Where you would rather drop the pre-compilation step entirely, place PNG and OGG files in the content directory instead and point Content->setRootDirectoryProperty() at it.

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 or OGG file and plays it as a fire-and-forget sound. API identical to XNA.
  • SoundEffectInstance — a controllable instance of a SoundEffect: pause, resume, loop, and volume control all work.
  • 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. 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 and on EasyGL, SDL_GPU or Vulkan when their compiled-effect build option is enabled. HLSL .fx source and MonoGame MGFX are not accepted. Query GraphicsCapability::CompiledEffects before depending on the path.
  • Surface formats are renderer-dependent — Skia exposes the broad public promoted set and IGL promotes verified Rg32/Single. Other families defer to the framework's Color-only public gate even where the native layer has additional internal mappings. Verify the active renderer.
  • XNB is supported, with explicit registration — built-in video, typed external-reference and renderer-qualified effect readers exist. Custom types can register a reader/creator, but there is no reflection-based catch-all. See XNB Content Pipeline.
  • GamerServices has no online service, but it is not a pure shim — 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 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. What genuinely does nothing: fifteen other Guide::Show* overlay entry points, and the XNA-faithful AvatarRenderer::Draw(). There is no Steam, Xbox Live or HTTP integration of any kind.
  • Media is platform-dependent — the catalogue and playback layers are real, while FFmpeg-backed video is present only where those translation units and dependencies are enabled. It is absent from Windows, Emscripten and Android builds.
  • Touch input — the TouchPanel API and gesture pipeline are fully wired, with all 10 XNA gesture types genuinely detected (~95%). 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 stateFillMode::WireFrame is not honoured on every renderer, and DIRECTX12 implements no scissor, viewport, stencil or blend factor. Do not trust SupportsCapability() to tell you which; verify visually. See Graphics State.

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::shared_ptr<Texture2D> _background;

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

    void LoadContent() override
    {
        _spriteBatch =
          std::make_unique<SpriteBatch>(
            GraphicsDevice);
        _background =
          Content->Load<Texture2D>("bg");
    }

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

    void Draw(GameTime gt) override
    {
        GraphicsDevice->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,
    Rectangle(10, 10, 64, 64),
    Color::White);

spriteBatch->DrawString(
    font,
    "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)
auto tex =
    Content->Load<Texture2D>("sprites/hero");

// Load a sound (WAV only)
auto boom =
    Content->Load<SoundEffect>("sfx/boom");

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

// Unload all assets
Content->Unload();