sots-re/findings/subsystems/snlv-measured.md

141 lines
7.5 KiB
Markdown

# `SnLv` measured — the sensor branch is what refreshes Spica, and the contact level is 2, not 1
- **Type:** subsystem (live measurement)
- **Status:** **verified** for Spica; `nvo-tshn-visible-owner.md` §6's first bullet moves **[H] → [V]**
- **Confidence:** high. Hardware data-write watchpoint on the word itself, with the canary self-test
passing and the determinism oracle reproduced byte for byte in the same run.
- **Owner / date:** lane L1 · 2026-09-08 · VM140
- **Instrument:** `src/shim/hooks/watchpoints.cpp`, new `watch.mode=snlv`, config `shim.cfg.l1snlv`,
build `0117495-dirty-20260908T2113Z`
- **Answers:** `findings/subsystems/nvo-tshn-visible-owner.md` §6 — *"One arming line
(`sys+0x24` in slot 1 instead of `_Mysize`) settles it on the next VM run."* It did.
---
## 0. The result
`ServerSystem::IsKnownTo` is `AFlags bit || (PlyrIdx < 15 && ((SnLv >> 2*PlyrIdx) & 3) != 0 &&
HasResearched(CCC_AdvSens))`. Lane W3 could not decide whether Spica's `NVO` record is refreshed
through the `AFlags` term or the sensor term, because **`SnLv` is not serialised** and had never been
measured.
On `ref-turn2.sav`, at the arming point, **Spica has `AFlags = 0x0` and `SnLv = 0x00000200`**, i.e.
contact level **2** for player index 4 — the very player its single `NVO` record is keyed to.
`AFlags = 0` makes the first term false. The sensor term is therefore the only way `IsKnownTo` can
return true for that pair, and the record is refreshed. **The sensor branch is what refreshes Spica**,
measured rather than inferred.
## 1. The whole map, because it costs no debug register
Printing `SnLv` and its decoded per-player 2-bit fields for every system during the arming sweep is
free. All 28 systems of `ref-turn2.sav`, at `ApplyAllTurnCommands` entry on the End Turn from turn 2
(only the non-zero rows shown; the other 20 systems read `SnLv = 0`):
| system | `AFlags` | `SnLv` | non-zero levels |
|---|---|---|---|
| Hyperion | 0x10 | 0x00000300 | player 4 → **3** |
| Gamma Cephei | 0x01 | 0x00000003 | player 0 → **3** |
| **Spica** | **0x00** | **0x00000200** | **player 4 → 2** |
| Ke'Dolarra | 0x02 | 0x0000000c | player 1 → **3** |
| Koa?Vo | 0x80 | 0x0000c000 | player 7 → **3** |
| Kaa?Vaalu | 0x10 | 0x00000300 | player 4 → **3** |
| Markab | 0x10 | 0x00000300 | player 4 → **3** |
| Kea?Pono | 0x10 | 0x00008300 | player 4 → **3**, player 7 → **2** |
| Ko'Rorkor | 0x10 | 0x00000300 | player 4 → **3** |
Two regularities fall out and neither was available from any save:
1. **Level 3 occurs exactly where the `AFlags` bit is set**, on all eight rows — which is the
`IsVisibleTo` early-out of `SensorPhase_ComputeContactLevel` doing what lane W3 read.
2. **Level 2 occurs exactly where it is not** — Spica for player 4, Kea'Pono for player 7. So the
non-visible contact state on this save is **2**, and **level 1 was never observed**.
## 2. A correction: `ComputeContactLevel` does not return 1 there
`ghidra/addresses.d/lane-w3.json` records
`SensorPhase_ComputeContactLevel` as *"3 if IsVisibleTo (AFlags), else 1 if inside p's sensor
envelope, else 0"*. The function has **four** return tails, read at instruction boundaries:
```
00850d18 mov eax,3 ; IsVisibleTo -> 3
00850e2f mov eax,ecx ; ecx = setne on an FPU range compare -> 0 or 1
00850e47 lea eax,[edx+2] ; edx = setne on the same compare -> 2 or 3
00850e52 mov eax,2 ; -> 2
```
which pair is taken is selected by `cl` and by `[ebx+0x10]`. So the value set is `{0,1,2,3}` and the
`{0,1}` pair is one of three tails, not the only alternative to 3. The measurement above lands on a
`2` tail. **`IsKnownTo` only tests `!= 0`, so nothing downstream changes** — but the constant in the
prototype is wrong and would mislead anyone reimplementing the sensor phase.
## 3. Who writes it, and how often
DR1 watched Spica's `sys+0x24` for the whole End Turn: **20 writes**, every one from the same
instruction.
| trap EIP (Ghidra VA) | store site | value written | count |
|---|---|---|---|
| 0x0086a78c | **0x0086a789** `mov [edi+0x24],edx` | 0x00000200 every time | **20** |
0x0086a789 is exactly the pack site lane W3 named from static reading — the trap reports the
instruction after the store, and 0x0086a789 + 3 = 0x0086a78c. Confirmed to the byte. The value never
changed across the turn: the recompute is idempotent on this workload, which is why the *absence* of
`SnLv` from the wire costs nothing on these saves and why no save could ever have shown it.
The enclosing loop at 0x0086a761..0x0086a79b walks `GetGame()->Systems` and calls
`ComputeContactLevel` once per system, masking `3 << (2*idx)` out and OR-ing the result in, guarded
by `[ebp-0x10] >= 0xf` — the `PlyrIdx < 15` test lane W3 read in `IsKnownTo`, here on the writing
side. Twenty full sweeps of the systems vector happen in one End Turn.
## 4. `TShn` refreshed, in the same run, twice
DR0 watched Spica's `NVO` record at `root+0x10` (`{int16 touched, int16 TShn}`): **4 writes**, in two
pairs, all with a `SetVisibleOwner` frame:
| seq | trap EIP (VA) | word after the store | reading |
|---|---|---|---|
| 8 | 0x0075b95a | `0x00020003` | `touched` := 3 (the new `Frame`), `TShn` still 2 |
| 9 | 0x0075b965 | `0x00030003` | `TShn` := 3 |
| 20 | 0x0075b95a | `0x00030003` | same pair again |
| 21 | 0x0075b965 | `0x00030003` | |
Return addresses 0x0075bd4a / 0x0075bdc3 with 0x0086a806 on the stack put the caller chain in the
sensor phase, next to the `SnLv` writer. This confirms three things lane W3 stated as a model:
`SetVisibleOwner` stamps `touched` and `TShn` in **two separate stores**; it runs **twice per turn**
for one (system, player) pair; and on this turn both are stamped with the new `Frame`.
## 5. Coverage — what this does NOT settle (rule 15)
* **Bismol is not on this map.** `nvo-tshn-visible-owner.md` §6's open question — *which* conjunct
fails at Bismol — belongs to `zuul-turn23-fleet23.sav`, a different game with a different star
list. This run settles the Spica half of that bullet and leaves the Bismol half open. The same
config on the Zuul save answers it: the arming sweep prints every system's `SnLv`, so one launch
and one End Turn is the whole experiment.
* **One system watched, 28 printed.** The 20 `SnLv` writes and the 4 `TShn` writes are Spica's. Every
other system's row above is a **read at arm time**, not a watched write.
* **`SnLv` never changed value** during the turn, so nothing here says what a *transition* looks like
or when a contact is lost.
* **Level 1 was never observed.** §2's four-tail reading is static; only 0, 2 and 3 have been seen.
* **Per-thread blind spot.** Debug registers are per-thread and the arm is on the turn thread only.
* **`ShareVisibleOwner` still has never executed** — no corpus save has an alliance (unchanged).
## 6. Instrument neutrality (rule 19)
The armed run **is** the oracle run: `ref-turn2.sav`, one End Turn, four watchpoints live and ~24
exception deliveries, plus the five lane-L1 hooks and lane Z's ledger:
| file | size | sha256 prefix | historical |
|---|---|---|---|
| `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3b` | **identical** |
| `(Autosave).sav` | 67,219 | `978041acd168b56e` | **identical** |
Canary self-test: `writes=1 traps=1 dr7=0xdddd0055 PASS`.
## 7. Files
- Arming log (all 28 systems with `SnLv` decoded, the base probe, the canary):
`verify/results/shim/l1/watch-arm.txt`
- Hit log: `verify/results/shim/l1/watch-hits.txt`
- Instrument: `sots-engine` `wip/l1`, `src/shim/hooks/watchpoints.cpp` (`watch.mode=snlv`),
configs `shim.cfg.l1snlv` / `shim.cfg.l1control`