Compare commits

..

2 commits

Author SHA1 Message Date
alex
fb23616a8d merge lane F: x87 control-word sensitivity - 53-bit and 64-bit identical; 24-bit and round-up each move 2 named leaves 2026-09-08 05:10:21 -04:00
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
11 changed files with 584 additions and 2 deletions

View file

@ -88,7 +88,7 @@ if(WIN32)
shim_colony shim_movement shim_events)
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}"

View file

@ -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,

312
src/shim/fpu_force.cpp Normal file
View 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
View 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

View file

@ -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<shim::hooks::StrategyServerMoveFleetHook>(tracer, exeBase, sots::addr::StrategyServer_MoveFleet);
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 -----------------------------------------------------------------------------
@ -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<unsigned long>(tracer.records_written()));

31
src/shim/shim.cfg.fpu007f Normal file
View 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
View 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
View 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
View 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
View 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
View 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