Tutorial 95: Speedy Blupi: A Real-World CNA Port
What you’ll learn
- Where Speedy Blupi came from, and the route the real port took onto CNA.
- The porting process as a set of generic patterns, and which API differences cause work.
- How the content was adapted, and the patterns that recurred.
Before you start — Tutorial 83: Migrating from MonoGame/FNA to CNA — this is the same migration, carried out on a real game rather than in the abstract.
How to read this tutorial. This is a worked porting-patterns walkthrough, not an audited case study. The history of the real Speedy Blupi port below is summarised from that project’s own repository; the code snippets on this page are generic patterns, not code taken from that repository. The port is an independent consumer of CNA: its results belong to the CNA revision it was built with (see “Where the real port stands”), not to CNA alpha.1 or to the current development snapshot, and it is not part of CNA’s test suite. For validation work that is mechanically checked, see the XNA oracle corpus and the FNA differential harness described on the Showcase page.
The starting point
Speedy Blupi is a 2D action platformer from the Blupi game series by EPSITEC SA; the version that was ported is the 2013 Windows Phone edition, a C#/XNA 4.0 game with a tile-based world, multiple game modes, animated sprites and sound. Its original source code has not been publicly released. The CNA port is the OpenEggbert project’s openeggbert/mobile-eggbert repository (MIT-licensed, not part of the libcna organisation), whose README describes the route: the 2013 release was decompiled with ILSpy to C# source, that C# was migrated from XNA 4.0 to MonoGame, then rewritten from C# to C++, and finally moved from MonoGame to CNA. The C++ is a class-by-class rewrite of roughly 32,000 lines (at commit d43c6a1, 2026-08-21). It stands in here for the general shape of a 2D XNA/MonoGame game: SpriteBatch throughout, content loaded through ContentManager, touch and accelerometer input, and a custom tilemap renderer.
What the game code used
The XNA-style game code being carried across used: SpriteBatch for all 2D rendering, ContentManager.Load<Texture2D> for textures, SoundEffect and MediaPlayer for audio, GamePad, Keyboard and touch input, the Accelerometer, isolated storage for save games, and a custom tilemap renderer built on SpriteBatch.Draw calls. In the real C++ port these map onto CNA as getContentProperty().Load<Texture2D>(...) over loose PNG files under a Content/ directory, System::IO::IsolatedStorage for saves, and Microsoft::Devices::Sensors::Accelerometer (see Tutorial 50).
Where the real port stands
Consumer projects are evidence for their own revision only, so here is what the port’s repository and site show, without claiming more:
- Web: a WebAssembly (Emscripten) build is playable at speedyblupi.com/SpeedyBlupi2013. It embeds an early, pre-modularization CNA revision from May 2026 with the
SDL_RENDERERbackend — neither the alpha.1 tag nor the current development snapshot. - Desktop: it builds from source (Linux with
SDL_RENDERER; Windows builds are documented). No prebuilt binaries or GitHub releases are published, and the repository’s own TODO list names open issues (for example sound and fullscreen behaviour). - Android: a Gradle/NDK project exists in the repository. No APK is published.
Nothing here is a CNA release of the game.
Porting process to CNA
- Replace
using Microsoft.Xna.Framework;withusing namespace Microsoft::Xna::Framework; - Replace
new SpriteBatch(GraphicsDevice)withstd::make_unique<SpriteBatch>(getGraphicsDeviceProperty()) getContentProperty().Load<Texture2D>("name")can stay as-is (it returns the texture by value) — CNA'sContentManagerreads.xnb, then CNA's own.cnb, then falls back to loose files. If you would rather drop the pipeline, construct directly:Texture2D("assets/name.png", gd)(a CNA extension)SongandMediaPlayerneed no replacement —MediaPlayerplays songs through the audio implementation's mixer (SDL3_mixer by default). Swapping to a loopingSoundEffectInstanceis an option, not a requirement- Replace
List<T>withstd::vector<T>or sharp-runtime'sSystem::Collections::Generic::List<T>(whose count isgetCountProperty(), notCount) - Replace
nullwithnullptr,stringwithstd::string - Replace
(float)gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty()withstatic_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty()) - XACT
AudioEngine/WaveBank/SoundBanknow work directly in CNA (real.xgs/.xsb/.xwbparsing) — no replacement needed unless you prefer the simplerSoundEffectconstructor API
API compatibility lessons
Rectangle.Intersects()works identically in CNA and MonoGame — no changes neededColorconstructornew Color(r,g,b,a)maps toColor(r,g,b,a)— identical. Two behaviours match real XNA in this snapshot: a defaultColor()is transparent black, andColorfloat construction saturates and rounds ties-to-evenSpriteBatch.Drawoverloads are matched shape-for-shape in CNA, with C++ types: the texture is aconst Texture2D&and an optional source rectangle is astd::optional<Rectangle>— usually no changes to draw calls beyond dereferencing your texture pointerVector2.Zero,Vector2.Oneare static members in CNA exactly as in XNAMathHelper.Clamp,MathHelper.Lerpbehave identicallyGamePad.GetState(PlayerIndex.One)maps toGamePad::GetState(PlayerIndex::One)— identical semantics, but the state members are accessors (getIsConnectedProperty(),getButtonsProperty().getAProperty())
Content adaptation
You have two routes, and they can be mixed asset by asset.
Keep compatible .xnb files. CNA reads them through 61 built-in readers (60 without a native 128-bit integer) and a Game registers them when it is constructed; only a stand-alone ContentManager needs CNA::Internal::Xnb::RegisterAllBuiltInXnbReaders(). There is no reflection-based discovery, but custom ContentTypeReader<T> creators can be registered explicitly. Compiled effects additionally require a capable renderer build.
Or export to loose files. Export PNGs from the original XNB textures with the MonoGame Content Builder into an assets/ or Content/ directory. CNA decodes PNG (and JPG, BMP, GIF, TGA, TIF, QOI) with its own image loader, and ContentManager falls back to loose files when no .xnb or .cnb is present, so Load<Texture2D>("name") keeps working unchanged. You can also build .cnb or .xnb content from the sources with the build-time cna-content tool.
Custom shaders still need a deliberate portability decision. CNA can load XNA/FNA D3D9 Effect Framework binaries on FNA3D and on renderers whose compiled-effects build option is enabled (eight default-OFF options; a default configure reports the capability on FNA3D only), but the runtime does not compile HLSL .fx source or convert DXBC/MGFX (cna-content can compile .fx at build time through an external legacy fxc that you supply). Effects outside that compatible binary path must use the active renderer's ShaderEffect source or binary format. A purely 2D SpriteBatch game like this one is usually unaffected.
Key porting patterns
// Pattern 1: Convert content loading
// C# MonoGame:
// texture = Content.Load<Texture2D>("Blupi/blupi");
// CNA C++ (the XNA way; Load<T> returns the texture by value):
// Texture2D texture = getContentProperty().Load<Texture2D>("Blupi/blupi");
// or, dropping ContentManager (CNAEXT constructor that reads a file):
// texture_ = std::make_unique<Texture2D>("assets/Blupi/blupi.png", gd);
// Pattern 2: Keep MediaPlayer, or convert Song to a looping SoundEffectInstance
// C# MonoGame:
// MediaPlayer.Play(Content.Load<Song>("music/theme"));
// MediaPlayer.IsRepeating = true;
// CNA C++, MediaPlayer route (properties become accessors; Play takes a Song*):
Song theme = getContentProperty().Load<Song>("music/theme"); // keep the Song alive while it plays (e.g. as a member)
MediaPlayer::setIsRepeatingProperty(true);
MediaPlayer::Play(&theme);
// CNA C++, SoundEffectInstance route (CreateInstance() returns the instance by value;
// set the loop flag BEFORE the first Play(), it cannot change afterwards):
SoundEffect music("assets/music/theme.ogg");
auto musicInst = music.CreateInstance();
musicInst.setIsLoopedProperty(true);
musicInst.Play();
// Pattern 3: Convert string interpolation to std::string
// C# MonoGame:
// string path = $"sprites/{name}.png";
// CNA C++:
std::string path = "assets/sprites/" + name + ".png";
// Pattern 4: Convert foreach to range-for
// C# MonoGame:
// foreach (var tile in tiles) tile.Draw(spriteBatch);
// CNA C++:
for (auto& tile : tiles_) tile.Draw(*spriteBatch_);
// Pattern 5: Convert nullable references to std::optional
// C# MonoGame:
// Texture2D? overrideTexture = null;
// CNA C++ (Texture2D is a cheap shared handle, so hold it by value):
std::optional<Texture2D> overrideTexture_; // empty == null
// Pattern 6: Convert C# events to sharp-runtime System::EventHandler<TEventArgs>
// C# MonoGame:
// public event Action<int> OnScoreChanged;
// CNA C++ with sharp-runtime: the payload travels in an EventArgs subclass.
struct ScoreChangedEventArgs : System::EventArgs {
SharpRuntime::intcs NewScore = 0;
};
System::EventHandler<ScoreChangedEventArgs> OnScoreChanged;
// Subscribe with += (the handler takes sender + args):
OnScoreChanged += [](System::Object* sender, const ScoreChangedEventArgs& e) {
// react to e.NewScore
};
// Raise it (Invoke() is an alias for Raise()):
ScoreChangedEventArgs args;
args.NewScore = score_;
OnScoreChanged.Raise(this, args);
// Pattern 7 (no code): device services carry over almost by name.
// WP7 IsolatedStorageFile -> System::IO::IsolatedStorage (sharp-runtime), or XNA's StorageDevice
// WP7 Accelerometer (events) -> Microsoft::Devices::Sensors::Accelerometer, polled or via CurrentValueChanged
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Porting case studies: the Blupi games, the official samples and the example catalogue — What real ports around CNA teach: a native-CNA port plan for a reconstructed Blupi game, the Speedy Blupi 2013 port, cna-samples and cna-examples, each pinned.