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.
12 KiB
The UI screen object at runtime — found in live guest memory
Status: ✅ CONFIRMED for the object's identity and its element array (five
independent field matches against the file, on a running game). 🟡 the remaining
arrays are unidentified. ❔ nothing here settles the paint order — see the
"what this does not answer" section, which is the reason the search happened.
How it was found, in one step
The item class from the emulator-era splash work has vtable 0x820b30b4, and
sub_823C2990 allocates its objects at 244 bytes each
(ui-quad-class-foothold.md). A vtable pointer
is a fixed 4-byte value at offset 0 of every instance, so the objects are
findable in the live guest image without a debugger:
tools/re-capture/gmem.py find hex:820b30b4 # while the title screen is up
Seven live objects, at 0xBCD24D88, 0xBCD25188, 0xBCD25388, 0xBCD25488,
0xBCD25588, 0xBCD25688, 0xBCD26488 — spaced 0x100 apart, consistent with a
244-byte object plus allocator overhead.
What they are
Each object begins with two vtable pointers (0x820b30b4, 0x820b31a4 —
multiple inheritance) and then a run of {pointer, count, capacity} triplets.
The counts identify the objects immediately:
| object | first triplet's count | which GP_TITLE build |
|---|---|---|
0xBCD25188 |
7 | builds 0/1 — the loading overlay (7 elements) |
0xBCD25388 |
24 | build 4 — the title (24 elements) |
0xBCD24D88 |
1 | builds 2/3 — the PRESS Ⓐ BUTTON bundle (1 element) |
So the three bundles this project decoded statically are all resident, with the element counts the files declare — including the two-bundle composition of the title screen that the draw capture inferred.
The element array
+0x08 of the screen object is {ptr, count, capacity} pointing at an array of
48-byte entries, one per element, in declaration order. Verified field-by-file
at five positions on the title build, with no misses:
| index | file says (build 4) | live memory at +0x10/+0x14 |
|---|---|---|
| 0 | ptlogo1.t32 pivot (451,50) |
451.0, 50.0 |
| 1 | ptlogo2.t32 pivot (449,46) |
449.0, 46.0 |
| 6 | pteff01.t32 pivot (320,160) |
320.0, 160.0 |
| 9 | ptbase2.t32 pivot (320,180) |
320.0, 180.0 |
| 21 | ptcopyright.t32 pivot (309,10) |
309.0, 10.0 |
Entry layout as far as it is read:
+0x00 u32 pointer (per-element data)
+0x04 u32 0
+0x08 u32 flags (0x10081021 on element 0)
+0x0C u32 ?
+0x10 f32 pivot X ← the declaration entry's pivot, as a FLOAT
+0x14 f32 pivot Y
+0x18 u32 0
+0x1C u32 ?
+0x20 u32 pointer }
+0x24 u32 count } the element's KEYFRAMES — 8/8 on elements 0 and 1,
+0x28 u32 capacity } which is exactly their keyframe count in the file
+0x2C u32 ?
So the pivot and the keyframe group, which this project reads out of the bundle, are present at runtime in the same order and with the same values.
What this does not answer
The paint order. The array is in declaration order, and the capture proves
the screen is not painted in that order, so the renderer either walks something
else or sorts. Three more {ptr,count,capacity} triplets are present in the
screen object and unidentified — +0x14 (24 entries of 12 bytes, each
{ptr,1,1}, pointing into a densely packed region), +0x24 (2 entries) and
+0x30 (24). The +0x14 one is not an index list (its entries are pointers,
not small integers), so the obvious "draw order table" reading is already out.
What is better than before: the question is now a data question on a live
structure that can be dumped in seconds, rather than a code-reading exercise. The
next probe is to dump +0x24 and +0x30 on the title build and see whether
either is 24 entries in a non-declaration order.
✅ The paint order is the screen object's CHILD array at +0x30
Status: CONFIRMED. The question this project has carried since the first
capture — what orders the elements, given that the declaration table does not —
is answered: the screen object holds a second list, a reordering of the
elements built at load time, and that list is the paint order.
+0x30 is {ptr, count, capacity} → an array of 24 pointers (not 12-byte
records like +0x14). Each pointer is the +0x00 field of one of the 48-byte
element records, so the two arrays hold the same objects in different orders. The
permutation, read live off the title screen:
| child slot | element | child slot | element | |
|---|---|---|---|---|
| 0 | 9 ptbase2 |
12 | 17 back2eff4 |
|
| 1 | 11 ptloop01 |
13 | 0 ptlogo1 |
|
| 2 | 12 ptloop02 |
14 | 2 ptlogo1 (copy) |
|
| 3 | 10 pteff04 |
15 | 4 ptlogo1 (copy) |
|
| 4 | 13 pteff02.prm |
16 | 7 ptlogo_tm |
|
| 5 | 6 pteff01 |
17 | 1 ptlogo2 |
|
| 6 | 20 ptlogo_back2eff |
18 | 3 ptlogo2 (copy) |
|
| 7 | 19 ptlogo_back2 |
19 | 5 ptlogo2 (copy) |
|
| 8 | 14 back2eff1 |
20 | 22 ptlogoall_eff |
|
| 9 | 15 back2eff2 |
21 | 23 ptlogoall_eff2 |
|
| 10 | 18 back2eff5 |
22 | 21 ptcopyright |
|
| 11 | 16 back2eff3 |
23 | 8 pteff00.prm |
Checked against the capture, not merely plausible
The seven elements the draw capture can name occupy child slots
ptbase2 0 back2eff 6 back2 7 ptlogo1 13 tm 16 ptlogo2 17 copyright 22
— strictly ascending, in exactly the captured submission order. Two details make this more than a coincidence of a short list:
- it resolves the ambiguity that no static field could: slot 6 is element
20 (
ptlogo_back2eff), slot 7 is element 19, so the pair paints 20-then-19 — descending in declaration terms. Nothing in the file predicts that, and the runtime list states it; - the three
kind = 0x4repeat instances of each logo sit immediately after their template (slots 13,14,15 and 17,18,19), which the declaration table interleaves (0,1,2,3,4,5). They draw at α=0 and so never appeared in the capture, but their placement in the list is consistent with the grouping.
What this means for the port, stated carefully
The reimplementation cannot read a runtime array — it has to derive this order
from the bundle. That derivation is not solved. What the order shows is
structure worth attacking: elements sharing a sprite are adjacent
(ptlogo1×3 together, ptlogo2×3 together, the back2eff* family together),
and the full-screen/effect elements lead. So the load-time build is doing some
grouping, not an arbitrary shuffle.
Until it is derived, a reimplementation has two honest options: hard-code the captured order for the screens that have been captured, or paint in declaration order and accept that the title screen composites wrongly. The first is what the evidence supports; the second is what the viewer does today.
Two ground-truth permutations, and what they rule out
Deriving the child order from the bundle is what the port needs, so the first step is data. Both live screen objects with more than one element, read off the title screen:
The developer-logo splash — GP_TITLE.pak entries 11/14, identified by
matching its pivots against every 7-element build on the disc. It is the
GAME ARTS / SETA / studio anima screen, i.e. the one the emulator-era Milestone 1
worked on:
0 palogo_eff0.prm (640,360) 1 palogo_gamearts (250,36) 2 palogo_gamearts_eff (260,46)
3 palogo_seta (120,44) 4 palogo_seta_eff (130,55)
5 palogo_anima (194,68) 6 palogo_anima_eff (204,78)
paint order (child slots): 0 2 4 6 1 3 5
— which, with the names, reads as the full-screen .prm, then all three
_eff glows, then all three base logos. The glow behind, the logo on top:
a semantic grouping, and one that the declaration table interleaves
(base, glow, base, glow, …).
This refines the "grouped by sprite" reading. Here every element has its own
sprite, so the grouping cannot be by sprite identity — it is by role. In the
title build the two readings coincide (the three ptlogo1 instances share both a
sprite and a role), so that build alone could not tell them apart.
The 24-element title build:
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
What the pair shows
- The list is grouped, not shuffled. Elements sharing a sprite are
contiguous:
ptlogo1's three instances (0,2,4) at slots 13–15,ptlogo2's (1,3,5) at 17–19, theback2eff*family at 8–12. The 7-element bundle's even/odd split is the same phenomenon in miniature if its evens and odds are two sprites. - The five single-keyframe elements lead. In the title build, exactly elements 9, 10, 11, 12, 13 have one keyframe, and the child list opens with 9, 11, 12, 10, 13 — all five, before anything animated. That is unlikely to be chance (5 of 24 in the first 5 slots), and it is a file-visible property.
Refuted as the ordering key, on this data
- first-keyframe time —
back2eff1starts at t=52 and paints afterback2(t=66); - resting-keyframe time — same pair, 56 against 80;
- declaration order and its reverse — neither, obviously;
- the RATC child (sprite) order — the group order does not ascend in it
(
ptbase2is RATC child 4 and paints first;ptlogo1is child 0 and paints fourteenth).
The order is deterministic
Two independent boots, the same screens: byte-for-byte identical permutations (and identical object addresses). So the child list is not built in I/O-completion order or anything else run-dependent — it is a pure function of the bundle, and therefore derivable in principle. That was worth testing before hunting for a rule, because a load-order-dependent list would have made the hunt pointless.
Not settled: the rule itself. What is known is that the list groups elements
by role — glows before bases, instances beside their template — which the
declaration table interleaves. What is not known is where that role comes from:
no decoded field carries it, and the names (_eff) are suggestive but a loader
sorting on a name suffix would be unusual. The next step is the loader — the code
that appends to +0x30 — which is now worth reading precisely because the
order is deterministic.
The runtime record carries no ordering field (measured)
The obvious next suspect was a role/depth field in the 48-byte element record, so every undecoded word was dumped for all 24 title elements next to its paint slot. The result is a clean negative and one confirmation:
+0x04is the declaration entry'skind, verified against the file for all 24:0x10on the two.prmelements (8 and 13),0x4on the four repeat instances (2,3,4,5),0x3000on the twoptlogoall_eff*(22,23), zero elsewhere. So the record mirrors the file here, as the pivot and keyframe count already did.- Every other undecoded word is zero —
+0x08,+0x0C,+0x18,+0x1C,+0x2Care00000000on 21 of the 24 elements. The three exceptions (elements 0, 1, 2) hold values that change nothing about ordering and look like live animation state rather than static fields.
So nothing in the element record orders the elements, and kind does not
either (the paint order interleaves kinds freely). Combined with the earlier
result that no field of the 60-byte declaration entry does, and that the region
after the placement groups is the RATC child stream, the ordering does not live
in the data this project has decoded.
What remains is the loader: the code that appends to +0x30. It is worth
reading now — the order is deterministic, so that code is a pure function of the
bundle — but it is a code-reading task, and this note is careful not to promise
that a static rule exists just because one could.