diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 39bf7fc..e1bd15a 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -347,7 +347,14 @@ search cannot find a *schedule*. Fan-in: only **41 of 116** records have craft (2×24, 4×1, 8×4, 18×12 = 300). ⇒ **an arrival is craft appearing for a record that had none; a kill is that count decreasing — both attributable to a named squadron**, hence to a route. - 🔴 **BLOCKER (2026-08-24): the per-record measurement does not reproduce.** + ✅ **Blocker downgraded (2026-08-24): the baseline DOES reproduce** — two more + runs give 116 roster records flat from t=0 (not a load race) with craft + declining 300→288 and 296→280, so **losses are observable**. Raw hits == + distinct VAs (116 == 116), so the VA-aliasing explanation is 🔴 refuted too. + 🟡 The single 42/170 run stays unexplained; rule adopted: **discard a run that + disagrees with 116 rather than interpreting it, and reproduce any finding in + ≥2 runs.** ❔ No arrival seen in any run yet. + Earlier framing: [`mission-per-record-strength.md`](mission-per-record-strength.md) — one run gives 116 records/300 craft, the next 42/170, same disc, save and script. Save drift is REFUTED (savedata untouched since 2026-08-23) and the guest was not diff --git a/docs/re/mission-per-record-strength.md b/docs/re/mission-per-record-strength.md index 2a351f2..50f5d76 100644 --- a/docs/re/mission-per-record-strength.md +++ b/docs/re/mission-per-record-strength.md @@ -72,3 +72,62 @@ Every sample was identical: 170 craft, 24 deployed, no `0 → n`, no `n → n− 2. **Get a confirmed kill.** Without one, the kill-versus-no-kill experiment cannot run. The pilot reaches firing range and does not destroy anything, so the gap is accuracy, not engagement. + +--- + +# The 42-vs-116 outlier does not reproduce (2026-08-24) + +Status: ✅ the baseline is stable and it is **not** a load race; 🔴 the VA-aliasing +explanation is refuted; 🟡 the outlier itself stays unexplained; ✅ losses are +observable. + +`tools/re-capture/census_probe.py` samples the raw counts every 12–20 s from the +instant flight is detected, and also reports what the scan is looking at. + +## ✅ Two further runs, both stable at 116 + +``` + t roster distVA craft defs extents MB + 0 116 116 300 14 272 342.2 + 60 116 116 294 14 262 342.3 + 120 116 116 292 14 258 342.4 + 168 116 116 288 14 258 342.4 +``` + +and a second run, 116 throughout, craft 296 → 280. + +**Not a load race.** The roster count is 116 in the very first sample and never +moves — it does not climb toward 116 from below, which is what a race would look +like. Definitions are 14 throughout, matching Stage 02's 14 unit types. + +## 🔴 Refuted: VA aliasing was not the cause + +The leading hypothesis was arithmetic rather than behavioural: `census_probe` +counts **raw aligned vtable hits**, while `wave5_probe` counted **distinct +`primary_va` values**, and if one offset can alias to a VA another offset also +claims, those are simply different numbers. Measuring both in the same run +settles it: **116 raw hits, 116 distinct VAs, in every sample**. They are the +same number here, so the difference in what was counted cannot explain 42. + +## 🟡 The outlier stands, unexplained + +Four runs now: 116/300, 116/300, 116/296 — and one 42/170. Same disc, save, +script and stage; save drift, freezing, load race and VA aliasing are all +individually refuted. I do not know what produced it. + +What that changes in practice: the baseline is trustworthy enough to build on, +but **any single run that disagrees with 116 should be discarded rather than +interpreted**, and a result must be reproduced in at least two runs before it is +written down as a finding. Recorded rather than quietly dropped, because a +one-in-four anomaly in the measurement apparatus is exactly the kind of thing +that later turns into a wrong conclusion. + +## ✅ Losses are observable; arrivals still are not + +Craft declines steadily and monotonically in both runs — 300 → 288 and +296 → 280, roughly 16–20 losses over 168 s under the hunting pilot. Whether the +player or the NPC crossfire is responsible is not determined here; what matters +is that **destruction is visible in this signal**. + +No arrival has yet been seen in any run. The per-record `0 → n` watch is still +the right instrument, and it can now be run against a known-good baseline. diff --git a/tools/re-capture/census_probe.py b/tools/re-capture/census_probe.py new file mode 100755 index 0000000..29e48fe --- /dev/null +++ b/tools/re-capture/census_probe.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Why does the roster-record count differ between runs (116 vs 42)? + +Samples the counts repeatedly WITHIN one run, from the instant flight is +detected, and also reports what the scan is actually looking at: + + * roster records -- vtable 0x820AF030 hits + * craft -- unit-definition-pointer sites in the entity heap + * definitions -- vtable 0x820AF844 hits + * extents/bytes -- what gmem.extents() enumerates, to test whether the scan + region itself differs between runs + +A count that climbs = a load race. A count flat at a run-specific value = not a +race, and the cause is in the guest, not the probe. +""" +import os, sys, time, struct, collections +sys.path.insert(0, __file__.rsplit('/', 1)[0]) +import gmem, gworld, entities2 + +ROSTER_VT = struct.pack('>I', 0x820AF030) +DEF_VT = struct.pack('>I', 0x820AF844) +DELTA = 0x130 + +def count_vt(fd, size, vt, want_vas=False): + """Raw aligned hits, and optionally how many DISTINCT primary VAs they map + to. wave5 printed the latter and census the former; if an offset can alias to + a VA another offset also claims, the two are different numbers and that alone + would explain 42 vs 116.""" + n = 0; vas = set() + for a, b in gmem.extents(fd, size): + pos = a + while pos < b: + m = min(1 << 24, b - pos) + blob = os.pread(fd, m, pos) + i = blob.find(vt) + while i != -1: + if (pos + i) % 4 == 0: + n += 1 + if want_vas: + v = gmem.primary_va(pos + i) + if v is not None: vas.add(v) + i = blob.find(vt, i + 1) + pos += m + return (n, len(vas)) if want_vas else n + +def extent_stats(fd, size): + n = tot = 0 + for a, b in gmem.extents(fd, size): + n += 1; tot += b - a + return n, tot + +def craft_count(fd, defs): + lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI) + n, pos = 0, lo + while pos < hi: + m = min(1 << 24, hi - pos) + blob = os.pread(fd, m, pos) + for k in range(0, len(blob) - 3, 4): + if blob[k:k+4] in defs: n += 1 + pos += m + return n + +def main(): + secs = int(sys.argv[1]) if len(sys.argv) > 1 else 180 + every = int(sys.argv[2]) if len(sys.argv) > 2 else 12 + w = gworld.World(); fd = w.fd; size = w.size + t0 = time.time() + print('%5s %8s %8s %7s %6s %8s %10s' % + ('t', 'roster', 'distVA', 'craft', 'defs', 'extents', 'MB')) + while time.time() - t0 < secs: + el = round(time.time() - t0) + defs = entities2.definitions(w) + r, rvas = count_vt(fd, size, ROSTER_VT, want_vas=True) + d = count_vt(fd, size, DEF_VT) + c = craft_count(fd, defs) if defs else 0 + en, eb = extent_stats(fd, size) + print('%5d %8d %8d %7d %6d %8d %10.1f' + % (el, r, rvas, c, d, en, eb / 2**20), flush=True) + time.sleep(max(0, every - (time.time() - t0 - el))) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/re-capture/census_session.sh b/tools/re-capture/census_session.sh new file mode 100755 index 0000000..e46d01b --- /dev/null +++ b/tools/re-capture/census_session.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages +SD="$(cd "$(dirname "$0")" && pwd)" +SECS="${1:-180}"; EVERY="${2:-10}"; HUNT="${3:-1}" +CFG=/tmp/nav-live.json +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then + SYLPH_HUNT="$HUNT" SYLPH_KILL_TURRETS=1 nohup python3 "$SD/pilot.py" "$CFG" "$SECS" \ + /tmp/live-pilot.log 2>&1 & + PILOT=$!; echo "--- pilot (SYLPH_HUNT=$HUNT)" +else PILOT=""; echo "--- BIND FAILED, no pilot"; fi +python3 "$SD/census_probe.py" ; rc=$? +[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null +echo "LIVENESS DONE rc=$rc"