Compare commits
No commits in common. "d4f868f823f3cd2847fdc721906906c697ac3048" and "d523d27f50495c8e7c55aea1bbcef77aca5c591d" have entirely different histories.
d4f868f823
...
d523d27f50
55 changed files with 90 additions and 1852 deletions
|
|
@ -1,20 +1,20 @@
|
|||
# SotS RE campaign — coverage dashboard
|
||||
|
||||
Generated 2026-09-08 09:10 UTC · `sots-re` @ f965c8c,2026-09-08 · `sots-engine` @ f0cc2a9,2026-09-08 (81 commits) · regenerate with `tools/dashboard.py`
|
||||
Generated 2026-09-08 07:47 UTC · `sots-re` @ fdd0b72,2026-09-08 · `sots-engine` @ 45bdf7d,2026-09-08 (74 commits) · regenerate with `tools/dashboard.py`
|
||||
|
||||
> **North star:** A functional reimplementation of the engine — behavior-equivalent, NOT byte-for-byte
|
||||
|
||||
## 1. Map coverage (campaign/board.md)
|
||||
|
||||
87 targets · mapped-or-better **67/87** `[████████░░] 77%` · verified **47/87** `[█████░░░░░] 54%`
|
||||
75 targets · mapped-or-better **55/75** `[███████░░░] 73%` · verified **36/75** `[█████░░░░░] 48%`
|
||||
|
||||
| Status | Count | % |
|
||||
|---|---:|---:|
|
||||
| verified | 47 | 54% |
|
||||
| mapped | 20 | 23% |
|
||||
| in-progress | 2 | 2% |
|
||||
| backlog | 16 | 18% |
|
||||
| blocked | 2 | 2% |
|
||||
| verified | 36 | 48% |
|
||||
| mapped | 19 | 25% |
|
||||
| in-progress | 3 | 4% |
|
||||
| backlog | 15 | 20% |
|
||||
| blocked | 2 | 3% |
|
||||
|
||||
| Type | verified | mapped | in-progress | backlog | blocked | total |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
|
|
@ -22,16 +22,16 @@ Generated 2026-09-08 09:10 UTC · `sots-re` @ f965c8c,2026-09-08 · `sots-engine
|
|||
| control-flow | 0 | 2 | 0 | 0 | 0 | 2 |
|
||||
| subsystems | 2 | 7 | 1 | 3 | 1 | 14 |
|
||||
| engine | 10 | 0 | 0 | 0 | 0 | 10 |
|
||||
| verify | 11 | 2 | 0 | 10 | 0 | 23 |
|
||||
| phase2 | 6 | 3 | 1 | 2 | 0 | 12 |
|
||||
| meta | 10 | 4 | 0 | 1 | 0 | 15 |
|
||||
| other | 3 | 0 | 0 | 0 | 1 | 4 |
|
||||
| verify | 8 | 2 | 0 | 9 | 0 | 19 |
|
||||
| phase2 | 5 | 2 | 2 | 2 | 0 | 11 |
|
||||
| meta | 6 | 4 | 0 | 1 | 0 | 11 |
|
||||
| other | 0 | 0 | 0 | 0 | 1 | 1 |
|
||||
|
||||
## 2. Binary understanding
|
||||
|
||||
- RTTI type descriptors: **1,924** (`Game::` 1,404, `Mars::` 194; serializable types 179)
|
||||
- Classes with recovered member layouts: **22** / 179 serializable types `[█░░░░░░░░░] 12%` — heuristic: distinct `Game::X`/`Mars::X` in `##`–`####` headings of `struct-recovery.md` + `schema-gaps-resolved.md`
|
||||
- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **414**, verified **406** `[██████████] 98%`
|
||||
- Classes with recovered member layouts: **21** / 179 serializable types `[█░░░░░░░░░] 12%` — heuristic: distinct `Game::X`/`Mars::X` in `##`–`####` headings of `struct-recovery.md` + `schema-gaps-resolved.md`
|
||||
- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **386**, verified **379** `[██████████] 98%`
|
||||
|
||||
## 3. Data layer
|
||||
|
||||
|
|
@ -49,17 +49,17 @@ Generated 2026-09-08 09:10 UTC · `sots-re` @ f965c8c,2026-09-08 · `sots-engine
|
|||
| `game/data` | 2,053 | 12 | 462 | yes | game-data.md |
|
||||
| `game/design` | 1,024 | 17 | 250 | yes | game-design.md |
|
||||
| `game/effects` | 973 | 3 | 231 | yes | game-effects.md |
|
||||
| `game/events` | 558 | 3 | 152 | yes | E-events.md |
|
||||
| `game/sim` | 2,720 | 9 | 611 | yes | game-sim.md |
|
||||
| `game/events` | 451 | 3 | 112 | yes | E-events.md |
|
||||
| `game/sim` | 2,637 | 9 | 599 | yes | game-sim.md |
|
||||
| `mars/parse` | 875 | 12 | 277 | yes | mars-parse.md |
|
||||
| `mars/rng` | 206 | 0 | 0 | yes | mars-rng.md |
|
||||
| `mars/stream` | 3,703 | 6 | 222 | yes | mars-stream.md |
|
||||
| `mars/text` | 899 | 8 | 245 | yes | mars-text.md |
|
||||
| `mars/vfs` | 788 | 9 | 140 | yes | mars-vfs.md |
|
||||
| `shim` | 8,120 | 0 | 0 | direct (WIN32) | M0.md |
|
||||
| `shim/hooks` | 5,550 | 0 | 0 | direct (WIN32) | M0.md |
|
||||
| `shim` | 7,620 | 0 | 0 | direct (WIN32) | M0.md |
|
||||
| `shim/hooks` | 5,050 | 0 | 0 | direct (WIN32) | M0.md |
|
||||
| `shim/trace` | 2,258 | 9 | 273 | direct (WIN32) | shim-trace.md |
|
||||
| **total** | **30,285** | **92** | **2994** | | |
|
||||
| **total** | **29,095** | **92** | **2942** | | |
|
||||
|
||||
Board `engine:` rows: verified **10**, mapped 0, in flight 0 (of 10) — verified & merged `[██████████] 100%`
|
||||
|
||||
|
|
@ -84,22 +84,22 @@ Board `engine:` rows: verified **10**, mapped 0, in flight 0 (of 10) — verifie
|
|||
|
||||
## 7. Open questions
|
||||
|
||||
Open **26** · resolved/parked 11 · backlog items: Now 4, Next 3, Later 2, Breadth queue 7, Parked 1, From the RE how-to 4, Behavioral slice 4
|
||||
Open **27** · resolved/parked 10 · backlog items: Now 4, Next 3, Later 2, Breadth queue 7, Parked 1, From the RE how-to 4, Behavioral slice 4
|
||||
|
||||
Most recent open:
|
||||
|
||||
- Some truths are unreachable by compare — B3's draw-divisor bug differed on 0.78% of draws yet flip…
|
||||
- x87 precision-control mode at runtime — 53-bit MSVC default vs 24-bit if D3D9 grabbed the FPU; mov…
|
||||
- Struct-modelling hazard (found by M2) — an MSVC-2010 `std::vector` member is three words, so a…
|
||||
- Notes disagree on MSVC-2010 `std::string` layout — `struct-recovery.md` §0 vs `turn-spine.md` §1.1…
|
||||
- SAVE_FORMAT tag corrections (fix Python reader + spec) — real on-disk tags: `otnF` (not `ontF`) in…
|
||||
- Not traced end-to-end — `Species/_NPC/weapons/*.weapon` loading and the `.effect` dictionary entry…
|
||||
|
||||
## 8. Delta since previous dashboard
|
||||
|
||||
- verified targets: 36 → 47 (+11) · mapped-or-better: 55 → 67 (+12)
|
||||
- engine LOC: 29,095 → 30,285 (+1,190) · test files: 92 → 92 (+0) · checks: 2,942 → 2,994 (+52)
|
||||
- addresses verified: 379 → 406 (+27) · recovered layouts: 21 → 22 (+1) · open questions: 27 → 26 (-1)
|
||||
- verified targets: 35 → 36 (+1) · mapped-or-better: 54 → 55 (+1)
|
||||
- engine LOC: 29,095 → 29,095 (+0) · test files: 92 → 92 (+0) · checks: 2,942 → 2,942 (+0)
|
||||
- addresses verified: 373 → 379 (+6) · recovered layouts: 21 → 21 (+0) · open questions: 27 → 27 (+0)
|
||||
|
||||
---
|
||||
warnings: board.md: unknown types objects; mars-rng.md: no oracle total row parsed; mars-stream.md: no oracle total row parsed; mars-vfs.md: no oracle total row parsed
|
||||
<!-- dashboard-metrics {"verified": 47, "mapped_plus": 67, "targets": 87, "loc": 30285, "tests": 92, "checks": 2994, "addr_verified": 406, "addr_total": 414, "layouts": 22, "open_q": 26} -->
|
||||
<!-- dashboard-metrics {"verified": 36, "mapped_plus": 55, "targets": 75, "loc": 29095, "tests": 92, "checks": 2942, "addr_verified": 379, "addr_total": 386, "layouts": 21, "open_q": 27} -->
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| engine: mars/stream + rng | engine | verified | high | 100% | 2026-09-07 | merging: 100% exact dump agreement on 3 saves; typed shapes round-trip byte-identical whole file; RNG = seed(RSeed)+2 twists confirmed; 4 tag-name fixes for SAVE_FORMAT |
|
||||
| engine: game/data catalogs | engine | verified | high | 100% | 2026-09-07 | merged: WeaponDef/ShipSectionDef/TurretTable/IdRegistry/TechTree/StringTable + cross_check; oracle 229,042 values 0 diffs; crosslink set reproduced exactly; 34 malformed shipped tokens pinned |
|
||||
| engine: mars/vfs (gob) | engine | verified | high | 100% | 2026-09-07 | merged: ZIP reader + native override; 8352+2035 entries = unzip -l; all 10268 files CRC-clean; byte-equal spot checks; ctest 11/11 |
|
||||
| determinism oracle | verify | verified | high | 100% | 2026-09-07 | BYTE-IDENTICAL across 5 runs incl. cross-process: (Autosave).sav 978041ac…, (Autosave EndTurn).sav bb4fd9ac…; gzip MTIME=0; only loaded-post-turn re-save differs (Player.Status 4->0, Summary.Checksum). findings/subsystems/determinism-oracle.md RE-CONFIRMED 2026-09-08 on engine `cef889e` (lane M's movement fix included) by lane F: `bb4fd9ac…` / `978041ac…` unchanged, and reproduced a further 3x under forced control words that leave the arithmetic alone (0x027f/0x127f/0x137f). |
|
||||
| determinism oracle | verify | verified | high | 100% | 2026-09-07 | BYTE-IDENTICAL across 5 runs incl. cross-process: (Autosave).sav 978041ac…, (Autosave EndTurn).sav bb4fd9ac…; gzip MTIME=0; only loaded-post-turn re-save differs (Player.Status 4->0, Summary.Checksum). findings/subsystems/determinism-oracle.md |
|
||||
| engine: shim trace/compare emitter | engine | verified | high | 100% | 2026-09-07 | merged (sots-engine 9e110b4): emitter byte-exact vs mkfixture, tracer, snapshot/diff, Hook<Descriptor>; ctest 18/18; tracecmp exits 0/1/2 as specified; shim links |
|
||||
| engine parity: first-wins keys + tokenizer rules | engine | verified | high | 100% | 2026-09-07 | merged: Mars::Script tokenizer rules, first-wins keys, trailing-pair drop in mars/parse+mars/text AND Python oracle; oracles 1531/1531, 64/64; only real-data effects: 2 dropped trailing pairs (engine uses defaults), systemnames.txt nesting |
|
||||
| save-format tag corrections | verify | verified | high | 100% | 2026-09-07 | byte-confirmed at offsets (otnF x62, nextid, FAIDes/DHide/DWep/DName x43, ords, wpts, paths); reader A()-matches them; 36 tests; strict 3/3, infos 259->197; SAVE_FORMAT §10 changelog |
|
||||
|
|
@ -58,7 +58,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| P2-B3 ProcessResearch (behavioral, RNG) | phase2 | mapped | high | 85% | 2026-09-08 | LIVE, PARTIAL PASS: 15 calls compared, 13 zero-divergence; RNG post-state matched 14/15 incl. every roll (validates MT19937 + draw mapping + odds together). 2 divergences are the declared SetResearched boundary. ORACLE FAILS by exactly one item across 40,300: an unposted EVENT_RESEARCH_OVERBUDGET - compare was blind because the event list was never a declared region. fpu_cw=0x127f => 53-bit double, x87 question SETTLED. No Zuul in the save: double roll still disassembly-only . RECAPTURED WITH GUARDS 2026-09-08 (lane R): the oracle gap is now a COMPARE DIVERGENCE - `side.events.after.v.next_id orig=4 ours=3` on call 0, its only divergent field, with node[144] progress 2879->5768 and flag 1->2 both reproduced and the single RNG draw identical. 15 calls over 5 turns: 3 diverged, **RNG 15/15** (better than the original 14/15 - no tech-effect draw in this session). Guards on the two completion calls map SetResearched: ConMod[0..2]/OutMod/PopMod, ResTNm, TechTree+0x20 order counter, and the undeclared otch vector |
|
||||
| RNG signatures (Ghidra) | meta | verified | high | 100% | 2026-09-08 | Seed/Twist/NextFloat/NextInt verified; draw = y/(2^32-1); NextInt [0,n] inclusive; lazy twist; left@+0x9c4. RUNTIME CONFIRMED: fpu_cw=0x127f (53-bit double, round-nearest) - our next_float model is right, float_from_pc24 is an unused contingency |
|
||||
| engine: game/effects | engine | verified | high | 100% | 2026-09-08 | merged: TechId enum (196 slots @10000+i), 44 ids with typed strategic effects, species flag bits, ApplyTechEffect; 254 checks |
|
||||
| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **FREE** (F-fpucw released 2026-09-08 05:05 local; M-movefleet before it). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-<lane>, stage dist-<lane>, deploy C:\SOTS\shimdist-<lane>. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) GOTCHA (lane F, confirms lane R): the >60 s startup is REAL and cost a whole wasted run — do not sleep-and-click, **verify the main menu from a screenshot** (`verify/fpu-cw/` run scripts poll a screenshot until the Load Game / Exit buttons are bright red; 3 probes ≈ 25 s was typical). TIP (lane F): reset `SavedGames\` to a fixed file set before every run — the Load dialog row positions depend on how many files are listed, so a constant set means the click path never has to be re-derived (with the 4-file set ref-turn2 sits at (400,348), not (400,436)). PowerShell over SSH mangles quoting badly: send snippets base64 as `powershell -EncodedCommand`, or use `-ExecutionPolicy Bypass -File`. Lane F left `C:\SOTS\shimdist-fpu` + `C:\SOTS\ui\f{deploy,grab,fpu}.ps1` + `C:\SOTS\ui\preF\` (the pre-lane-F SavedGames snapshot, restored) in place. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, SavedGames back to the 7-file pre-F set, main menu verified by screenshot. |
|
||||
| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **F-fpucw** (M-movefleet released 2026-09-08 03:49 local; R-recapture before it). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-<lane>, stage dist-<lane>, deploy C:\SOTS\shimdist-<lane>. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) |
|
||||
| Zuul double-roll (behavioural) | verify | backlog | — | 0% | 2026-09-08 | CONFIRMED NEEDED: ref-turn2 has only species 0 and 2, so the double roll is verified by disassembly + host tests only. Needs one compare from a species-5 save; the check is just that `left` drops by 2 not 1 |
|
||||
| budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail . CONFIRMED AND WORSE 2026-09-08 (lane R, 4284 calls): **13 of 22 slots are 0 on every call** - tradeIncome, shipCarriedPop, secondaryManager, bonusIncome, systemIncomeNeg, debtInterest, construction, expenses, researchMoneyGiven, savingsGiven, tra, researchPointsGiven, trp |
|
||||
| hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants |
|
||||
|
|
@ -75,23 +75,20 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| B1 replace double-run | verify | backlog | — | 0% | 2026-09-08 | ComputeBudget replace mode runs the original a second time to harvest budget slots; ComputeOutput repairs ships in orbit as a side effect, so this is a real per-turn double effect on objects no region covers. Needs a design fix (harvest without re-running, or declare+revert) |
|
||||
| ReVa MCP link drop (workaround) | meta | verified | high | 100% | 2026-09-08 | The ReVa MCP client link dropped mid-session while the CT111 server stayed healthy (systemd active, :8080 listening, valid key -> 200). `tools/reva_call.py <tool> '<json>'` calls the same server over plain HTTP (initialize -> notifications/initialized -> tools/call; replies are SSE with a leading `id:` line, initialize is plain JSON). Key is NEVER stored in the repo: $REVA_KEY, else ~/.claude.json, else ssh to the CT properties file. Use this whenever mcp__plugin_ReVa_ReVa__* is unavailable |
|
||||
| event posting API | subsystem | mapped | high | 90% | 2026-09-08 | RECOVERED (lane E, `findings/subsystems/events.md`). Container: `EventStorage` embedded at `ServerPlayer+0x29c` (0x1c), `EvNxID` at +0x14 = player+0x2b0 — exactly the guard's byte run. Nested `vector<TurnEvents{int EvTurn; vector<PlayerEvent>}>`, record 0x74 B, tags `EvEID EvDsc EvMsg EvImg EvLoc EvPos EvAct EvCID`; layout confirmed field-by-field against turn3-state.sav, which CONTAINS the overbudget record. Entry point `int __thiscall EventStorage::PostEvent(this, string BYVAL, string BYVAL, obj*, Vector3*, turn, const char* img, int act)` 0x008862b0 RET 0x4c — **161 call sites in 113 functions, the whole sim's event API**. B3 defect fully explained: 0x00587b97, in the completion-roll-FAILED branch under `!wasDone && nowDone && owner`. 3 note corrections (EvPos is FLT_MAX not inf; the save array is turn-bucketed not flat; TECHS_UNLOCKED has no parent clause). 56 entries in addresses.json; 11 prototypes + 13 labels + 12 comments + 2 structs written back to Ghidra. Engine: `sots-engine` branch `wip/events` a7348be, `src/game/events` + 112 checks, ctest 32/32. NOT YET WIRED INTO A HOOK — see `docs/E-events.md` for the proposed region/Coverage change |
|
||||
| state-checksum replay harness | verify | verified | high | 90% | 2026-09-08 | Lane C: `verify/state-checksum/` (tool, 38 tests, `STATE_CHECKSUM.md`, evidence in `verify/results/state-checksum/`). Whole-state digest tree; **coverage is PROVED by byte-for-byte re-serialisation**, not declared - the answer to empty-region-set green verdicts. Localises: the known load->re-save delta reports as exactly 5 named leaves (`/Sim/players/Player[496 "Singularity"]/Status: 4 -> 0`, `/Summary/Checksum`), and one real End Turn as 108 attributed diffs. All 10 saves STABLE + COVERED. Float policy = exact bits by default, `canonical` for -0.0/NaN only, **tolerance deliberately not a hashing mode** (it lives in `--ulps` on the differ); corpus has 0 NaN/-0.0/subnormals so canonical is a no-op today. Chain record/verify validated on the real turn1-3 saves. REMAINING 10%: the VM-driven replay loop is designed (§5) but UNRUN - needs the VM holder. Open question named in §3.5 with the experiment that settles it (force `fpu_cw` 0x027f/0x127f/0x137f across End Turn, checksum the three autosaves) **§3.5 CLOSED 2026-09-08 by lane F** (see the fpu_cw row): measured, not assumed — 53-bit == 64-bit, so `floats=bits` costs the SSE port nothing; 24-bit and round-up each name one witness. The tool localised both to single leaves out of 35,394, with coverage PROVED on all 8 new saves. |
|
||||
| state-checksum replay harness | verify | verified | high | 90% | 2026-09-08 | Lane C: `verify/state-checksum/` (tool, 38 tests, `STATE_CHECKSUM.md`, evidence in `verify/results/state-checksum/`). Whole-state digest tree; **coverage is PROVED by byte-for-byte re-serialisation**, not declared - the answer to empty-region-set green verdicts. Localises: the known load->re-save delta reports as exactly 5 named leaves (`/Sim/players/Player[496 "Singularity"]/Status: 4 -> 0`, `/Summary/Checksum`), and one real End Turn as 108 attributed diffs. All 10 saves STABLE + COVERED. Float policy = exact bits by default, `canonical` for -0.0/NaN only, **tolerance deliberately not a hashing mode** (it lives in `--ulps` on the differ); corpus has 0 NaN/-0.0/subnormals so canonical is a no-op today. Chain record/verify validated on the real turn1-3 saves. REMAINING 10%: the VM-driven replay loop is designed (§5) but UNRUN - needs the VM holder. Open question named in §3.5 with the experiment that settles it (force `fpu_cw` 0x027f/0x127f/0x137f across End Turn, checksum the three autosaves) |
|
||||
| MoveFleet position ULP divergence | phase2 | verified | high | 100% | 2026-09-08 | **DONE (lane M).** First arithmetic divergence caught by BEHAVIOURAL compare rather than static reading, and it is fixed by matching the original's precision sequence rather than by fitting numbers - see the `MoveFleet position rounding (1 ULP)` row for the mechanism. Live 8 -> 0 on the same 45 calls, control run included so the before/after is this lane's own measurement. **COVERAGE IS UNCHANGED AND STILL THIN**: 15 of 45 calls move and all 15 are the same straight-run waypoint type. Waypoint types 2-5 were ATTEMPTED and could NOT be reached - the only player that would travel a node line has `DE 00 CR 00 DN 00` at its home system on this save, so its Move/Manage Fleets buttons are greyed out every turn and there is literally nothing to send along the node lines the map draws. Reaching them needs a ship BUILT over several turns, or (cheaper) a purpose-built save that starts with a fleet in orbit beside a node line. The node-line step is still wrong by construction: NodeLineStep/BuildStutterSegments exist and are unit-tested but are not wired into the hook |
|
||||
| ObservedTech append (undeclared) | verify | backlog | — | 0% | 2026-09-08 | Lane R's guards caught a vector<ObservedTech> append at `player+0x274` during SetResearched. It is SERIALIZED state and appears in NO coverage note anywhere - found only because guards localise rather than just flag a moved hash. Needs a declared region + a model in ours |
|
||||
| fpu_cw sensitivity experiment | verify | verified | high | 100% | 2026-09-08 | **DONE (lane F): 53-bit vs 64-bit x87 makes NO difference — the SSE port has no double-rounding budget to preserve, `STATE_CHECKSUM.md` §3.5 CLOSED.** 7 End Turns from ref-turn2, 6 control words, whole-state checksum on each. stock / 0x027f / 0x127f / **0x137f (64-bit)** all give `978041ac…` identical across all 35,394 leaves. Two settings DO move state, each reproduced on a repeat run: **0x007f (true 24-bit)** -> `/Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC 540000000->540000002` (+derived Summary/Checksum); **0x1a7f (53-bit, round-UP)** -> `/Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0]` and `/Pos/.[2]`, 1 ULP each. So the port must hold intermediates at 53 bits (never compute a chain in `float`) and use round-to-nearest — both SSE defaults, now measured. **THE BRIEFED TRIPLE WAS UNDER-POWERED: 0x027f is 53-bit (differs from 0x127f only in bit 12, infinity control, ignored since the 387) and 0x137f is 64-bit, not a rounding change** — run as written, all three come back identical and would have 'proved' something false. PC=bits 8-9, RC=bits 10-11. Setting PROVEN to hold: read-back at each force + 38 in-pipeline hook samples per run spanning turn phases 4/6/8, all at the forced value (`verify/results/fpu-cw/cw-census.txt`). `Mars::Application::Run` calls `_controlfp(0x50000,0x3070300)` at 0x0089f606 EVERY FRAME = 0x127f, so forcing at EndTurn is wiped before BeginProcessTurn; BeginProcessTurn is the point that works. TRAP: under 24-bit the CRT's own %g rendering degrades, so trace TEXT is not a comparison surface — use `verify/fpu-cw/trace_bitdiff.py`. findings/subsystems/fpu-precision-sensitivity.md |
|
||||
| fpu_cw sensitivity experiment | verify | backlog | — | 0% | 2026-09-08 | QUEUED FOR VM140 (lane M holds it). Lane C cannot tell whether any turn-pipeline value actually DEPENDS on x87 intermediate precision - every save on this host was made at fpu_cw=0x127f, so "the x64/SSE port must match bit-for-bit" is currently POLICY, not a measured requirement. Experiment: End Turn from ref-turn2.sav 3x with the shim forcing fpu_cw to 0x027f / 0x127f / 0x137f, then state_checksum the three autosaves. All equal => no double-rounding budget needed, closes STATE_CHECKSUM 3.5. Different => the diff IS the answer, naming every precision-sensitive field |
|
||||
| Summary.Checksum algorithm | objects | blocked | — | 0% | 2026-09-08 | Lane C RULED OUT two candidates so nobody repeats them: NOT a byte sum over the inflated stream, NOT a sum over the int leaves. Each is consistent with the -16 re-save delta but leaves no constant residual across turns |
|
||||
| event posting in ours | phase2 | in-progress | — | 0% | 2026-09-08 | Lane P: make ours actually post events so ProcessResearch's `side.events.after.v.next_id` 4->3 divergence closes. Converts harness-audit row 1 from known-defect to checked, and unbounds B2/B3 whose clean compares currently cover economy fields only |
|
||||
| LAB RULE: no `git add -A` in sots-re while lanes run | meta | verified | high | 100% | 2026-09-08 | MY error, caught by lane M: an integrator `git add -A` in the SHARED `sots-re` clone swept a running lane's in-progress files into commit 9d385a7 mid-run (remainder landed in f5b37c2). Nothing was lost, but authorship and atomicity were. RULE: while any lane is live, the integrator stages sots-re by explicit PATH only (`git add campaign/board.md campaign/DASHBOARD.md`), never `-A`. Lanes own their own subtrees. sots-engine is unaffected - lanes work in per-lane worktrees there, which is exactly why that repo has not had this problem |
|
||||
| MoveFleet waypoint types 2-5 | verify | backlog | — | 0% | 2026-09-08 | Still ZERO behavioural coverage after lane M. Not for lack of trying: the only mover in ref-turn2 is the AI (straight runs only), and the player that would travel a node line has DE/CR/DN all 00 at its home system, so Move/Manage Fleets are greyed out every turn - there is nothing to send along the node lines the map draws. Needs a ship built over several turns or a purpose-built save. The type-2 node-line step is still WRONG BY CONSTRUCTION (B4). Also: sim::Distance deliberately left in double (only stutter geometry uses it); Mars_Vec3_Length says it is probably 1 ULP out the same way, but there is zero behavioural evidence to correct it against - do not "fix" it blind |
|
||||
| P2-P event posting in ours | phase2 | mapped | high | 80% | 2026-09-08 | HOST-VERIFIED, VM RUN QUEUED (lane F holds VM140). next_id reaches 4 in a host reproduction of recap-b3 call 0, fixture rebuilt from raw bytes at the real 0x1c/0x18/0x74 strides and cross-checked against turn3-state.sav with lane C's state_checksum --tree. Count-only (lane E option a): ours never calls the game's PostEvent and REPLACE MODE WRITES NOTHING - a bumped EvNxID with no record behind it would corrupt the very save the oracle hashes. Three design points: the event scan is taken in describe_args BEFORE the original (taken after, ours would dedup against the original's own posts and agree for the wrong reason); dedup risk is MEASURED and reported as events_dedup_risk, not assumed; KeylessEventText resolves keys to "%s" so the shim carries no prose. VERIFIED from the instruction stream: SetResearched 0x00581e10 calls owner vft+0x10 with (flags>>2)&1 and ProcessResearch passes flags=2, so silent=false and the completion event IS posted - previously only inferable from "EvNxID moved by two". ctest 33/33, shim cross-builds on CT111 (lane P could only syntax-check) |
|
||||
| EVENT_TECHS_UNLOCKED not posted (predicted residual) | verify | backlog | — | 0% | 2026-09-08 | Lane P FLAGGED RATHER THAN GUESSED. Trigger IS pinned (SetResearched's sweep sets state=2 + stamps turnAvailable sticky at -1; tail loop collects state==2 && turnAvailable==currentTurn) but evaluating it needs the unlock cascade ours deliberately does not run. The driver takes the unlock list as an INPUT and is handed nullptr ("no list") - deliberately distinct from an empty list ("computed, empty"). PREDICTED RESIDUAL: next_id short by exactly 1 on every completion call. Posting it "whenever something completed" would score on this save and be WRONG the first time a completion unlocks nothing - the exact false-pass shape this project keeps catching |
|
||||
| sizeof(ObservedTech) unpinned | objects | verified | high | 100% | 2026-09-08 | **PINNED (lane X).** `sizeof(Game::ObservedTech) = 0x2c (44)` -- three independent proofs: the magic divide `0x2e8ba2e9 sar 3` (= /44, exact) at 0x0087239f, `imul reg,reg,0x2c` at 0x0087243a / 0x007b735b, and the search stride `add edi,0x2c` at 0x007ba257. **Append site = `RecordObservedTech+0xdf` (0x007ba27f): `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`** -- a de-duplicating append, direct callee of OnTechResearched 0x00891790; the realloc through 0x007b5820 is why all three vector words move. Element FULLY MAPPED (lane S, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40): +0x00 vptr 0x00a2439c (RTTI `.?AVObservedTech@Game@@`), +0x04 uint16 `otnF`, +0x06 uint16 `otnL`, +0x08 **bool** `odet` (1 byte), +0x0c std::string `otch` (**0x1c**, so +0x24 is its _Alval, NOT a field), +0x28 int `owith` -- 0x2c exactly, nothing unaccounted. Same shape as Game::ObservedWeapon (Write 0x00817bc0, tag `owep`). Built the general tool the row asked for: `tools/x86disp.py`, an x86 displacement xref scanner (100% code coverage, 0.17% desync). `findings/subsystems/observedtech-append.md` |
|
||||
| sizeof(ObservedTech) unpinned | objects | verified | high | 100% | 2026-09-08 | **PINNED (lane X).** `sizeof(Game::ObservedTech) = 0x2c (44)` -- three independent proofs: the magic divide `0x2e8ba2e9 sar 3` (= /44, exact) at 0x0087239f, `imul reg,reg,0x2c` at 0x0087243a / 0x007b735b, and the search stride `add edi,0x2c` at 0x007ba257. **Append site = `RecordObservedTech+0xdf` (0x007ba27f): `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`** -- a de-duplicating append, direct callee of OnTechResearched 0x00891790; the realloc through 0x007b5820 is why all three vector words move. Element: vptr 0x00a2439c at +0 (RTTI `.?AVObservedTech@Game@@`), std::string at +0x0c (0x18 B); +0x04/+0x06 (16-bit) and +0x08/+0x24/+0x28 hold the four on-disk ints in an order NOT yet determined. Built the general tool the row asked for: `tools/x86disp.py`, an x86 displacement xref scanner (100% code coverage, 0.17% desync). `findings/subsystems/observedtech-append.md` |
|
||||
| lea-displacement xref scanner | meta | verified | high | 100% | 2026-09-08 | `tools/x86disp.py` -- fixes the systemic blind spot that Ghidra does not index ModRM displacements. Full x86-32 length decoder swept from Ghidra's 41,089 function starts: 2,174,504 instructions, 612,166 displacement sites, **100.0% code coverage, 70 desyncs (0.17%), zero unknown opcodes**. Validated against ground truth before use (re-finds `lea eax,[ecx+0x29c]` in GetEventStorage, both OnTechResearched +0x29c sites, and one NEW ProcessTurn site). HONEST LIMITS: it is a **recall** tool, not an oracle -- class-level precision at 0x274 is ~13% by function (99 sites / 45 functions, ~6 real), i.e. a 900x search-space cut that still needs one call-graph check. The naive byte scan it replaces is not wrong so much as **blind**: it misses 80/99 real sites at 0x274 and 13,784/14,611 at disp8 0x14. `cohort` ranking must never be used as a hard filter -- it would have discarded the correct ObservedTech answer. Works off a gitignored local cache in `dumps/`, so it does not hammer CT111 |
|
||||
| lea-displacement xref scanner | meta | verified | high | 100% | 2026-09-08 | `tools/x86disp.py` - full x86-32 length decoder swept from 41,089 Ghidra function starts: 2,174,504 instructions, 612,166 disp sites, 0 unknown opcodes, 100.0% code coverage, 0.17% desyncs. VALIDATED against ground truth before any new claim (rediscovers GetEventStorage's lea ecx+0x29c, EvNxID +0x2b0; positive control: given 50 known ServerPlayer offsets, FUN_0087fac0 scores 50/50 = the serializer, nothing close). BUILD GOTCHA: clipping sweeps at fva+Ghidra sizeInBytes lost 11% of functions to mid-instruction truncation; sweeping to the NEXT function start took coverage 89% -> 100%. HONEST LIMIT: the win is RECALL not precision - naive lea-only scan MISSES 80 of 99 real 0x274 sites; class-level precision ~13% by function. Value = search space 41,411 -> 45 (~900x), then disambiguate by call graph |
|
||||
| RANKER TRAP: cohort filter discards correct answers | meta | verified | high | 100% | 2026-09-08 | Lane X's cohort ranker WOULD HAVE DISCARDED THE CORRECT ANSWER. RecordObservedTech touches only 0x274/0x278 and nothing else on ServerPlayer, so every --min>=1 cohort filter drops it. What actually closed the case was plain `query` + ONE call-graph lookup on OnTechResearched's callees. RULE now in the tool docstring: it is a RANKER, NEVER a filter. Displacement scan for recall, call graph for disambiguation; neither alone sufficed |
|
||||
| ObservedTech struct | objects | verified | high | 100% | 2026-09-08 | sizeof = 0x2c (44) by THREE independent proofs: exact magic divide 0x2e8ba2e9 sar 3 (= ceil(2^35/44), emulated against n=0..1000) at 0x0087239f; imul reg,reg,0x2c at 0x0087243a/0x007b735b; search stride add edi,0x2c at 0x007ba257. APPEND SITE: RecordObservedTech+0xdf (0x007ba27f) `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`. RecordObservedTech (0x007ba1a0) is a DIRECT CALLEE of OnTechResearched and DE-DUPLICATES BY TECH NAME before appending - a naive push_back in the reimpl WOULD DIVERGE on re-observation. Realloc through 0x007b5820 explains why lane R saw all three vector words move. Element FULLY MAPPED by lane S via the serializer lane X pointed at: +0x00 vptr 0x00a2439c (RTTI .?AVObservedTech@Game@@), +0x04 uint16 otnF, +0x06 uint16 otnL, +0x08 bool odet (ONE BYTE), +0x0c std::string otch (0x1c -> +0x24 is the string's _Alval, not a field), +0x28 int owith. Matches save_reader.py's on-disk order exactly. Lane P's live byte delta should still read exactly 44 |
|
||||
| std::string size 0x18 vs 0x1c CONTRADICTION | objects | verified | high | 100% | 2026-09-08 | **RESOLVED (lane S): 0x1c was right all along; ONE layout binary-wide.** `_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`, sizeof 0x1c. `ObservedTech+0x24` is the string's own trailing allocator word, not the unaccounted data field lane X read it as. Settled by three COMPLETE ENUMERATIONS of the element, each of which skips +0x24: `ObservedTech::Write` 0x00817cf0 (serialises +0x04/+0x06/+0x08/+0x0c/+0x28 and nothing else), `ObservedTech_ctor` 0x008562a0, and the inlined copy ctor at 0x0079a184. Then generalised: new `tools/strfootprint.py` recovers every `(base,disp,tag)` handed to the Mars::Stream string helpers across the whole exe -- **65 std::string members off a non-stack base, ZERO with a sibling field inside the 0x1c span, 51 of the 52 measurable inter-member gaps exactly +0x1c** (the one +0x20 is StrategyServer KeyPath, +0x1c on its own Read side -- the writer skips a member). Corroborated by the vector<string> walk stride `add esi,0x1c` @0x00699c29, PostEvent's by-value strings at [ebp+8]/[ebp+0x24] with RET 0x4c, and MoraleEvent 0x50 = name@0x34 + 0x1c. NO empty-base variant, no custom allocator, no game-local string class. BLAST RADIUS: **zero recovered struct tables were wrong** -- every string-bearing layout in struct-recovery / save-editor-structs / events / schema-gaps-resolved already used 0x1c, `ServerPlayer::pswd` @0x2dc..0x2f7 included (Write 0x008563e0 puts the next member exactly 0x1c above). Only 4 prose statements carried the 0x18 number, all corrected. save_reader 36/36 and state_checksum unaffected and still green. **LESSON: never size a struct member from the offsets the code TOUCHES** -- `_Alval` is an empty allocator, never loaded or stored, so a touch-scan undercounts every string AND every vector by exactly 4. Size from an enumeration: serializer, ctor, copy ctor, or container stride. `observedtech-append.md` §9 |
|
||||
| ObservedTech struct | objects | verified | high | 100% | 2026-09-08 | sizeof = 0x2c (44) by THREE independent proofs: exact magic divide 0x2e8ba2e9 sar 3 (= ceil(2^35/44), emulated against n=0..1000) at 0x0087239f; imul reg,reg,0x2c at 0x0087243a/0x007b735b; search stride add edi,0x2c at 0x007ba257. APPEND SITE: RecordObservedTech+0xdf (0x007ba27f) `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`. RecordObservedTech (0x007ba1a0) is a DIRECT CALLEE of OnTechResearched and DE-DUPLICATES BY TECH NAME before appending - a naive push_back in the reimpl WOULD DIVERGE on re-observation. Realloc through 0x007b5820 explains why lane R saw all three vector words move. Element: vptr 0x00a2439c at +0 (RTTI .?AVObservedTech@Game@@). The four on-disk ints map onto +0x04/+0x06 (16-bit) and +0x08/+0x24/+0x28 - NOT DETERMINED, lane X did not guess it; settle via ObservedTech's serializer or lane P's live byte delta (should read exactly 44) |
|
||||
| std::string size 0x18 vs 0x1c CONTRADICTION | objects | backlog | — | 0% | 2026-09-08 | Lane X reports ObservedTech's embedded std::string at +0x0c occupying 0x18 bytes, NOT the 0x1c our standing MSVC fact asserts (_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18). 0x1c is used across MANY recovered layouts, so if the real allocator member is elided (empty-base optimisation) in some instantiations we may have LATENT OFF-BY-4 ERRORS in other structs. Foundational - audit before trusting any further string-bearing layout |
|
||||
| harness-audit row 11 CORRECTED (Budget+0x64) | verify | verified | high | 100% | 2026-09-08 | Lane X: row 11 is NOT SUPPORTED. ComputeBudget writes its Budget* only through esi into +0x00..+0x54; its only two +0x64 accesses are LOADS OFF A DIFFERENT BASE. And ProcessResearch's `int* overbudget` is a ProcessTurn STACK LOCAL (lea edx,[ebp-0x14] at 0x008914a5), not Budget+0x64. Agrees with lane R's 0-of-4284 guard result. Reclassified to "nothing shown to write it"; only a watchpoint settles it definitively |
|
||||
| STANDING RULE: size structs by enumeration, never by touch-scan | meta | verified | high | 100% | 2026-09-08 | Produced by lane S after lane X's 0x18/0x1c scare. `_Alval` is std::allocator<char>, an EMPTY class: it occupies a word but is NEVER loaded or stored, so it is INVISIBLE to any analysis based on what the code touches. Sizing a member that way undercounts by exactly 4. RULE: size a member from an ENUMERATION - serializer, ctor, copy ctor, or container stride - because an enumeration can show ABSENCE where a touch-scan cannot. Same trap is live for std::vector here: {_Myfirst,_Mylast,_Myend,_Alval} = 0x10, ALLOCATOR-LAST, the opposite of the MSVC _String_val allocator-first shape the textbooks describe. Now in re-windows-2000s-howto.md 1c and struct-recovery.md 0 |
|
||||
| std::string 0x18 vs 0x1c CONTRADICTION - RESOLVED | objects | verified | high | 100% | 2026-09-08 | 0x1c WAS RIGHT ALL ALONG; lane X mis-attributed ObservedTech+0x24, which is the string's trailing empty-allocator word. ONE layout binary-wide: _Bx@0 (16-byte SSO union), _Mysize@0x10, _Myres@0x14, _Alval@0x18. Proof by three COMPLETE ENUMERATIONS of ObservedTech (Write 0x00817cf0, ctor 0x008562a0, inlined copy ctor 0x0079a184) each of which skips +0x24, then generalised by new `tools/strfootprint.py`: 65 std::string members off a non-stack base, ZERO with a sibling inside the 0x1c span, 51 of 52 measurable inter-member gaps exactly 0x1c (the one 0x20 is StrategyServer::KeyPath, 0x1c on its own Read side - the WRITER skips a member). No 0x18 instantiation, no EBO variant, no custom allocator, no game-local string class. BLAST RADIUS: 65 layouts audited, ZERO were wrong - including ServerPlayer::pswd, the row lane X flagged. Only PROSE carried the 0x18 number. Separately found+fixed: struct-recovery.md 0 had _Mysize/_Myres TRANSPOSED while every table in the same file used the correct offsets |
|
||||
| ObservedTech on-disk mapping | objects | verified | high | 100% | 2026-09-08 | READ, NOT GUESSED, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40: +0x00 vptr, +0x04 u16 otnF, +0x06 u16 otnL, +0x08 bool odet (ONE byte, WriteBool), +0x0c std::string otch (0x1c), +0x28 int owith = 0x2c exactly, nothing unaccounted. Lane X's flagged-as-hypothesis first-seen/last-seen pair CONFIRMED by the tag names. Game::ObservedWeapon (0x00817bc0/0x00817b10) is the identical element with tag owep. Matches save_reader.py's on-disk order exactly - independent agreement between disassembly and the save oracle. `odet` typed int in the reader is BENIGN (for a 4-char tag a bool item and an int item are both 12 bytes, same value); all 60/61/61 real-save values are 00000000 so the SAVES do not discriminate - the binary does |
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re-
|
|||
- **SAVE_FORMAT tag corrections (fix Python reader + spec)** — real on-disk tags: `otnF` (not `ontF`) in Odes/Owep/Otch, `nextid` (not `nextId`) in NdGr2, Design = `FAIDes/DHide/DWep/DName`; `ords`/`wpts` are real tags. Python's positional R() matching hid these. RNG: float mapping `(float)(y*2^-32)`, `next_int` mask, and lazy-vs-eager twist at `left==0` still need binary confirmation. (from [[mars-stream]])
|
||||
- **RESOLVED: tech `allows` default** — unlisted species = 1.0 (confirmed in `FUN_005822d0` tree-creation roll, strategic-turn-internals §2); `game/data` uses 100. Still open from game/data: what the engine's converter does with the 34 malformed tokens (`force_right o`, `crew false`, `1.0f`, `0-5`, ``90\``); repeated scalars in a block are last-wins per the sequential if/else consumers (loader-prototypes §M3) — confirm on a fixture in compare mode. (from [[game-data]])
|
||||
- **RESOLVED: SAVE_FORMAT tag names** — all corrections confirmed by bytes and applied to the Python reader + spec (§10). Residual doc debt: `findings/objects/save-editor-structs.md` and `verify/design-rules/SHIP_DESIGN_RULES.md` still quote R1's `ontF/faiDes/nextId` spellings (R1-provenance; annotate rather than rewrite). (from [[SAVE_FORMAT]])
|
||||
- **RESOLVED (2026-09-08, lane S): `std::string` layout and size, once and for all** — `_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`, **sizeof 0x1c**, and there is exactly **one** instantiation in this binary. `struct-recovery.md` §0 was right (its §0 prose had `size`/`res` transposed — fixed); `turn-spine.md` §1.1 was wrong (already annotated); `loader-prototypes.md` and one `addresses.json` prototype said 0x18 — fixed. Lane X's `ObservedTech` 0x18 reading was a mis-attribution: `+0x24` is the string's trailing `_Alval`, not a data field, proved by three complete enumerations of the element (`ObservedTech::Write` 0x00817cf0, ctor 0x008562a0, copy ctor 0x0079a184) and generalised over the whole exe by `tools/strfootprint.py` (65 string members, 0 collisions inside the 0x1c span, 51/52 gaps exactly 0x1c). **Zero recovered struct tables were wrong** — `pswd` included. Standing rule that follows: *never size a struct member from the offsets the code touches* — this build's STL puts the empty allocator **last** in both `string` (0x1c) and `vector` (0x10), and an empty allocator is never loaded or stored, so a touch-scan undercounts by 4 every time. See `observedtech-append.md` §9. (from [[re-windows-2000s-howto]], [[observedtech-append]])
|
||||
- **Notes disagree on MSVC-2010 `std::string` layout** — `struct-recovery.md` §0 vs `turn-spine.md` §1.1 give different offsets; pin both from the `_Myres >= 16` SSO branch in `Stream::WriteString` and correct the loser. (from [[re-windows-2000s-howto]])
|
||||
- **RESOLVED: formula gaps (all 8)** — see [[formula-gaps]]; game/sim's low-confidence functions can now be pinned. **RESOLVED: std::string** = `_Bx@0,_Mysize@0x10,_Myres@0x14,_Alval@0x18`, sizeof 0x1c (struct-recovery §0 right; turn-spine §1.1 WRONG — annotated). **CORRECTION:** the 116-entry table @0x00a19718 is a name-membership list, not the effects table; use `g_TechIdNames` (196) + `OnTechResearched`. Open: values of the 6-entry AI-tech bonus table (0x00a17888); producer of ServerPlayer +0x224/+0x228/+0x22c beyond the setup-record copy (0x0077b620). (from [[tech-effects]])
|
||||
- **RESOLVED: RNG semantics** — `Seed` is `thiscall(this, uint32)` RET 4 (MT19937 init + immediate twist); `Twist` takes `this` in ECX only; **twist is LAZY** (`if (left==0) Twist()` inside the draw), `left` lives at `+0x9c4`, confirming the save blob layout. **float mapping, needs a targeted check (not a red flag):** `NextFloat` decompiles as returning `float10`, but that is simply how x86 float returns look (value in `ST(0)`), so it is weak evidence of extended-precision *computation*; a single `y * 2^-32` multiply rounds once either way. B3 should still compare the mapping explicitly (exact constant and whether the draw is `y*2^-32`, `(y>>8)*2^-24`, or divided by 2^32-1), and the x87-vs-SSE float-parity policy from the RE how-to is still needed before the x64 standalone. `RNG_NextInt` signature still unverified. (own Ghidra pass)
|
||||
- **Struct-modelling hazard (found by M2)** — an MSVC-2010 `std::vector` member is **three words**, so a naive C translation put the next pointer at `+0x18` when it is really at `+0x14`; the hook silently reported the unmodelled word (string bytes `"TION"`) as a real field. Any hand-modelled game struct must pin its size with `static_assert` and account for 3-word vectors. (from [[M2]])
|
||||
|
|
|
|||
|
|
@ -68,17 +68,8 @@ Readers accept legacy tags (`ISuit`, `Income`, `HPop`, `Bats`, `Builds`, `Clr`,
|
|||
`NShps`, `SysID`, `TrdID`, `Caps`, `GtTrf`, `FtSens`, `FtInc`, `Pris`, `NumPlgs`, `lcid`, `morev`, `cme`)
|
||||
by reading them into scratch/NULL — these are pre-1.8 fields, NOT members.
|
||||
|
||||
Common Mars/MSVC layouts seen: **`std::string` = 0x1c bytes** — `_Bx` union@0 (16-byte SSO buffer, or a
|
||||
`char*` when `_Myres >= 16`), `_Mysize`@**0x10**, `_Myres`@**0x14**, `_Alval`@0x18 (empty allocator, occupies
|
||||
a word, never read or written). `FUN_008b9d70` does the `res>=16 ? heap : sso` check.
|
||||
*(2026-09-08: this line previously transposed the two to `size@0x14, res@0x18`; the 0x10/0x14 offsets are the
|
||||
verified ones — `Stream::WriteString` `cmp [str+0x14],0x10`, `basic_string::assign` 0x00425550.)*
|
||||
The allocator is **trailing** in this build's STL, in strings and vectors alike, so it is invisible to any
|
||||
scan of "which offsets does the code touch" and costs exactly 4 bytes if you size a member that way. See
|
||||
`findings/subsystems/observedtech-append.md` §9 for the whole-binary audit (`tools/strfootprint.py`): 65
|
||||
`std::string` members across every serializer, zero with a sibling field inside the 0x1c span, 51 of 52
|
||||
measurable inter-member gaps exactly 0x1c. There is one string layout in this binary.
|
||||
`std::vector<T>` = {begin@0, end@4, cap@8, `_Alval`@0xc — 0x10 bytes};
|
||||
Common Mars/MSVC layouts seen: `std::string` = 0x1c bytes (MSVC10 `_Bx` union@0, size@0x14, res@0x18;
|
||||
`FUN_008b9d70` does the `res>=16 ? heap : sso` check); `std::vector<T>` = {begin@0, end@4, cap@8};
|
||||
`std::map/set` node = {left@0, parent@4, right@8, key@0xc, value@0x10, …, color/isnil bytes at tail};
|
||||
`std::list` = {head*@0, size@4}. `Mars::NetworkObject` = {vptr@0, int id@4}.
|
||||
|
||||
|
|
@ -380,32 +371,6 @@ Four `std::list`s: `+4 list<SpyReportDefences>` (count `defc2`@+8, items `def`),
|
|||
### 2.5 `Game::TechTree` — Write `FUN_005890a0` (tags `NumTechs`, `TNm`, `NumBrs`; per-tech body in a
|
||||
sub-writer not decompiled here; logs "TechTree: Tech %d not found saving tech tree").
|
||||
|
||||
### 2.6 `Game::ObservedTech` (0x2c) — Write `0x00817cf0`, Read `0x00817c40`
|
||||
Elements of `ServerPlayer::otch`, the `vector<ObservedTech>` at `ServerPlayer+0x274`. Polymorphic:
|
||||
vftable `0x00a2439c`, RTTI `.?AVObservedTech@Game@@`, slots `{[0] 0x00793610 dtor, [1] Read, [2] Write}`.
|
||||
|
||||
| off | type | save name | notes |
|
||||
|---|---|---|---|
|
||||
| 0x00 | vptr | — | `0x00a2439c` |
|
||||
| 0x04 | `uint16` | `otnF` | turn first observed; widened to int32 on disk |
|
||||
| 0x06 | `uint16` | `otnL` | turn last observed; widened to int32 on disk |
|
||||
| 0x08 | `bool` | `odet` | **one byte** (+3 pad); `WriteBool`/`ReadBool` |
|
||||
| 0x0c..0x27 | `std::string` | `otch` | tech name; `_Mysize`@0x1c, `_Myres`@0x20, `_Alval`@0x24 |
|
||||
| 0x28 | `int` | `owith` | last member; `0x28 + 4 = 0x2c` = sizeof, no slack |
|
||||
|
||||
`sizeof` = `0x2c` (44), pinned three ways by lane X (magic divide `0x2e8ba2e9 sar 3` at `0x0087239f`,
|
||||
`imul reg,reg,0x2c`, search stride `add edi,0x2c`); the member map is the serializer's own order.
|
||||
`Game::ObservedWeapon` (Write `0x00817bc0`, Read `0x00817b10`) is the identical element with tag `owep`
|
||||
in place of `otch`; `odes` elements are the smaller `otnF otnL odid opid` record.
|
||||
|
||||
Appended by `RecordObservedTech` `0x007ba1a0`, which **de-duplicates by tech name** — a reimplementation
|
||||
that just `push_back`s will diverge on re-observation. It writes `otnF` and `otnL` from the *same* source
|
||||
word on first sighting, so first-seen == last-seen initially.
|
||||
|
||||
**Do not read `+0x24` as a field.** It is the name string's trailing `_Alval`. See §0 and
|
||||
`findings/subsystems/observedtech-append.md` §9 — this is the class that produced the 0x18-vs-0x1c
|
||||
`std::string` scare, and the resolution is that `0x1c` was right everywhere.
|
||||
|
||||
---
|
||||
|
||||
## 3. `Game::StarFleet` (R1 `SimFleetDetails`) — Write `FUN_00701070`, Read `FUN_00702470`; `this` = obj+8
|
||||
|
|
|
|||
|
|
@ -1,207 +0,0 @@
|
|||
# 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=<cw>|off` and `fpu.sample_ticks=on|off`.
|
||||
* Evidence: `verify/results/fpu-cw/run-<tag>/` — 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.
|
||||
|
|
@ -12,9 +12,7 @@ Raw decompiles (before rename) are in `/srv/re-lab/handoff/loader-decompiles/rec
|
|||
decompiles in `/srv/re-lab/handoff/loader-decompiles/verify/<addr>.c`.
|
||||
|
||||
Conventions: `thiscall` = `this` in ECX, args pushed right-to-left, callee pops (`RET n`); `cdecl` = all on stack, caller
|
||||
pops. `std::string` = **0x1c bytes**: 16-byte SSO buffer / pointer `_Bx` @+0, `_Mysize` @+0x10, `_Myres` @+0x14,
|
||||
`_Alval` (empty allocator, never read or written) @+0x18. *Corrected 2026-09-08 — this line said 0x18, which is
|
||||
the extent of the fields the code touches, not the size of the object; see `observedtech-append.md` §9.*
|
||||
pops. `std::string` = MSVC-2010 layout (16-byte SSO buffer / pointer, size @+0x10, capacity @+0x14, 0x18 bytes).
|
||||
"Case-insensitive" always means MSVCR100 `_stricmp` (ASCII fold).
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -168,49 +168,30 @@ COL `0x00a81c78` → type descriptor `0x00aeede4` → **`.?AVObservedTech@Game@@
|
|||
`ObservedTech` is polymorphic and its first word is a vptr, not a data field — which the
|
||||
on-disk shape does not tell you.
|
||||
|
||||
> **Superseded 2026-09-08 by lane S — see §9.** The table as first written called the
|
||||
> embedded string 0x18 bytes and left `+0x08`, `+0x24`, `+0x28` unaccounted. `+0x24` is not a
|
||||
> field: it is the string's own trailing allocator word. The corrected map is below; the
|
||||
> original reasoning is kept in §9 because the way it went wrong is the useful part.
|
||||
| offset | size | evidence |
|
||||
|---|---|---|
|
||||
| `+0x00` | 4 | vptr `0x00a2439c`; ctor writes it, RTTI-confirmed |
|
||||
| `+0x04` | 2 | `mov word [eax-0x28],cx` at `0x007ba2b0` (`eax` = `_Mylast`, element = `_Mylast-0x2c`), source `[ebx+0xc]` |
|
||||
| `+0x06` | 2 | `mov word [eax-0x26],dx` at `0x007ba2c6`, **same source word** `[ebx+0xc]` |
|
||||
| `+0x08` | 4 | **unaccounted** |
|
||||
| `+0x0c..+0x23` | 0x18 | `std::string` (the `otch` tech name). Object base is `+0x0c`, MSVC layout `{_Bx[16] @+0x00, _Mysize @+0x10, _Myres @+0x14}`. Ctor writes `[+0x0c]=0`, `[+0x1c]=0`, `[+0x20]=0xf`; the search loop reads `[+0x1c]` as the length and calls the compare with `this = +0x0c`; the post-append assign uses `lea ecx,[_Mylast-0x20]` = `+0x0c` |
|
||||
| `+0x24` | 4 | **unaccounted** |
|
||||
| `+0x28` | 4 | **unaccounted** |
|
||||
|
||||
| offset | size | member | evidence |
|
||||
|---|---|---|---|
|
||||
| `+0x00` | 4 | vptr `0x00a2439c` | ctor writes it, RTTI-confirmed |
|
||||
| `+0x04` | 2 | `uint16 otnF` (turn first observed) | `mov word [eax-0x28],cx` at `0x007ba2b0` (`eax` = `_Mylast`, element = `_Mylast-0x2c`), source `[ebx+0xc]`; tag from `ObservedTech::Write` |
|
||||
| `+0x06` | 2 | `uint16 otnL` (turn last observed) | `mov word [eax-0x26],dx` at `0x007ba2c6`, **same source word** `[ebx+0xc]` — first sighting sets first == last |
|
||||
| `+0x08` | 1 | `bool odet` | ctor stores a **byte** (`mov [esi+0x8],bl`, `0x008562f4`); copy-ctor copies a byte; serialised with `WriteBool`/`ReadBool` |
|
||||
| `+0x0c..+0x27` | 0x1c | `std::string otch` (tech name) | object base `+0x0c`, MSVC `{_Bx[16] @0, _Mysize @0x10, _Myres @0x14, _Alval @0x18}`. Ctor writes `[+0x0c]=0`, `[+0x1c]=0`, `[+0x20]=0xf`; the search loop reads `[+0x1c]` as the length and calls compare with `this = +0x0c`; the post-append assign uses `lea ecx,[_Mylast-0x20]` = `+0x0c`. `+0x24` is `_Alval` — never read, never written, by anything |
|
||||
| `+0x28` | 4 | `int owith` | ctor zeroes it; `ObservedTech::Write` emits it last |
|
||||
Note the string here is **0x18 bytes**, not the 0x1c the `ServerPlayer::pswd` row in
|
||||
`struct-recovery.md` implies — three separate reads inside this element agree on
|
||||
`{buf16, _Mysize@+0x10, _Myres@+0x14}`. Worth re-checking `pswd` against that.
|
||||
|
||||
`4 + 2 + 2 + 1(+3 pad) + 0x1c + 4 = 0x2c` exactly — sizeof is fully accounted for, with no
|
||||
padding slack and no unaccounted field.
|
||||
The four on-disk ints (`otnF`, `otnL`, `odet`, `owith`) have to map onto `+0x04`/`+0x06` (two
|
||||
16-bit fields) and the three 4-byte slots `+0x08`, `+0x24`, `+0x28`. **That mapping is not
|
||||
determined here and is deliberately not guessed.** The one suggestive observation, flagged as a
|
||||
*hypothesis only*: `+0x04` and `+0x06` are two adjacent 16-bit fields written from the same
|
||||
source word on first observation, which is the shape you would expect of a first-seen /
|
||||
last-seen turn pair — but nothing here proves it.
|
||||
|
||||
### Where the mapping came from — the serializer
|
||||
|
||||
`Game::ObservedTech`'s vftable `0x00a2439c` is the usual 3 slots
|
||||
`{ [0] 0x00793610 scalar deleting dtor, [1] 0x00817c40 Read, [2] 0x00817cf0 Write }`.
|
||||
`Write` enumerates the entire object, in order, and touches nothing else:
|
||||
|
||||
```
|
||||
0x00817cff movzx ecx,word [edi+0x04] push 0xa2b38c "otnF" -> stream vft+0x24 (int)
|
||||
0x00817d0f movzx ecx,word [edi+0x06] push 0xa2b384 "otnL" -> stream vft+0x24 (int)
|
||||
0x00817d26 lea eax,[edi+0x08] push 0xa2b36c "odet" -> WriteBool 0x008b9c20
|
||||
0x00817d37 lea ecx,[edi+0x0c] push 0xa2b3e8 "otch" -> WriteString 0x008b9d70
|
||||
0x00817d46 mov eax,[edi+0x28] push 0xa2b364 "owith" -> stream vft+0x24 (int)
|
||||
```
|
||||
|
||||
`Read` (`0x00817c40`) is the exact mirror: `otnF`/`otnL` read as ints and stored back with
|
||||
16-bit `mov word [ebx],ax`, `odet` through `ReadBool`, `otch` through `ReadString`, `owith`
|
||||
reached as `add edi,0x28`. That matches the on-disk order `save_reader.py` already had
|
||||
(`Otch = otnF otnL odet otch(string) owith`), which is a nice independent agreement between
|
||||
the disassembly and the save oracle.
|
||||
|
||||
`Game::ObservedWeapon` (`Write` `0x00817bc0`, `Read` `0x00817b10`) is the same element shape
|
||||
with tag `owep` (`0x00a2b3f0`) in place of `otch` — a second instance of the identical 0x2c
|
||||
layout.
|
||||
|
||||
Lane P's `observed_techs` region byte delta remains a valid live cross-check and should come
|
||||
back as exactly 44 per completion.
|
||||
What would settle the mapping: read `ObservedTech`'s `Read`/`Write` serializer, where the
|
||||
member order is explicit. Lane P's `observed_techs` region byte delta remains a valid live
|
||||
cross-check and should now come back as exactly 44 per completion.
|
||||
|
||||
## 5. False-positive rate, honestly
|
||||
|
||||
|
|
@ -295,111 +276,3 @@ the stride evidence on the first four.
|
|||
`RecordObservedTech`, `vector_ObservedTech_push_back`, `vector_ObservedTech_assign`,
|
||||
`ObservedTech_ctor`, `vector_44B_grow`; `ServerPlayer_off_ObservedTechs` updated from
|
||||
"NOT PINNED" to `verified`.
|
||||
|
||||
**Lane S round (2026-09-08).** Labels: `ObservedTech_Write` `0x00817cf0`, `ObservedTech_Read`
|
||||
`0x00817c40`, `ObservedTech_scalar_deleting_dtor` `0x00793610`, `ObservedWeapon_Write`
|
||||
`0x00817bc0`, `ObservedWeapon_Read` `0x00817b10`, `vector_ObservedTech_uninit_copy` `0x0079a150`,
|
||||
`vector_string_find_by_name` `0x00699bd0`. Prototypes on the five class methods. Plate comments
|
||||
carrying the full member map on the two serializers, the ctor, the dtor and the copy helper, and
|
||||
the `sizeof(std::string) = 0x1c` fact on `Stream::WriteString` `0x008b9d70` and on the
|
||||
`vector<string>` stride site `0x00699bd0` — the two places a future lane is most likely to look.
|
||||
`addresses.json` +10 entries (`std_string_sizeof`, `ObservedTech_Read/_Write/_dtor/_copy_ctor`,
|
||||
`ObservedTech_off_TurnFirst/_TurnLast/_Detected/_With`, `ObservedWeapon_Write`), 4 corrected.
|
||||
|
||||
---
|
||||
|
||||
## 9. `std::string` is 0x1c, not 0x18 — the correction, and why it mattered (lane S, 2026-09-08)
|
||||
|
||||
§4 above originally reported the embedded string as **0x18 bytes**, against the campaign-wide
|
||||
`_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`, sizeof `0x1c`. Lane X flagged it as "worth
|
||||
re-checking" rather than asserting it, which was the right call: **`0x1c` is correct, and it is
|
||||
correct everywhere in this binary.** `ObservedTech+0x24` is the string's own trailing allocator
|
||||
word, not a data member.
|
||||
|
||||
### Why the 0x18 reading looked right
|
||||
|
||||
Everything lane X observed was accurate. The string's *live* fields really do stop at `+0x14`
|
||||
(`_Bx@0`, `_Mysize@0x10`, `_Myres@0x14`), because `_Alval` is `std::allocator<char>` — an empty
|
||||
class. It occupies a word of the object but is never loaded or stored, so it is invisible to
|
||||
any evidence based on **what the code touches**. Sizing a type from its accessed fields
|
||||
undercounts it by exactly the tail padding. The same trap is live for `std::vector` in this
|
||||
build, which is `{_Myfirst, _Mylast, _Myend, _Alval}` = `0x10` while only three words are ever
|
||||
read (`events.md` already records the `_Alval` word at `EventStorage+0x10` and `+0x14`).
|
||||
|
||||
### The three things that settle it inside `ObservedTech`
|
||||
|
||||
Each is a *complete enumeration* of the object, which is the right instrument here — an
|
||||
enumeration can show a field's **absence**, a touch-scan cannot.
|
||||
|
||||
1. **`ObservedTech::Write` `0x00817cf0`** serialises `+0x04, +0x06, +0x08, +0x0c, +0x28`.
|
||||
No `+0x24`. (`Read` `0x00817c40` mirrors it.)
|
||||
2. **`ObservedTech_ctor` `0x008562a0`** initialises `+0x00, +0x04, +0x08, +0x0c(string), +0x28`.
|
||||
No `+0x24` — while zeroing every other scalar in the object.
|
||||
3. **The copy constructor**, inlined at `0x0079a184` inside the vector's uninitialised-copy
|
||||
helper `FUN_0079a150`, copies `+0x04, +0x06, +0x08`, the string at `+0x0c`, and `+0x28`.
|
||||
No `+0x24`.
|
||||
|
||||
A 4-byte data member that the constructor, the copy constructor and the serializer all ignore
|
||||
is not a data member.
|
||||
|
||||
### Binary-wide check — `tools/strfootprint.py`
|
||||
|
||||
One class is an anecdote, so the same question was put to the whole binary. Every serializer
|
||||
hands member pointers to the `Mars::Stream` primitive helpers with a fixed idiom
|
||||
(`lea r,[base+disp]; push r; push tag; push stream; call helper`), so the scanner recovers
|
||||
`(base, disp, tag)` for every string site and then asks: does the same function touch any other
|
||||
offset inside `(N, N+0x1c)` off the same base register?
|
||||
|
||||
```
|
||||
string helper call sites : 241
|
||||
resolved to a class member offset : 65
|
||||
stack temporaries (ebp/esp base) : 30
|
||||
offset not reached by a plain lea : 146
|
||||
|
||||
members with a sibling inside (N, N+0x1c) : 0
|
||||
|
||||
gap from a string member to the next member on the same base:
|
||||
+0x1c : 51
|
||||
+0x20 : 1
|
||||
```
|
||||
|
||||
**65 string members across every serializer in the exe, zero collisions, and 51 of the 52
|
||||
measurable gaps are exactly `0x1c`.** The single `+0x20` is `StrategyServer::KeyPath` at
|
||||
`+0x134`, whose *Read* side gives `+0x1c` to `+0x150` — the writer simply skips a member.
|
||||
There is no `0x18` instantiation, no empty-base-optimised variant, and no game-local string
|
||||
class. One layout, `0x1c`.
|
||||
|
||||
Two exclusions matter or the scan reports noise, and both are why a naive version of this
|
||||
would have "confirmed" 0x18:
|
||||
|
||||
* **`ebp`/`esp` bases are stack temporaries, not members.** A local string at `[ebp-0x2c]`
|
||||
shows accesses at `-0x1c` and `-0x18` — which read exactly like two sibling fields inside
|
||||
the span. All 30 such sites are excluded.
|
||||
* **`N+0x10` and `N+0x14` are the string's own `_Mysize`/`_Myres`**, touched inline whenever
|
||||
the compiler expands the `_Myres >= 16 ? _Ptr : _Buf` test at a call site.
|
||||
|
||||
### Independent corroboration outside the scan
|
||||
|
||||
* `FUN_00699bd0` walks a `vector<std::string>` doing the SSO test at `[esi+0x14]`/`[esi]` —
|
||||
element base *is* string base — and advances `add esi,0x1c` (`0x00699c29`). The stride of a
|
||||
vector of strings **is** `sizeof(std::string)`.
|
||||
* `EventStorage::PostEvent` `0x008862b0` takes two by-value `std::string`s at `[ebp+0x08]` and
|
||||
`[ebp+0x24]` (spacing `0x1c`) and `RET 0x4c` = `2*0x1c + 5*4`.
|
||||
* `MoraleEvent` is 0x50 bytes with its name string at `+0x34`: `0x34 + 0x1c = 0x50` exactly.
|
||||
|
||||
### Blast radius — what actually had to change
|
||||
|
||||
Nothing in any recovered struct table. Every string-bearing layout in `struct-recovery.md`,
|
||||
`save-editor-structs.md`, `events.md` and `schema-gaps-resolved.md` already used `0x1c` spans
|
||||
and `0x1c` inter-field gaps, and the binary agrees with all of them. `ServerPlayer::pswd` at
|
||||
`0x2dc..0x2f7` — the row §4 asked to re-check — is **correct**: `Write` `0x008563e0` puts the
|
||||
next member exactly `0x1c` above it. The corrections were confined to three prose statements
|
||||
that had propagated the 0x18 number (`loader-prototypes.md` conventions line, the
|
||||
`GlobalConst_ParseString` prototype in `addresses.json`, and §4 here) plus one transposition in
|
||||
`struct-recovery.md` §0 that said `size@0x14, res@0x18` where the verified offsets are
|
||||
`0x10`/`0x14`.
|
||||
|
||||
The lesson worth carrying, and the reason this was worth chasing rather than reconciling: **do
|
||||
not size a struct member from the offsets the code touches.** Trailing empty-allocator words in
|
||||
this build's STL are invisible to a touch-scan and cost exactly 4 bytes every time. Size types
|
||||
from an enumeration — a serializer, a constructor, a copy constructor, or a container stride.
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@
|
|||
"name": "GlobalConst_ParseString",
|
||||
"addr": "0x008b7670",
|
||||
"convention": "cdecl",
|
||||
"prototype": "void (std::string* storage, const char* text) /* *storage = text; MSVC std::string is 0x1c bytes {+0 char buf[16] | char* ptr when capacity > 15, +0x10 _Mysize, +0x14 _Myres, +0x18 _Alval (empty allocator, never read/written)} -- see std_string_sizeof; 146 keys (*_NAME, *_SOUND, *_TEXTURENAME, DERELICT_SECTION_nn ...); seen in the M1 trace */",
|
||||
"prototype": "void (std::string* storage, const char* text) /* *storage = text; MSVC2010 std::string 0x18 bytes {+0 char buf[16] | char* ptr when capacity > 15, +0x10 size, +0x14 capacity}; 146 keys (*_NAME, *_SOUND, *_TEXTURENAME, DERELICT_SECTION_nn ...); seen in the M1 trace */",
|
||||
"status": "verified-by-trace",
|
||||
"source": "sots-engine docs/M1.md"
|
||||
},
|
||||
|
|
@ -3174,7 +3174,7 @@
|
|||
"name": "ObservedTech_sizeof",
|
||||
"offset": "0x2c",
|
||||
"convention": "constant",
|
||||
"prototype": "sizeof(Game::ObservedTech) = 0x2c (44 bytes). Confirmed independently by (a) the compiler's magic division by 44 (mov eax,0x2e8ba2e9; imul; sar edx,3) at 0x0087239f and 0x007b7339, (b) imul reg,reg,0x2c at 0x0087243a, 0x00872468, 0x007b735b, and (c) the iterator advance `add edi,0x2c` in RecordObservedTech's linear search at 0x007ba257. FULL MEMBER MAP, from ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40 (lane S 2026-09-08): +0x00 vptr 0x00a2439c; +0x04 uint16 otnF; +0x06 uint16 otnL; +0x08 bool odet (1 byte, +3 pad); +0x0c std::string otch (0x1c, so _Mysize at +0x1c, _Myres at +0x20, _Alval at +0x24); +0x28 int owith. 4 + 2 + 2 + 4 + 0x1c + 4 = 0x2c exactly, no padding slack and no unaccounted field.",
|
||||
"prototype": "sizeof(Game::ObservedTech) = 0x2c (44 bytes). Confirmed independently by (a) the compiler's magic division by 44 (mov eax,0x2e8ba2e9; imul; sar edx,3) at 0x0087239f and 0x007b7339, (b) imul reg,reg,0x2c at 0x0087243a, 0x00872468, 0x007b735b, and (c) the iterator advance `add edi,0x2c` in RecordObservedTech's linear search at 0x007ba257. Matches the on-disk lower bound exactly (4 int + one 0x1c std::string = 44), so there is no padding slack.",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/observedtech-append.md (lane X, tools/x86disp.py displacement scan)"
|
||||
},
|
||||
|
|
@ -3190,7 +3190,7 @@
|
|||
"name": "ObservedTech_off_Name",
|
||||
"offset": "0x0c",
|
||||
"convention": "offset",
|
||||
"prototype": "std::string (save tag `otch`, the tech name) at ObservedTech+0x0c, 0x1c bytes, spanning +0x0c..+0x27. MSVC layout relative to the string object: _Bx[16] @+0x00, _Mysize @+0x10, _Myres @+0x14, _Alval @+0x18 -- i.e. ObservedTech+0x1c is the length, +0x20 the capacity, +0x24 the empty-allocator word (never read or written by anything). Evidence: ObservedTech_ctor 0x008562a0 writes [elem+0x0c]=0, [elem+0x1c]=0, [elem+0x20]=0xf; RecordObservedTech's search reads [elem+0x1c] as the length and calls compare with this=elem+0x0c; the post-append assign uses lea ecx,[_Mylast-0x20]. CORRECTION (lane S 2026-09-08): an earlier revision called this string 0x18 bytes and listed +0x24 as an unaccounted data field. It is not one -- three independent whole-object enumerations skip +0x24 entirely: ObservedTech_ctor 0x008562a0, ObservedTech_copy_ctor 0x0079a184, and ObservedTech_Write 0x00817cf0 (which serialises +0x04,+0x06,+0x08,+0x0c,+0x28 and nothing else). sizeof(std::string)=0x1c holds binary-wide; see std_string_sizeof.",
|
||||
"prototype": "std::string (save tag `otch`, the tech name) at ObservedTech+0x0c, 0x18 bytes, spanning +0x0c..+0x23. MSVC layout relative to the string object: _Bx[16] @+0x00, _Mysize @+0x10, _Myres @+0x14 -- i.e. ObservedTech+0x1c is the length and +0x20 the capacity. Evidence: ObservedTech_ctor 0x008562a0 writes [elem+0x0c]=0, [elem+0x1c]=0, [elem+0x20]=0xf; RecordObservedTech's search reads [elem+0x1c] as the length and calls compare with this=elem+0x0c; the post-append assign uses lea ecx,[_Mylast-0x20]. NOTE this string is 0x18 bytes, not the 0x1c implied by the ServerPlayer pswd row in struct-recovery.md. UNACCOUNTED in the element: +0x08, +0x24, +0x28 (4 bytes each) plus two 16-bit fields at +0x04/+0x06 written from one source word at 0x007ba2b0 / 0x007ba2c6 -- the mapping of the four on-disk ints (otnF, otnL, odet, owith) onto those slots is NOT determined.",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/observedtech-append.md (lane X, tools/x86disp.py displacement scan)"
|
||||
},
|
||||
|
|
@ -3198,7 +3198,7 @@
|
|||
"name": "RecordObservedTech",
|
||||
"addr": "0x007ba1a0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void (this, ServerPlayer* observer, ?, std::string* techName) -- appends to observer->otch. Linear-searches observer->otch (ServerPlayer+0x274) with stride 0x2c comparing each element's name string; if not found, default-constructs an ObservedTech on the stack (0x008562a0) and push_backs it (0x007b7320 @0x007ba288), then writes otnF (+0x04) and otnL (+0x06), both 16-bit, from the same source word param_1+0xc -- i.e. first-observed turn == last-observed turn on the first sighting, which is what the tag names now confirm. DIRECT CALLEE of ServerPlayer::OnTechResearched 0x00891790; also called from 0x007be228, 0x007be4e1, 0x007be535. This is the append lane P could not find. DE-DUPLICATING: appends only when no existing element carries that tech name, so a reimplementation must not naively push_back on re-observation.",
|
||||
"prototype": "void (this, ServerPlayer* observer, ?, std::string* techName) -- appends to observer->otch. Linear-searches observer->otch (ServerPlayer+0x274) with stride 0x2c comparing each element's name string; if not found, default-constructs an ObservedTech on the stack (0x008562a0) and push_backs it (0x007b7320 @0x007ba288), then writes two 16-bit fields into the new element at +0x04 and +0x06 from param_1+0xc. DIRECT CALLEE of ServerPlayer::OnTechResearched 0x00891790; also called from 0x007be228, 0x007be4e1, 0x007be535. This is the append lane P could not find.",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/observedtech-append.md (lane X, tools/x86disp.py displacement scan)"
|
||||
},
|
||||
|
|
@ -3222,89 +3222,9 @@
|
|||
"name": "ObservedTech_ctor",
|
||||
"addr": "0x008562a0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "Game::ObservedTech* (ObservedTech* this) -- default ctor. Writes exactly: vptr 0x00a2439c at +0x00; dword 0 at +0x04 (otnF+otnL zeroed together); BYTE 0 at +0x08 (`mov [esi+0x8],bl`, 0x008562f4 -- odet is a bool, not an int); the empty string at +0x0c (_Buf[0]=0, _Mysize=0, _Myres=0xf, then assign(\"\") 0x00425550); dword 0 at +0x28 (owith). It never touches +0x24 -- that word is the string's _Alval.",
|
||||
"prototype": "Game::ObservedTech* (ObservedTech* this) -- default ctor; sets vptr 0x00a2439c and empties the name string.",
|
||||
"status": "verified",
|
||||
"source": "findings/subsystems/observedtech-append.md (lane X; element map corrected + serializer read by lane S 2026-09-08)"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_Write",
|
||||
"addr": "0x00817cf0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void Game::ObservedTech::Write(Mars::Stream* s) RET 4. Vftable 0x00a2439c slot [2]. Serialises the whole object, in order and with nothing else: `otnF` = movzx word [this+0x04] via stream vft+0x24 (int); `otnL` = movzx word [this+0x06] via vft+0x24; `odet` = WriteBool 0x008b9c20 (&this[+0x08]); `otch` = WriteString 0x008b9d70 (&this[+0x0c]); `owith` = [this+0x28] via vft+0x24. THIS IS THE MEMBER MAP: there is no field at +0x24. Tag pointers 0x00a2b38c/0x00a2b384/0x00a2b36c/0x00a2b3e8/0x00a2b364.",
|
||||
"status": "verified",
|
||||
"source": "lane S own disassembly 2026-09-08"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_Read",
|
||||
"addr": "0x00817c40",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void Game::ObservedTech::Read(Mars::Stream* s) RET 4. Vftable 0x00a2439c slot [1]. Mirror of ObservedTech_Write: `otnF`/`otnL` through stream vft+0x10 (int-by-ref) stored back as 16-bit (`mov word [ebx],ax`) at +0x04/+0x06, `odet` = ReadBool 0x008b9c00 (&this[+0x08]), `otch` = ReadString 0x008b9d90 (&this[+0x0c]), `owith` at +0x28 (reached as `add edi,0x28`).",
|
||||
"status": "verified",
|
||||
"source": "lane S own disassembly 2026-09-08"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_dtor",
|
||||
"addr": "0x00793610",
|
||||
"convention": "thiscall",
|
||||
"prototype": "ObservedTech* (ObservedTech* this, int flags) RET 4. Vftable 0x00a2439c slot [0], scalar deleting dtor. Inlines ~basic_string on the name at +0x0c (`cmp [esi+0x20],0x10` -> operator delete [esi+0x0c], then _Tidy: [esi+0x20]=0xf, [esi+0x1c]=0, byte [esi+0x0c]=0), restores the base vptr 0x009e22bc, and frees `this` when flags&1. The string is the ONLY member needing destruction -- confirming there is no second string or owned pointer in the element.",
|
||||
"status": "verified",
|
||||
"source": "lane S own disassembly 2026-09-08"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_copy_ctor",
|
||||
"addr": "0x0079a184",
|
||||
"convention": "site",
|
||||
"prototype": "site inside the vector<ObservedTech> uninitialised-copy helper FUN_0079a150(&_Alval, dest, src). The inlined copy constructor writes vptr 0x00a2439c, `mov word [dst+0x04],[src+0x04]`, `mov word [dst+0x06],[src+0x06]`, `mov byte [dst+0x08],[src+0x08]`, the string at +0x0c (via basic_string::assign 0x00425430), and `mov [dst+0x28],[src+0x28]`. Nothing is copied at +0x24 -- second independent proof that +0x24 belongs to the 0x1c string, not to a data member.",
|
||||
"status": "verified",
|
||||
"source": "lane S own disassembly 2026-09-08"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_off_TurnFirst",
|
||||
"offset": "0x04",
|
||||
"convention": "offset",
|
||||
"prototype": "uint16 otnF -- turn the tech was first observed. Widened to int on disk by stream vft+0x24. Written together with otnL from one source word at RecordObservedTech 0x007ba2b0.",
|
||||
"status": "verified",
|
||||
"source": "lane S: ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_off_TurnLast",
|
||||
"offset": "0x06",
|
||||
"convention": "offset",
|
||||
"prototype": "uint16 otnL -- turn the tech was last observed. Widened to int on disk. Written at RecordObservedTech 0x007ba2c6 from the same source word as otnF.",
|
||||
"status": "verified",
|
||||
"source": "lane S: ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_off_Detected",
|
||||
"offset": "0x08",
|
||||
"convention": "offset",
|
||||
"prototype": "bool odet -- ONE BYTE (ctor `mov [esi+0x8],bl` 0x008562f4; copy-ctor `mov byte` 0x0079a1a0), serialised through WriteBool/ReadBool, so on disk it is 1 byte + 3 NUL joint padding. Note the save reader types `odet` as int; for a 4-character tag a bool item and an int item are the same 12 bytes and the value is identical, so that is benign, not a parse error.",
|
||||
"status": "verified",
|
||||
"source": "lane S: ObservedTech_Write 0x00817cf0 / ObservedTech_ctor 0x008562a0"
|
||||
},
|
||||
{
|
||||
"name": "ObservedTech_off_With",
|
||||
"offset": "0x28",
|
||||
"convention": "offset",
|
||||
"prototype": "int owith -- last member of the element; serialised through stream vft+0x24 with tag 0x00a2b364. +0x28..+0x2b is the tail of the 0x2c-byte element, which is why sizeof is 0x2c with no padding slack.",
|
||||
"status": "verified",
|
||||
"source": "lane S: ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40"
|
||||
},
|
||||
{
|
||||
"name": "ObservedWeapon_Write",
|
||||
"addr": "0x00817bc0",
|
||||
"convention": "thiscall",
|
||||
"prototype": "void Game::ObservedWeapon::Write(Mars::Stream* s) RET 4. Byte-for-byte the same shape as ObservedTech_Write with tag `owep` (0x00a2b3f0) in place of `otch`: otnF u16 @+0x04, otnL u16 @+0x06, odet bool @+0x08, owep std::string (0x1c) @+0x0c, owith int @+0x28. Reader is 0x00817b10. Independent second instance of the same 0x2c element shape.",
|
||||
"status": "verified",
|
||||
"source": "lane S own disassembly 2026-09-08"
|
||||
},
|
||||
{
|
||||
"name": "std_string_sizeof",
|
||||
"offset": "0x1c",
|
||||
"convention": "constant",
|
||||
"prototype": "sizeof(std::basic_string<char>) = 0x1c (28) binary-wide, ONE layout only: _Bx (16-byte SSO union, char[16] or char*) @+0x00, _Mysize @+0x10, _Myres @+0x14, _Alval (empty allocator) @+0x18. The allocator word is trailing, never read and never written -- the same allocator-last shape as this build's std::vector {_Myfirst,_Mylast,_Myend,_Alval} = 0x10. Evidence: (a) Stream::WriteString 0x008b9d76 `cmp [str+0x14],0x10` / `mov eax,[str]` and basic_string::assign 0x00425550 (_Mysize +0x10, _Myres +0x14) fix the field offsets; (b) the vector<std::string> scan loop FUN_00699bd0 advances `add esi,0x1c` (0x00699c29) while doing the SSO test at [esi+0x14]/[esi] -- element base == string base, so the stride IS the sizeof; (c) PostEvent 0x008862b0 takes two by-value strings at [ebp+0x08] and [ebp+0x24] and RET 0x4c = 2*0x1c + 5*4; (d) a whole-binary sweep of the Stream string helpers recovered 65 std::string members off a non-stack base across all serializers -- ZERO have any sibling member inside (N, N+0x1c), and 51 of the 52 that have a next member have it at exactly N+0x1c (the one exception, StrategyServer KeyPath @+0x134, is +0x20 on the Write side and +0x1c on the Read side, i.e. the writer skips a member). There is no 0x18 instantiation, no empty-base variant and no game-local string class.",
|
||||
"status": "verified",
|
||||
"source": "lane S 2026-09-08; scanner tools/strfootprint.py"
|
||||
"source": "findings/subsystems/observedtech-append.md (lane X, tools/x86disp.py displacement scan)"
|
||||
},
|
||||
{
|
||||
"name": "vector_44B_grow",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ d523d27, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ 1a58bcd, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -89,7 +89,7 @@ constexpr uint32_t GlobalConst_ParseColour = 0x004b7110;
|
|||
constexpr uint32_t GlobalConst_ParseRect = 0x004b7040;
|
||||
// cdecl void (float xyz[3], const char* text) /* '%f %f %f'; 5 keys (HIVER_SPAWN_*_GATE_HEIGHT / _SHIP_OFFSET ...); inferred from the M1 trace */ [verified-by-trace]
|
||||
constexpr uint32_t GlobalConst_ParseVec3 = 0x004b7080;
|
||||
// cdecl void (std::string* storage, const char* text) /* *storage = text; MSVC std::string is 0x1c bytes {+0 char buf[16] | char* ptr when capacity > 15, +0x10 _Mysize, +0x14 _Myres, +0x18 _Alval (empty allocator, never read/written)} -- see std_string_sizeof; 146 keys (*_NAME, *_SOUND, *_TEXTURENAME, DERELICT_SECTION_nn ...); seen in the M1 trace */ [verified-by-trace]
|
||||
// cdecl void (std::string* storage, const char* text) /* *storage = text; MSVC2010 std::string 0x18 bytes {+0 char buf[16] | char* ptr when capacity > 15, +0x10 size, +0x14 capacity}; 146 keys (*_NAME, *_SOUND, *_TEXTURENAME, DERELICT_SECTION_nn ...); seen in the M1 trace */ [verified-by-trace]
|
||||
constexpr uint32_t GlobalConst_ParseString = 0x004b7670;
|
||||
// cdecl void (void) /* called once from Application::Initialize; sets g_GlobalConstsLoaded */ [verified]
|
||||
constexpr uint32_t GlobalConsts_LoadAll = 0x004b76d0;
|
||||
|
|
@ -799,40 +799,20 @@ constexpr uint32_t Mars_Vec3_NormaliseEpsilon = 0x005e1ef8;
|
|||
constexpr uint32_t StrategyServer_MoveFleet_straight_leg = 0x003da0f2;
|
||||
// site site inside StrategyServer::MoveFleet /* the move != distance branch: exactly two roundings per component, tmp.c = float32(dir.c * move) then pos.c = float32(pos.c + tmp.c). `move` is reloaded from a float32 slot. The sibling branch (move == distance, an EXACT float compare) copies the destination's three words verbatim with mov, so an arrival never steps onto its destination. */ [verified]
|
||||
constexpr uint32_t StrategyServer_MoveFleet_position_update = 0x003da2ac;
|
||||
// constant sizeof(Game::ObservedTech) = 0x2c (44 bytes). Confirmed independently by (a) the compiler's magic division by 44 (mov eax,0x2e8ba2e9; imul; sar edx,3) at 0x0087239f and 0x007b7339, (b) imul reg,reg,0x2c at 0x0087243a, 0x00872468, 0x007b735b, and (c) the iterator advance `add edi,0x2c` in RecordObservedTech's linear search at 0x007ba257. FULL MEMBER MAP, from ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40 (lane S 2026-09-08): +0x00 vptr 0x00a2439c; +0x04 uint16 otnF; +0x06 uint16 otnL; +0x08 bool odet (1 byte, +3 pad); +0x0c std::string otch (0x1c, so _Mysize at +0x1c, _Myres at +0x20, _Alval at +0x24); +0x28 int owith. 4 + 2 + 2 + 4 + 0x1c + 4 = 0x2c exactly, no padding slack and no unaccounted field. [verified]
|
||||
// constant sizeof(Game::ObservedTech) = 0x2c (44 bytes). Confirmed independently by (a) the compiler's magic division by 44 (mov eax,0x2e8ba2e9; imul; sar edx,3) at 0x0087239f and 0x007b7339, (b) imul reg,reg,0x2c at 0x0087243a, 0x00872468, 0x007b735b, and (c) the iterator advance `add edi,0x2c` in RecordObservedTech's linear search at 0x007ba257. Matches the on-disk lower bound exactly (4 int + one 0x1c std::string = 44), so there is no padding slack. [verified]
|
||||
constexpr uint32_t ObservedTech_sizeof = 0x0000002c;
|
||||
// data Game::ObservedTech vftable. RTTI COL 0x00a81c78 -> type descriptor 0x00aeede4 = '.?AVObservedTech@Game@@'. Written to element+0x00 by ObservedTech_ctor 0x008562a0 -- so ObservedTech is polymorphic and its first word is the vptr, not a data field. [verified]
|
||||
constexpr uint32_t ObservedTech_vftable = 0x0062439c;
|
||||
// offset std::string (save tag `otch`, the tech name) at ObservedTech+0x0c, 0x1c bytes, spanning +0x0c..+0x27. MSVC layout relative to the string object: _Bx[16] @+0x00, _Mysize @+0x10, _Myres @+0x14, _Alval @+0x18 -- i.e. ObservedTech+0x1c is the length, +0x20 the capacity, +0x24 the empty-allocator word (never read or written by anything). Evidence: ObservedTech_ctor 0x008562a0 writes [elem+0x0c]=0, [elem+0x1c]=0, [elem+0x20]=0xf; RecordObservedTech's search reads [elem+0x1c] as the length and calls compare with this=elem+0x0c; the post-append assign uses lea ecx,[_Mylast-0x20]. CORRECTION (lane S 2026-09-08): an earlier revision called this string 0x18 bytes and listed +0x24 as an unaccounted data field. It is not one -- three independent whole-object enumerations skip +0x24 entirely: ObservedTech_ctor 0x008562a0, ObservedTech_copy_ctor 0x0079a184, and ObservedTech_Write 0x00817cf0 (which serialises +0x04,+0x06,+0x08,+0x0c,+0x28 and nothing else). sizeof(std::string)=0x1c holds binary-wide; see std_string_sizeof. [verified]
|
||||
// offset std::string (save tag `otch`, the tech name) at ObservedTech+0x0c, 0x18 bytes, spanning +0x0c..+0x23. MSVC layout relative to the string object: _Bx[16] @+0x00, _Mysize @+0x10, _Myres @+0x14 -- i.e. ObservedTech+0x1c is the length and +0x20 the capacity. Evidence: ObservedTech_ctor 0x008562a0 writes [elem+0x0c]=0, [elem+0x1c]=0, [elem+0x20]=0xf; RecordObservedTech's search reads [elem+0x1c] as the length and calls compare with this=elem+0x0c; the post-append assign uses lea ecx,[_Mylast-0x20]. NOTE this string is 0x18 bytes, not the 0x1c implied by the ServerPlayer pswd row in struct-recovery.md. UNACCOUNTED in the element: +0x08, +0x24, +0x28 (4 bytes each) plus two 16-bit fields at +0x04/+0x06 written from one source word at 0x007ba2b0 / 0x007ba2c6 -- the mapping of the four on-disk ints (otnF, otnL, odet, owith) onto those slots is NOT determined. [verified]
|
||||
constexpr uint32_t ObservedTech_off_Name = 0x0000000c;
|
||||
// thiscall void (this, ServerPlayer* observer, ?, std::string* techName) -- appends to observer->otch. Linear-searches observer->otch (ServerPlayer+0x274) with stride 0x2c comparing each element's name string; if not found, default-constructs an ObservedTech on the stack (0x008562a0) and push_backs it (0x007b7320 @0x007ba288), then writes otnF (+0x04) and otnL (+0x06), both 16-bit, from the same source word param_1+0xc -- i.e. first-observed turn == last-observed turn on the first sighting, which is what the tag names now confirm. DIRECT CALLEE of ServerPlayer::OnTechResearched 0x00891790; also called from 0x007be228, 0x007be4e1, 0x007be535. This is the append lane P could not find. DE-DUPLICATING: appends only when no existing element carries that tech name, so a reimplementation must not naively push_back on re-observation. [verified]
|
||||
// thiscall void (this, ServerPlayer* observer, ?, std::string* techName) -- appends to observer->otch. Linear-searches observer->otch (ServerPlayer+0x274) with stride 0x2c comparing each element's name string; if not found, default-constructs an ObservedTech on the stack (0x008562a0) and push_backs it (0x007b7320 @0x007ba288), then writes two 16-bit fields into the new element at +0x04 and +0x06 from param_1+0xc. DIRECT CALLEE of ServerPlayer::OnTechResearched 0x00891790; also called from 0x007be228, 0x007be4e1, 0x007be535. This is the append lane P could not find. [verified]
|
||||
constexpr uint32_t RecordObservedTech = 0x003ba1a0;
|
||||
// thiscall void (std::vector<ObservedTech>* this, ObservedTech* value) RET 4. Stride 0x2c. Calls vector_44B_grow 0x007b5820 when _Mylast==_Myend -- the realloc that moves all three vector words. Exactly one caller (RecordObservedTech), so this instantiation is not COMDAT-ambiguous. [verified]
|
||||
constexpr uint32_t vector_ObservedTech_push_back = 0x003b7320;
|
||||
// thiscall std::vector<ObservedTech>& operator=(const std::vector<ObservedTech>&) RET 4. Both callers pass ServerPlayer+0x274 (0x00878091 in the settings copy-out, 0x00892507 in the copy-in). [verified]
|
||||
constexpr uint32_t vector_ObservedTech_assign = 0x00472380;
|
||||
// thiscall Game::ObservedTech* (ObservedTech* this) -- default ctor. Writes exactly: vptr 0x00a2439c at +0x00; dword 0 at +0x04 (otnF+otnL zeroed together); BYTE 0 at +0x08 (`mov [esi+0x8],bl`, 0x008562f4 -- odet is a bool, not an int); the empty string at +0x0c (_Buf[0]=0, _Mysize=0, _Myres=0xf, then assign("") 0x00425550); dword 0 at +0x28 (owith). It never touches +0x24 -- that word is the string's _Alval. [verified]
|
||||
// thiscall Game::ObservedTech* (ObservedTech* this) -- default ctor; sets vptr 0x00a2439c and empties the name string. [verified]
|
||||
constexpr uint32_t ObservedTech_ctor = 0x004562a0;
|
||||
// thiscall void Game::ObservedTech::Write(Mars::Stream* s) RET 4. Vftable 0x00a2439c slot [2]. Serialises the whole object, in order and with nothing else: `otnF` = movzx word [this+0x04] via stream vft+0x24 (int); `otnL` = movzx word [this+0x06] via vft+0x24; `odet` = WriteBool 0x008b9c20 (&this[+0x08]); `otch` = WriteString 0x008b9d70 (&this[+0x0c]); `owith` = [this+0x28] via vft+0x24. THIS IS THE MEMBER MAP: there is no field at +0x24. Tag pointers 0x00a2b38c/0x00a2b384/0x00a2b36c/0x00a2b3e8/0x00a2b364. [verified]
|
||||
constexpr uint32_t ObservedTech_Write = 0x00417cf0;
|
||||
// thiscall void Game::ObservedTech::Read(Mars::Stream* s) RET 4. Vftable 0x00a2439c slot [1]. Mirror of ObservedTech_Write: `otnF`/`otnL` through stream vft+0x10 (int-by-ref) stored back as 16-bit (`mov word [ebx],ax`) at +0x04/+0x06, `odet` = ReadBool 0x008b9c00 (&this[+0x08]), `otch` = ReadString 0x008b9d90 (&this[+0x0c]), `owith` at +0x28 (reached as `add edi,0x28`). [verified]
|
||||
constexpr uint32_t ObservedTech_Read = 0x00417c40;
|
||||
// thiscall ObservedTech* (ObservedTech* this, int flags) RET 4. Vftable 0x00a2439c slot [0], scalar deleting dtor. Inlines ~basic_string on the name at +0x0c (`cmp [esi+0x20],0x10` -> operator delete [esi+0x0c], then _Tidy: [esi+0x20]=0xf, [esi+0x1c]=0, byte [esi+0x0c]=0), restores the base vptr 0x009e22bc, and frees `this` when flags&1. The string is the ONLY member needing destruction -- confirming there is no second string or owned pointer in the element. [verified]
|
||||
constexpr uint32_t ObservedTech_dtor = 0x00393610;
|
||||
// site site inside the vector<ObservedTech> uninitialised-copy helper FUN_0079a150(&_Alval, dest, src). The inlined copy constructor writes vptr 0x00a2439c, `mov word [dst+0x04],[src+0x04]`, `mov word [dst+0x06],[src+0x06]`, `mov byte [dst+0x08],[src+0x08]`, the string at +0x0c (via basic_string::assign 0x00425430), and `mov [dst+0x28],[src+0x28]`. Nothing is copied at +0x24 -- second independent proof that +0x24 belongs to the 0x1c string, not to a data member. [verified]
|
||||
constexpr uint32_t ObservedTech_copy_ctor = 0x0039a184;
|
||||
// offset uint16 otnF -- turn the tech was first observed. Widened to int on disk by stream vft+0x24. Written together with otnL from one source word at RecordObservedTech 0x007ba2b0. [verified]
|
||||
constexpr uint32_t ObservedTech_off_TurnFirst = 0x00000004;
|
||||
// offset uint16 otnL -- turn the tech was last observed. Widened to int on disk. Written at RecordObservedTech 0x007ba2c6 from the same source word as otnF. [verified]
|
||||
constexpr uint32_t ObservedTech_off_TurnLast = 0x00000006;
|
||||
// offset bool odet -- ONE BYTE (ctor `mov [esi+0x8],bl` 0x008562f4; copy-ctor `mov byte` 0x0079a1a0), serialised through WriteBool/ReadBool, so on disk it is 1 byte + 3 NUL joint padding. Note the save reader types `odet` as int; for a 4-character tag a bool item and an int item are the same 12 bytes and the value is identical, so that is benign, not a parse error. [verified]
|
||||
constexpr uint32_t ObservedTech_off_Detected = 0x00000008;
|
||||
// offset int owith -- last member of the element; serialised through stream vft+0x24 with tag 0x00a2b364. +0x28..+0x2b is the tail of the 0x2c-byte element, which is why sizeof is 0x2c with no padding slack. [verified]
|
||||
constexpr uint32_t ObservedTech_off_With = 0x00000028;
|
||||
// thiscall void Game::ObservedWeapon::Write(Mars::Stream* s) RET 4. Byte-for-byte the same shape as ObservedTech_Write with tag `owep` (0x00a2b3f0) in place of `otch`: otnF u16 @+0x04, otnL u16 @+0x06, odet bool @+0x08, owep std::string (0x1c) @+0x0c, owith int @+0x28. Reader is 0x00817b10. Independent second instance of the same 0x2c element shape. [verified]
|
||||
constexpr uint32_t ObservedWeapon_Write = 0x00417bc0;
|
||||
// constant sizeof(std::basic_string<char>) = 0x1c (28) binary-wide, ONE layout only: _Bx (16-byte SSO union, char[16] or char*) @+0x00, _Mysize @+0x10, _Myres @+0x14, _Alval (empty allocator) @+0x18. The allocator word is trailing, never read and never written -- the same allocator-last shape as this build's std::vector {_Myfirst,_Mylast,_Myend,_Alval} = 0x10. Evidence: (a) Stream::WriteString 0x008b9d76 `cmp [str+0x14],0x10` / `mov eax,[str]` and basic_string::assign 0x00425550 (_Mysize +0x10, _Myres +0x14) fix the field offsets; (b) the vector<std::string> scan loop FUN_00699bd0 advances `add esi,0x1c` (0x00699c29) while doing the SSO test at [esi+0x14]/[esi] -- element base == string base, so the stride IS the sizeof; (c) PostEvent 0x008862b0 takes two by-value strings at [ebp+0x08] and [ebp+0x24] and RET 0x4c = 2*0x1c + 5*4; (d) a whole-binary sweep of the Stream string helpers recovered 65 std::string members off a non-stack base across all serializers -- ZERO have any sibling member inside (N, N+0x1c), and 51 of the 52 that have a next member have it at exactly N+0x1c (the one exception, StrategyServer KeyPath @+0x134, is +0x20 on the Write side and +0x1c on the Read side, i.e. the writer skips a member). There is no 0x18 instantiation, no empty-base variant and no game-local string class. [verified]
|
||||
constexpr uint32_t std_string_sizeof = 0x0000001c;
|
||||
// thiscall vector<T>::_Reserve/grow for a 44-byte element type. Shared with FUN_0086dec0, so it may be a COMDAT-folded body -- do NOT assume it is ObservedTech-specific. [mapped]
|
||||
constexpr uint32_t vector_44B_grow = 0x003b5820;
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ call-outs cite `findings/` and `~/sots-engine/docs/`.
|
|||
|
||||
**1c. Library triage.** Apply Ghidra's VS2010 x86 FID databases (plus threatrack's) or FLIRT via ApplySig to fence off CRT/STL/D3DX code; anything left is game code. STL template instantiations compiled from headers won't match a library signature — recognise them by shape instead: MSVC-2010 `std::vector` = `{begin, end, cap}`, `std::list` = `{head*, size}`, red-black node = `{left, parent, right, key…, color/isnil tail}`, `std::string` = 0x1c bytes on x86 with the SSO union and `_Mysize`/`_Myres`.
|
||||
|
||||
> **Our experience — settled 2026-09-08.** `_Bx` starts at **+0** in this build: `_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`, sizeof `0x1c`. The lever that pins the field offsets is the `_Myres >= 16 ? _Ptr : _Buf` branch in any string consumer (`Stream::WriteString` @ 0x008b9d70). The lever that pins the **size** is different and easy to miss: `_Alval` is an empty allocator, so it is never loaded or stored and no touch-based analysis can see it. Size the type from an *enumeration* instead — a container stride (`vector<string>` walk `add esi,0x1c` @ 0x00699c29), a by-value argument's frame spacing (`PostEvent` strings at `[ebp+8]`/`[ebp+0x24]`, `RET 0x4c`), or a copy constructor / serializer that lists every member. Sizing from touched fields undercounts by exactly the trailing allocator word — that is how a lane got 0x18 for the same type. Note the allocator is **trailing** in this build's `std::vector` too (`{_Myfirst,_Mylast,_Myend,_Alval}` = 0x10), which is the opposite of the MSVC `_String_val`/`_Vector_val` allocator-first shape you will read about; verify per binary, do not trust a table.
|
||||
> **Our experience:** our own notes disagree on whether `_Bx` starts at +0 or +4 (allocator stub) in this build (`struct-recovery.md` §0 vs `turn-spine.md` §1.1). The reliable lever is the `_Myres >= 16 ? _Ptr : _Buf` branch in any string consumer (`Stream::WriteString` @ 0x008b9d70), which pins both offsets. Verify per binary; do not trust a table.
|
||||
|
||||
**1d. Strings are the map.** Config keys, file names, log format strings and — decisively — **serialization tag names**. If the save format is tagged, each `Read/Write` references dozens of a struct's tag strings in order; rank functions by distinct tag xrefs, decompile, and read `this+offset → tag → type` straight off. This is the single highest-yield lever for the object model, and it is under-documented in the literature (see §4).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,210 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Whole-binary audit of the std::string member footprint in Sword of the Stars.exe.
|
||||
|
||||
Why this exists
|
||||
---------------
|
||||
`sizeof(std::string)` is load-bearing: it is embedded in ~65 recovered class
|
||||
layouts, and a 4-byte error in it silently shifts every field that follows.
|
||||
Lane X's ObservedTech work reported the embedded string as 0x18 bytes, against
|
||||
the campaign-wide 0x1c. Arguing about it from one class is how you get a
|
||||
plausible answer instead of a true one, so this settles it from the whole
|
||||
binary at once.
|
||||
|
||||
Method
|
||||
------
|
||||
Every serialiser in this exe hands a member pointer to one of the Mars::Stream
|
||||
primitive helpers with the same idiom:
|
||||
|
||||
push 0xff ; default arg
|
||||
lea <r>,[<base>+<disp>] ; &this->member
|
||||
push <r>
|
||||
push <tag> ; -> .rdata "otch", "pswd", ...
|
||||
push <stream>
|
||||
call WriteString / ReadString / WriteBool / ...
|
||||
|
||||
So for each helper call we recover (base register, displacement, tag). Then,
|
||||
per (function, base register), we take every OTHER displacement the same
|
||||
function touches off that register. If sizeof(std::string) were 0x18 for some
|
||||
instantiation, that class would necessarily have a real member somewhere in
|
||||
(N+4 .. N+0x1b). The audit is: does one exist, anywhere?
|
||||
|
||||
Two things must be excluded or the answer is noise:
|
||||
* ebp/esp bases -- those are stack temporaries, not class members. (A local
|
||||
string at [ebp-0x2c] shows _Mysize at -0x1c and _Myres at -0x18, which
|
||||
looks exactly like two "members" inside the span.)
|
||||
* N+0x10 and N+0x14 -- the string's OWN _Mysize/_Myres, read inline whenever
|
||||
the compiler inlines the SSO `_Myres >= 16 ? _Ptr : _Buf` test.
|
||||
|
||||
Result (2026-09-08, lane S): 65 string members off a non-stack base, ZERO with
|
||||
a sibling inside the 0x1c span, and 51 of the 52 that have a next member have
|
||||
it at exactly +0x1c. One layout, 0x1c, binary-wide.
|
||||
|
||||
Usage: uv run python3 tools/strfootprint.py [--all]
|
||||
--all also lists every recovered string member and its gap.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.dirname(HERE)
|
||||
sys.path.insert(0, HERE)
|
||||
from x86disp import decode, Desync, load_pe, EXE, FUNCS # noqa: E402
|
||||
|
||||
# Mars::Stream primitive wrappers (struct-recovery.md S0). All take
|
||||
# (stream, tagname, &member, default) and are called from the class Read/Write.
|
||||
HELPERS = {
|
||||
0x008b9d70: "WriteString", 0x008b9d90: "ReadString",
|
||||
0x008b9c20: "WriteBool", 0x008b9c00: "ReadBool",
|
||||
0x008b9be0: "WriteFloat", 0x008b9bc0: "ReadFloat",
|
||||
0x008b9d50: "WriteInt", 0x008b9d20: "ReadInt",
|
||||
0x008b9d00: "WriteInt16", 0x008b9cd0: "ReadInt16",
|
||||
0x008b9c60: "WriteInt64", 0x008b9c40: "ReadInt64",
|
||||
}
|
||||
STRING_HELPERS = ("WriteString", "ReadString")
|
||||
STR_SIZE = 0x1c
|
||||
SSO_OWN = (0x10, 0x14) # the string's own _Mysize / _Myres
|
||||
STACK_BASES = ("ebp", "esp")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- .rdata reader
|
||||
def _load_rdata():
|
||||
data = open(EXE, "rb").read()
|
||||
pe = struct.unpack_from("<I", data, 0x3C)[0]
|
||||
nsec = struct.unpack_from("<H", data, pe + 6)[0]
|
||||
optsz = struct.unpack_from("<H", data, pe + 20)[0]
|
||||
base = struct.unpack_from("<I", data, pe + 24 + 28)[0]
|
||||
secs = []
|
||||
for i in range(nsec):
|
||||
o = pe + 24 + optsz + i * 40
|
||||
vsz, va, rsz, ro = struct.unpack_from("<IIII", data, o + 8)
|
||||
secs.append((base + va, vsz, ro, rsz))
|
||||
return data, secs
|
||||
|
||||
|
||||
_DATA, _SECS = _load_rdata()
|
||||
|
||||
|
||||
def cstr(va, maxn=16):
|
||||
for sva, vsz, ro, rsz in _SECS:
|
||||
if sva <= va < sva + max(vsz, rsz):
|
||||
d = va - sva
|
||||
if d >= rsz:
|
||||
return None
|
||||
o = ro + d
|
||||
e = _DATA.find(b"\0", o, o + maxn)
|
||||
if e < 0:
|
||||
return None
|
||||
try:
|
||||
return _DATA[o:e].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
show_all = "--all" in sys.argv
|
||||
_, secs = load_pe(EXE)
|
||||
sva, eva, buf, _ = secs[0]
|
||||
funcs = json.load(open(FUNCS))
|
||||
starts = sorted((int(k, 16), v[0]) for k, v in funcs.items())
|
||||
svas = [s[0] for s in starts]
|
||||
rd_lo, rd_hi = 0x9DD000, 0xAD9000
|
||||
|
||||
records, touch = [], {}
|
||||
for idx, (fva, nm) in enumerate(starts):
|
||||
if not (sva <= fva < eva):
|
||||
continue
|
||||
fend = svas[idx + 1] if idx + 1 < len(starts) else eva
|
||||
ins, i, end = [], fva - sva, fend - sva
|
||||
while i < end:
|
||||
try:
|
||||
ln, info = decode(buf, i, len(buf))
|
||||
except Desync:
|
||||
break
|
||||
ins.append((sva + i, ln, info, buf[i:i + ln]))
|
||||
i += ln
|
||||
if not any(r[0] == 0xE8 and l == 5 and
|
||||
(v + 5 + struct.unpack_from("<i", r, 1)[0]) in HELPERS
|
||||
for v, l, _, r in ins):
|
||||
continue
|
||||
for v, l, info, r in ins:
|
||||
if info and info["base"] is not None and info["disp"] is not None:
|
||||
touch.setdefault((fva, info["base"]), set()).add(info["disp"])
|
||||
for k, (v, l, _info, r) in enumerate(ins):
|
||||
if not (r[0] == 0xE8 and l == 5):
|
||||
continue
|
||||
tgt = v + 5 + struct.unpack_from("<i", r, 1)[0]
|
||||
if tgt not in HELPERS:
|
||||
continue
|
||||
tag, ptr_reg = None, None
|
||||
for j in range(k - 1, max(-1, k - 15), -1):
|
||||
rr, ll = ins[j][3], ins[j][1]
|
||||
if rr[0] == 0x68 and ll == 5:
|
||||
imm = struct.unpack_from("<I", rr, 1)[0]
|
||||
if rd_lo <= imm < rd_hi:
|
||||
s = cstr(imm)
|
||||
if s and 1 <= len(s) <= 8 and s.isprintable():
|
||||
tag = s
|
||||
for m in range(j - 1, max(-1, j - 6), -1):
|
||||
r2, l2 = ins[m][3], ins[m][1]
|
||||
if 0x50 <= r2[0] <= 0x57 and l2 == 1:
|
||||
ptr_reg = ["eax", "ecx", "edx", "ebx",
|
||||
"esp", "ebp", "esi",
|
||||
"edi"][r2[0] - 0x50]
|
||||
break
|
||||
break
|
||||
base = disp = None
|
||||
if ptr_reg:
|
||||
for m in range(k - 1, max(-1, k - 25), -1):
|
||||
i2 = ins[m][2]
|
||||
if i2 and i2["lea"] and i2["reg"] == ptr_reg \
|
||||
and i2["base"] is not None:
|
||||
base, disp = i2["base"], i2["disp"]
|
||||
break
|
||||
records.append((fva, nm, base, disp, tag, HELPERS[tgt]))
|
||||
|
||||
strs = [r for r in records if r[5] in STRING_HELPERS]
|
||||
members, stack, unresolved = [], 0, 0
|
||||
for fva, nm, base, disp, tag, helper in strs:
|
||||
if base is None or disp is None:
|
||||
unresolved += 1
|
||||
elif base in STACK_BASES:
|
||||
stack += 1
|
||||
else:
|
||||
members.append((fva, nm, base, disp, tag, helper))
|
||||
|
||||
violations, gaps, seen = [], {}, set()
|
||||
for fva, nm, base, disp, tag, helper in members:
|
||||
others = sorted(d for d in touch.get((fva, base), set())
|
||||
if d > disp and (d - disp) not in SSO_OWN)
|
||||
inside = [d for d in others if d < disp + STR_SIZE]
|
||||
if inside:
|
||||
violations.append((fva, nm, base, disp, tag, helper, inside))
|
||||
if others:
|
||||
g = others[0] - disp
|
||||
gaps[g] = gaps.get(g, 0) + 1
|
||||
if show_all and (fva, base, disp, helper) not in seen:
|
||||
seen.add((fva, base, disp, helper))
|
||||
nxt = f"+0x{others[0]:x}" if others else "-"
|
||||
print(f"0x{fva:08x} {nm:26s} {base} +0x{disp:<6x} {tag!r:12s} "
|
||||
f"{helper:12s} next={nxt}")
|
||||
|
||||
print(f"\nstring helper call sites : {len(strs)}")
|
||||
print(f" resolved to a class member offset : {len(members)}")
|
||||
print(f" stack temporaries (ebp/esp base) : {stack}")
|
||||
print(f" offset not reached by a plain lea : {unresolved}")
|
||||
print(f"\nmembers with a sibling inside (N, N+0x{STR_SIZE:x}) "
|
||||
f": {len(violations)} <-- must be 0 for sizeof(std::string)==0x1c")
|
||||
for v in violations:
|
||||
print(f" 0x{v[0]:08x} {v[1]} {v[2]}+0x{v[3]:x} {v[4]!r} "
|
||||
f"inside={[hex(x) for x in v[6]]}")
|
||||
print("\ngap from a string member to the next member on the same base:")
|
||||
for g in sorted(gaps):
|
||||
print(f" +0x{g:<4x} : {gaps[g]}")
|
||||
return 1 if violations else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
#!/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 <shim.trace.jsonl> [...]
|
||||
"""
|
||||
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()
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
#!/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 <a.jsonl[.gz]> <b.jsonl[.gz]> [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("<I", struct.pack("<f", float(v)))[0]
|
||||
|
||||
|
||||
def f64_bits(v):
|
||||
return struct.unpack("<Q", struct.pack("<d", float(v)))[0]
|
||||
|
||||
|
||||
def norm(o):
|
||||
if isinstance(o, dict):
|
||||
t = o.get("t")
|
||||
if t == "ptr":
|
||||
return {"t": "ptr", "n": o.get("n")}
|
||||
if t == "f32":
|
||||
return {"t": "f32", "n": o.get("n"), "bits": f32_bits(o["v"])}
|
||||
if t == "f64":
|
||||
return {"t": "f64", "n": o.get("n"), "bits": f64_bits(o["v"])}
|
||||
return {k: norm(v) for k, v in o.items() if k not in NOISE}
|
||||
if isinstance(o, list):
|
||||
return [norm(x) for x in o]
|
||||
if isinstance(o, str) and PTR.match(o):
|
||||
return "<ptr>"
|
||||
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())
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
== 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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,31 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,31 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,31 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,31 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,31 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,24 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
Binary file not shown.
|
|
@ -1,50 +0,0 @@
|
|||
===== 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]
|
||||
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
===== 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
|
||||
|
||||
|
|
@ -165,20 +165,6 @@ satl satk`).
|
|||
Objective records (Player `odes`/`owep`/`otch`, each a framed VectorHelper of `"."` elements):
|
||||
`odes` = `otnF otnL odid opid`; `owep` = `otnF otnL odet owep`(string) `owith`; `otch` = `otnF otnL odet
|
||||
otch`(string) `owith`.
|
||||
|
||||
> **Confirmed against the binary (2026-09-08, lane S).** `otch` elements are
|
||||
> `Game::ObservedTech` (`Write` `0x00817cf0`, `Read` `0x00817c40`, `sizeof` `0x2c`) and `owep`
|
||||
> elements are `Game::ObservedWeapon` (`Write` `0x00817bc0`, `Read` `0x00817b10`) — the same
|
||||
> element shape with a different string tag. The serializers emit exactly the order this file
|
||||
> already had. In-memory types, which the on-disk framing does not reveal: `otnF` and `otnL` are
|
||||
> **`uint16`** widened to int by the stream (`movzx` then stream `vft+0x24`); `odet` is a
|
||||
> **`bool`** written through `WriteBool`, i.e. 1 byte plus 3 NUL joint-padding bytes, not an
|
||||
> int32. `save_reader.py` types `odet` as `int`, which is **benign and deliberate**: `odet` is a
|
||||
> 4-character tag, so a bool item and an int item are both `pad4(4+4+1) == pad4(4+4+4) == 12`
|
||||
> bytes and the little-endian value is identical (§2's stated "bool and int have identical item
|
||||
> sizes for a name whose length is a multiple of 4"). Re-typing it would change nothing on
|
||||
> disk; it is recorded here so the *reimplementation* uses a `bool`, and so nobody later
|
||||
> "discovers" the same 3 padding bytes as a missing field.
|
||||
`NdGr2` (node grid): `paths{"." n, n×"."{npt npid npfr npto npctm npcby npdtn npdtf npenp npuse nptf}}`
|
||||
then `nextid`(int).
|
||||
`BQ` (system build queue): `ords{"." n, n×"."{desID con conleft sav ordID}}` — count 0 in turn1..3.
|
||||
|
|
|
|||
|
|
@ -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 — MEASURED 2026-09-08, no budget needed
|
||||
### 3.5 The x64/SSE budget, and the one thing this cannot settle
|
||||
|
||||
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,43 +281,28 @@ 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.
|
||||
|
||||
**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/`.
|
||||
**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.
|
||||
|
||||
| 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.
|
||||
**The experiment that would settle it** (VM140, lane R):
|
||||
|
||||
> 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue