Skip to content

feat(math): Route game logic math through WWMath with 3-mode deterministic support#2670

Open
Okladnoj wants to merge 9 commits intoTheSuperHackers:mainfrom
Okladnoj:okji/feat/deterministic-math-v2
Open

feat(math): Route game logic math through WWMath with 3-mode deterministic support#2670
Okladnoj wants to merge 9 commits intoTheSuperHackers:mainfrom
Okladnoj:okji/feat/deterministic-math-v2

Conversation

@Okladnoj
Copy link
Copy Markdown

@Okladnoj Okladnoj commented May 1, 2026

Rework of #2602, incorporating review feedback:

  • GameMath via FetchContent (per @stephanmeesters, @OmniBlade recommendation)
  • Trig.cpp preserved, redirected to WWMath instead of deleted (per @xezon request for standalone change)
  • 3 math modes: VC6 (x87 inline asm), CRT (standard library), GameMath deterministic (per @Mauller recommendation)
  • USE_DETERMINISTIC_MATH unconditional for non-VC6 — missing gmath.h is now a compile error instead of silent fallback to x87/CRT
  • Clean history from main

CI: win32 + vc6 ✅, replay checks ✅

Open question: Replay checks pass both with and without USE_DETERMINISTIC_MATH, even though golden replays were recorded with an x87 build. The replays may not contain MSG_LOGIC_CRC messages, meaning the check only validates absence of crashes rather than game state CRC parity. If anyone has insight on this — please share.

Testing results

Cross-platform deterministic math parity verified with SimulationMathCrc::runBenchmark — computes CRC over 10 000 iterations of sin/cos/tan/atan2/sqrt/pow across a fixed input set.

System Compiler Math Library CRC Perf (10 000 iters)
Win32 x86 MSVC (modern) fdlibm (deterministic) 🟩 76B53840 ~6 ms
macOS ARM64 Apple Clang fdlibm (deterministic) 🟩 76B53840 ~11 ms
Win32 x86 MSVC (modern) system math (native) 🟦 E8B6385A ~3 ms
macOS ARM64 Apple Clang system math (native) 🟦 E8B6385A ~5 ms
Win32 x86 VC6 (legacy) x87 CRT (no fdlibm) 🟧 B7B83850 ~17 ms
Win32 x86 VC6 (legacy) system math (native) 🟥 8BB5B841 ~5 ms
  • 🟩 cross-platform deterministic parity achieved (Win32 modern = macOS ARM64)
  • 🟦 native system math match (Win32 modern = macOS ARM64)
  • 🟧 VC6 deterministic (x87 CRT, separate group — fdlibm not supported)
  • 🟥 VC6 native (x87 CRT, separate group)

Key fix: -ffp-contract=off in cmake/compilers.cmake — prevents Clang from emitting FMA instructions (fmadd) that skip intermediate rounding, breaking bit-exact parity with MSVC's /fp:precise default.

image

Integrate GameMath library (fdlibm) for bit-perfect cross-platform
floating-point reproducibility. When USE_DETERMINISTIC_MATH is active,
all WWMath functions (Sin, Cos, Sqrt, Inv_Sqrt, Float_To_Long, Acos,
Asin, Atan, Atan2) use fdlibm instead of x87 asm or system CRT.

- Add GameMath via FetchContent with global include paths
- Replace Trig.cpp with inline WWMath::*Trig wrappers
- Add WWMath::*Origin wrappers for bare CRT calls in game logic
- Prioritize USE_DETERMINISTIC_MATH over _MSC_VER/_M_IX86 guards
- Verified: win32 replay CRC matches original x87 output
Add global Sqrt(double) to trig.h/Trig.cpp, routing through
WWMath::SqrtOrigin(). Replace 5 bare CRT sqrt() calls in BaseType.h
(Coord2D::length, Coord2D::toAngle, ICoord2D::length,
Coord3D::length, ICoord3D::length) with the new Sqrt() gateway.

This closes the last known CRT math leak in CRC-critical code paths.
Same pattern as existing Sin/Cos/ACos routing via Trig.cpp.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR routes all game-logic floating-point math through a new WWMath dispatch layer that supports three modes: VC6 x87 inline assembly (legacy), CRT fallback (non-deterministic), and GameMath fdlibm (cross-platform deterministic). The GameMath library is pulled via FetchContent, and -ffp-contract=off is added for Clang to prevent FMA contraction that would break bit-exact parity with MSVC.

  • Core dispatch layer (wwmath.h): ~80 new static WWINLINE wrappers under #if USE_DETERMINISTIC_MATH / #else guards; USE_DETERMINISTIC_MATH is set via __has_include("gmath.h") in the header rather than CMake.
  • Breadth of call-site migration: ~80 files across Generals/ and GeneralsMD/ replace bare CRT calls (sqrtf, fabs, atan2, etc.) with WWMath::*_Origin counterparts, and Trig.cpp is preserved but redirected to the new deterministic wrappers.
  • Diagnostic tooling (SimulationMathCrc): renamed to appendSimulationMathCrc_Deterministic, a new _Native baseline function added, and a runBenchmark helper exposed for performance/CRC comparison.

Confidence Score: 5/5

The change is safe to merge; all findings are non-blocking style and benchmark-clarity issues that do not affect runtime correctness.

The core dispatch logic in wwmath.h is correct, the __has_include guard reliably activates USE_DETERMINISTIC_MATH only when gmath.h is on the include path, Trig.cpp redirection is clean, and CI passes on both win32 and VC6. The _Native benchmark precision change and the global include_directories in gamemath.cmake are worth addressing but do not affect game correctness or build reproducibility for the currently defined targets.

cmake/gamemath.cmake (global include_directories side-effects) and SimulationMathCrc.cpp (_Native baseline precision) are worth a second look, but neither blocks the PR.

Important Files Changed

Filename Overview
Core/Libraries/Source/WWVegas/WWMath/wwmath.h Core dispatch layer: adds ~80 deterministic wrappers via USE_DETERMINISTIC_MATH guards; double overloads narrow to float before gm_* call (acknowledged in comments); __has_include detection is correct for non-VC6 targets.
Core/GameEngine/Source/Common/Diagnostic/SimulationMathCrc.cpp Splits CRC into _Deterministic and _Native paths; adds runBenchmark; _Native uses double-precision CRT whereas the original code used float-precision, so the benchmark native CRC baseline differs from the pre-PR calculate() output; mixed tabs/spaces in runBenchmark body.
Core/GameEngine/Include/Common/Diagnostic/SimulationMathCrc.h Adds runBenchmark declaration and defines RUN_MATH_BENCHMARK_REPLAY400_FLAG to 0; the macro is never referenced anywhere in the PR, leaving dead code in the public header.
cmake/gamemath.cmake Fetches GameMath at a pinned commit; uses global include_directories instead of target-scoped include; FORCE on GM_ENABLE_INTRINSICS cache variable silently overrides user settings; cache description mismatched to variable name polarity.
cmake/compilers.cmake Adds -ffp-contract=off for Clang only; correctly scoped inside the non-MSVC branch; well-commented rationale for FMA prevention.
Core/Libraries/Include/Lib/BaseType.h Replaces bare sqrt calls in Coord2D/Coord3D/ICoord2D/ICoord3D::length() with Sqrt() from trig.h; float call sites are lossless, but ICoord2D/ICoord3D integer coordinates can exceed float-mantissa range before narrowing (acknowledged by prior review thread).
Core/Libraries/Source/WWVegas/WWMath/CMakeLists.txt Links core_wwmath against gamemath as a PUBLIC dependency for non-VC6 builds; correctly guarded with IS_VS6_BUILD check.
Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp Replaces fabs/sqrt with WWMath::FAbs_Origin/Sqrt_Origin throughout pathfinding; all call sites use float-width values so narrowing in deterministic mode is lossless.
Generals/Code/GameEngine/Source/Common/System/Trig.cpp Redirects Sin/Cos/Tan/ACos/ASin to WWMath::*_Trig wrappers and adds Sqrt(double) forwarding to WWMath::Sqrt_Origin; game title header previously flagged is now fixed.
GeneralsMD/Code/GameEngine/Source/Common/System/Trig.cpp Identical changes to Generals/Trig.cpp; correctly retains Command & Conquer Generals Zero Hour game title per project convention.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    CS[Call site e.g. sqrtf, atan2, sinf] --> WW[WWMath wrapper]
    WW --> Q{USE_DETERMINISTIC_MATH?}
    Q -->|YES - non-VC6 + gmath.h found| GM[GameMath fdlibm\ngm_sqrtf / gm_sinf ...]
    Q -->|NO - VC6 or no gmath.h| CQ{MSVC x86?}
    CQ -->|YES| X87[x87 inline asm\nfistp / fcos / fsin]
    CQ -->|NO| CRT[CRT functions\nsqrtf / sinf / atan2f]
    GM --> DET[Deterministic CRC\nbit-exact Win32 = macOS ARM64]
    X87 --> VC6[VC6 group CRC]
    CRT --> NAT[Native CRC]
    DET --> CALC[SimulationMathCrc::calculate]
    NAT --> BENCH[SimulationMathCrc::runBenchmark]
    VC6 --> BENCH
Loading
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
Core/GameEngine/Include/Common/Diagnostic/SimulationMathCrc.h:21-24
`RUN_MATH_BENCHMARK_REPLAY400_FLAG` is defined to `0` and never referenced anywhere in the PR. A permanently-zero macro in a public header is dead code — if it's a future placeholder it should at least be a comment, or be added in the PR that actually uses it.

```suggestion
class SimulationMathCrc
```

### Issue 2 of 5
Core/GameEngine/Source/Common/Diagnostic/SimulationMathCrc.cpp:75-90
**`_Native` baseline uses different precision than the original code**

The original `appendSimulationMathCrc` called `WWMath::Sin(0.7f)`, `log10f(2.3f)`, `powf(...)` — all float-precision. The new `appendSimulationMathCrc_Native` calls `::sin(0.7)`, `::log10(2.3)`, `::pow(...)` — double-precision CRT with a `(float)` cast at the end. Because double-precision intermediate results are rounded before the final `(float)` cast, the `crcNat` produced here will differ from whatever CRC the game produced before this PR. If the intent is to provide a "what did the old code produce" baseline, float-precision calls (`sinf`/`log10f`) should be used instead.

### Issue 3 of 5
Core/GameEngine/Source/Common/Diagnostic/SimulationMathCrc.cpp:111-150
**Mixed tabs and spaces in `runBenchmark`**

The outer loop and local variable declarations use 4-space indentation, but the `if (i == 0)` blocks inside both loops are indented with a tab character. This creates invisible inconsistency that editors with different tab-stop settings will render differently. The existing surrounding code uses spaces exclusively.

### Issue 4 of 5
cmake/gamemath.cmake:16
**Global `include_directories` pollutes all targets**

`include_directories(${gamemath_SOURCE_DIR}/include)` adds the GameMath include path to every target in the project, not just those that link `core_wwmath`. Any translation unit in an unrelated target can now silently `#include "gmath.h"`, and if the `__has_include` guard in `wwmath.h` resolves positively it will activate `USE_DETERMINISTIC_MATH` in contexts that are not linked against `gamemath`, turning `gm_*` calls into unresolved symbols at link time. `target_include_directories(gamemath INTERFACE ...)` — or relying on the transitive `PUBLIC` link from `core_wwmath` — would scope the include path correctly without global side-effects.

### Issue 5 of 5
cmake/gamemath.cmake:3
The `CACHE BOOL` description says "Disable intrinsics…" while the variable is named `GM_ENABLE_INTRINSICS` (enable). A developer reading `cmake-gui` or `ccmake` will see a confusing inversion. The description should match the variable's polarity.

```suggestion
set(GM_ENABLE_INTRINSICS OFF CACHE BOOL "Enable GameMath intrinsics (OFF required for cross-arch deterministic math)" FORCE)
```

Reviews (8): Last reviewed commit: "style(math): Fix indentation of if state..." | Re-trigger Greptile

Comment thread Generals/Code/GameEngine/Source/Common/System/Trig.cpp Outdated
Comment thread Core/GameEngine/Source/Common/Diagnostic/SimulationMathCrc.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWMath/wwmath.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWMath/wwmath.h Outdated
@Okladnoj
Copy link
Copy Markdown
Author

Okladnoj commented May 1, 2026

image Here is what replay playback looks like at the moment.

I’m testing this on a separate branch:
https://github.com/Okladnoj/GeneralsGameCode/okji/test/deterministic-math-v2

I slightly adjusted the CI there so I can run Win32 and get access to the game resources.

@Okladnoj Okladnoj force-pushed the okji/feat/deterministic-math-v2 branch from 854cc7b to 779f714 Compare May 1, 2026 00:49
@Skyaero42
Copy link
Copy Markdown

You did not review the changes you made with AI. It has issues that you should fix before asking it to be reviewed.

…erage

- Restore original Trig.cpp code (author comments, defines, REGENERATE_TRIG_TABLES)
- Keep only functional changes: sinf->WWMath::SinTrig redirects + Sqrt(double)
- Restore original tab alignment in wwmath.h #define block
- Route remaining CRT calls in SimulationMathCrc through WWMath (sinh/cosh/tanh/logf)
- Add cmake comments explaining FORCE usage
Copy link
Copy Markdown
Author

@Okladnoj Okladnoj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviwed all changes

@xezon
Copy link
Copy Markdown

xezon commented May 2, 2026

This change does too many things. It is better to first consolidate trig and wwmath and maybe other sources of math, before going into gamemath territory.

- Add RUN_MATH_BENCHMARK_REPLAY400_FLAG to SimulationMathCrc.h
- Implement 10000-iteration dual-path benchmark in SimulationMathCrc.cpp
- Inject benchmark trigger into GameLogic::update at replay frame 400
- Benchmark measures CRT vs WWMath precision and performance
@Okladnoj Okladnoj force-pushed the okji/feat/deterministic-math-v2 branch from 4b5675d to ddea128 Compare May 3, 2026 15:09
Okladnoj added 2 commits May 3, 2026 18:09
- Declare loop iterator outside of for loop in runBenchmark to support legacy MSVC scoping rules
@Okladnoj
Copy link
Copy Markdown
Author

Okladnoj commented May 3, 2026

This change does too many things. It is better to first consolidate trig and wwmath and maybe other sources of math, before going into gamemath territory.

@xezon Hey! I understand your point, but the reason I didn't fully consolidate trig and wwmath in this PR is exactly to avoid doing too many things at once.

As we saw in PR #2602, fully removing trig.h and replacing it with WWMath across the codebase touches over 120 files. Mixing a massive 120+ file architectural refactoring with a core feature addition (GameMath) made the previous PR extremely difficult to review and broke compilation for some standalone utilities, because trig.h is used outside of just game math.

That's exactly why I chose this "routing" approach for this PR. By keeping the trig.h interface intact and just routing its internal implementation to WWMath, we achieve the deterministic math goals with a much smaller and safer footprint.

Perhaps the best option would be to test this PR first, and if everything is fine — merge it. And only after that, we can focus on a second PR dedicated purely to the architectural cleanup (removing trig.h across all 120+ files)?

Comment thread Core/Libraries/Source/WWVegas/WWMath/wwmath.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWMath/wwmath.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWMath/wwmath.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWMath/wwmath.h
Comment thread Core/Libraries/Source/WWVegas/WWMath/wwmath.h
Comment thread Core/Libraries/Include/Lib/BaseType.h
Comment thread cmake/gamemath.cmake Outdated
Comment thread Core/GameEngine/Source/Common/Diagnostic/SimulationMathCrc.cpp Outdated
Comment thread Core/GameEngine/Source/Common/Diagnostic/SimulationMathCrc.cpp Outdated
Comment thread Core/GameEngine/Source/Common/Diagnostic/SimulationMathCrc.cpp Outdated
Okladnoj added a commit to Okladnoj/GeneralsGameCode that referenced this pull request May 5, 2026


- Merge gmath.h include + USE_DETERMINISTIC_MATH into single __has_include block
- Replace all #ifdef/#if defined() with #if USE_DETERMINISTIC_MATH
- Remove TheSuperHackers @fix prefix from cmake comment
- Expand ODR abbreviation in gamemath.cmake comment
- Add blank lines after setFPMode() in benchmark
- Fix iters abbreviation in printf
- Simplify benchmark: remove replay dependency, auto-trigger at frame 400


- Merge gmath.h include + USE_DETERMINISTIC_MATH into single __has_include block
- Replace all #ifdef/#if defined() with #if USE_DETERMINISTIC_MATH
- Remove TheSuperHackers @fix prefix from cmake comment
- Expand ODR abbreviation in gamemath.cmake comment
- Add blank lines after setFPMode() in benchmark
- Fix iters abbreviation in printf
- Simplify benchmark: remove replay dependency, auto-trigger at frame 400
- Rename WWMath wrappers to Function_Name convention (578 replacements, 79 files)
fbraz3 added a commit to fbraz3/GeneralsX that referenced this pull request May 7, 2026
* feat(deterministic-math): scaffold phase 4 routing

Port the first deterministic math batch derived from TheSuperHackers PR TheSuperHackers#2670 with incremental gating and attribution compliance.

- add non-MSVC anti-FMA compile flag (-ffp-contract=off)

- route trig and sqrt gateways through WWMath wrappers

- add gamemath.cmake integration scaffold with deterministic flag

- update project rule for upstream PR attribution comments

- update lessons learned and May dev diary

* fix(headless): stabilize replay simulation on macOS

- Override ParticleSystemManagerDummy::update() as no-op to prevent
  headless replay from executing the full particle update path, which
  caused EXC_BAD_ACCESS crash at ParticleSystemManager::update()+560

- Route SDL3GameEngine::createRadar() and createParticleSystemManager()
  to their Dummy counterparts when dummy=true (headless mode), matching
  upstream Win32GameEngine factory behavior

- Guard ParticleSystemManager::update() loop against stale null entries
  with early continue before sys->update() dispatch

- Skip smudge rendering path in headless via m_headless guard in
  ParticleSystemManager::update()

- Add null-file guards in RecorderClass::readNextFrame(),
  appendNextCommand(), and updatePlayback() for both Generals and ZH
  to prevent null dereference when playback file is closed mid-loop

* fix(replay-headless): harden texture creation flow

Guard D3DX8 and DX8 wrapper texture allocation paths when device or caps are unavailable in headless replay windows. Fail texture load tasks safely instead of dereferencing null state.

Also harden missing texture fallback handling and record session notes in May diary and lessons.

* fix(replay-recording): handle mixed path separators correctly when serializing map name

The loop condition checking for path separators was incomplete on Linux/macOS paths:
- realMapPathToPortableMapPath() converts platform paths to portable format
- Portable paths may contain forward slashes (Linux/macOS standard)
- Loop condition find(backslash) never matched forward-slash-only paths
- This left newMapName EMPTY when writing replay header
- Result: replays stored with corrupted map name field

Fix: Check !isEmpty() AND (find(backslash) OR find(forward slash))
- Loop correctly terminates when last token (filename) is reached
- Works with both Windows (backslash) and Unix (forward slash) separators
- Applies to both GameInfoToAsciiString() and GameInfo::setMap()

Test results:
- macos_skirmish_1v1.rep: PASS
- macos_6p_custom_map_2.rep: PASS (CRC fallback resolves map)
- macos_1v1_custom_map_1.rep: CRC mismatch (expected, data incompatible)

* fix(replay-mapcache): normalize map cache path and replay map field

Fix cross-platform replay/map issues found on macOS:\n- write/read MapCache.ini using portable path join (no literal \ filename)\n- keep replay header path handling for absolute and directory-based -replay inputs\n- add explicit replay CRC mismatch diagnostics for headless runs\n- encode/decode replay map field to preserve special characters in map names\n\nValidation:\n- macOS z_generals build completed successfully\n- replay tests: official/custom map cases load natively; incompatible replay reports frame-0 CRC mismatch

* fix(particle-emitter): null-safe strdup in copy constructor

ParticleEmitterClass copy constructor called ::_strdup() on NameString
and UserString without null checks, causing SIGSEGV when either field
was null.

Crash observed at:
  ParticleEmitterClass::Clone() -> copy ctor -> ::_strdup(nullptr)
  -> strlen(nullptr) -> SIGSEGV (KERN_INVALID_ADDRESS at 0x0)

Triggered by W3DGhostObject::snapShot() during normal gameplay.

Fix: guard strdup calls with null check before dereferencing.
Applied to both GeneralsMD and Generals variants.

* docs(replay): add headless testing reference and tech debt notes

- HEADLESS_REPLAY_TESTING.md: commands, parameters, output interpretation,
  platform notes, debug tips (GDB/lldb) for macOS and Linux
- REPLAY_MAPCACHE_TECH_DEBT.md: tracked known issues for custom map CRC
  fallback and (resolved) MapCache.ini backslash filename bug
@Okladnoj
Copy link
Copy Markdown
Author

Okladnoj commented May 8, 2026

Hi @xezon! I have addressed all your review feedback points and updated the PR.

CI Status:
The CI is completely green. I ran the benchmarks on both Win32 and VC6 with the latest changes, and the CRC results perfectly match our previous deterministic baselines (76B53840 for deterministic, E8B6385A for native).

To save you from hunting through all the comment threads, here is a consolidated list of the answers and solutions to your review points:

  • Function_Name convention / Naming inconsistencies
    Fixed. Renamed all math wrappers to use the _Origin and _Trig convention. The _Trig suffix also cleanly resolves conflicts with legacy EA names (e.g., ACos_Trig vs Acos).
  • Move #define next to #include gmath
    Fixed.
  • Redundant VC6 guard
    Fixed — removed the outer #if !(defined(_MSC_VER)...) guard, kept only __has_include. VC6 doesn't support __has_include, so the block is naturally skipped.
  • "origin" terminology
    "Origin" means the original EA code called bare CRT functions (sqrt, acos, sinf...). The suffix explicitly marks which exact CRT function was used originally. These are not just type variants — they are different precision math paths.
  • Missing gm math variants / CeilfOrigin identical to Ceil
    Ceil(float) and Floor(float) are original EA code used only in rendering (visrasterizer.cpp). Determinism isn't needed there. However, CeilfOrigin(float) is a game logic wrapper that routes to gm_ceilf. Therefore, they are not identical.
  • C++ overloads instead of f suffix
    Overloads are dangerous here. GameMath only provides float functions (the double version always narrows). With overloads, the compiler silently picks the version by argument type and could inadvertently change the precision path. Explicit names protect against this.
  • No @fix prefix in CMake / What is ODR? / Line breaks / iters typo
    Fixed.
  • Benchmark in GameLogic::update()
    Moved the auto-benchmark out of the replay loop. It is now a simple compile-time flag. (Did not prepare an ImGui stub since ImGui does not exist in the project).
  • VS6 exclusion necessary in CMake?
    Yes, it is necessary. VC6 doesn't support <stdint.h> and long long required by GameMath. Removing the exclusion will break the build.
  • Sqrt(double) intentional in BaseType.h?
    Yes, intentional. Coord3D::length() is used in game logic and participates in CRC — it must be strictly deterministic.

Okladnoj added a commit to OKJID/GameClient that referenced this pull request May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants