Migrating an XNA 4.0, FNA or MonoGame C# Game to CNA.NET

CNA snapshot b0e97bb1

ⓘ

What this guide does: it takes an existing XNA 4.0 C# game — or one that was moved to FNA or MonoGame — and runs it on CNA.NET, CNA’s C# binding, by wrapping the original source and content in a small SDK-style project. The game’s .cs files are not edited. If you want a native C++ game instead, read Migrating from MonoGame / XNA to C++.

The steps follow CNA.NET’s own guide (docs/migrating-xna-games.md in libcna/cna-cs, revision 860f92c), condensed. The commands are quoted from it and were not re-run for this page; Linux x86_64 is the platform on which they were qualified.

The pieces

PieceWhat it is
CNAThe C++ runtime. You build its C ABI library, cna_c_api (libcna_c_api.so on Linux).
The C ABICNA’s stable-within-a-version C interface, currently 0.44.0 (C API page). CNA.NET calls nothing else.
CNA.FrameworkThe idiomatic managed layer over the ABI: handles, lifetimes, error translation.
CNA.XnaCompatThe Microsoft.Xna.Framework facade your game compiles against, with an MSBuild targets file that applies the XNA build settings.
Your wrapper projectA new SDK-style .csproj that lists the original source files and content. It is the only file you write.

Requirements

  • .NET 8 SDK or later for desktop (the generators need SDK 8.0.4xx); .NET 11 with the wasm-tools or android workload for the browser and Android heads.
  • A C++23 compiler, CMake 3.20 or newer, Ninja, and the CNA checkout with its siblings (sharp-runtime, easy-gl, meta-gl) and submodules (git submodule update --init --recursive). See Building.
  • A cna-cs checkout next to it. There are no published packages yet: you build from source.

1. Build CNA’s C ABI library

The configuration CNA.NET qualifies on Linux: the OpenGL ES 3 renderer, the C API, and compiled XNA effects enabled (needed for games that load .xnb effects; the option is off by default).

cmake -S "$CNA_ROOT" -B "$CNA_ROOT/build" -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DCNA_GRAPHICS_RENDERER=OPENGLES3 \
  -DCNA_BUILD_C_API=ON \
  -DCNA_EASYGL_COMPILED_EFFECTS=ON
cmake --build "$CNA_ROOT/build" --target cna_c_api --parallel
export CNA_NATIVE_LIBRARY="$CNA_ROOT/build/modules/c-api/libcna_c_api.so"

The library reports its ABI version at load time; CNA.NET at 860f92c admits exactly 0.44.0, the version of this documentation snapshot. Windows build steps exist in the upstream guide, but nothing has been run on Windows yet, so treat them as a source-build workflow rather than a support promise.

2. Build CNA.NET

cd cna-cs
dotnet restore CNA.sln
dotnet build CNA.sln -c Release --no-restore
# optional: the binding's own tests (the integration suite needs a display and the native library)
CNA_NATIVE_LIBRARY=/path/to/libcna_c_api.so \
  xvfb-run -a dotnet test tests/CNA.Integration.Tests/CNA.Integration.Tests.csproj

Point your projects at the checkout with CNA_CS_ROOT (an environment variable or MSBuild property).

3. Wrap the original game in an SDK-style project

Keep the original project, its content project, sources and built output read-only. Record what the original built: Windows or Windows Phone, the Reach or HiDef profile, Debug or Release, its conditional symbols and its exact list of compiled files — do not glob every .cs in the folder. Then add a wrapper next to it:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
    <XnaProfile>HiDef</XnaProfile>          <!-- as in the original project -->
    <XnaPlatform>Windows</XnaPlatform>
    <DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="../OriginalGame/Game1.cs" />
    <Compile Include="../OriginalGame/Program.cs" />
    <!-- ...exactly the files the original project compiled -->
    <ProjectReference Include="$(CnaCsRoot)/src/CNA.XnaCompat/CNA.XnaCompat.csproj" />
    <None Include="../OriginalGame/Content/**" LinkBase="Content" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
  <Import Project="$(CnaCsRoot)/src/CNA.XnaCompat/build/CNA.XnaCompat.targets" />
</Project>

This is a sketch of the shape the upstream guide describes, not a file copied from it; its own example names every property. XnaProfile matters: GraphicsDeviceManager picks Reach or HiDef from the embedded runtime profile, as XNA did. The targets file also applies a few compile-time repairs for differences between .NET Framework 4.0 and modern .NET (for example List<T>.ForEach semantics and 32-bit struct layouts); each has an opt-out property.

4. Use the original content

  • Prefer the original XNA 4.0 build output (.xnb, and .xgs/.xwb/.xsb for XACT). CNA reads XNB directly. If you only have the content project, build it with XNA 4.0’s own BuildContent (cna-cs-samples has a Linux/Wine script); CNA.NET has no content pipeline of its own.
  • Keep paths under Content/. Linux is case-sensitive, and raw System.IO paths with backslashes need attention.
  • XNA songs are WMA, which CNA does not decode: put an .ogg, .oga or .qoa beside the unchanged song .xnb.
  • Compiled effects need the effect option from step 1 and the right profile. Do not swap content just to make a run green.
  • Prebuilt XNA libraries: rebuild from source when you can. Pure-IL libraries can reference CNA.NET’s XNA-named forwarding assemblies; native, mixed-mode or Windows-only libraries need their own port.

5. Build and run

CNA_CS_ROOT=/path/to/cna-cs dotnet build OriginalGame.CNA.csproj -c Release
CNA_NATIVE_LIBRARY=/path/to/libcna_c_api.so dotnet run --project OriginalGame.CNA.csproj -c Release

Run from the original working directory when the game loads XACT projects or other files by relative path.

Coming from FNA or MonoGame

  • FNA: remove the FNA.dll reference, add CNA.XnaCompat and its targets, keep the Microsoft.Xna.Framework.* source and the original content. Audit every FNA extension (*EXT members, FNA environment variables, raw FNA3D, custom audio or video) and isolate it.
  • MonoGame: remove the MonoGame.Framework.* packages and the platform bootstrap, use an ordinary Program.Main with Game.Run, keep the profile and symbols, and use XNA-built content rather than MGCB-only types. MonoGame-specific APIs, shader dialects, content processors and window hooks are not provided.
  • CNA.NET does not implement engine-specific extensions to raise the number of projects that compile; a game that depends on them keeps that engine as its backend. The template shows how one source can build against CNA, FNA or MonoGame with -p:Engine=….

Browser (WebAssembly)

The browser host is a plain Microsoft.NET.Sdk.WebAssembly project (not Blazor) that statically links CNA. The staging script is Linux-only:

"$DOTNET_ROOT_BROWSER/dotnet" workload install wasm-tools
./scripts/Build-BrowserNative.sh --dotnet-root "$DOTNET_ROOT_BROWSER" --configure      # in cna-cs
CNA_CS_ROOT=/path/to/cna-cs "$DOTNET_ROOT_BROWSER/dotnet" publish Platforms/Browser -c Release
cd Platforms/Browser/bin/Release/net11.0/publish/wwwroot && python3 -m http.server 8080

Games that start their own threads need the threaded variant (--threads, <WasmEnableThreads>true</WasmEnableThreads>, served with cross-origin isolation headers); WebGL calls are then proxied to the main thread, which is slow. Qualification so far is headless Chromium only; video and UDP networking are not available in the browser build.

Android

"$DOTNET_ROOT_ANDROID/dotnet" workload install android
./scripts/Build-AndroidNative.sh --abi x86_64 --ndk "$ANDROID_NDK_ROOT" --configure      # in cna-cs
CNA_CS_ROOT=/path/to/cna-cs "$DOTNET_ROOT_ANDROID/dotnet" build Platforms/Android -c Release -t:Install

The minimum Android platform is API 24. Qualification so far is the x86_64 emulator; --abi arm64-v8a with -p:RuntimeIdentifier=android-arm64 packages for ARM, but nothing has run on a physical device yet.

Debugging, and finding the right layer

  • Load failures: give an absolute CNA_NATIVE_LIBRARY, check the architecture and the ABI version, and set CNA_NATIVE_DIAGNOSTICS=1. The loader’s message names the configuration it tried, the expected and detected ABI and the runtime identifier.
  • Editors: open the wrapper in Visual Studio 2022 or VS Code (a coreclr launch configuration) and set CNA_CS_ROOT and CNA_NATIVE_LIBRARY as debug environment variables. Neither IDE builds CNA itself.
  • Native or managed? Native exceptions never cross the C ABI: they become a result code plus error information, and CNA.NET raises the matching XNA exception at the important boundaries. A CNA.CnaException in a stack trace therefore marks a refusal that came from the native side; its CanonicalExceptionType names the .NET exception CNA intended (C ABI 0.37 and later).
  • Compare with C++: if the same sample’s C++ port in cna-samples shows the problem too, it is a CNA (native) issue; if only the C# route does, it is a CNA.NET issue. For a native issue, a few lines of C against the ABI (CNA’s modules/c-api/examples/c/hello_cna.c is a starting point) make the smallest reproduction. This is advice, not a procedure CNA.NET documents.
  • Reporting: include the CNA and CNA.NET commits, the C ABI version, OS and architecture, the renderer, the full exception and native log, the original project and profile, where the content came from, the smallest unchanged reproduction, and whether the game runs on XNA, FNA or MonoGame.

What to expect

XNA 4.0 API behaviour is the compatibility contract. Many unchanged games run, but CNA.NET is beta: some behaviour is still incomplete, some refusals surface as CNA.CnaException, and host APIs outside XNA (Win32 P/Invoke, WPF, full Windows Forms, Silverlight, retired online services) are not supplied. The current list lives in CNA.NET’s docs/final-compatibility-audit.md and docs/native-behavior-blockers.md; the evidence behind the numbers is summarised on the CNA.NET page.