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.
253 lines
12 KiB
Markdown
253 lines
12 KiB
Markdown
# 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`](../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:
|
||
|
||
```bash
|
||
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 = 0x4` repeat 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, the `back2eff*` 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** — `back2eff1` starts at t=52 and paints *after*
|
||
`back2` (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
|
||
(`ptbase2` is RATC child 4 and paints first; `ptlogo1` is 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:
|
||
|
||
* **`+0x04` is the declaration entry's `kind`**, verified against the file for
|
||
all 24: `0x10` on the two `.prm` elements (8 and 13), `0x4` on the four repeat
|
||
instances (2,3,4,5), `0x3000` on the two `ptlogoall_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`,
|
||
`+0x2C` are `00000000` on 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*.
|