sots-engine/src/shim/fpu_force.h
alex d462513f84 shim: force and verify the x87 control word around the turn gate
New fpu_force module (4 register-transparent asm stubs, same pattern as the M0
Initialize hook, so the [unverified] prototypes of the turn-gate functions are
never relied on) plus two shim.cfg keys:

  fpu.force=<cw>|off      fldcw at StrategyClient::EndTurn and
                          StrategyServer::BeginProcessTurn, and nowhere else
  fpu.sample_ticks=on|off per-frame sampler, logs only when the word CHANGES

Forcing is deliberately one write per turn: re-forcing inside the pipeline would
guarantee the value is present without proving it ever held, which is the exact
false negative this experiment has to avoid. Verification is kept separate --
StrategyServer::ProcessTurn is hooked sample-only, and its reading plus the
existing per-hook fpu_cw fields (38 samples per turn across phases 4, 6 and 8)
are what establish that the setting lasted the whole turn.

Six shim.cfg variants, identical apart from the fpu.force line, and docs.

Used to settle STATE_CHECKSUM.md 3.5: 53-bit and 64-bit x87 produce byte-
identical turn results, so an x64/SSE port has no double-rounding budget to
preserve; 24-bit and round-up each move exactly one thing. Findings and evidence
live in the notes repo (findings/subsystems/fpu-precision-sensitivity.md).

clean_room_check.sh OK; host ctest 32/32.
2026-09-08 05:09:16 -04:00

48 lines
2.6 KiB
C++

// x87 control-word forcing + timeline sampling (lane F: the precision-sensitivity experiment).
//
// The question this exists to answer: does any value the turn pipeline produces actually
// *depend* on x87 intermediate precision? Every save on the lab host was made with the game's
// own control word (0x127f = 53-bit significand, round-to-nearest). A future x64/SSE port has
// no 80-bit intermediates at all, so "bit-exact" is only a real constraint if changing the
// precision-control field changes the resulting state.
//
// Method: force the control word once, at the entry to the turn gate, then let the whole
// pipeline run under it and checksum the autosave. Forcing is deliberately a *single* write at
// the top of the turn -- re-forcing at every hook would guarantee the value is present without
// proving it ever held, which is exactly the false-negative this experiment must avoid.
//
// Verification is separate from forcing:
// * every force/sample site logs observed-before, requested, and read-back-after;
// * the sampler on DemoApp::OnTick logs the control word on every *change*, per thread, so
// shim.log carries a complete timeline of the value for the session rather than a claim;
// * the existing hooks (research, colony turn, fleet movement, compute budget) already emit
// `fpu_cw` per call, giving independent samples from inside phases 4, 6 and 8 of the turn.
//
// shim.cfg keys owned here:
// fpu.force = 0x027f | 0x127f | 0x137f | <hex> control word to force at the turn gate
// (absent or 0 = force nothing, sample only)
// fpu.sample_ticks = on | off per-tick change sampler (default on)
#pragma once
#include <cstdint>
#include <string>
namespace shim::fpu {
// Handles the `fpu.*` shim.cfg keys. Returns true when `key` belongs to this module; sets
// *err (and leaves the setting unchanged) when the key is ours but the value is unusable.
bool apply_config(const char* key, const char* value, std::string* err);
// Read the x87 control word of the calling thread.
std::uint16_t read_cw();
// One line for shim.log at shutdown: how many times the word was forced, whether any read-back
// disagreed, and whether the per-tick sampler ever saw it move away again.
std::string summary();
// Installs the force/sample hooks. `log` receives one preformatted line at a time.
// Safe to call with no `fpu.force` configured: the sampler still runs, which is what makes a
// stock 0x127f run comparable to a forced one.
void install(std::uintptr_t exeBase, void (*log)(const char*));
} // namespace shim::fpu