U: live verification - 0 divergences on 35 calls, three workloads
First End Turn 3/3/0, five-turn continuation 15/15/0, Zuul 20/20/0, tracecmp exit 0 on all three. The End-Turn oracle hashes are unchanged, so the cascade does not perturb the game. All 22 divergent fields lane V recorded are gone. The prediction in section 4 held field for field on the deterministic half; call 9 turned out to be a different completion from lane V's (the AI picked another target from turn 5), which the model reproduced anyway - three unlock costs that appear in no earlier report. Honest limit: roll_draws was 0 on all 35 calls. ResearchRollPending is normally consumed by ProcessTurn before ProcessResearch runs, so the RollResearchEvent draw is modelled and inside the compare but has never been seen to fire.
This commit is contained in:
parent
405ba41a1e
commit
4e729212ba
2 changed files with 147 additions and 2 deletions
147
docs/U-unlock.md
147
docs/U-unlock.md
|
|
@ -282,4 +282,149 @@ together.
|
|||
|
||||
## 5. Outcome
|
||||
|
||||
To be filled in from the run.
|
||||
Build `unlock-405ba41-20260908T1026Z`, cross-built on CT111
|
||||
(`/srv/re-lab/build/sots-engine-u`, exports byte-identical to the real `binkw32.dll`), staged
|
||||
`C:\SOTS\shimdist-u` on VM140. Recipe `shim.cfg.recapb3`, unchanged.
|
||||
|
||||
**The residual is gone. All three runs are 0 divergent, `tracecmp` exit 0.**
|
||||
|
||||
| run | calls | compared | diverged | exit |
|
||||
|---|---|---|---|---|
|
||||
| first End Turn (`ref-turn2` → Launch → End Turn) | 3 | 3 | **0** | 0 |
|
||||
| five-turn continuation (turn 2 → 7) | 15 | 15 | **0** | 0 |
|
||||
| Zuul (`zuul-turn5` → 10 End Turns, turn 5 → 15) | 20 | 20 | **0** | 0 |
|
||||
|
||||
Reports `verify/results/compare/unlock-b3-{t1,t1-5,zuul}.md` in the RE repo; traces
|
||||
`verify/traces/unlock-b3-*`; shim log `verify/results/shim/unlock-shim.log`.
|
||||
|
||||
### 5.1 The oracle first
|
||||
|
||||
`(Autosave EndTurn).sav` = `bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948`,
|
||||
`(Autosave).sav` = `978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921` — **the same two
|
||||
hashes lane R and lane V recorded**. The cascade does not perturb the running game. That check
|
||||
comes first because a clean compare from a build that moved the game would be worthless.
|
||||
|
||||
### 5.2 First End Turn — §4.1 held field for field
|
||||
|
||||
3 calls, 0 divergent, exit 0, and every predicted argument:
|
||||
|
||||
| | predicted | observed |
|
||||
|---|---|---|
|
||||
| `observed_techs_in` | 10 / 20 / 20 | **10 / 20 / 20** ✓ |
|
||||
| `roll_pending_in` (call 0) | false | **false** ✓ |
|
||||
| `order_counter_in` (call 0) | ≥ 0, not −1 | **22** ✓ |
|
||||
| `research_target` | non-null | `0x049812f8` ✓ |
|
||||
| `observed_scan_failed` / `_truncated` | absent | absent ✓ |
|
||||
| shim log, all 3 calls | `ok=1 completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0` | exactly that ✓ |
|
||||
|
||||
The collector ran on all three and came back empty, which is the §3.2 check: it is only silent
|
||||
because nothing became available, not because it was not asked.
|
||||
|
||||
### 5.3 Five turns — §4.2 held on the deterministic half, and the rest was better than predicted
|
||||
|
||||
Call 3 is exactly the call lane P and lane V pinned, and every number matched:
|
||||
|
||||
| call 3 | predicted | observed |
|
||||
|---|---|---|
|
||||
| `events.next_id` | 5 → **7** | 5 → **7** ✓ |
|
||||
| `node[132]` state / cost_rp / turn_available | 0→2 / INT_MAX→10000 / −1→4 | ✓ |
|
||||
| `node[136]` | 0→2 / INT_MAX→16000 / −1→4 | ✓ |
|
||||
| `node[142]` | 0→2 / INT_MAX→8000 / −1→4 | ✓ |
|
||||
| `node[144]` state / turn_researched / order | 3→4 / 4 / **22** | ✓ |
|
||||
| `observed_techs.bytes` | 440 → **484** | ✓ |
|
||||
| `order_counter_in` | **22** | **22** ✓ |
|
||||
| `roll_pending_in` | **false** | **false** ✓ |
|
||||
| `observed_techs_in` | **10** | **10** ✓ |
|
||||
| shim log | `completions=1 unlocked=3 otch_appends=1 roll_draws=0` | exactly that ✓ |
|
||||
|
||||
**Call 9 was not the call I predicted, and that is the more interesting result.** From turn 5 the
|
||||
AI picked a different research target than in lane V's session — lane R's documented trap #2, and
|
||||
the reason the board calls the continuation *a* run and not *the* run. Lane V's call 9 completed
|
||||
tech 142 and unlocked one node; mine completed **tech 9** and unlocked **three**:
|
||||
|
||||
```
|
||||
node[9] state 3->4 progress 3064->6000 turn_researched -1->6 order -1->23
|
||||
node[3] state 0->2 cost_rp INT_MAX->13000 turn_available -1->6
|
||||
node[12] state 0->2 cost_rp INT_MAX->35000 turn_available -1->6
|
||||
node[18] state 0->2 cost_rp INT_MAX->4000 turn_available -1->6
|
||||
```
|
||||
|
||||
`next_id` 10 → **12** and `observed_techs.bytes` 484 → **528** as predicted, but the three costs —
|
||||
13000, 35000, 4000 — appear in no earlier report and were not predicted by anyone. The model
|
||||
reproduced them with zero divergences on a case it had never seen. An unrehearsed instance is
|
||||
worth more than a rehearsed one, and it is the answer to "did the model just memorise lane V's
|
||||
numbers": it cannot have, because lane V's numbers are not what happened.
|
||||
|
||||
Call 12 then allocates to node 18 — the 4000-cost tech the cascade had just unlocked — which is
|
||||
the cascade's output feeding the next turn's budget.
|
||||
|
||||
### 5.4 One prediction missed: the guard count
|
||||
|
||||
Predicted **10 undeclared writes in 2 calls**, observed **9 in 2 calls**, and the span list is not
|
||||
the same one:
|
||||
|
||||
```
|
||||
lane V: player+0x10c:3 +0x110:3 +0x114:3 +0x124:3 +0x294:4 +0x196:1 +0x3b4:1 tree_header+0x20:1
|
||||
lane U: player+0x10c:3 +0x110:3 +0x114:3 +0x124:3 +0x294:4 +0x130:3 tree_header+0x20:1
|
||||
```
|
||||
|
||||
This is the workload difference, not the code. `player+0x3b4` is gone because the pending-roll
|
||||
byte was **already 0** when the completing call ran, so the original never wrote it (see §5.5).
|
||||
`+0x196` (design-option mask B) is replaced by `+0x130` (`PopMod`) because a different tech
|
||||
completed and different tech effects fired. Both are `OnTechResearched`'s writes and both are
|
||||
declared unmodelled. `tree_header+0x20` is still there and still undeclared, exactly as §4.3 said
|
||||
it would be: `ours` seeds the counter and advances its own copy, it does not model the live word.
|
||||
|
||||
Coverage verdict `partial` with **8** unmodelled notes, as predicted.
|
||||
|
||||
### 5.5 The `RollResearchEvent` draw — modelled, inside the compare, but NOT exercised
|
||||
|
||||
This is the honest limit of the run, and it should not be read as more than it is.
|
||||
|
||||
`roll_draws` was **0 on all 35 compared calls**. Not one completion in three runs had the pending
|
||||
byte still set. The reason is visible in the trace and it is a real finding: `ResearchRollPending`
|
||||
is normally consumed by `ServerPlayer::ProcessTurn` *before* `ProcessResearch` runs, because that
|
||||
call site fires when the progress ratio crosses a threshold — which is precisely the turns
|
||||
approaching completion. In the Zuul run `roll_pending_in` is `true` on the funded call for turns
|
||||
8–12 and flips to **false** on turn 13, the turn before the tech completes on turn 14.
|
||||
|
||||
So lane V's call-9 draw was the *rare* case (a tech that jumped past the threshold to completion
|
||||
in one turn), not the normal one — which also explains lane R's "RNG matched 15 of 15". Three
|
||||
sessions, three different answers: 0 extra draws, 1, and 0.
|
||||
|
||||
What can be claimed: the draw is implemented at the right point in the stream (inside the owner
|
||||
callback, between this entry's roll and the next entry's), its two inputs are read pre-call and
|
||||
**reported in every record** (`research_target`, `roll_pending_in`), and `region:rng` compared
|
||||
clean on all 35 calls — so the model is not drawing a word the original does not. What cannot be
|
||||
claimed: that the branch has been seen to fire live. **The boundary is inside the compare, but the
|
||||
compare has not yet had the chance to test it.** It needs a workload where a tech goes from below
|
||||
the ProcessTurn threshold to complete in a single turn.
|
||||
|
||||
### 5.6 Zuul — the double roll and the completion path, together
|
||||
|
||||
20 calls, 0 divergent, exit 0, turn 5 → 15, **two** Zuul completions. Call 2 is the one lane V's
|
||||
save was one End Turn short of:
|
||||
|
||||
```
|
||||
call 2 turn 7 species 5 alloc {144, 1376} left 449 -> 447 next_index 175 -> 177
|
||||
node[144] state 3->4 turn_researched 7 order 21
|
||||
node[132] state 0->2 cost_rp INT_MAX->10000 turn_available 7
|
||||
node[136] state 0->2 cost_rp INT_MAX->16000 turn_available 7
|
||||
events.next_id 10 -> 12 observed_techs 396 -> 440
|
||||
```
|
||||
|
||||
The generator advances by **two** (the species-5 double roll) *and* the completion cascade runs,
|
||||
in the same call, and `ours` reproduces the post-state bit for bit. Note the Zuul tree unlocks
|
||||
only 132 and 136 from tech 144 where the Human tree also unlocks 142 — a per-species tree, so this
|
||||
is an independent instance of the cascade and not a repeat of §5.3. A second completion follows on
|
||||
call 16 (turn 14, `next_id` 26 → 28, `observed_techs` 440 → 484).
|
||||
|
||||
### 5.7 What is now owed
|
||||
|
||||
* The `RollResearchEvent` draw needs a workload that fires it (§5.5).
|
||||
* `TechTree::GetProgressRatio` (0x0057e950) — the COMPLETE/UNDERBUDGET split is still analogy.
|
||||
Count-neutral, so no run can see it.
|
||||
* `def+0xb0`'s write site. The byte is read and honoured; where it comes from is not read.
|
||||
* The `0x00889d60` branch behind the research-event roll, the temperance sweep, `PruneOldTurns`,
|
||||
and replace mode on this hook — all still untouched.
|
||||
* The ObservedTech element's own fields. `ours` decides the append; it does not build the element.
|
||||
|
|
|
|||
|
|
@ -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 @ 545c715, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ ae00053, 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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue