Strategy selection — one decision per subsystem
The single most important architectural insight: a port is not one decision, it's one decision per subsystem, chosen by API surface size and determinism risk.
| Strategy | When | Instance |
|---|---|---|
| Translate | Huge embedded API; bit-faithful behavior required | DirectX 8 → DXVK → Vulkan (→ Metal) |
| Shim | Many small scattered calls, simple semantics | CompatLib: 28 headers reimpl of Win32 POSIX |
| Swap behind interface | Codebase already has device/manager abstractions | SDL3GameEngine beside Win32GameEngine |
| Stub now, impl later | Non-gameplay-critical | WWStub lib (screenshots, file dialogs) |
| Rewrite | Last resort | One fork rewrote D3D8→Metal directly — rejected upstream as unreviewable |
Rule of thumb: translate for the renderer (fidelity), swap for audio/video (natural seams), shim for OS plumbing, stub for periphery.
Universal porting sequence (5 phases)
- Analysis & references — before code: map the architecture, find every reference implementation (even partial ones).
- Make it compile with stubs — compiler/ABI fixes, whole-function stubbing of Windows code.
- Graphics + windowing + input first among real subsystems — longest pole, only then can you test.
- Audio, then video/cutscenes — natural manager seams, lower risk.
- Polish & hardening — paths, lifecycle, packaging, CI, upstream sync.
- New-architecture pass (64-bit/ARM) — only after same-arch works.
Compat-shim pattern, done right
- One shim layer, included first. Stubs scattered inline conflict with third-party headers.
- Wrap whole functions, not lines. When 50%+ Windows API,
#ifdefthe entire function. - Never wrap gameplay logic. That's how determinism dies invisibly.
- Keep Win32 signatures in the shim so game code doesn't change — diffable against upstream.
- Expect ~dozens of headers, a few hundred lines total.
Portability bug taxonomy (check proactively)
| Class | Bug | Detection |
|---|---|---|
| ABI: integer width | long = 4 bytes Win32, 8 bytes LP64 → binary file parsing breaks | Fixed-width types + static_assert(sizeof(X)==N) |
| ABI: wchar_t | 2 bytes Windows, 4 bytes elsewhere → Unicode desync | Hard-code disk format (UTF-16LE) |
| Exit semantics | ExitProcess skips dtors; POSIX return runs them | _exit() after cleanup |
| Case sensitivity | Asset paths working on NTFS fail on case-sensitive FS | Case-normalizing VFS layer |
| Path separators | \\ literals in data-driven paths | Normalize in FS layer |
| MSVC builtins | __max, __int64, __forceinline | One compat-types header + grep audit |
| High-DPI | Window points ≠ drawable pixels → wrong input/render | Per-API scaling decision + corner-tap test |
| Dual package roots | Intel + ARM Homebrews → wrong arch picked | Pin PKG_CONFIG_PATH |
Determinism as the master gate
The deepest practice, directly transferable to any engine with replays/lockstep:
- Replay determinism testing in CI — run recorded games headless, assert frame-exact outcomes.
- Both platforms built in every session — even when focused on one.
- Artifact verification over exit codes —
strings/nm/otoolinto the binary, not whether commands succeeded. - Behavioral acceptance criteria per phase ("menu renders", "10-min stability"), not "it compiles".
If the target has replays, demos, or lockstep networking, make this gate #1 before touching anything.
Translation-layer integration (DXVK chapter)
When you adopt a translation layer (DXVK, MoltenVK, ANGLE, Wine pieces, Rosetta shims):
- Pin it to an immutable commit of your own fork; integrate via ExternalProject/submodule.
- Translation layers stack: D3D8 → Vulkan → Metal worked, but each boundary has capability mismatches.
- Configuration is part of the port: ship the layer's config (
dxvk.conf) with tuned options. - Learn the layer's conventions before patching — DXVK loads SDL via function-pointer table, not linking.
Process patterns (meta-engineering)
- Session handoff documents — every session ends with blockers, achievements, "next session first task".
- Lessons library — every painful debugging session becomes a permanently citable artifact.
- Source annotation —
// ProjectName @bugfix author date description. - Minimal-diff discipline — one PR per category, platform never mixed with logic.
- Agent-assisted dev with guardrails — agents must research reference repos before coding.
- Reproducible build environments — Docker for Linux targets.
- Progress metric honesty — track error categories resolved, not LOC.
De-risking ladder for arbitrary legacy codebases
- Ecosystem sweep — hunt for partial references, abandoned branches, similar-engine ports.
- Choose per-subsystem strategies + write architecture-decision log before coding.
- Compile-with-stubs milestone — stub Windows-only functions whole.
- Graphics translate + windowing swap → first pixels (half the total effort).
- Audio/video swaps behind existing manager seams.
- Determinism gate online as early as replays can run; both-platform builds every session.
- New-architecture pass (64-bit/ARM) as its own phase.
- Platform-specific delivery last (packaging, signing, lifecycle, input paradigm).
- Document as you go — documentation is the velocity.
Calibration: with full references available, platform delivery ≈ days. Without ≈ months. With neither (true rewrite) ≈ everyone abandons.