The game receives the (A) press - measured, vk=5800 flags=0001, out of ~6500 polls. Input delivery was never broken; four harness bugs were, and with those fixed the boot goes title -> one (A) -> main menu. GP_DIALOG DIFFICULTY is still unreached (NEW GAME hits the standing black-screen hang), but the main menu carries two primitives, which is what the question needed. Records the slip in the instrumentation too: logging on XSUCCEEDED counts X_ERROR_EMPTY as success and buried the two real keystrokes under 6499 empty polls. Next step is now STATIC: a primitive's position is stable by role across three measured screens but nothing in the file predicts it, so dump the full 60-byte declaration entries - four words unread - and look for a word that sorts like the measured orders.
174 lines
8.6 KiB
Markdown
174 lines
8.6 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.
|