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.
This commit is contained in:
parent
cef889ecef
commit
d462513f84
11 changed files with 584 additions and 2 deletions
|
|
@ -83,7 +83,7 @@ if(WIN32)
|
||||||
shim_colony shim_movement)
|
shim_colony shim_movement)
|
||||||
target_compile_options(shim_hooks PRIVATE -Wall -Wextra -Werror)
|
target_compile_options(shim_hooks PRIVATE -Wall -Wextra -Werror)
|
||||||
|
|
||||||
add_library(binkw32 SHARED src/shim/main.cpp src/shim/binkw32.def)
|
add_library(binkw32 SHARED src/shim/main.cpp src/shim/fpu_force.cpp src/shim/binkw32.def)
|
||||||
target_link_libraries(binkw32 PRIVATE minhook sots_addresses shim_trace shim_hooks)
|
target_link_libraries(binkw32 PRIVATE minhook sots_addresses shim_trace shim_hooks)
|
||||||
target_compile_definitions(binkw32 PRIVATE
|
target_compile_definitions(binkw32 PRIVATE
|
||||||
SHIM_BUILD_ID="${SHIM_BUILD_ID}"
|
SHIM_BUILD_ID="${SHIM_BUILD_ID}"
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ hook.Shim::SelfTest::Fill=off
|
||||||
trace.path=C:\SOTS\shim.trace.jsonl # default: shim.trace.jsonl beside the DLL; overwritten per run
|
trace.path=C:\SOTS\shim.trace.jsonl # default: shim.trace.jsonl beside the DLL; overwritten per run
|
||||||
trace.inline_max=256 # regions <= this many bytes are logged as hex, else sha256 + 32-byte head
|
trace.inline_max=256 # regions <= this many bytes are logged as hex, else sha256 + 32-byte head
|
||||||
trace.flush=always # always (default) | lazy (flush only on divergence / err / exit)
|
trace.flush=always # always (default) | lazy (flush only on divergence / err / exit)
|
||||||
|
fpu.force=0x007f # force the x87 control word at the turn gate; off (default) = force nothing
|
||||||
|
fpu.sample_ticks=on # on (default): log the control word to shim.log whenever it CHANGES
|
||||||
```
|
```
|
||||||
|
|
||||||
Unusable values are logged to `shim.log` (`config: key=value rejected (...)`) and ignored.
|
Unusable values are logged to `shim.log` (`config: key=value rejected (...)`) and ignored.
|
||||||
|
|
@ -59,6 +61,31 @@ every template hook reports mode `off` (the game runs un-instrumented, never hal
|
||||||
The M0 `Mars::Application::Initialize` hook is an asm stub, not a template hook: it is installed
|
The M0 `Mars::Application::Initialize` hook is an asm stub, not a template hook: it is installed
|
||||||
whenever `hooks` is not `off`, and it only logs to `shim.log`. See "thiscall" below.
|
whenever `hooks` is not `off`, and it only logs to `shim.log`. See "thiscall" below.
|
||||||
|
|
||||||
|
### `fpu.*` — the x87 control word (`src/shim/fpu_force.cpp`)
|
||||||
|
|
||||||
|
Four more asm-stub hooks, installed alongside the M0 one. They exist to answer "does anything
|
||||||
|
the turn pipeline computes actually *depend* on x87 intermediate precision?", which decides
|
||||||
|
whether a future x64/SSE port has a double-rounding budget to preserve.
|
||||||
|
|
||||||
|
* `fpu.force=<16-bit control word>` writes that word with `fldcw` at `StrategyClient::EndTurn`
|
||||||
|
and `StrategyServer::BeginProcessTurn`, and **nowhere else** -- deliberately a single write per
|
||||||
|
turn, because re-forcing at every hook would guarantee the value is present without proving it
|
||||||
|
ever held. Each site logs observed-before, requested, and read-back-after.
|
||||||
|
* `StrategyServer::ProcessTurn` is hooked **sample-only**: its reading is the independent
|
||||||
|
evidence that the forced word survived into the simulation. The template hooks' own `fpu_cw`
|
||||||
|
fields give ~38 more samples per turn from inside phases 4, 6 and 8.
|
||||||
|
* `fpu.sample_ticks` hooks `DemoApp::OnTick` and logs only when the word *moves*, so `shim.log`
|
||||||
|
carries a timeline rather than a single claim.
|
||||||
|
|
||||||
|
Field layout: bits 0-5 exception masks, bits 8-9 precision control (`00`=24-bit, `10`=53-bit,
|
||||||
|
`11`=64-bit), bits 10-11 rounding control (`00`=nearest, `01`=down, `10`=up, `11`=truncate),
|
||||||
|
bit 12 infinity control (ignored since the 387, so `0x027f` and `0x127f` are the same
|
||||||
|
arithmetic). `Mars::Application::Run` re-arms `0x127f` via `_controlfp` **every frame**, which is
|
||||||
|
why the forcing has to happen inside the turn call chain: a word forced at `EndTurn` is gone by
|
||||||
|
the time `BeginProcessTurn` runs on a later frame.
|
||||||
|
|
||||||
|
Result of the sweep: `notes-repo:findings/subsystems/fpu-precision-sensitivity.md`.
|
||||||
|
|
||||||
## Log
|
## Log
|
||||||
|
|
||||||
`shim.trace.jsonl`: line 1 is the `meta` record (`build`, `exe_sha256` of the running exe,
|
`shim.trace.jsonl`: line 1 is the `meta` record (`build`, `exe_sha256` of the running exe,
|
||||||
|
|
|
||||||
312
src/shim/fpu_force.cpp
Normal file
312
src/shim/fpu_force.cpp
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
// See fpu_force.h. Register-transparent asm stubs, exactly like main.cpp's Initialize hook:
|
||||||
|
// the prototypes of the turn-gate functions are [unverified], so a C++ detour that *called*
|
||||||
|
// the original would bake in a calling convention. These stubs save everything, call out to C
|
||||||
|
// for the log / the fldcw, and tail-jump to the trampoline, leaving the original's own `ret`
|
||||||
|
// in charge of stack cleanup.
|
||||||
|
|
||||||
|
#include "shim/fpu_force.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstdarg>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "MinHook.h"
|
||||||
|
#include "generated/sots_addresses.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void (*g_line)(const char*) = nullptr;
|
||||||
|
|
||||||
|
void logf(const char* fmt, ...) {
|
||||||
|
if (!g_line) return;
|
||||||
|
char buf[512];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
std::vsnprintf(buf, sizeof buf, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
g_line(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- configuration ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool g_forceEnabled = false;
|
||||||
|
unsigned g_forceValue = 0;
|
||||||
|
bool g_sampleTicks = true;
|
||||||
|
|
||||||
|
// ---- state -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct ThreadCw {
|
||||||
|
unsigned long tid = 0;
|
||||||
|
std::uint16_t cw = 0;
|
||||||
|
unsigned long ticks = 0;
|
||||||
|
};
|
||||||
|
ThreadCw g_seen[8];
|
||||||
|
unsigned g_seenCount = 0;
|
||||||
|
|
||||||
|
unsigned g_forceEvents = 0; // times we executed fldcw
|
||||||
|
unsigned g_forceFailures = 0; // times the read-back did not match what we asked for
|
||||||
|
unsigned g_changeEvents = 0; // times the tick sampler saw the word move
|
||||||
|
unsigned g_revertEvents = 0; // ... to something other than the value we forced
|
||||||
|
std::uint16_t g_lastSampled = 0;
|
||||||
|
|
||||||
|
// ---- x87 primitives --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
std::uint16_t cw_read() {
|
||||||
|
#if defined(__i386__) || defined(__x86_64__)
|
||||||
|
unsigned short cw = 0;
|
||||||
|
__asm__ __volatile__("fnstcw %0" : "=m"(cw));
|
||||||
|
return cw;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void cw_write(unsigned v) {
|
||||||
|
#if defined(__i386__) || defined(__x86_64__)
|
||||||
|
unsigned short cw = static_cast<unsigned short>(v);
|
||||||
|
__asm__ __volatile__("fldcw %0" : : "m"(cw));
|
||||||
|
#else
|
||||||
|
(void)v;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human-readable decode, so shim.log states the mode rather than a bare number. The
|
||||||
|
// precision-control field is bits 8-9; the rounding-control field is bits 10-11.
|
||||||
|
const char* pc_name(unsigned cw) {
|
||||||
|
switch ((cw >> 8) & 3) {
|
||||||
|
case 0: return "24bit-single";
|
||||||
|
case 1: return "reserved";
|
||||||
|
case 2: return "53bit-double";
|
||||||
|
default: return "64bit-extended";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const char* rc_name(unsigned cw) {
|
||||||
|
switch ((cw >> 10) & 3) {
|
||||||
|
case 0: return "nearest";
|
||||||
|
case 1: return "down";
|
||||||
|
case 2: return "up";
|
||||||
|
default: return "truncate";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force at a named site and report all three values, so the log proves the write landed
|
||||||
|
// rather than asserting it.
|
||||||
|
void force_at(const char* site, void* self) {
|
||||||
|
const std::uint16_t before = cw_read();
|
||||||
|
if (!g_forceEnabled) {
|
||||||
|
logf("fpu: sample at %s (this=%p): cw=0x%04x %s/%s [no fpu.force configured]", site, self,
|
||||||
|
before, pc_name(before), rc_name(before));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cw_write(g_forceValue);
|
||||||
|
const std::uint16_t after = cw_read();
|
||||||
|
++g_forceEvents;
|
||||||
|
const bool ok = (after == static_cast<std::uint16_t>(g_forceValue));
|
||||||
|
if (!ok) ++g_forceFailures;
|
||||||
|
logf("fpu: FORCE at %s (this=%p): observed=0x%04x %s/%s -> requested=0x%04x -> readback=0x%04x "
|
||||||
|
"%s/%s %s",
|
||||||
|
site, self, before, pc_name(before), rc_name(before), g_forceValue, after, pc_name(after),
|
||||||
|
rc_name(after), ok ? "OK" : "*** READBACK MISMATCH ***");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample only. Its value at StrategyServer::ProcessTurn is the evidence that a control word
|
||||||
|
// forced at the turn gate survived the gap to the simulation itself, on the same thread.
|
||||||
|
void sample_at(const char* site, void* self) {
|
||||||
|
const std::uint16_t cw = cw_read();
|
||||||
|
logf("fpu: sample at %s (this=%p): cw=0x%04x %s/%s%s", site, self, cw, pc_name(cw), rc_name(cw),
|
||||||
|
(g_forceEnabled && cw != static_cast<std::uint16_t>(g_forceValue))
|
||||||
|
? " *** NOT THE FORCED VALUE ***"
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---- the stubs -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
extern "C" void* g_origFpuEndTurn;
|
||||||
|
extern "C" void* g_origFpuBeginProcessTurn;
|
||||||
|
extern "C" void* g_origFpuProcessTurn;
|
||||||
|
extern "C" void* g_origFpuOnTick;
|
||||||
|
void* g_origFpuEndTurn = nullptr;
|
||||||
|
void* g_origFpuBeginProcessTurn = nullptr;
|
||||||
|
void* g_origFpuProcessTurn = nullptr;
|
||||||
|
void* g_origFpuOnTick = nullptr;
|
||||||
|
|
||||||
|
extern "C" void FpuEndTurnDetour();
|
||||||
|
extern "C" void FpuBeginProcessTurnDetour();
|
||||||
|
extern "C" void FpuProcessTurnDetour();
|
||||||
|
extern "C" void FpuOnTickDetour();
|
||||||
|
|
||||||
|
extern "C" void ShimFpuOnEndTurn(void* self) { force_at("StrategyClient::EndTurn", self); }
|
||||||
|
extern "C" void ShimFpuOnBeginProcessTurn(void* self) {
|
||||||
|
force_at("StrategyServer::BeginProcessTurn", self);
|
||||||
|
}
|
||||||
|
extern "C" void ShimFpuOnProcessTurn(void* self) { sample_at("StrategyServer::ProcessTurn", self); }
|
||||||
|
|
||||||
|
// Per-frame sampler. Logs only when the word moves (or on the first tick seen on a thread), so
|
||||||
|
// shim.log ends up holding a complete timeline of the control word for the whole session
|
||||||
|
// instead of one claim made at one instant.
|
||||||
|
extern "C" void ShimFpuOnTick(void* self) {
|
||||||
|
if (!g_sampleTicks) return;
|
||||||
|
const std::uint16_t cw = cw_read();
|
||||||
|
g_lastSampled = cw;
|
||||||
|
const unsigned long tid = GetCurrentThreadId();
|
||||||
|
for (unsigned i = 0; i < g_seenCount; ++i) {
|
||||||
|
if (g_seen[i].tid != tid) continue;
|
||||||
|
++g_seen[i].ticks;
|
||||||
|
if (g_seen[i].cw == cw) return;
|
||||||
|
const std::uint16_t prev = g_seen[i].cw;
|
||||||
|
g_seen[i].cw = cw;
|
||||||
|
++g_changeEvents;
|
||||||
|
const bool away = g_forceEnabled && cw != static_cast<std::uint16_t>(g_forceValue);
|
||||||
|
if (away) ++g_revertEvents;
|
||||||
|
logf("fpu: TICK CHANGE at OnTick (this=%p) after %lu ticks: 0x%04x -> 0x%04x %s/%s%s", self,
|
||||||
|
g_seen[i].ticks, prev, cw, pc_name(cw), rc_name(cw),
|
||||||
|
away ? " *** REVERTED AWAY FROM THE FORCED VALUE ***" : "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (g_seenCount < (sizeof g_seen / sizeof g_seen[0])) {
|
||||||
|
g_seen[g_seenCount].tid = tid;
|
||||||
|
g_seen[g_seenCount].cw = cw;
|
||||||
|
g_seen[g_seenCount].ticks = 1;
|
||||||
|
++g_seenCount;
|
||||||
|
logf("fpu: TICK BASELINE at OnTick (this=%p): cw=0x%04x %s/%s", self, cw, pc_name(cw),
|
||||||
|
rc_name(cw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
asm(R"(
|
||||||
|
.text
|
||||||
|
.globl _FpuEndTurnDetour
|
||||||
|
_FpuEndTurnDetour:
|
||||||
|
pushfl
|
||||||
|
pushal
|
||||||
|
pushl %ecx
|
||||||
|
call _ShimFpuOnEndTurn
|
||||||
|
addl $4, %esp
|
||||||
|
popal
|
||||||
|
popfl
|
||||||
|
jmp *_g_origFpuEndTurn
|
||||||
|
|
||||||
|
.globl _FpuBeginProcessTurnDetour
|
||||||
|
_FpuBeginProcessTurnDetour:
|
||||||
|
pushfl
|
||||||
|
pushal
|
||||||
|
pushl %ecx
|
||||||
|
call _ShimFpuOnBeginProcessTurn
|
||||||
|
addl $4, %esp
|
||||||
|
popal
|
||||||
|
popfl
|
||||||
|
jmp *_g_origFpuBeginProcessTurn
|
||||||
|
|
||||||
|
.globl _FpuProcessTurnDetour
|
||||||
|
_FpuProcessTurnDetour:
|
||||||
|
pushfl
|
||||||
|
pushal
|
||||||
|
pushl %ecx
|
||||||
|
call _ShimFpuOnProcessTurn
|
||||||
|
addl $4, %esp
|
||||||
|
popal
|
||||||
|
popfl
|
||||||
|
jmp *_g_origFpuProcessTurn
|
||||||
|
|
||||||
|
.globl _FpuOnTickDetour
|
||||||
|
_FpuOnTickDetour:
|
||||||
|
pushfl
|
||||||
|
pushal
|
||||||
|
pushl %ecx
|
||||||
|
call _ShimFpuOnTick
|
||||||
|
addl $4, %esp
|
||||||
|
popal
|
||||||
|
popfl
|
||||||
|
jmp *_g_origFpuOnTick
|
||||||
|
)");
|
||||||
|
|
||||||
|
namespace shim::fpu {
|
||||||
|
|
||||||
|
std::uint16_t read_cw() { return cw_read(); }
|
||||||
|
|
||||||
|
bool apply_config(const char* key, const char* value, std::string* err) {
|
||||||
|
if (std::strcmp(key, "fpu.force") == 0) {
|
||||||
|
if (value[0] == '\0' || std::strcmp(value, "off") == 0 || std::strcmp(value, "none") == 0) {
|
||||||
|
g_forceEnabled = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long v = std::strtoul(value, &end, 0);
|
||||||
|
if (end == value || *end != '\0' || v > 0xffff) {
|
||||||
|
if (err) *err = "expected off or a 16-bit control word (e.g. 0x127f)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
g_forceValue = static_cast<unsigned>(v);
|
||||||
|
g_forceEnabled = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (std::strcmp(key, "fpu.sample_ticks") == 0) {
|
||||||
|
if (std::strcmp(value, "on") == 0) {
|
||||||
|
g_sampleTicks = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (std::strcmp(value, "off") == 0) {
|
||||||
|
g_sampleTicks = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (err) *err = "expected on or off";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string summary() {
|
||||||
|
char buf[320];
|
||||||
|
std::snprintf(buf, sizeof buf,
|
||||||
|
"fpu: SUMMARY force=%s value=0x%04x fldcw_events=%u readback_failures=%u "
|
||||||
|
"tick_changes=%u tick_reverts=%u last_sampled=0x%04x",
|
||||||
|
g_forceEnabled ? "on" : "off", g_forceValue, g_forceEvents, g_forceFailures,
|
||||||
|
g_changeEvents, g_revertEvents, g_lastSampled);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void install(std::uintptr_t exeBase, void (*log)(const char*)) {
|
||||||
|
g_line = log;
|
||||||
|
const std::uint16_t entry = cw_read();
|
||||||
|
logf("fpu: module init, entry cw=0x%04x %s/%s; force=%s value=0x%04x sample_ticks=%s", entry,
|
||||||
|
pc_name(entry), rc_name(entry), g_forceEnabled ? "on" : "off", g_forceValue,
|
||||||
|
g_sampleTicks ? "on" : "off");
|
||||||
|
|
||||||
|
struct Site {
|
||||||
|
const char* name;
|
||||||
|
std::uint32_t rva;
|
||||||
|
void* detour;
|
||||||
|
void** orig;
|
||||||
|
bool install;
|
||||||
|
};
|
||||||
|
const Site sites[] = {
|
||||||
|
{"StrategyClient::EndTurn", sots::addr::StrategyClient_EndTurn,
|
||||||
|
reinterpret_cast<void*>(&FpuEndTurnDetour), &g_origFpuEndTurn, true},
|
||||||
|
{"StrategyServer::BeginProcessTurn", sots::addr::StrategyServer_BeginProcessTurn,
|
||||||
|
reinterpret_cast<void*>(&FpuBeginProcessTurnDetour), &g_origFpuBeginProcessTurn, true},
|
||||||
|
{"StrategyServer::ProcessTurn", sots::addr::StrategyServer_ProcessTurn,
|
||||||
|
reinterpret_cast<void*>(&FpuProcessTurnDetour), &g_origFpuProcessTurn, true},
|
||||||
|
{"DemoApp::OnTick", sots::addr::DemoApp_OnTick, reinterpret_cast<void*>(&FpuOnTickDetour),
|
||||||
|
&g_origFpuOnTick, g_sampleTicks},
|
||||||
|
};
|
||||||
|
for (const Site& s : sites) {
|
||||||
|
if (!s.install) {
|
||||||
|
logf("fpu: %s rva=0x%08x not installed (sampler off)", s.name, s.rva);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
void* target = reinterpret_cast<void*>(exeBase + s.rva);
|
||||||
|
MH_STATUS st = MH_CreateHook(target, s.detour, s.orig);
|
||||||
|
logf("fpu: %s rva=0x%08x -> va=%p MH_CreateHook -> %s (trampoline=%p)", s.name, s.rva,
|
||||||
|
target, MH_StatusToString(st), *s.orig);
|
||||||
|
if (st != MH_OK) continue;
|
||||||
|
st = MH_EnableHook(target);
|
||||||
|
logf("fpu: %s MH_EnableHook -> %s", s.name, MH_StatusToString(st));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace shim::fpu
|
||||||
48
src/shim/fpu_force.h
Normal file
48
src/shim/fpu_force.h
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
// 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
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include "MinHook.h"
|
#include "MinHook.h"
|
||||||
#include "generated/sots_addresses.h"
|
#include "generated/sots_addresses.h"
|
||||||
|
#include "shim/fpu_force.h"
|
||||||
#include "shim/hooks/dictionaries.h"
|
#include "shim/hooks/dictionaries.h"
|
||||||
#include "shim/hooks/colony_turn.h"
|
#include "shim/hooks/colony_turn.h"
|
||||||
#include "shim/hooks/compute_budget.h"
|
#include "shim/hooks/compute_budget.h"
|
||||||
|
|
@ -79,7 +80,10 @@ Config ReadConfig() {
|
||||||
char* val = eq + 1;
|
char* val = eq + 1;
|
||||||
val[std::strcspn(val, "\r\n")] = '\0';
|
val[std::strcspn(val, "\r\n")] = '\0';
|
||||||
std::string err;
|
std::string err;
|
||||||
if (cfg.trace.apply(p, val, &err)) {
|
if (shim::fpu::apply_config(p, val, &err)) {
|
||||||
|
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
||||||
|
else Log("config: %s=%s", p, val);
|
||||||
|
} else if (cfg.trace.apply(p, val, &err)) {
|
||||||
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
|
||||||
else Log("config: %s=%s", p, val);
|
else Log("config: %s=%s", p, val);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -207,6 +211,10 @@ void InstallHooks(shim::trace::Tracer& tracer) {
|
||||||
shim::hooks::init_fleet_movement(exeBase, &ShimLogLine);
|
shim::hooks::init_fleet_movement(exeBase, &ShimLogLine);
|
||||||
InstallTemplateHook<shim::hooks::StrategyServerMoveFleetHook>(tracer, exeBase, sots::addr::StrategyServer_MoveFleet);
|
InstallTemplateHook<shim::hooks::StrategyServerMoveFleetHook>(tracer, exeBase, sots::addr::StrategyServer_MoveFleet);
|
||||||
InstallTemplateHook<shim::hooks::StrategyServerProcessFleetMovementHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessFleetMovement);
|
InstallTemplateHook<shim::hooks::StrategyServerProcessFleetMovementHook>(tracer, exeBase, sots::addr::StrategyServer_ProcessFleetMovement);
|
||||||
|
|
||||||
|
// Lane F: x87 control-word forcing at the turn gate + the per-tick change sampler.
|
||||||
|
// Installed last so it is nowhere near the template hooks it is meant to measure.
|
||||||
|
shim::fpu::install(exeBase, &ShimLogLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- lifecycle -----------------------------------------------------------------------------
|
// ---- lifecycle -----------------------------------------------------------------------------
|
||||||
|
|
@ -282,6 +290,7 @@ void Shim_Init(HMODULE self) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shim_Shutdown() {
|
void Shim_Shutdown() {
|
||||||
|
Log("%s", shim::fpu::summary().c_str());
|
||||||
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
|
shim::trace::Tracer& tracer = shim::trace::Tracer::instance();
|
||||||
if (tracer.is_open()) {
|
if (tracer.is_open()) {
|
||||||
Log("shutdown: trace records=%lu", static_cast<unsigned long>(tracer.records_written()));
|
Log("shutdown: trace records=%lu", static_cast<unsigned long>(tracer.records_written()));
|
||||||
|
|
|
||||||
31
src/shim/shim.cfg.fpu007f
Normal file
31
src/shim/shim.cfg.fpu007f
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Lane F, x87 precision-sensitivity experiment (verify/results/fpu-cw in the notes repo).
|
||||||
|
# 0x007f = 24-bit significand (SINGLE precision), round-to-nearest. The genuine single-precision setting the brief meant by 0x027f, and the run most likely to move a result: it is the strongest available positive control on the whole apparatus.
|
||||||
|
#
|
||||||
|
# Every shim.cfg.fpu* file is identical except the fpu.force line: the hook set, the trace path
|
||||||
|
# and the flush policy are held fixed so the control word is the only variable across runs.
|
||||||
|
#
|
||||||
|
# x87 control-word fields: bits 0-5 exception masks, bits 8-9 precision control
|
||||||
|
# (00 = 24-bit / 10 = 53-bit / 11 = 64-bit significand), bits 10-11 rounding control
|
||||||
|
# (00 nearest / 01 down / 10 up / 11 truncate), bit 12 infinity control (ignored since the 387).
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=trace
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=trace
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=trace
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::MoveFleet=trace
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.inline_max=256
|
||||||
|
trace.flush=always
|
||||||
|
|
||||||
|
# Forced once at the turn gate (StrategyClient::EndTurn and StrategyServer::BeginProcessTurn),
|
||||||
|
# never re-forced inside the pipeline -- re-forcing would guarantee the value is present without
|
||||||
|
# proving it ever held.
|
||||||
|
fpu.force=0x007f
|
||||||
|
# Per-tick sampler: logs to shim.log whenever the word MOVES, so the log carries a timeline of
|
||||||
|
# the value rather than a single claim made at a single instant.
|
||||||
|
fpu.sample_ticks=on
|
||||||
31
src/shim/shim.cfg.fpu027f
Normal file
31
src/shim/shim.cfg.fpu027f
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Lane F, x87 precision-sensitivity experiment (verify/results/fpu-cw in the notes repo).
|
||||||
|
# 0x027f = 53-bit significand, round-to-nearest. The MSVC CRT default. Numerically identical to the game's own 0x127f: they differ only in bit 12 (infinity control), which the 387 and later ignore. Named in the lane brief as "24-bit"; it is not.
|
||||||
|
#
|
||||||
|
# Every shim.cfg.fpu* file is identical except the fpu.force line: the hook set, the trace path
|
||||||
|
# and the flush policy are held fixed so the control word is the only variable across runs.
|
||||||
|
#
|
||||||
|
# x87 control-word fields: bits 0-5 exception masks, bits 8-9 precision control
|
||||||
|
# (00 = 24-bit / 10 = 53-bit / 11 = 64-bit significand), bits 10-11 rounding control
|
||||||
|
# (00 nearest / 01 down / 10 up / 11 truncate), bit 12 infinity control (ignored since the 387).
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=trace
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=trace
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=trace
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::MoveFleet=trace
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.inline_max=256
|
||||||
|
trace.flush=always
|
||||||
|
|
||||||
|
# Forced once at the turn gate (StrategyClient::EndTurn and StrategyServer::BeginProcessTurn),
|
||||||
|
# never re-forced inside the pipeline -- re-forcing would guarantee the value is present without
|
||||||
|
# proving it ever held.
|
||||||
|
fpu.force=0x027f
|
||||||
|
# Per-tick sampler: logs to shim.log whenever the word MOVES, so the log carries a timeline of
|
||||||
|
# the value rather than a single claim made at a single instant.
|
||||||
|
fpu.sample_ticks=on
|
||||||
31
src/shim/shim.cfg.fpu127f
Normal file
31
src/shim/shim.cfg.fpu127f
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Lane F, x87 precision-sensitivity experiment (verify/results/fpu-cw in the notes repo).
|
||||||
|
# 0x127f = 53-bit significand, round-to-nearest. THE CONTROL: exactly what the game sets itself, so this run must reproduce the stock oracle hashes.
|
||||||
|
#
|
||||||
|
# Every shim.cfg.fpu* file is identical except the fpu.force line: the hook set, the trace path
|
||||||
|
# and the flush policy are held fixed so the control word is the only variable across runs.
|
||||||
|
#
|
||||||
|
# x87 control-word fields: bits 0-5 exception masks, bits 8-9 precision control
|
||||||
|
# (00 = 24-bit / 10 = 53-bit / 11 = 64-bit significand), bits 10-11 rounding control
|
||||||
|
# (00 nearest / 01 down / 10 up / 11 truncate), bit 12 infinity control (ignored since the 387).
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=trace
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=trace
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=trace
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::MoveFleet=trace
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.inline_max=256
|
||||||
|
trace.flush=always
|
||||||
|
|
||||||
|
# Forced once at the turn gate (StrategyClient::EndTurn and StrategyServer::BeginProcessTurn),
|
||||||
|
# never re-forced inside the pipeline -- re-forcing would guarantee the value is present without
|
||||||
|
# proving it ever held.
|
||||||
|
fpu.force=0x127f
|
||||||
|
# Per-tick sampler: logs to shim.log whenever the word MOVES, so the log carries a timeline of
|
||||||
|
# the value rather than a single claim made at a single instant.
|
||||||
|
fpu.sample_ticks=on
|
||||||
31
src/shim/shim.cfg.fpu137f
Normal file
31
src/shim/shim.cfg.fpu137f
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Lane F, x87 precision-sensitivity experiment (verify/results/fpu-cw in the notes repo).
|
||||||
|
# 0x137f = 64-bit significand (x87 extended), round-to-nearest. Named in the lane brief as "53-bit, different rounding"; it is in fact the precision axis, and the one an x64/SSE port cannot reproduce.
|
||||||
|
#
|
||||||
|
# Every shim.cfg.fpu* file is identical except the fpu.force line: the hook set, the trace path
|
||||||
|
# and the flush policy are held fixed so the control word is the only variable across runs.
|
||||||
|
#
|
||||||
|
# x87 control-word fields: bits 0-5 exception masks, bits 8-9 precision control
|
||||||
|
# (00 = 24-bit / 10 = 53-bit / 11 = 64-bit significand), bits 10-11 rounding control
|
||||||
|
# (00 nearest / 01 down / 10 up / 11 truncate), bit 12 infinity control (ignored since the 387).
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=trace
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=trace
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=trace
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::MoveFleet=trace
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.inline_max=256
|
||||||
|
trace.flush=always
|
||||||
|
|
||||||
|
# Forced once at the turn gate (StrategyClient::EndTurn and StrategyServer::BeginProcessTurn),
|
||||||
|
# never re-forced inside the pipeline -- re-forcing would guarantee the value is present without
|
||||||
|
# proving it ever held.
|
||||||
|
fpu.force=0x137f
|
||||||
|
# Per-tick sampler: logs to shim.log whenever the word MOVES, so the log carries a timeline of
|
||||||
|
# the value rather than a single claim made at a single instant.
|
||||||
|
fpu.sample_ticks=on
|
||||||
31
src/shim/shim.cfg.fpu1a7f
Normal file
31
src/shim/shim.cfg.fpu1a7f
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Lane F, x87 precision-sensitivity experiment (verify/results/fpu-cw in the notes repo).
|
||||||
|
# 0x1a7f = 53-bit significand, round-toward-+infinity. The genuine rounding-mode change the brief meant by 0x137f.
|
||||||
|
#
|
||||||
|
# Every shim.cfg.fpu* file is identical except the fpu.force line: the hook set, the trace path
|
||||||
|
# and the flush policy are held fixed so the control word is the only variable across runs.
|
||||||
|
#
|
||||||
|
# x87 control-word fields: bits 0-5 exception masks, bits 8-9 precision control
|
||||||
|
# (00 = 24-bit / 10 = 53-bit / 11 = 64-bit significand), bits 10-11 rounding control
|
||||||
|
# (00 nearest / 01 down / 10 up / 11 truncate), bit 12 infinity control (ignored since the 387).
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=trace
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=trace
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=trace
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::MoveFleet=trace
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.inline_max=256
|
||||||
|
trace.flush=always
|
||||||
|
|
||||||
|
# Forced once at the turn gate (StrategyClient::EndTurn and StrategyServer::BeginProcessTurn),
|
||||||
|
# never re-forced inside the pipeline -- re-forcing would guarantee the value is present without
|
||||||
|
# proving it ever held.
|
||||||
|
fpu.force=0x1a7f
|
||||||
|
# Per-tick sampler: logs to shim.log whenever the word MOVES, so the log carries a timeline of
|
||||||
|
# the value rather than a single claim made at a single instant.
|
||||||
|
fpu.sample_ticks=on
|
||||||
31
src/shim/shim.cfg.fpuoff
Normal file
31
src/shim/shim.cfg.fpuoff
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Lane F, x87 precision-sensitivity experiment (verify/results/fpu-cw in the notes repo).
|
||||||
|
# STOCK: nothing forced. Oracle re-confirmation and the game's own control-word timeline.
|
||||||
|
#
|
||||||
|
# Every shim.cfg.fpu* file is identical except the fpu.force line: the hook set, the trace path
|
||||||
|
# and the flush policy are held fixed so the control word is the only variable across runs.
|
||||||
|
#
|
||||||
|
# x87 control-word fields: bits 0-5 exception masks, bits 8-9 precision control
|
||||||
|
# (00 = 24-bit / 10 = 53-bit / 11 = 64-bit significand), bits 10-11 rounding control
|
||||||
|
# (00 nearest / 01 down / 10 up / 11 truncate), bit 12 infinity control (ignored since the 387).
|
||||||
|
hooks=trace
|
||||||
|
hook.Shim::SelfTest::Fill=off
|
||||||
|
hook.Mars::GlobalConsts::LoadFile=off
|
||||||
|
hook.Game::WeaponDictionary::Init=off
|
||||||
|
hook.Game::SectionDictionary::SectionDictionary=off
|
||||||
|
hook.Game::StrategyServer::ProcessFleetMovement=off
|
||||||
|
hook.Game::TechTree::ProcessResearch=trace
|
||||||
|
hook.Game::ServerPlayer::ComputeBudget=trace
|
||||||
|
hook.Game::ServerPlayer::OnTechResearched=trace
|
||||||
|
hook.Game::ServerSystem::ProcessTurn=trace
|
||||||
|
hook.Game::StrategyServer::MoveFleet=trace
|
||||||
|
trace.path=C:\SOTS\shim.trace.jsonl
|
||||||
|
trace.inline_max=256
|
||||||
|
trace.flush=always
|
||||||
|
|
||||||
|
# Forced once at the turn gate (StrategyClient::EndTurn and StrategyServer::BeginProcessTurn),
|
||||||
|
# never re-forced inside the pipeline -- re-forcing would guarantee the value is present without
|
||||||
|
# proving it ever held.
|
||||||
|
fpu.force=off
|
||||||
|
# Per-tick sampler: logs to shim.log whenever the word MOVES, so the log carries a timeline of
|
||||||
|
# the value rather than a single claim made at a single instant.
|
||||||
|
fpu.sample_ticks=on
|
||||||
Loading…
Add table
Reference in a new issue