273 lines
17 KiB
Markdown
273 lines
17 KiB
Markdown
# Lane H — five live probes, predicted before the build
|
||
|
||
Lane H, 2026-09-08. Worktree `wip/probes` off `main` `b2bad30`. Every number below was written
|
||
**before** the shim was built and before any run (method rule 2). Each section ends with the ways
|
||
the prediction can fail and the symptom of each, so a wrong prediction is informative rather than
|
||
merely wrong.
|
||
|
||
The lane exists because of method rule 18: five open questions that are one probe away and that
|
||
other lanes have been circling statically.
|
||
|
||
---
|
||
|
||
## P1 — the determinism oracle, regenerated
|
||
|
||
Lane N's End Turns overwrote the three autosave *contents* on VM140. The file set is unchanged, so
|
||
every documented Load-dialog row position still holds, and `ref-turn2.sav` on the VM still hashes
|
||
`ab4ac2d7e2977260…` (checked before the run). What is gone is the byte-identical reference pair.
|
||
|
||
**P1. Loading `ref-turn2.sav` and pressing End Turn once, under build
|
||
`recap-7584bad-20260908T0615Z` with `hooks=trace`, writes**
|
||
|
||
```
|
||
(Autosave EndTurn).sav 66,732 B bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948
|
||
(Autosave).sav 67,219 B 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921
|
||
```
|
||
|
||
These are the values `findings/subsystems/determinism-oracle.md` recorded on 2026-09-07 across five
|
||
runs in two processes, and which lane R reproduced under the recap build.
|
||
|
||
**Why this is worth a run of its own.** Everything since has changed the process the oracle runs in:
|
||
a different shim build with nineteen template hooks and seven entry-point detours installed, an
|
||
x87 control-word module, a Zuul game played to turn 64 in the same `SavedGames` directory, and eight
|
||
save files where there were four. If the pair still reproduces, the oracle is revalidated **across
|
||
all of that**, and every Phase-2 comparison that leans on it keeps its footing.
|
||
|
||
**Falsification.**
|
||
|
||
* *Either hash differs.* Then the oracle is not stable across the session's changes and nothing
|
||
downstream of it can be trusted until the cause is found. Candidate causes, in the order they
|
||
would be checked: the installed `binkw32.dll` is not the recap build (hash it); `shim.cfg` is not
|
||
the recap trace config (read it); `display.cfg` / `Profile_re.ini` changed (they are inputs to
|
||
the process, not to the save, so this would be a *new* finding); `ref-turn2.sav` is not the file
|
||
it was (already excluded — hashed before the run).
|
||
* *Only `(Autosave).sav` differs.* The pre-turn marker is written from loaded state and the
|
||
post-turn one from computed state, so a pre-turn match with a post-turn mismatch localises the
|
||
problem to the turn computation rather than to the load or the writer.
|
||
* *Only `(Autosave EndTurn).sav` differs.* That would be the more alarming of the two: it is the
|
||
load → save round trip, which `determinism-oracle.md` proved stable for the *pre-turn* form, and
|
||
it would mean the loader or the writer moved.
|
||
|
||
---
|
||
|
||
## P2 — the four tail callees that can draw, and have never been seen to
|
||
|
||
Lane V2 closed lane K's tier-4 blind spot: of the eleven phase-23/33 vtable targets on
|
||
`StrategyServer+0x158` / `+0x15c`, **four** reach a draw on the strategic generator at
|
||
`StrategyServer+0x16c`, at eight instructions —
|
||
|
||
| outer callee | slot | inner | draw sites |
|
||
|---|---|---|---|
|
||
| `ServerSpyManager_vslot13` 0x008877b0 | spy 13 | — | `Chance` 0x00887c8a |
|
||
| " | " | 0x008408e0 | `Chance` 0x00840929, `Chance` 0x00840a3c, `NextInt` 0x008409c7 |
|
||
| `ServerSpyManager_vslot14` 0x0088db80 | spy 14 | — | `Chance` 0x0088dc43 |
|
||
| `ServerTradeManagerImpl_vslot15` 0x0082cca0 | trade 15 | — | `Chance` 0x0082cdb8 |
|
||
| `ServerTradeManagerImpl_vslot13` 0x0088ef80 | trade 13 | 0x00820ca0 | `NextFloat` 0x00820e18 |
|
||
| " | " | 0x0088b440 | `NextInt` 0x0088b613 |
|
||
|
||
Lane Z measured **0 tail words on 8 turns**, so `tail-rng-ledger.md`'s "the tail draws nothing" is a
|
||
property of *eight turns*, not of the code. Our corpus has no live trade routes and no spy program.
|
||
|
||
**The distinction this probe exists to make** is the one a word count cannot: *not reached* versus
|
||
*reached and gated*. "Never observed firing" is currently ambiguous between them, and the two have
|
||
opposite consequences for a reimplementation — the first says the phase is conditional, the second
|
||
says the phase always runs and the gate is a data condition our saves never satisfy.
|
||
|
||
**P2a. Each of the four outer callees is entered exactly once per End Turn, on every turn, on every
|
||
save.** The phase-23 and phase-33 dispatch sites (0x007d97a7 … 0x007d9811, 0x007d989b, 0x007d98a8)
|
||
are unconditional `call [vptr+n]` in a straight-line block, so entry is not a function of game
|
||
state at all. Entry counters read `calls = 1` per End Turn for each of 0x008877b0, 0x0088db80,
|
||
0x0082cca0, 0x0088ef80.
|
||
|
||
**P2b. On the existing corpus none of the eight draw sites fires.** No `draw_sites` row appears with
|
||
`ret_rva` in {0x00887c8f, 0x0084092e, 0x00840a41, 0x008409cc, 0x0088dc48, 0x0082cdbd, 0x00820e1d,
|
||
0x0088b618} (the return address is the site plus the five bytes of the `call`), and the tail's
|
||
bracket cost stays 0 — reproducing lane Z's number under a strictly better instrument.
|
||
|
||
**P2c. The three inner functions 0x008408e0, 0x00820ca0 and 0x0088b440 are entered 0 times.** This
|
||
is the load-bearing half. Together with P2a it says the gate is *inside* the outer callee, above the
|
||
inner call — i.e. an empty container (no spy programs, no trade routes) short-circuits before any
|
||
draw is reached — and it localises the condition to one function each rather than to "somewhere in
|
||
a 200-function closure".
|
||
|
||
**Falsification.**
|
||
|
||
* *An outer callee is entered 0 times.* Then phase 23/33 is itself conditional, and lane V2's
|
||
"reached ONLY virtually, from `<site>`" needs a gate note added: reachability from that site is
|
||
not the same as being called. This is the single most useful way for P2 to be wrong.
|
||
* *An inner is entered but draws 0.* The gate is below the inner call, not above it. The word count
|
||
is still 0 and lane Z's table is unchanged, but the map moves.
|
||
* *Any of the eight sites fires on the existing corpus.* Then `tail-rng-ledger.md` §2's "the tail's
|
||
cost on these turns is 0" is still true of those eight turns and false of ours, and the ledger
|
||
needs the tail term after all — the standalone's generator model would be short by that many
|
||
words on exactly the turns that matter.
|
||
* *An entry counter reads more than 1 per turn.* The dispatch block would then be inside a loop
|
||
nobody has read, which changes the cost model from "0 or 1 draw per site" to "per iteration".
|
||
|
||
### P2b-alt — an amendment, written after P2b and before any run
|
||
|
||
Everything above was committed before I read the four callee bodies. I then read them (to name the
|
||
three inner functions for `addresses.d`), and **one of the four does not look like the other three**.
|
||
This is recorded as a competing prediction rather than folded into P2b, because retrofitting P2b
|
||
after seeing the code would destroy the test.
|
||
|
||
Read from the decompiled bodies, each confirmed to be a real function start with exactly one caller:
|
||
|
||
| callee | the loop it is | empty when |
|
||
|---|---|---|
|
||
| `ServerTradeManagerImpl_vslot15` 0x0082cca0 | over the vector at `tradeManager+0x3c .. +0x40` | no trade routes |
|
||
| `ServerSpyManager_vslot14` 0x0088db80 | over the vector at `spyManager+0x10 .. +0x14` | no spy programs |
|
||
| `ServerSpyManager_vslot13` 0x008877b0 | 1,869 bytes, same manager, not fully read | — |
|
||
| **`ServerTradeManagerImpl_vslot13` 0x0088ef80** | **over `GetServer()+0x64 .. +0x68` — the FLEETS vector** | **no fleets** |
|
||
|
||
The first two are the shape P2b assumes: an empty container short-circuits the loop and no draw is
|
||
reachable, which is exactly "no trade routes and no spy program". **The fourth is not.** Its loop
|
||
iterates fleets — every save has fleets — and the draw is gated by a chain of per-fleet tests inside
|
||
the body (`0x0088f036` calls 0x00820ca0, whose result gates `0x0088f042`'s call to 0x0088b440).
|
||
|
||
**P2b-alt. On a save with fleets, `ServerTradeManagerImpl_vslot13`'s loop body runs, and probe row 5
|
||
(0x00820ca0) fires with a non-zero count — which means a `NextFloat` at 0x00820e18 and therefore a
|
||
NON-ZERO TAIL WORD COUNT.** If so, `tail-rng-ledger.md`'s "the tail's cost on these turns is 0" is
|
||
not merely workload-limited, it is limited by a per-fleet gate that our two ledger saves happened to
|
||
fail, and the Zuul save named for having 23 fleets is the obvious place to look.
|
||
|
||
P2b and P2b-alt disagree about the same run, which is what makes it worth making. If P2b holds on a
|
||
23-fleet save, the gate is above 0x00820ca0 and the fleet loop is not the reason. If P2b-alt holds,
|
||
the four-callee blind spot is not "no trade, no spies" — it is one gate on one fleet property, and
|
||
the ledger needs the tail term now rather than after a manufactured workload.
|
||
|
||
**What this probe cannot settle without a manufactured workload.** Whether the sites draw *when*
|
||
trade routes or spy programs exist. That needs a save with both, and building one is the expensive
|
||
part of this lane; if it is not built, P2 downgrades from "the tail never draws" to "the tail never
|
||
draws on a game with no trade and no espionage", which is a **narrower** claim than the one in the
|
||
ledger today and must be written that way (rule 6).
|
||
|
||
---
|
||
|
||
## P3 — `DetectEncounters`, the last non-research generator residual
|
||
|
||
Lane Y's standalone models 16 of a turn's ~20 words; the residual on its two calibrated pairs is 4
|
||
and 2, and **2 words on both are encounter detection** — sites 0x0050329d (`NextFloat`, depth 4) and
|
||
0x007929a4 (`NextInt`, depth 3), each 1 call and 1 word per turn on all three measured turns. Lane Y
|
||
refused to model it: *"fitting 2/turn to three observations is not a model."*
|
||
|
||
Lane I bounds the containing family at `|contacts| × |detectors|` and established that
|
||
`EncounterDetect_AssignContacts` draws one inlined `NextFloat` **per (contact, detector) trial,
|
||
before the accept test** — so an unteched detector has threshold `0.0f`, burns a word, and can never
|
||
accept. But lane Z's site run found `AssignContacts` **never ran**: the gate in
|
||
`EncounterDetect_ProcessTeamRecord` (0x007ca640) was not satisfied on any measured turn.
|
||
|
||
So the probe is on the **caller**. `EncounterDetect_ProcessTeamRecord` is bracketed with the three
|
||
quantities lane I derived from its instruction stream, computed at hook entry from the record:
|
||
|
||
* `gate` — ∃ entry of `rec->(+0x28 .. +0x2c)`, stride 0x44, with `entry[0]->+0xfc != 0`;
|
||
* `contacts` — entries whose object has `+0xfc != 0`;
|
||
* `detectors` — entries whose object has `+0xfc == 0` **and** `+0xfb == 0`;
|
||
* `bound` = `contacts × detectors`, the `AssignContacts` worst case.
|
||
|
||
**P3a. `ProcessTeamRecord` is called once per 0x74-byte `TeamRecord`** in the vector
|
||
`EncounterDetect_Run` receives — the same `StrategyServer+0x1e8` vector the tail hooks count as
|
||
`encounters`. On `ref-turn2` that vector holds **1** record by the time the tail runs, so the
|
||
expected call count per turn is small and single-digit.
|
||
|
||
**P3b. The turn's 2 detection words are consumed inside `ProcessTeamRecord`.** Both residual sites
|
||
sit at depth 3 and 4 below `DetectEncounters`, and `ProcessTeamRecord` is the only depth-2 function
|
||
on that path. The bracket over it therefore reads **2 words per turn**, and the detection residual
|
||
outside it is **0**.
|
||
|
||
**P3c — the one that changes the map. The 2 words are *not* the `AssignContacts` family.** `gate`
|
||
reads **false** on every record, `AssignContacts` is not called, and the `|contacts| × |detectors|`
|
||
bound is **irrelevant to the observed residual**. If that holds, lane Y's 2-word residual and lane
|
||
I's bound are two different things that have been discussed as one, and the residual is a *fixed*
|
||
per-record cost (one `NextInt` + one `NextFloat`) somewhere above the gate — which is a formula, and
|
||
a much cheaper one than the family bound.
|
||
|
||
**Falsification.**
|
||
|
||
* *`ProcessTeamRecord` costs 0 words.* Then the two residual sites are reached by some other path
|
||
out of `DetectEncounters` and P3b's depth reasoning is wrong; the next probe is a bracket on
|
||
`DetectEncounters` itself.
|
||
* *`gate` is true and `AssignContacts` fires.* Excellent outcome, not a failure: `contacts` and
|
||
`detectors` are then both recorded live and the bound becomes checkable against a measured word
|
||
count for the first time. P3c is wrong and lane I's family is the residual after all.
|
||
* *The cost is 2 per record rather than 2 per turn.* Then it scales with the record count and the
|
||
standalone needs `2 × |records|`, not `+2`. This is distinguishable only if some turn has more
|
||
than one record — which is exactly why the call count is recorded, not assumed.
|
||
* *My computed `contacts`/`detectors` disagree with the callee's own when `AssignContacts` does
|
||
fire.* Then lane I's classification rules are wrong or the record layout is, and the hook says so
|
||
by printing both numbers side by side rather than only mine.
|
||
|
||
---
|
||
|
||
## P4 — `CreateRaidEncounter`: separating two hypotheses a word count cannot
|
||
|
||
A *successful* trade-raid roll calls `ServerTradeManagerImpl` slot 17,
|
||
`ServerTradeManager_CreateRaidEncounter` 0x008938a0, which draws a `NextInt` at 0x008939ee to pick a
|
||
target but returns at 0x0089391c **without drawing** when the candidate vector is empty. It cost 0
|
||
words on all three of lane Z's measured turns.
|
||
|
||
Lane Z read that as "consistent either with no roll succeeding (≈11% on the defaults, so three quiet
|
||
turns in a row is unremarkable) or with an empty candidate list every time".
|
||
|
||
**A correction to that reading, before the run (rule 11).** 11% is the probability of a quiet
|
||
*turn*, not of three. With eight player entries and the image defaults the per-turn probability that
|
||
no roll at all succeeds is `0.8^8 × 0.95^8 = 0.1113`; three consecutive quiet turns is `0.00138`, or
|
||
about **1 in 720**. The two hypotheses were never equally likely. The expected number of successes
|
||
per turn is `8 × 0.2 + 8 × 0.05 = 2.0`.
|
||
|
||
**P4. `CreateRaidEncounter` is entered on essentially every turn — about 2 calls per turn — and
|
||
draws 0 words because the candidate list is empty.** Entry counter ≥ 1 on at least two of three
|
||
turns; no `draw_sites` row at `ret_rva` 0x008939f3.
|
||
|
||
**Falsification.**
|
||
|
||
* *0 entries across three turns.* Under the model that is a 1-in-720 event, so it would mean the
|
||
model is wrong, not that we were unlucky. Two concrete candidates, both checkable next: the odds
|
||
are not the image defaults on this save (a `StrategyVar` override in the game data), or one of the
|
||
two per-player skip gates lane Z found in `GenerateTradeRaidEncounters` (the `visitedMask` bit
|
||
test at 0x00893302, the `>2` pre-filter at 0x008933e0) suppresses the *call* while still spending
|
||
the `Chance` word — which would also mean the 16 words are not 16 independent rolls.
|
||
* *Entries > 0 and words > 0.* Then a raid target was picked, the candidate list is non-empty, and
|
||
the standalone needs the extra word **and** the target-selection draw — the row lane Y flagged as
|
||
"the one that will bite" becomes live rather than theoretical.
|
||
* *Entries far above 2 per turn.* The roll is not the only caller, or the odds are higher than the
|
||
defaults.
|
||
|
||
---
|
||
|
||
## P5 — the Zuul word count: what the raid loop actually iterates
|
||
|
||
Lane Z's free prediction, restated by lane Y as a consequence of its model: the two `Chance` sites in
|
||
`GenerateTradeRaidEncounters` cost one word per entry of `StrategyServer::Players`, which holds
|
||
**8** on the Human saves and **7** on the Zuul ones (`Sim.NumPlrs`: `#empires + one rebel AI per
|
||
distinct empire species + 4 NPC pseudo-players`). Lane N ran a Zuul End Turn but with the RNG hooks
|
||
**off**, so it does not answer this.
|
||
|
||
**P5. On a Zuul save each of 0x00893426 and 0x00893513 records `calls = 7`, `words = 7`, for 14
|
||
words at the trade-raid block instead of 16**, and the turn total is `14 + 2 (detection) + 0–2
|
||
(research)` = **16–18**, against 18–22 on the Human save.
|
||
|
||
**Falsification, and this is the point of the probe.**
|
||
|
||
* *8 calls per site on a 7-entry player vector.* The loop is not iterating the player vector. The
|
||
next candidates, in order: the `TeamRecord` vector it is handed as an argument, a species list, or
|
||
a fixed NPC count plus empires. This is the outcome that would matter most — lane Y's
|
||
`trade_raid.cpp` counts `NumPlrs` and would be wrong on every save.
|
||
* *7 calls at one site and 8 at the other.* The two sites have different loop bounds; the NPC site's
|
||
player-independent gate (`0.0f < S->+0x1a0`) is already known to be all-or-nothing per turn, so
|
||
this would mean something else again.
|
||
* *Fewer than 7.* One of the two per-player skip gates fires on the Zuul save and never fired on the
|
||
Human one — which would make the 16 an upper bound rather than the hard bound lane Z reported.
|
||
|
||
---
|
||
|
||
## What is being measured, and what is not
|
||
|
||
Every hook in this lane declares the strategic generator and **nothing else**, exactly as lane Z's
|
||
family does. None of them makes any claim about whether the turn was computed correctly. The entry
|
||
counters are register-transparent asm stubs that log and tail-jump to the trampoline: they cannot
|
||
be wrong about a calling convention because they never assume one, and they cannot report a word
|
||
count, only a call count — which is deliberate, because the word counts already come from two
|
||
independent instruments that agree.
|
||
|
||
A clean run here is a statement about RNG accounting and coverage, and about nothing else.
|