diff --git a/CMakeLists.txt b/CMakeLists.txt index b0d09c5..ed815a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,7 @@ if(WIN32) shim_colony shim_movement) 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_compile_definitions(binkw32 PRIVATE SHIM_BUILD_ID="${SHIM_BUILD_ID}" diff --git a/docs/shim-trace.md b/docs/shim-trace.md index 1f84fcf..d4c496d 100644 --- a/docs/shim-trace.md +++ b/docs/shim-trace.md @@ -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.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) +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. @@ -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 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 `shim.trace.jsonl`: line 1 is the `meta` record (`build`, `exe_sha256` of the running exe, diff --git a/src/shim/fpu_force.cpp b/src/shim/fpu_force.cpp new file mode 100644 index 0000000..762c91f --- /dev/null +++ b/src/shim/fpu_force.cpp @@ -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 + +#include +#include +#include +#include + +#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(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(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(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(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(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(&FpuEndTurnDetour), &g_origFpuEndTurn, true}, + {"StrategyServer::BeginProcessTurn", sots::addr::StrategyServer_BeginProcessTurn, + reinterpret_cast(&FpuBeginProcessTurnDetour), &g_origFpuBeginProcessTurn, true}, + {"StrategyServer::ProcessTurn", sots::addr::StrategyServer_ProcessTurn, + reinterpret_cast(&FpuProcessTurnDetour), &g_origFpuProcessTurn, true}, + {"DemoApp::OnTick", sots::addr::DemoApp_OnTick, reinterpret_cast(&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(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 diff --git a/src/shim/fpu_force.h b/src/shim/fpu_force.h new file mode 100644 index 0000000..ee84870 --- /dev/null +++ b/src/shim/fpu_force.h @@ -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 | 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 +#include + +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 diff --git a/src/shim/main.cpp b/src/shim/main.cpp index 22f9975..cfcab31 100644 --- a/src/shim/main.cpp +++ b/src/shim/main.cpp @@ -15,6 +15,7 @@ #include "MinHook.h" #include "generated/sots_addresses.h" +#include "shim/fpu_force.h" #include "shim/hooks/dictionaries.h" #include "shim/hooks/colony_turn.h" #include "shim/hooks/compute_budget.h" @@ -79,7 +80,10 @@ Config ReadConfig() { char* val = eq + 1; val[std::strcspn(val, "\r\n")] = '\0'; 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()); else Log("config: %s=%s", p, val); } else { @@ -207,6 +211,10 @@ void InstallHooks(shim::trace::Tracer& tracer) { shim::hooks::init_fleet_movement(exeBase, &ShimLogLine); InstallTemplateHook(tracer, exeBase, sots::addr::StrategyServer_MoveFleet); InstallTemplateHook(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 ----------------------------------------------------------------------------- @@ -282,6 +290,7 @@ void Shim_Init(HMODULE self) { } void Shim_Shutdown() { + Log("%s", shim::fpu::summary().c_str()); shim::trace::Tracer& tracer = shim::trace::Tracer::instance(); if (tracer.is_open()) { Log("shutdown: trace records=%lu", static_cast(tracer.records_written())); diff --git a/src/shim/shim.cfg.fpu007f b/src/shim/shim.cfg.fpu007f new file mode 100644 index 0000000..f5d7d18 --- /dev/null +++ b/src/shim/shim.cfg.fpu007f @@ -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 diff --git a/src/shim/shim.cfg.fpu027f b/src/shim/shim.cfg.fpu027f new file mode 100644 index 0000000..622a040 --- /dev/null +++ b/src/shim/shim.cfg.fpu027f @@ -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 diff --git a/src/shim/shim.cfg.fpu127f b/src/shim/shim.cfg.fpu127f new file mode 100644 index 0000000..762e124 --- /dev/null +++ b/src/shim/shim.cfg.fpu127f @@ -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 diff --git a/src/shim/shim.cfg.fpu137f b/src/shim/shim.cfg.fpu137f new file mode 100644 index 0000000..d737879 --- /dev/null +++ b/src/shim/shim.cfg.fpu137f @@ -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 diff --git a/src/shim/shim.cfg.fpu1a7f b/src/shim/shim.cfg.fpu1a7f new file mode 100644 index 0000000..9530f21 --- /dev/null +++ b/src/shim/shim.cfg.fpu1a7f @@ -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 diff --git a/src/shim/shim.cfg.fpuoff b/src/shim/shim.cfg.fpuoff new file mode 100644 index 0000000..02b9085 --- /dev/null +++ b/src/shim/shim.cfg.fpuoff @@ -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