mxbmrp3

Headless cross-platform test build

Cross-compiles the plugin to a loadable Windows x64 DLL using mingw-w64, so a non-Windows host (CI, a Linux dev box, Wine) can exercise the real plugin data pipeline end-to-end - not just the isolated pure functions in ../unit.

This is not the shipping build. The shipping .dlo is built with MSVC (see the root CLAUDE.md). This is a parallel portability configuration used only for testing.

Where the tests live and how to run them: ../../TESTING.md. This file covers only the cross-build itself - the build engine and how it diverges from the shipping MSVC build.

What runs here

The cross-build powers the Layer-2 and Layer-3 tests (see ../../TESTING.md):

./run_tests.sh          # every doctest integration test in tests/ (pass a basename to run one)
./run_persist_test.sh   # settings round-trip property test
./run_fuzz.sh           # config-file survival fuzzing
./run_fuzz_callbacks.sh # DLL-boundary callback survival fuzzing
./run_perf.sh           # CPU performance baseline (50-rider grid)
./run_installer_test.sh # NSIS installer install/uninstall mechanics (makensis + Wine)
./run_tape_bench.sh     # per-HUD render footprint over a real tape (inspection, not gated)

This directory also holds the enforced invariant checks - the check_*.sh scripts (compile/grep passes, no Wine, CI fails on violations). What each one enforces is in its own header, alongside the escape-hatch annotation that opts a line out of it; DEVELOPMENT.md lists them in one place, one line each. Not repeated here: this listing was eleven of the nineteen by the time anyone noticed, which is what a second copy of a list is for.

Every *.cpp under core/, handlers/, hud/, diagnostics/ (minus discord_manager.cpp, which mxbmrp3/CMakeLists.txt drops under MXBMRP3_TEST_BUILD because GAME_HAS_DISCORD is 0 there and the TU would only drag the SDK in), plus mxb_api.cpp and the miniz .c files, compiles clean into a genuine PE32+ DLL exporting the full PiBoSo plugin API - ~185 translation units. build.sh prints the exported-symbol count on each link; a sudden drop means a TU quietly stopped being compiled. Under Wine it runs the real lifecycle: all managers initialize, settings load/save round-trips, HUDs rebuild render primitives, and the HTTP overlay server starts on :8080. Feature parity with the shipping build except Discord Rich Presence and Aptabase analytics (see below). All of it is wired into CI (.github/workflows/tests.yml).

Requirements

./tools/install_deps.sh mingw wine     # from the repo root

tools/install_deps.sh is the single source of truth for the toolchain (CI, the SessionStart hook and DEVELOPMENT.md all call it) and applies the two fixups that are easy to miss by hand: the posix mingw threading alternative (std::thread/std::mutex need it) and the /usr/bin/wine launcher. python3 is needed for the config-fuzz / persist runners; ccache is optional but installed with the mingw group.

Build

./build.sh            # incremental parallel build -> build/mxbmrp3_test.dlo
./build.sh clean      # remove the build tree and the DLL
./build.sh -B         # force full rebuild

build.sh is a thin wrapper over CMake, which replaced the Makefile that used to live here - that Makefile was a second, independent description of the same source tree sitting alongside the vcxproj’s explicit list, with nothing comparing the two. Both are gone: mxbmrp3/CMakeLists.txt is the single definition for every toolchain. The build tree is build/cross/, configured from the cross preset so the toolchain file and MXBMRP3_TEST_BUILD live only in CMakePresets.json. It doesn’t rebuild everything every time:

Approximate timings (4 cores):

Scenario Time
Clean build, cold cache ~120s
One .cpp changed ~1s
Clean rebuild, warm ccache ~1s

(A flat “recompile everything serially” script was ~370s every time.)

How it differs from the MSVC build

All divergences are gated in-source by MXBMRP3_TEST_BUILD or _MSC_VER, so the shipping MSVC build is byte-for-byte unchanged:

Area MSVC (shipping) This build Why
Discord Rich Presence on off std::thread::native_handle → HANDLE cast assumes win32 threads
Aptabase analytics on off external service; adds nothing under test
Steam friends on on SEH FFI wrappers made portable; runtime hook is inert without steam_api64.dll (as under Wine)
XInput controller name lookup WinRT no-op mingw ships no WinRT headers; real XInput state is unaffected
SEH crash guards (__try/__except) native SEH run unguarded SEH is MSVC-only; a compiler shim keeps the wrappers compiling
Xinput.h include as-is case shim Linux is case-sensitive; mingw ships lowercase xinput.h

Source changes that support this (all no-ops on MSVC):

Test-only exports

core/test_hooks.cpp adds MXBMRP3_Test_* exports (start the web server, reset settings, compare versions, force a save) used by the tests. The whole file is gated on MXBMRP3_TEST_BUILD, so these exports never exist in a shipping DLL. It’s compiled only under MXBMRP3_TEST_BUILD; mxbmrp3/CMakeLists.txt removes it from the source list for every shipping target.