Float parity policy¶
This project targets high-fidelity replay and deterministic simulation parity. For gameplay code, float behavior is part of the contract.
Default rule¶
In deterministic gameplay paths, prefer native float32 fidelity over source readability:
- Keep decompiled float constants when they influence simulation outcomes.
- Keep native operation ordering when it changes rounding boundaries.
- Keep float32 store/truncation points where native stores to
float.
Do not auto-normalize literals like 0.6000000238418579 to 0.6 in parity
critical code unless parity evidence shows the change is behavior-neutral.
For an expression-level lookup table (with decompile anchors), see float expression precision map.
Why¶
Small float deltas can reorder branch decisions and collision outcomes, then amplify into RNG drift and deterministic divergence over long runs.
Concrete findings about original x87 usage¶
The original executable is x87-heavy in gameplay hot paths, but persistent gameplay state is still mostly float32.
Evidence in decompile artifacts¶
- CRT startup explicitly sets x87 precision-control to 53-bit (
PC_53): _startcallscrt_run_initializers: analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:83734, analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:83777.crt_run_initializersinvokesFUN_00460cb8viadata_47b160: analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:83626, analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:104544.FUN_00460cb8callssub_4636e7, which returnssub_469e81(0x10000, 0x30000): analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:81032, analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:81036, analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:84238.- In the CRT mapping helper,
arg1 & 0x30000 == 0x10000sets CW precision bits to0x200(53-bit mode): analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:91988, analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:91992, analysis/binary_ninja/raw/crimsonland.exe.bndb_hlil.txt:91993. - IDA function names align with this path:
__setdefaultprecision -> __controlfp:analysis/ida/raw/crimsonland.exe/functions.jsonlines around13692,13687. - Trig and atan paths are emitted as x87 transcendental ops with
float10temporaries: angle_approachcallsites in creature movement:analysis/ghidra/raw/crimsonland.exe_decompiled.c(0x0041f430, lines around21767).- Heading/direction math uses
fpatan+fcos/fsinwithfloat10casts: same file, lines around12248,12226,12230,21754,21771,21775. - Player movement/aim branches repeatedly compute
fcos(heading - 1.5707964)/fsin(heading - 1.5707964)viafloat10. - Those results are then spilled back to
floatstate fields at explicit assignment points, for example: (float)(fVar17 * ...)assignments intomove_dx/move_dyand velocity slots (same file, lines around12227-12233,21772-21778).- similar float spills in low-health effect direction math (
11961-11962). - Binary Ninja HLIL shows the same pattern as
fconvert.t(...)(widen) andfconvert.s(...)(spill to float32), confirming “extended intermediate, float32 storage” rather than all-float64 storage.
What this means (non-handwavy)¶
- The game is not “everything in 80-bit all the way down”.
- Startup default precision is
PC_53, so “x87 intermediate” is not equivalent to “always full 80-bit precision.” - Intermediates in many arithmetic/trig expressions are x87-extended.
- Authoritative long-lived state slots (player/creature/projectile fields) are float32 stores.
- Therefore parity errors come from two specific failure modes:
- wrong trig/atan evaluation behavior around branch boundaries,
- wrong placement of float32 spills (too early or too late).
Differential evidence this matters in practice¶
- Session notes repeatedly show divergence movement when arithmetic order or spill points differ:
docs/frida/differential-sessions/session-18.md: decompile-orderangle_approachfix moved first mismatch from7722to7756.docs/frida/differential-sessions/session-19.md: tighter float32 spill behavior in creature heading/tau-boundary handling cleared the remainingquest_1_8capture.
Implementation consequence¶
Treat native math as: - x87-like trig/atan intermediates where possible, - explicit float32 store boundaries in gameplay state, - no blanket “upgrade everything to f64” and no blanket “truncate every op”.
Rewrite math model (current)¶
Deterministic gameplay math follows three rules:
- Use
f32as the gameplay-domain type (positions, headings, timers, speeds, projectile scalar state) unless a value is truly boundary-only. - Widen only at boundaries (replay decode, serialization, diagnostics), then
immediately spill back to
f32at the native-equivalent store point. - Route parity-critical trig/angle helpers through shared native-style math helpers, not ad-hoc per-module implementations.
Zig runtime implementation¶
- Canonical helpers live in
crimson-zig/src/runtime/native_math.zig. - Native constants are sourced from exact
f32bit patterns (pi,half_pi,tau, turn-rate scale), not simplified decimal literals. roundF32(...)is the canonical spill helper for boundary/store truncation.sinNative/cosNative/atan2Nativebehavior:- use
sinl/cosl/atan2lwhenc_longdoubleis wider thanf64, - otherwise use
sin/cos/atan2, - freestanding builds fall back to
std.math. - Shared angle helpers (
wrapAngle0Tau,headingFromDeltaNative,headingAddPiNative) encode decompile/native corner-case behavior in one place. crimson-zig/src/runtime/math.zigdispatches by type:f32uses the native helper path,f64/comptime_floatremain available for non-domain/boundary use.
Allowed normalization¶
Literal simplification is acceptable when all of the following are true:
- The path is non-deterministic or presentation-only (not gameplay simulation).
- Differential evidence (capture + verifier) shows no behavior change.
- A test or session note records that evidence.
If any condition is missing, keep the native-looking float behavior.
Implementation guidance¶
- Prefer a single shared helper source over local math wrappers:
runtime/native_math.zig+runtime/math.zig. - Keep gameplay-domain state in
f32; avoid repeatedf64 -> f32 -> f64churn inside hot loops. - Use explicit spill points (
roundF32) where native would store tofloat. - Prefer parity captures and focused traces over intuitive “cleanup”.
- Document any intentional float deviation in the differential session docs:
docs/frida/differential-sessions.mdand the relevantdocs/frida/differential-sessions/session-*.md.