Yes, x87. In 2026.
Why a 2012 game's floating-point results change on a 64-bit build, and how my Sonic the Hedgehog 4: Episode II reconstruction keeps them identical. October 2026.
Context
In my spare time I'm reconstructing Sonic the Hedgehog 4: Episode II, the 2012 PC release, as C++17. It isn't a byte-matching decompilation: the goal is code that behaves like the original. To check that, selected routines from the original executable run in a CPU emulator with constructed inputs, and their results are compared with my C++ versions on 32-bit and 64-bit builds. Getting those comparisons to match exactly is where the x87 came in.
Same formula, different floats
The original is a 32-bit Windows game, and compilers of that era turned float arithmetic
into x87 instructions. On the x87, values live in 80-bit registers; each result is rounded
to whatever precision the FPU's control word currently selects, and a value only becomes a
real 32-bit float when it is written back to memory. A 64-bit build uses SSE
instead, where every float operation rounds to IEEE single precision straight away.
Most of the time the two agree. Sometimes they differ by one unit in the last place. In a game, that is enough to matter: positions and angles feed into the next frame, and when a comparison against a threshold lands on the other side, the difference is no longer a digit but behaviour.
The precision isn't even fixed
On the x87, precision is state, not part of the instruction. The control word's precision
field selects a 24-bit, 53-bit or 64-bit significand: 0x007F gives 24-bit,
0x027F (the usual default) gives 53-bit. And a Direct3D 9 game doesn't fully own
that state. Unless the device is created with D3DCREATE_FPU_PRESERVE, Direct3D
"defaults to single-precision round-to-nearest mode", in the words of
Microsoft's
documentation. So the same routine in the original can run at 24-bit or 53-bit precision,
depending on what touched the FPU before it.
Precision as a parameter
So the reconstruction doesn't guess. Every routine whose result depends on it takes the
precision as an explicit parameter, Single or Double, and models
what the x87 does in that mode in plain C++, so it behaves the same on a 32-bit or a 64-bit
build. The core of it is small:
struct Arithmetic {
ObjectMovementPrecision precision;
double rounded(double value) const {
if (precision == ObjectMovementPrecision::Double || !std::isfinite(value)) {
return value;
}
int exponent = 0;
const float mantissa = static_cast<float>(std::frexp(value, &exponent));
return std::ldexp(static_cast<double>(mantissa), exponent);
}
double add(double left, double right) const {
return rounded(left + right);
}
double multiply(double left, double right) const {
return rounded(left * right);
}
float spill(double value) const {
return static_cast<float>(value);
}
};
In Double mode, values pass through untouched: double arithmetic is the 53-bit
case. In Single mode, each intermediate result has its significand rounded to
24 bits while keeping its exponent, which is what the x87 does at that setting, instead of
being squeezed into a 32-bit float after every step. spill marks the places
where the original writes a value to memory: the only point where it really becomes a
float.
Where the original calls into DirectX's own maths library, the 32-bit build sets the real x87 control word for the duration of the call, puts the SSE control register at its defaults, and restores both afterwards.
How it's checked
Each check pairs a constructed input with a precision setting, runs the original routine in the emulator, and compares its typed outputs with the C++ version on Win32 and on x64. The homing-attack target selector went through 364 case-and-precision pairs per architecture, and the homing and rebound helpers another 130, with zero recorded differences.
Those checks cover individual routines, not whole scenes, and their inputs stay private.
The public tests use synthetic inputs instead and need no game files: 52 native, 423 managed
and 51 Python tests. The code is public in the
Sonic4Episode2 repository;
the arithmetic above is from object_movement.cpp, the control-word handling
from camera_matrix_d3dx.cpp.
Lessons
Matching behaviour means matching the hardware state the original ran under, not just its code. A float result is a function of the inputs and of the FPU's mode.
Making that mode an explicit parameter instead of hidden global state is what keeps 32-bit and 64-bit builds deterministic, and testable: every check says which precision it ran at.
And yes, it's 2026. The x87 is still worth understanding, for as long as there is software from before 64-bit worth preserving.
Unofficial research project. Sonic the Hedgehog belongs to SEGA; the repository contains no game code or assets.