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, newwatch.mode=snlv, configshim.cfg.l1snlv, build0117495-dirty-20260908T2113Z - Answers:
findings/subsystems/nvo-tshn-visible-owner.md§6 — "One arming line (sys+0x24in 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:
- Level 3 occurs exactly where the
AFlagsbit is set, on all eight rows — which is theIsVisibleToearly-out ofSensorPhase_ComputeContactLeveldoing what lane W3 read. - 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 tozuul-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'sSnLv, so one launch and one End Turn is the whole experiment. - One system watched, 28 printed. The 20
SnLvwrites and the 4TShnwrites are Spica's. Every other system's row above is a read at arm time, not a watched write. SnLvnever 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.
ShareVisibleOwnerstill 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
SnLvdecoded, the base probe, the canary):verify/results/shim/l1/watch-arm.txt - Hit log:
verify/results/shim/l1/watch-hits.txt - Instrument:
sots-enginewip/l1,src/shim/hooks/watchpoints.cpp(watch.mode=snlv), configsshim.cfg.l1snlv/shim.cfg.l1control