Commit Graph

247 Commits

Author SHA1 Message Date
Sylpheed RE agent
e3ad6ad28c tools: fix four boot-harness bugs; find the swallowed-keystroke cause and refute it
Chasing why (A) does not advance the title turned up four harness bugs, one real
root cause, and a refutation of that root cause as the explanation.

FIXED, each verified:
  * boot_menu.sh sent the emulator stdout to /dev/null, which is why none of the
    rest was visible. It now keeps a log and prints its path.
  * skip_intro.sh probed pixel (625,618) for the green (A) glyph - a 1280x720
    coordinate. screenshot returns the 1279x675 game surface, where that point
    is on the copyright line and reads (8,17,31). Symptom: a 600s TIMEOUT with
    the title on screen. Now classified with screen_id.py; next boot printed
    "TITLE at 239s -> A".
  * skip_intro.sh no longer taps through the movies. Making that press real (the
    vgamepad fix) was a REGRESSION: the boot then reached the title in 90s and
    that title accepted nothing. menu_draw_capture.sh records the same finding
    independently.
  * boot_menu.sh signs in whichever profile exists instead of a hard-coded XUID.

ROOT CAUSE FOUND: naming a XUID with no profile behind it opens a sign-in
dialog, xam_dialogs_shown_ goes to 1, and IsUIActive() then discards every
XamInputGetKeystrokeEx. Measured 8,388,601 swallowed calls in one boot with the
pad log showing presses arriving normally throughout. Only B13EBABEBABEBABE
exists on disc; the script asked for E0300000EFBEA3D4. Signing in the real
profile takes the swallow count 8,388,601 -> 0.

AND REFUTED: that is not why the title is stuck. With a valid profile and zero
swallowed keystrokes, a single (A) on the title that ends the boot still does
nothing. The dialog bug was real and worth fixing; it is not the explanation.

Next probe is specific: --log_mask=13 records no kernel calls, so it is not even
known whether the game polls at the title. A log on the SUCCESSFUL
XamInputGetKeystrokeEx return, mirroring the one already on the swallow path,
answers it in one boot.
2026-08-19 08:21:08 +00:00
Sylpheed RE agent
99081a6198 docs: the boot log confirms the vgamepad failure verbatim
'skip_intro.sh: line 44: vgamepad: command not found' right after 'movie -> skip
A'. Every '-> skip A' and 'TITLE -> A' this harness has printed pressed nothing,
which is also why the intro movie ran to 434s: it was never skipped.
2026-08-19 07:49:37 +00:00
Sylpheed RE agent
efee309d8f docs+tools: screen_children.py, validated; the third paint order is blocked on input
screen_children.py walks every resident screen object (vtable 0x820b30b4), reads
the element array at +0x08 and the child array at +0x30, and prints the paint
permutation with each pivot. Validated in one run off the live title: it
reproduces BOTH previously measured orders character-for-character - the splash
[0,2,4,6,1,3,5] and the 24-element title permutation - so the next screen it is
pointed at can be trusted.

The screen that matters was not reached. GP_DIALOG DIFFICULTY carries exactly
one primitive and would say whether the game paints it first or last, which is
the bit that decides where primitives belong. But (A) does not advance the title
screen at holds of 0.10, 0.25 or 0.40s, and neither does START, with the pad
verified end to end: pad.py writes /tmp/xenia_pad.txt, the emulator runs with
--hid=file --pad_file pointing at it, and the mtime updates on every press. The
title is unambiguously the interactive one - PRESS (A) BUTTON is rendered.

Recorded as a blocker rather than worked around. Next step is specific: boot with
the emulator stdout kept and read the [RE-INPUT] IsUIActive log already present
on the canary branch, which would say whether the keystroke is being swallowed
emulator-side.
2026-08-19 07:48:58 +00:00
Sylpheed RE agent
01580d294e tools: two dead flags that made every emulator boot script fail silently
Trying to read a third paint order off the running game turned up two bugs in
the capture harness, both of which fail in ways that look like the game
misbehaving rather than the script being wrong.

1. `--audio` is not a cvar in this tree, and eight boot scripts passed it.
   Xenia calls ShowSimpleMessageBox from ParseLaunchArguments, BEFORE logging is
   initialised, so the symptom is a 10x10 window, no log, no guest memory and a
   dialog that blocks on XIfEvent forever - i.e. a hang deep in the emulator.
   run-canary`s own header documents this exact trap; the scripts predate it.
   Removed from all eight.

2. `vgamepad` no longer exists - the uinput pad was replaced by the --hid=file
   driver and pad.py - but skip_intro.sh still called it. The script runs
   without `set -e`, so the call failed silently and the title branch pressed
   nothing while still exiting 0. A caller was told "TITLE -> A" with the game
   sitting on the title screen. It now presses through pad.py and exits 6 if
   that fails.

The first bug is fixed and verified: the boot now reaches the title screen with
PRESS (A) BUTTON. The second is fixed but does NOT unblock the title - see the
next commit.
2026-08-19 07:48:16 +00:00
Sylpheed RE agent
71d9b73afe docs: backlog — primitives drawn opt-in; their paint order needs a runtime read 2026-08-19 07:21:48 +00:00
Sylpheed RE agent
eb86142e4f formats/cli: draw the .prm primitives, opt-in, because their paint order is unsolved
fill_quad composites an untextured primitive as a solid rectangle of the
keyframes fade colour, pivot x 2 in size, placed and scaled exactly as a sprite
is. Behind ComposeOptions::include_primitives and screen render --primitives.

On the title screen, whose paint order is ground truth, it is measurably right:
mean luminance 76.30 -> 63.72 against the captures 64.58, i.e. from +18% to
-1.3%, and mean absolute difference 16.07 -> 13.08. The background was ~40% too
bright; pteff02.prm, a 25% black dim, was what was missing. The wordmark is not
dimmed by it because the measured order paints that quad at slot 4, beneath the
logo. Edge correlation moves 0.9538 -> 0.9480, which is not informative here: a
uniform dim scales gradients uniformly so a normalised edge score barely sees it.

OFF BY DEFAULT, and that is the finding. A primitive has no T8aD header, so no
layer key, and derived_paint_order sorts the keyless to the end. GP_DIALOGs
pzeff00.prm is a single keyframe of opaque black at full screen; painted last it
wipes the build. Of the 125 builds that draw a visible primitive, 36 come out
>99% one colour with the derived order.

No constant default works, because the two screens read off the running game
disagree: the splash paints its primitive FIRST (the black backdrop) while the
title paints one at slot 4 and another LAST (the fade-out). Declaration order
fails the title too. A disc test measures the damage rather than asserting the
feature works, so the number moves when the ordering is solved.

Also records a false alarm worth keeping: a first pass reported 36 GP_DIALOG
builds at "100% black", which was a crude near-black pixel threshold and not a
black screen - those dialogs are dimmed 50% and perfectly legible. The genuinely
wiped builds are a different set.
2026-08-19 07:21:32 +00:00
Sylpheed RE agent
6b65f7ea91 docs: backlog — fade alpha applied; drawing the .prm quads is unblocked 2026-08-19 06:56:56 +00:00
Sylpheed RE agent
7fbdcc078c formats: apply the keyframe fade alpha, and fix the resting rule it exposed
blit modulated by tint only, which is 0xffffffff on essentially every keyframe,
so the fade word was decoded, stored and then discarded. Applying it as an ARGB
modulate takes the title composite's edge correlation against the framebuffer
capture of the running game from 0.4597 to 0.9538, both at zero shift. The white
wordmark with its blue outline, the trademark, the copyright and the orange
exploding planet all appear -- the planet because a full-screen blue effect that
rests at alpha 0 had been painting over it at full opacity.

ARGB is measured: across a fade-in the high byte walks 0x00 -> 0x80 -> 0xc0 ->
0xe0 -> 0xff while the low three stay ffffff, and the low 24 bits are 0xffffff on
5276 of the disc's 5453 resting keyframes.

A modulate can only remove pixels, so the risk is a blank screen. Measured: no-op
on 4060 of 5200 sprite elements, partial on 453, hides 687 -- transient HUD
indicators that should not be lit at rest -- and NO build is left with nothing
visible. A disc test asserts that, and that the no-op share stays high, which
also guards the resting rule against drifting onto ramp frames.

Both changes are in one commit because the second is invisible without the first.
Applying fade erased the word PAUSE, which the running-game capture plainly
shows: pgptitle.rat has three runs of two identical keyframes -- pre-roll, hold,
exit -- and the 'later run wins' tie-break grabbed the exit. A group carries the
screen's entry animation AND its exit, so a run ending on the last keyframe is
now excluded unless it is the only one. The title correlation is unchanged.
2026-08-19 06:56:38 +00:00
Sylpheed RE agent
2d2c281805 docs: backlog — resting pose settled; the unapplied fade alpha is next 2026-08-19 06:41:41 +00:00
Sylpheed RE agent
b5c44b7c2a formats: the resting pose is the hold, not the longest dwell
Element::rest() picked the keyframe with the largest gap to the next keyframe's
time. That reads a keyframe as a value held until the next one; it is the start
of a ramp toward it. A long gap after keyframe k means the screen spends that
time arriving at k+1, so the settled pose is at the far end of the gap.

The title wordmark zooms in over five frames and holds at (184,193) at 100% from
t=251 to t=264. The old rule picked the frame before the long gap: (179,186) at
101%, still mid-zoom.

Measured against the framebuffer capture of the running title screen, which is a
1:1 crop so frame coordinates map directly (confirmed: the copyright line lands
on row 669 in the capture and in both composites). Edge-correlated over the
wordmark box:

    plateau (landed)      best 0.4597 at shift (0,0)
    longest dwell (old)   best 0.1511 at shift (+3,+8), 0.1268 at (0,0)

The old composite scores 3x lower and only peaks after being moved, by about the
(-5,-7) that picking kf4 instead of kf5 predicts.

It also fixes six title elements the old rule rested at alpha 0x00 where the
capture plainly shows them, and pteff00.prm - the full-screen fade quad painted
last - which rested at opaque black. That was the blocker on .prm compositing.

Adds tools/re-capture/align_to_capture.py, which is how this was scored, and
turns the .prm test that deliberately asserted the old defect into a guard on
the fix.

Not settled and now the next item: compose ignores the keyframe fade alpha
entirely (blit modulates by tint only), which is why choosing the wrong keyframe
was invisible until now.
2026-08-19 06:41:24 +00:00
Sylpheed RE agent
eb61368d23 docs: backlog — .prm decoded; the resting rule is the new blocker 2026-08-19 06:30:23 +00:00
Sylpheed RE agent
393e549752 formats: settle what .prm elements are — untextured quads marked by kind bit 0x10
369 .prm elements exist in the disc's screen builds and every composite is
missing them. Swept statically:

  * none of the 369 has a RATC child of its own name — no payload, nothing to
    draw, so .prm is a primitive and not a sprite;
  * kind & 0x10 and a .prm name agree with ZERO exceptions in either direction
    over all 965 builds, so the format marks primitives as a decoded field and a
    port need not parse filenames;
  * 361 of 369 are exactly 1280x720 at scale 100% in the corner, and their
    keyframe 'fade' ARGB is overwhelmingly black at some alpha — these are the
    fade-to-black, dim-behind-menu and flash layers, i.e. the PRMD dim-quad the
    compositor's backdrop has been standing in for.

Refuted before believing: drawing them at Element::rest() is wrong. The title's
pteff00.prm is opaque -> transparent -> transparent -> opaque, a transition whose
resting pose is the transparent plateau; rest() picks by longest dwell and lands
on the opaque endpoint, which is painted LAST on that screen and would black out
the title. A test asserts that wrong answer deliberately so that fixing the
resting rule fails it and leads to the note.

No compositing change: the resting rule is not .prm-specific and has to be A/B'd
against the title framebuffer capture first.
2026-08-19 06:30:05 +00:00
Sylpheed RE agent
a87d5b5255 docs: backlog — two items solved, four opened by them 2026-08-19 06:04:10 +00:00
Sylpheed RE agent
a1c370e810 cli/formats: let the screen commands reach bundles with no .rat child
The developer-logo splash declares its sprites directly and has no .rat layout
child, so is_build rejected it and no screen command could render it — despite
it being one of only two screens whose paint order has been read off the running
game, and the one where the layer key explains the whole permutation.

Adds ui_layout::is_composable (a declaration table plus at least one element
resolving to a T8aD the bundle carries) and an opt-in --all on screen
list/info/render. Measured on the disc: 2859 RATC bundles, 965 pass is_build,
2751 pass is_composable, and 0 pass is_build without passing it — a strict
superset. It is opt-in because the 1786 extra bundles are mostly two-element
fragments (a button and its glow), and because widening the default would
renumber --build for every pak, invalidating the build indices the corpus's
notes cite by number.

The splash now renders 6/7 elements, painting its glows first in the order
measured off the game; a disc test pins that order.
2026-08-19 06:03:52 +00:00
Sylpheed RE agent
b28763db0b docs: record the _eff-vs-focused-state confusion and both refuted rules 2026-08-19 05:57:38 +00:00
Sylpheed RE agent
2708abb482 formats: a focused-state record needs the element it is the focused state of
`compose` skips focused-state records by default, so whatever that flag matches
vanishes from every composite. It matched a trailing `f` in the name and nothing
else, which is not the convention — it is a letter.

Measured across the disc: 2458 elements match the suffix and only 54 of them
have the base element they would be the focused version of; all 54 are
`pgmenu_btnNNf.t32`. The other 2404, spread over 864 bundles, are `_eff` glow
layers whose names end in the same letter — `pb_name_eff.t32` alone accounts for
1122 of them, and `palogo_gamearts_eff.t32` is one the draw capture shows the
game painting, before its logo, with its own layer key.

Requiring the pair recovers 587 glows in the composable builds. GP_OPTIONS is
the plainest case: without `pbmwindow_eff.t32` the panel had no window at all,
just two floating brackets (captures/ui-layout/options-with{out,}-its-glow-layer.png).

Two other candidate rules were tried and refuted first. The `opt` link is not a
focus link: 221 elements are opt-targets, only 2 of them suffix-match, and the
targets include `pjnet_bg.rat` and `pv_loading_loop1.rat`. And the suffix alone,
as shown above, is unrelated to anything.
2026-08-19 05:57:12 +00:00
Sylpheed RE agent
fb4f035f08 docs: record the paint-order key as landed, with what it left unsettled 2026-08-19 05:43:29 +00:00
Sylpheed RE agent
deff5a4d6d formats: check the derived paint order against the screens already verified
The layer-key order was adopted from two measured screens and then applied to
every build on the disc, so it owed a regression check against the screens the
corpus had already validated against the running game.

Rendered the tutorial PAUSE menu and the title main menu both ways and diffed:
3.8 % and 1.1 % of pixels differ, max delta 45/255 and 34/255, and the two
renders are indistinguishable in layout — the change is confined to blends where
translucent sprites overlap. No regression, but which order is more faithful on
those two screens is unsettled and recorded as such.

Adds a corpus-wide test asserting every composite's draw list is strictly
increasing in (layer key, declaration index), streaming one pak at a time so it
does not OOM alongside the other whole-disc tests. It reports the rule's reach:
341 of 965 builds are reordered, and it fails if that share collapses.
2026-08-19 05:43:08 +00:00
Sylpheed RE agent
d9ae42dd55 formats: derive the paint order from the sprite layer key
compose now sorts elements by the word at +0x08 of their sprite's T8aD header
instead of painting in declaration order, for every build except the two whose
measured order is hard-coded. That word is non-decreasing in the order the game
actually paints both measured screens, so every screen nobody has captured now
gets its layering from the file rather than from the declaration table, which is
provably not the paint order.

Verified with artifacts and both ways, not by a green build: the disc test
asserts the measured orders never invert the key and that the composite's key
sequence is sorted, and reading the word from +0x0c instead makes it fail; the
title composites identically; and GP_MISSION_SELECT — uncaptured — now composites
cleanly, committed as a capture.

Two things recorded rather than smoothed over: ties keep declaration order
because the game breaks them some other way that is not known, and the
developer-logo splash has no .rat child, so is_build rejects it and the
compositor never sees that bundle at all — its measured order is inert in
practice and screen render cannot draw it.
2026-08-19 05:28:05 +00:00
Sylpheed RE agent
aa1f49633e docs/re: point the paint-order item at the derivation
The entry said deriving the order from the bundle was open. A key has been found
— the T8aD header word at +0x08 — so the entry now says that, what it does not
cover (ties, bit meanings), and that it is not wired into the compositor yet.
2026-08-19 05:17:44 +00:00
Sylpheed RE agent
56ac88e128 docs/re: the paint order is sorted by a layer key in the T8aD sprite header
The word at +0x08 of a T8aD header — which this project's decoder never read,
taking width/height/tiles from +0x14 onward — is non-decreasing in the order the
game paints a screen, on BOTH screens whose order has been measured: 20 of the
title's 24 elements (the other four have no T8aD sprite) and 6 of the splash's 7.
No inversion anywhere.

On the splash it explains the whole permutation: the three _eff glows carry
0xa100 and their base logos 0xa110, so the glows paint first even though the
declaration table interleaves them.

This is the first FILE-DERIVABLE account of the paint order. Everything checked
before failed — declaration order and its reverse, the placement region, the RATC
child order, keyframe start and rest times, resting Y, the runtime record's
fields, and every other build's table.

Recorded as unsettled: the ties (two groups share a key and are painted in an
order that is not declaration order), what the bits actually mean (the values
look like flag words, and the two screens use different ranges), and the fact
that two screens is two screens — a third permutation either promotes this to a
rule or breaks it.
2026-08-19 05:17:28 +00:00
Sylpheed RE agent
a290791a30 docs/re: point the ship item at the end-to-end mission-path measurement
The entry said the crash blocks it; it now also says how far a run gets (past
SELECT DATA, into a cinematic) and that the crash's firing point moves, so nobody
goes looking for a menu route around it.
2026-08-19 05:08:59 +00:00
Sylpheed RE agent
fa48e39d90 docs/re: the mission path measured end to end — the cache crash is the blocker
Driving menu -> NEW GAME -> DIFFICULTY -> SELECT DATA -> slot 01 with plain flags
gets further than any run so far: SELECT DATA is reached with ZERO crashes (the
screen is alive — a log_ui_draws probe there records 140 draws over 8 frames),
and then choosing a slot lets the game proceed into a cinematic or load, where it
crashes at 0x82307128 — the same cache-flush std::map erase.

So the crash is intermittent in WHERE it fires, not whether: boot, SELECT DATA,
and now after slot selection. There is no safe path through the menus to be found
by choosing different options.

And it is the blocker for every mission-side experiment — the second capital-ship
capture included. Navigation is not the obstacle any more; it is scripted and
works.

Not settled and said so: how to get past it. --mem_watch=false does not (twice).
--eh_dispatch is still untested because no run with it on has reached a throw.
The black-screen hang is a separate intermittent failure with no diagnosis. A fix
is emulator guest-race work, not RE.
2026-08-19 05:08:46 +00:00
Sylpheed RE agent
d0e3dc6f33 docs/re: three more runs, and eh_dispatch is still untested — with the proof why
Drove NEW GAME deliberately (the menu's first item, no d-pad, which is what the
run that first hit the crash actually did). Three runs with --eh_dispatch on: no
crash, no throw, black screen before the save-slot screen. It is tempting to read
that as the flag working.

The run with --cache_throw_diag as well disproves it. That cvar logs the throw
BEFORE any dispatch is attempted, so a throw would appear whatever dispatch then
did — and there were zero. No guest exception happened, so neither flag ran any
code, so neither can explain the difference. What took those runs out is the
intermittent content-load hang, before the crash path was reached.

eh_dispatch therefore stays untested, and the entry says so with the reasoning
rather than banking a false pass.

Confirmed on the way: the screen after NEW GAME is DIFFICULTY — its whole-image
signature matches the earlier capture exactly — so the menu path is understood
even though the runs die after it.
2026-08-19 04:43:25 +00:00
Sylpheed RE agent
743ddda8b1 docs/re: eh_dispatch is untested, and the black screen follows a content load
The run with --eh_dispatch=true produced zero crashes and zero throws, which is
not the result it looks like: the crash path was never reached, so the flag is
UNTESTED. Recording that rather than banking a false pass.

What the run did establish, with screenshots: TUTORIAL leads to a lesson list
(a fifth screen nobody had captured, committed), and selecting BASIC CONTROLS
puts the guest on a permanent black screen — no crash, no GUEST-THROW, emulator
healthy. That gives the black screen a trigger it did not have before: it follows
a CONTENT LOAD, which also fits the earlier black screens.

So the two blockers on the way to a mission are distinct and both unbeaten: the
SELECT DATA crash when the path goes through save-slot enumeration, and the
black-screen hang on loading content. The eh_dispatch test needs a run that
actually reaches SELECT DATA.
2026-08-19 04:17:36 +00:00
Sylpheed RE agent
10b89c77a3 docs/re: restate the ship item's blocker as the crash, not navigation
Navigation is solved and scripted as far as the save-slot screen; what stops the
second capital-ship capture is the cache-flush crash firing at SELECT DATA. The
backlog entry now says that, so the next person does not re-solve navigation.
2026-08-19 04:01:09 +00:00
Sylpheed RE agent
72b0e23e3b docs/re: the cache-flush crash also fires at SELECT DATA, blocking every mission
Driving toward a mission for the second capital-ship capture works as far as two
screens nobody had captured — DIFFICULTY, then SELECT DATA — and then the guest
dies at PC 0x82307128, the SAME std::map/set erase as the boot-time throw, with
537 stacked dumps. Both screenshots are committed.

So the ship item's blocker is the crash, not navigation: the game reaches the
save-slot screen and dies enumerating save data. mem_watch stays eliminated —
this run had it off. And the save/cache path is the common factor across both
firings.

Stated as unsettled: whether a warm cache prevents this firing the way it
prevents the boot-time one. The cache WAS warm here, so it looks like no, but
that is a single run and the cold/warm A/B was only ever run against the
boot-time throw.
2026-08-19 04:00:53 +00:00
Sylpheed RE agent
24b68ed063 docs/re: flag the load confound behind the input and boot-time numbers
The container runs the emulator, the Canary build and the disc test suite on the
same 12 cores, and Xenia alone takes about 5.5 of them. Several (A) experiments
and most boot-duration numbers in this entry were measured while a cargo test or
a build was running; one boot under that load never reached the title in 400 s,
against 130-290 s unloaded.

It does not explain the (A) result by itself — the interleaved A/B alternated
arms under whatever load was present, and the latest title recorded accepted
while an earlier one refused — but "a starved guest misses the input window" is a
hypothesis the existing data cannot exclude, and it should have been flagged when
the numbers were taken rather than after. The same navigation is now being run on
an idle box.
2026-08-19 03:43:32 +00:00
Sylpheed RE agent
0b4dcc6e8f tools/docs: the second-capital-ship blocker is stale; retry boots, not presses
BACKLOG said the per-class ship generalisation needs "the capture-ship-placement
build of Canary". Checked: the ship capture is in the CURRENT build —
RequestShipCaptureFrame and CaptureShipDrawForRE are in command_processor.cc on
auto/re-ui-draw-order, and F10 wrote a 2.9 MB ship capture from this session's
binary. No separate build is needed.

What actually blocks it is navigation: a mission is behind the main menu, and (A)
at the title is accepted on about half of boots with nothing observable
predicting which. tutorial_launch.sh drives boot -> title -> menu -> TUTORIAL and
retries the WHOLE BOOT on refusal, because re-pressing the same title never
works — measured, not assumed.

First run of it: (A) refused, so it rebooted. That is the expected shape rather
than a failure of the harness.
2026-08-19 03:24:40 +00:00
Sylpheed RE agent
343864cc2f docs/re: note that the viewer shares the compositor path
Checked rather than assumed: the Bevy viewer's UI Screens browser calls the same
ui_layout::compose, so the measured paint order and the ghost skip reach the
window a person looks at, not just the CLI's screen render. Its element table
stays in declaration order, so the per-element visibility toggles still index
correctly.
2026-08-19 03:13:37 +00:00
Sylpheed RE agent
f66e9de430 docs/re: the paint-order item now has a landed fix, not just a finding
BACKLOG said the viewer paints in declaration order and the title composites
wrongly. That is no longer true for the two builds with measured orders, so the
entry says what landed, what it exposed (the kind=0x4 ghosts), and what is still
open: deriving the order for screens nobody has captured. The order is
deterministic, so a rule exists; it is not in any decoded structure, and the
loader is what is left to read.
2026-08-19 02:32:31 +00:00
Sylpheed RE agent
c3c79ad028 formats: composite in the MEASURED paint order, and drop the ghost instances
The compositor painted in declaration order, which the draw capture proved wrong
— the title's background is declared ninth and paints first. The order the game
uses is its runtime child list, and no decoded field reproduces it, so rather
than leave the screen composited wrongly this lands the orders that HAVE been
read off the running game and falls back to declaration order everywhere else.
Two builds are covered: GP_TITLE's title build and the GAME ARTS / SETA / studio
anima splash. Keyed by element names, which identify a build across paks and
language variants.

Rendering it exposed a second defect, and the same capture settles it: the
kind = 0x4 elements are motion-trail ghosts, not resting content. The bundle
declares three instances of each wordmark; the capture shows exactly ONE quad at
each wordmark's position. Drawing them at their resting keyframe put three
oversized PROJECT SYLPHEED copies across the composite. They are now skipped.

Verified with an artifact, not a green build: the composite is committed
(captures/title-composited-measured-order.png) and now reads as the title screen
— background, planet, ship, wordmark, TM, copyright, correctly layered.

The test is disc-gated and was checked BOTH ways: it passes as landed, and
disabling the order table makes it fail. It reads one pak rather than every
build on the disc — the first version used the all-builds helper and got the
test process OOM-killed running alongside the other three.
2026-08-19 02:32:14 +00:00
Sylpheed RE agent
f35a871baa docs/re: no ordering field in the runtime element record either
The next suspect was a role or depth field in the 48-byte element record, so
every undecoded word was dumped for all 24 title elements against its paint slot.

Clean negative: +0x08, +0x0C, +0x18, +0x1C and +0x2C are zero on 21 of 24
elements, and the three exceptions hold what looks like live animation state.
Nothing there orders anything.

One confirmation on the way: +0x04 is the declaration entry's `kind`, verified
against the file for all 24 — 0x10 on the two .prm elements, 0x4 on the four
repeat instances, 0x3000 on the two ptlogoall_eff, zero elsewhere. The record
mirrors the file here as the pivot and keyframe count already did. And `kind`
does not explain the order either: the paint order interleaves kinds freely.

So the ordering is in none of the decoded data — not the declaration entry, not
the placement region, not the runtime record. What is left is the loader that
appends to +0x30, which is worth reading precisely because the order is
deterministic. Stated without promising a static rule exists merely because one
could.
2026-08-19 02:20:01 +00:00
Sylpheed RE agent
72afde7115 docs/re: the paint order is deterministic, and the 7-element screen is the logo splash
Two things this iteration, both of which change what is worth doing next.

The order is DETERMINISTIC: two independent boots give byte-for-byte identical
permutations for both live screens. That kills the hypothesis that the child list
is built in I/O-completion order — which mattered, because a run-dependent list
would have made deriving a rule pointless. It is a pure function of the bundle.

And the 7-element bundle is identified by matching its pivots against every
7-element build on the disc: it is the developer-logo splash (GAME ARTS / SETA /
studio anima, GP_TITLE entries 11/14). With names attached, its paint order
0 2 4 6 1 3 5 reads as the full-screen .prm, then all three _eff glows, then all
three base logos — glow behind, logo on top.

That refines the earlier "grouped by sprite" reading and partly withdraws it:
here every element has its own sprite, so the grouping is by ROLE, not sprite
identity. The title build could not tell the two apart because its repeated logo
instances share both.

Still not derived: where the role comes from. No decoded field carries it, and
sorting on a "_eff" name suffix would be an odd thing for a loader to do.
2026-08-19 02:08:50 +00:00
Sylpheed RE agent
875ac2632d docs/re: two ground-truth paint permutations, and four more refutations
Deriving the child order from the bundle is what the port needs, so this collects
data rather than guessing. Both multi-element screen objects live on the title:

* a 7-element bundle paints 0 2 4 6 1 3 5 — evens then odds, a stable partition;
* the 24-element title build paints 9 11 12 10 13 6 20 19 14 15 18 16 17 0 2 4 7
  1 3 5 22 23 21 8.

Two things the pair establishes. The list is GROUPED, not shuffled: elements
sharing a sprite are contiguous (ptlogo1's three instances at slots 13-15,
ptlogo2's at 17-19, the back2eff family at 8-12). And the five single-keyframe
elements lead — exactly elements 9,10,11,12,13 have one keyframe, and the list
opens with all five, which is a file-visible property.

Refuted on this data: first-keyframe time and resting time (back2eff1 starts at
52 and rests at 56, yet paints after back2 at 66/80), declaration order and its
reverse, and the RATC child order (ptbase2 is child 4 and paints first while
ptlogo1 is child 0 and paints fourteenth).

Not settled, and said so: the rule. The group CONTENTS are explained; the group
ORDER is not, and two permutations are too few to build it.
2026-08-19 01:56:04 +00:00
Sylpheed RE agent
069165c03d docs/re: mark the paint-order item solved, and say what is left
The item that has been open through this whole run is answered: the paint order
is the screen object's reordered child array at +0x30, not any table in the file.
BACKLOG and INDEX now lead with that, and the investigation behind it is kept in
full underneath — most of it refutations, which is what made the answer findable.

What is left is stated in the same breath, because it is what the port needs:
deriving that order from the bundle without running the game. Until then the
viewer paints in declaration order and the title screen composites wrongly, which
is now a documented defect rather than a mystery.
2026-08-19 01:46:20 +00:00
Sylpheed RE agent
688851122d docs/re: SOLVED — the paint order is the screen object's child array at +0x30
The screen object holds a SECOND list of its elements, a reordering built at load
time, and that list is the paint order. It is 24 pointers at +0x30, each the
+0x00 field of one of the 48-byte element records, so both arrays hold the same
objects in different orders.

Checked against the draw capture rather than asserted: the seven elements the
capture can name sit at child slots 0, 6, 7, 13, 16, 17, 22 — strictly ascending,
in exactly the captured submission order. It also resolves the one sub-order no
static field could explain, the pair that decodes to the same 1133x280: slot 6 is
element 20 and slot 7 is element 19, so they paint 20-then-19, DESCENDING in
declaration terms. And the kind=0x4 repeat instances sit immediately after their
template, where the declaration table interleaves them.

Stated as unsolved, because the port cannot read a runtime array: deriving this
order from the bundle. The order is clearly structured rather than arbitrary —
elements sharing a sprite are adjacent and the full-screen effects lead — so it
is worth attacking, but it is not attacked here.
2026-08-19 01:46:00 +00:00
Sylpheed RE agent
4134c21766 docs/re: the UI screen object, found and verified in live guest memory
The item class's vtable (0x820b30b4) is a fixed 4-byte value at offset 0 of every
instance, so gmem.py finds the objects in the running game with no debugger and
no emulator change. Seven are live on the title screen.

Their first {ptr,count,capacity} triplet identifies them outright: counts of 24,
7 and 1 — exactly GP_TITLE's title build, loading overlay and PRESS (A) BUTTON
bundle. The two-bundle composition of the title screen, which the draw capture
had inferred, is visible directly in memory.

The element array at +0x08 is 48-byte entries in declaration order, and it was
checked against the file at five positions with no misses: elements 0, 1, 6, 9
and 21 carry pivots (451,50), (449,46), (320,160), (320,180) and (309,10) as
floats, matching build 4 exactly, and elements 0/1 carry keyframe count 8, which
is their count in the file.

Stated as the reason this was done and NOT achieved: the array is in declaration
order, which the capture already proves is not the paint order, so the renderer
walks something else or sorts. Three further triplets in the object are
unidentified; the 24-entry one at +0x14 is already ruled out as an index list
(its entries are pointers, not indices). The gain is that the question is now a
data question on a structure that dumps in seconds.
2026-08-19 01:35:13 +00:00
Sylpheed RE agent
82b4a9e523 docs/re: more UI map, no walk — and say that this is becoming a rabbit hole
Three facts, none of them the thing being looked for:

* the drawing API is enumerable — the renderer accessor the quad emitter uses has
  exactly 6 callers, four of them sibling quad emitters;
* sub_823C2990 is a FACTORY, not the singleton accessor its use in the top-level
  render function suggested: it allocates 4 bytes plus a 244-byte object from the
  heap at [0x828E2B14] and runs the chain that ends at the constructor installing
  vtable 0x820b30b4. So a UI item is 244 bytes;
* the 0x823C region holds both the item class and four of the RATC-fourcc
  loaders, so bundle parsing and item construction live together.

And the part worth writing down more than any of them: three iterations have
added map without answering the question, and each step has been a plausible next
query rather than a decisive test — the shape of a search that can run forever.
The decisive alternative is costed instead of started: a Canary memory watch on
the UI vertex buffers would report the guest PC that writes them, naming the
emitter and its caller outright. That is real emulator work, and it is now a
choice to weigh against leaving the paint order unresolved, which costs the port
one screen's fidelity and nothing else.
2026-08-19 01:23:12 +00:00
Sylpheed RE agent
4808751113 docs/re: find the guest's UI quad class from the layout the capture measured
The capture pinned the UI vertex exactly — prim 13, stride 24, float3 position at
+0, k_8_8_8_8 colour at +12, float2 UV at +16 — so the binary was searched for
code that writes that shape. Fifteen candidates; the tightest, sub_82250138, is
unambiguous: it asks a renderer for 4 vertices of primitive type 13, then writes
x/y/z, colour 0xFFFFFFFF at +12, u/v at +16/+20, stepping by 24. Every constant
matches something measured rather than assumed.

Its caller is a constructor that installs vtable 0x820A7264
(ANON_Class_AAFDBF89, 2 slots) — the UI quad/sprite class.

Recorded with equal weight: this is NOT what the search was for. The chain up
from it ends at sub_821A8578, which turns out to be an unrolled run of ~30
identical call triplets — a fixed subsystem sequence, not an element walk. No
function that iterates screen elements has been found, and nothing here bears on
the paint order yet.
2026-08-19 01:16:14 +00:00
Sylpheed RE agent
38df977b30 docs/re: alpha does not break the tie — but it validates the fade decode
The plan was to separate ptlogo_back2eff from ptlogo_back2eff5 (same 1133x280,
declaration 20 vs 18, either side of ptlogo_back2 at 19) by their resting fade
alphas. Carried out, and refuted: both quads come back at alpha FF, and reading
the bundle again, both elements REST at 255 under this project's own max-dwell
rule — back2eff5's longest hold is t=74 at 255, and the 192 I had quoted is a
later, shorter keyframe, not its resting value. The ambiguity stands.

What the colour did buy is worth more than the tie-break: the first check of the
fade/keyframe decode against the RUNNING GAME rather than against another parse.
Every static element draws at exactly the resting alpha the bundle predicts, and
the only two quads whose alpha moves between consecutive frames (88->88/86->87
and 3C->38) are the rotating effect pair and the PRESS (A) glow — the two things
visibly animating on screen. Nothing had confirmed before that the alpha channel
of a keyframe is what drives that throb.
2026-08-19 01:09:25 +00:00
Sylpheed RE agent
3dc6d9d32c docs/re: the loading screen never boots, and no other build explains the order
Two routes closed, both cheaply.

The loading screen was the best discriminator reachable WITHOUT navigation — its
full-screen element is declared 7th of 10, so the two candidate orders would look
completely different. It does not appear during the boot at all: 23 captures
armed across an entire boot are all intro-movie frames, two full-screen quads
apiece. It belongs to a content load, which is behind the main menu, which is
behind the (A) that works half the time.

And a loophole this document had left open is shut: the title's sprites were
matched to build 4 BY SIZE, so the game might have been running some other build
whose table lists them in the captured order. Only two bundles in GP_TITLE.pak
contain all seven sprites, and neither is ascending in the captured order.

Recorded rather than glossed: one sub-order is genuinely ambiguous, because
ptlogo_back2eff and ptlogo_back2eff5 decode to the same 1133x280 and sit at
declaration 20 and 18, either side of ptlogo_back2 at 19. The UI quad carries a
k_8_8_8_8 colour attribute the hook does not log yet, and the two rest at
different fade alphas — logging it would name them apart.
2026-08-19 00:58:46 +00:00
Sylpheed RE agent
2483b7c3c9 docs/re: my own flag hypothesis fails its test, and the input path is mapped
With the capture cvar gone from the launch line entirely, a fresh run reached the
boot title and (A) still did nothing. The flag is therefore not necessary for the
failure, and 0-of-7-with vs 4-of-6-without does not survive as an explanation.

The conclusion is the plain one: (A) at the title succeeds about half the time
and nothing measurable from outside predicts which. Five candidates are now
eliminated — Xenia UI swallowing the keystroke, the driver filter, a sign-in or
content call on the press, the age of the title, and the capture cvar.

So the guest side was traced instead. XamInputGetKeystrokeEx has exactly one
caller in the title, and that has one caller: entry_point -> main loop
(sub_8216EA68) -> per-frame input update (sub_822F1AA8) -> the pad poll
(sub_82457038). The poller's layout is readable and matches the runtime trace
(keystroke queue at +68/+72 drained with flags=3; current state at +36 compared
field-by-field against the previous at +52), and nothing in it is state-gated.
Whatever ignores (A) is a consumer further up — a real RE job, and that is where
the thread now stands, stated rather than guessed at again.
2026-08-19 00:47:53 +00:00
Sylpheed RE agent
a04c133edc docs/re: boot time refuted as the gate on (A); the flag correlates with no mechanism
An interleaved A/B (arms alternating in one series, so cache warming cannot pose
as either variable) breaks the confound the last entry flagged — and not in the
direction the first table suggested. The LATEST title of every run recorded,
268 s, accepted (A); a 232 s title refused. "The title arrived late" is dead.

That leaves --log_ui_draws correlating at 0 of 7 against 4 of 5, with no
mechanism available: the cvar is read only when F10 arms a capture, F10 was never
pressed in those runs, the per-draw hook is one relaxed atomic load, and the two
arms' startup config dumps are byte-identical across 246 settings. A correlation
with no mechanism — and an arm that failed once — is not a finding; it is a
variable to remove. Removed: F10 now arms the capture unconditionally.

Also recorded: three boots this session ended on a permanent black screen, two of
them with no input at all. Not the pad, not the cache-flush crash, about one boot
in five.

Five candidate explanations for the (A) gate are now eliminated or refuted. The
next step is the guest's own state machine in sylpheed.db, not another guess from
outside.
2026-08-19 00:37:03 +00:00
Sylpheed RE agent
4990cf6eb3 docs/re: the boot-title recipe is 3/3 without my capture flag and 0/5 with it — and confounded
Using last iteration's recipe (first title, one (A)) four more times, it failed
every time. The nine-run table splits perfectly on --log_ui_draws: 3 of 3
successes without it, 0 of 5 with it. That would indict my own instrumentation
cvar, which is the one that captures paint order.

It is also perfectly confounded and the entry says so rather than picking the
exciting reading: every success had its title inside 147 s, every failure at
177 s or later. Flag and boot-time move together across all nine runs. A
mechanism is hard to see for the flag (it is read only when F10 arms a capture;
the per-draw hook is one relaxed atomic load), which argues for the boot-time
story — but "I cannot see how" is not evidence.

The separating experiment is named: ON runs with a fast boot. One that accepts
(A) exonerates the flag.

Also recorded: widening the menu signature last commit made screen_id classify
some boot frames as "menu", twice in one run before any menu existed. A dark-blue
movie frame and this menu are not separable from a single grab — navigation
scripts need the classification to hold across two grabs, which is a caller-side
fix, not a classifier one.
2026-08-19 00:00:43 +00:00
Sylpheed RE agent
b9062ea3bc tools: a working route to the menu, and the classifier that could not see it
screen_id.py called the main menu "other". Its menu rule required a near-white
fraction above 1.5%, measured in 2026-07; the menu reached from the boot title
measures 0.03% (mean 13,26,59 — dark, strongly blue, essentially green-free).
That is worse than a cosmetic miss: a script that waits for "menu" and never
sees it reports the navigation as failed while the menu is on screen, which is
exactly what happened here. Both measured signatures are now documented in the
code and both classify.

menu_draw_capture.sh now taps ONCE on the first title rather than up to 40 times:
repeating was measured to be useless (the attract title accepts nothing) and the
first title accepts a single press.

title_states_capture.sh is new — it captures the draw list in both title states
in one run, which is what refuted the "the attract title omits the button plate"
theory.
2026-08-18 23:10:29 +00:00
Sylpheed RE agent
9e16331155 docs/re: a second screen captured, and the two title states pinned down
Two results, one of which closes the navigation problem the last three
iterations kept hitting.

**The title states.** The hypothesis was that the attract-loop title is a
non-interactive presentation that omits the PRESS (A) plate. Half right:

* the state distinction is REAL — a single (A) on the title that ends the boot
  opens the main menu, 2 of 2 in independent runs, one of which never pressed
  F10; the title the attract loop returns to accepts nothing, not (A), START, B,
  BACK, X or Y, across dozens of delivered presses;
* the proposed tell is REFUTED — capturing the draws in both states in one run
  gives 13 quads at identical rects, ptbtn00 and ptbtn00f included. The two are
  identical to the renderer and different only to the guest.

So there is now a reliable route to the menu: first title after boot, one tap.

**The main menu's paint order**, captured with it. Its sprites are GP_TITLE
build 5's, and ptframe1/ptframe2/ptbtn01f land within 4 px of their declared
resting placements — an independent placement check on an untouched bundle.

It does NOT settle the ordering question, and the entry says so: build 5 lists
its background at indices 1-2, so declaration order and "background first"
predict the same sequence here. Same failure mode as GP_READY_ROOM/GP_OPTIONS.
What it does establish is that the title's disagreement is not a decode
artefact — same pak, same engine, one build that follows its table and one that
does not.
2026-08-18 23:10:04 +00:00
Sylpheed RE agent
4b0263b80f docs/re: the scripted route to the menu does not work — measured, not assumed
The previous entry named the next step: "tap (A) once per second and see whether
the success rate goes to 1". It was run. The answer is no.

40 presses at 1/s on the title change nothing, and neither does one each of
START, B, BACK, X and Y. Every press was delivered (the driver logs down/up for
each) and the new [RE-INPUT] diagnostic never fired, so nothing was swallowed —
the guest received them and ignored them.

Also recorded, because it destroys a run rather than merely wasting it: tapping
(A) through the boot (88 presses over the intro) ends on a permanent black
screen with no crash and no throw.

What is left is a difference in STATE, not in input: the one success came on a
title that appeared ~83 s into a warm boot, the failures on titles that appeared
after a full attract cycle. "The attract-loop title is a non-interactive
presentation" is written down as a hypothesis with the experiment that would
settle it — a log_ui_draws capture in each state, looking for ptbtn00.
2026-08-18 22:45:08 +00:00
Sylpheed RE agent
f5603aa107 docs/re: the main menu HAS been reached — the Ⓐ blocker is withdrawn
The previous entry called the title's Ⓐ a hard blocker. It is not: the main menu
is reached and screenshotted (NEW GAME / LOAD GAME / TUTORIAL / OPTIONS /
EXTRAS). What is true is narrower — Ⓐ advances the title only intermittently,
about one attempt in four, with the press verifiably delivered every time and no
Xenia UI active.

Three candidate causes were eliminated with measurements rather than argument:

* IsUIActive is now observable (Canary logs when it swallows a keystroke) and it
  never fires on the failing runs;
* the driver filter is fine — the game polls with flags=3 and the file pad
  reports Controller=1, so FilterDrivers keeps it;
* the game makes no content/user/signin call on the press at all — tracing every
  Xam call around it shows only input polling.

And two traps in my own measuring rig, which cost more than the bug and are
written down so nobody repeats them: a FIFO trace consumer that exits STALLS the
emulator (the guest stops polling — indistinguishable from a dead pad, and it
produced two runs of false evidence), and phase-A's kernel.return events carry a
placeholder return_value of literally 0, so "every keystroke call returns
SUCCESS" was an artifact of the logger, not a finding.
2026-08-18 22:18:51 +00:00
Sylpheed RE agent
44ff5454f9 docs/re: the cache-flush throw — a 100-second trigger, and two withdrawals
Chasing the title-screen crash into the emulator's own diagnostics turned it
from a mystery into a controlled experiment, and knocked over two things this
corpus said yesterday.

Withdrawn #1: "the fault address 0x1_0000000C is a pointer with a stale high
word". The crash dump prints r25 = 0x0000000C, clean. Xenia maps the guest's
4 GiB at host 0x1_00000000, so that IS guest address 12. The guest dereferenced
the small integer 12.

Withdrawn #2: "with --mem_watch=false the crash does not happen at all", which
named the crash-oracle handoff's suspect #1 as measured. It was confounded —
every --mem_watch=false run had also had a warm cache. Held cold, the throw
happens with the probe off (2 437 crash dumps). mem_watch is eliminated for this
crash.

What it actually is: the access violation is the guest's own `throw` RETURNING,
because this build logs guest C++ exceptions and continues rather than
unwinding. So the event is the throw, and with --cache_throw_diag=true the guest
names it: std::out_of_range, from the cache-manager flush, with a deque of 38
entries (37 distinct, one duplicated) against a 37-key map, every one of them
absent from the flush's snapshot but present in the live map — the TOCTOU race
the logger's own message describes.

And the new, useful part: the trigger is the on-disc cache. Complete cache, no
throw (2 runs). Directory moved aside or half-rebuilt, throw ~100 s into the boot
(3 runs, including one that threw with NO access violation behind it — which is
why crash dialogs are the wrong thing to count). A suspect in that bisection plan
now costs a `mv` and two minutes instead of a mission.
2026-08-18 21:44:22 +00:00