# RP 4.12 on Linux — compatibility assessment Requirements exploration for making RP412 playable on Linux (desktop and Steam Deck). Status: assessment complete (2026-08-12), **parked until late playtesting** — nothing has been run on Linux yet; the Proton test matrix in §3 is the next concrete step when this resumes. A formatted copy of this report lives at . **Verdict: target Proton as the official Linux path; treat a native port as a separately-scoped future project.** The existing 32-bit Win32/D3D9 build is squarely on Proton's best-supported path, every Proton-blocking issue found is fixable inside the Windows build, and Valve's Deck Verified program accepts Windows-only titles. A native port is a genuine second platform layer (~77k lines of `MUNGA_L4` plus ~6,300 lines of raw Win32 UI in `RP_L4`) and it forces the 32/64-bit question, which reaches into the wire protocol and the `.RES` format. Nothing on the Proton path is throwaway: each fix is an improvement to the shipping Windows build. Provenance: code findings below are measured against this repo (four exploration sweeps over `MUNGA`, `MUNGA_L4`, `RP`, `RP_L4`, `DivLoader` and the build files, 2026-08-12, main @ 1e0c412). External claims — Proton/DXVK behavior, Steam Runtime direction, Deck Verified criteria — are from Valve partner docs, the DXVK source, the steam-runtime repository, and Wine release notes as of August 2026, with unconfirmed items called out as needing live tests. ## 1. How the Windows dependency actually distributes The architecture is far more portable than the mechanics. The engine is single-threaded by construction, has exactly one timing seam (`SystemClock`, implemented in [MUNGA_L4/L4TIME.cpp](../MUNGA_L4/L4TIME.cpp)), one file-I/O seam ([MUNGA/FILESTUB.cpp](../MUNGA/FILESTUB.cpp), six syscalls), a formal network transport interface with all real socket calls in one ~350-line class ([MUNGA_L4/L4NETTRANSPORT.cpp](../MUNGA_L4/L4NETTRANSPORT.cpp)), and audio already speaks OpenAL. But the mechanical surface is total: **1,800 of 1,844 quoted `#include` lines cite a filename in a case that doesn't exist on disk, and 334 use backslash separators** — on a case-sensitive filesystem, nothing compiles until a repo-wide normalization pass is done. | Tree | Lines | Portability | |------|-------|-------------| | `RP\` | 16,441 | **Clean.** Zero Win32 API, zero Win32 types, zero threads. Only 16 `stricmp` and 4 `getenv`. | | `MUNGA\` | 116,695 | **~90% clean** (267 of 297 files carry no Win32/MSVC token). The leaks are few and named: `PostQuitMessage` as the abort path (32 sites in 9 files; the portable prototype still sits commented out at `MUNGA/STYLE.H:31`), [MUNGA/MATRIX.h](../MUNGA/MATRIX.h) line 4 including `` (which transitively feeds `windows.h` to most of the engine), `HWND`/`SOCKADDR_IN` carried as opaque values in a handful of headers, and `SPOOLER`'s `CreateDirectoryA`/`CopyFileA`. | | `RP_L4\` | 22,588 | **Mixed.** 20 of 34 files clean. The rest is `WinMain`/`WndProc` ([RP_L4/RPL4.CPP](../RP_L4/RPL4.CPP)) plus ~6,300 lines of hand-written Win32+GDI UI (front end, lobby, console board) behind no platform interface. | | `MUNGA_L4\` | 76,575 | **The platform layer.** 41 of 100 files touch D3D9 / DirectInput / XInput / Winsock / serial / WinRT / Steam. This is what a native port rewrites. | | `DivLoader\` | 1,714 | **No callers.** Entirely non-portable (D3D9 in its public API, an implementation-defined bitfield read off disk) — and nothing in this tree calls it. Delete, don't port. | Inventories that came back smaller than expected: - **Input**: the only XInput entry point used anywhere is `XInputGetState` (3 call sites). The live DirectInput reader ([MUNGA_L4/L4JOY.cpp](../MUNGA_L4/L4JOY.cpp)) models 8 axes / 32 buttons / 4 hats — a direct match for SDL's joystick model. - **Audio**: OpenAL (~91 calls) + libsndfile (3 calls), **zero** winmm/mmsystem anywhere. EFX is already optional and self-degrading. One engine-core leak: `MUNGA/AUDIO.cpp` calls three `al*` functions directly. - **Networking**: TCP only, no UDP, no `select()`/`poll()` — a pure per-frame nonblocking poll loop branching on exactly three error codes (`WSAECONNREFUSED`/`WSAECONNRESET`/`WSAEWOULDBLOCK`). - **Steam**: four interfaces total (NetworkingSockets, NetworkingUtils, Matchmaking, Friends+User), all gated behind the `SteamNetTransport_ClientLibraryPresent()` probe. The vendored SDK already ships `linux32/linux64 libsteam_api.so`. Steam Input is not used. - **Renderer**: 100% fixed-function D3D9 — no shaders, no D3D9Ex. All ~193 device calls sit in five `MUNGA_L4` files. D3DX usage is mesh loading (`D3DXLoadMeshFromXA`), texture loading (PNG), and matrix helpers. The leaky seam is **windowing**: of ~430 user32/gdi32 call sites, about two-thirds live *outside* the platform layer, and the per-frame message pump sits in engine core (`MUNGA/APPMGR.cpp:126`). The cockpit composition is genuinely exotic Win32: up to 11 top-level windows, D3D `Present` into a child `STATIC` control via `hDestWindowOverride`, GDI-painted panes clipped over the 3D viewscreen, and hit-testing that relies on the stock STATIC proc returning `HTTRANSPARENT`. ## 2. Path A — Proton as the official Linux target (recommended) External findings (August 2026): 32-bit D3D9 through DXVK is Proton's most-traveled path and is deliberately preserved — Steam Runtime 4 dropped most i386 libraries *except* the ones Proton needs. Wine 10 (in current Proton) merged Vulkan child-window rendering, removing the one historical blocker that would have hit the viewscreen pane directly. Valve's stated position is neutral between native and Proton, Deck Verified is achievable with a Windows-only build, and the prevailing small-studio practice for legacy Win32 titles is to ship Proton officially. Wine resolves filenames case-insensitively, so the repo's asset-case inconsistencies cost nothing on this path. One dated fact that matters: a 32-bit-specific Proton bug corrupted `ISteamNetworkingSockets::ReceiveMessagesOnConnection` data — exactly RP412's receive path — and was fixed only in **Proton 10.0-4b / 11.0-1b (July 2026)**. The multiplayer test matrix should pin that as the minimum version. ### Fix in the Windows build (confirmed in code) | Item | Evidence | Candidate fix | |------|----------|---------------| | **WinRT lamp mirror is a static import.** `windowsapp.lib` via `#pragma comment` — unlike `steam_api.dll`, not delay-loaded. Wine has no `windows.devices.lights` at all; the in-code try/catch guards only run if the image loads. | [MUNGA_L4/L4KEYLIGHT.cpp](../MUNGA_L4/L4KEYLIGHT.cpp) line 33 | Delay-load it the way `steam_api.dll` is handled, or move activation behind a `LoadLibrary` probe. Cheap, and hardens the Windows build too. | | **Second top-level window fights gamescope.** The "Plasma Display" window plus the exploded `L4MFDSPLIT=2` mode (6 top-level panes) hit gamescope's known multi-window weakness on Deck. | [MUNGA_L4/L4PLASMASCREEN.cpp](../MUNGA_L4/L4PLASMASCREEN.cpp) line 271, [MUNGA_L4/L4MFDVIEW.cpp](../MUNGA_L4/L4MFDVIEW.cpp) line 297 | On Deck/gamescope, default the plasma glass into the cockpit window (the `SetParent` path already exists at `L4PLASMASCREEN.cpp:91`) and treat exploded view as desktop-only. | | **Implicit hit-test transparency.** Clicks fall through the viewscreen only because the stock STATIC proc returns `HTTRANSPARENT`; no explicit `WM_NCHITTEST` handler exists. If Wine's STATIC differs, all mouse input over the 3D view dies silently. | [MUNGA_L4/L4VB16.cpp](../MUNGA_L4/L4VB16.cpp) line 4659 | Handle `WM_NCHITTEST` explicitly in the viewscreen subclass. Removes the dependency on an undocumented control behavior everywhere. | | **Deck legibility floor.** The 1920×1080 canvas lands on Deck at 1280×720 — a 1.5× shrink. Valve's gate is 9 px minimum font height, so anything under ~14 px on the canvas fails. | Fixed canvas: [MUNGA_L4/L4APP.cpp](../MUNGA_L4/L4APP.cpp) line 269 | Audit the smallest cockpit and front-end type; bump or provide a Deck-scale preset. | | **Gamepad-complete front end.** The menu/lobby is mouse-driven GDI; callsign entry is a Win32 `EDIT` control. Deck Verified requires the full flow on pad alone, with the Steam on-screen keyboard for text. | [RP_L4/RPL4FE.cpp](../RP_L4/RPL4FE.cpp) line 2467 | Add pad navigation to the front end and invoke `ShowFloatingGamepadTextInput` for the callsign field. The largest Path-A work item. | ### Verify under Proton (needs live testing; no code change assumed) - **Child-window Present**: `Present(…, hDestWindowOverride)` into a child STATIC under DXVK — supported per the DXVK source (per-HWND presenter map) and Wine 10's child-window Vulkan work, but this exact composition (GDI siblings clipped over the presented pane) is the least-exercised path in any D3D9 stack. First thing to smoke-test. - **Steam networking end-to-end**: repeat the three-machine SDR/FakeIP race ([STEAM-3-MACHINE-TEST.md](STEAM-3-MACHINE-TEST.md)) with one or more peers on Proton ≥ 10.0-4b. No Proton-specific FakeIP defects are on record, but absence of reports is not confirmation. - **Wine's d3dx9**: `D3DXLoadMeshFromXA` (.x meshes) and `D3DXCreateTextureFromFile` (PNG) are the two heaviest leans on Wine's reimplementation. - **Input odds and ends**: the `IG_` RawInput device-path heuristic for XInput de-duplication (`L4JOY.cpp:72-146`) assumes Windows-shaped HID paths; `GetAsyncKeyState` + foreground-window focus gating under gamescope's focus model; the legacy DirectInput paths' `DISCL_EXCLUSIVE` claims. - **The joyconfig wizard** (`AllocConsole` + `conio`, `L4JOY.cpp:811-1392`) very likely misbehaves under Proton — it is optional, and pad/keyboard defaults don't need it; document rather than block on it. - **Multi-adapter gauge windows** (up to 4 D3D devices, one per adapter, `L4VB16.cpp:207`) are thin ice under DXVK — but that's the arcade multi-monitor config, not the consumer default. Confirm the default path never creates them. ## 3. Path B — the native port, honestly scoped Feasible — the clean core makes it a real option, not a rewrite of the game — but it is a phased engineering project, not a compatibility fix: 0. **Mechanical compile pass — everything blocks on this.** Normalize include case and separators repo-wide (267 of 289 distinct include names don't match disk case; renaming files inside a case-insensitive checkout takes care), shim the MSVC CRT tail (100 `stricmp`, the Annex K `_s` family, the `_time64` family, `itoa`/`strlwr`, 9 `std::ostrstream` sites), fix `Fail_To_Debugger`'s non-const `char*` signature (GCC/Clang reject it outright, so the `DEBUG_LEVEL>0` build doesn't compile at all), and address the `SKIPPY_CAST` strict-aliasing punning (`MUNGA/STYLE.H:108`). 1. **Drain the core leaks.** Replace `PostQuitMessage`-as-abort with the portable hook whose prototype still sits commented out in `STYLE.H:31` (32 sites); strip `` out of `MATRIX.h` and give it a neutral interop type; typedef-shim the `HWND`/`HINSTANCE`/`SOCKADDR_IN` values carried in core headers; split `RIOBase` out of [MUNGA_L4/L4RIO.h](../MUNGA_L4/L4RIO.h) so the controls stack stops dragging `` through the serial-packet header; port `FILESTUB` (six syscalls) and `L4TIME` (one file); add a path-separator/case layer to `FILEUTIL` and the 19 hardcoded `"dir\\"` literals. 2. **The 32/64-bit fork — the strategic decision.** The wire protocol and `.RES` format are `/Zp1`-packed host-order struct dumps carrying `size_t` and `unsigned long` fields (`MUNGA/RECEIVER.h:172`, `MUNGA/RESOURCE.h:208-210`). A **32-bit native build** keeps every format byte-identical but targets Steam's legacy *scout* runtime — swimming against the current. A **64-bit build** (what Steam Runtime 4 wants) means retyping the serialized fields to fixed widths, discovering and pack-annotating the serialized struct set (46 raw stream overloads, no manifest exists), and versioning the wire so Linux and Windows builds can still race each other. Dovetails with the un-`/Zp1` cleanup already on the roadmap; if the native port ever happens, 64-bit with fixed-width formats is the right side of the fork. 3. **The new platform layer.** SDL for window/events/input (XInput's one function → `SDL_GameController`; `L4JOY`'s 8-axis/32-button model maps directly to `SDL_Joystick`, and SDL makes the RawInput de-dup unnecessary); OpenGL for the fixed-function renderer (~193 device calls in five files, no shaders to translate — the real work is replacing D3DX mesh/texture loading); the CPU-side gauge canvases become streaming textures instead of GDI blits; OpenAL-soft is a near drop-in; sockets go BSD (mechanically small: 3 errno branches, ~45 `SOCKADDR_IN` occurrences); Steam loads via `dlopen` through the flat API (~30 symbols) since ELF has no `/DELAYLOAD`. 4. **Rebuild the desktop UI.** The front end, lobby, and console board — ~6,300 lines of GDI drawing and Win32 controls behind no interface — get redrawn through the engine or SDL, and the ~580-line console-mode joyconfig wizard gets a portable rewrite. This is also where the four separate message pumps collapse into one event loop. 5. **Delete, don't port.** The legacy DirectInput paths (`L4DINPUT`, `ThrustMaster`), DOS-era `L4KEYBD`/`L4MOUSE` stubs, the AWE32 audio layer and `sos/`, `JOYSTICK.asm`, DivLoader (no call sites), and Windows Dynamic Lighting (OS-exclusive by nature). The serial RIO path is optional hardware support — stub it, or port it to termios only if pod preservation on Linux ever matters. ## 4. Sequencing The Path-A fixes (lamp-mirror delay-load, explicit hit-test, Deck window policy, font audit, pad-driven front end) all improve the Windows build on their own, and none are wasted if Path B ever runs. The reverse is not true. When this resumes: an afternoon smoke test of the current build under Proton on a Linux box — child-window Present first — before committing to anything.