Porting case studies: the Blupi games, the official samples and the example catalogue
Evidence basis: source-verified at the pinned commit. Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Sibling and consumer repositories were read at the pinned revisions named on the page; CNA facts were read at 009d40f5. Nothing was built or run; pixel percentages, sweep counts and statuses are those repositories' own records.
Real ports teach what an API reading cannot: which calls a game really makes, which deviations its callers make harmless, where timing and content hide, and how much of a catalogue's status is evidence. This page collects the ports around CNA that are documented well enough to learn from: two legacy Blupi games running through the free-direct compatibility layer and a study for porting one of them natively, the finished Speedy Blupi 2013 translation, the official XNA Game Studio 4.0 samples and the cna-examples catalogue. Every one of them is a separate repository and is evidence for its own pinned revision only, never for the CNA snapshot 009d40f5 these pages document. The method they illustrate is on migrating an XNA game.
The repositories and their pins
| Repository | Revision read | What it is | Relation to CNA |
|---|---|---|---|
openeggbert/free-direct | 934f72f (2026-07-18) | A narrow, game-driven DirectDraw, DirectSound and DirectPlay subset on SDL3 | Backs CNA's 2D-only FREEDIRECT renderer |
openeggbert/free-eggbert | 7ea63d1 (2026-07-10), plus an untracked cna.md | A reconstruction of Speedy Eggbert 2 from decompiled and reverse-engineered code; GPL-3.0; "partially functional … still defective" by its own README | Runs through free-direct; the subject of a native-CNA feasibility study |
openeggbert/planetblupi | 8e42e1c (2026-07-18, branch feature/free_direct) | Epsitec's published 1997 Planet Blupi source, made buildable on Linux and the web | Runs through free-direct; not a CNA consumer |
openeggbert/mobile-eggbert | d43c6a1 (2026-08-21) | The C++ port of the 2013 Windows Phone Speedy Blupi, on CNA | A CNA consumer; builds against a sibling CNA checkout |
libcna/cna-samples | 4da98a0 (2026-09-20) | C++ ports of the XNA Game Studio 4.0 samples | A CNA consumer, built against CNA next of that date |
libcna/cna-examples | ea33c9a (2026-09-13) | One CNA-native catalogue application | A CNA consumer |
None of these repositories is pinned by CNA, and TARGET has no test, CI job or build gate that builds any of them. A result quoted below belongs to the named revision of that repository and the CNA revision it was built with. The original Speedy Blupi and Speedy Eggbert sources have not been published; the C# and C++ reconstructions come from decompilation, so neither game is described here as open source.
Call-site-driven scope: the free-direct audits
free-direct's subsystem boundaries were set by auditing the two games that run through it rather than by implementing DirectX broadly (its own audits are in docs/audit-24h-free-direct.md and docs/audit_ddraw.md at 934f72f). Both games use DirectDraw and DirectSound; only free-eggbert uses DirectPlay. free-eggbert calls IDirectDrawSurface::Blt at four sites and BltFast at six; Planet Blupi at three and five. In each game one Blt is CPixmap::Display(), the once-per-frame present from the back buffer to the primary surface, the hottest path in both. That call-site pressure is why DirectDraw is free-direct's most developed subsystem, and why the FREEDIRECT renderer described in the ecosystem page is a 2D path.
Raw occurrences versus interface calls
A later recount found eighteen BltFast occurrences in free-eggbert and seventeen in Planet Blupi. Both counts are right about different populations: the larger ones include header declarations, the definitions of each game's CPixmap::BltFast wrapper and calls to that wrapper; only six and five calls reach IDirectDrawSurface::BltFast, the boundary free-direct implements. Counting both the wrapper calls and the interface calls counts the same traffic twice; a call-site total is meaningful only with its population named.
Deviations that the callers make harmless
A pixel-format mismatch that never became visible
Both games request an explicit display depth: free-eggbert selects 8 or 16 bits through a runtime flag, Planet Blupi asks for 8. free-direct's SetDisplayMode logs the requested depth, stores only width and height, and the primary surface is created at 32 bits. Their off-screen surfaces follow a separate, hard-coded 32-bit path, so every observed producer and consumer agrees on the delivered format: a verified harmless deviation for these callers, not a general guarantee.
Colour keying: implementation broader than observed use
Planet Blupi uses source colour keying at two call sites through one caching helper. Both construct a single-value range and match white through a GetDC/SetPixel/Lock round trip rather than through a fixed palette index. free-direct's blitter implements full low-to-high ranges (a palette-index range on 8-bit surfaces, and a raw or RGB range on 32-bit ones), but the audited games exercise only the single-value case, so a third game would need its own evidence for ranged behaviour.
GetDC sits on a gameplay path
Planet Blupi's IsIconPixel performs a per-click hit test through GetDC/ReleaseDC. An API that looks like incidental GDI interoperation is on an active gameplay path. At 934f72f free-direct implements both depths (GetDC wraps a 32-bit surface's pixels directly and expands an 8-bit surface into a temporary 32-bit buffer through its palette, converting back to indices on ReleaseDC), but the audit traced the surfaces IsIconPixel uses to a loader that creates them without an explicit pixel format, so they are 32-bit and the 8-bit branch is unreachable for both audited games; a second GetDC before the matching ReleaseDC now returns DDERR_DCALREADYCREATED instead of silently handing back the same context.
A performance audit on the same call sites
DirectDraw's BltFast has no destination-size parameter, so it cannot scale: every audited BltFast interface call in both games is a 1:1 copy (free-eggbert pixmap.cpp lines 406, 574, 580, 612, 1762 and 1818; Planet Blupi 395, 401, 433, 1235 and 1291, as the audit lists them). When the audit benchmarked the compiled library headlessly (SDL_VIDEODRIVER=dummy), the shared blit engine still ran a per-pixel loop with an integer division and a bit-depth branch for these copies. One 640×480 32-bit blit measured:
| Build | BltFast | memcpy of the same bytes | Ratio |
|---|---|---|---|
| Default configure (no optimisation flags) | 4.704 ms | 0.0229 ms | 205.8× |
-DCMAKE_BUILD_TYPE=Release | 0.715 ms | 0.0244 ms | 29.4× |
| Release, after the fast path (follow-up benchmark) | 0.466 ms | (reference) | 14.8× |
The first two rows are discovery evidence, not the current implementation. At 934f72f an otherwise unspecified top-level, single-configuration free-direct build defaults to Release (a consuming project's choice is untouched), so the 205.8× row no longer describes a plain standalone configure. BlitFrom now takes a per-row memcpy when the copy is unscaled, no source colour key is active and both surfaces share an 8- or 32-bit depth, followed by the required opaque-alpha fix-up on 32-bit rows; that alpha pass is most of the remaining gap. The fast path is chosen by comparing the two rectangles at run time, not by which method called it, because one Blt-family call does scale: free-eggbert's CPixmap::Display() (pixmap.cpp 1509–1516) presents the fixed internal resolution into a window whose client area may differ. The same audit found game-side dead code: free-eggbert's CPixmap::DrawMap(int, RECT, RECT) holds one Blt call but has no caller with that three-argument signature in the inspected source.
Interfaces and paths the games never reach
Across both games the only QueryInterface call belongs to a DirectPlay object; none targets the DirectDraw objects, whose free-direct implementations return DDERR_UNSUPPORTED. Planet Blupi uses no DirectPlay at all, so free-direct's DirectPlay scope comes entirely from free-eggbert. Further unexercised paths bound what free-direct needs:
- free-eggbert's alternate BASS-backed sound implementation sits behind a compile-time flag fixed to false with no build override, and each game's
src/wave.cpp(LoadWave,wave_ParseWaveMemory) is compiled but unreachable: its header is included by nothing but the file itself and no code callsLoadWave. - Every inspected call to the sound-playback method passes zero for the looping flags, agreeing with the implementation-side conclusion that neither game reaches DirectSound looping.
- Excluding the vendored SDK headers, neither game calls
GetCurrentPosition; free-direct declares onlySetCurrentPosition, whose stream is not seekable (the new cursor is used on the nextPlay). The getter stays unimplemented until a target caller needs it. - Neither game requests an 8-bit
CreateSurfaceformat, so with the 32-bit primary above the audited games never reachBlitFrom's silent fall-through for mixed 8/32-bit copies. That behaviour remains a limitation of free-direct; adding an error route was outside the target-driven scope.
A DirectSound structure-layout hazard
Both games populate PCMWAVEFORMAT, not the larger WAVEFORMATEX, and pass it through a void*. The two layouts put wBitsPerSample at different offsets (16 versus 14 on LP64, because WAVEFORMAT carries two bytes of tail padding), so reading the caller's pointer as WAVEFORMATEX would take padding as the sample depth and silently produce 0-bit audio. free-direct therefore reads these buffers as PCMWAVEFORMAT, with the reason written beside the cast in DirectSound.cpp.
Both games also call SetPan() only on mono effects, which is why the contract was documented as mono-only. That does not make panning audible: at 934f72f SetPan clamps and stores the value and calls the gain helper, which computes left and right gains for a mono source and then discards them; only the ordinary volume reaches SDL through SDL_SetAudioStreamGain. The README row "PARTIAL (constant-power pan, mono sources only)" describes the intent, not the output. The tests prove clamping, return values and no-crash behaviour, not an audible stereo split.
From bridge to destination: a native-CNA plan for Free Eggbert
A separate document in the free-eggbert working tree, cna.md (untracked; 30,145 bytes, SHA-256 87b1e3de041cdbe2af85c8c4b4faf6e74f0e518f9ff72f851f339e5419ceadb3, dated 2026-07-16), studies a different question: what it would take to port the game directly onto CNA's Game, GraphicsDevice and SpriteBatch surface, removing free-direct and free-api instead of running through them. It frames the compatibility layer as a migration bridge, not a destination: a CNA application using the FREEDIRECT renderer has adopted the XNA-facing API but still transitively depends on free-direct.
The game is already C++. The study measures about 31,773 lines across src/*.cpp and include/*.hpp, more than 21,000 of them simulation, world, object and event logic, and recommends rewriting the platform-services layer (graphics, input, audio, video, files, networking) while leaving the simulation files (decblupi, decmove, decblock, decdesign, decio and the game's tables) recognisable. That also keeps a direct comparison point between old and new behaviour.
Keep the CPixmap boundary
The study keeps CPixmap's public shape across its 98 direct call sites and replaces only its implementation, with Texture2D, RenderTarget2D and SpriteBatch standing in for DirectDraw surfaces; rendering equivalence is then tested in one subsystem instead of across 98 edits. The one piece of that surface CNA's loose-content formats cannot express is SetTransparent2's colour-key range. At this snapshot a Texture2D .cnj reads colorKey as exactly three integers from 0 to 255 (CnjCanonicalRead.cpp, ReadCnjColorKey), and the loader keys out only texels that match all three exactly, setting their alpha to 0 (ContentManager.cpp, ApplyColorKey). A range therefore needs a game-specific loader that reads the pixels, zeroes alpha across the requested range and writes them back with GetData/SetData, which the study calls straightforward because the sheets involved are small.
Keep the 50 ms simulation cadence
Free Eggbert updates from a 50 ms multimedia timer, a fixed 20 Hz cadence, while CNA's Game defaults to 166,667 ticks (about 16.67 ms). Without an explicit TargetElapsedTime the port would run the simulation about three times too fast. The game's F5–F8 speed controls add a constraint; the study recommends expressing them as 1, 2, 4 or 8 fixed 50 ms steps per tick rather than shortening the step, so that every simulation step keeps the original size.
MIDI: a public integration gap
The game's ten music files are Standard MIDI data, and no CNA Song route accepts them: the build-time SongImporter lists .mp3 .ogg .oga .qoa .flac .opus .aac .wma (SongContentPipeline.cpp), the XNB Song reader probes .ogg .oga .qoa (SongContentTypeReader.cpp), and the media library indexes only formats the bundled mixer can play (MediaLibraryIndex.cpp). Below that surface a decoder does exist: CNA's SDL_mixer build turns FluidSynth off (-DSDLMIXER_MIDI_FLUIDSYNTH=OFF in ThirdPartySDL.cmake) and passes nothing for MIDI or Timidity, and at the pinned SDL_mixer submodule both SDLMIXER_MIDI and SDLMIXER_MIDI_TIMIDITY default to on, so a Timidity decoder is compiled in, subject to a usable instrument configuration. What is missing is a supported Song and content mapping, an asset and instrument policy, and a retained test showing the route works in CNA rather than merely compiling beneath it.
The study offers three options: pre-render every track to OGG or FLAC against a chosen SoundFont (lowest implementation risk, but a licensing and distribution question for the rendered files and the SoundFont); expose and verify a reusable CNA-level MIDI route (cleaner for later games, but a framework decision one port should not take alone); or keep a Free Eggbert-specific MIDI service built on free-api's existing TinySoundFont path while everything else moves to CNA. Its staged recommendation uses the game-specific bridge for a first, behaviour-preserving port, pre-rendered assets for a low-maintenance release if asset policy allows, and a CNA-level route only if neither is acceptable.
A game-side networking defect
CNetwork::Receive in the reconstruction (src/network.cpp at 7ea63d1) receives into a local 500-byte stack buffer, zeroes the caller's destination, and never copies the received bytes into it. This is a defect of the decompiled game, not of CNA or free-direct, and it makes the reconstruction an unreliable specification of multiplayer behaviour. Future network work needs a versioned wire format and two-process tests against intended behaviour, not preservation of the omission.
Renderer progression and the licensing boundary
The recommended sequence starts on SDL_RENDERER for focused portable 2D validation, builds the same port against FREEDIRECT as a differential bridge between the old and new public APIs, and adds an independent GL run for evidence beyond one renderer. The document predates CNA's renderer naming migration and calls that GL identity EASYGL; today it is one of the five GL profiles of the EasyGL family, typically OPENGLES3. FREEDIRECT remains a migration tool rather than the target architecture: a build that links it has adopted CNA's public API but keeps the DirectDraw-era dependency. Free Eggbert declares GPL-3.0 and CNA declares Ms-PL; combining and distributing them in one linked program needs a licensing review, and neither the study nor these pages make that legal determination.
A finished translation: Speedy Blupi 2013 on CNA
mobile-eggbert is the other kind of port: a C# XNA 4.0 game translated to C++. Its README records the route: the 2013 Windows Phone release was decompiled to C# with ILSpy, moved from XNA 4.0 to MonoGame, rewritten class by class in C++ (about 32,000 lines), and moved from MonoGame to CNA. At d43c6a1 the game target WindowsPhoneSpeedyBlupi links only the modules whose public API it uses (CNA::Runtime, CNA::Devices, CNA::GamerServices and SharpRuntime::IO.IsolatedStorage), loads loose content from a Content root through getContentProperty().Load, saves through System::IO::IsolatedStorage, reads Microsoft::Devices::Sensors::Accelerometer, and documents SDL_RENDERER as its Linux renderer.
Its timing choice is the one recommended above: Game1 sets TargetElapsedTime to 500,000 ticks (50 ms, the original 20 Hz) divided by a TIME_SCALE that its legacy configuration locks to 1.0. Its experimental higher-rate modes take the other route, multiplying frame-count delays by TIME_SCALE and per-frame speeds by SPEED_SCALE, which changes the simulation step and so has to be judged as new behaviour rather than as the original game.
CNA's own documents record two dated runs of this game against earlier CNA revisions: WEBGPU-130 reached the main menu with correct SpriteBatch output and played the mission-start cut-scene on a Linux desktop on 2026-07-12 (webgpu-renderer.md), and a HEADLESS build ran more than 20 seconds without crashes (headless-renderer.md). The playable web build at speedyblupi.com embeds an early (May 2026) CNA revision on SDL_RENDERER, neither alpha.1 nor this snapshot, and no Android package is published; Tutorial 95 has the details.
The official samples as integration pressure
cna-samples translates Microsoft's XNA Game Studio 4.0 sample collection. Its plan.md at 4da98a0 is the only status source (there is no machine-readable manifest) and says to recount it from the table: 153 upstream directories, one row each, of which 87 are complete, 49 await owner decisions, 14 are documented non-ports, 2 are in progress and Racing Game is governed by a separate plan. 40 complete samples are published as WebGL 2 bundles at samples.libcna.com (repository 736b8aa). The alpha.1-era split of 63 ported, 23 placeholders and 67 ignored is gone: the plan re-audited every directory, treats historical labels such as Done or Ignored as "evidence to re-check, not current conclusions", and the shader-driven samples once said to be blocked by .fx are complete through official XNA-built effect XNBs on the compiled-effect path.
What makes the count meaningful is the definition behind it. A row becomes complete only after seven gates: classification of the upstream directory; original evidence (the original built and run under real XNA, on a Linux-side route or an offline Windows 7 virtual machine); a complete translation of every C# file; zero workarounds; a native run on OPENGLES3 compared with the original; a real-browser run of the WEBGL2 bundle; and an updated missing.md, which "is an audit record, not a waiver". The sources of truth are ranked (the original sample, then a running original, then FNA, then documentation mirrors, and MonoGame ports "only as supplemental evidence"), and a framework gap found by a sample is fixed in its owning repository, CNA or sharp-runtime, in the same session rather than worked around in the sample.
The re-port of SimpleAnimation shows why old labels must be re-tested. An earlier pass had marked it done after hand-converting tank.fbx into JSON and binary buffers and repairing that conversion by hand. The current row replaces all of it with the three XNB files XNA's own pipeline wrote, dumps the 12-bone hierarchy from the loaded model, and compares with real XNA at four pinned instants: 99.96–99.98 % of pixels within 8 levels natively and 99.74 % for the WEBGL2 build in a real browser, with nothing changed in CNA or sharp-runtime for the sample. Its bundle gate was calibrated by breaking what it watches (disabling TextureEnabled drops the green fraction from 23.35 % to 5.77 %). It also shows the limit of a sample as an oracle: a translation can misread the C# source, replace assets or skip a scene, so a port is strong evidence only when paired with the running original and retained captures. Read each sample's missing.md the same way: as a dated case record that names an affected program and a mechanism (an asset substitution, a reader gap, a hardware fallback, a framework defect), which makes it better evidence than the top-level ratio and also makes it age. The plan overturns old reasons repeatedly: the ShatterEffect placeholder's claim that the .fx had to be hand-translated to GLSL was "stale twice over" because compiled effects had worked since SAMPLE-032, and CustomModelEffect's old write-up called its custom shader and three chained processors blockers although the unchanged upstream processors ran through the official pipeline. So read the old reason, find the current API and test, then port the sample or replace the reason with a new, evidenced boundary. The maintainer-side procedure for a failing sample is investigating a sample failure.
The example catalogue: one navigable CNA-native application
cna-examples is original code: at ea33c9a one application with 13 areas, 79 categories and 249 demo screens, each calling real XNA or CNA APIs across framework, math, content, storage, diagnostics, input, audio, devices, networking, media, avatars and 2D and 3D graphics. --list-demos is its authoritative list, and tools/check_catalog.py enforces that the README table, its plan, the screen files and the registrations agree. Each screen serves a person and a sweep: --demo takes an Area/Category/Demo path or any unambiguous substring, --keys scripts input, --frames bounds the run and ignores real input devices, and --screenshot writes the result. Because all screens share one process, global state has to be restored: the Framework sweep checks that after the resolution demo switches the back buffer to 800×600 and leaves, it is measurably back at 960×640.
The repository distinguishes its verification forms rather than calling everything "verified": real hardware interaction for the keyboard, the mouse, most of the Input area's Other category and audio; headless screenshot and behaviour sweeps under Xvfb for the Framework and Media areas (tools/sweep.sh, tools/check_shots.py, and tools/headless.sh, because SDL3 prefers Wayland when WAYLAND_DISPLAY is set and would ignore the virtual display); C++ claim programs (tools/checks/math_claims.cpp, cnj_claims.cpp, xact_claims.cpp) that assert what a screen states; self-checking 3D screens that turn readback or occlusion results into a visible verdict swatch; and explanatory screens for missing controllers, touch, sensors, cameras or dialogs. The separation matters because a screen that catches its own exception can still paint a clean page: a screenshot checker detects blank or overflowing output, while only a claim program or self-check decides whether the operation was correct.
Its build instructions at ea33c9a already use CNA_GRAPHICS_RENDERER with OPENGLES3 (the older backend-named option of alpha.1-era copies is gone), and it records 249 of 249 screens rendering on each of OPENGLES3 and SDL_RENDERER, where the 3D area explains its unavailability instead of throwing. Its README's list of override values still names an identity that CNA has since retired, so revalidation should update the selector names first and keep screenshots plus tool output; the 249/249 figure is that repository's dated record, not a run against this snapshot.
Use them together
For learning, start with the examples: one build, searchable screens, current CNA-shaped code and explicit evidence helpers. For migration risk, read the samples: original provenance, per-sample scope records and failures driven by realistic combinations of calls, default states, component timing and content. For framework confidence, make them disagree productively:
| Observation | Where to look next |
|---|---|
| A CNA-native screen passes, the official port fails | The translation and the lifecycle order the original relies on |
| Both fail on one renderer and pass on another | The renderer; build the same revision on a second identity |
| Both pass, yet differ from XNA | Bring in a real-XNA capture or FNA as the oracle (oracles and tolerances) |
Examples demonstrate, samples integrate, and tests and independent references decide.
What transfers to other ports
The call-site method yields a narrower layer than a speculative full-surface implementation and surfaces what an API-only reading misses (GetDC's gameplay role, the PCMWAVEFORMAT layout, a pan call that stores but does not pan). Applied to a native CNA port it finds the stable seams, timing assumptions, evidence gaps and licensing questions before code moves; the samples add that a completion label must be re-earned whenever its evidence changes.
Evidence and limits
free-direct was read at 934f72f (sources and audit documents), free-eggbert at 7ea63d1 plus the hashed cna.md, and the consumer repositories at the pins in the first table. CNA statements (colour-key reading, Song extension lists, the SDL_mixer configuration, the default timestep) were read at 009d40f5 and its pinned SDL_mixer submodule. Nothing was built or run; the MIDI decoder inside CNA is unverified, and pixel percentages, sweep counts and statuses are those repositories' own records.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- Architecture overview
- Maintainer workflow
- Investigate a sample failure
- Deep dives
- Migrating an XNA game · The CNA ecosystem