From 29ad567ffbe452665e08cc98874cdb1e7b3493f9 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 14 Aug 2026 18:07:05 -0500 Subject: [PATCH] What a native Linux build would actually cost, measured Filed, not started. Proton already carries Linux players and the Windows build reaches a running mission under it; this is the answer to the separate question of what one source tree building natively on both would take, so that the number exists before anyone needs it. Three sweeps of the tree, and the measurements are the part worth keeping. Two of them make the job smaller than the earlier assessment feared, and one makes it different. /Zp1 was called an entanglement with no manifest of which structs mattered. There is a manifest now: 31 distinct whole-object stream types, of which 29 are enums, scalar typedefs or all-float aggregates. Modelled at both packings and compared by sizeof, exactly one struct changes - the anonymous .met reader in L4D3D.cpp, 14 bytes packed against 16 default. Dropping /Zp1 costs one pragma. That also retires the packing divergence between RPL4TOOL and the game that FORCE:MULTIPLE has been hiding. The include mess is not the blocker. 172 of 172 core translation units fail under GCC, and 171 still fail with every 64-bit error removed, because MUNGA/SCHAIN.h uses a friend-declared name as a type and sits in the precompiled block. One line breaks the entire core, and 155 friend sites over 117 names share the shape. The whole conformance list is about thirty sites across twenty headers - small, and it gates everything. And the windows.h leak into engine core is NETWORK.h, not MATRIX.h. Winsock2.h reaches APP.h and 157 translation units, which is how APPMGR.h's bare 'extern HWND ghWnd' has been compiling all along. The D3DX9 include in MATRIX.h is real but narrow: ten files, two members. docs/RP412-LINUX.md said otherwise and now says so. What the sweeps found in our favour: RP is clean outright, 190 of 241 units touch no platform symbol, the renderer's distinct surface is nineteen render states and one mesh draw call with no shaders, all 224 .X files are text using eight templates, and the entire gauge production path - including four thousand seven hundred lines of L4VB16 - is already portable C++. RIOBase, the whole input seam, is two pure virtuals and five scalars. DivLoader is referenced by nothing and linked by nothing; the lobby room screen has no callers at all. The plan is phased so that almost everything happens on Windows first, verified by the existing build and the two-pod harness, with Linux arriving only at phase four. A port that spends six months unable to run is a port that gets abandoned. The document ends by saying when this would not be worth doing, which is most of the time: Proton already delivers the player experience, and only the first three phases have standalone value. It becomes worth starting when the goal changes to retiring the 2010 SDK dependency or removing Wine as a support variable. Co-Authored-By: Claude Fable 5 --- README.md | 1 + docs/RP412-LINUX.md | 6 + docs/RP412-UNIFIED-BUILD.md | 296 ++++++++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 docs/RP412-UNIFIED-BUILD.md diff --git a/README.md b/README.md index a90026f..5ac4016 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ mission. Full map with pad and keyboard diagrams: | [docs/STEAM-3-MACHINE-TEST.md](docs/STEAM-3-MACHINE-TEST.md) | Multiplayer test procedure, Steam Input notes, the abort key | | [docs/RP412-LINUX.md](docs/RP412-LINUX.md) | Linux compatibility assessment — Proton path vs native port; parked until late playtesting | | [docs/NET-TEST.md](docs/NET-TEST.md) | Network test protocol — WAN emulation profiles, the NetLog/NetStats telemetry, the stall-recovery test, `-spoolstats` | +| [docs/RP412-UNIFIED-BUILD.md](docs/RP412-UNIFIED-BUILD.md) | Proposal (not scheduled) — one source tree building native on Windows and Linux; the portability survey behind it | | [BUILD.md](BUILD.md) | Toolchain and build steps | Dev tooling: `tools/two-pod-test.ps1` races two pods on loopback, diff --git a/docs/RP412-LINUX.md b/docs/RP412-LINUX.md index 9e050eb..728ac86 100644 --- a/docs/RP412-LINUX.md +++ b/docs/RP412-LINUX.md @@ -121,6 +121,12 @@ is genuinely required; Steam installs it as a depot prerequisite.) 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). +- **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`, diff --git a/docs/RP412-UNIFIED-BUILD.md b/docs/RP412-UNIFIED-BUILD.md new file mode 100644 index 0000000..f48f5de --- /dev/null +++ b/docs/RP412-UNIFIED-BUILD.md @@ -0,0 +1,296 @@ +# RP 4.12 — unified Linux/Windows native build (proposal) + +**Status: proposal, not scheduled.** Written 2026-08-14 against HEAD `9fae440`. +Nothing here is committed to. It is filed so the measurements survive — the +survey behind it took three full sweeps of the tree, and the numbers are the +durable part whether or not the plan is ever run. + +The shipping Linux path today is **Proton**, and it works: see +[RP412-LINUX.md](RP412-LINUX.md) §0 for the field test and the `d3dx9_43.dll` +fix that unblocked it. This document describes the *other* thing — one source +tree producing a native binary on both platforms — and exists because the +question "what would that actually take?" deserved a real answer rather than a +guess. + +## The four decisions this plan assumes + +Taken 2026-08-14. Changing any of them changes the plan's shape. + +| Question | Decision | Consequence | +|----------|----------|-------------| +| Cross-play Windows↔Linux? | **Yes, same race** | Serialized platform-sized types must be pinned to fixed width | +| Renderer | **GL on Linux now, converge Windows later** | The GL path must be Windows-capable from day one — SDL context creation, portable GL loader, no GLX-specific code | +| Build system | **One CMake for both** | The working MSBuild build gets migrated; highest-regression-risk step | +| What ships meanwhile | **Proton stays the shipping path** | No deadline pressure; Deck work proceeds separately and benefits both | + +## What was measured + +The survey is the reason to keep this document. Three findings materially +changed the risk picture from the earlier 2026-08-12 assessment. + +### 1. `/Zp1` is nearly a non-issue + +The prior audit flagged project-wide 1-byte struct packing as a serious +entanglement with "no manifest of which structs matter." There is now a +manifest. Of **31 distinct whole-object stream types**, 29 are enums, scalar +typedefs, or all-`float` aggregates. Modelled at both packings and compared by +`sizeof`, exactly **one** struct changes layout: + +```c +// MUNGA_L4/L4D3D.cpp:264-273 — read whole from every .met file at :282 +struct { unsigned char minify, magnify, alpha, wrap_u, wrap_v; + bool doScroll; float scrollUDelta, scrollVDelta; } fileTexOp; +// /Zp1 = 14 bytes; default packing = 16 (padding after the bool) +``` + +Dropping `/Zp1` costs one `#pragma pack(1)`. It also retires +`WINDOWS_IGNORE_PACKING_MISMATCH`, the Steamworks `pack(push,8)` guards, and +the latent `RPL4TOOL`-vs-`rpl4opt` packing divergence that `/FORCE:MULTIPLE` +currently hides. + +### 2. ISO conformance is the real blocker, not includes + +Under GCC 15 with `-fsyntax-only`, **172 of 172 `MUNGA`+`RP` translation units +fail — and 171 still fail with every 64-bit-related error removed.** The +keystone is one line: + +```cpp +// MUNGA/SCHAIN.h — friend-declared name used as a type +class SChainLink : public Link { + friend class SChain; // :10 + SChainLink(SChain *chain, Plug *plug, …); // :18 ← not a type under ISO +}; +``` + +`schain.h` sits in `MUNGA.h`'s precompiled block, so that single line breaks +the entire core. The same pattern appears at **155 `friend class` sites over +117 names**. Other classes: iterator typedefs of friend-declared names (66 +TUs), `Now()`/`HiResNow()` declared friend-only inside `SystemClock` but called +at 50+ sites (25 TUs), `new (Subsystem (*[n]))` array syntax GCC parses as a +lambda (4 sites), `__try`/`__except`, `std::ios::_Openprot`, MSVC `` +`SW_*` constants, `goto` crossing initialization. + +Roughly **30 distinct sites across ~20 headers** — small in absolute terms, +but they gate all 241 TUs. + +### 3. LP64 has two systemic breaks + +- `MEMSTRM.H:20` `void Rewind() { SetPointer(0U); }` becomes ambiguous between + `SetPointer(void*)` and `SetPointer(size_t)` the moment `size_t` ≠ + `unsigned int`. **Fails in 171 of 172 TUs on 64-bit, zero on 32-bit.** +- On LP64, `size_t` matches **none** of the eight `MemoryStream_Read` + overloads. On ILP32 it silently binds to the `unsigned int` one. + +### The good news, quantified + +- **`RP\` is 100% clean** — 0 of 20 TUs touch a platform API. Of 241 compiled + TUs, **190 touch no Win32/D3D/Winsock symbol at all**. +- **The renderer's distinct surface is tiny**: 19 render states, 7 + texture-stage states (stages 1–2 only ever `D3DTOP_DISABLE`, so effectively + one texture unit), 5 sampler states hardcoded to linear, 5 FVFs, **one** mesh + draw call, no shaders, no indexed draws, no render targets, no stencil, no + scissor, one directional light. +- **The assets are a small closed subset**: all 224 `.X` files are *text* + format using 8 templates, no skinning, no vertex colours; 56,521 verts total, + largest single mesh 2,195. All 104 PNGs are 8-bit RGBA. +- **The gauge/MFD production path is already portable C++** — `GRAPH2D`, + `GAUGREND`, `GAUGE`, `GAUGMAP`, `L4VB8`, `L4GAUGE`, `L4GREND`, `L4GAUIMA`, + `RPL4GAUG` all measure zero Win32/D3D hits, plus **~4,700 of `L4VB16.cpp`'s + 7,230 lines** (the software rasterizer, the `L4GraphicsPort` channel packing + that emulates the pod's five-MFDs-in-two-video-outputs trick, the palette + fades). Only its *destination* is platform-bound. +- **`RIOBase` — the whole input seam — is 2 pure virtuals** (`GetNextEvent`, + `SetLamp`), 13 virtuals with working empty bodies, and 5 public `Scalar`s. + `PadRIO` implements it in 6 methods. +- **The GDI vocabulary is minimal**: no `Rectangle`, `Ellipse`, `LineTo` or + `CreatePen` anywhere. The entire visual language is `FillRect`, `FrameRect`, + `DrawTextA`, one `Polygon` (a combo arrow), `BitBlt`, `StretchDIBits`. + +### Counts at HEAD + +1856 quoted includes · 351 with backslashes · **1764 case-wrong** (only 30 +fully correct) across 468 files · 226 `#pragma hdrstop` · 129 `stricmp` in 28 +files · 12 `stdext::hash_map` in 5 files · **54 `PostQuitMessage`** in 17 files +· 111 `getenv` in 38 files · 421 user32/gdi32 calls in 27 files · 54 backslash +path literals in 17 files, of which 8 are directory-case and 3 file-case +mismatches against disk. + +### Confirmed dead — delete rather than port + +`DivLoader` (zero references tree-wide; `DivLoader.lib` is 2.6 MB and linked by +nothing) · `RPL4Lobby_Room`/`_Join` plus `RunRoom`/`PaintRoom`/`RoomWndProc`, +~410 lines with no callers — the front end absorbed the room · `L4KEYBD` (only +call site commented out) · `L4MOUSE` (fully stubbed) · `MUNGA_L4/sos/` and +`libDPL/` · `LOGGER.cpp` (on disk, in no project) · the 44 `.tcp` test includes. + +## The strategic shape + +**Almost all the work happens on Windows, verified by the existing build and +the two-pod harness, before a single Linux compile is attempted.** Phases 0–3 +make the *Windows* build ISO-conformant, platform-decoupled, fixed-width and +CMake-built, each step green against a live race. Only Phase 4 adds Linux. + +That ordering is the point. It is what stops a year-long project from having a +six-month stretch where nothing runs and nothing can be tested. + +## Phases + +### Phase 0 — ISO conformance and hygiene *(Windows only)* + +Fix the `friend class`-as-type family (keystone `SCHAIN.h:18`; same shape in +`CAMINST.h`, `ENTITY.h`, `INTORGN.h`, `PLUG.h`, `TRACE.h`, `L4AUDHDW.h`, and +the iterator typedefs in `HOSTMGR.h`, `HOST.h`, `COLLORGN.h`, `COLLASST.h`, +`RNDORGN.h`, `MISSION.h`). Declare `Now()`/`HiResNow()`/ +`Get_Frame_Percent_Used()` at namespace scope. Clear the ~30 remaining +conformance sites. Scripted include normalization across 468 files. A CRT +compat header (`stricmp`, Annex K `_s`, `ostrstream`→`ostringstream`, `itoa`). +Drop `#pragma hdrstop`. Delete the dead list. + +*Gate: MSBuild Release clean · two-pod passes · `g++ -fsyntax-only` reaches +zero errors over `MUNGA`+`RP` on a 32-bit target.* + +### Phase 1 — Break platform coupling *(Windows only)* + +- **`MUNGA/NETWORK.h:8-9` is the real `` leak** — `` + reaches `APP.h` and **157 TUs**, and is why `APPMGR.h:5`'s bare + `extern HWND ghWnd;` compiles at all. (`MATRIX.h`'s `` is the other + route and is much narrower: 10 files, 2 members.) Replace `SOCKADDR_IN` in + engine headers with a portable address struct. +- `VIDREND.h:60` `std::vector` → portable rect struct. + `APPMGR.h`/`SPOOLER.h` `HWND`/`HINSTANCE` → opaque handle. +- Split `RIOBase` out of `L4RIO.h` (lines 16–220 have no Win32 dependency; only + `RIO` below needs `l4pcspak.h` → ``). Retype ~8 `RIO::` → + `RIOBase::` in `L4CTRL.h`. +- `PostQuitMessage` → the portable abort hook whose prototype is still + commented out at `MUNGA/STYLE.H:31` (54 sites). +- Remove `` from `MUNGAL4.H:6` (reaches 62 TUs). Fix the 3 OpenAL calls + leaking into `MUNGA/AUDIO.cpp`. +- One `ResolveAssetPath()` for separator, case, and an **exe-relative base** — + `GetModuleFileName` is called *nowhere* today, so everything resolves against + the working directory, which breaks read-only installs and XDG conventions. + **Careful:** `RPL4VID.cpp:1099` does `TempFileName[8] = skeleton_type;`, + hardcoding the `"video\\"` prefix length. + +*Gate: MSBuild clean · two-pod passes · a race against an **unmodified** build +still works, since nothing on the wire has changed yet.* + +### Phase 2 — Fixed-width formats and LP64 readiness *(Windows only)* + +Retype, don't redesign — the files already contain 32-bit values. + +Pin to `uint32_t`: `RESOURCE.h:208-210` and the `.RES` offset table +(`RESOURCE.cpp:658/807/867` — note `:867` already writes `sizeof(long)` against +`:658`'s `sizeof(size_t)`, a latent inconsistency) · `RECEIVER.h:172` +`messageLength`, which is in every wire packet and every spool byte · +`SIMULATE.h:57`, `DAMAGE.h:137`, `BOXSOLID.h:171`, `CAMINST.h:12` · +`TIME.h:53` `Time::ticks` · the spool headers in `SPOOLER.cpp` and +`L4CTRL.cpp:2662` · `L4VIDEO.cpp:920/970` `fread(sizeof(size_t))`. + +`L4NET.H:404/431` `unsigned long streamPointer` carries a `SOCKET` **and those +classes are on the wire** — must become a fixed-width handle. + +Fix the `MemoryStream` overload set for LP64. Handle `VDATA.h:9/12` +(`RegisteredClassMemoryAddress` is a truncating pointer cast used as a +container key) and `VERIFY.cpp:132/213` `*(int*)(0xFFFFFFFF)`, a hard 64-bit +compile error. + +Drop `/Zp1`; add `#pragma pack(1)` to `fileTexOp`. + +**Egg text rasterization**: `AppendNameBitmap` (`RPL4FE.cpp:1809`) uses GDI to +rasterize the callsign into 1bpp hex rows **inside the egg**. With cross-play +on, both platforms must emit identical bytes — so this moves to a bundled +bitmap font used on **both**, not a native text API on each. + +*Gate: `.RES` loads unchanged (validated through +`tools/resbuild/build-res.ps1`, the only regeneration path) · `.met` textures +still read · a cross-version race proving the wire is byte-identical.* + +### Phase 3 — CMake migration *(Windows first)* + +Reproduce the settings exactly: Unicode charset, the four defines, +`/FORCE:MULTIPLE`, `steam_api.dll` delay-load + `delayimp`, `/DYNAMICBASE:NO`, +`LargeAddressAware`, LTCG/COMDAT in Release, `$(DXSDK_DIR)` paths appended +*after* the Windows SDK, and the `stamp-version` pre-build step (~30 lines of +CMake; the logic has nothing PowerShell-specific). + +**Use `-iquote`, never `-I`** for the source directories: `MUNGA/TIME.h`, +`SET.h`, `RANDOM.h`, `ITERATOR.h` collide with standard headers — verified to +break `` → `` → `MUNGA/TIME.h`. + +*Gate: the CMake-built Windows exe passes the two-pod harness and a smoke +mission before MSBuild is retired.* + +### Phase 4 — The Linux platform layer + +- **SDL3** for window, events, timers, input. **Map SDL scancodes into the + Windows VK namespace** — `bindings.txt` stores VK codes and is a shipped, + user-editable file; changing the namespace breaks every existing profile. +- **GL 2.1 compatibility profile** implementing `VideoRenderer` — a near-1:1 + mapping for fixed-function. `D3DZB_USEW` has no GL equivalent: use a normal + depth buffer and `glPolygonOffset` for the decal pass, which is the correct + GL tool and replaces the `_33 -= 5e-7` projection hack. +- **Asset loaders**: a text `.X` parser for the 8 templates in use; `stb_image` + for PNG; a hand-written mesh merge replacing `D3DXConcatenateMeshes`. **The + merge must reproduce D3DX's subset-numbering order exactly** — + `ConsolidateSingleObject` remaps attribute IDs against it and both + lookup-failure branches are empty `//Freak out` blocks. It must also preserve + one-texture-object-per-image, because `HashDrawOp` hashes the raw texture + pointer (which is why the 1×1 per-plate marker textures exist). +- **BSD sockets** behind the existing `NetTransport` seam (3 errno branches). + **Steam via `dlopen` + `steam_api_flat.h`** — ELF has no delay-load + equivalent and C++ vtable calls can't be resolved by `dlsym`. The + `linux32`/`linux64` `.so` files are already on disk but `.gitignore`d. +- **Shims**: `FILESTUB` (6 syscalls; `_lseek`'s `long` caps files at 2 GB) · + `L4TIME` → `clock_gettime` · the console thread → `std::thread` · the crash + handler → `signal` + `backtrace`. +- **Fix the particle VB lock length** (`L4PARTICLES.cpp:513` requests 1/6 of + what it writes) — benign under `D3DLOCK_DISCARD`, not under a mapped GL range. + +*Gate: a Linux binary reaching `LocalConsole: mission running` with no crash, +then a Linux↔Windows two-machine race.* + +### Phase 5 — The UI + +Reimplement the front end's drawing against a portable primitive set: rect +fill, rect frame, single-line text with ellipsis, a monospace font at a +requested pixel height, a text-extent query, plus dropdown and single-line-entry +widgets. `RPL4FE.cpp` is ~32% drawing/windowing and ~68% already portable, and +`LayoutMenu` is 137 lines of pure rect arithmetic before it touches Win32. +Candidate substrate: the engine's own `GraphicsDisplay` interface +(`MUNGA/GRAPH2D.h:213`), which already provides `DrawFilledRectangle`, +`DrawText` and `DrawBitMap`. + +### Phase 6 — Convergence + +Switch Windows to the GL renderer, retiring the D3D9 path, the June-2010 SDK +dependency and `d3dx9_43.dll` shipping — but only after the GL path has field +time on Linux. + +## Risks + +- **`.gitattributes` forces `eol=crlf`** on all source in the working tree, so a + Linux checkout gets CRLF. Decide early: normalize to LF, or keep and ensure + tooling tolerates it. +- **Resource-embedded filenames** (`GRAPH2D.cpp:188/614/801`, + `RPL4VID.cpp:1093`) concatenate a prefix with names read out of `RPL4.RES`; + their case cannot be determined from source and needs a resource-table dump. +- **Phase 3 is the highest-regression-risk step** — it replaces a working build + system for a shipping product. It is deliberately placed after the code is + already portable, so a rollback to MSBuild stays possible. +- **`bindings.txt` and the egg name bitmaps are user- and wire-visible + formats.** Both are handled above, but a mistake in either is a silent + compatibility break. + +## When this would *not* be worth doing + +Stated plainly, because the honest answer matters more than the plan: + +Proton already delivers the Linux player experience, and Phases 0–2 are the +only parts with standalone value (ISO conformance, decoupled seams, fixed-width +formats and an exe-relative path layer would each improve the Windows build on +their own). If the goal is "Linux players can buy and play it," that goal is +**already met** by the shipping path. This project is worth starting when the +goal is different: retiring a 2010-era closed SDK dependency, owning the render +path outright, or removing Wine as a variable in support. If none of those +becomes pressing, Phases 0–2 can be cherry-picked as ordinary maintenance and +the rest left on the shelf.