141 lines
7.5 KiB
Markdown
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`
|