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.