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.
48 lines
2.6 KiB
C++
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
|