Skip to content

Float expression precision map (decompile-derived)

Purpose: fast lookup for Python + Zig parity work. This map classifies common expression families by the precision model they need in deterministic gameplay paths.

Use this with float parity policy.

Precision classes

  • F32_STORE: expression result should be stored/kept as float32 state.
  • X87_INTERMEDIATE_THEN_F32: compute with x87-like widened intermediate, then spill to float32 at the native-equivalent store point.
  • F64_BOUNDARY_ONLY: f64/double is acceptable at formatting/IO boundaries, but should not drive gameplay state evolution.

Runtime assumption for this map: - Gameplay follows the PC24 policy: Direct3D device creation uses flags 0x20, without D3DCREATE_FPU_PRESERVE (0x02). The earlier CRT startup PC_53 setting therefore does not describe gameplay. - “X87 intermediate” does not mean that ordinary arithmetic stays wide. Add, subtract, multiply, divide, and square root follow PC24 rounding at each operation. Preserve extended transcendental results until the following arithmetic operation or store, as described in the policy. - The frame witness matrix in tools/match/evidence/creature-frame-selection-2026-09-11/results.json demonstrates 126 frame-index differences between PC24 and PC64 on the same stored inputs. Port regressions use the PC24 observations.

Decompile-wide signal counts (for confidence)

Historical Ghidra whole-view scan:

  • float10 occurrences: 1496
  • (float10)fcos/(float10)fsin/(float10)fpatan: 270
  • explicit (float)(...) casts: 996
  • explicit (double) casts: 31 (mostly CRT/vararg boundaries)

Historical IDA whole-view scan:

  • fcos/fsin/fpatan/fsqrt: 62/60/15/36
  • fld/fstp/fmul/fadd/fsub/fcomp/fnstsw: 1094/897/704/363/259/296/296

Expression lookup table

ID Expression family Class Native pattern (decompile) Required model Example anchor
E01 Heading from delta (atan2 path) X87_INTERMEDIATE_THEN_F32 (float10)fpatan((float10)dy,(float10)dx) then (float) store Widen for atan2, spill to f32 immediately when writing heading player_update @ 0x004136b0
E02 Direction from heading (cos) X87_INTERMEDIATE_THEN_F32 (float10)fcos((float10)heading - (float10)1.5707964) Widen for trig op, then f32 spill into velocity/move components player_update @ 0x004136b0
E03 Direction from heading (sin) X87_INTERMEDIATE_THEN_F32 (float10)fsin((float10)heading - (float10)1.5707964) Same as E02 player_update @ 0x004136b0
E04 Creature steering trig+spill X87_INTERMEDIATE_THEN_F32 fpatan target heading; fcos/fsin heading-to-vel; (float) assignments Keep transcendental intermediates widened; persist heading/vel_* as f32 creature_update_all @ 0x00426220
E05 Projectile angle step trig X87_INTERMEDIATE_THEN_F32 (float10)angle - half_pi; fcos/fsin in per-substep motion Widen trig/intermediate multiply chain; spill to f32 locals/state at native stores projectile_update @ 0x00420b90
E06 Length / distance compare X87_INTERMEDIATE_THEN_F32 (branch-sensitive) SQRT(dx*dx + dy*dy) < threshold Preserve op order and compare boundary behavior; treat as branch-sensitive creature_update_all @ 0x00426220
E07 Vec2 length helper return F32_STORE return SQRT(v[1]*v[1] + *v * *v); API return is f32; avoid carrying widened value past the return boundary vec2_length @ 0x00417660
E08 Normalize via reciprocal sqrt X87_INTERMEDIATE_THEN_F32 (1.0 / SQRT(a)) * ... assigned into vector components Keep rsqrt/sqrt intermediate precision; write normalized components as f32 d3dx_c_vec2_normalize @ 0x00455587
E09 Angle wrapping/approach arithmetic F32_STORE add/sub/abs/clamp around tau; no transcendental Keep exact operation order and f32 store cadence; no need for boundary f64 promotion angle_approach @ 0x0041f430
E10 Timers/cooldowns/speeds state updates F32_STORE state = state +/- frame_dt * k, clamp to bounds Keep state as f32; avoid accidental long-lived f64 accumulators in gameplay paths player_update @ 0x004136b0
E11 Float-int conversion hotspots (__ftol family) X87_INTERMEDIATE_THEN_F32 (int boundary) explicit __ftol() calls in movement/effects code Route through native-compatible helper; treat conversion semantics as parity-sensitive projectile_update @ 0x00420b90
E12 Formatting/vararg conversion F64_BOUNDARY_ONLY crt_sprintf(..., (double)f32_value) double here is boundary formatting ABI, not simulation precision policy mods_menu_update @ 0x0040e9a0
E13 Creature atlas frame selection F32_STORE (int boundary) __ftol(phase + 0.5f) or __ftol((float)(base + 15) - lifecycle) Round the add/subtract at PC24 before truncating; select the lifecycle branch directly creature_render_type @ 0x00418b60
E14 Heading-derived 60-unit aim point X87_INTERMEDIATE_THEN_F32 Native keeps cosine wide through scaling but stores sine first Subtract native half-pi at PC24; multiply wide cosine and stored sine at PC24; round each position add player_update @ 0x004136b0
E15 Conventional projectile trail corners F32_STORE at PC24 (camera + origin/position) +/- velocity * width_factor Round input fields, width product, camera sums, and corner additions/subtractions before viewport scaling projectile_render @ 0x004230e5..0x00423663
E16 Sharpshooter laser corners X87_INTERMEDIATE_THEN_F32 Offset near heading by 0.150915f, scale trig products by 15/512/1.1, then form camera-relative corners Preserve wide trig through multiplication except the stored far-end sine; round PC24 arithmetic and complete corners before per-axis viewport scaling projectile_render laser geometry starting at 0x00422d63

E14 is an exception to treating both direction components as stored floats. The original-image witnesses in tools/match/evidence/player-aim-direction-2026-09-11/results.json pin the asymmetry and held-turn ordering. Python and Zig share 1,050 point and 240 turn witnesses; see that evidence package for modeled boundaries and limits.

E15 is checked against the original executable's submitted corner words in tools/match/evidence/conventional-corner-rounding-2026-09-11/. Python's bullet_trail_corners preserves the game's PC24 arithmetic before applying the rewrite's viewport scale. The evidence package separately documents the native wider-register/store asymmetries under diagnostic PC64; those are not a reason to use binary64 arithmetic for the Python gameplay renderer.

E16 uses the native 15-unit angled near endpoint and 2.2-unit total width. The witnesses in tools/match/evidence/laser-trig-rounding-2026-09-11/ check Python's actual submitted corners against native PC24 values at four viewport scales and in both perk-ownership modes. Native Grim2D color-setter execution also pins red near vertices, black far vertices, and truncating alpha-byte conversion. These are CPU submission checks, not GPU pixel proof.

Binary Ninja cross-check pattern

Binary Ninja HLIL repeatedly shows the same model:

  • fconvert.t(...) before transcendental/extended arithmetic
  • fconvert.s(...) at storage boundaries

Example: gameplay_update_and_render at 0x00409e1d widens the projectile heading through fpatan before the float32 call boundary.

Practical implementation guidance

  • Python gameplay domain:
  • Keep long-lived runtime state in np.float32/explicit f32 wrappers.
  • For E01..E06/E08/E11, run native-style helper path then spill to f32 at the native-equivalent assignment point.
  • Do not keep replay-decoded f64 values live in gameplay state.
  • Zig gameplay domain:
  • Keep state fields as f32.
  • Use shared native math helpers for trig/atan/angle routines; call roundF32-style spill helpers at explicit store points.

Fast lookup recipes

  • Resolve the function and current tool snapshots:
  • just analysis-function player_update
  • Search live Binary Ninja HLIL:
  • bn search text 'fconvert\\.(t|s)|fpatan|fcos|fsin' --view hlil --target crimsonland.exe.bndb
  • Save a reusable function artifact:
  • bn bundle function player_update --target crimsonland.exe.bndb --out /tmp/player_update.json