Commit Graph

722 Commits

Author SHA1 Message Date
Sylpheed RE agent
92b9ad6b01 re: CONFIRMED -- the A-route arrival times predict the OB counter's steps exactly
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.
2026-08-27 01:55:07 +00:00
Sylpheed RE agent
d8baf92fb5 re: mission TIME is the liveness oracle, and it explains the earlier "0 movers"
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.
2026-08-27 01:34:09 +00:00
Sylpheed RE agent
8ebe4edff2 re: test what the 'A' route kind means across all 28 stages -- enriched, not "attackers"
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.
2026-08-27 01:15:49 +00:00
Sylpheed RE agent
7e0c391b5c re: the 'A' route kind names the objective squadrons and predicts the whole OB trajectory
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.
2026-08-27 01:08:21 +00:00
Sylpheed RE agent
f5b030e3db re: phase-1's four attacker squadrons are measured -- all n=4, 16 craft
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.
2026-08-27 01:02:06 +00:00
Sylpheed RE agent
74314b457d re: test the OB hypothesis's weak point statically -- it holds, but a premise was wrong
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.
2026-08-27 00:55:27 +00:00
Sylpheed RE agent
f041de8636 re: hypothesis for what REMAINING OB counts -- marked attackers, 4 per squadron
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.
2026-08-27 00:48:52 +00:00
Sylpheed RE agent
fbea9f9148 re: refute the OB entity-flag on the FULL enumeration -- the stationary-entity hole is closed
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.
2026-08-27 00:40:53 +00:00
Sylpheed RE agent
e3098f7316 re: LOCATE REMAINING OB at 0xbdb69668, and refine the ...9668 pattern
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.
2026-08-27 00:23:22 +00:00
Sylpheed RE agent
7e89249f46 re: refute the "counter sits at page offset 0x9668" prediction
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.
2026-08-26 23:57:25 +00:00
Sylpheed RE agent
6e081c5437 re: correct my own OB write-up -- I refuted a stale status line, not an open question
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.
2026-08-26 23:57:01 +00:00
Sylpheed RE agent
dd052287e9 re: REMAINING OB's address is run-dependent -- cross-run stability refuted
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.
2026-08-26 23:34:36 +00:00
Sylpheed RE agent
d2c81249cd re: WITHDRAW "the absence is structural" -- capital ships do have +0x130 blocks
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.
2026-08-26 23:20:48 +00:00
Sylpheed RE agent
549134c415 re: the guarded route now verifies end-to-end, and the menu order is confirmed
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.
2026-08-26 23:08:11 +00:00
Sylpheed RE agent
290cbe3497 tools: never crop to a window too narrow to be a game frame
The app owns TWO windows of class xenia_canary -- measured in the tree right now
as 10x10+10+10 and 1280x745+1+20.  Largest-by-area picks the game window while
both are present, but during a load or mode switch the game window is briefly
absent from the tree, the 10x10 helper wins by default, and the crop produces a
10x710 SLIVER.  That is exactly the grab screen_id classified as `menu` on
2026-08-26, which let a nav guard pass on garbage and loaded the wrong stage.

Ignore candidates narrower than 640 so nothing is selected in that case and the
existing fall-through hands back the raw root grab -- itself a valid full frame.

Verified on the selection logic directly: with both windows listed the pick is
unchanged (1280x745+1+45); with only the helper listed the old logic returned
10x10+10+10 and the new one selects nothing.  Pairs with 30e53f5, which rejects
such a frame at the consumer.
2026-08-26 23:02:08 +00:00
Sylpheed RE agent
540eefeae8 tools: add assert_stage.py and a menu guard; record that the guard was not enough
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 30e53f5.

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.
2026-08-26 22:56:22 +00:00
Sylpheed RE agent
30e53f599c tools: reject degenerate captures in screen_id instead of classifying them
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.
2026-08-26 22:55:58 +00:00
Sylpheed RE agent
67001f04fb re: pin the GHAD field base -- it is tag+4, not the tag+8 I guessed
Closes the caveat I flagged in c9dc8dd.  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.
2026-08-26 22:39:02 +00:00
Sylpheed RE agent
c9dc8dd4db re: the stage drift is navigation, not the save -- slot 01 is deterministically Stage 02
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.
2026-08-26 22:32:30 +00:00
Sylpheed RE agent
a21927ab01 re: the two enumerations are different structures; only the vtable scan is complete
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.
2026-08-26 22:24:35 +00:00
Sylpheed RE agent
dddff2007a re: refute the projectile hypothesis; the count inflation is 2x duplication + arrivals
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.
2026-08-26 22:09:07 +00:00
Sylpheed RE agent
cc6d088573 re: the live entity readout works end-to-end; "0 movers" was a state
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.
2026-08-26 21:54:23 +00:00
Sylpheed RE agent
f59c4e595c re: refute my own position anchor, withdraw the entities2 "defect", find the trap
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.
2026-08-26 21:36:47 +00:00
Sylpheed RE agent
9770eadad4 re: withdraw "the speed cannot be used", and close the world-unit item
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.
2026-08-26 21:19:22 +00:00
Sylpheed RE agent
2d144ae7d0 re: RESTORE "one world unit is one metre" -- the objection conflated two families
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.
2026-08-26 21:18:54 +00:00
Sylpheed RE agent
042fb41fe3 backlog: record what the world-unit measurement now actually needs
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.
2026-08-26 21:10:26 +00:00
Sylpheed RE agent
f384fc42ee re: world-unit attempt -- control paired, HUD reached, two tool defects found
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.
2026-08-26 21:10:10 +00:00
Sylpheed RE agent
06f4dec741 backlog: the world-unit blocker was the ROUTE, not entities2.py's VA window
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.
2026-08-26 20:47:43 +00:00
Sylpheed RE agent
1f17726526 tools: classify the mission briefing instead of filing it as menu
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`.
2026-08-26 20:47:15 +00:00
Sylpheed RE agent
779bdde390 re: entity objects ARE spawned -- the vtables were stale-labelled, the ROUTE was wrong
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.
2026-08-26 20:46:21 +00:00
Sylpheed RE agent
1bada3ba4c re: the working entity captures came from a SAVE SLOT, not mission select
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.
2026-08-26 20:13:33 +00:00
Sylpheed RE agent
b56a4ab9f9 re: "follow the name pointer" is structurally dead for pak strings
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.
2026-08-26 20:06:10 +00:00
Sylpheed RE agent
432cc43885 re: the mission DOES load -- DEF_VTABLE=0 was a stale constant, not an empty game
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.
2026-08-26 19:56:52 +00:00
Sylpheed RE agent
9e421b341c re: the post-take-off state is measurably neither a menu nor flight
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.
2026-08-26 19:43:40 +00:00
Sylpheed RE agent
454be7b0de re: the take-off sequence lands in the launch CUTSCENE, not a hang
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.
2026-08-26 19:30:21 +00:00
Sylpheed RE agent
915d6adbd9 re: the sign-in fix holds end-to-end, but no units are loaded yet
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.
2026-08-26 19:20:24 +00:00
Sylpheed RE agent
45c5ff8234 re: the sign-in fix must be applied at launch, not by the navigator
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.
2026-08-26 19:07:25 +00:00
Sylpheed RE agent
15e58d93b1 re: correct the "screen id" -- they are monotonic counters, not identities
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.
2026-08-26 18:51:25 +00:00
Sylpheed RE agent
d712e5aa1a re: SOLVED -- the mission freeze is a modal sign-in dialog
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.
2026-08-26 18:41:09 +00:00
Sylpheed RE agent
18e9cecf21 re: WITHDRAW "the freeze requires the rasterizer" -- the control was not controlled
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.
2026-08-26 18:28:47 +00:00
Sylpheed RE agent
534da24658 re: find the menu screen id and cursor in guest memory
Menu navigation here has always been screenshot-driven, which is unusable under
--gpu=null -- the only configuration where the game does not hit the
software-rasterizer freeze.  Without a memory signal, the one backend that runs
is the one that cannot be steered.

Snapshotting 0x82800000+3 MB at each menu of a rendered run and keeping the
4-byte words that differ between screens and hold small integers leaves exactly
four of 786 432.  One has the property that matters -- it changes on a screen
transition and holds steady when only the highlight moves:

    0x828A690C  screen id     1 title, 3 main menu, 4 extras
    0x828F38AC  menu cursor   (second copy at 0x828F38BC)
    0x828F37B4  per-menu value

Verified on a fresh --gpu=null run with no display at all, driving the same keys
blind: title 1/2/12, main menu 3/4/45, after 4x down 3/12/45, extras 4/14/49 --
4/4 exact against the rendered run, across two runs and two GPU backends.  That
is the check that matters, since this corpus has already had to mark one runtime
address run-dependent.

tools/re-capture/menu_state.py reads them; `menu_state.py watch` prints on
change.

Open: the rest of the sequence into a mission.  Blind driving reached extras
(screen 4) and a further A did not move it, so MISSION SELECT needs a cursor
move first.  Screen ids beyond 4 are unmapped, and the rendered run freezes on
entering that screen -- so map ids up to the freeze, then step blind past it.
2026-08-26 18:21:04 +00:00
Sylpheed RE agent
5ecdb1c460 re: the freeze requires the rasterizer -- --gpu=null does not freeze
The cleanest control available, and it settles which side the bug is on.  Xenia
has a null graphics backend.  Same ISO, same route, same presses:

                          lavapipe            --gpu=null
    Main XThread CPU      0 ms / 3 s          3860 ms / 4 s
    state / wchan         S / futex_do_wait   R / not blocked
    guest memory churn    ~0 (18 B of 8.2 MB) 22.6% of 6 MB in 4 s
    allocations           stall               114 -> 120, no failures
    process CPU           265% (all llvmpipe) 330% (mostly guest)

With no rasterizer the guest does not freeze: it runs at close to a full core and
walks through the content load -- the exact point that hangs every rendered run.
So the fault is in the host rendering path, which explains why every game-side
hypothesis on that page was refuted in turn.

Blocked, and recorded as such rather than improvised around: deciding whether
lavapipe is deadlocked or merely taking hours over one pathological draw needs
hardware Vulkan, which this container does not have.  Six minutes of nine
saturated llvmpipe threads with no frame is suggestive, not conclusive.

Amber workaround: --gpu=null gives a live guest, and everything the backlog needs
-- entity positions, flight model, world unit -- is read from guest memory, not
pixels.  The gap is navigation: menu-walking is screenshot-driven, and blind A
presses advanced allocations 114 -> 120 but never reached a mission (DEF_VTABLE
and INST_VTABLE scans both 0).  Next step is to drive navigation from guest
memory instead of the screen, which is engineering rather than mystery.
2026-08-26 18:12:35 +00:00
Sylpheed RE agent
ffdccba655 re: WITHDRAW the infinite-loop conclusion -- gdb was the confound
The previous section concluded the guest spins forever in sub_82457780.
Measured without gdb, that is false.  Every observation behind it came from a
gdb-hosted run, and gdb intercepts every SIGSEGV -- which Xenia uses for guest
memory watches -- so it perturbs precisely what was being measured.

Same measurement, no debugger, per-thread /proc sampling parsed after the last
')':

    Main XThread          0 ms CPU per 3 s, 0 faults, state S, futex_do_wait
    GPU Commands         10 ms
    WSI swapchain queue 130 ms
    llvmpipe-0..9      ~1030 ms each per 6 s

Process at 265% of a core, essentially all in the software rasterizer.  The
guest thread is BLOCKED on a futex using zero CPU -- not spinning, and not in
sub_82457780.

Under gdb that same thread appeared as the top CPU consumer (890 ms / 4 s) and
appeared to be in guest JIT code, then in xe::ExceptionHandlerCallback.  That
was signal interception plus gdb re-stopping it between samples (state t,
wchan ptrace_stop).

Withdrawn: "an infinite copy loop in sub_82457780 is the freeze", and "Main
XThread is the top CPU consumer, so it is a spin not a block".  The opposite
holds.

Survives: the emit_source_annotations technique is sound and did prove the PC
was 0x824578A0 while the guest ran under gdb, so the guest does pass through
that copy loop.  The bne-exact-equality reading remains an accurate description
of the disassembly and a plausible hazard -- just not this freeze.

What the freeze actually is: guest blocked on a host futex at zero CPU, software
rasterizer saturated for six minutes without producing a frame, nothing
faulting, screen unchanged.  That is the guest waiting on a GPU operation that
never completes, with lavapipe spinning -- a HOST RENDERING problem, which is
also why every game-side hypothesis this session was refuted in turn.

Method rule earned the hard way and recorded on the page: do not diagnose a
performance or liveness question under a debugger.  Use it to read state at a
known stopping point, then re-measure timing and CPU detached.
2026-08-26 18:02:54 +00:00
Sylpheed RE agent
abed25fd67 re: prove the guest PC -- an infinite copy loop in sub_82457780
The previous section withdrew this localisation for resting on a stale
PPCContext.  It is now re-established by a sound method.

Xenia has a CPU cvar, emit_source_annotations, that makes MarkSourceOffset emit
"nop; nop; mov eax,<guest_address>; nop; nop" at EVERY guest instruction
boundary -- 90 90 B8 xx xx xx xx 90 90 in the code bytes.  So the guest PC reads
straight out of memory around rip, with no context and no offset guessing.

Dumping backwards from rip gives a clean run of them -- 0x82457874, 78, 7C, 80,
84 -- and the annotation immediately before rip is 0x824578A0.  That is
`sth r6, 0(r9)` inside sub_82457780: the exact instruction predicted from the
disassembly last time, now reached independently.

The loop advances r11 by 8 and terminates on `bne cr6` -- exact inequality.  If
r31 - r7 is not a positive multiple of 8, the comparison never becomes equal and
the loop never exits.

And the thread is genuinely running: over 4 s, Main XThread consumed 890 ms of
CPU, the top of any thread, with the process at ~238%.  A spin, not a block.

One bad metric recorded: I tried to show the loop marching through memory by
watching the last non-zero byte above 0x70200000.  It never moved -- but that
statistic saturates in an already-written region, so it could not have moved.
Not evidence either way, though it briefly looked like a refutation.

Open and now narrow: what makes r31 - r7 non-congruent to 0 mod 8.  r7 comes
from 0(r30), r31 is the container end, and r3 holds a fresh buffer from
0x824F7240 -- read those three from HOST registers at the freeze.
2026-08-26 17:50:57 +00:00
Sylpheed RE agent
ee2f737d78 re: audit the gdb session -- withdraw the sub_82457780 localisation
Went back to verify the instrumentation before building on it.  Some does not
hold.

Verified: the trailing comments in ppc_context.h are not struct offsets (the
struct opens with eight cr unions and fpscr), but rsi+0x20 for r[32] is
nonetheless correct -- confirmed empirically because the f[32] doubles begin at
+0x140 and 32*8 back from there lands on +0x20.

Withdrawn: the context is STALE mid-function.  The same dump shows r1 = 0, and a
guest stack pointer cannot be zero inside a function -- Xenia's JIT keeps live
guest registers in host registers and spills only at certain points.  So
r13 = 0x82457864 does not show where the guest is executing, and identifying
sub_82457780 as the stuck function is withdrawn as unproven.  The description of
what that code does (grow-and-copy, beq exit) is still accurate; it is just not
established that the guest is in it.

Three readings retracted for broken instrumentation:

  * "rip identical across samples ⇒ no progress" -- gdb stops the process
    between samples, and the JIT lays code out differently per run (a05be939 vs
    a05bc839 for the same instruction).
  * "zero minor faults on the stuck thread" and "utime=0" -- both parsed
    /proc/<pid>/task/<tid>/stat with awk positional fields, but comm is
    parenthesised AND contains spaces ("Main XThread (F"), so every index after
    it was wrong.  Printing state=XThread should have been the giveaway.
  * the SIGSEGV fault-storm refutation rested on the same parse; downgraded to
    unproven, though the process-wide ~300/s still argues against a tight loop.

Survives: the freeze reproduces without gdb; with the process stopped exactly
one thread is in userspace JIT code while a wchan census puts the rest in
futex_do_wait (55) or hrtimer_nanosleep (11); the instruction is a 16-bit store
through the membase to guest 0x701d0000, a stack-region address, first of a
four-store group; and that memory was unchanged over seconds of running time.

Next step is a host->guest code mapping from Xenia's own code cache rather than
inference from a stale context.
2026-08-26 17:38:49 +00:00
Sylpheed RE agent
a70b1d8724 re: locate the stuck thread -- a grow-and-copy in sub_82457780
Ran the freeze under the corpus's gdb wrapper (ptrace_scope is 1, so a debugger
must launch rather than attach, and its handle lines are needed because Xenia
uses SIGSEGV for guest memory watches).

One thread of 80 is in guest code: thread 50, Main XThread, at rip a05be939 in
JIT output.  All others are in a futex or clock_nanosleep.

Xenia's x64 backend keeps PPCContext in rsi, with r[32] at +0x20.  The guest
GPRs there give r8 = a3ac0000 (the 64 MB buffer from the doubling sequence),
r12 = a3ac0a18 inside it, and r13 = 82457864 -- guest code, sub_82457780.

That address sits in a grow-and-copy: size = count*8 clamped to 0x1FFFFFFF, a
call to 0x824F7240, then a loop copying halfwords eight bytes at a time.  The
host instruction it is stopped on is that copy's store:

    mov %r12w,(%rdi,%rax,1)     rdi = membase, rax = 0x701d0000

Sampled three times seconds apart with continue in between: rip, r13, r11 and
r31 identical every time.  No progress.

The loop's exit test is beq -- equality, not >= -- so an inconsistent start/end
pair never terminates it.  Recorded as amber: that is a reading of the
disassembly, not a demonstration.

Two of my own readings corrected:

  * "the guest spins at ~400% CPU" was ps's CUMULATIVE AVERAGE since process
    start, not an instantaneous rate.  Per-thread sampling puts Main XThread
    nowhere near the top.
  * a SIGSEGV fault storm fitted the constant rip nicely and is refuted: 1500
    minor faults in 5 s, zero major.

Caveat kept on the page: these are gdb-hosted observations and gdb intercepts
every SIGSEGV, so absolute timings are not the ungoverned ones.  The freeze is
not a gdb artefact -- it reproduces in every non-gdb run.
2026-08-26 17:28:31 +00:00
Sylpheed RE agent
d8f0a5e798 re: the allocation failure is definitively not the freeze -- four runs, three outcomes
Four runs now freeze at the first content load after the main menu with three
different allocation outcomes: stock (fails at 128 MB and throws), GPU-writeback
patched (no failure, stops at 32 MB), heavy XMA logging (no failure, stops at
16 MB), and no-savegame (no failure, no throw).  The memory account on this page
is accurate but describes a symptom; everything treating the 128 MB refusal as
the freeze is superseded.

The savegame is not the variable.  --content_root pointed at a profile-only tree
(the real content untouched) boots to the identical pools -- BC220000 Size
03A80000 and B50C0000 Size 070E0000, same addresses and sizes, same 43
allocations by the menu -- then freezes at the same press with zero failures.

Not slow shader compilation either.  Every earlier run called it frozen within
~30 s; this one was left six minutes at 0.00% liveness with CPU still 399-479%.

Correction made mid-measurement: the allocation counter creeping 119->124 looked
like "guest running, display stuck".  It is not.  A broad sample -- 400 extents,
8.2 MB -- changed 18 bytes in 3 seconds.  The guest is spinning, with one thread
allocating about once a minute.  The earlier "8 MB slab unchanged" reading was
weak for the same reason in reverse: one arbitrary region proves little.  Sample
broadly before calling a guest alive or dead, exactly as the screen needed two
frames rather than one.

Now excluded: the heap failure, the leak, rounding, MmQueryStatistics, a
heap-size knob, the exception cvar, a kernel-object wait, a build regression, the
route, the savegame, and shader compilation.
2026-08-26 17:15:03 +00:00
Sylpheed RE agent
5ed90577ee re: the 171 MB of boot allocations are the game's own pools, ~90% empty
Read from a LIVE guest (main menu, liveness 98.59%) rather than a frozen one,
sampling a page per megabyte:

    B50C0000  112.82 MB  89.7% zeros, header all zero
    BC220000   58.45 MB  90.8% zeros, header = pointers into itself

Neither holds content at the main menu and neither matches a disc file's size.
They are pre-reserved pools claimed at boot and mostly untouched.

BC220000's first words -- bc220010, bc79c9c0, bcd24c00, bc220040, bd17cda0,
bc79ccc0, all inside [BC220000, BFCA0000) -- interleaved with ordinals 6,0,4,5,
0,1,2,3, are an intrusive free-list.  That closes a loop from this morning: the
22 "Release failed because address is not a region start" messages were frees of
interior pointers into this exact block.  The game suballocates from its own
pool and hands the sub-pointers to MmFreePhysicalMemory.  Independent
confirmation that Xenia is right to refuse them.

It sharpens the contradiction rather than resolving it.  The budget adds up --
~171 MB of pools, ~379 MB live at the menu, 128 MB wanted for a content load,
about 495 MB of a 512 MB console against Xenia's fixed 16 MB GPU reservation,
which is the measured ~15 MB shortfall.  But the SAME emulator with the SAME
reservation ran a mission for 500 s on 08-10, and that run's evidence is entity
hull values sampled from guest memory at 2 Hz, not screenshots, so it is not the
frozen-frame illusion that fooled this session twice.

So the next thing to vary is the boot inputs -- profile, save data, config --
since B50C0000's request is 0x070D28B0, an odd computed size rather than a round
reservation, and so plausibly depends on something configurable.
2026-08-26 16:59:26 +00:00
Sylpheed RE agent
a9ce94ec83 re: MISSION SELECT is not special -- NEW GAME freezes too
Last iteration concluded the freeze is "entering MISSION SELECT" and made
avoiding that screen the next experiment.  Ran it; the conclusion was too
narrow.

First, a liveness metric that actually separates the states: two frames five
seconds apart, percentage of pixels changed.  The menu animates, so healthy is
99.80-99.97% and frozen is 0.00%.  No navigation script needed, and no
classifier.  Committed as tools/re-capture/route_liveness_probe.sh; this is what
should have been used from the first run.

Then the menu's FIRST item, NEW GAME, which never touches MISSION SELECT:

    main menu             99.80% alive
    after A on NEW GAME    7.33%
    after the next A       0.00%  -- frozen, and screen_id calls it "flight"

with the same 134217728-byte AllocRange failure in the log.

So the correct statement is broader: the game freezes on the first content load
after the main menu, whichever item is taken.  MISSION SELECT was just the route
every earlier run used.  "Avoid MISSION SELECT" is withdrawn -- there is nothing
to avoid, and that also puts the memory account back at the centre, since ~379
MB live plus a 128 MB content load fails on any route.

Worth repeating because it caught me twice: screen_id.py called a frozen frame
"flight" on a run that never left the menus.  Liveness first, classification
second.
2026-08-26 16:51:59 +00:00
Sylpheed RE agent
afc74ca9f4 re: it is not a stage-load freeze -- the game freezes entering MISSION SELECT
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.
2026-08-26 16:42:13 +00:00