Z: reconcile with lane J's rules 16/17, add the Zuul ledger table
Lane J landed rule 16 (inlined draws are invisible to call-graph sweeps) while this run was in flight, and it bears directly on three claims here that rested on direct-call sweeps. Those claims now rest on the behavioural measurement instead, which is immune to it: ProcessNodeSpaceTravel moved the generator by 0 words on 16 observations and node-line decay on 8. The instrument does not ask which function drew, only whether the generator moved. Lane J's resolver map and this ledger pair up: the resolver has no unconditional draw, so its prediction -- a plain fleet battle costs the same 18-22 words as a peaceful turn -- is testable with these hooks as soon as someone builds a save where two hostile fleets meet.
This commit is contained in:
parent
1d50f1edda
commit
c24bf6e7e0
1 changed files with 53 additions and 17 deletions
|
|
@ -9,9 +9,9 @@ autosave that byte-matches the original's*. Generator state is part of the saved
|
|||
and that both run **before** the autosave, and concluded that a reimplementation reproducing both
|
||||
`ProcessTurn` functions exactly would still diverge. Nobody had measured what a turn actually costs.
|
||||
|
||||
**The answer, up front.** On the reference save, a strategic turn advances the strategic generator by
|
||||
**18–20 words**, *all* of it inside `StrategyServer::ProcessTurn`, and the residual outside the two turn
|
||||
drivers is **exactly zero**. The tail's cost on these turns is **0**. The defect lane K found is real and
|
||||
**The answer, up front.** Across eight measured End Turns on two saves, a strategic turn advances the
|
||||
strategic generator by **18–22 words**, *all* of it inside `StrategyServer::ProcessTurn`, and the residual
|
||||
outside the two turn drivers is **exactly zero**. The tail's cost on these turns is **0**. The defect lane K found is real and
|
||||
**latent**: it will bite the first turn a node line expires or a real battle resolves, and our saves reach
|
||||
neither.
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ neither.
|
|||
|
||||
Three sentences, then the evidence.
|
||||
|
||||
1. A turn on the reference save costs **18–20 generator words**, all of it inside
|
||||
1. A turn costs **18–22 generator words**, all of it inside
|
||||
`StrategyServer::ProcessTurn`; the two autosave files bracket exactly that interval and nothing draws
|
||||
between them outside the two turn drivers.
|
||||
2. `verify/results/shim/tailrng/z-t6-endturn.sav` → `z-t6-autosave.sav` is a **byte-identical oracle pair
|
||||
|
|
@ -41,11 +41,23 @@ commissioned says by how much. The image has **four** draw entry points, not thr
|
|||
| 0x0047d830 | `Mars::RNG::NextFloat(&mt)` — `__thiscall`, no stack args, plain `ret` | **exactly 1** |
|
||||
| 0x004271c0 | `Mars::RNG::NextInt(&mt, uint* pMax)` — `ret 4`, rejection loop, bound re-read each iteration | **1 or more** |
|
||||
| 0x008e6dd0 | `Mars::RNG::Chance(RNG*, float p)` — `ret 4`; takes the **object** base and does the `add ecx,4` itself | **0 or 1** — see §6.1 |
|
||||
| **0x004f7670** | **`Mars::RNG::NextUInt(RNG*)`** — plain `ret`, no stack args. **In no previous lane's primitive set**, and called from 0x007b6700 inside `ProcessTurn`'s closure | 1 |
|
||||
| **0x004f7670** | **`Mars::RNG::NextUInt(RNG*)`** — plain `ret`, no stack args, 84 B. **In no previous lane's primitive set**, and called from 0x007b6700 inside `ProcessTurn`'s closure | 1 |
|
||||
|
||||
plus **inlined draws in twelve functions**, two of them reachable from the turn roots (0x007aa240 under
|
||||
`ProcessTurn`, 0x007a7f30 under the combat resolver). A primitive-counting hook would have missed all of
|
||||
those silently.
|
||||
(Lane J's scan reaches 0x004f7670 from the other direction and calls it one of the fourteen *inlined-draw*
|
||||
functions rather than a primitive. Both readings are of the same object and neither is wrong: it is a small
|
||||
function whose draw is inlined, so it is a draw site that calls no primitive — which is exactly why a
|
||||
call-graph sweep for `NextFloat`/`NextInt` cannot see it.)
|
||||
|
||||
plus **inlined draws**: this lane's scan for the MT tempering immediates found twelve such functions, two of
|
||||
them reachable from the turn roots (0x007aa240 under `ProcessTurn`, 0x007a7f30 under the combat resolver).
|
||||
Lane J's independent scan, landed while this run was in flight, found **fourteen** and turned it into method
|
||||
rule 16 — *inlined draws are invisible to call-graph sweeps, so any RNG accounting built from the call graph
|
||||
alone is a lower bound.* Take lane J's number over this one; the two scans agree on the shape and the point
|
||||
is the same either way. A primitive-counting hook would have missed every one of them silently.
|
||||
|
||||
**This instrument is not built from the call graph**, which is why rule 16 does not apply to it: it reads
|
||||
the generator's state before and after a boundary and reports the difference. An inlined draw, a draw in a
|
||||
function nobody has named, a draw through a vtable — all of them move `left`, and all of them are counted.
|
||||
|
||||
So the instrument reads **state**, not calls. `Mars::RNG` is
|
||||
`{void* vptr; uint32 mt[624]; uint32* next; int32 left}`, `sizeof 0x9cc` — `next` at +0x9c4 is a **pointer**
|
||||
|
|
@ -97,7 +109,16 @@ file, which is exactly the interval a standalone has to reproduce. The turn-3 br
|
|||
construction** and is reported rather than dropped: the pre-turn autosave of the first End Turn after a load
|
||||
runs before any turn driver, so the hook has no server pointer yet and its record carries `words: null`.
|
||||
|
||||
**So: we consume 18–20 words per turn and model 0 of them.**
|
||||
A second save, `zuul-turn16-noderoute` (Zuul vs Zuul), build `z-tailrng2-20260908T1328Z`:
|
||||
|
||||
| turn | `ProcessTurn` | tail | `ApplyEncounterResult` | node-line decay | `ProcessNodeSpaceTravel` ×2 | **bracket total** | **residual** |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 17 | **20** | 0 | 0 | 0 | 0, 0 | (incomplete) | — |
|
||||
| 18 | **20** | 0 | 0 | 0 | 0, 0 | **20** | **0** |
|
||||
| 19 | **22** | 0 | 0 | 0 | 0, 0 | **22** | **0** |
|
||||
| 20 | **20** | 0 | 0 | 0 | 0, 0 | **20** | **0** |
|
||||
|
||||
**So: we consume 18–22 words per turn and model 0 of them.**
|
||||
|
||||
Not 0 because the modelling is bad — because *nothing in the repo models any part of a turn's RNG
|
||||
consumption as a count*. B1/B3/B4 compare the generator's state around three specific functions and get it
|
||||
|
|
@ -254,10 +275,19 @@ by construction.
|
|||
|
||||
## 8. What is not settled, listed as loudly as the results
|
||||
|
||||
* **The combat resolver 0x007d5af0 (7499 B) has still never run under an instrument.** Every encounter this
|
||||
workload produced had the no-battle flag set, so `ApplyEncounterResult` was a no-op every time. Its
|
||||
measured 0 words says nothing whatever about combat's RNG cost. **The largest unmeasured term in the turn
|
||||
is untouched**, and the residual-0 result above holds only for turns with no battle.
|
||||
* **The combat resolver has still never run under an instrument.** Every encounter this workload produced
|
||||
had the no-battle flag set, so `ApplyEncounterResult` was a no-op every time; its measured 0 words says
|
||||
nothing whatever about combat's RNG cost, and the residual-0 result above holds only for turns with no
|
||||
battle.
|
||||
|
||||
Lane J read all 7,641 bytes of it in parallel with this run (`combat-resolver.md`; and note **7,641**, not
|
||||
the 7,499 Ghidra reports — method rule 17). Its conclusion pairs with this one exactly: **the resolver has
|
||||
no unconditional draw.** All three sites in its subtree are conditional — a node-cannon `NextInt`, an
|
||||
inlined `NextFloat` per back-engineering candidate, and a `NextInt` per successful roll of that. So lane
|
||||
J's cheap first prediction is directly testable with this instrument: **a plain fleet battle with no node
|
||||
cannon and no salvage should cost the same 18–22 words as a peaceful turn.** That is the next run this
|
||||
hook family should do, and it needs a workload nobody has built yet: a save where two hostile fleets
|
||||
actually meet.
|
||||
* **Node-line expiry did not fire.** See §9 for the quantified distance rather than an absence.
|
||||
* **A turn with a genuinely empty encounter vector was not observed** (§3). Every turn of `ref-turn2` in
|
||||
contact produces exactly one sighting encounter. The tail-runs-every-turn claim is settled; the
|
||||
|
|
@ -268,10 +298,16 @@ by construction.
|
|||
paid for once, at `S+0x64`). **Nothing in the ledger depends on it** — it is decoration on the argument
|
||||
record — but it should not be reused until someone resolves it.
|
||||
* **Which of the twelve-to-fourteen `S+0x8` increments per turn come from where** (§5).
|
||||
* The direct-call sweeps behind "node-line decay's downstream pair draws nothing" and
|
||||
"`ProcessNodeSpaceTravel` draws nothing" do not model indirect calls. Both are now **also** behavioural
|
||||
facts on this workload (0 words, eight observations of `ProcessNodeSpaceTravel` and four of node-line
|
||||
decay), which is the stronger evidence of the two.
|
||||
* The direct-call sweeps behind "node-line decay's only RNG site is the `Chance(0.5f)`", "its downstream
|
||||
pair draws nothing" and "`ProcessNodeSpaceTravel` draws nothing" are **worth less than they look**, and
|
||||
method rule 16 (landed by lane J while this run was in flight) says why: an inlined draw leaves no
|
||||
call-graph edge at all, so a call sweep is a lower bound. Rule 17 applies too — those sweeps clipped at
|
||||
Ghidra's reported function sizes.
|
||||
|
||||
**The behavioural measurement is what carries these claims, not the sweeps.** `ProcessNodeSpaceTravel`
|
||||
moved the generator by 0 words on **16** observations (twice per turn, eight turns) and node-line decay by
|
||||
0 on **8**. That evidence is immune to both rules, because it does not ask which function drew — it asks
|
||||
whether the generator moved.
|
||||
* **The ledger's block-chain machinery has never run live.** Every observation in both runs sat inside a
|
||||
single MT block — `left` walked 432 → 413 → 395 → 375 → 357 on `ref-turn2` and 263 → 243 → 223 → 201 → 181
|
||||
on the Zuul save, never reaching 0. So every live word count reduces to `left_before − left_after`, and the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue