Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3229a82380 | ||
|
|
29ad567ffb | ||
|
|
9fae4406b5 | ||
|
|
b07e328553 | ||
|
|
879b76c6e1 |
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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, ~60–80 lines using only helpers Wine implements
|
||||||
|
(`D3DXCreateMesh`, `D3DXGetDeclVertexSize`, `D3DXVec3TransformCoordArray`,
|
||||||
|
`D3DXMatrixInverse`, …). Worth doing eventually because `D3DXSimplifyMesh`
|
||||||
|
and `D3DXSplitMesh` are stubs too, so any future mesh work hits the same
|
||||||
|
wall. Verify by comparing vertex/face counts and attribute ranges against
|
||||||
|
the D3DX output on Windows.
|
||||||
|
|
||||||
## 1. How the Windows dependency actually distributes
|
## 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 2–5
|
||||||
|
> 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
|
||||||
|
|||||||
@@ -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 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 `<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 16–220 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 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.
|
||||||
@@ -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)">0–200, default 0 (the pod's own geometry)</span></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user