The tie-break needs a fourth measured screen with a tied group, and OPTIONS is three d-pad steps from the main menu, avoiding the NEW GAME hang. It was not reached. Advancing past the title is intermittent: 1 success in 3 attempts today with the same binary, profile and procedure. Attempt 1 stalled before the title existed. The guest was alive - RSS steady at 598MB and the keystroke poll counter climbing past 15000 - but the screen was black and screen_children.py found ONLY the splash object resident. So that failure is not a presentation problem: the game never constructed the title. Being able to separate "stalled before building the screen" from "built but not presenting" is new, and it is what the reader is for. Attempt 2 reached the title at 265s and refused four presses - (A) at 0.25s, 0.25s and 0.5s, START at 0.3s - every one of them logged as DELIVERED to the guest. Third independent confirmation that input delivery is fine and the game simply does not act on the press. Waiting longer does not help either: presses were spread from 10s to about 4 minutes after the title appeared, while the earlier successful run's press landed within seconds. Records the log as evidence and states the next probe: it has to be guest-side now, on whatever consumes the keystroke, since the input path is excluded.
219 lines
11 KiB
Markdown
219 lines
11 KiB
Markdown
# A third measured paint order — tool built and validated, screen not reached
|
||
|
||
**Status:** ✅ the reader works and is `CONFIRMED` against both previously
|
||
measured screens. 🔴 **BLOCKED** on the emulator: Ⓐ does not advance the title
|
||
screen, so `GP_DIALOG`'s DIFFICULTY box — the screen that would answer the
|
||
question — was never reached. Two harness bugs were found and fixed on the way.
|
||
|
||
## Why a third permutation
|
||
|
||
Drawing the `.prm` primitives is implemented and measurably right on the title,
|
||
but it is off by default because **nothing derives where a primitive paints**
|
||
([`structures/ui-prm-primitives.md`](structures/ui-prm-primitives.md)). A
|
||
primitive has no `T8aD` header, hence no layer key, and the two screens read off
|
||
the running game disagree about the answer: the splash paints its primitive
|
||
first, the title paints one at slot 4 and another last.
|
||
|
||
`GP_DIALOG`'s DIFFICULTY box carries **exactly one** primitive, `pceff00.prm`, a
|
||
50 % black dim. Whether the game paints it first (dimming what is behind the
|
||
dialog) or last (dimming the dialog too) is a single bit that would discriminate
|
||
between the candidate rules. It is reachable from the main menu: NEW GAME.
|
||
|
||
## The reader
|
||
|
||
`tools/re-capture/screen_children.py` walks every resident screen object
|
||
(vtable `0x820b30b4`), reads the element array at `+0x08` (48-byte records,
|
||
declaration order) and the child array at `+0x30` (pointers to those records,
|
||
**paint** order), and prints the permutation with each element's pivot — which is
|
||
what identifies the build in the file.
|
||
|
||
**Validated against both known-good screens in one run, off the live title:**
|
||
|
||
```
|
||
== object 0xbcd25288: 7 elements, 7 children
|
||
pivots: 0:(640,360) 1:(250,36) 2:(260,46) 3:(120,44) 4:(130,55) 5:(194,68) 6:(204,78)
|
||
paint order: [0, 2, 4, 6, 1, 3, 5]
|
||
|
||
== object 0xbcd25488: 24 elements, 24 children
|
||
paint order: [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8]
|
||
```
|
||
|
||
Both are **character-for-character** what
|
||
[`structures/ui-screen-runtime.md`](structures/ui-screen-runtime.md) recorded
|
||
from the hand-driven read. So the next screen this is pointed at can be trusted.
|
||
|
||
## Two harness bugs found on the way
|
||
|
||
Both fail in ways that look like the *game* misbehaving.
|
||
|
||
**`--audio` is not a cvar**, and eight boot scripts passed it. Xenia calls
|
||
`ShowSimpleMessageBox` from `ParseLaunchArguments`, *before* logging is
|
||
initialised, so the symptom is a 10×10 window, an empty log, no guest memory, and
|
||
a dialog blocking on `XIfEvent` forever — which reads as a hang deep in the
|
||
emulator. `run-canary`'s own header documents this trap; the scripts predate it.
|
||
Fixed in all eight, and the boot now reaches the title.
|
||
|
||
**`vgamepad` no longer exists.** The uinput pad was replaced by the `--hid=file`
|
||
driver plus `pad.py`, but `skip_intro.sh` still called `vgamepad tap A 250`. The
|
||
script runs without `set -e`, so the call failed silently and the title branch
|
||
pressed **nothing** while still `exit 0` — reporting "TITLE → A" with the game
|
||
sitting on the title. Now presses through `pad.py` and exits 6 if that fails.
|
||
|
||
The boot log states it outright, which is the only reason it was caught:
|
||
|
||
```
|
||
movie (rmse 1662) at 434s -> skip A
|
||
tools/re-capture/skip_intro.sh: line 44: vgamepad: command not found
|
||
```
|
||
|
||
Every "-> skip A" and every "TITLE -> A" this harness has ever printed pressed
|
||
nothing. Note the timing too: the movie ran to **434 s** because it was never
|
||
skipped, which is most of why a boot takes seven minutes here.
|
||
|
||
That second bug looked like it would explain the standing "Ⓐ at the title works
|
||
about half the time" note. **It does not.** See below.
|
||
|
||
## ✅ Resolved: Ⓐ was never the problem — the harness was
|
||
|
||
Instrumenting the *other* side of the keystroke path settled it. The existing
|
||
`[RE-INPUT]` line only fires when `IsUIActive()` swallows a keystroke, so its
|
||
silence was ambiguous between "the game polls and gets nothing" and "the game
|
||
does not poll". Two logs were added on the reaching path (canary
|
||
`f9170fd3f`, corrected in `8d3299975`), and the boot said:
|
||
|
||
```
|
||
[RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0001 (call flags 00000003)
|
||
[RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0002 (call flags 00000003)
|
||
```
|
||
|
||
Exactly two events — a KEYDOWN and a KEYUP for `0x5800` (`VK_PAD_A`) — out of
|
||
~6 500 polls. **The game polls constantly and receives precisely the press that
|
||
was sent.** Input delivery was never broken.
|
||
|
||
With all four harness bugs fixed the boot then went straight through: title at
|
||
289 s, one Ⓐ, **main menu** ([capture](captures/main-menu-oracle.png)).
|
||
|
||
### A slip in the instrumentation, kept because it nearly misled
|
||
|
||
The first version logged on `XSUCCEEDED(result)`. `X_ERROR_EMPTY` is `0x10D2`
|
||
and `XSUCCEEDED` is `((s & 0xC0000000) == 0)`, so an **empty poll passes it** —
|
||
6 499 lines of `vk=0000` buried the two that mattered, and for a few minutes the
|
||
reading was "the driver is flooding the guest with junk keystrokes". Testing
|
||
`result == X_ERROR_SUCCESS` exactly fixes it. (The pre-existing assignment above
|
||
shares the quirk and writes `user_index_ptr` on an empty poll; left alone, since
|
||
the struct is zeroed first.)
|
||
|
||
## ✅ And the third permutation was obtained — from the main menu
|
||
|
||
Not from `GP_DIALOG`: pressing Ⓐ on NEW GAME leads to the standing black-screen
|
||
hang, and no dialog object was ever constructed. But the **main menu itself**
|
||
carries two primitives, which is what the question needed. Recorded in
|
||
[`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md):
|
||
|
||
```
|
||
GP_TITLE.pak ratc-index 8, 16 elements
|
||
paint order: 1 3 4 2 5 8 9 6 7 15 10 11 12 13 14 0
|
||
```
|
||
|
||
`pteff02.prm` (the 25 % dim) paints 4th, beneath the UI; `pteff00.prm` (the
|
||
transition fade) paints **last** — both exactly where they sit on the title
|
||
screen. The composite scores **0.9591 at shift (0,0)** against the live capture.
|
||
|
||
## Three more harness bugs, all of which read as the game misbehaving
|
||
|
||
**`--audio` is not a cvar**, and eight boot scripts passed it. Xenia calls
|
||
`ShowSimpleMessageBox` from `ParseLaunchArguments`, *before* logging is
|
||
initialised, so the symptom is a 10×10 window, an empty log, no guest memory and
|
||
a dialog blocking on `XIfEvent` forever — a hang deep in the emulator, to look
|
||
at. `run-canary`'s own header documents this trap; the scripts predate it.
|
||
|
||
**`vgamepad` no longer exists.** `skip_intro.sh` still called
|
||
`vgamepad tap A 250`, and without `set -e` the call failed silently: the branch
|
||
pressed **nothing** and still `exit 0`. The boot log states it outright —
|
||
|
||
```
|
||
movie (rmse 1662) at 434s -> skip A
|
||
tools/re-capture/skip_intro.sh: line 44: vgamepad: command not found
|
||
```
|
||
|
||
— so every "→ skip A" and "TITLE → A" this harness ever printed pressed nothing,
|
||
and the intro ran to 434 s because it was never skipped.
|
||
|
||
**Do not tap through the movies.** Making that press real for the first time was
|
||
a *regression*: the boot then reached the title in 90 s and that title accepted
|
||
nothing at all. `menu_draw_capture.sh` records the same thing independently —
|
||
"a run that tapped (A) every 4 s through the boot delivered 88 presses and ended
|
||
on a black screen that never came back", and "the title the attract loop returns
|
||
to accepts nothing at all, and 40 taps at 1/s do not change that". `skip_intro.sh`
|
||
now waits the intro out and spends its single press on the title that ends the
|
||
boot.
|
||
|
||
**The title probe sampled a dead pixel.** `skip_intro.sh` tested `p{625,618}` for
|
||
the green Ⓐ glyph — a coordinate from a 1280×720 grab. `screenshot` returns the
|
||
**1279×675 game surface**, where that point is on the copyright line and reads
|
||
`(8,17,31)`. The symptom was a 600 s `TIMEOUT` with the title on screen the whole
|
||
time. Now classified with `screen_id.py`, which called those same frames "title";
|
||
the next boot printed `TITLE at 239s -> A`.
|
||
|
||
**And `boot_menu.sh` sent the emulator's stdout to `/dev/null`** — which is why
|
||
none of this was visible. A boot harness that discards the emulator's own account
|
||
of itself can only ever report symptoms. It now keeps a log and prints its path.
|
||
|
||
## What is not settled
|
||
|
||
* ❔ **`GP_DIALOG` DIFFICULTY is still unreached.** Ⓐ on NEW GAME leads to a
|
||
black screen the run never comes back from — the standing post-load hang, now
|
||
the only thing between here and a fourth permutation.
|
||
* ❔ **The derivation for primitives.** Three measured permutations now, and the
|
||
positions are stable *by role* (backdrop first, dim at slot 4, fade last), but
|
||
nothing in the file predicts them. A primitive has no `T8aD` header, so any
|
||
key must be in the 60-byte declaration entry; four of its words are still
|
||
unread.
|
||
* ❔ `ptframe1`/`ptframe2` rest at alpha 0 and so are not composited, but the
|
||
capture shows the menu frame plainly.
|
||
|
||
|
||
## A fourth permutation: attempted, blocked on an intermittent boot (2026-08-19)
|
||
|
||
The tie-break needs a fourth measured screen with a tied group
|
||
([`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md)), and
|
||
OPTIONS is reachable from the main menu with three d-pad steps — avoiding the
|
||
NEW GAME path that hangs. It was not reached.
|
||
|
||
**Advancing past the title is intermittent: 1 success in 3 attempts today**, with
|
||
the same binary, the same profile and the same procedure.
|
||
|
||
* **Attempt 1** stalled before the title existed at all. The guest was *alive* —
|
||
RSS steady at 598 MB, `[RE-INPUT] … reached the driver (15001 so far)`, i.e.
|
||
the game polling input normally — but the screen was black and
|
||
`screen_children.py` found **only the splash object** resident. So that failure
|
||
is not a presentation problem: the game never constructed the title screen.
|
||
Being able to tell those two apart is new, and it is what the reader is for.
|
||
* **Attempt 2** reached the title at 265 s and then refused four presses — Ⓐ at
|
||
0.25 s, 0.25 s and 0.5 s, and START at 0.3 s. Every one was **delivered**:
|
||
|
||
```
|
||
[RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0001
|
||
[RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5800 flags=0002
|
||
[RE-INPUT] XamInputGetKeystrokeEx -> user=0 vk=5814 flags=0001
|
||
```
|
||
|
||
([log](captures/title-input-delivered-not-acted-on.log)). That is the third
|
||
independent confirmation that input delivery is fine and the game simply does not
|
||
act on the press.
|
||
|
||
Waiting longer does not help: the presses were spread from 10 s to ~4 minutes
|
||
after the title appeared, and the earlier successful run's press landed within
|
||
seconds of it. No predictor has been found — which is the same conclusion the
|
||
corpus reached before, now with the input path excluded rather than suspected.
|
||
|
||
## What is not settled
|
||
|
||
* 🔴 **Why the title accepts Ⓐ only sometimes.** Input delivery is excluded three
|
||
ways over. The next probe has to be on the guest side: the title's handler is
|
||
reachable from the emulator-era notes (`sub_821C5690` and the item vtable
|
||
`0x820b30b4`), so a watchpoint or a log on whatever consumes the keystroke
|
||
would say whether the game reads it and discards it, or never reads it.
|
||
* 🔴 A fourth measured paint order, and with it the tie-break.
|
||
* ❔ Why a boot sometimes stalls at the splash with the guest still running.
|