diff --git a/findings/subsystems/fpu-precision-sensitivity.md b/findings/subsystems/fpu-precision-sensitivity.md new file mode 100644 index 0000000..5774a68 --- /dev/null +++ b/findings/subsystems/fpu-precision-sensitivity.md @@ -0,0 +1,207 @@ +# x87 precision sensitivity of the turn pipeline — measured (2026-09-08, lane F) + +Question (`verify/state-checksum/STATE_CHECKSUM.md` §3.5): *does any value the turn pipeline +produces actually **depend** on the x87 control word?* Until now every save on this host was +made at the game's own `fpu_cw = 0x127f`, so "the x64/SSE port must match bit-for-bit" was +**policy, not measurement**. + +It is now measured. Seven End-Turn runs from the same `ref-turn2.sav`, six distinct control-word +settings, whole-state checksum on each post-turn autosave. + +## Verdict + +| forced `fpu_cw` | precision | rounding | `(Autosave).sav` | vs baseline | +|---|---|---|---|---| +| *(stock, none forced)* | 53-bit | nearest | `978041ac…` 67,219 B | — (baseline) | +| `0x027f` | 53-bit | nearest | `978041ac…` 67,219 B | **identical** | +| `0x127f` | 53-bit | nearest | `978041ac…` 67,219 B | **identical** | +| `0x137f` | **64-bit (extended)** | nearest | `978041ac…` 67,219 B | **identical** | +| `0x007f` | **24-bit (single)** | nearest | `ba2435be…` 67,222 B | **2 leaves** | +| `0x1a7f` | 53-bit | **up (+∞)** | `e48e25fa…` 67,219 B | **2 leaves** | + +The pre-turn `(Autosave EndTurn).sav` is `bb4fd9ac…` in **all seven** runs — the state at the +moment End Turn was pressed is upstream of every forced value, which is the run-to-run control. + +**For the reimplementation:** + +1. **The x87's 64-bit intermediates are not load-bearing.** 53-bit and 64-bit produce the same + state, leaf for leaf, across 35,394 leaves. A port that computes in IEEE `double` reproduces + this turn exactly, and there is **no double-rounding budget to preserve** — the strict + `floats=bits` policy costs an x64/SSE port nothing on this axis. §3.5 closes. +2. **Narrowing intermediates to `float` does change state.** At 24-bit the home system's + civilian population lands two people higher. So the port must hold in `double` exactly where + the original holds in an x87 register, and narrow exactly where the original stores to a + `dword` — which is precisely the discipline lane M documented for `MoveFleet` + (`movefleet-position-rounding.md`: five separate float32 narrowings, products and sums held + at 53 bits in between). Getting a narrowing point wrong is not a rounding nicety; it moves + saved integers. +3. **Round-to-nearest is required.** Round-toward-+∞ moves fleet 34's position by 1 ULP in two + of three components. SSE defaults to round-to-nearest, so this is satisfied for free — but it + is now a measured requirement rather than an assumption. + +### The two named precision-sensitive witnesses + +Everything the experiment found, in full — this is the complete list for this turn: + +``` +0x007f (24-bit) vs baseline: + /Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC : 540000000 -> 540000002 + /Summary/Checksum : -769976634 -> -769976632 (derived) + +0x1a7f (round-up) vs baseline: + /Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0] : -10.563499450683594 -> -10.563498497009277 [1 ulp] + /Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[2] : 1.564733624458313 -> 1.5647337436676025 [1 ulp] +``` + +Both reproduce exactly on a repeat run (`run-007f-rep`, `run-1a7f-rep`) — same root hash, same +leaves. `Summary/Checksum` moves by the sum of the changed bytes, consistent with the additive +checksum already characterised in `determinism-oracle.md`; treat it as derived, never as state. + +The two witnesses are on **different axes and different subsystems**: the population integer is +precision-sensitive but not rounding-sensitive, and the fleet position is rounding-sensitive but +not precision-sensitive. That is a useful shape — it says an SSE port can go wrong in two +independent ways, and each has a cheap regression witness on turn 2 of `ref-turn2.sav`. + +## The evidence that the control word actually held + +This is what makes the result mean anything: an experiment where the setting silently reverted +would produce identical saves and a confident, false "nothing depends on precision". + +**1. Read-back at the point of forcing.** Each force site logs observed-before → requested → +read-back-after. Every one reports `OK`, e.g. (`run-007f/shim.log`): + +``` +fpu: FORCE at StrategyServer::BeginProcessTurn: observed=0x127f 53bit-double/nearest + -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +fpu: sample at StrategyServer::ProcessTurn: cw=0x007f 24bit-single/nearest +``` + +`StrategyServer::ProcessTurn` is a **sample-only** hook — it never writes the word. Its reading +is independent evidence that the value forced at the turn gate survived into the simulation. + +**2. 38 independent in-pipeline samples per run.** Every template hook already snapshots the +control word it finds. In each of the five forced runs, all 38 samples read the forced value and +nothing else (`verify/results/fpu-cw/cw-census.txt`): + +``` +run-007f: ServerSystem::ProcessTurn cw=0x007f x28 call_id 1110..1137 + StrategyServer::MoveFleet cw=0x007f x7 call_id 1103..1109 + TechTree::ProcessResearch cw=0x007f x3 call_id 1140..1144 + ALL SAMPLES: 0x007f x38 +``` + +Those three hooks sit in turn phases **4 (movement), 6 (colony) and 8 (per-player research)** — +so the samples span the pipeline from its first mover to its last research pass, in call-id +order, with no gap and no other value. The stock run reads `0x127f` x38 in the same places. + +**3. The forcing is a single write per turn, never re-applied inside the pipeline.** Re-forcing +at each hook would have guaranteed the samples without proving anything. + +## Why the forcing point matters: the game re-arms the word every frame + +`Mars::Application::Run` (`0x0089f5b0`) calls **`_controlfp(0x50000, 0x3070300)` once per frame** +at `0x0089f606`, between `OnUpdate` (vft+0x18) and `OnTick` (vft+0x1c). In MSVC's abstract +encoding that is `_PC_53 | _IC_AFFINE` under the mask `_MCW_DN|_MCW_IC|_MCW_PC|_MCW_RC`, which +is exactly the x87 word **`0x127f`**. There is no literal `0x127f` anywhere in the image +(`find-constant-uses` → 0 hits); the value is synthesised by the CRT. `_controlfp` is also +called from `Mars::Application::Initialize` (`0x008a10c6`) and from `FUN_00451920` +(`0x00451929`, one caller, a data reference). + +Two consequences, both visible in the logs: + +* Forcing at `StrategyClient::EndTurn` **does not survive**. The client's End Turn only raises + `SETurnEndPending` and sends `SNMEndTurn`; the server's `OnMessage` → `BeginProcessTurn` → + `ProcessTurn` runs on a **later frame**, and the per-frame `_controlfp` wipes the setting in + between. Every log shows `FORCE at StrategyClient::EndTurn … readback=0x007f` at *t*, then + `FORCE at StrategyServer::BeginProcessTurn: observed=0x127f` about a second later. + `BeginProcessTurn` is the forcing point that works, because `BeginProcessTurn` and + `ProcessTurn` are called from the same `OnMessage`, inside one `OnUpdate`, with no + `_controlfp` between them. +* The per-tick sampler never logs a change, and that is **expected, not evidence**: `_controlfp` + runs immediately before `OnTick`, so `OnTick` always observes a freshly re-armed `0x127f`. + The sampler's real contribution is the negative one — it proves the process never sits in a + forced state outside the turn, so nothing leaks between runs. + +This also retro-explains why every previous lane measured `fpu_cw = 0x127f` at every hook: the +main loop guarantees it, frame by frame. + +## The lane brief's three values do not span three FPU modes + +The experiment as specified in §3.5 and in the lane brief would have been under-powered, and +this is worth recording so nobody re-derives it: + +| value | actually is | brief called it | +|---|---|---| +| `0x027f` | 53-bit, nearest (MSVC CRT default) | "24-bit mantissa (single precision)" | +| `0x127f` | 53-bit, nearest (+ infinity-control bit) | "53-bit — the game's own setting" ✓ | +| `0x137f` | **64-bit extended**, nearest | "53-bit but a different rounding mode" | + +`0x027f` and `0x127f` differ **only in bit 12** (infinity control), which the 387 and every later +x87 ignore — they are the same arithmetic. So the briefed triple spans two modes (53-bit and +64-bit), not three, tests single precision **not at all**, and tests rounding **not at all**. +Run literally, all three come back identical (they do — see the table) and the honest conclusion +from them alone would have been "no precision sensitivity", which is false for rounding and +unproven for single precision. + +The control-word 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). +So the genuine probes are `0x007f` (24-bit) and `0x1a7f` (53-bit, round-up), and those are the +two runs that found something. + +## Trap: the trace's own float rendering is precision-sensitive + +Under a forced 24-bit control word, **the shim's trace text is not a valid comparison surface.** +The emitter prints an f32 with `%.9g`, and the CRT's digit generation is itself x87 arithmetic, +so the same float32 renders differently: + +``` +127f run: "z":{"t":"f32","v":1.56473362} both are float32 0x3fc84931 +007f run: "z":{"t":"f32","v":1.5647336} — the value did not move, the printf did +127f run: "suit":{"t":"f32","v":1.65671718} both are float32 0x3fd40f4f +007f run: "suit":{"t":"f32","v":1.65671721} +``` + +A text diff of the two traces reports dozens of these as divergences. `verify/fpu-cw/trace_bitdiff.py` +re-quantises every float to its IEEE bit pattern before comparing, and then the 24-bit run shows +**zero** differences in any hooked call — the population change happens in a byte no hook +declares. This is the `STATE_CHECKSUM.md` §1 thesis with a fresh example: the per-function +verdict was clean and the state had still moved; only the whole-state checksum saw it. +Any future `tracecmp`-style comparison across control words must compare bits, not text. + +## Incidental: guard spans differ between byte-identical runs + +`ServerSystem::ProcessTurn`'s undeclared-write spans are not stable run to run even when the +saves are byte-identical (`trace-bitdiff.txt`, `127f vs off`): the same three writes at +`system+420..432` are reported once as one 12-byte span and once as three 3-byte spans, and once +shifted by a byte. Harmless for a verdict (the *set* of touched bytes is the same) but it means +guard-span text is not a stable comparison surface either. Not chased — outside this lane. + +## Method + +* VM140, `ref-turn2.sav` (`ab4ac2d7…`), click path per `determinism-oracle.md`. Shim build + `fpucw-cef889e-20260908T0803Z`, hook set held **identical** across all seven runs — the only + difference between `shim.cfg.fpu*` files is the `fpu.force` line. +* `SavedGames\` is reset to the same four files before every run, so the Load dialog rows never + move and the click path is constant. +* Forcing implemented in `sots-engine/src/shim/fpu_force.cpp` (branch `wip/fpucw`): + register-transparent asm stubs, same pattern as the `Application::Initialize` hook, so the + `[unverified]` prototypes of the turn-gate functions are never relied on. + shim.cfg keys `fpu.force=|off` and `fpu.sample_ticks=on|off`. +* Evidence: `verify/results/fpu-cw/run-/` — both autosaves, `shim.log`, `shim.cfg`, + gzipped trace; plus `cw-census.txt`, `state-checksum-diffs.txt`, `trace-bitdiff.txt`. +* Tools: `verify/fpu-cw/cw_census.py` (per-hook control-word census), + `verify/fpu-cw/trace_bitdiff.py` (bit-exact trace comparison). + +## Coverage and limits + +* The whole-state checksum **proves** coverage on every run: 609,080 bytes rebuilt byte-for-byte + from 35,394 leaves. "Identical" here means no leaf anywhere moved, not "no declared region + moved". +* **One turn, one save, one galaxy.** Turn 2→3 of `ref-turn2.sav`: 28 systems, 3 owned, 7 + `MoveFleet` calls of which one fleet actually moves, 3 research passes, no combat, no Zuul, no + plague/rebellion/terraform. The two witnesses are what *this* turn exposes; a turn with combat + or more movers could expose more. The result "53-bit == 64-bit" is the one that generalises + best, because it held across all 35,394 leaves and all 38 in-pipeline samples. +* Nothing here says the *original* is x87-free — only that its results on this turn do not move + between 53-bit and 64-bit intermediates, which is the property an SSE port needs. diff --git a/verify/fpu-cw/cw_census.py b/verify/fpu-cw/cw_census.py new file mode 100644 index 0000000..d153303 --- /dev/null +++ b/verify/fpu-cw/cw_census.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Census of the `fpu_cw` field the shim's template hooks record on every call. + +Each hook snapshots the x87 control word as it finds it, so a run's trace carries independent +samples of the word from *inside* the turn pipeline -- phase 4 (MoveFleet), phase 6 +(ServerSystem::ProcessTurn) and phase 8 (ComputeBudget / ProcessResearch). That is the evidence +that a control word forced at the turn gate actually held for the duration of the turn, rather +than being silently restored partway through (which would produce identical saves and a +completely false "nothing depends on precision" conclusion). + +Usage: cw_census.py [...] +""" +import gzip +import json +import sys +from collections import Counter, defaultdict + + +def _open(path): + op = gzip.open if path.endswith(".gz") else open + return op(path, "rt", encoding="utf-8", errors="replace") + + +def census(path): + per_hook = defaultdict(Counter) + first_last = {} + n = 0 + with _open(path) as f: + for line in f: + line = line.strip() + if not line.startswith("{"): + continue + try: + rec = json.loads(line) + except json.JSONDecodeError: + continue + hook = rec.get("hook") + if not hook: + continue + n += 1 + for arg in rec.get("args", []) or []: + if arg.get("n") == "fpu_cw": + cw = int(arg["v"]) + per_hook[hook][cw] += 1 + key = (hook, cw) + if key not in first_last: + first_last[key] = [rec.get("call_id"), rec.get("call_id")] + else: + first_last[key][1] = rec.get("call_id") + return n, per_hook, first_last + + +def main(): + for path in sys.argv[1:]: + n, per_hook, first_last = census(path) + print(f"== {path} ({n} hook records)") + total = Counter() + for hook in sorted(per_hook): + for cw, count in sorted(per_hook[hook].items()): + lo, hi = first_last[(hook, cw)] + total[cw] += count + print(f" {hook:<42} cw=0x{cw:04x} x{count:<5} call_id {lo}..{hi}") + if not per_hook: + print(" (no hook recorded an fpu_cw field)") + else: + vals = ", ".join(f"0x{cw:04x} x{c}" for cw, c in sorted(total.items())) + print(f" ALL SAMPLES: {vals}") + print() + + +if __name__ == "__main__": + main() diff --git a/verify/fpu-cw/trace_bitdiff.py b/verify/fpu-cw/trace_bitdiff.py new file mode 100644 index 0000000..b58593e --- /dev/null +++ b/verify/fpu-cw/trace_bitdiff.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Bit-exact comparison of two shim traces of the same turn, hook by hook. + +Why not just diff the text: **the trace's own float rendering is not a stable comparison +surface when the x87 control word has been forced.** The emitter prints an f32 with `%.9g`, +and the CRT's digit generation is itself done in x87 arithmetic, so under `fpu_cw = 0x007f` +(24-bit) the *same* float32 renders as `1.5647336` instead of `1.56473362`. Comparing the text +of a 24-bit run against a 53-bit run reports dozens of "differences" that are all the printf, +not the simulation. Every float here is therefore re-quantised to its IEEE bit pattern before +comparison, and per-process noise (pointers, thread id, timestamps, call ids, the `fpu_cw` +field that is the independent variable) is scrubbed. + +Usage: trace_bitdiff.py [hook ...] +""" +import gzip +import json +import re +import struct +import sys + +PTR = re.compile(r"^0x[0-9a-f]{8}$") +# Only hooks that fire *inside* the turn pipeline: ServerPlayer::ComputeBudget also runs once +# per UI frame, so its call count depends on how long the session sat at a menu and is not +# comparable between runs. Name it explicitly on the command line if you want it anyway. +DEFAULT_HOOKS = ( + "Game::ServerSystem::ProcessTurn", + "Game::StrategyServer::MoveFleet", + "Game::TechTree::ProcessResearch", + "Game::ServerPlayer::OnTechResearched", +) +NOISE = ("ts", "call_id", "thread") + + +def f32_bits(v): + return struct.unpack("" + return o + + +def _open(path): + op = gzip.open if path.endswith(".gz") else open + return op(path, "rt", encoding="utf-8", errors="replace") + + +def load(path, hook): + out = [] + needle = '"hook":"%s"' % hook + with _open(path) as f: + for line in f: + if needle not in line: + continue + rec = norm(json.loads(line)) + rec["args"] = [a for a in rec.get("args", []) if a.get("n") != "fpu_cw"] + for side in (rec.get("side") or {}).values(): + if not isinstance(side, dict): + continue + for snap in side.values(): + if isinstance(snap, dict) and isinstance(snap.get("v"), dict): + snap["v"].pop("fpu_cw", None) + out.append(rec) + return out + + +def leafdiff(a, b, path=""): + out = [] + if isinstance(a, dict) and isinstance(b, dict): + for k in sorted(set(a) | set(b)): + out += leafdiff(a.get(k), b.get(k), f"{path}.{k}") + elif isinstance(a, list) and isinstance(b, list) and len(a) == len(b): + for i, (x, y) in enumerate(zip(a, b)): + out += leafdiff(x, y, f"{path}[{i}]") + elif a != b: + out.append((path, a, b)) + return out + + +def main(): + pa, pb = sys.argv[1], sys.argv[2] + hooks = sys.argv[3:] or list(DEFAULT_HOOKS) + print(f"A {pa}\nB {pb}") + total = 0 + for hook in hooks: + a, b = load(pa, hook), load(pb, hook) + if not a and not b: + continue + if len(a) != len(b): + print(f"{hook}: CALL COUNT DIFFERS {len(a)} vs {len(b)}") + total += 1 + continue + diffs = [(i, p, x, y) for i, (ra, rb) in enumerate(zip(a, b)) for p, x, y in leafdiff(ra, rb)] + total += len(diffs) + print(f"{hook}: {len(diffs)} leaf difference(s) over {len(a)} record(s)") + for i, p, x, y in diffs: + print(f" rec[{i}] {p}: {x} -> {y}") + print(f"TOTAL: {total}") + return 1 if total else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/verify/results/fpu-cw/README.md b/verify/results/fpu-cw/README.md new file mode 100644 index 0000000..8de2cd4 --- /dev/null +++ b/verify/results/fpu-cw/README.md @@ -0,0 +1,55 @@ +# x87 precision-sensitivity experiment — artefacts (lane F, 2026-09-08) + +Read `findings/subsystems/fpu-precision-sensitivity.md` for the result and the argument. +This directory is the evidence. + +Seven End-Turn runs from `C:\SOTS\SavedGames\ref-turn2.sav` (`ab4ac2d7…`) on VM140, shim build +`fpucw-cef889e-20260908T0803Z`. The hook set, trace path and flush policy are identical in every +run; the ONLY difference between the `shim.cfg` files is the `fpu.force=` line. + +``` +run-off/ nothing forced -- oracle re-confirmation + the game's own control-word timeline +run-027f/ fpu.force=0x027f 53-bit, nearest (the MSVC CRT default; NOT 24-bit) +run-127f/ fpu.force=0x127f 53-bit, nearest -- the CONTROL: what the game sets itself +run-137f/ fpu.force=0x137f 64-bit extended, nearest +run-007f/ fpu.force=0x007f 24-bit single, nearest +run-1a7f/ fpu.force=0x1a7f 53-bit, round-toward-+infinity +run-007f-rep/ repeat of run-007f -- reproducibility of the divergence +run-1a7f-rep/ repeat of run-1a7f +``` + +Each run directory holds `(Autosave).sav` (post-turn, the thing under test), +`(Autosave EndTurn).sav` (pre-turn, the run-to-run control -- `bb4fd9ac…` in every run), +`shim.log` (the force / read-back / per-tick-sampler timeline), `shim.cfg` and the gzipped +trace. The two `-rep` runs keep only the saves and the log. + +Derived, regenerate with the commands below: + +* `cw-census.txt` — the `fpu_cw` every hooked call observed, per run. This is the evidence that + the forced word held for the *duration* of the turn: 38 samples per run spanning turn phases + 4, 6 and 8, all reading the forced value. +* `state-checksum-diffs.txt` — every run's post-turn autosave against `run-127f`. +* `trace-bitdiff.txt` — bit-exact trace comparison. Note the trap it exists for: under a forced + 24-bit control word the CRT's own `%g` rendering degrades, so trace *text* is not a valid + comparison surface. + +```sh +uv run python3 verify/fpu-cw/cw_census.py verify/results/fpu-cw/run-*/shim.trace.jsonl.gz +uv run python3 verify/state-checksum/state_checksum.py \ + "verify/results/fpu-cw/run-127f/(Autosave).sav" "verify/results/fpu-cw/run-007f/(Autosave).sav" +uv run python3 verify/fpu-cw/trace_bitdiff.py \ + verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz verify/results/fpu-cw/run-1a7f/shim.trace.jsonl.gz +``` + +Headline hashes (sha256 of the file, gzip container included -- MTIME is 0, so this is valid): + +``` +bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 (Autosave EndTurn).sav ALL SEVEN RUNS +978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 (Autosave).sav stock / 0x027f / 0x127f / 0x137f +ba2435beb17265af3abddb58cd26e926d42b39b358ef6d0c76f9897386174918 (Autosave).sav 0x007f (both runs) +e48e25fa8425a8a1d174efe3e8a553153447f11f8f91c5fda25bd7db3a26cca7 (Autosave).sav 0x1a7f (both runs) +``` + +The first two lines are also the **determinism-oracle re-confirmation** on engine `cef889e` +(lane M's movement fix included): unchanged, as it must be — that fix touches `ours`, never the +original. diff --git a/verify/results/fpu-cw/cw-census.txt b/verify/results/fpu-cw/cw-census.txt new file mode 100644 index 0000000..6459add --- /dev/null +++ b/verify/results/fpu-cw/cw-census.txt @@ -0,0 +1,36 @@ +== verify/results/fpu-cw/run-007f/shim.trace.jsonl.gz (2212 hook records) + Game::ServerSystem::ProcessTurn cw=0x007f x28 call_id 1110..1137 + Game::StrategyServer::MoveFleet cw=0x007f x7 call_id 1103..1109 + Game::TechTree::ProcessResearch cw=0x007f x3 call_id 1140..1144 + ALL SAMPLES: 0x007f x38 + +== verify/results/fpu-cw/run-027f/shim.trace.jsonl.gz (2231 hook records) + Game::ServerSystem::ProcessTurn cw=0x027f x28 call_id 1134..1161 + Game::StrategyServer::MoveFleet cw=0x027f x7 call_id 1127..1133 + Game::TechTree::ProcessResearch cw=0x027f x3 call_id 1164..1168 + ALL SAMPLES: 0x027f x38 + +== verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz (2317 hook records) + Game::ServerSystem::ProcessTurn cw=0x127f x28 call_id 1195..1222 + Game::StrategyServer::MoveFleet cw=0x127f x7 call_id 1188..1194 + Game::TechTree::ProcessResearch cw=0x127f x3 call_id 1225..1229 + ALL SAMPLES: 0x127f x38 + +== verify/results/fpu-cw/run-137f/shim.trace.jsonl.gz (2280 hook records) + Game::ServerSystem::ProcessTurn cw=0x137f x28 call_id 1152..1179 + Game::StrategyServer::MoveFleet cw=0x137f x7 call_id 1145..1151 + Game::TechTree::ProcessResearch cw=0x137f x3 call_id 1182..1186 + ALL SAMPLES: 0x137f x38 + +== verify/results/fpu-cw/run-1a7f/shim.trace.jsonl.gz (2304 hook records) + Game::ServerSystem::ProcessTurn cw=0x1a7f x28 call_id 1187..1214 + Game::StrategyServer::MoveFleet cw=0x1a7f x7 call_id 1180..1186 + Game::TechTree::ProcessResearch cw=0x1a7f x3 call_id 1217..1221 + ALL SAMPLES: 0x1a7f x38 + +== verify/results/fpu-cw/run-off/shim.trace.jsonl.gz (2449 hook records) + Game::ServerSystem::ProcessTurn cw=0x127f x28 call_id 1152..1179 + Game::StrategyServer::MoveFleet cw=0x127f x7 call_id 1145..1151 + Game::TechTree::ProcessResearch cw=0x127f x3 call_id 1182..1186 + ALL SAMPLES: 0x127f x38 + diff --git a/verify/results/fpu-cw/run-007f-rep/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-007f-rep/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-007f-rep/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-007f-rep/(Autosave).sav b/verify/results/fpu-cw/run-007f-rep/(Autosave).sav new file mode 100644 index 0000000..80b5606 Binary files /dev/null and b/verify/results/fpu-cw/run-007f-rep/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-007f-rep/shim.log b/verify/results/fpu-cw/run-007f-rep/shim.log new file mode 100644 index 0000000..fc1766e --- /dev/null +++ b/verify/results/fpu-cw/run-007f-rep/shim.log @@ -0,0 +1,61 @@ +04:47:30.428 [tid 2160] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:47:30.428 [tid 2160] exe: C:\SOTS\Sword of the Stars.exe +04:47:30.428 [tid 2160] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=7180 shim=696a0000 +04:47:30.428 [tid 2160] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:47:30.428 [tid 2160] config: hooks=trace +04:47:30.428 [tid 2160] config: hook.Shim::SelfTest::Fill=off +04:47:30.428 [tid 2160] config: hook.Mars::GlobalConsts::LoadFile=off +04:47:30.428 [tid 2160] config: hook.Game::WeaponDictionary::Init=off +04:47:30.428 [tid 2160] config: hook.Game::SectionDictionary::SectionDictionary=off +04:47:30.428 [tid 2160] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:47:30.428 [tid 2160] config: hook.Game::TechTree::ProcessResearch=trace +04:47:30.428 [tid 2160] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:47:30.428 [tid 2160] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:47:30.428 [tid 2160] config: hook.Game::ServerSystem::ProcessTurn=trace +04:47:30.428 [tid 2160] config: hook.Game::StrategyServer::MoveFleet=trace +04:47:30.428 [tid 2160] config: trace.path=C:\SOTS\shim.trace.jsonl +04:47:30.428 [tid 2160] config: trace.inline_max=256 +04:47:30.428 [tid 2160] config: trace.flush=always +04:47:30.428 [tid 2160] config: fpu.force=0x007f +04:47:30.428 [tid 2160] config: fpu.sample_ticks=on +04:47:30.490 [tid 2160] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:47:30.490 [tid 2160] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:47:30.490 [tid 2160] hook: MH_Initialize -> MH_OK +04:47:30.490 [tid 2160] hook: MH_CreateHook -> MH_OK (trampoline=017d0fe0) +04:47:30.506 [tid 2160] hook: MH_EnableHook -> MH_OK +04:47:30.506 [tid 2160] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:47:30.506 [tid 2160] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:47:30.506 [tid 2160] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:47:30.506 [tid 2160] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:47:30.506 [tid 2160] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:47:30.506 [tid 2160] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:47:30.506 [tid 2160] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=017d0fc0) +04:47:30.522 [tid 2160] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:47:30.522 [tid 2160] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:47:30.522 [tid 2160] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=017d0fa0) +04:47:30.522 [tid 2160] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:47:30.522 [tid 2160] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=017d0f80) +04:47:30.537 [tid 2160] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:47:30.537 [tid 2160] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=017d0f60) +04:47:30.553 [tid 2160] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:47:30.553 [tid 2160] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=017d0f40) +04:47:30.569 [tid 2160] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:47:30.569 [tid 2160] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:47:30.569 [tid 2160] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=on value=0x007f sample_ticks=on +04:47:30.569 [tid 2160] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=017d0f20) +04:47:30.584 [tid 2160] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:47:30.584 [tid 2160] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=017d0f00) +04:47:30.600 [tid 2160] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:47:30.600 [tid 2160] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=017d0ee0) +04:47:30.615 [tid 2160] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:47:30.615 [tid 2160] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=017d0ec0) +04:47:30.631 [tid 2160] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:47:30.631 [tid 2160] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:47:30.631 [tid 2160] Application::Initialize called (this=03b08128) +04:47:31.365 [tid 2160] fpu: TICK BASELINE at OnTick (this=03b08128): cw=0x127f 53bit-double/nearest +04:51:34.694 [tid 2160] fpu: FORCE at StrategyClient::EndTurn (this=0e8b0ba0): observed=0x127f 53bit-double/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:51:37.757 [tid 2160] fpu: FORCE at StrategyClient::EndTurn (this=351a8bf0): observed=0x127f 53bit-double/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:51:37.788 [tid 2160] fpu: FORCE at StrategyClient::EndTurn (this=351a6788): observed=0x007f 24bit-single/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:51:37.819 [tid 2160] fpu: FORCE at StrategyClient::EndTurn (this=351ab7a0): observed=0x007f 24bit-single/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:51:38.788 [tid 2160] fpu: FORCE at StrategyServer::BeginProcessTurn (this=0e8ebc90): observed=0x127f 53bit-double/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:51:38.788 [tid 2160] fpu: sample at StrategyServer::ProcessTurn (this=0e8ebc90): cw=0x007f 24bit-single/nearest diff --git a/verify/results/fpu-cw/run-007f/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-007f/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-007f/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-007f/(Autosave).sav b/verify/results/fpu-cw/run-007f/(Autosave).sav new file mode 100644 index 0000000..80b5606 Binary files /dev/null and b/verify/results/fpu-cw/run-007f/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-007f/shim.cfg b/verify/results/fpu-cw/run-007f/shim.cfg new file mode 100644 index 0000000..f5d7d18 --- /dev/null +++ b/verify/results/fpu-cw/run-007f/shim.cfg @@ -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/verify/results/fpu-cw/run-007f/shim.log b/verify/results/fpu-cw/run-007f/shim.log new file mode 100644 index 0000000..f59ce1f --- /dev/null +++ b/verify/results/fpu-cw/run-007f/shim.log @@ -0,0 +1,61 @@ +04:19:06.590 [tid 7008] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:19:06.590 [tid 7008] exe: C:\SOTS\Sword of the Stars.exe +04:19:06.590 [tid 7008] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=1276 shim=696a0000 +04:19:06.590 [tid 7008] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:19:06.606 [tid 7008] config: hooks=trace +04:19:06.606 [tid 7008] config: hook.Shim::SelfTest::Fill=off +04:19:06.606 [tid 7008] config: hook.Mars::GlobalConsts::LoadFile=off +04:19:06.606 [tid 7008] config: hook.Game::WeaponDictionary::Init=off +04:19:06.606 [tid 7008] config: hook.Game::SectionDictionary::SectionDictionary=off +04:19:06.606 [tid 7008] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:19:06.606 [tid 7008] config: hook.Game::TechTree::ProcessResearch=trace +04:19:06.606 [tid 7008] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:19:06.606 [tid 7008] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:19:06.606 [tid 7008] config: hook.Game::ServerSystem::ProcessTurn=trace +04:19:06.606 [tid 7008] config: hook.Game::StrategyServer::MoveFleet=trace +04:19:06.606 [tid 7008] config: trace.path=C:\SOTS\shim.trace.jsonl +04:19:06.606 [tid 7008] config: trace.inline_max=256 +04:19:06.606 [tid 7008] config: trace.flush=always +04:19:06.606 [tid 7008] config: fpu.force=0x007f +04:19:06.606 [tid 7008] config: fpu.sample_ticks=on +04:19:06.653 [tid 7008] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:19:06.653 [tid 7008] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:19:06.653 [tid 7008] hook: MH_Initialize -> MH_OK +04:19:06.653 [tid 7008] hook: MH_CreateHook -> MH_OK (trampoline=01740fe0) +04:19:06.668 [tid 7008] hook: MH_EnableHook -> MH_OK +04:19:06.668 [tid 7008] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:19:06.668 [tid 7008] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:19:06.668 [tid 7008] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:19:06.668 [tid 7008] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:19:06.668 [tid 7008] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:19:06.668 [tid 7008] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:19:06.668 [tid 7008] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=01740fc0) +04:19:06.684 [tid 7008] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:19:06.684 [tid 7008] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:19:06.684 [tid 7008] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=01740fa0) +04:19:06.700 [tid 7008] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:19:06.700 [tid 7008] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=01740f80) +04:19:06.715 [tid 7008] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:19:06.715 [tid 7008] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=01740f60) +04:19:06.731 [tid 7008] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:19:06.731 [tid 7008] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=01740f40) +04:19:06.747 [tid 7008] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:19:06.747 [tid 7008] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:19:06.747 [tid 7008] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=on value=0x007f sample_ticks=on +04:19:06.747 [tid 7008] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=01740f20) +04:19:06.762 [tid 7008] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:19:06.762 [tid 7008] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=01740f00) +04:19:06.762 [tid 7008] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:19:06.762 [tid 7008] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=01740ee0) +04:19:06.778 [tid 7008] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:19:06.778 [tid 7008] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=01740ec0) +04:19:06.793 [tid 7008] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:19:06.793 [tid 7008] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:19:06.809 [tid 7008] Application::Initialize called (this=03528128) +04:19:07.528 [tid 7008] fpu: TICK BASELINE at OnTick (this=03528128): cw=0x127f 53bit-double/nearest +04:23:06.089 [tid 7008] fpu: FORCE at StrategyClient::EndTurn (this=0e2aa720): observed=0x127f 53bit-double/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:23:09.152 [tid 7008] fpu: FORCE at StrategyClient::EndTurn (this=34b68d80): observed=0x127f 53bit-double/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:23:09.167 [tid 7008] fpu: FORCE at StrategyClient::EndTurn (this=34b64bf8): observed=0x007f 24bit-single/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:23:09.198 [tid 7008] fpu: FORCE at StrategyClient::EndTurn (this=34b65340): observed=0x007f 24bit-single/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:23:10.152 [tid 7008] fpu: FORCE at StrategyServer::BeginProcessTurn (this=0e208b28): observed=0x127f 53bit-double/nearest -> requested=0x007f -> readback=0x007f 24bit-single/nearest OK +04:23:10.152 [tid 7008] fpu: sample at StrategyServer::ProcessTurn (this=0e208b28): cw=0x007f 24bit-single/nearest diff --git a/verify/results/fpu-cw/run-007f/shim.trace.jsonl.gz b/verify/results/fpu-cw/run-007f/shim.trace.jsonl.gz new file mode 100644 index 0000000..3a2f097 Binary files /dev/null and b/verify/results/fpu-cw/run-007f/shim.trace.jsonl.gz differ diff --git a/verify/results/fpu-cw/run-027f/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-027f/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-027f/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-027f/(Autosave).sav b/verify/results/fpu-cw/run-027f/(Autosave).sav new file mode 100644 index 0000000..6b3e2e4 Binary files /dev/null and b/verify/results/fpu-cw/run-027f/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-027f/shim.cfg b/verify/results/fpu-cw/run-027f/shim.cfg new file mode 100644 index 0000000..622a040 --- /dev/null +++ b/verify/results/fpu-cw/run-027f/shim.cfg @@ -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/verify/results/fpu-cw/run-027f/shim.log b/verify/results/fpu-cw/run-027f/shim.log new file mode 100644 index 0000000..ca114a5 --- /dev/null +++ b/verify/results/fpu-cw/run-027f/shim.log @@ -0,0 +1,61 @@ +04:24:39.698 [tid 7416] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:24:39.698 [tid 7416] exe: C:\SOTS\Sword of the Stars.exe +04:24:39.698 [tid 7416] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=7776 shim=696a0000 +04:24:39.698 [tid 7416] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:24:39.698 [tid 7416] config: hooks=trace +04:24:39.698 [tid 7416] config: hook.Shim::SelfTest::Fill=off +04:24:39.698 [tid 7416] config: hook.Mars::GlobalConsts::LoadFile=off +04:24:39.698 [tid 7416] config: hook.Game::WeaponDictionary::Init=off +04:24:39.698 [tid 7416] config: hook.Game::SectionDictionary::SectionDictionary=off +04:24:39.698 [tid 7416] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:24:39.698 [tid 7416] config: hook.Game::TechTree::ProcessResearch=trace +04:24:39.698 [tid 7416] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:24:39.698 [tid 7416] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:24:39.698 [tid 7416] config: hook.Game::ServerSystem::ProcessTurn=trace +04:24:39.698 [tid 7416] config: hook.Game::StrategyServer::MoveFleet=trace +04:24:39.698 [tid 7416] config: trace.path=C:\SOTS\shim.trace.jsonl +04:24:39.698 [tid 7416] config: trace.inline_max=256 +04:24:39.698 [tid 7416] config: trace.flush=always +04:24:39.698 [tid 7416] config: fpu.force=0x027f +04:24:39.698 [tid 7416] config: fpu.sample_ticks=on +04:24:39.761 [tid 7416] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:24:39.761 [tid 7416] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:24:39.761 [tid 7416] hook: MH_Initialize -> MH_OK +04:24:39.761 [tid 7416] hook: MH_CreateHook -> MH_OK (trampoline=00be0fe0) +04:24:39.777 [tid 7416] hook: MH_EnableHook -> MH_OK +04:24:39.777 [tid 7416] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:24:39.777 [tid 7416] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:24:39.777 [tid 7416] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:24:39.777 [tid 7416] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:24:39.777 [tid 7416] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:24:39.777 [tid 7416] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:24:39.777 [tid 7416] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=00be0fc0) +04:24:39.777 [tid 7416] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:24:39.777 [tid 7416] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:24:39.777 [tid 7416] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=00be0fa0) +04:24:39.792 [tid 7416] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:24:39.792 [tid 7416] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=00be0f80) +04:24:39.808 [tid 7416] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:24:39.808 [tid 7416] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=00be0f60) +04:24:39.823 [tid 7416] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:24:39.823 [tid 7416] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=00be0f40) +04:24:39.839 [tid 7416] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:24:39.839 [tid 7416] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:24:39.839 [tid 7416] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=on value=0x027f sample_ticks=on +04:24:39.839 [tid 7416] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=00be0f20) +04:24:39.855 [tid 7416] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:24:39.855 [tid 7416] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=00be0f00) +04:24:39.855 [tid 7416] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:24:39.855 [tid 7416] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=00be0ee0) +04:24:39.870 [tid 7416] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:24:39.870 [tid 7416] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=00be0ec0) +04:24:39.886 [tid 7416] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:24:39.886 [tid 7416] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:24:39.902 [tid 7416] Application::Initialize called (this=038a8128) +04:24:40.636 [tid 7416] fpu: TICK BASELINE at OnTick (this=038a8128): cw=0x127f 53bit-double/nearest +04:28:38.077 [tid 7416] fpu: FORCE at StrategyClient::EndTurn (this=0e5c3758): observed=0x127f 53bit-double/nearest -> requested=0x027f -> readback=0x027f 53bit-double/nearest OK +04:28:41.139 [tid 7416] fpu: FORCE at StrategyClient::EndTurn (this=3572eed0): observed=0x127f 53bit-double/nearest -> requested=0x027f -> readback=0x027f 53bit-double/nearest OK +04:28:41.155 [tid 7416] fpu: FORCE at StrategyClient::EndTurn (this=357354c0): observed=0x027f 53bit-double/nearest -> requested=0x027f -> readback=0x027f 53bit-double/nearest OK +04:28:41.186 [tid 7416] fpu: FORCE at StrategyClient::EndTurn (this=3572f618): observed=0x027f 53bit-double/nearest -> requested=0x027f -> readback=0x027f 53bit-double/nearest OK +04:28:42.155 [tid 7416] fpu: FORCE at StrategyServer::BeginProcessTurn (this=0e5b3c40): observed=0x127f 53bit-double/nearest -> requested=0x027f -> readback=0x027f 53bit-double/nearest OK +04:28:42.155 [tid 7416] fpu: sample at StrategyServer::ProcessTurn (this=0e5b3c40): cw=0x027f 53bit-double/nearest diff --git a/verify/results/fpu-cw/run-027f/shim.trace.jsonl.gz b/verify/results/fpu-cw/run-027f/shim.trace.jsonl.gz new file mode 100644 index 0000000..6ed5548 Binary files /dev/null and b/verify/results/fpu-cw/run-027f/shim.trace.jsonl.gz differ diff --git a/verify/results/fpu-cw/run-127f/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-127f/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-127f/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-127f/(Autosave).sav b/verify/results/fpu-cw/run-127f/(Autosave).sav new file mode 100644 index 0000000..6b3e2e4 Binary files /dev/null and b/verify/results/fpu-cw/run-127f/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-127f/shim.cfg b/verify/results/fpu-cw/run-127f/shim.cfg new file mode 100644 index 0000000..762e124 --- /dev/null +++ b/verify/results/fpu-cw/run-127f/shim.cfg @@ -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/verify/results/fpu-cw/run-127f/shim.log b/verify/results/fpu-cw/run-127f/shim.log new file mode 100644 index 0000000..8f94954 --- /dev/null +++ b/verify/results/fpu-cw/run-127f/shim.log @@ -0,0 +1,61 @@ +04:29:33.858 [tid 4476] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:29:33.858 [tid 4476] exe: C:\SOTS\Sword of the Stars.exe +04:29:33.858 [tid 4476] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=592 shim=696a0000 +04:29:33.858 [tid 4476] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:29:33.858 [tid 4476] config: hooks=trace +04:29:33.858 [tid 4476] config: hook.Shim::SelfTest::Fill=off +04:29:33.858 [tid 4476] config: hook.Mars::GlobalConsts::LoadFile=off +04:29:33.858 [tid 4476] config: hook.Game::WeaponDictionary::Init=off +04:29:33.858 [tid 4476] config: hook.Game::SectionDictionary::SectionDictionary=off +04:29:33.858 [tid 4476] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:29:33.858 [tid 4476] config: hook.Game::TechTree::ProcessResearch=trace +04:29:33.858 [tid 4476] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:29:33.858 [tid 4476] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:29:33.858 [tid 4476] config: hook.Game::ServerSystem::ProcessTurn=trace +04:29:33.858 [tid 4476] config: hook.Game::StrategyServer::MoveFleet=trace +04:29:33.858 [tid 4476] config: trace.path=C:\SOTS\shim.trace.jsonl +04:29:33.858 [tid 4476] config: trace.inline_max=256 +04:29:33.858 [tid 4476] config: trace.flush=always +04:29:33.858 [tid 4476] config: fpu.force=0x127f +04:29:33.858 [tid 4476] config: fpu.sample_ticks=on +04:29:33.920 [tid 4476] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:29:33.920 [tid 4476] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:29:33.920 [tid 4476] hook: MH_Initialize -> MH_OK +04:29:33.920 [tid 4476] hook: MH_CreateHook -> MH_OK (trampoline=00e60fe0) +04:29:33.936 [tid 4476] hook: MH_EnableHook -> MH_OK +04:29:33.936 [tid 4476] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:29:33.936 [tid 4476] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:29:33.936 [tid 4476] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:29:33.936 [tid 4476] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:29:33.936 [tid 4476] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:29:33.936 [tid 4476] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:29:33.936 [tid 4476] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=00e60fc0) +04:29:33.952 [tid 4476] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:29:33.952 [tid 4476] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:29:33.952 [tid 4476] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=00e60fa0) +04:29:33.967 [tid 4476] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:29:33.967 [tid 4476] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=00e60f80) +04:29:33.983 [tid 4476] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:29:33.983 [tid 4476] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=00e60f60) +04:29:33.998 [tid 4476] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:29:33.998 [tid 4476] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=00e60f40) +04:29:34.014 [tid 4476] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:29:34.014 [tid 4476] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:29:34.014 [tid 4476] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=on value=0x127f sample_ticks=on +04:29:34.014 [tid 4476] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=00e60f20) +04:29:34.014 [tid 4476] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:29:34.014 [tid 4476] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=00e60f00) +04:29:34.030 [tid 4476] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:29:34.030 [tid 4476] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=00e60ee0) +04:29:34.045 [tid 4476] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:29:34.045 [tid 4476] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=00e60ec0) +04:29:34.061 [tid 4476] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:29:34.061 [tid 4476] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:29:34.077 [tid 4476] Application::Initialize called (this=039a8128) +04:29:34.811 [tid 4476] fpu: TICK BASELINE at OnTick (this=039a8128): cw=0x127f 53bit-double/nearest +04:33:32.312 [tid 4476] fpu: FORCE at StrategyClient::EndTurn (this=0e6e6c30): observed=0x127f 53bit-double/nearest -> requested=0x127f -> readback=0x127f 53bit-double/nearest OK +04:33:35.375 [tid 4476] fpu: FORCE at StrategyClient::EndTurn (this=3503a4a0): observed=0x127f 53bit-double/nearest -> requested=0x127f -> readback=0x127f 53bit-double/nearest OK +04:33:35.390 [tid 4476] fpu: FORCE at StrategyClient::EndTurn (this=3503d050): observed=0x127f 53bit-double/nearest -> requested=0x127f -> readback=0x127f 53bit-double/nearest OK +04:33:35.422 [tid 4476] fpu: FORCE at StrategyClient::EndTurn (this=3503ed70): observed=0x127f 53bit-double/nearest -> requested=0x127f -> readback=0x127f 53bit-double/nearest OK +04:33:36.375 [tid 4476] fpu: FORCE at StrategyServer::BeginProcessTurn (this=0e705cd8): observed=0x127f 53bit-double/nearest -> requested=0x127f -> readback=0x127f 53bit-double/nearest OK +04:33:36.390 [tid 4476] fpu: sample at StrategyServer::ProcessTurn (this=0e705cd8): cw=0x127f 53bit-double/nearest diff --git a/verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz b/verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz new file mode 100644 index 0000000..6c7dd9d Binary files /dev/null and b/verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz differ diff --git a/verify/results/fpu-cw/run-137f/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-137f/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-137f/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-137f/(Autosave).sav b/verify/results/fpu-cw/run-137f/(Autosave).sav new file mode 100644 index 0000000..6b3e2e4 Binary files /dev/null and b/verify/results/fpu-cw/run-137f/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-137f/shim.cfg b/verify/results/fpu-cw/run-137f/shim.cfg new file mode 100644 index 0000000..d737879 --- /dev/null +++ b/verify/results/fpu-cw/run-137f/shim.cfg @@ -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/verify/results/fpu-cw/run-137f/shim.log b/verify/results/fpu-cw/run-137f/shim.log new file mode 100644 index 0000000..ab2e3cd --- /dev/null +++ b/verify/results/fpu-cw/run-137f/shim.log @@ -0,0 +1,61 @@ +04:34:28.562 [tid 7608] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:34:28.562 [tid 7608] exe: C:\SOTS\Sword of the Stars.exe +04:34:28.562 [tid 7608] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=3296 shim=696a0000 +04:34:28.562 [tid 7608] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:34:28.562 [tid 7608] config: hooks=trace +04:34:28.562 [tid 7608] config: hook.Shim::SelfTest::Fill=off +04:34:28.562 [tid 7608] config: hook.Mars::GlobalConsts::LoadFile=off +04:34:28.562 [tid 7608] config: hook.Game::WeaponDictionary::Init=off +04:34:28.562 [tid 7608] config: hook.Game::SectionDictionary::SectionDictionary=off +04:34:28.562 [tid 7608] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:34:28.562 [tid 7608] config: hook.Game::TechTree::ProcessResearch=trace +04:34:28.562 [tid 7608] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:34:28.562 [tid 7608] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:34:28.562 [tid 7608] config: hook.Game::ServerSystem::ProcessTurn=trace +04:34:28.562 [tid 7608] config: hook.Game::StrategyServer::MoveFleet=trace +04:34:28.562 [tid 7608] config: trace.path=C:\SOTS\shim.trace.jsonl +04:34:28.562 [tid 7608] config: trace.inline_max=256 +04:34:28.562 [tid 7608] config: trace.flush=always +04:34:28.562 [tid 7608] config: fpu.force=0x137f +04:34:28.562 [tid 7608] config: fpu.sample_ticks=on +04:34:28.640 [tid 7608] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:34:28.640 [tid 7608] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:34:28.640 [tid 7608] hook: MH_Initialize -> MH_OK +04:34:28.640 [tid 7608] hook: MH_CreateHook -> MH_OK (trampoline=003f0fe0) +04:34:28.672 [tid 7608] hook: MH_EnableHook -> MH_OK +04:34:28.672 [tid 7608] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:34:28.672 [tid 7608] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:34:28.672 [tid 7608] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:34:28.672 [tid 7608] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:34:28.672 [tid 7608] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:34:28.672 [tid 7608] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:34:28.672 [tid 7608] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=003f0fc0) +04:34:28.687 [tid 7608] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:34:28.687 [tid 7608] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:34:28.687 [tid 7608] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=003f0fa0) +04:34:28.703 [tid 7608] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:34:28.703 [tid 7608] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=003f0f80) +04:34:28.718 [tid 7608] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:34:28.718 [tid 7608] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=003f0f60) +04:34:28.718 [tid 7608] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:34:28.734 [tid 7608] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=003f0f40) +04:34:28.734 [tid 7608] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:34:28.734 [tid 7608] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:34:28.734 [tid 7608] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=on value=0x137f sample_ticks=on +04:34:28.734 [tid 7608] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=003f0f20) +04:34:28.750 [tid 7608] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:34:28.750 [tid 7608] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=003f0f00) +04:34:28.765 [tid 7608] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:34:28.765 [tid 7608] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=003f0ee0) +04:34:28.781 [tid 7608] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:34:28.781 [tid 7608] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=003f0ec0) +04:34:28.797 [tid 7608] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:34:28.797 [tid 7608] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:34:28.812 [tid 7608] Application::Initialize called (this=02da8128) +04:34:29.562 [tid 7608] fpu: TICK BASELINE at OnTick (this=02da8128): cw=0x127f 53bit-double/nearest +04:38:23.424 [tid 7608] fpu: FORCE at StrategyClient::EndTurn (this=0e02e4b0): observed=0x127f 53bit-double/nearest -> requested=0x137f -> readback=0x137f 64bit-extended/nearest OK +04:38:26.502 [tid 7608] fpu: FORCE at StrategyClient::EndTurn (this=338724a0): observed=0x127f 53bit-double/nearest -> requested=0x137f -> readback=0x137f 64bit-extended/nearest OK +04:38:26.534 [tid 7608] fpu: FORCE at StrategyClient::EndTurn (this=33876d70): observed=0x137f 64bit-extended/nearest -> requested=0x137f -> readback=0x137f 64bit-extended/nearest OK +04:38:26.565 [tid 7608] fpu: FORCE at StrategyClient::EndTurn (this=33871d58): observed=0x137f 64bit-extended/nearest -> requested=0x137f -> readback=0x137f 64bit-extended/nearest OK +04:38:27.534 [tid 7608] fpu: FORCE at StrategyServer::BeginProcessTurn (this=0df4cbf8): observed=0x127f 53bit-double/nearest -> requested=0x137f -> readback=0x137f 64bit-extended/nearest OK +04:38:27.534 [tid 7608] fpu: sample at StrategyServer::ProcessTurn (this=0df4cbf8): cw=0x137f 64bit-extended/nearest diff --git a/verify/results/fpu-cw/run-137f/shim.trace.jsonl.gz b/verify/results/fpu-cw/run-137f/shim.trace.jsonl.gz new file mode 100644 index 0000000..e612bf8 Binary files /dev/null and b/verify/results/fpu-cw/run-137f/shim.trace.jsonl.gz differ diff --git a/verify/results/fpu-cw/run-1a7f-rep/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-1a7f-rep/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-1a7f-rep/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-1a7f-rep/(Autosave).sav b/verify/results/fpu-cw/run-1a7f-rep/(Autosave).sav new file mode 100644 index 0000000..cc31046 Binary files /dev/null and b/verify/results/fpu-cw/run-1a7f-rep/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-1a7f-rep/shim.log b/verify/results/fpu-cw/run-1a7f-rep/shim.log new file mode 100644 index 0000000..6330098 --- /dev/null +++ b/verify/results/fpu-cw/run-1a7f-rep/shim.log @@ -0,0 +1,61 @@ +04:52:25.866 [tid 8672] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:52:25.866 [tid 8672] exe: C:\SOTS\Sword of the Stars.exe +04:52:25.866 [tid 8672] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=5980 shim=696a0000 +04:52:25.866 [tid 8672] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:52:25.866 [tid 8672] config: hooks=trace +04:52:25.866 [tid 8672] config: hook.Shim::SelfTest::Fill=off +04:52:25.866 [tid 8672] config: hook.Mars::GlobalConsts::LoadFile=off +04:52:25.866 [tid 8672] config: hook.Game::WeaponDictionary::Init=off +04:52:25.866 [tid 8672] config: hook.Game::SectionDictionary::SectionDictionary=off +04:52:25.866 [tid 8672] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:52:25.866 [tid 8672] config: hook.Game::TechTree::ProcessResearch=trace +04:52:25.866 [tid 8672] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:52:25.866 [tid 8672] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:52:25.866 [tid 8672] config: hook.Game::ServerSystem::ProcessTurn=trace +04:52:25.866 [tid 8672] config: hook.Game::StrategyServer::MoveFleet=trace +04:52:25.866 [tid 8672] config: trace.path=C:\SOTS\shim.trace.jsonl +04:52:25.866 [tid 8672] config: trace.inline_max=256 +04:52:25.866 [tid 8672] config: trace.flush=always +04:52:25.866 [tid 8672] config: fpu.force=0x1a7f +04:52:25.866 [tid 8672] config: fpu.sample_ticks=on +04:52:25.929 [tid 8672] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:52:25.929 [tid 8672] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:52:25.929 [tid 8672] hook: MH_Initialize -> MH_OK +04:52:25.929 [tid 8672] hook: MH_CreateHook -> MH_OK (trampoline=009e0fe0) +04:52:25.944 [tid 8672] hook: MH_EnableHook -> MH_OK +04:52:25.944 [tid 8672] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:52:25.944 [tid 8672] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:52:25.944 [tid 8672] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:52:25.944 [tid 8672] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:52:25.944 [tid 8672] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:52:25.944 [tid 8672] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:52:25.944 [tid 8672] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=009e0fc0) +04:52:25.960 [tid 8672] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:52:25.960 [tid 8672] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:52:25.960 [tid 8672] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=009e0fa0) +04:52:25.976 [tid 8672] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:52:25.976 [tid 8672] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=009e0f80) +04:52:25.976 [tid 8672] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:52:25.976 [tid 8672] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=009e0f60) +04:52:25.991 [tid 8672] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:52:25.991 [tid 8672] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=009e0f40) +04:52:26.007 [tid 8672] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:52:26.007 [tid 8672] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:52:26.007 [tid 8672] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=on value=0x1a7f sample_ticks=on +04:52:26.007 [tid 8672] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=009e0f20) +04:52:26.023 [tid 8672] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:52:26.023 [tid 8672] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=009e0f00) +04:52:26.038 [tid 8672] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:52:26.038 [tid 8672] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=009e0ee0) +04:52:26.054 [tid 8672] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:52:26.054 [tid 8672] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=009e0ec0) +04:52:26.069 [tid 8672] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:52:26.069 [tid 8672] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:52:26.085 [tid 8672] Application::Initialize called (this=03748128) +04:52:26.851 [tid 8672] fpu: TICK BASELINE at OnTick (this=03748128): cw=0x127f 53bit-double/nearest +04:56:21.683 [tid 8672] fpu: FORCE at StrategyClient::EndTurn (this=0e3f2ee8): observed=0x127f 53bit-double/nearest -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:56:24.746 [tid 8672] fpu: FORCE at StrategyClient::EndTurn (this=354f8628): observed=0x127f 53bit-double/nearest -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:56:24.777 [tid 8672] fpu: FORCE at StrategyClient::EndTurn (this=354f4be8): observed=0x1a7f 53bit-double/up -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:56:24.793 [tid 8672] fpu: FORCE at StrategyClient::EndTurn (this=354f6908): observed=0x1a7f 53bit-double/up -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:56:25.761 [tid 8672] fpu: FORCE at StrategyServer::BeginProcessTurn (this=0e3e65a8): observed=0x127f 53bit-double/nearest -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:56:25.761 [tid 8672] fpu: sample at StrategyServer::ProcessTurn (this=0e3e65a8): cw=0x1a7f 53bit-double/up diff --git a/verify/results/fpu-cw/run-1a7f/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-1a7f/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-1a7f/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-1a7f/(Autosave).sav b/verify/results/fpu-cw/run-1a7f/(Autosave).sav new file mode 100644 index 0000000..cc31046 Binary files /dev/null and b/verify/results/fpu-cw/run-1a7f/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-1a7f/shim.cfg b/verify/results/fpu-cw/run-1a7f/shim.cfg new file mode 100644 index 0000000..9530f21 --- /dev/null +++ b/verify/results/fpu-cw/run-1a7f/shim.cfg @@ -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/verify/results/fpu-cw/run-1a7f/shim.log b/verify/results/fpu-cw/run-1a7f/shim.log new file mode 100644 index 0000000..d8dcf2c --- /dev/null +++ b/verify/results/fpu-cw/run-1a7f/shim.log @@ -0,0 +1,61 @@ +04:39:19.877 [tid 8716] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:39:19.877 [tid 8716] exe: C:\SOTS\Sword of the Stars.exe +04:39:19.877 [tid 8716] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=8004 shim=696a0000 +04:39:19.877 [tid 8716] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:39:19.877 [tid 8716] config: hooks=trace +04:39:19.877 [tid 8716] config: hook.Shim::SelfTest::Fill=off +04:39:19.877 [tid 8716] config: hook.Mars::GlobalConsts::LoadFile=off +04:39:19.877 [tid 8716] config: hook.Game::WeaponDictionary::Init=off +04:39:19.877 [tid 8716] config: hook.Game::SectionDictionary::SectionDictionary=off +04:39:19.877 [tid 8716] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:39:19.877 [tid 8716] config: hook.Game::TechTree::ProcessResearch=trace +04:39:19.877 [tid 8716] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:39:19.877 [tid 8716] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:39:19.877 [tid 8716] config: hook.Game::ServerSystem::ProcessTurn=trace +04:39:19.893 [tid 8716] config: hook.Game::StrategyServer::MoveFleet=trace +04:39:19.893 [tid 8716] config: trace.path=C:\SOTS\shim.trace.jsonl +04:39:19.893 [tid 8716] config: trace.inline_max=256 +04:39:19.893 [tid 8716] config: trace.flush=always +04:39:19.893 [tid 8716] config: fpu.force=0x1a7f +04:39:19.893 [tid 8716] config: fpu.sample_ticks=on +04:39:19.940 [tid 8716] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:39:19.940 [tid 8716] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:39:19.940 [tid 8716] hook: MH_Initialize -> MH_OK +04:39:19.940 [tid 8716] hook: MH_CreateHook -> MH_OK (trampoline=009b0fe0) +04:39:19.956 [tid 8716] hook: MH_EnableHook -> MH_OK +04:39:19.956 [tid 8716] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:39:19.956 [tid 8716] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:39:19.956 [tid 8716] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:39:19.956 [tid 8716] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:39:19.956 [tid 8716] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:39:19.956 [tid 8716] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:39:19.956 [tid 8716] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=009b0fc0) +04:39:19.971 [tid 8716] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:39:19.971 [tid 8716] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:39:19.971 [tid 8716] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=009b0fa0) +04:39:19.987 [tid 8716] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:39:19.987 [tid 8716] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=009b0f80) +04:39:20.002 [tid 8716] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:39:20.002 [tid 8716] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=009b0f60) +04:39:20.018 [tid 8716] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:39:20.018 [tid 8716] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=009b0f40) +04:39:20.034 [tid 8716] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:39:20.034 [tid 8716] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:39:20.034 [tid 8716] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=on value=0x1a7f sample_ticks=on +04:39:20.034 [tid 8716] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=009b0f20) +04:39:20.049 [tid 8716] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:39:20.049 [tid 8716] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=009b0f00) +04:39:20.065 [tid 8716] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:39:20.065 [tid 8716] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=009b0ee0) +04:39:20.081 [tid 8716] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:39:20.081 [tid 8716] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=009b0ec0) +04:39:20.096 [tid 8716] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:39:20.096 [tid 8716] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:39:20.112 [tid 8716] Application::Initialize called (this=03258128) +04:39:20.862 [tid 8716] fpu: TICK BASELINE at OnTick (this=03258128): cw=0x127f 53bit-double/nearest +04:43:18.459 [tid 8716] fpu: FORCE at StrategyClient::EndTurn (this=0e1fac88): observed=0x127f 53bit-double/nearest -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:43:21.537 [tid 8716] fpu: FORCE at StrategyClient::EndTurn (this=33ad9ee0): observed=0x127f 53bit-double/nearest -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:43:21.553 [tid 8716] fpu: FORCE at StrategyClient::EndTurn (this=33ad7330): observed=0x1a7f 53bit-double/up -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:43:21.584 [tid 8716] fpu: FORCE at StrategyClient::EndTurn (this=33ad7a78): observed=0x1a7f 53bit-double/up -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:43:22.553 [tid 8716] fpu: FORCE at StrategyServer::BeginProcessTurn (this=0e1878a0): observed=0x127f 53bit-double/nearest -> requested=0x1a7f -> readback=0x1a7f 53bit-double/up OK +04:43:22.553 [tid 8716] fpu: sample at StrategyServer::ProcessTurn (this=0e1878a0): cw=0x1a7f 53bit-double/up diff --git a/verify/results/fpu-cw/run-1a7f/shim.trace.jsonl.gz b/verify/results/fpu-cw/run-1a7f/shim.trace.jsonl.gz new file mode 100644 index 0000000..b071fd2 Binary files /dev/null and b/verify/results/fpu-cw/run-1a7f/shim.trace.jsonl.gz differ diff --git a/verify/results/fpu-cw/run-off/(Autosave EndTurn).sav b/verify/results/fpu-cw/run-off/(Autosave EndTurn).sav new file mode 100644 index 0000000..c264263 Binary files /dev/null and b/verify/results/fpu-cw/run-off/(Autosave EndTurn).sav differ diff --git a/verify/results/fpu-cw/run-off/(Autosave).sav b/verify/results/fpu-cw/run-off/(Autosave).sav new file mode 100644 index 0000000..6b3e2e4 Binary files /dev/null and b/verify/results/fpu-cw/run-off/(Autosave).sav differ diff --git a/verify/results/fpu-cw/run-off/shim.cfg b/verify/results/fpu-cw/run-off/shim.cfg new file mode 100644 index 0000000..f823952 --- /dev/null +++ b/verify/results/fpu-cw/run-off/shim.cfg @@ -0,0 +1,24 @@ +# Lane F, x87 precision-sensitivity experiment. STOCK: no forcing. Oracle re-confirmation + the game's own control-word timeline. +# Identical to every other shim.cfg.fpu* 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. +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 + +# x87 control word forced once at the turn gate (StrategyClient::EndTurn and +# StrategyServer::BeginProcessTurn), never re-forced inside the pipeline. +fpu.force=off +# Per-tick sampler: logs the control word to shim.log whenever it MOVES, so the log carries a +# timeline of the value rather than a single claim. +fpu.sample_ticks=on diff --git a/verify/results/fpu-cw/run-off/shim.log b/verify/results/fpu-cw/run-off/shim.log new file mode 100644 index 0000000..32593ef --- /dev/null +++ b/verify/results/fpu-cw/run-off/shim.log @@ -0,0 +1,61 @@ +04:06:40.848 [tid 2040] ==== sots-engine shim (binkw32 proxy) build fpucw-cef889e-20260908T0803Z ==== +04:06:40.848 [tid 2040] exe: C:\SOTS\Sword of the Stars.exe +04:06:40.848 [tid 2040] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=8848 shim=696a0000 +04:06:40.848 [tid 2040] addresses: Source: sots-re ghidra/addresses.json @ f5b37c2, generated 2026-09-08 by tools/gen_addresses.py +04:06:40.848 [tid 2040] config: hooks=trace +04:06:40.848 [tid 2040] config: hook.Shim::SelfTest::Fill=off +04:06:40.848 [tid 2040] config: hook.Mars::GlobalConsts::LoadFile=off +04:06:40.848 [tid 2040] config: hook.Game::WeaponDictionary::Init=off +04:06:40.848 [tid 2040] config: hook.Game::SectionDictionary::SectionDictionary=off +04:06:40.848 [tid 2040] config: hook.Game::StrategyServer::ProcessFleetMovement=off +04:06:40.848 [tid 2040] config: hook.Game::TechTree::ProcessResearch=trace +04:06:40.848 [tid 2040] config: hook.Game::ServerPlayer::ComputeBudget=trace +04:06:40.848 [tid 2040] config: hook.Game::ServerPlayer::OnTechResearched=trace +04:06:40.848 [tid 2040] config: hook.Game::ServerSystem::ProcessTurn=trace +04:06:40.848 [tid 2040] config: hook.Game::StrategyServer::MoveFleet=trace +04:06:40.848 [tid 2040] config: trace.path=C:\SOTS\shim.trace.jsonl +04:06:40.848 [tid 2040] config: trace.inline_max=256 +04:06:40.848 [tid 2040] config: trace.flush=always +04:06:40.848 [tid 2040] config: fpu.force=off +04:06:40.848 [tid 2040] config: fpu.sample_ticks=on +04:06:40.910 [tid 2040] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always) +04:06:40.910 [tid 2040] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50 +04:06:40.910 [tid 2040] hook: MH_Initialize -> MH_OK +04:06:40.910 [tid 2040] hook: MH_CreateHook -> MH_OK (trampoline=00940fe0) +04:06:40.926 [tid 2040] hook: MH_EnableHook -> MH_OK +04:06:40.926 [tid 2040] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295) +04:06:40.926 [tid 2040] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed) +04:06:40.926 [tid 2040] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174) +04:06:40.926 [tid 2040] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed) +04:06:40.926 [tid 2040] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed) +04:06:40.926 [tid 2040] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f) +04:06:40.926 [tid 2040] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=00940fc0) +04:06:40.941 [tid 2040] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=trace +04:06:40.941 [tid 2040] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f) +04:06:40.941 [tid 2040] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 -> va=01311790 MH_CreateHook -> MH_OK (trampoline=00940fa0) +04:06:40.957 [tid 2040] hook: Game::ServerPlayer::OnTechResearched MH_EnableHook -> MH_OK mode=trace +04:06:40.957 [tid 2040] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 -> va=012e3030 MH_CreateHook -> MH_OK (trampoline=00940f80) +04:06:40.973 [tid 2040] hook: Game::ServerPlayer::ComputeBudget MH_EnableHook -> MH_OK mode=trace +04:06:40.973 [tid 2040] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 -> va=011d98e0 MH_CreateHook -> MH_OK (trampoline=00940f60) +04:06:40.988 [tid 2040] hook: Game::ServerSystem::ProcessTurn MH_EnableHook -> MH_OK mode=trace +04:06:40.988 [tid 2040] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 -> va=01259ee0 MH_CreateHook -> MH_OK (trampoline=00940f40) +04:06:41.004 [tid 2040] hook: Game::StrategyServer::MoveFleet MH_EnableHook -> MH_OK mode=trace +04:06:41.004 [tid 2040] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed) +04:06:41.004 [tid 2040] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=on +04:06:41.004 [tid 2040] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=00940f20) +04:06:41.020 [tid 2040] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK +04:06:41.020 [tid 2040] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=00940f00) +04:06:41.035 [tid 2040] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK +04:06:41.035 [tid 2040] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=00940ee0) +04:06:41.051 [tid 2040] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK +04:06:41.051 [tid 2040] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=00940ec0) +04:06:41.066 [tid 2040] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK +04:06:41.066 [tid 2040] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0 +04:06:41.082 [tid 2040] Application::Initialize called (this=03248128) +04:06:41.832 [tid 2040] fpu: TICK BASELINE at OnTick (this=03248128): cw=0x127f 53bit-double/nearest +04:12:04.617 [tid 2040] fpu: sample at StrategyClient::EndTurn (this=0e19c4b8): cw=0x127f 53bit-double/nearest [no fpu.force configured] +04:12:07.695 [tid 2040] fpu: sample at StrategyClient::EndTurn (this=34b76be0): cw=0x127f 53bit-double/nearest [no fpu.force configured] +04:12:07.726 [tid 2040] fpu: sample at StrategyClient::EndTurn (this=34b79048): cw=0x127f 53bit-double/nearest [no fpu.force configured] +04:12:07.757 [tid 2040] fpu: sample at StrategyClient::EndTurn (this=34b77a70): cw=0x127f 53bit-double/nearest [no fpu.force configured] +04:12:08.710 [tid 2040] fpu: sample at StrategyServer::BeginProcessTurn (this=0e1bf750): cw=0x127f 53bit-double/nearest [no fpu.force configured] +04:12:08.710 [tid 2040] fpu: sample at StrategyServer::ProcessTurn (this=0e1bf750): cw=0x127f 53bit-double/nearest diff --git a/verify/results/fpu-cw/run-off/shim.trace.jsonl.gz b/verify/results/fpu-cw/run-off/shim.trace.jsonl.gz new file mode 100644 index 0000000..cfe2986 Binary files /dev/null and b/verify/results/fpu-cw/run-off/shim.trace.jsonl.gz differ diff --git a/verify/results/fpu-cw/state-checksum-diffs.txt b/verify/results/fpu-cw/state-checksum-diffs.txt new file mode 100644 index 0000000..f2d515b --- /dev/null +++ b/verify/results/fpu-cw/state-checksum-diffs.txt @@ -0,0 +1,50 @@ +===== state_checksum: run-127f vs run-off (post-turn autosave) ===== +A 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-127f/(Autosave).sav +B 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-off/(Autosave).sav +policy: floats=bits mask=none reader=fe5a6f7cd4ae7910 +IDENTICAL + +===== state_checksum: run-127f vs run-007f (post-turn autosave) ===== +A 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-127f/(Autosave).sav +B 222b03e932f684d5d61f54c155be4f36 ../results/fpu-cw/run-007f/(Autosave).sav +policy: floats=bits mask=none reader=fe5a6f7cd4ae7910 +DIVERGED: 2 leaf difference(s) + /Summary/Checksum: -769976634 -> -769976632 + /Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC: 540000000 -> 540000002 + +===== state_checksum: run-127f vs run-027f (post-turn autosave) ===== +A 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-127f/(Autosave).sav +B 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-027f/(Autosave).sav +policy: floats=bits mask=none reader=fe5a6f7cd4ae7910 +IDENTICAL + +===== state_checksum: run-127f vs run-137f (post-turn autosave) ===== +A 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-127f/(Autosave).sav +B 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-137f/(Autosave).sav +policy: floats=bits mask=none reader=fe5a6f7cd4ae7910 +IDENTICAL + +===== state_checksum: run-127f vs run-1a7f (post-turn autosave) ===== +A 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-127f/(Autosave).sav +B e86df4556578665ce1b4cdf7a77d22a7 ../results/fpu-cw/run-1a7f/(Autosave).sav +policy: floats=bits mask=none reader=fe5a6f7cd4ae7910 +DIVERGED: 2 leaf difference(s) + /Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0]: -10.563499450683594 -> -10.563498497009277 [1 ulp] + /Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[2]: 1.564733624458313 -> 1.5647337436676025 [1 ulp] + +===== state_checksum: run-127f vs run-007f-rep (post-turn autosave) ===== +A 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-127f/(Autosave).sav +B 222b03e932f684d5d61f54c155be4f36 ../results/fpu-cw/run-007f-rep/(Autosave).sav +policy: floats=bits mask=none reader=fe5a6f7cd4ae7910 +DIVERGED: 2 leaf difference(s) + /Summary/Checksum: -769976634 -> -769976632 + /Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC: 540000000 -> 540000002 + +===== state_checksum: run-127f vs run-1a7f-rep (post-turn autosave) ===== +A 5ac4a24197e82de49f3077cd8dd25fad ../results/fpu-cw/run-127f/(Autosave).sav +B e86df4556578665ce1b4cdf7a77d22a7 ../results/fpu-cw/run-1a7f-rep/(Autosave).sav +policy: floats=bits mask=none reader=fe5a6f7cd4ae7910 +DIVERGED: 2 leaf difference(s) + /Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0]: -10.563499450683594 -> -10.563498497009277 [1 ulp] + /Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[2]: 1.564733624458313 -> 1.5647337436676025 [1 ulp] + diff --git a/verify/results/fpu-cw/trace-bitdiff.txt b/verify/results/fpu-cw/trace-bitdiff.txt new file mode 100644 index 0000000..47b8a65 --- /dev/null +++ b/verify/results/fpu-cw/trace-bitdiff.txt @@ -0,0 +1,50 @@ +===== 127f vs off ===== +A verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz +B verify/results/fpu-cw/run-off/shim.trace.jsonl.gz +Game::ServerSystem::ProcessTurn: 6 leaf difference(s) over 28 record(s) + rec[4] .coverage.n: 3 -> 1 + rec[4] .coverage.undeclared: [{'region': 'system', 'off': 420, 'len': 3}, {'region': 'system', 'off': 424, 'len': 3}, {'region': 'system', 'off': 428, 'len': 3}] -> [{'region': 'system', 'off': 420, 'len': 12}] + rec[15] .coverage.n: 6 -> 4 + rec[15] .coverage.undeclared: [{'region': 'system', 'off': 216, 'len': 1}, {'region': 'system', 'off': 368, 'len': 1}, {'region': 'system', 'off': 420, 'len': 3}, {'region': 'system', 'off': 424, 'len': 3}, {'region': 'system', 'off': 428, 'len': 3}, {'region': 'system', 'off': 568, 'len': 4}] -> [{'region': 'system', 'off': 216, 'len': 1}, {'region': 'system', 'off': 368, 'len': 1}, {'region': 'system', 'off': 420, 'len': 12}, {'region': 'system', 'off': 568, 'len': 4}] + rec[16] .coverage.n: 3 -> 1 + rec[16] .coverage.undeclared: [{'region': 'system', 'off': 420, 'len': 3}, {'region': 'system', 'off': 424, 'len': 3}, {'region': 'system', 'off': 428, 'len': 3}] -> [{'region': 'system', 'off': 420, 'len': 12}] +Game::StrategyServer::MoveFleet: 0 leaf difference(s) over 7 record(s) +Game::TechTree::ProcessResearch: 0 leaf difference(s) over 3 record(s) +TOTAL: 6 + +===== 127f vs 007f ===== +A verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz +B verify/results/fpu-cw/run-007f/shim.trace.jsonl.gz +Game::ServerSystem::ProcessTurn: 0 leaf difference(s) over 28 record(s) +Game::StrategyServer::MoveFleet: 0 leaf difference(s) over 7 record(s) +Game::TechTree::ProcessResearch: 0 leaf difference(s) over 3 record(s) +TOTAL: 0 + +===== 127f vs 027f ===== +A verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz +B verify/results/fpu-cw/run-027f/shim.trace.jsonl.gz +Game::ServerSystem::ProcessTurn: 2 leaf difference(s) over 28 record(s) + rec[15] .coverage.undeclared[2].len: 3 -> 2 + rec[15] .coverage.undeclared[2].off: 420 -> 421 +Game::StrategyServer::MoveFleet: 0 leaf difference(s) over 7 record(s) +Game::TechTree::ProcessResearch: 0 leaf difference(s) over 3 record(s) +TOTAL: 2 + +===== 127f vs 137f ===== +A verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz +B verify/results/fpu-cw/run-137f/shim.trace.jsonl.gz +Game::ServerSystem::ProcessTurn: 0 leaf difference(s) over 28 record(s) +Game::StrategyServer::MoveFleet: 0 leaf difference(s) over 7 record(s) +Game::TechTree::ProcessResearch: 0 leaf difference(s) over 3 record(s) +TOTAL: 0 + +===== 127f vs 1a7f ===== +A verify/results/fpu-cw/run-127f/shim.trace.jsonl.gz +B verify/results/fpu-cw/run-1a7f/shim.trace.jsonl.gz +Game::ServerSystem::ProcessTurn: 0 leaf difference(s) over 28 record(s) +Game::StrategyServer::MoveFleet: 2 leaf difference(s) over 7 record(s) + rec[6] .side.pos.after.v.x.bits: 3240690712 -> 3240690711 + rec[6] .side.pos.after.v.z.bits: 1070090545 -> 1070090546 +Game::TechTree::ProcessResearch: 0 leaf difference(s) over 3 record(s) +TOTAL: 2 + diff --git a/verify/state-checksum/STATE_CHECKSUM.md b/verify/state-checksum/STATE_CHECKSUM.md index 9ae76be..fac2cf5 100644 --- a/verify/state-checksum/STATE_CHECKSUM.md +++ b/verify/state-checksum/STATE_CHECKSUM.md @@ -267,7 +267,7 @@ glance that all four are genuine simulation changes (tens of thousands of ULPs growing over a turn), not float-path noise. Had a port produced `[1 ulp]` on these instead, the same line would say so and the judgement call would be an explicit one. -### 3.5 The x64/SSE budget, and the one thing this cannot settle +### 3.5 The x64/SSE budget — MEASURED 2026-09-08, no budget needed x87 with PC=53 rounds an intermediate to double and then to float32 — **double rounding**. SSE `mulss`/`addss` rounds once, directly to float32. For a minority of inputs those differ by one @@ -281,28 +281,43 @@ double rounding (compute in double, narrow explicitly at each store — which is `fpu_cw = 0x127f` makes the original do) or to accept a documented `--ulps` budget on a named list of fields. -**What I cannot settle from the host side:** whether the turn pipeline's results depend on the -x87 intermediate precision at all. Every save on this host was produced with `fpu_cw = 0x127f`, -so the corpus is one point, not a curve. It is possible that every value in the turn pipeline -is computed in a way that gives the same float32 under 24-bit, 53-bit and 64-bit precision -control, in which case an SSE port has **no** double-rounding budget to spend and the strict -policy is free forever. It is equally possible that a handful of fields are precision-sensitive. +**SETTLED on the VM, 2026-09-08 (lane F).** It was not settleable from the host side — every +save in the corpus was made at `fpu_cw = 0x127f`, one point rather than a curve — so lane F ran +the End Turn seven times from `ref-turn2.sav` with the shim forcing the control word, and +checksummed the results with this tool. Full write-up and the evidence chain that the setting +actually *held* (38 independent in-pipeline samples per run): +`findings/subsystems/fpu-precision-sensitivity.md`; artefacts in +`verify/results/fpu-cw/`. -**The experiment that would settle it** (VM140, lane R): +| forced `fpu_cw` | precision / rounding | root vs baseline | +|---|---|---| +| stock, `0x027f`, `0x127f`, `0x137f` | 53-bit and **64-bit**, nearest | **identical**, all 35,394 leaves | +| `0x007f` | **24-bit**, nearest | 1 leaf + derived `Summary/Checksum` | +| `0x1a7f` | 53-bit, **round-up** | 2 leaves, 1 ULP each | + +**The answer for §3, in one line: 53-bit and 64-bit x87 give the same state, so an SSE port +computing in IEEE `double` has no double-rounding budget to preserve and `floats=bits` is free.** +What is *not* free is the two things the other two runs found: + +``` +0x007f (24-bit): /Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC : 540000000 -> 540000002 +0x1a7f (round-up):/Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0] and /Pos/.[2] : 1 ulp each +``` + +So the port must (a) hold intermediates at 53 bits and narrow to float32 only where the original +stores to a `dword` — never compute a chain in `float` — and (b) use round-to-nearest. Both are +SSE defaults; they are now measured requirements rather than assumptions, each with a named +regression witness on turn 2 of `ref-turn2.sav`. + +**Correction to the experiment as it was written here.** The three values proposed above span +only *two* FPU modes. `0x027f` is 53-bit — it differs from `0x127f` only in bit 12 (infinity +control), which every x87 since the 387 ignores — and `0x137f` is 64-bit extended, not a +rounding change. Precision control is bits 8–9 (`00`=24, `10`=53, `11`=64) and rounding control +is bits 10–11. The genuine single-precision word is `0x007f` and a genuine rounding change is +`0x1a7f`; run as originally specified, all three settings come back identical and the tempting +conclusion — "nothing depends on the control word" — would have been wrong on both of the axes +that actually matter. -> Load `ref-turn2.sav`, press End Turn, and capture `(Autosave).sav` three times, with the shim -> forcing `fpu_cw` to `0x027f` (24-bit), `0x127f` (53-bit, the baseline) and `0x137f` (64-bit) -> across the turn call. Then run -> `state_checksum.py baseline.sav variant.sav` on each pair. -> -> * All three roots equal → no turn-pipeline value depends on x87 intermediate precision. The -> strict policy costs the SSE port nothing, and §3.5 can be closed. -> * Roots differ → the diff *is* the answer: it names every precision-sensitive field, and that -> list becomes the port's work item and the only place a `--ulps` budget is ever justified. -> -> Cheap, because the oracle is already byte-identical and the tool already localises. It needs -> nothing from this lane; it needs a shim knob that sets the control word around the turn call -> and the existing End-Turn click path from §5. Until that runs, "the checksum is exact and the port must match bit-for-bit" is a *policy*, not a measured requirement. It is the right default either way — it fails loudly rather than