Author SHA1 Message Date
CydandClaude Fable 5 3229a82380 The audio stack we ship is whatever the packing machine had installed
pack-dist copies OpenAL32.dll out of the build machine's SysWOW64. That is
the same shape as the d3dx9_43 bug found this week - a runtime dependency
the dev box quietly satisfies - except here it does not fail outright, it
just means the audio implementation a customer gets is decided by whatever
was installed on the machine that happened to pack the release.

And what that machine has is Creative's 2009 stack. OpenAL32.dll v6.14 is
only a router; the real implementation is wrap_oal.dll v2.2.0.5 beside it,
with oalinst.exe bundled as a 791KB fallback installer. The router exists
so several vendors' OpenAL implementations could coexist on one machine,
which stopped being a thing about fifteen years ago.

Bundling OpenAL Soft instead is the fix: vendored like steam_api.dll is,
not lifted from the system. wrap_oal.dll and oalinst.exe go, dist gets
1.2MB smaller, and the same implementation runs on Windows and Linux -
which matters more than the size, because it means an audio bug reproduces
on both instead of being someone's Windows. It also removes the only
__declspec in the tree, which lives in Creative's al.h.

Only al.h, alc.h and efx.h are ever included, eleven sites across ten
files. efx-creative.h, EFX-Util.h and xram.h are vendored and referenced by
nothing, so they simply go.

Filed as a near-term item rather than a port phase, and written up in both
docs, because it stands on its own: OpenAL is not what makes the game hard
to move. The API is cross-platform and all ninety-one call sites compile
unchanged against libopenal - it is the reason audio barely appears in the
port plan at all. Dropping OpenAL itself would mean reimplementing EFX
reverb and rewriting ninety-one working call sites to remove a dependency
that is not in the way. The note says so, so nobody talks themselves into
it later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:17:53 -05:00
CydandClaude Fable 5 29ad567ffb 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 <noreply@anthropic.com>
2026-08-14 18:07:05 -05:00
CydandClaude Fable 5 9fae4406b5 The DirectX runtime we never shipped
A tester ran 4.12.233 under Proton and it died on first mission load. The
cause turned out to name itself, and to be a Windows bug we have been
shipping the whole time.

Wine implements d3dx9_43 itself, and 63 of its 329 exports are stubs. The
game imports twenty of them and exactly one is stubbed:
D3DXConcatenateMeshes, called once per session from
ConsolidateStaticObjects on the first-mission-load latch - so every player
hits it on their first race. Wine raised EXCEPTION_WINE_STUB naming the
function in plain text, and our own crash filter caught it and wrote a
minidump, which is how a self-describing failure arrived as a mystery.

The fix is one file, and it was overdue on Windows. d3dx9_43.dll is a HARD
import of the exe - only steam_api.dll is delay-loaded - and it is not part
of Windows. It ships with the June 2010 DirectX End-User Runtime and
nothing else. Any customer on a clean Windows 10 or 11 who never installed
that redistributable cannot start the game at all: the loader fails at
0xC0000135 before WinMain, no window, no log line. Every machine this has
been built or tested on had the SDK or some older game installed, which is
precisely why nobody has ever seen it. pack-dist now extracts the DLL from
the SDK's redist cab into dist. Wine prefers an application-directory DLL
over its builtin, so the same file closes the Windows failure and the Linux
blocker together, and it is verified working on a stock prefix with nothing
installed. The SDK terms nominate DXSETUP as the delivery method for this
redistributable; the loose DLL is what is verified and is near-universal
practice, but that wants confirming before a public release.

/LARGEADDRESSAWARE, because the same session reported VmSize 3193MB and an
older Wine that died before video init with the address space exhausted.
The game is not using that memory: measured here, a standalone mission
peaks at 166MB committed against 795MB of address space. The gap is
reservations, mapped files, and mostly the graphics driver mapping VRAM
apertures into our process - under wined3d's OpenGL path that grows several
times over. For a 32-bit process the 2GB address ceiling is a hard limit
with nothing to do with RAM, so it is entirely possible to run out of
addresses while using 170MB. The flag raises it to 4GB, frees nothing, and
changes no behaviour. Worth having on Windows too: 795MB of 2GB is already
40% before 1080p, eight pods, and the 100MB buffer RP412RECORDSIZE takes
when recording is armed.

And two diagnostics so the next one costs a log line instead of an
investigation. The crash filter now decodes 0x80000100 and writes the
offending module.function before the minidump; startup probes
wine_get_version in ntdll and logs the platform beside the version banner.
Neither can appear on Windows - verified: the smoke run's log has the
version line and no platform line.

Not fixed here, deliberately: the durable version of this is to write the
mesh merge by hand, since D3DXSimplifyMesh and D3DXSplitMesh are stubs too
and any future mesh work hits the same wall. And the field test ran
wined3d, not DXVK, so the child-window Present with GDI panes clipped over
it - the risk the assessment led with - is still untested. The doc records
both, along with the prediction it got wrong: the windowsapp.lib import was
called a load-time failure risk under Wine, and it simply is not one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:39:48 -05:00
CydandClaude Opus 4.8 b07e328553 document RP412MFDPROTRUDE for players (handbook + the shipped environ.ini)
The knob landed with only a source comment, so a player had no way to find
it.  Two channels, matching where the sibling display settings already live:

  - docs/rp412-handbook.html: a row in the "environ.ini - display layout"
    table beside L4MFDSCALE / L4RADARSCALE / L4RADARPOS, in that table's own
    voice (what it does, that it is a longer bar rather than a wider button,
    range and default).
  - RP_L4/RPL4ENVIRON.cpp: a commented block in the self-documenting default
    environ.ini the exe lays down on first run, placed at the end of the
    display-layout section after L4MAPSCALE.  Shown commented with an example
    value (10) since the default 0 means "leave the pod geometry alone" --
    uncommenting it is the action.

Both state the gotcha the type invites: it takes a NUMBER, and a word parses
to 0 (off).

Verified: RP_L4 rebuilds clean and the block reads back out of the freshly
linked rpl4opt.exe exactly as written, so a first run will lay it down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 14:09:33 -05:00
CydandClaude Opus 4.8 879b76c6e1 RP412MFDPROTRUDE: optional extra indicator strip on the MFD/map buttons
Ports BattleTech 4.11's button-protrusion knob to Red Planet, DEFAULT 0 --
nothing changes unless a player asks for it.  BT411 shipped this at 10 because
its players asked for longer lamps; Red Planet keeps the pod's geometry until
someone wants otherwise, so the two games stay tuned separately.

Mechanism: the bonus is added to `indicatorStrip` (10) at the two places it is
consumed -- strip_h for the MFD strips, strip_w for the map's side columns.
Everything else already derives from those two locals (the client size, the
display offset, both banks), so the window grows to fit and no other code needs
to know.  Only the strip moves, so the button keeps its width and its reach
behind the glass: a LONGER lamp bar, not a fatter face, and the press target
and the picture over it are unchanged.  Scope is the red MFD strips and the
amber side columns -- the only two ButtonStyles that have glass to hide behind.

Value rules match BT411's: unset -> 0; a number is that many pixels; negative
clamps to 0, over 200 clamps to 200; a WORD parses to 0, i.e. off.  A non-zero
value logs 'MFDView: RP412MFDPROTRUDE=N (indicator strip 10 -> N+10)' once.

Named for this repo's convention (RP412-prefixed, no underscores), following
RP412MFDLAYOUT -- itself the port of BT411's BT_GLASS_LAYOUT.

Verified live in dist with the freshly linked rpl4opt.exe: RP412MFDPROTRUDE=25
prints the receipt and the exploded windows lay out around the longer strips;
UNSET prints nothing at all, i.e. bonus 0 and geometry byte-identical to before
this commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 13:59:23 -05:00
10 changed files with 687 additions and 13 deletions
+9
View File
@@ -30,6 +30,15 @@ Committed under [lib/](lib/), no install needed: `OpenAL32.lib`, `libsndfile-1.l
`assets/RP411/oalinst.exe` — or beside the exe) and `libsndfile-1.dll` (beside the `assets/RP411/oalinst.exe` — or beside the exe) and `libsndfile-1.dll` (beside the
exe; a copy ships in `assets/RP411/`). exe; a copy ships in `assets/RP411/`).
It also needs **`d3dx9_43.dll`**, which is a hard import and is *not* part of
Windows — it ships only with the June 2010 DirectX End-User Runtime. A dev box
with the SDK installed has it system-wide and never notices; a clean Windows
10/11 fails to start the exe at all (0xC0000135, before `WinMain`).
[pack-dist.ps1](pack-dist.ps1) therefore extracts it from the SDK's
`Redist\Jun2010_d3dx9_43_x86.cab` into `dist\`. That same file is also what
makes the game run under Wine/Proton, whose builtin `d3dx9_43` stubs
`D3DXConcatenateMeshes` — see [docs/RP412-LINUX.md](docs/RP412-LINUX.md).
## 2. Building ## 2. Building
```powershell ```powershell
+42 -2
View File
@@ -92,6 +92,46 @@ namespace
const int buttonDepth = 240; const int buttonDepth = 240;
const int indicatorStrip = 10; const int indicatorStrip = 10;
//---------------------------------------------------------------
// Extra pixels of indicator strip, on top of the 10 above, so a
// FLASHING lamp is easier to notice on desktop glass - the pod's
// buttons were physical and backlit and never had that problem.
// Only the strip moves, so the button keeps its width and reaches
// the same distance behind the glass: this makes the lamp a
// LONGER bar, not a fatter face. The client size, the display
// offset and both banks all derive from strip_h/strip_w below, so
// the window grows to fit and nothing needs to know about it.
//
// DEFAULT 0 - Red Planet keeps the pod's geometry unless a player
// asks otherwise. (BattleTech 4.11 carries the same knob defaulted
// to 10, where the players asked for it; the two games are tuned
// separately on purpose.) Ported alongside RP412MFDLAYOUT, which
// took the same route from BT411's BT_GLASS_LAYOUT.
//---------------------------------------------------------------
const int defaultProtrude = 0;
int ProtrudeBonus()
{
static int bonus = -1;
if (bonus < 0)
{
const char *setting = getenv("RP412MFDPROTRUDE");
bonus = (setting != NULL && setting[0] != '\0')
? atoi(setting) // a word parses to 0, i.e. off
: defaultProtrude;
if (bonus < 0) bonus = 0;
if (bonus > 200) bonus = 200;
if (bonus != 0)
{
DEBUG_STREAM << "MFDView: RP412MFDPROTRUDE=" << bonus
<< " (indicator strip " << indicatorStrip << " -> "
<< (indicatorStrip + bonus) << ")\n" << std::flush;
}
}
return bonus;
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// The map paints its own legend beside each side button, and that // The map paints its own legend beside each side button, and that
// grid is not the naive height/6: measured off the 640-tall map, // grid is not the naive height/6: measured off the 640-tall map,
@@ -649,7 +689,7 @@ void
// and the picture above/below it is the click target. Paint // and the picture above/below it is the click target. Paint
// draws the buttons before the MFD image for that reason. // draws the buttons before the MFD image for that reason.
//----------------------------------------------------------- //-----------------------------------------------------------
int strip_h = indicatorStrip; int strip_h = indicatorStrip + ProtrudeBonus();
int button_h = buttonDepth; int button_h = buttonDepth;
// on a scaled-down cockpit, keep the two banks from meeting // on a scaled-down cockpit, keep the two banks from meeting
@@ -692,7 +732,7 @@ void
// indicatorStrip clearing the edge, so the lamp reads as a // indicatorStrip clearing the edge, so the lamp reads as a
// slim column and the map itself is the click target. // slim column and the map itself is the click target.
//----------------------------------------------------------- //-----------------------------------------------------------
int strip_w = indicatorStrip; int strip_w = indicatorStrip + ProtrudeBonus();
int button_w = buttonDepth; int button_w = buttonDepth;
// keep the two columns from meeting behind a narrow map // keep the two columns from meeting behind a narrow map
+1
View File
@@ -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/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/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/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 | | [BUILD.md](BUILD.md) | Toolchain and build steps |
Dev tooling: `tools/two-pod-test.ps1` races two pods on loopback, Dev tooling: `tools/two-pod-test.ps1` races two pods on loopback,
+71
View File
@@ -83,6 +83,49 @@ HWND hWnd;
// //
static LONG WINAPI RPL4CrashDumpFilter(EXCEPTION_POINTERS *exception) static LONG WINAPI RPL4CrashDumpFilter(EXCEPTION_POINTERS *exception)
{ {
//
// Wine raises 0x80000100 (EXCEPTION_WINE_STUB) when the game calls an
// API Wine declares but has never implemented, and puts the module and
// function names in the first two exception parameters as plain ANSI
// strings. Wine prints that to its own stderr - which a player running
// through Steam never sees - and then this filter caught the exception
// and wrote a minidump, so a perfectly self-describing failure arrived
// as a mystery crash. That is exactly how D3DXConcatenateMeshes cost a
// full investigation on 4.12.233: one stubbed function out of the
// twenty d3dx9 imports, and nothing in our log named it.
//
// Unlike an access violation this is an orderly raise from Wine with
// the heap intact, so the log stream is safe to use here. Written
// before the dump, in case the dump is what fails.
//
if (exception != NULL &&
exception->ExceptionRecord != NULL &&
exception->ExceptionRecord->ExceptionCode == 0x80000100 &&
exception->ExceptionRecord->NumberParameters >= 2)
{
const char *stub_module =
(const char *) exception->ExceptionRecord->ExceptionInformation[0];
const char *stub_function =
(const char *) exception->ExceptionRecord->ExceptionInformation[1];
if (stub_module != NULL && stub_function != NULL &&
!IsBadStringPtrA(stub_module, 128) &&
!IsBadStringPtrA(stub_function, 128))
{
DEBUG_STREAM << "\n*** WINE STUB: " << stub_module << "."
<< stub_function << " is not implemented by this Wine.\n"
<< "*** The game called an API Wine only declares. Shipping the"
<< " real DLL beside the exe overrides Wine's builtin and is"
<< " usually the whole fix; see docs/RP412-LINUX.md.\n"
<< std::flush;
}
else
{
DEBUG_STREAM << "\n*** WINE STUB: an unimplemented API was called"
<< " (names unreadable)\n" << std::flush;
}
}
HMODULE dbghelp = LoadLibraryA("dbghelp.dll"); HMODULE dbghelp = LoadLibraryA("dbghelp.dll");
if (dbghelp != NULL) if (dbghelp != NULL)
{ {
@@ -225,6 +268,34 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
// //
DEBUG_STREAM << "Red Planet " << RP412_VERSION_LONG << std::endl << std::flush; DEBUG_STREAM << "Red Planet " << RP412_VERSION_LONG << std::endl << std::flush;
//
// And whether this is Windows at all.
//
// Wine exports wine_get_version from ntdll and Windows does not, so
// one GetProcAddress separates the two. It matters because the
// failure modes differ: Wine implements Direct3D and the D3DX helpers
// itself, and where a helper is only a stub the game dies at the call
// with no hint in its own log (see the filter above). A tester's log
// saying which platform it came from turns that from an investigation
// into a lookup. Never true on Windows, where the lookup fails and
// nothing is printed.
//
{
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
if (ntdll != NULL)
{
typedef const char * (__cdecl *WineGetVersionFn)(void);
WineGetVersionFn wine_get_version =
(WineGetVersionFn) GetProcAddress(ntdll, "wine_get_version");
if (wine_get_version != NULL)
{
DEBUG_STREAM << "Platform: Wine " << wine_get_version()
<< " (not Windows) - D3DX comes from Wine's own"
<< " implementation\n" << std::flush;
}
}
}
// load up our environment variables // load up our environment variables
//controls //controls
if(getenv("L4CONTROLS") == NULL) if(getenv("L4CONTROLS") == NULL)
+14
View File
@@ -178,6 +178,20 @@ namespace
"L4MAPPOS=LEFT\n" "L4MAPPOS=LEFT\n"
"L4MAPSCALE=100\n" "L4MAPSCALE=100\n"
"\n" "\n"
"# How far the lit buttons around each display reach past its edge, in\n"
"# EXTRA pixels. The pod's buttons were physical and backlit; on a\n"
"# desktop panel they are a thin bar along the glass edge, and a\n"
"# FLASHING one is easy to miss. This lengthens that bar - the button\n"
"# keeps its width and still reaches the same distance behind the\n"
"# picture, so the display and the click target are unchanged. The red\n"
"# MFD strips and the amber map columns only.\n"
"#\n"
"# 0 - the default - is the pod's own geometry, exactly as it shipped.\n"
"# 1-200 adds that many pixels. Give it a number: a word counts as 0.\n"
"# (BattleTech 4.11 carries the same setting defaulted to 10, where its\n"
"# players asked for it; the two games are tuned separately.)\n"
"#RP412MFDPROTRUDE=10\n"
"\n"
"# The Winners Circle: at the end of a race the finishers are stood on\n" "# The Winners Circle: at the end of a race the finishers are stood on\n"
"# the award platform in finishing order, with each pilot's callsign on\n" "# the award platform in finishing order, with each pilot's callsign on\n"
"# the plate beside their spot, and held there for a few seconds before\n" "# the plate beside their spot, and held there for a few seconds before\n"
+17
View File
@@ -77,6 +77,23 @@
helper. --> helper. -->
<DelayLoadDLLs>steam_api.dll;%(DelayLoadDLLs)</DelayLoadDLLs> <DelayLoadDLLs>steam_api.dll;%(DelayLoadDLLs)</DelayLoadDLLs>
<RandomizedBaseAddress>false</RandomizedBaseAddress> <RandomizedBaseAddress>false</RandomizedBaseAddress>
<!-- /LARGEADDRESSAWARE: a 32-bit process gets 2GB of user ADDRESS
SPACE by default, which is a separate and much scarcer resource
than memory. Measured standalone mission: 166MB committed
against 795MB of address space - the graphics driver maps VRAM
apertures and its own bookkeeping into our map, and managed-pool
resources are shadowed in system memory as well as VRAM, so the
map runs about five times the memory actually used. Under Wine
the same mission mapped 3193MB, because wined3d routes through
OpenGL and the NVIDIA driver maps far more aggressively; on an
older Wine that exhausted the space outright and the game died
before video init with nothing useful in the log. This raises
the ceiling to 4GB on any 64-bit host. It frees no memory and
changes no behaviour - it stops the process running out of
numbers. Equally worth having on Windows: 795MB of 2GB is
already 40% before 1080p, eight pods, and the 100MB spool
buffer RP412RECORDSIZE takes when recording is armed. -->
<LargeAddressAware>true</LargeAddressAware>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
</Link> </Link>
<!-- rpl4build.h is generated, not committed: the patch number is the <!-- rpl4build.h is generated, not committed: the patch number is the
+132 -11
View File
@@ -1,12 +1,15 @@
# RP 4.12 on Linux — compatibility assessment # RP 4.12 on Linux — compatibility assessment
Requirements exploration for making RP412 playable on Linux (desktop and Steam Requirements exploration for making RP412 playable on Linux (desktop and Steam
Deck). Status: assessment complete (2026-08-12), **parked until late Deck). A formatted copy of the original assessment lives at
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
<https://claude.ai/code/artifact/47e4cf5f-a9a3-416e-bd5e-25c810983551>. <https://claude.ai/code/artifact/47e4cf5f-a9a3-416e-bd5e-25c810983551>.
**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 **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 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 squarely on Proton's best-supported path, every Proton-blocking issue found is
@@ -25,6 +28,120 @@ 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 notes as of August 2026, with unconfirmed items called out as needing live
tests. 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 `<windows.h>` 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, ~6080 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 ## 1. How the Windows dependency actually distributes
The architecture is far more portable than the mechanics. The engine is The architecture is far more portable than the mechanics. The engine is
@@ -94,28 +211,32 @@ The multiplayer test matrix should pin that as the minimum version.
### Fix in the Windows build (confirmed in code) ### 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 25
> stand and remain Deck work. See §0.
| Item | Evidence | Candidate fix | | 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. | | ~~**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. | | **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. | | **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. | | **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. | | **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) ### Verify under Proton (status as of the 2026-08-14 field test)
- **Child-window Present**: `Present(…, hDestWindowOverride)` into a child - **Child-window Present**: `Present(…, hDestWindowOverride)` into a child
STATIC under DXVK — supported per the DXVK source (per-HWND presenter map) 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 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 siblings clipped over the presented pane) is the least-exercised path in any
D3D9 stack. First thing to smoke-test. 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 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 ([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 on Proton ≥ 10.0-4b. No Proton-specific FakeIP defects are on record, but
absence of reports is not confirmation. absence of reports is not confirmation. **STILL OPEN.**
- **Wine's d3dx9**: `D3DXLoadMeshFromXA` (.x meshes) and - ~~**Wine's d3dx9**: `D3DXLoadMeshFromXA` and `D3DXCreateTextureFromFile`~~ —
`D3DXCreateTextureFromFile` (PNG) are the two heaviest leans on Wine's **DONE, and both are fine.** The stubbed import was `D3DXConcatenateMeshes`;
reimplementation. see §0.
- **Input odds and ends**: the `IG_` RawInput device-path heuristic for XInput - **Input odds and ends**: the `IG_` RawInput device-path heuristic for XInput
de-duplication (`L4JOY.cpp:72-146`) assumes Windows-shaped HID paths; de-duplication (`L4JOY.cpp:72-146`) assumes Windows-shaped HID paths;
`GetAsyncKeyState` + foreground-window focus gating under gamescope's focus `GetAsyncKeyState` + foreground-window focus gating under gamescope's focus
+351
View File
@@ -0,0 +1,351 @@
# 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.
## Do this one now, independent of the plan
**Swap Creative's OpenAL for OpenAL Soft.** This is not port work. It is the
same shape of problem as the `d3dx9_43.dll` bug found on 2026-08-14 — a runtime
dependency the build machine quietly satisfies — and it is worth fixing on
Windows whether or not any of the phases below ever run.
What ships today is Creative's 2009-era stack: `OpenAL32.dll` v6.14 is only a
**router** that dispatches to `wrap_oal.dll` v2.2.0.5 (the real implementation),
with `oalinst.exe` (791 KB) bundled as a fallback installer. Worse,
`pack-dist.ps1` copies `OpenAL32.dll` **out of the build machine's `SysWOW64`**,
so the audio stack that ships is whatever happened to be installed on the
machine that packed it. The router architecture exists so multiple vendors'
implementations could coexist, which stopped mattering fifteen years ago.
The work:
- Bundle **OpenAL Soft** — ship its `soft_oal.dll` renamed to `OpenAL32.dll`,
vendored in the repo like `steam_api.dll` is, not copied from the system.
- Delete `wrap_oal.dll` and `oalinst.exe` from `dist\` and from
`pack-dist.ps1`; drop the "copy from `SysWOW64` if present" branch. Net
~1.2 MB smaller.
- Replace the vendored Creative headers in `MUNGA_L4\openal\` with OpenAL
Soft's. Only `al.h`, `alc.h` and `efx.h` are ever included (11 sites across
10 files) — `efx-creative.h`, `EFX-Util.h` and `xram.h` are referenced by
**nothing** and can simply go.
- Replace `lib\OpenAL32.lib` with OpenAL Soft's import library.
Why it is worth doing on its own merits:
- **The same implementation then runs on both platforms**, since Linux distros
ship OpenAL Soft as `libopenal.so`. Audio bugs reproduce across platforms
instead of being "works on my Windows."
- It removes the **only `__declspec` in the entire tree**, which lives in
Creative's `al.h`.
- It ends the silent dependency on the packing machine's system state.
Licensing is unremarkable: OpenAL Soft is LGPL, and shipping it as a separate,
dynamically-linked DLL is the standard compliant arrangement for a closed-source
game.
*Verify: audio plays in a mission; EFX reverb still engages (`EFX_Available()`
in `L4AUDEFX.cpp` — it probes `ALC_EXT_EFX` and degrades silently, so confirm
the log does **not** say "filters and reverb inert"); the two-pod harness
passes; `dist\` contains no `wrap_oal.dll` or `oalinst.exe`.*
**What this does *not* do: remove OpenAL.** The API is cross-platform and is
precisely why audio barely features in the phases below — all ~91 call sites
compile unchanged against `libopenal.so`. Dropping OpenAL entirely is possible
in principle (the game disables OpenAL's own attenuation and Doppler, does its
own 3D math, and never streams — it is using OpenAL as little more than a
mixer) but would mean reimplementing EFX reverb and rewriting ~91 working call
sites to remove a dependency that causes no portability problem. Not
recommended.
## 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 `<float.h>`
`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 12 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 03
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 `<windows.h>` leak** — `<Winsock2.h>`
reaches `APP.h` and **157 TUs**, and is why `APPMGR.h:5`'s bare
`extern HWND ghWnd;` compiles at all. (`MATRIX.h`'s `<D3DX9.h>` 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<MONITORINFO>` → portable rect struct.
`APPMGR.h`/`SPOOLER.h` `HWND`/`HINSTANCE` → opaque handle.
- Split `RIOBase` out of `L4RIO.h` (lines 16220 have no Win32 dependency; only
`RIO` below needs `l4pcspak.h``<windows.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 `<dos.h>` 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 `<pthread.h>``<time.h>``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 02 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 02 can be cherry-picked as ordinary maintenance and
the rest left on the shelf.
+1
View File
@@ -1133,6 +1133,7 @@
<tr><th>L4MFDSCALE_<span style="color:var(--phosphor)">xx</span></th><td>Any one on its own — <span class="addr" style="font-family:var(--mono)">UL UC UR LL LR</span>. Unset inherits the line above.</td></tr> <tr><th>L4MFDSCALE_<span style="color:var(--phosphor)">xx</span></th><td>Any one on its own — <span class="addr" style="font-family:var(--mono)">UL UC UR LL LR</span>. Unset inherits the line above.</td></tr>
<tr><th>L4RADARSCALE</th><td>The radar alone. It already sits at 1.35× the MFDs.</td></tr> <tr><th>L4RADARSCALE</th><td>The radar alone. It already sits at 1.35× the MFDs.</td></tr>
<tr><th>L4RADARPOS</th><td class="mono">CENTER · LEFT · RIGHT · MIDLEFT · MIDRIGHT</td></tr> <tr><th>L4RADARPOS</th><td class="mono">CENTER · LEFT · RIGHT · MIDLEFT · MIDRIGHT</td></tr>
<tr><th>RP412MFDPROTRUDE</th><td>Extra pixels of lit button reaching past each display edge, so a flashing one is easier to catch. A longer bar, not a wider button — the picture and the click target are unchanged. <span style="color:var(--ink-quiet)">0200, default 0 (the pod's own geometry)</span></td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
+49
View File
@@ -8,6 +8,8 @@
# TEST.EGG) - but not the arcade launch scripts or the old 4.10 exe # TEST.EGG) - but not the arcade launch scripts or the old 4.10 exe
# - libsndfile-1.dll beside the exe; OpenAL32.dll copied from the system # - libsndfile-1.dll beside the exe; OpenAL32.dll copied from the system
# when installed, with oalinst.exe included as the fallback installer # when installed, with oalinst.exe included as the fallback installer
# - d3dx9_43.dll from the DirectX SDK redist cab: a hard import that is
# NOT part of Windows, and also what makes the game run under Proton
# (environ.ini is NOT shipped - the exe writes it on first run) # (environ.ini is NOT shipped - the exe writes it on first run)
# - start/joyconfig scripts, HANDBOOK.html, VTV-PRESETS.html, TRACKS.html, # - start/joyconfig scripts, HANDBOOK.html, VTV-PRESETS.html, TRACKS.html,
# CONTROLS.txt and a README # CONTROLS.txt and a README
@@ -192,6 +194,53 @@ $handbookPage
"$dist\TRACKS.html", "$dist\TRACKS.html",
[System.IO.File]::ReadAllBytes((Join-Path $root 'docs\tracks.html'))) [System.IO.File]::ReadAllBytes((Join-Path $root 'docs\tracks.html')))
# --- D3DX runtime ----------------------------------------------------------
# d3dx9_43.dll is a HARD import of the exe, and it is NOT part of Windows.
# It ships only with the June 2010 DirectX End-User Runtime, so a customer
# who has never installed that redistributable - which is most of them, on a
# clean Windows 10 or 11 - cannot start the game at all: the loader fails at
# 0xC0000135 before WinMain, with no window and no log line. Every machine
# this has ever been built or tested on had the SDK or some older game
# installed, which is exactly why it went unnoticed.
#
# The same file is also what makes the game work under Wine/Proton. Wine
# implements 63 of d3dx9_43's 329 exports as stubs, and the game imports
# exactly one of them - D3DXConcatenateMeshes, called once per session from
# ConsolidateStaticObjects on first mission load. Wine loads a DLL from the
# application directory in preference to its own builtin, so shipping the
# Microsoft one here means the stub is never reached. One file closes a
# hard Windows failure and the entire Linux blocker at once.
#
# Taken from the SDK's redistributable cab rather than from SysWOW64: that
# cab is the licensed redistribution source, and it is the same signed
# Microsoft binary. NOTE: 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.
$d3dxCab = Join-Path $env:DXSDK_DIR 'Redist\Jun2010_d3dx9_43_x86.cab'
$d3dxDll = Join-Path $dist 'd3dx9_43.dll'
if ($env:DXSDK_DIR -and (Test-Path $d3dxCab)) {
& expand.exe $d3dxCab -F:d3dx9_43.dll $dist | Out-Null
if (Test-Path $d3dxDll) {
Write-Host " d3dx9_43.dll extracted from the DirectX SDK redist cab"
}
}
if (-not (Test-Path $d3dxDll)) {
# Fall back to an installed copy so a machine without the SDK can still
# produce a working package.
$d3dxSystem = "$env:WINDIR\SysWOW64\d3dx9_43.dll"
if (-not (Test-Path $d3dxSystem)) { $d3dxSystem = "$env:WINDIR\System32\d3dx9_43.dll" }
if (Test-Path $d3dxSystem) {
Copy-Item $d3dxSystem $dist
Write-Host " d3dx9_43.dll copied from $(Split-Path $d3dxSystem)"
} else {
Write-Warning ("d3dx9_43.dll NOT FOUND - this package will fail to start on any " +
"machine without the June 2010 DirectX runtime, and will crash " +
"under Wine/Proton on first mission load. Install the DirectX SDK " +
"or the end-user runtime and repack.")
}
}
# --- OpenAL runtime -------------------------------------------------------- # --- OpenAL runtime --------------------------------------------------------
# The exe links OpenAL32.dll (32-bit). Prefer shipping the already-installed # The exe links OpenAL32.dll (32-bit). Prefer shipping the already-installed
# runtime beside the exe; oalinst.exe covers machines where that misses. # runtime beside the exe; oalinst.exe covers machines where that misses.