Files
Sylpheed/docs/re/ui-paint-order-third-permutation.md
Sylpheed RE agent 03aeb1be7e docs: input was never the blocker; the third permutation came from the main menu
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.
2026-08-19 08:57:01 +00:00

8.6 KiB
Raw Blame History

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). 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 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).

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:

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.