# RP 4.12 on Linux — compatibility assessment Requirements exploration for making RP412 playable on Linux (desktop and Steam Deck). A formatted copy of the original assessment lives at . **Status: it runs.** The assessment below was written 2026-08-12 as prediction; on 2026-08-14 a field test on real hardware found and cleared the one blocker, and the game played a full mission on Linux. §0 is what the test found and what was done about it — read it first, it corrects several predictions in §2. The remaining work is the Deck-specific and DXVK items in §0's open list. **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. ## 0. Field test, 2026-08-14 — one stubbed function, and a Windows bug behind it Test environment: Linux Mint 22.3, RTX 5070 Ti (driver 595.84), Wine 10.0 Staging from GE-Proton10-21, 32-bit prefix, **wined3d** renderer, windowed 1920×1080, symbols from the shipped `rpl4opt.pdb`. Build 4.12.233. **The blocker was `D3DXConcatenateMeshes`.** Wine has never implemented it. The game calls it exactly once per session, from `ConsolidateStaticObjects` ([MUNGA_L4/L4VIDEO.cpp](../MUNGA_L4/L4VIDEO.cpp)) via [MUNGA/APP.cpp](../MUNGA/APP.cpp)'s first-mission-load latch, so every player hits it on their first race. Wine raised `EXCEPTION_WINE_STUB` (0x80000100) naming the function in plain text; our own crash filter caught it and wrote `rpl4crash.dmp`, so it presented as a mystery crash. Of Wine's 329 `d3dx9_43` exports, 63 are stubs; the game imports 20 of them; exactly one is stubbed. There is no second landmine on the current code path. **The fix was already a Windows bug.** `d3dx9_43.dll` is a *hard* import of `rpl4opt.exe` — confirmed in the import table, and only `steam_api.dll` is delay-loaded — and it is **not part of Windows**. It ships solely with the June 2010 DirectX End-User Runtime. A customer on a clean Windows 10/11 who has never installed that redistributable cannot start the game at all: the loader fails at 0xC0000135 before `WinMain`, with no window and no log line. Every machine this was built or tested on had the SDK or some older game installed, which is why it went unnoticed. `pack-dist.ps1` now ships the DLL beside the exe, extracted from the SDK's redistributable cab. Wine prefers an application-directory DLL over its own builtin, so the same one file closes the Windows failure and the Linux blocker together — verified working on a stock prefix with no overrides and nothing installed. > **Licensing:** the DirectX SDK terms nominate DXSETUP as the delivery > method for this redistributable. Shipping the loose DLL is near-universal > practice and is what is verified working on both platforms, but confirm the > terms before a public release. **Address space, not memory.** The 32-bit process mapped **VmSize 3193 MB** under Wine, and on an older Wine that exhausted the address space outright — the game died before video init with nothing useful logged. It is not using that much memory: measured on Windows, a standalone mission peaks at **166 MB committed / 179 MB working set against 795 MB of address space**, already a ~4.8× ratio. Address space is inflated by reservations, file mappings, and above all the graphics driver mapping VRAM apertures and its own bookkeeping into the process; under wined3d's OpenGL path with an NVIDIA driver that grows several-fold again. For a 32-bit process this is a hard 2 GB ceiling entirely separate from RAM, so it is possible to run out of *addresses* while using 170 MB. `/LARGEADDRESSAWARE` is now set: it raises the ceiling to 4 GB on any 64-bit host, frees no memory and changes no behaviour. Worth having on Windows too — 795 MB of 2 GB is already 40% before 1080p, eight pods, and the 100 MB buffer `RP412RECORDSIZE` takes when recording is armed. **Diagnostics added, so the next gap names itself.** The crash filter now decodes 0x80000100 and writes the offending `module.function` to `rpl4.log` before the minidump, and startup probes `wine_get_version` in `ntdll` and logs the platform next to the version banner. Neither line can ever appear on Windows. Together they would have reduced this entire investigation to reading one line of a tester's log. **Do NOT install the VC++ redistributable into a Wine prefix.** It *causes* a crash — a null dereference inside `std::mutex`, reached from `KeyLight_SetLogger` via `PadRIO::PadRIO`. Wine's builtin C++ runtime works correctly. This is a support-desk trap worth knowing, because installing the redistributable is a common first instinct. (On real Windows the VC++ runtime is genuinely required; Steam installs it as a depot prerequisite.) ### Predictions in §2 that the test corrected - **The `windowsapp.lib` static-import worry was wrong.** §2 called it a load-time failure risk under Wine. The game boots, and the VC++ trace above runs *through* `KeyLight_SetLogger` — that code executes. Wine satisfies the WinRT API-set imports. - **`d3dx9` was the right area, the wrong function.** §2 named `D3DXLoadMeshFromXA` and `D3DXCreateTextureFromFile` as the heaviest leans on Wine's reimplementation. Both are fully implemented. The lesson is method: rank by *what Wine stubs*, not by what the game uses most — intersecting the import table against Wine's stub list yields the answer directly. - **Address-space exhaustion was not predicted at all**, and static analysis would not have found it. ### Still open after this test - **DXVK is untested.** The test ran wined3d; Proton defaults to DXVK for D3D9. The §2 headline risk — `Present` into a child `STATIC` via `hDestWindowOverride` with GDI panes clipped over it — is specifically a DXVK question, and whether the run exercised cockpit split mode (`L4MFDSPLIT=1`) at all is not recorded. **This is the next test**, and the rig now exists to do it cheaply. - **Steam networking under Proton** — SDR/FakeIP through the lsteamclient bridge. The test's networking was almost certainly plain TCP. - **Everything Deck-specific**: gamescope with the plasma window and the exploded MFD view, the gamepad-complete front end, the 9 px font floor. - **Optional load-time polish, both platforms**: 303 `d3dx_load_pixels_from_pixels` calls (texture format conversion at load — shipping textures in device format skips it) and 150 `OptimizeInplace` calls (the `.X` loader; baking meshes to an in-house format at build time would retire the `.X` dependency). - **Swap Creative's OpenAL for OpenAL Soft** — the same shape of bug as the `d3dx9_43.dll` one above: `pack-dist.ps1` copies `OpenAL32.dll` out of the build machine's `SysWOW64`, and what ships is Creative's 2009 router + `wrap_oal.dll` + a 791 KB installer. Bundling OpenAL Soft instead means the same implementation runs on Windows and Linux. Scoped in [RP412-UNIFIED-BUILD.md](RP412-UNIFIED-BUILD.md) — near-term, independent of the port. - **The native port** is scoped separately in [RP412-UNIFIED-BUILD.md](RP412-UNIFIED-BUILD.md) (proposal, not scheduled). Its survey supersedes several figures in §1 and §3 below — notably that `/Zp1` affects exactly one struct, that the `` leak into engine core is `NETWORK.h` rather than `MATRIX.h`, and that ISO conformance rather than include hygiene is what actually blocks a GCC compile. - **The durable fix for the blocker**: replace `D3DXConcatenateMeshes` with a hand-written mesh merge, ~60–80 lines using only helpers Wine implements (`D3DXCreateMesh`, `D3DXGetDeclVertexSize`, `D3DXVec3TransformCoordArray`, `D3DXMatrixInverse`, …). Worth doing eventually because `D3DXSimplifyMesh` and `D3DXSplitMesh` are stubs too, so any future mesh work hits the same wall. Verify by comparing vertex/face counts and attribute ranges against the D3DX output on Windows. ## 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) > Written 2026-08-12 as prediction. The first row is **disproven** by the > field test and is kept only so the reasoning is on the record; rows 2–5 > stand and remain Deck work. See §0. | Item | Evidence | Candidate fix | |------|----------|---------------| | ~~**WinRT lamp mirror is a static import.**~~ **WRONG — see §0.** The reasoning was that `windowsapp.lib` is not delay-loaded and Wine has no `windows.devices.lights`, so the image would fail to load before the in-code guards could run. In practice Wine satisfies the WinRT API-set imports and the game boots; `KeyLight` code demonstrably executes. | [MUNGA_L4/L4KEYLIGHT.cpp](../MUNGA_L4/L4KEYLIGHT.cpp) line 33 | None needed. | | **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 (status as of the 2026-08-14 field test) - **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. **STILL OPEN — the field test ran wined3d, not DXVK.** Next 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. **STILL OPEN.** - ~~**Wine's d3dx9**: `D3DXLoadMeshFromXA` and `D3DXCreateTextureFromFile`~~ — **DONE, and both are fine.** The stubbed import was `D3DXConcatenateMeshes`; see §0. - **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.