Two files (isl_report.py's docstring and structures/isl-builtins.md) recorded the
same blocker on a faithful per-phase condition listing: that it needs the coroutine
entry points from start_coroutine's operand. Measured against isl.call_sites(),
which enumerates by scanning the encoding rather than by decoding and so is an
independent denominator:
linear + jumps, stopping at ret (what the tool did) 133 / 2846 = 4.7%
linear + jumps, continuing past ret 2275 / 2846 = 79.9%
... + following start_coroutine (the recorded fix) 2355 / 2846 = 82.7%
plain linear decode, no control flow at all 2846 / 2846 = 100.0%
Following the coroutine entries buys 2.8 points. Disc-wide, a plain linear decode
from the first phase base reaches 25705/25705 call sites over all 28 stages, and
28/28 decode clean to code_end with no desync.
The real bug was isl.dis ending on `if op == 20: break`. Op 20 is `ret`, but this
is a coroutine VM -- the thread suspends and resumes at the FOLLOWING instruction,
so code continues past it. dis() now takes stop_at_ret (default True, preserving
the old output: data/isl-stage02.txt regenerates byte-identical) and
isl.linear_offsets() is the correct walk.
By-product, kept with its control: start_coroutine's target is staged slot 0 --
73/83 phase-1 sites land on a valid instruction, against a 38.7% chance rate for an
arbitrary 4-aligned offset.
New artefact data/isl-stage02-phase-ends.txt with a committed generator
(isl_report.py phase-ends). It shows END_PHASE's call site is the WRONG place to
read a clear condition: all 12 Stage-02 sites sit in one stereotyped outro. Not
settled, and stated as such: op10/op13/op14/op21/op23 are unread handlers, so the
condition in the poll loop upstream cannot be named yet.
Answers the backlog's open "first step: diff the two readers across the disc and
count disagreements", statically over every IDXD object.
Of 7750 objects and 738922 named fields whose true value is numeric, legacy
get_f32 is correct 39.42%, returns None (harmless) 43.04%, and returns a WRONG
NUMBER 17.54% (129612 fields).
The wrongness has an exact predicate: single-record objects 0 of 29822 wrong
(0.00%); multi-record objects 129612 of 709100 (18.28%). The mechanism is in
get_raw itself -- it flattens the pool to a token list, finds the FIRST occurrence
of the key, and returns the preceding token, with no notion of records. So every
record after the first inherits record 0's value: Weight truth=1.0 legacy=0.3,
Points truth=10000 legacy=4000.
Practical rule recorded: a get_f32 number from a single-record object is safe; from
a multi-record object only the first record is.
Withdrawn in the same document: my first sweep compared against "the string before
THIS field's own key" and reported 65.90% -- that is not what get_raw does, so the
figure is not the legacy reader's error rate.
Two updates to the mission-freeze entry, both measured.
DONE: the entry's "first step, revised" was "make pilot.py shoot, then re-run
ob_flag.py ... the actual obstacle is that nothing the pilot does moves the
counter". pilot.py now has SYLPH_WEAKEST=1 (target score scaled by remaining
hull) and the next run moved REMAINING OB 008 -> 007 concurrent with the live e010
floor dropping 16 -> 15. The counter is fully solved; the freeze work now needs
only a frozen sample for the v2 wait probe.
DEAD END, with numbers: --log_mask=0 does not surface kernel call traces. A full
Stage 02 run produced 199 MB at ~33 MB/min, and a 300k-line tail is 254127 A>
(Apu/XMA), 42444 d>, 2897 G>, 532 w>, with ZERO k> and only 14 K> lines per 58k of
boot. XamShowSigninUI / KeWaitForSingleObject / NtWaitForSingleObject each appear
exactly once in the whole log -- an export listing, not call traces. That
independently confirms the entry's own cost note: those calls are kHighFrequency
and silent without --log_high_frequency_kernel_calls=true.
The run also did not freeze (healthy TIME 00:24.28 -> 03:33.28), making it the
fourth consecutive non-freezing run.
mission-phase-advance.md still carried "🔴 Not settled: where the script bytecode
lives". It was settled, in a sibling file I had never opened:
structures/mission-script-ssb.md, which says in its own opening that this file
"recorded the bytecode as not on the disc under any obvious name. It is on the
disc."
Verified independently before correcting: name_hash resolves Stage\script.tbl (the
manifest, 838 B compressed) and 28 Stage\StageNN.ssb records -- S01-S16, S18-S29 --
all in dat/GP_MAIN_GAME_S.pak, with Stage\Stage17.ssb absent, matching the loader
guard sub_8225EC78 (n == 16 || n > 32).
Recorded WHY the last three iterations' searches could not have worked, since that
is the reusable part: MISSION1..33 and MISSION_*_PRT are manifest FIELD KEYS, not
record names. The records are named Stage\StageNN.ssb and the manifest maps
between them, so probing the record namespace with field-key names cannot hit --
2148 hashes over 41 paks and 26443 records returned zero for that reason alone.
Two side findings survive: the .embsec_ sections hold PPC code, not bytecode; and
the archive lookup keys by tag_hash (0x00FFFFDF) while the pak record index uses
name_hash -- not interchangeable.
0x82448AA0 and 0x82448C50 are not strcmp. Both pass the name to 0x82447DF0 and
use the result as a key: the first binary-searches a table of 16-byte records
(x16 for the end, /16 for the count, >>1 for the midpoint), the second packs the
hash into a three-word key and calls 0x8244E338.
Decoded 0x82447DF0 from the disassembly as ((sum of extsb bytes) & 0xFF) << 24 |
(rolling mod 0x00FFFFDF) -- i.e. tag_hash. tools/re-capture/unitgroup.py::tag_hash
already documents itself as "a transcription of sub_82447DF0", so this was in the
corpus; the useful part is that it identifies which hash the ARCHIVE uses.
That exposed a defect in my previous sweep: it probed name_hash (0x00FFF9D7,
lowercased) only, while the archive keys by tag_hash (0x00FFFFDF, case-sensitive).
Re-ran with BOTH: 1380 names -> 2148 distinct hashes, 41 paks, 26443 records.
STILL ZERO. The pak-record hypothesis is now refuted with the right hash rather
than merely unsupported.
Left open: the XEX's compressed/encrypted region (default.xex never decrypted
here), plus two untried static threads -- the second pair of SCRIPTS/GP_SCRIPT
references at 0x82262374 / 0x822622bc, in a different function, and tracing
[r31+80] back to whoever opened the archive being name-tested.
My earlier negative was under-scoped: it probed 35 paks, missing hidden/resource3d/
and dat/movie/. Redone across every *.pak on the disc -- 41 archives, 26443
indexed records, 768 distinct name hashes -- still ZERO hits. The scripts are not
a pak record under any of those names.
Located the three loader strings by VA (the .pe is a flat VA dump, VA = 0x82000000
+ offset) and pulled their xrefs from sylpheed.db:
SCRIPTS 0x820a823c <- 0x8225f1b8, 0x82262374
GP_SCRIPT 0x820a8244 <- 0x8225f168, 0x822622bc
MISSION1 0x820a8264 <- 0x8225eed8
Disassembling sub_8225EE20 shows both archive-name references call THE SAME
routine 0x82448AA0 with (object, string) and test the result with cmpi -- so the
loader COMPARES a name against an already-open archive rather than building a path
like dat\GP_SCRIPT.pak. That fits GP_SCRIPT being a name an archive reports,
which is why no such file exists to find.
Next concrete steps recorded: identify 0x82448AA0 (strcmp/strstr/hash-compare) and
its neighbours 0x82448C50 / 0x8216F218, and trace [r31+80] -- the object being
name-tested -- back to whoever opened it. That names the container.
Both candidates this file named for the missing phase-script bytecode were tested
statically, from the flat-VA .pe and the extracted paks.
REFUTED: the seven .embsec_ sections hold PPC CODE. Parsed from the section table
(offset 592, 40 bytes apart) they total 129472 bytes, matching this file's own
"~130 KB" estimate, so they are the right sections -- but six of seven begin
7d8802a6 (mflr r12) and all carry the standard prologue (stwu r1,-N(r1), std
r30,-16(r1), bl). That is not bytecode for a 147-builtin VM.
Also recorded: ".embsec_P" from `strings` is a false lead -- the name field is
exactly ".embsec_" and the P is byte 0x50 of the following VirtualSize (0x1350 =
4944, the fifth section's size).
CORRECTION: this file says grepping for MISSION_START_PRT "returns nothing". That
grep was over the DISC EXTRACTION; all five MISSION_*_PRT names are present in the
executable image, in an .rdata table reading "SCRIPTS" "GP_SCRIPT" "script load
cancel\n" "MISSION1".."MISSION33".
NARROWED: the paks are name-hash addressed, so names can be probed rather than
eyeballed. 616 distinct hashes -- MISSION1..33 and the five MISSION_*_PRT under
prefixes SCRIPTS\, GP_SCRIPT\, scripts\, script\, SCRIPT\ and none, with suffixes
.prt/.PRT/.scr/.bin and none -- across all 35 paks: ZERO hits.
Left open: the XEX's compressed/encrypted region, or a name outside those guesses.
GP_SCRIPT is the strongest remaining thread -- referenced by code, absent from disc.
SYLPH_WEAKEST=1 (a25ddab) worked on its first live run: concentrating fire on the
already-damaged attacker produced the kill that four previous runs could not.
All gates enforced first -- FLIGHT confirmed, stage asserted, mission clock shown
advancing -- then:
live e010 floor: 16 for samples 0-11, then 15 for samples 12-39 (one death)
counter: 004 -> 008 at 02:06.33 (t=120 arrival, +4)
008 -> 007 at 03:10.28 <-- DECREMENT, bracket (171.2s, 190.3s]
007 -> 011 at 03:48.40 (t=210 arrival, +4 FROM 7, not from 8)
011 held to 06:50.30 -- ceiling 11, where every prior run reached 12
Conclusive because: it fell by exactly 1 and not by 4, so the counter tracks CRAFT
not squadrons; exactly one attacker died and exactly one decrement occurred, in the
same window; and the CEILING moved with it -- a counter that merely read arrivals
would still have shown 012. Control held: turrets fell 109 -> 92, seventeen
deaths, none of which moved it.
Settled reading: REMAINING OB is the number of objective-marked craft still alive,
the marked craft being exactly the members of the phase's A-route squadrons. It
rises by a squadron's membership on that squadron's route arrival time and falls by
one per marked craft destroyed. Every number was predicted from Route_S02.tbl and
UnitGroup_S02.tbl before it was measured.
pilot.py gains SYLPH_WEAKEST=1, which scales a target's score by its remaining
hull (pos+0x154) so the pilot finishes what is already hurt instead of
re-engaging whatever is nearest. Motivated by 00dc016: over ~8 minutes the pilot
damaged 14 of 16 e010 attackers (hulls 360..500) and killed none, because 500 HP
spread across a squadron kills nobody.
STATUS: the flag is implemented and its targeting works -- 3105 of 3105 target
samples selected e010 -- but it is UNVERIFIED in combat, because the run it was
written for was lost.
That loss is the second half of this commit. The run printed "READY ROOM / >>>
HUD / Stage 02 OK" and I began the experiment; there was no FLIGHT: line, because
the flight check failed three times and fell through silently while the next line
read like success. The game was frozen on a near-black screen (screen_id `other`,
mean 10.8/2.8/2.1, frozen.py max_pixel_delta=0) and the pilot's every sample from
t=0.0 to t=406.1 is byte-identical with speed 0.
assert_stage.py could not have caught it: it reads the DEFINITION table, which is
populated when the STAGE loads, independently of whether the mission is running.
Recorded in nav-guards.md with the rule -- enforce the flight gate with a non-zero
exit, and run the three-crop TIME liveness check before any experiment.
Fourth Stage 02 run: 520s of pilot with SYLPH_PREFER=e010 (198 fire=1), per-class
counts every 10s, OB+TIME throughout.
ARRIVAL TIMING n=4: 008 first at 02:05.00 (step bracket contains 120), 012 first
at 03:45.47 (bracket contains 210), then 012 held 3.5 more minutes with no fall
and no 016.
THE BLOCKER IS MEASURED, and it is not aim. Reading hull at pos+0x154 for every
live e010 at the end: 14 of 16 are damaged, hulls 360..500, lowest 360/500 = 28%
gone. The shots land; they just do not finish. A kill needs roughly 3-4x longer
on one target than ~8 minutes of combat produced.
The contrast with the control shows the mechanism is hit points: e007 turrets
(HP 100) lost ~9 dead, 106 -> 97 live, while e010 attackers (HP 500) lost none and
the floor never moved off 16.
Decrement therefore still unproven after four runs, and "fly longer" is not a fix
since ~half of runs end early. Options recorded for a redesign: point the pilot
at the WEAKEST attacker (hull is readable per entity; one is at 360/500), use
missiles (never deliberately fired), or wait on a scripted kill.
Third Stage 02 run, pilot with SYLPH_PREFER=e010 (312 fire=1 samples), per-class
live counts logged every 11s beside the HUD (new tools/re-capture/class_count.py).
CONTROL CONFIRMED: the live turret population fell 108 -> 101 -- seven e007 deaths
-- and REMAINING OB never decremented, only rose. Previously this was inferred
from a run whose kill log happened to be turrets; it is now measured with the
classes counted directly.
ARRIVAL TIMING n=3: 004 -> 008 in (108.7s, 125.5s] and 008 -> 012 in (204.2s,
221.7s], both brackets containing the predicted 120 and 210.
The live e010 count sat at exactly 16 in 20 of 26 samples -- precisely phase 1's
e010 roster (ADT102/ADT107/ADT113/ADS151, each n=4) -- an independent runtime
corroboration of the static roster.
DECREMENT STILL UNPROVEN: the e010 floor never fell, so no marked attacker died
and the counter had no chance to move. Three runs have failed to kill one. The
blocker is combat effectiveness, not instrumentation.
Artifact recorded: six of 26 class samples read 17-28. Spikes are always upward
and transient -- the tool dedups on a position triple read just after the pattern
scan, so an entity written between the two reads is counted twice.
pilot.py with SYLPH_PREFER=e010, 300s, mission clock sampled throughout.
RISES REPRODUCED: 004 -> 008 in (114.8s, 134.7s] and 008 -> 012 in (196.4s,
213.3s], both brackets containing the predicted 120 and 210. Second independent
run, so the arrival half is now n=2.
DECREMENT INCONCLUSIVE. The pilot fought properly -- 424 fire=1 samples, 865 with
the target inside 1500 units, closest approach 79, target e010 throughout, hull
untouched -- and 13 ADAN died (129 -> 116). The counter held 012 for 98s. That is
NOT evidence against the decrement: the 13 dead were not identified by class, this
pilot's kills historically skew to turrets, and the live e010 count ROSE over the
run so attacker deaths cannot be inferred from it. Recording it as inconclusive
rather than as a negative, which is the error this corpus has already logged twice.
POSITIVE RESULT: at TIME 05:56.85, both arms sampled together, REMAINING OB = 012
while live UN_e010_ADAN_Attacker_S = 23. So the counter is NOT the live attacker
head-count; it tracks a subset, and 12 is exactly the three A-route squadrons'
membership (3 x n=4). Phase 1 fields only 16 e010, so 23 live means later-phase or
F-route squadrons joined without touching the counter.
Anomaly recorded, not explained: the enumeration reports 2 _Player entities at two
distinct positions.
Predicted from the disc alone, before the run: ADT102/ADT107/ADT113, each n=4,
arrive at t=0/120/210, so the counter reads 004, steps to 008 at t=120 and 012 at
t=210, and goes no higher in phase 1.
Measured with the mission clock sampled beside the counter throughout:
004 held over six samples to 01:45.44
008 first seen at 02:00.87 -> step bracketed in (105.4s, 120.9s]
008 held over five samples to 03:17.55
012 first seen at 03:30.66 -> step bracketed in (197.6s, 210.7s]
012 held four more samples to 04:36.95, no 016
Both steps land on the predicted second, and the ceiling holds. Three independent
features -- starting value, both step TIMES, and the ceiling -- came from
Route_S02.tbl and UnitGroup_S02.tbl with nothing fitted to runtime data. So the
counter's rises ARE the A-route attacker squadrons arriving.
Status upgraded to CONFIRMED for the rises. The DECREMENT half stays 🟡 and is
now the only open part: this run killed nothing, and the single observed 12 -> 11
remains one sample.
The OB kill test did not run -- the guest froze about a minute into the mission --
but sampling the HUD's own mission clock beside the counter caught it instantly.
Twelve samples over ~4 minutes of wall time: the first reads TIME 00:43.24, every
one after reads 01:02.23 unchanged, with REMAINING OB 004 throughout. frozen.py
agrees (max_pixel_delta=0), movers in the entity window are 0, and screen_id still
says `flight` with the process alive.
Worth a file because the mission clock defeats both traps this corpus has paid
for: it is the simulation's own counter, so neither a frozen world nor a finished
one advances it, where screen_id and pixel-churn are fooled by a GAME OVER screen
that animates. Rule recorded: "X never changed" is only evidence if TIME changed
across the same window.
It also closes an open ❔ from earlier this session -- entity-position-anchor-
refuted.md's "0 of 64 regions changed across 357 MB, cause unknown, not
reproduced". Same signature, now reproduced: it was this freeze.
Still unrun for the fourth iteration: the kill test itself. Not conceptually
blocked -- blocked by attrition, with this run dying at t~62s, before even the
t=120 arrival the trajectory prediction needs.
Swept every Route_*_p<n><kind> in every stage table, resolving each squadron to
its unit via that stage's own UnitGroup, with F/S/M routes as the control:
A/B routes : 23 of 67 resolved are attacker/bomber = 34.3%
F/S/M control: 27 of 928 = 2.9%
A ~12x enrichment, so the signal is real and not a one-stage accident. But "A
means the marked attackers" remains too strong: of the 67 resolved A/B routes, 35
carry UN_mn040_Asteroid_Big and 9 carry UN_n001_TTRL_Box (TTRL = tutorial). A
reading covering all four is "things the mission wants shot" -- attackers in a
combat stage, asteroids in an asteroid stage, boxes in a tutorial -- which keeps A
as an objective marker rather than a unit-class marker, but that is a reading of
four unit types, not a measurement.
Records a near-miss: a truncated listing showed the first ~26 rows, dominated by
early stages, in which every resolved row was an attacker. I nearly wrote "23/23,
unanimous". The full set is 23/67 -- the tail of a sorted listing is not the
distribution.
The Stage 02 derivation is unaffected: its three A routes are still exactly the
three n=4 attacker squadrons.
Pure static, from Route_S02.tbl. Only THREE routes in the entire stage are kind
'A', all in phase 1 -- ADT102, ADT107, ADT113 -- and every one is a
UN_e010_ADAN_Attacker_S squadron of n=4. The fourth phase-1 e010 squadron,
ADS151, is kind 'F'.
Their first-keyframe times are t=0, t=120 and t=210, so counting only the 'A'
squadrons predicts the counter exactly: 004 at entry, 008 at t=120, 012 at t=210,
and never above 012 in phase 1. That is what every run has shown -- starting
value, step size and ceiling all fall out of the disc with nothing fitted.
It also explains the two awkward observations: the fifteen-minute hold at 012 (all
'A' squadrons arrived, and those kills were turrets), and why 016 never appeared.
The earlier prediction of a 016 cap is WITHDRAWN: ADS151 is an F route and is not
counted, so the ceiling is 12.
Kept 🟡: "A = marked attack objective" is inference from three routes in one
stage, consistent with the mission dialogue and with A appearing only in early
phases, but the letter's meaning is not proven and the kill test is still unrun.
Replaces the 16/4 MEAN with a per-group measurement, closing the caveat that a
5/5/3/3 split would have made the counter's step-of-four a coincidence.
Route_S02.tbl names routes Route_<squadron>_p<phase><kind>, so the squadron->phase
link is readable off the disc. Stage 02 has 37 phase-1 squadrons; intersecting
with the e010 roster gives exactly FOUR -- ADS151, ADT102, ADT107, ADT113 -- and
every one has n=4, summing to 16 craft.
The five e010 groups NOT in phase 1 (ADN204, ADN206, ADN208, ADN209, ADS251)
include both n=6 groups and the n=9 group. That is why the stage-wide mean
misled: the odd-sized squadrons belong to later phases. Restricted to phase 1 the
size is uniform.
Pure static, no emulator. The hypothesis itself stays 🟡 -- the runtime
kill-one-e010 test is still the thing that would confirm or kill it.
Decoded stage\UnitGroup_S02.tbl with the existing unitgroup.py (pure static, no
emulator; 111 squadrons, roster self-check 111/111).
The "four per squadron" premise was 16/4, a mean, and I flagged that 5/5/3/3 would
make the step-of-four a coincidence. Measured: Stage 02 has NINE e010_Attacker_S
squadrons totalling 45 members, sized 4,4,4,6,4,6,4,9,4 -- not uniform across the
stage, but SIX of the nine hold exactly 4, and phase 1's share is independently
recorded as 4 groups / 16 craft = 4 x 4. So the phase-1 squadrons are the n=4
ones and the step-of-four is a real size, not an averaging artifact.
But a premise of the note is refuted: ADN110/111/112, which I cited as the polled
squadrons arriving at t=170, are UN_e007_ADAN_Turret with n=9 -- TURRET squadrons,
not attackers. That arrival evidence is about turrets and is withdrawn from the
argument.
Records the tension it exposes: mission-wave-arrivals.md calls ADN110/111/112 the
squadrons the phase-1 clear condition polls, while mission-objectives-text.md has
the phase-1 objective as the marked attackers. Unresolved.
Assembled from existing captures, not newly measured, and marked 🟡 accordingly.
mission-objectives-text.md gives phase 1's roster as 4 e010_Attacker_S GROUPS
totalling 16 CRAFT -- four per squadron -- and the counter moves in steps of
exactly four (004 -> 008 -> 012). The phase-1 objective is stated in the mission
dialogue as "the attackers with the orange markers", i.e. the e010s, not the
turrets that outnumber them 7:1. mission-wave-arrivals.md pins the arrival of the
three polled squadrons at t=170 SECONDS, which is when the counter is seen to
climb.
That accounts for every number taken so far, including the two that looked
contradictory: the counter held at 012 while the hostile population fell by a
third because those kills were almost all turrets, and it decremented 12 -> 11 in
the one run whose pilot actually killed attackers.
Recorded with its own refutation tests (kill one e010 -> must fall by exactly 1;
kill a turret -> must not move; phase 1 should cap at 016) and with the weak point
stated: "four per squadron" is 16/4, a MEAN, and if the squadrons are 5/5/3/3 the
step-of-four pattern is coincidence.
Every earlier refutation in this file carried the caveat that entities2.typed
types entities by their position CHANGING, so a stationary objective is invisible
to it. This session's definition-pointer enumeration does not have that limit, so
the sweep was re-run against it (ob_flag_all.py, guarded route, stage asserted,
HUD cropped beside each sample):
A: HUD 004, 147 entities -> 152 candidates
B: HUD 012, 133 entities -> 15 candidates
intersection: 1
The lone survivor pos+0x0250 = 239d6732 is the same offset AND identical value
this file already characterised as a per-group word. Membership test: all 12
holders are UN_e010_ADAN_Attacker_S, 12 of 16 live attackers. It is a squad
parameter, and it survived only because that population equalled the counter at
both samples.
Also reconfirms "not a class head-count" on 147 entities including capital ships.
Trap recorded: the first sweep reported 298 entities and a class with head-count
exactly 4 -- a perfect-looking hit that was pure artifact. Deduping by ADDRESS
leaves the measured exact 2x duplication (pairs 0x1000 apart, byte-identical
positions) intact and doubles every population. Dedup on the position VALUE.
Ran this file's own transition filter on a guarded Stage 02 run (stage asserted),
reading the HUD from a crop taken at the same instant as each memory sample:
scan at HUD 004 -> 41537 candidates; filter at HUD 008 -> 8; verify across the
008 -> 012 transition, which was NOT selected on -> exactly ONE survivor.
That survivor, 0xbdb69668, tracked 4 -> 8 -> 12 against the HUD's 004 -> 008 ->
012. The other seven collapsed into noise at the first unselected transition,
which is precisely what that rule exists to catch.
The file's "try 0xbdb59668 first, re-scan when it reads 0" rule worked verbatim:
it read a hard 0 here, and the re-scan cost about the predicted five minutes.
The ...9668 page-offset pattern is REFINED, not reinstated: the three located
addresses (0xbdb49668, 0xbdb59668, 0xbdb69668) are three ADJACENT 64 KB pages at
one offset, and in this run exactly one of 8192 probed pages held 12 -- the
counter -- making it a one-step lookup. But the 2026-08-26 refutation stands as
measured (zero ...9668 VAs held the HUD value in that run), so this is a fast
heuristic to be HUD-checked, not a law.
The file proposed that the counter lives at a fixed offset inside an allocation
whose base moves by whole 64 KB pages, and stated the test itself: "a third scan
should again land on ...9668". Ran it on a fresh guarded Stage 02 run.
Probing all 8192 pages of the form 0x????9668 across 0xa0000000-0xbfffffff: with
the HUD at 004, exactly two VAs held 4 (0xbc3f9668, 0xbe3f9668); with the HUD at
012, ZERO held 12. Both candidates also failed the file's own transition rule --
over 252 s 0xbc3f9668 held a flat 4 and 0xbe3f9668 flickered 4/0 while the HUD
went 004 -> 012.
Both arms were sampled at the same instant (cropped HUD digits beside each memory
read), after a stale-screenshot comparison earlier in this session produced a
spurious 13-vs-004 mismatch.
Scope kept narrow: this refutes the page-offset prediction, not the confirmed
finding that 0xbdb59668 carries the counter in some runs. The counter's address
in THIS run remains unknown -- no transition filter was run.
The section I added last iteration claimed to be testing an untested 🟡 and
concluded "the address is run-dependent, always re-derive". Both halves were
wrong, and the evidence was already further down the SAME file:
- cross-run stability was not untested -- the body records 0xbdb59668 carrying
the counter in 3 of 5 measured runs and reading a hard 0 in the other two. The
stale claim was in the status line at the top, which I took at face value.
- "always re-derive" is worse advice than the rule the file already gives: try
0xbdb59668 first, check it against the HUD, re-scan only when it reads 0.
What my run genuinely adds is a SIXTH data point with a new failure mode: the
address read neither the counter nor 0 but 95748078, constant over four samples.
Status line rewritten to match the body (🟡 recurs but not universal), so the
document no longer contradicts itself.
The doc carried a 🟡 saying cross-run stability was untested. Tested now on a
fresh guarded Stage 02 run (stage asserted): the HUD reads "Remaining OB : 004"
while RAM at the documented 0xbdb59668 reads 95748078, constant over four samples
12s apart. Not 4, not near 4, not moving. So the address belongs to that run's
heap, as the corpus's own heap-reallocation warning predicts.
No constant-shift shortcut either: a BE u32 equal to 4 occurs 1654 times within
+-1 MB of the old address and 11202 times within +-16 MB, far too many to isolate
without the transition filter. The durable result is the METHOD (ob_scan.py:
scan at one value, filter against live memory at a DIFFERENT value), not the
number.
Also fixes a contradiction in INDEX.md, which said in one row that the address is
"still ❔" while another row linked the doc that had already CONFIRMED it.
My enumeration sourced entities from moving(), which requires a position to CHANGE
between two samples. A capital ship holding station changes by exactly zero, so it
can never appear -- which is also why lo=0 and the whole-map scan did not help, and
why parked ArrowHead wingmen were missing. The filter was the problem, not the
range, and not the game.
navigator.py already does it right and says so in its header: search for the
DEFINITION POINTER and take position = hit - 0x130, "which finds every entity
whether it is moving or not". The existing mission_state.py scan on Stage 02
returns 149 entities with hull(+0x154) == definition HP for 129 of them:
UN_f101_TCAF_Acropolis (+5000, +0, +0) hull=25000.0 HP=25000.0 frac=1.000
UN_e105_ADAN_Cruiser hull=30000.0 HP=30000.0
UN_e106_ADAN_Destroyer hull= 9150.0 HP=10000.0 (under fire)
So "an autopilot that must protect the Acropolis cannot locate it through the
+0x130 method at all" is withdrawn outright -- it can, and INDEX.md had already
recorded the Acropolis falling 25000 -> 23038 over 240s.
What survives: the two enumerations are still different structures in different
regions (0/116 vtable instances lie in the +0x130 region). What does not survive
is any claim that the +0x130 model covers only four types.
With the sliver stopped at both ends (914ada4 producer, 6aa31e5 consumer) a full
guarded run reached Stage 02 and asserted it: "guard: menu confirmed (1279x675)"
then "OK: UN_f101_TCAF_Acropolis is in the 14 definitions", exit 0.
Read the menu the guard confirmed rather than continuing to assume it:
NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS with the cursor on NEW GAME.
So dpad-down x1 = LOAD GAME is correct and dpad-down x2 = TUTORIAL, one press
further -- exactly consistent with the observed mis-selection when the guard was
being fed a sliver. Capture committed as captures/main-menu-items.png.
Stated plainly: n=1 for "reproducible", and why the game window leaves the window
tree during a load was guarded against, not investigated.
assert_stage.py checks the DEFINITION table against an expected stage marker and
earned its keep immediately: its first live run reported MISMATCH -- the capture
had a live flight HUD and would have been filed as Stage 02, but was the S01
tutorial. That is exactly the failure that silently invalidated an earlier
cross-run comparison.
require_menu (launch_mission.sh) refuses to press until screen_id reads `menu`.
It is NOT sufficient, and this refutes my previous explanation: the run DID
confirm the menu and still loaded the tutorial. The real cause was that the
guard's own capture was a 10x710 sliver which classified as `menu` -- fixed
separately in 6aa31e5.
Left open: whether the menu guard suffices now that slivers are rejected (not
re-run), and why the capture was a sliver at all when the other shots in the same
run were 1279x675.
Closes the caveat I flagged in 4ae63cb. savegame.rs documents the chunk stream
(read off the title's own serializer at 0x822C00E8) as GDAA / phase string /
'GHAD' + 122 bytes / SHAB table, and FieldSpec.offset is "offset within the
122-byte GHAD block" -- so the base is the byte after the tag, tag+4.
Reading there yields three fields, two corroborated independently elsewhere in the
corpus: Stage +52 = 2, Points +24 = 4101 (weapon-datasheet-runtime.md's "4101 P"),
FlightTime +4 = 324773 (the 05:24.77 Stage-01 best time). tag+8 reproduced only
the stage. The payload's phase string reads GP_BUNK.
Left open: the .header mirror does not reproduce -- savegame.rs lists Stage at
header offset 0x14 but a BE u32 there reads 2097200 (00 20 00 30), which looks
like UTF-16 text. Not chased; the payload reading does not depend on it.
Refutes my own claim from the previous commit that slot 01 is an auto-save whose
restored mission drifts. savedata has not been written since 2026-08-23; every run
today left it untouched (only the .gpd profile files moved), and there is exactly
one save, so a wandering save-list cursor cannot explain it either.
Decoded the save statically (GDHA + zlib at 0x92, 545 bytes): only SHAB record 0
carries clear data, records 1-15 are zeroed. Three independent readings agree it
is Stage 02 -- the 324773 ms clear time is the 05:24.77 that SESSION-2026-08-11
documents for Stage 01, the 4101 points match weapon-datasheet-runtime.md's
"Stage 02, At Standby, 5% clear, 4101 P", and the GHAD block at tag+8 gives
+52 = 2. Stage 02 is the Acropolis escort mission, which is exactly the roster the
last run produced.
So the odd run out was the S01 "Glasner Training Area" one -- a TUTORIAL, reached
because the nav issued dpad-down + A from a state never confirmed to be the main
menu. Fix is two guards: verify screen_id reads `menu` immediately before the
dpad, and assert Acropolis is in the definition table before comparing rosters.
Caveats kept: the GHAD field base was not isolated by the search (7 candidates, 3
giving stage 2 -- tag+8 chosen because it agrees with the other two readings), and
the tutorial mis-selection was not demonstrated by re-running the emulator.
Same moment, same stage: the INST_VTABLE scan sees 116 objects across 14 types;
the moving+0x130 method sees 30 across 4. Every capital ship, station, missile and
the objective-critical Acropolis reads ZERO in the +0x130 method.
The absence is structural, not a filter artifact -- both obvious explanations were
tested and failed. Dropping the speed floor to 0 raised the count 30 -> 59 and
recovered the _Player but still only 4 types; scanning the WHOLE map with no floor
gives 181824 movers and still 4 types. And 0/116 vtable instances lie inside the
window where the +0x130 blocks are found (instances 0xbc372cc0-0xbc9bc720, window
0xbd000000-0xbe000000), independently confirming these are separate allocations.
Consequence: an autopilot that must protect the Acropolis cannot find it via the
+0x130 method at all.
Also found: LOAD GAME -> slot 01 no longer restores the S01 training area but a
Stage-02-style escort mission (Acropolis, SchlosBase, cruisers, frigates). Slot 01
is the AUTO-SAVE, so the restored mission moves as the save is written -- which
invalidates the earlier "101 vs 42" comparison outright, since those came from
different stages.
Corrects the previous note: entities2 prints its count AFTER dedup, so the 101 was
already deduplicated; the gap was the stage change, not duplication.
Three checks kill "the fast turrets are shots":
- persistence: 42/42 survived 15s, including 22/22 of the FAST (>300/s) ones;
- hull at the documented pos+0x154 is a clean per-type constant -- 1000.0 for
DeltaSaber_T, 100.0 for e007_Turret, with one turret at 79.0 (damaged);
- the speed was never suspicious: isl-builtins.md already records e007's
MaximumVelocity 500 / CruisingVelocity 280 and notes the data models turrets as
mobile. My premise "a turret is a fixed emplacement" was an inference from the
NAME that the corpus had already contradicted from the DATA.
The inflated count is two measured effects, neither mis-typing: exact 2x
duplication (64 raw -> 32 deduped, every per-name count halving, commonest offset
gap 0x1000 = one page), and a growing population (42 -> 60 raw in 15s, matching
the documented wave arrivals). So "101 vs 42" compared different mission times
with at least one count un-deduplicated.
Left open: whether the earlier 101 was raw or deduped (it is odd, so not a pure 2x
artifact); whether the 0x1000 spacing is a real second copy or page aliasing; and
which of the two enumerations is the entity list, since they describe different
sets.
Fresh run, unmodified tools: entities2.py self 0x130 returns 74-101 typed live
entities with positions and speeds, a player entity, and the orientation matrix at
pos-0x70 with 16-byte stride -- exactly the documented layout. Artifact committed
as docs/data/live-entities-2026-08-26.txt.
Last iteration's "zero moving triples across 357 MB" does not reproduce: sampled
seven times 4s apart, the whole map gives 47k-123k movers and the committed
ENT_VA window 10k-20k. So the window is populated and the tooling is sound; what
differed between the sessions is NOT determined and is recorded as open, since the
earlier zero persisted for minutes across several commands. Practical rule added:
check movers are non-zero before concluding anything from a memory probe.
New open discrepancy: the vtable scan reports 42 instances (20 turrets) while the
+0x130 method reports 101 (82 turrets), most of the excess moving at ~375/s -- and
a turret is a fixed emplacement. Likely projectiles typed as their shooter, but
explicitly NOT asserted: no test here separates a projectile from a fast craft.
Three results, two of them against my own earlier claims:
- REFUTED: position = instance - 0x12c. The corpus anchors on position (def ptr
at +0x130, orientation at -0x70, hull at +0x154) and name_of reads the def ptr
at instance+4, which predicts -0x12c. Measured over all 42 named instances:
almost every read is (0,0,0), three are garbage, and 0/42 move. The vtable
object and the position block are different structures.
- WITHDRAWN: last iteration's claim that entities2.py's ENT_VA window is aimed at
the definitions rather than the instances. entities2 does not look for vtable
objects at all -- it hunts moving position triples and checks +0x130, the
documented anchor -- and it had worked minutes earlier (6914 movers, +0x130
voted 92x; then 6634/138x). I built the defect report on the single sample in
between that returned zero. The +0x29d0 position candidate falls with it.
- FOUND: sampling 64 spots across the 505 extents (357 MB) twice, 2.5s apart,
with the mission visibly running, only 5 change. The megabyte holding all 42
instances is byte-identical over seconds, as is its primary-VA counterpart.
The mapping IS live (5 regions prove it); what is undetermined is whether the
entity records are simply static or whether writes land in a different alias.
Either way: verify a region changes before measuring through it.
Last iteration I ruled the speed route out because the emulator is not real-time
so the wall-clock denominator is unknown. Wrong: the game prints its own clock,
and flight-speed-law.md had already used it -- mission TIME across a wall interval
gives 1.26x, turning 443.6 units/wall-second into 352 per game-second against a
HUD 350. Withdrawn in place, with the reasoning, rather than deleted.
Closes the backlog item: one world unit is one metre, by two independent routes.
The 2026-08-26 downgrade rested on two numbers, and grouping the CollisionSet
names by prefix dissolves both:
- The 447 km "largest object" is rob_f002_cmesh, one of only FOUR rob_ meshes
(44k-448k). That family is not hull size: rob_f001_bdy_cmesh measures 50179
and f001 is the player's own Delta Saber, whose disc record gives Size_Radius
10.0. A 50 km player fighter is impossible under every unit convention, so
rob_ spans something else entirely and is not evidence about the world unit.
- The "small" 133 m craft is small: rou_e010_cmesh = 133.2 is the 7th smallest
of 78 rou_ meshes (median 636.2, max 9534.9). Against its own family, the
game calling e010 small AGREES with the metre.
Measured this run: the 500 km box is a cube of exactly 500000.0 units on every
axis (+-250000), and the second box mesh is exactly 100000.0 -- the same ruler.
Independent dynamic confirmation already in the corpus (flight-speed-law.md):
443.6 world units per wall-second, mission clock 1.26x wall, so 352 units per
game-second against a HUD reading of 350.
The HUD is reachable and the control is paired; what blocks the measurement is
narrower and now named -- two tool defects (entities2.py's VA window covering the
definitions rather than the instances, gworld.py's 0x600 instance window) plus the
missing piece itself, a locked target whose HUD range and position can be read in
the same second. Also records the speed shortcut as refuted so it is not retried.
Discharges the control owed by entities-live-roster.md: same process, title screen
scans 0/0 against 13/42 in flight.
Reaching a live HUD needed two steps no script had: START skips the post-take-off
cutscene, and a modal "tell you your objective?" dialog DIMS the frame (so the
classifier reads `other` and liveness looks like a stall) until Ⓑ/NO answers it.
After that, green 0.0145 -- inside the documented 1.3-1.5% flight band.
Two tool defects measured, not inferred:
- entities2.py's ENT_VA_LO/HI (0xBD000000-0xBE000000) misses every live instance
(they sit at 0xBC384CE0-0xBC9BAC20) and instead covers the DEFINITIONS. Rescoped
to the instance region, find_delta's +-0x400 radius yields zero votes.
- gworld.py's WINDOW=0x600 is too small: no position-like triple moves inside the
first 0x600 bytes of any of the 42 instances; 0x4000 finds one at +0x29d0.
The unit itself is NOT measured. That needs a locked target so the HUD prints a
numeric range to an entity whose position can be read at the same moment; this run
never locked one. The tempting shortcut -- 116.6 units per 0.6s wall-clock against
the HUD's 350 -- is recorded as refuted, because the emulator is not real-time.
The entry blamed a stale committed VA window for "0 unit definitions". Measured
2026-08-26: the scan works and returns 13 definitions + 42 named live instances
once the run reaches the mission via LOAD GAME -> slot 01 rather than via MISSION
SELECT. What still blocks the measurement itself is narrower and now stated: the
flight HUD was not up (green 0.03% vs 1.3-1.5%), so there was no distance readout
to compare positions against.
The briefing map is cyan and satisfies every clause of the menu rule (b-r > 30,
r < 45, little white), with no earlier rule claiming it -- so it was labelled
`menu`. That made wait_screen.sh report NEVER REACHED READY ROOM on a run that
had successfully done LOAD GAME -> slot 01 -> YES and was three screens further
on: a working route scored as a failed one, pointing the next debugging step at
an input path that was fine.
Cyan has b and g nearly equal (b-g ~ 5) where the menu's blue leads its green
(b-g ~ 32), so `r < 20 and g > 30 and b - g < 20` separates them; the r floor
keeps the title screen out. The file's own docstring already carried the
briefing's mean as an aside -- it just never had a class.
Verified against all eight signatures the file documents (2 menu variants, title,
ready room, flight, 3 briefing measurements): no regressions, and the captured
briefing image now reads `briefing`.
gworld.py's DEF_VTABLE 0x820AF844 and INST_VTABLE 0x820AF030 resolve exactly as
written: 13 unit definitions and 42 live instances, every one name-resolved, with
52 moving triples. The roster is coherent and stage-specific -- UN_S01_Asteroid_*
in the S01 training area, 2 e106 destroyers against 20 e007 turrets, 6 DeltaSaber_T
and exactly one _Player -- which is far stronger evidence than a hit count.
The constants were never stale. structures/unit-struct-runtime.md records its own
provenance ("all six tutorials and Stage 02 loaded from save slot 01") and
launch_mission.sh already encoded that route: title -> LOAD GAME -> slot 01 -> YES
-> READY ROOM -> TAKE OFF. LOAD GAME is the menu's SECOND item; the previous
iteration pressed the first (NEW GAME) and read the resulting 0/0 as evidence
about the constants. One `step down` separated a dozen iterations from this.
Control still owed and stated in the doc: the title-screen arm was measured in
earlier processes, not this one.
Before deriving a new vtable I checked where gworld.py's constants came from.
They cite structures/unit-struct-runtime.md, which states its provenance:
"Captured 2026-07-29 ... all six tutorials and Stage 02 'Declaration of War'
loaded from save slot 01."
Loaded from a save slot -- not through MISSION SELECT, which is the route every
run this session has taken, and which needs the cleared-stage mask poke to offer
a stage at all. So the constants may not be stale; they may just need the state
that route produces. That is a cheaper question than writing a new vtable
finder, and it should be answered first.
First attempt inconclusive: driving the main menu's first item blind, five
presses deep, advanced the progress counter every time (3 -> 5 -> 6 -> 8 -> 10 ->
12, so the game responds) but left DEF_VTABLE / INST_VTABLE at 0/0 throughout.
Without a screen identity this is dead reckoning, and the first item may not be
the load-game entry -- newgame_path.sh documents it as NEW GAME with SELECT DATA
two screens further in.
Next: reach the save-slot screen deliberately rather than by counting presses,
and load slot 01.
Locating DeltaSaber at mission time works: 53 occurrences, all in the heap
(0xBC66..-0xBC6C..), none in the XEX image -- DeltaSaber_T.xpr, _Special,
_NoseGun, _Missile, _TwinGun.
But searching all of guest memory for a big-endian pointer to those addresses
returns ZERO references for every one tried. That is the format, not a search
bug: those strings live in the mission pak's IDXD string pool, and this corpus
decoded that container long ago -- records reference names by OFFSET into the
pool, never by absolute pointer. So there are no pointers to find, and "find the
name, follow what points at it" cannot work on pak data by construction.
This also narrows what the string counts proved. The title-vs-mission control
stands (DeltaSaber 0 -> 81), so mission-specific DATA is loaded, which is more
than "the tables load at boot". But these are asset-table strings, not live
entity objects, so they do not show entities have been spawned. Honest split:
the mission's pak data is loaded; whether entity objects exist is still
unmeasured, because both probes tried -- gworld's vtable constants and
name-pointer following -- are respectively stale and structurally inapplicable.
Remaining routes, untried: derive the entity vtable from CODE via sylpheed.db's
vptr_writes table, which exists for exactly this; or find the entity list from
the mission update function rather than from data.
Three iterations concluded "no units are loaded" from scan_vtable(DEF_VTABLE)
returning 0. Withdrawn: the scan was measuring the wrong thing.
Counting unit-name strings in guest memory with the TITLE as a control (same boot
recipe, no mission):
string title after take-off
DeltaSaber 0 81
rou_ 1,981 7,680
UN_ 19 167
e010 67 134
ADAN 1,450 1,921
DeltaSaber is the player's craft: absent at the title, present 81 times after
take-off. Every other count rises several-fold. The stage content is
unambiguously in memory.
The control is what makes this a result. Raw counts prove nothing on their own --
rou_ and ADAN are numerous at the title too, because the unit tables load at boot.
Only the title-vs-mission difference, and DeltaSaber's 0 -> 81 in particular,
separates "tables loaded" from "mission loaded". My first reading skipped the
control and over-claimed; the control was run before publishing.
Withdrawn as a consequence: "no units are loaded"; and the inference that the
take-off lands in a never-ending cutscene (the movie accesses are real, but the
conclusion rested on DEF_VTABLE=0). gworld.py's DEF_VTABLE 0x820AF844 and
INST_VTABLE 0x820AF030 do not locate entities in this build/state despite being
genuine vtables in sylpheed.db.
Next: derive the correct entity vtable. The UN_ strings reachable by search are
UN_NOSE/UN_MOUNT attachment names in the XEX image, not runtime records -- so use
a name that only exists at mission time. DeltaSaber is exactly that: find its
heap occurrences, find what points at them, read the referencing object's vtable.
wait_flight.sh carries the corpus's best discriminator, and it is a whole-image
statistic rather than a named pixel: the flight HUD paints green over the frame
at 1.3-1.5% green fraction, against <0.1% on every menu.
Measured on the post-take-off state: green=0.0022, mean (1.6, 45.6, 50.1). That
is 0.22% -- an order of magnitude above a menu, an order of magnitude below
flight, on a dark blue-green frame. screen_id.py files it under "menu" because
that is the closest bucket it has; the state is neither.
wait_flight.sh then tapped A for 280 seconds and never saw the HUD.
With the 12 \dat\movie accesses, the reading is consistent: this is the cutscene,
it does not end on A, and it does not reach the flight HUD within five minutes.
Open, cheapest first: how the movie path signals completion (the corpus already
decoded the movie handler for the intro video and found a software-decode
deadline); whether a mission can be started by a route that skips the launch
cinematic; or whether the cutscene simply needs far longer under lavapipe.
Oracle status overall: boot, menus and take-off are reliable and reproducible;
only the cutscene->mission step is unsolved -- a much smaller and better-defined
gap than the freeze it replaced.
Logging file and kernel activity through the whole navigation and counting names:
12 \dat\movie <- heavy, repeated movie access
3 \dat\sound
2 Stage_S01 <- the stage resource is referenced
1 XamShowDeviceSelectorUI
So after take-off the game plays the launch cutscene. Under software rendering a
720p decode is very slow -- this corpus already raised INSTRUCTIONS_PER_MS 100x
because the movie handler has a 2000 ms software-decode deadline -- which is why
the state persists for minutes.
It is unambiguously live, by the widest margin yet measured:
frozen menus this state
screen liveness 0.00% 0.22% 11.00%
guest churn 0.000% 0.190% 0.671%
and the progress counter advances on input (17 -> 101 across the skip attempts),
so the pad reaches the game.
But it does not skip: four rounds of START / A / B advanced the counter every
time and left DEF_VTABLE / INST_VTABLE at 0/0. The input registers, it just does
not end the sequence.
The remaining gap is now narrow and named: the take-off cutscene runs and is not
skippable by the obvious buttons. Either it must play out, or the right skip
input is unknown, or it waits on something the movie path never delivers.
With the emulator launched correctly and the boot verified by the progress
counter first (counter > 0 = title reached; a flaky boot never leaves 0),
nav_to_flight.sh ran the whole sequence -- main menu at 20 s, mask poke, stage
select, briefing skip, ready room, take-off -- where every previous attempt
stalled.
The game is alive at the end of it: screen liveness 2.13% against 0.00% when
frozen, guest churn 0.288-2.932% against 0.000%. An animating screen and a busy
guest at the point that used to be stone dead.
But no units. DEF_VTABLE (0x820AF844) has no hits on a direct byte search, with
a working control in the same run: searching for the poked cleared-stage mask
0x0001FFFE finds it at 0x828F40C0, exactly where nav_to_flight wrote it. So the
search is sound and the vtable is genuinely absent. Both vtable constants were
also checked against sylpheed.db's vtables table and are real vtables, so they
are not stale. Three minutes of waiting left DEF/INST at 0/0 with the counter
static at 17.
So the run reaches a live, animating state that is not a mission with spawned
units -- most likely the launch cinematic or a post-take-off screen. The freeze
is gone; what still separates this from unit spawn is the remaining work.
nav_to_flight.sh contains no run-canary invocation -- it drives an
already-running emulator -- so whether a profile is signed in depends entirely on
how the caller launched it. Every nav_to_flight run in this session was launched
with a bare run-canary, which is why they froze. Worth stating plainly because
the script prints "signing in profile B13EBABEBABEBABE", which comes from the
emulator's startup rather than from the script arranging it.
Refuted: "each screen loads its own GP_*.pak", a plausible route to a real screen
identity. With full file logging the only names that appear are at boot
(sound.p0x, tables.p00, hidden) and nothing new opens as the UI moves between
screens -- the paks are opened once and read by offset, so file I/O carries no
per-screen signal.
Also recorded: one launch this iteration never reached the title -- progress
counter stuck at 0, black screen, guest churn 0.001%. That is a DIFFERENT
failure from the sign-in freeze, which stalls after the main menu with the
counters already advanced, and it matches the boot flakiness
canary-scripted-input-traps.md already documents. The counters distinguish
them: sign-in freeze stalls past the title, a bad boot never leaves 0.
The names in menu-state-in-memory.md are wrong for at least two of the three
words, and the test that shows it is going backwards. Driving deep into the
menus, then three presses of B:
0x828F38AC "cursor" 36 -> 38 -> 40 -> 41
0x828F37B4 "misc" 971 -> 1067 -> 1068 -> 1068
0x828A690C "screen" 56 -> 56, 56, 56
A cursor returns when you go back. These only ever increase -- monotonic
counters, with 0x828F38AC advancing about 2 per input. Driving forward produced
1,3,4,5,6,8,10,12,25,29,32,33,50,53,56 for the "screen id", which is an identity
sequence only if the game has 56+ screens and never revisits one -- exactly what
a counter also looks like.
Withdrawn: 0x828A690C as a screen IDENTITY (1 title, 3 main menu, 4 extras). The
values are path-dependent; they matched across runs because the same key sequence
produces the same count, not because 3 means main menu.
Survives: all three advance if and only if the game responds, and are stable when
it does not. That is a real input-progress signal, reproducible across runs and
both GPU backends, and it is what made blind navigation work. Read it as "did
the game react?", never "which screen is this?".
It also retro-confirms the freeze diagnosis: with the sign-in fix the sequence
runs 3 -> 5, skipping 4 entirely, so "4 = extras" was never a screen -- it was the
count at which the game stopped responding. The counter reading explains both
observations.
Open: no mission reached. Counters at 56/41/1068, guest churn 0.190% (alive;
frozen is 0.000%), DEF_VTABLE and INST_VTABLE still 0. Without a real screen
identity, navigation is dead reckoning; finding a genuine state enum is next, and
the snapshot-and-diff method can be repeated with these counters excluded.
Every Xam UI dialog goes through xeXamDispatchDialog, and the calling guest
thread blocks on fence.Wait() until it is dismissed. With kernel logging on the
last call before the freeze is XamShowSigninUI(00000001, 00000001): the game
asks for a signed-in profile, Xenia opens a modal dialog, and nothing in a
scripted run ever dismisses an ImGui dialog.
That accounts for every symptom at once -- Main XThread futex-blocked at 0 ms CPU
rather than spinning, emulator alive, no guest progress, no faults, and
independence from both the GPU backend and the allocation outcome.
Fix: run-canary --logged_profile_slot_0_xuid=B13EBABEBABEBABE. Same route, one
variable changed:
no profile with profile
screen id 4, forever 4 -> 5 -> 6 -> 8 -> 9 -> 10
XamShowSigninUI called not called
alloc failures 1 0
guest throws 1 0
guest churn 0.000% 1.006%
XamShowDeviceSelectorUI is then requested but storage_selection_dialog defaults
to false, so it is not shown and does not block.
Why it took so long: --log_mask=13, used by every script in this corpus,
DISABLES kernel logging -- the one category that names the dialog. Without it
the freeze presents as a bare futex block, which is why it was attributed in
turn to a heap leak, rounding, MmQueryStatistics, a build regression, the route,
the savegame, shader compilation, an infinite guest loop and the software
rasterizer, each refuted in its own section.
Nearest miss worth recording: challenge-mission-gate.md reported a "Disc Read
Error" dialog on this failure. That is XamShowDirtyDiscErrorUI, which calls
exit(1) -- "This is death, and should never return" -- so it cannot be our
freeze, whose process stays alive. Checking that distinction is what led to the
dialog mechanism.
Open: no mission reached yet (DEF_VTABLE/INST_VTABLE still 0 at screen 10), so
the remaining screens are menus. Ordinary navigation, not a blocker.
Driving the null backend blind, using the memory screen-id as the guide, to the
SAME screen the rendered run froze on (screen 4):
guest churn 19 of 8,089,600 bytes in 4 s (0.000%)
Main XThread 0 ms CPU / 4 s, state S, wchan futex_do_wait
log AllocRange unable ... 134217728 bytes; guest throw
Frozen identically with no rasterizer at all.
The flaw is the comparison, not the data. The earlier "control" measured the
null-GPU run while it was still in the MENUS and the lavapipe run AT the freeze,
then reported the difference as a backend effect. The allocations I cited as
proof it had walked through the content load (114 -> 120) were menu-time
allocations. A control needs both arms verified to be in the same state -- and
the memory screen-id built this same session is exactly what makes that
checkable, which I failed to use on the arm that mattered.
Withdrawn: "the freeze requires the rasterizer", "the fault is in the host
rendering path", and the hardware-Vulkan blocker that followed from it. The
rendering question is no longer load-bearing.
Survives: the freeze happens on the first content load after the main menu, on
every route and BOTH GPU backends, with Main XThread futex-blocked at zero CPU
rather than spinning; and four runs froze with three different allocation
outcomes, so the 128 MB refusal is correlated but not necessary.
Also survives and is independently verified: the memory screen-id navigation,
now reproduced on a third run. It is what exposed this error.
Open again: what is Main XThread waiting on? Identify the futex and its holder,
carefully -- gdb perturbs this measurement.