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

7.5 KiB

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