its timing is on the disc
Q7. Every title-side screen carries a full-screen black .prm quad that
paints last, and its keyframe group IS the transition: black at T0, clear
by T1, clear until T2, then back to black on exit. Read with the corpus's
start-of-a-ramp rule and Q1's time unit that gives 0.87s for EXTRAS,
0.97s for the main menu, 4.08s for the title -- from the file, not from a
stopwatch.
The disc-wide check is per-pak all-or-nothing rather than the 41% the
headline count suggests, and GP_TITLE's 6 of 12 is the useful row: the
six builds carrying a fade quad are exactly the six SCREENS, and the six
without are exactly the six overlays. GP_DIALOG is 0 of 133. That is
independent corroboration of the overlay finding from two iterations ago.
One piece is NOT on the disc and says so: the fade-OUT length. The fourth
keyframe has no time slot, because a group's last block stops four bytes
short. Measured instead, at 30fps, ~0.4s and the same both directions.
And a warning I earned: the luminance rise after a transition is NOT the
quad's ramp. The incoming screen's own elements animate in after the quad
has cleared -- 1.47s observed against a declared 0.97s. Time the fade
from where the frame is pure black.
Rig: screenshot samples at 0.5 Hz and cannot see a 0.4s fade at all,
which is why an earlier burst called this an instant cut. ffmpeg x11grab
at 30fps instead; both go in METHOD.
method lines withdrawn
Q5, measured off two boots: up/down move one item and WRAP at both ends
on the 5-item main menu and the 3-item EXTRAS alike; left/right do
nothing; B goes up one level and restores focus to the item you came
from (4/4); B on the main menu returns to the title; B on the title does
nothing. The menu opens on TUTORIAL -- the middle item -- 2/2, though a
third recorded run implies NEW GAME, so that one is reproducible rather
than invariant and says so.
Q4 by driving: LOAD GAME opens the save-slot list, TUTORIAL the lesson
list, OPTIONS the settings menu, EXTRAS build 6, MISSION SELECT the stage
list. NEW GAME is not tested -- A on it hangs the emulator and this
iteration needed the session. The GamePart ID behind each is NOT
measured: it is the entry of the decoded id table whose name matches the
screen I saw, and the page says so rather than wearing the badge.
And the withdrawal. Last iteration I wrote that these menus drop d-pad
presses shorter than ~0.3s. They do not. Once wrap is measured, every
press count I had is exactly right -- four presses moved four steps
THROUGH the bottom, which lands one above where a non-wrapping menu
would. I invented hardware flakiness instead of testing the ends of the
list. METHOD keeps the withdrawal rather than deleting the line.
Also: label brightness is not a cursor oracle here -- the background art
outshines the highlight on some rows. menu_focus.py reads the focus ring
instead, 254 vs <82, no tuning.
The handoff had Q3 as "runtime-solved only". It is not: the layer key at
+0x0A of the T8aD sprite header, stable-sorted, is a file-derivable paint
order, already checked against five measured orders and already driving
the compositor for every build on the disc. That answer had simply never
reached the page the port agent reads.
The new evidence is EXTRAS -- the first screen composited from the rule
alone and scored against a framebuffer capture the rule had never seen.
0.9620 at zero shift, against a same-tooling control of 0.9657 on the
screen the rule was fitted to.
Written down with its limit rather than its headline: align_to_capture
correlates edges, and a paint-order change moves blends, not edges, by
=<45/255 on a few per cent of pixels. So the score is evidence for
placement and only CONSISTENT with the order. The sharp A/B needs a
rebuild and is named in the doc instead of being claimed.
Q2. The archive is eight screens shipped twice, English and Japanese --
not the "build 4 title, 5 main menu, 6/8/9 submenus" the handoff claimed.
Build 8 is the JAPANESE main menu; 6 and 9 are the EN and JP EXTRAS, and
EXTRAS is the only submenu GP_TITLE holds. The PRESS (A) BUTTON plate is
its own build (2/3), composited over the title art and faded in a beat
later, not a state of build 4.
Confirmed by booting to the main menu and walking it: title, PRESS (A),
main menu and EXTRAS each match their render element for element. Builds
0/1 and 10/11 -- a DELTASABER / SYLPHEED A.I. plate -- were looked for in
the whole boot filmstrip, every title-side screen and the attract loop,
and appear in none of them; the reach of that negative is written down
rather than filled in with a guess.
Two rig traps went into METHOD: the menus drop d-pad presses shorter than
~0.3 s, and a grab 2.5 s after a transition can catch a screen mid-fade
-- which nearly wrote "the returned title has no plate" into the corpus.
Q1 of the menu port, measured against the running game rather than reasoned
about. The developer-logo splash is the cheap target: it is the first thing the
guest draws and its bundle declares short, unambiguous ramps.
Two results, both frame-exact and both emulator-speed-independent (frame numbers
are VdSwap counts, the guest's own frames):
* the ramp is LINEAR. A declared 15-unit fade lands on round(255*k/15) for all
seven of its samples with zero error, k stepping 2,4,6,8,10,12,14. No ease
can reproduce a constant step of 34 at both ends.
* the animation clock advances 2.000 time units per submitted frame, over six
consecutive intervals with no residual, with 1 unit as the quantum
underneath (one frame in the fade-out advances by 1).
The conversion to seconds is one step further and is flagged as such: 300 frames
took 10.87 s = 27.6 present-frames/second, which reads as a 30 Hz title at 92 %
under the emulator and gives 1 unit = 1/60 s -- the title build 4.2 s, the main
menu build 1.1 s. That reading is not proven, because the rate was measured
while the guest was still streaming from the ISO; the page names the one test
that would settle it and says what changes if it goes the other way.
Committed beside it: the raw draw capture and the per-frame quad CSV, so the
numbers can be re-derived without a disc or an emulator.
SYLPH_WEAKEST=1 (00052b0) 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.
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.
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 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.
With the sliver stopped at both ends (290cbe3 producer, 30e53f5 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.
Every statistic in screen_id is an AREA FRACTION, so a capture that is not a game
frame still produces clean numbers. Measured 2026-08-26: a guard shot came back
10x710 -- a sliver -- and classified as `menu` with green=0.0000, white=0.0157.
The guard passed, the fixed key sequence went out anyway, and the run loaded a
TUTORIAL instead of the save's Stage 02.
This is the second time this failure has been paid for. bin/screenshot's own
header records the first (2026-08-18): a second window of class "xenia_canary"
meant grabs came back as slivers and "a whole session's screen ids were noise".
That fix hardened the CAPTURE side only, so the same failure still reached the
oracles by any other path. Reject it at the point the answer is consumed too:
features() now returns None below 640x360 and classify() reports `none`.
Verified: the 10x710 sliver -> `none`; readyroom, flight and the briefing capture
all still classify as before.
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.
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.
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 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.
The whole page called this a stage-load/take-off freeze. Wrong, and the
evidence was in each run's own screenshots.
nav_to_flight.sh saves a shot per step. Compared CONSECUTIVELY:
01-extras -> 02-missionselect rmse 51.07, 99.42% changed
02-missionselect -> 03-selected rmse 0.00, 0.00% changed
03 -> 04 -> 05 -> 06 -> 07 -> 08-flight all 0.00
Shots 02 through 08 are pixel-identical. The screen stops at MISSION SELECT and
never changes. Every later step the script reports -- selected, readyroom,
takeoff, flight -- is it pressing buttons at a frozen image while screen_id.py
classifies a static frame.
Withdrawn as a result:
* "MISSION SELECT and the stage list worked, the failure came later during the
take-off load". It froze entering MISSION SELECT.
* challenge-mission-gate.md §5.6 already said entering MISSION SELECT fails on
a 128 MB request and shows "Disc Read Error". It was right; I contradicted
it because my navigation script claimed to have got further.
* Every "reached flight" in this session is false, including runs where
screen_id.py returned "flight".
Method note kept on the page: the first comparison ran every shot against shot 1
and returned rmse 51.07 / 99.42% for all seven, identical to two decimals across
seven different images. That is a tell, not a result. I then talked myself out
of it because md5sum showed eight distinct hashes -- which was PNG metadata.
Compare consecutive frames, and treat a suspiciously constant statistic as a bug
in the measurement.
Consequence for the oracle: the allocation failure happens while the display is
already frozen, so it is downstream, which fits the three runs that froze with
the allocation succeeding at 16, 32 and 128 MB. MISSION SELECT is the thing to
avoid -- every route this session used goes EXTRAS -> MISSION SELECT, and
newgame_path.sh / tutorial_launch.sh reach a mission by other menus.
The last file still resting on the "defaulted on disc" premise. Re-checked
through the record table over every unit-like object on the disc -- 114 with
a Generic.Type of Craft or Vessel:
present in ALL 114 objects 50
present in some 49
present in NONE 2 <- FiringRange, SELength
So only FiringRange and SELength are genuinely absent, out of 101 listed.
FiringRange was already recorded correctly elsewhere (runtime 0 is the
accessor's miss value); SELength joins it.
One pattern worth keeping rather than dismissing: 15 of the fields sit in
exactly 5 objects each -- AB_* (afterburner), PowerCut*, AA_/AV_AxisMode_*.
That is the Delta Saber family, so those fields are player-craft-only rather
than defaulted, which is a different fact than either the file or my
correction to it would suggest on its own.
The runtime values themselves stay -- they are real measurements. What is
withdrawn is the claim that the disc does not carry the fields.
That completes the sweep: weapon-datasheet-runtime.md, unit-struct-runtime.md,
live-unit-definitions.md, INDEX.md and now this capture all carried the same
wrong premise from the same blind reader.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
movie_manifest::parse now reads BASE_INFO's positional field keys (the game's
own cutscene ids, stage*100 + slot) and follows each to its record, instead of
scraping the string pool. The pool stores each distinct string once, so a
REPEAT reference produced no token and read as "no binding".
That single cause explains every wrong cell: 13 later references to
VOICE_D_450..454, two to SUBTITLE_hokyu_LS_s11A.tbl, and MS01A's share of
pwterop_s01a.prt. All 18 hokyu movies are bound, not five.
Counts, verified independently by me against the disc before recording:
104 cutscene SLOTS binding 101 distinct MOVIES; 99 slots / 96 movies with a
voice track, 99 / 96 with a subtitle, 22 / 22 with a telop. The docs' old
94 / 83 / 21 are exactly the counts of DISTINCT POOL STRINGS -- not wrong
measurements, measurements of the wrong thing. Three denominators were being
conflated; the new test pins all three.
Two assertions in movie_manifest_disc.rs were false and are corrected:
hokyu_DS_s13A binds VOICE_D_452 and resolves to eng\etc\VOICE_D_452.slb. The
in-game verdict that rejected that value tested an INFERENCE from a shared
demo id, on a decoder that discards 85-87% of banks in this class -- see
voice-bank-leading-region.md, committed earlier today.
The ~104 script ids are no longer open: they are literal positional keys,
each naming its record, and all 104 resolve. The old "counts differ by three,
positional pairing does not work" has a concrete cause -- three resupply
movies are bound by TWO slots each.
Also corrected: the naming convention has 3 subtitle exceptions (s24A/s27A
borrow s11A's track) and 18 voice exceptions, not one and five.
The legacy scraper is kept as a fallback for blobs with no record table, so
the synthetic unit fixtures still exercise it.
Artifacts: examples/movie_map_csv.rs regenerates the CSV, now slot-keyed
(104 rows; the movie-keyed version silently dropped one slot of each
duplicate). Disc tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
Last iteration I said launch_mission died because skip_intro only tests for
the title on a static frame, gated at rmse <= 1500, and that run logged 1503
and 1549 just above the cut. I also said the fix was NOT to nudge 1500 but
to measure both signals through a boot first. Measured, and the diagnosis
does not survive.
boot_trace.sh logs the two signals skip_intro decides on -- frame-to-frame
RMSE and the is_title.py green-glyph count -- through a clean boot with no
presses at all. One run, 29 samples over 484 s:
8 samples had rmse <= 1500, so the gate OPENED eight times
0 samples had glyph > 0, so the title was never seen
At t=145 s the RMSE was 1205, comfortably inside the threshold, and the
glyph test was called and answered zero. A frame can be perfectly static
without being the title -- the intro movie has long quiet stretches, three
reading RMSE exactly 0. So 1503/1549 were almost certainly movie frames too,
and raising the constant would have admitted two more of them.
What is left is narrower and honest: the interactive title never appeared,
rather than appearing and being missed by a threshold.
The limitation is recorded rather than buried: the tracer intended 1 s
sampling and achieved 16.9 s, because each iteration forks two screenshots,
ImageMagick compare and a fresh Python. So this does NOT prove the title
never appeared -- only that it was absent from 29 samples. A window shorter
than ~17 s falls between them. The recorded next step is to make the tracer
sample at the rate it claims before concluding anything stronger.
Artifact: docs/re/captures/boot-signal-trace.tsv.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
Hammering settles what a single write could not: hull 0x44BB8000 (1500.0f),
944,387 writes of 1 over 15s, and afterwards the value HELD at 1 -- the game
stopped rewriting it. The screen left 'flight', the HUD is gone, the ship is
burning, and Natalie radios 'I've lost contact with Rhino 3!', the player's own
callsign. The game read the poked value and killed the player.
Established: writes to /dev/shm reach the running guest; hull at pos+0x154 is
authoritative, not a readout; and a single write loses a race against the game's
own continuous writes.
This upgrades two earlier results from inconclusive to genuine negatives. The
unit-record pokes were downgraded because I could not tell 'ignored' from 'never
arrived'. The write arrives -- and those pokes persisted untouched for 60s, so
the game genuinely saw state=4 and handle=0 on all three objective squadrons and
did nothing. That is real evidence the phase-1 condition coroutine is not polling
and its checks run only when a trigger starts them.
Withdrawn: last iteration's claim that the pilot's hull= is a different field or
scale. I read 1000.0f at pos+0x154 and inferred a mismatch with the logged 1500;
this run reads 1500.0f at the same offset. Same field, different value per run.
With the per-entity searches refuted at word and bit level, the question became
which object owns the counter. Sampling +-0x200 around it across a 4->8
transition: the control interval moved 0 of 256 words, and the step moved nine -
the counter plus four words holding ASCII '4' -> '8' NUL-padded, and four
pointers into 0xbcad2xxx that swap with them. Read live at HUD 008, all four
character slots hold '8'.
So the neighbourhood is the HUD's rendered text for this counter, which reframes
the address: it is the HUD widget's value rather than "the mission's own
objective counter" as this file called it.
Recorded against that, because it is already measured: there is no separate
mission-side copy moving on the same step. ob_hunt scans all of guest memory and
requires a match across two transitions, and it left exactly one address.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The short-run strategy paid off: both counter steps happen in the first minutes
of flight, so one run yielded two complete passes before anything froze or was
killed. Pass 1 scanned at counter 4 and filtered on 4->8, leaving 29 of 710
candidates; pass 2 scanned at 8 and filtered on 8->12, leaving 2 of 197. The
intersection of the two survivor sets is EMPTY.
So no per-entity bit, in either polarity, anywhere in -0x400..+0xC00 around the
position triple, has a population that tracks REMAINING OB. With the word-level
refutation that closes the "objectives are marked in the entity object" family as
far as this method reaches.
Also recorded: 16 of pass 1's 29 survivors are the SAME word, pos+0x0250, with
sixteen different bits - the signature of a shared value rather than a flag, and
the same offset the word-level sweep found shared by 12 attackers. And a caveat
kept rather than smoothed: pass 2's B-sample HUD read 8 while RAM read 12.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The fourth run froze 9 seconds into the watcher's window, in flight, and the
probe built for that moment showed the healthy-run baseline and nothing else: one
pair, the same poller on the same object VA as every healthy run, only the thread
handle differing. No new (thread, object) pair appeared.
So the hypothesis the probe was built to catch is refuted - the freeze is not a
guest thread looping on KeWaitForSingleObject timeouts against ONE object - while
the CPU signature is unchanged from the gdb run: 1255 ticks over 10 s, 401 in the
TimerQueue thread and 292/280 in two guest threads.
What survives is stated as two specific blind spots of the instrument rather than
a shrug: the waits may cycle over DIFFERENT objects, which resets the streak and
makes them invisible to a same-object counter; or they may SUCCEED rather than
time out, which leaves a timeout counter nothing to count and would fit the
kernel-log evidence of a self-suspending worker cycling thousands of times
successfully.
Next is a v2 that counts calls per thread per second regardless of object or
result. The freeze lottery paid out on the first attempt this time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The gdb route works, and the recipe is written down: run-canary execs $XENIA_BIN,
so a wrapper that execs "gdb --args <real binary>" keeps the lockfile, the flags
and the process name (gdb forks and execs the real binary, so ps -C xenia_canary
still finds the inferior) while satisfying ptrace_scope=1 by being the parent.
The Release binary is not stripped - 26595 symtab entries - so frames have names.
The handle SIGSEGV/SIGBUS/SIG32-35 lines are mandatory: xenia uses SIGSEGV for
guest memory watches and the RT signals for thread suspend.
A run froze after ~4 minutes of flight, screen still "flight" rather than GAME
OVER, and all 79 threads had backtraces. EVERY ONE is in a wait - guest threads
in KeWaitForSingleObject / NtWaitForSingleObjectEx / SelfSuspend, the GPU command
processor parked idle, the main thread in poll().
It is nevertheless burning 1253 ticks per 10 s: 403 in the TimerQueue thread
(nanosleep inside TimerThreadMain) and 290 + 274 in two guest threads that the
backtrace shows blocked in KeWaitForSingleObject. A thread genuinely blocked
cannot burn 28% of a core, so those two are CYCLING - a timed wait that expires
and is re-entered - with the timer thread servicing them hot. Three samples
minutes apart show identical frames.
That refines the earlier "the guest is spinning, not deadlocked": the CPU burn is
real but it is in the WAIT PATH inside the kernel layer, not in guest code. The
shape is an event that never gets signalled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
With the pilot finally shooting, the experiment completed both halves. Sample A
at counter 12 over 120 entities gave 2 offsets where exactly 12 entities share a
value; the counter then went 12 -> 11 and NEITHER survived. So within +-0x400 of
an entity's position triple there is no 4-byte word whose shared-value population
tracks REMAINING OB.
The limits are recorded as part of the result, because they bound it: the test
asks which entities share an EXACT 32-bit value, so a single bit ORed into a word
that also carries health or a timer would never show up - a bit-level version of
the same differential is the follow-on. Anything outside the window, or on
entities that entities2 cannot see (it types by position CHANGING, so stationary
objectives are invisible), is untested too, and the populations differed a lot
between samples - 120 against 194.
Separately: REMAINING OB went 12 -> 11, the first decrement of this whole
investigation, while pilot.py logged 411 fire=1 samples and the HUD reached YOU
KILLED WARPLANES 0003. Stated carefully - it does NOT show the counter counts
kills, since an earlier run had the hostile population fall by a third with no
movement; it shows some kills close something the counter tracks.
Two robustness fixes: ob_flag retries an empty entity sample (one void run was
caused by exactly that), and Pad releases everything on interpreter exit - a
file-backed pad PERSISTS after its writer dies, so a tool killed mid-press would
leave a button held and the game would walk through menus on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
Measured, not argued: command a 45-degree-off-the-nose error and watch whether it
shrinks, both axes, both sides, two pulse widths, with the opposite sign as a
control. Yaw's sign is correct (45 -> 11.3/32.7 at 0.6 s, 41.6/33.8 at 1.2 s).
Pitch's is inverted - the pilot's own sign GREW the error every time
(48.3/55.6/70.7/91.7) and the opposite shrank it every time (30.7/41.5/15.7/9.6).
A method artefact is recorded because it gave the opposite answer first: a 3 s
full-deflection pulse overshoots a 45-degree error so far that BOTH signs look
wrong (45 -> 164 and 45 -> 178). A long pulse cannot answer a sign question.
Verified against the game rather than by inspection. Before: fire=1 in 0 of 13521
samples, |aim yaw| pinned at 90.0, target 36-43 km away. After: 43 of 1732, aim
down to 2.3 degrees, range median 6.3 km, and the HUD's own ammunition counters
moving - NOSE BM 06000 -> 05723, MAIN MPM 00300 -> 00298.
Also fixed a leftover of the same FIFO era: pilot.py called pad.f.write("tap A
90") for the target-select double tap, which raised AttributeError once Pad
stopped having an `f`. Pad gained tap()/dpad(); ctrl_probe.py and target_probe.py
still use pad.f and now say so in place.
Still open: YOU KILLED is 0000 after 250 s of firing and REMAINING OB is still
012. The craft shoots, closes and selects; whether it destroys anything is next.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The control this file never had: a run with the Kernel channel on, analysed WHILE
STILL FLYING, has 2738 refused resumes on one pair - more than the frozen run's
1171. The target's own lines show why. The game runs a self-suspending worker
(NtSuspendThread on itself, a manager thread resumes it, thousands of times), and
a self-suspended thread is not host-suspended, so the host Resume legitimately
returns false EVERY cycle: 3115 refusals against 3115 resumes.
The error is named rather than buried: the warning's commit says "~7 times in a
normal boot" and this file generalised that from boot to gameplay, where the
number is thousands. The 150x anomaly was an artefact of the baseline. The
zero-CPU threads go with it - the healthy run has four of those too.
What the instrumented reproduction DOES establish is sharper than the lead was.
The last kernel event in 690000 lines is "Thread F8000204 self-suspending", with
self-suspends 3116 against resumes 3115 - but the resumer never issues another
NtResumeThread at all, so nothing was dropped in flight; every thread stopped
together. And the guest is SPINNING, not deadlocked: over 10 s while frozen the
main thread is in state R gaining 409 ticks and guest threads gain ~680 in total
while making not one kernel call. So it is guest code waiting on something in
guest memory, and the next question is which guest PC.
Two corrections fall out: "the log stopped growing" is not a freeze detector (it
goes quiet for 25 s in normal flight), and 0xbdb59668 held the counter again -
4 of 6 runs now.
freeze_report.py makes the analysis repeatable, and refuses to answer "did this
thread ever run" when the Kernel channel was off rather than reporting a false NO.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
A Stage 02 run froze at TIME 01:02 with a radio line caught mid-word. The
emulator was alive at ~200% CPU with its main thread in state R, two screenshots
six seconds apart were byte-identical, and the log ended in a spin: 1171 of the
run's 1200 "host resume was refused" lines are the single pair F80002AC ->
F8000240, starting at the line immediately after F80002AC is created, and the log
never grows again. The commit that added that warning records what normal looks
like - about 7 in a whole boot - so this is a 150x anomaly on one pair rather
than noise. F8000240 itself appears exactly once outside the spin, at creation,
and calls nothing.
Eleven of the frozen process's 79 host threads have zero CPU, four of them
consecutive late-created guest threads - the same signature as the lost resume
that c1b57f93b fixed for the title screen. That fix IS in this build, so either
there is a second window in that race or this only looks alike.
The inference is named as one: nothing here maps a guest handle to a host tid, so
"the zero-CPU threads are the ones being resumed" is a reading of two consistent
observations. And the refusals could equally be the game's reaction to a worker
stuck for another reason - log_mask=13 has the Kernel channel disabled, so not
one of F8000240's waits is visible. The next experiment is written down
concretely: reproduce with LOG_MASK=12 LOG_LEVEL=3 and map the handle to a tid.
Also caps ob_hunt's survivor listing at 40 - an aborted run printed all 21482 and
buried the line that mattered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
Three things from one Stage 02 run.
The address recurs a third time: HUD=4 RAM=4 at 0xbdb59668, so 3 of the 5 runs
measured put the counter exactly there.
The counter is NOT a live class head-count. With the counter at 4 the typed
entity list was 8 attackers, 7 friendly Delta Sabers, 7 turrets and the player -
no class has 4 members and no pair of them sums to 4. That sharpens the corpus's
existing "012 against 118 live ADAN" note from "not the hostile count" to "not
the count of any class this enumeration can see".
The flag experiment itself proves nothing, and why is the useful part. It found
20 offsets where exactly 4 of 23 entities agree, then reported "the counter never
moved" for 600 s. The guest had stopped advancing ten seconds into flight:
pilot.py logged 724 s of identical speed/yaw/pitch, and two screenshots six
seconds apart were byte-identical, max delta 0 over 863325 pixels - while
screen_id said "flight", the emulator burned 212% CPU and every liveness check
passed. So that was a fact about a dead world. Withdrawn along with it: the claim
in ob_session.sh that the counter climbs on its own in the first minutes, which
one advancing run supports and this one cannot.
frozen.py makes it a single call, checked in both directions (0 on the frozen
pair, 254 on two frames of a live run), and ob_hunt/ob_flag now say GUEST FROZEN
rather than waiting out their timeouts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
One filter to select (004 -> 008, 35897 -> 7), a second on a transition it was
NOT selected by (008 -> 012, 7 -> 1), leaving exactly one address; then three
live paired readings, RAM/screenshot/RAM, all agreeing with the HUD.
The address is the same one run 1 reported. That does NOT reverse yesterday's
refutation and the entry says so explicitly: runs 2 and 3 read a hard 0 there on
an allocated page while the HUD counted, so "it is there in every run" stays
refuted. What is withdrawn is the stronger claim that the number was meaningless
- it recurs exactly, in 2 of the 4 runs measured, and run 3's amber candidate
0xbdb49668 sits one 64 KB page below it at the identical page offset 0x9668. The
practical rule is therefore: try 0xbdb59668, check it against the HUD, re-scan
when it reads 0.
Why run 3 failed and run 4 did not is also recorded, because it is a method
lesson rather than luck: the evidence was always in the first four minutes of the
stage, and the earlier runs simply could not look often enough - every HUD
reading cost a human round trip, so the 008 step went by between two of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
Two separate reasons the scripted route stopped, both measured rather than guessed:
* LOAD -> READY ROOM is not 28 s. Both runs on 2026-08-23 overran it, so the next
press was eaten by the transition and the run ended up in OPTIONS once and
BRIEFINGS once. wait_screen.sh now waits for the screen, with an optional
--tap that clears a dialog the caller cannot know about (a freshly restored
profile inserts "Auto-Save is active. OK?" here).
* The READY ROOM is DRAWN long before it is USABLE: it comes up with a
"Preparing to Sortie" spinner and TAKE OFF greyed out. The two states differ by
1.7 units of blue whole-image, so screen_id.py cannot separate them and should
not try. take_off_armed.py tests the label instead: 0.0000 bright pixels while
preparing, 0.1633 once armed, on three captures from two runs. It carries its
own position check - the always-enabled BRIEFINGS label below reads 0.1027 in
all three, to four decimals, so if that reference is dark the boxes are off the
labels and the answer is "unknown", not a confident wrong one.
screen_id.py gains a "readyroom" class from the same measurements; nothing else
reclassifies.
Verified end to end and unattended: boot -> title -> LOAD GAME -> slot 01 ->
READY ROOM -> TAKE OFF -> "IN FLIGHT at 34s", pilot bound and engaging.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
0xbdb59668 reads 0 in two independent Stage 02 runs while the HUD counts
004 -> 008 -> 012. Not an unmapped read: SEEK_DATA at that offset returns the
offset itself and the next hole is 5 MB later, so it is an allocated,
zero-filled word. The address was a per-run artefact, exactly as that file
already suspected it might be; the method is the durable result.
Re-finding it in the new run also failed, and both failures are recorded because
they are the instructive part. Two candidates were produced and both died on the
corpus's own rule -- verify across a transition you did not select on:
0xbc2377dc went 12 -> 18 while the HUD stayed 012 and read 3 two minutes later,
and 0xbd295b04 was plain noise.
One correction to the method note in that file: the scan is not slow. Over the
live /dev/shm image it takes 0.9 s. The real trap is that REMAINING OB climbs
004 -> 012 within about four minutes as waves spawn, so a scan is only valid if
the HUD is confirmed to hold the same value immediately before AND after it --
which is why the earlier 4-then-8 intersection came back empty.
What blocked finishing: with pilot.py retired at hull 340/1500 nothing was
killing objectives and the counter sat at 012 for five minutes, so there was no
later transition to filter on. What the counter counts, and whether an OB-badged
entity carries a flag, is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
The baseline binary, the profile, the Stage 02 save and the caches all live
outside both repos and had been wiped, and sylph-doctor reports 'all good'
without them. This is the recipe, verified by driving it to Stage 02 flight
rather than by inspecting files.
Three things worth having found:
* the baseline is an INCREMENTAL build, not a scratch one -- auto/upstream-baseline
differs from the instrumented branch in 202 files and zero submodule gitlinks,
so checking it out in the shared tree and rebuilding into the existing build dir
takes ~9 min at -j4. git worktree prune first; a dead worktree still claims the
branch.
* the baseline CANNOT make a profile (--create_profile_if_none is one of our 50
commits) and the Sign In dialog cannot be typed into, so bootstrap the profile
with the instrumented binary once and run the baseline against it.
* the Xbox content .header is NOT needed -- ContentManager::ListContent falls back
to the directory name when ReadContentHeaderFile fails, checked in the source.
The LOAD GAME screen then shows date, difficulty, flight time and clear ratio
anyway, which proves those come out of savedata itself. No header was fabricated.
Trap paid for once: keep the file name xenia_canary. A copy saved as
xenia_canary.baseline-... survives every pkill -x in the toolkit and holds the
run-canary lockfile while ps -C xenia_canary shows nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
Built upstream/canary_experimental (a5a18f5c7, 82 commits ahead of our merge
base) clean, to answer whether the 0x82307128 mission crash is ours or theirs.
Two tooling gaps found building it: version.h is never generated (upstream's
xenia-build.py writes it, build-canary does not invoke it, and our tree only
builds because a stale copy from an old sylpheed-re build sits in the build dir),
and build-canary reports exit 0 on a FAILED build - only the missing binary gave
it away.
Pure upstream cannot be driven at all: --hid=file is ours and there is no uinput
here. So the honest baseline is upstream plus those three pad commits. That
baseline reproduced the LOST RESUME on its second boot - black screen, guest at
522% CPU, newest host thread at 00:00:00 CPU, zero kernel calls from its handle -
and upstream's threading_posix.cc still has the two-lock-scope publish. So pure
upstream is not a stable state; it carries a bug we had already fixed.
Adding ONLY the threading fix on top - still none of the RE instrumentation -
reaches Stage 02 FLIGHT: TIME 05:03.75, wingman Rhino 103, radio dialogue, and
REMAINING OB 020 with OB badges on targets. The run ended in a legitimate GAME
OVER (the unattended craft was shot down, as autopilot-memory-driven.md warns),
not a crash. Crash dumps for the whole run: 0, against 384 on the instrumented
branch at the briefing alone.
So the mission crash is not upstream's. Whether it is in our 50 commits or fixed
among upstream's 82 this run does not separate; that needs a bisect.
REMAINING OB is visible and moves (020->019->018->017) but is NOT found. A
three-snapshot BE-u32 filter left 0 survivors; a differential across widths gave
exactly one candidate, 0xbc22e83c, and it is REFUTED - read live it held 26 while
the HUD showed 017. The empty u32 filter is itself informative: the counter is
not a plain persistent big-endian word.
Finding REMAINING OB in RAM means getting to the escort stage, and
launch_mission.sh exists for exactly that. It carried three of the dead patterns
this session has been clearing: six vgamepad calls (the command no longer exists,
so the entire scripted route pressed nothing), the XUID E0300000EFBEA3D4 which
has no profile behind it and therefore opens the sign-in dialog that swallows
every keystroke, and the emulator's stdout going to /dev/null. wait_flight.sh had
a fourth, in the tap that skips the launch cinematic.
Fixed, the route works far past where it used to stop: boot -> title -> LOAD GAME
-> slot 01 -> READY ROOM -> TAKE OFF -> the Stage 02 briefing map, with ACROPOLIS
labelled on it.
And it stops there: 384 dumps at PC 0x82307128 and no flight HUD within 300s. The
briefing is still animating (two shots a minute apart differ by RMSE 2986), so
the emulator is not frozen - flight simply never starts.
One discriminator worth recording: the TUTORIAL reaches flight and has run
completely crash-free, while this path crashes during the briefing, which is
where the stage content loads. That is consistent with the cache-flush shape the
crash has always had, and points at WHAT is being loaded rather than at how far
the navigation gets.
REMAINING OB is therefore still not found - the mission was never entered.
The autopilot doc's own problem #2 is that the pilot ignores the mission
objective - its 300s run took no damage, killed one fighter, and watched
REMAINING OB RISE from 004 to 011. So this surveys where that knowledge is.
GP_TUTORIAL.pak yields the game's own list of what a player can do: BASIC
CONTROLS, HEADS-UP DISPLAY, RADAR, SUPPLY AND SPECIAL MOVES, RADIO ORDERS,
ADVANCED CONTROLS, with descriptions. Worth stating against what the pilot
actually does: pilot.py and navigator.py use move and attack only. Resupply,
special moves, radio orders to wingmen and advanced maneuvers are all unused, and
two of them bear directly on open problems - resupply is the untested RETIRE
mode, radio orders is an escort lever that flying skill does not substitute for.
tables.pak is a name-resolved config seam: 79 IDXD objects, 6275 distinct field
names, including DLG_MISSION_OBJECTIVE and DLG_STAGE_TITLE01..16 (so 16 stages).
GP_MAIN_GAME_<lang>.pak holds 131 weapon\ tables, 113 unit\ and 64 message\ -
the weapon ones being what problem #1 wants for real projectile speed and range.
But the objective TEXT is not text: DLG_MISSION_OBJECTIVE = pdscr099.prt, a
screen part, and the message tables resolve to face textures. All human-readable
text in this game is rendered sprite art, which is why the UI work has been
reading pixels all along. A per-mission objective table is an OCR job, not a
table dump - so the cheaper and more actionable route is the runtime REMAINING OB
counter, which is on the HUD and therefore in RAM.
ship_capture_window.sh polls for the flight screen and presses F10 the moment it
appears rather than after a fixed sleep. One run gave three results.
First, the mission ran with ZERO crashes through t+152s - the first clean mission
run, where the three before it ended at 13243 / 11898 / 11497 - and it renders
and plays: player ship, starfield, full HUD, no dialog.
Second, the capture armed and wrote its file, so the mechanism works in-mission.
Third, and against expectation, the file holds NO ship geometry. 329KB, 181
deduped draws: 180 of them share a single vertex shader, all stride=28 vcount=3
prim=8 at full-screen coordinates, plus one full-screen quad, and not one draw
has positions outside the 1280x720 rectangle. The budget is not the limit -
kShipCaptureBudget is 8000 and only 181 distinct (vbase, WVP) pairs were seen -
and the scene was definitely drawing. The known-good capture from an earlier
session is 2.9 MB.
Fourth, a correction to the previous commit. It said the cache is "REFUTED as the
cure". Too strong: this run used the IDENTICAL complete cache as tut4 and
produced 0 crashes against tut4's 11497. What four runs support is that a
complete cache is not SUFFICIENT to prevent the storm and that run-to-run
variance dominates a 3-run comparison - not that the cache does nothing.
Next step is static: compare this capture's shape against the known-good one to
find why the 3D draws never reach CaptureShipDrawForRE.
The backlog has said a second capital-ship capture needs the 0x82307128 crash
dealt with first, because the run died at SELECT DATA before any mission
existed. That is no longer where it stops. With the Canary threading fix making
the menu dependable, tutorial_launch.sh drives boot -> title -> menu -> TUTORIAL
and the mission LOADS AND RENDERS: flight HUD, "Go to the box on your screen",
warship counters, controller diagram.
It then freezes. 13243 crash dumps, every one at PC 0x82307128, guest thread 9,
Access Violation read at 0x10000000C, preceded by exactly ONE
RtlRaiseException(E06D7363) right after a HostPathDevice::ResolvePath() with
empty arguments - the cache-flush shape. Two screenshots 6s apart are identical
(RMSE 0), no new dumps accumulate, and the process still burns 400% CPU.
So: reachable, renders, unusable for a capture. The blocker moved rather than
lifted, which is worth recording precisely because the entry's stated reason is
now wrong.
One new lead, deliberately not claimed as a cause: the resume-refused diagnostic
added with the threading fix fires 1671 times on this path and 1663 of them are
the SAME thread, F80001D8, with 152 before the first crash - against about 7 on
the menu path. Resuming a non-suspended thread legitimately returns false and
"Resume to kick a worker" would produce exactly this, so it is a lead, not a bug.
It is cheap to settle: check whether that thread makes kernel calls between
refusals.
Cheapest next test named: this page already shows a COMPLETE on-disc cache
produces no throw at all, and the cache is 40MB with .partial/.cold-rebuilt
leftovers beside it. One boot to warm, one to re-test.
The three orders the derived rule was built from all live in GP_TITLE.pak, so
they cannot confirm it - the rule was fitted to them. These two are from
GP_SAVE_LOAD.pak, read off the running game now that the Canary threading fix
makes the main menu dependable.
The 9-element slot-list header composites EXACTLY as the sort predicts, on all 6
instances of it, and nothing about this screen was fed into the rule:
measured 7 8 0 1 2 3 4 5 6
derived 7 8 0 1 2 3 4 5 6
including TWO tied groups (0xb102 x2 and 0xb210 x5) that both come out in
declaration order, and the unkeyed pfeff00.prm fade quad last.
The 13-element save/load frame differs in exactly the two open questions and no
new ones: two unkeyed pfbase.tbm backgrounds paint FIRST where the sort puts the
keyless last - the splash's palogo_eff0.prm behaviour in a different file type,
so implied_layer_key now covers it - and the 0xb100 group of four paints
10,11,8,12 where declaration order is 8,10,11,12.
That second point is a SECOND screen with a mis-ordered tie, which is what the
question needed, and it immediately kills a candidate: 10 and 11 are kind=0x2002
while 8 and 12 are 0x0000, so "descending kind then declaration index"
reproduces 10,11,8,12 exactly - and then fails both title groups, where every
element of 0x8083 is kind 0 and where 0x80a0 would predict 2,3,4,5,0,1,7 against
a measured 0,2,4,7,1,3,5. Seven candidates refuted now.
16 disc tests green.
resume_reliability.sh counts, per boot, whether the title's loader thread became
the CALLER of a kernel call. Counting boots where the press reached the title:
before the fix 1 of 5 ran the loader; after it, 5 of 5. Three of the post-fix
boots were driven far enough to classify the screen and all three reached the
MAIN MENU, each with the same shape - title at 216-252s, 40 kernel calls by the
loader, 6 ResolvePath reads. The failing boots before had zero of both, every
time.
Caveat kept in the doc: pre-fix runs were interleaved with compiles and a race is
load-sensitive, so the counts corroborate the mechanism rather than carry it.
Also fixes a self-inflicted confound. Two runs came back 'title_at_s=none' with
the emulator log ending in 'Killed', which I first read as the title detector
being too strict. It was not: an earlier copy of the script was still running
because 'pkill -f resume_reliability.sh' had matched the shell running it rather
than the script, and each of that copy's iterations kills the emulator BY NAME -
outside run-canary's lockfile. The harness now takes its own lockfile and refuses
a second copy, and the note records that pkill -f matches your own shell.
Two findings, the second only visible because of the first.
1. The harness has always had kernel logging switched off. log_mask DISABLES
categories (Kernel=1, Apu=2, Cpu=4, Gpu=8), so the long-standing --log_mask=13
meant Kernel+Cpu+Gpu off; kernel calls also log at Debug while log_level
defaults to Info. Seeing one needs BOTH LOG_MASK=12 and LOG_LEVEL=3, and no
boot log this project has taken ever contained a kernel call. boot_menu.sh now
takes LOG_MASK / LOG_LEVEL / EXTRA_FLAGS. A whole boot at Debug with Kernel on
is 23 MB, so the default was costing far more than it saved.
2. With that on, a captured failure shows the (A) handler doing everything right:
XamUserGetXUID(0, 7, ...)
NtCreateEvent(...)
ExCreateThread(..., entry=821748F0, ..., 00000001)
ExCreateThread Active: Thread Initially Suspended,
XThreadF80000CC (1F) Stack: 70880000-70900000
NtResumeThread(F80000CC, ...)
and the thread then never executing. Measured two independent ways: it makes
ZERO kernel calls - it appears 13 times in the log and every one is as an
ARGUMENT, never as the calling thread, while five other threads make 31905
calls after the resume - and its host thread has 00:00:00 CPU time while the
process sits at 546% and has burned 37 minutes of CPU in 6:46 wall.
A spinning thread burns CPU. This one has not run at all.
So the chain is: press delivered -> handler runs -> thread created suspended ->
resumed -> never scheduled. Input, the cache-flush crash and the game's own logic
are all excluded. A lost resume is a race, which is the first explanation that
fits the ~1-in-3 success rate.
Not settled: where the resume is lost, and no successful boot has been captured
with kernel logging to compare against.
"The title screen ignores (A)" is withdrawn. First-divergence across three boots
of the same binary says otherwise.
A slot-(1F) guest thread is spawned BY the press: exactly once per run,
immediately after the keydown, same stack base 70880000-70900000 in both runs
that got one, and never at all in the run that never accepted a press - which
rules out a periodic worker starting around the same time.
prm6, reached the menu: (A) at line 6498, (1F) at 6500, 6 ResolvePath after
opt2, stuck on the title: (A) at line 1287, (1F) at 1288, 0 ResolvePath after
opt, stalled before title: no (A) ever, no (1F) thread at all
In the successful run the loader immediately reads six paths out of the on-disc
cache and the menu appears. In the failed run the same thread starts and performs
no file I/O ever again. Total ResolvePath for the three boots is 90/84/78 - the
successful run's extra six are exactly the ones after the press, so the boots are
otherwise identical in I/O.
Refuted as the cause: the cache-flush std::out_of_range. All four of today's runs
have zero GUEST-THROW, zero CRASH DUMP and zero Access Violation, and the guest
stays alive throughout with its keystroke-poll counter climbing past 15000.
Next probe is neither input nor the crash: what the (1F) thread waits on.
The tie-break needs a fourth measured screen with a tied group, and OPTIONS is
three d-pad steps from the main menu, avoiding the NEW GAME hang. It was not
reached.
Advancing past the title is intermittent: 1 success in 3 attempts today with the
same binary, profile and procedure.
Attempt 1 stalled before the title existed. The guest was alive - RSS steady at
598MB and the keystroke poll counter climbing past 15000 - but the screen was
black and screen_children.py found ONLY the splash object resident. So that
failure is not a presentation problem: the game never constructed the title.
Being able to separate "stalled before building the screen" from "built but not
presenting" is new, and it is what the reader is for.
Attempt 2 reached the title at 265s and refused four presses - (A) at 0.25s,
0.25s and 0.5s, START at 0.3s - every one of them logged as DELIVERED to the
guest. Third independent confirmation that input delivery is fine and the game
simply does not act on the press. Waiting longer does not help either: presses
were spread from 10s to about 4 minutes after the title appeared, while the
earlier successful run's press landed within seconds.
Records the log as evidence and states the next probe: it has to be guest-side
now, on whatever consumes the keystroke, since the input path is excluded.
Read off the running game with screen_children.py and identified by pivot
signature as GP_TITLE.pak ratc-index 8, the NEW GAME / LOAD GAME / TUTORIAL /
OPTIONS / EXTRAS screen:
paint order: 1 3 4 2 5 8 9 6 7 15 10 11 12 13 14 0
It is the first measured screen carrying TWO primitives, and they land in
different places, which is the point. pteff02.prm (the 25% dim) paints 4th,
beneath the whole UI; pteff00.prm (the transition fade, resting transparent)
paints last. Both match their positions on the title screen exactly. So a
primitive's place is per-element and stable by role - backdrop first, dim at
slot 4, fade last - and there are now three permutations to test a derivation
against rather than two.
Wired into measured_paint_order, keyed by element names so both language builds
get it. The English build composited with --primitives edge-correlates at 0.9591
at shift (0,0) against a framebuffer capture taken in the same session - a third
screen confirming paint order, resting pose, fade alpha and primitives at once,
against a capture this project had not seen before.
13 disc tests green.