Two things this iteration, both of which change what is worth doing next. The order is DETERMINISTIC: two independent boots give byte-for-byte identical permutations for both live screens. That kills the hypothesis that the child list is built in I/O-completion order — which mattered, because a run-dependent list would have made deriving a rule pointless. It is a pure function of the bundle. And the 7-element bundle is identified by matching its pivots against every 7-element build on the disc: it is the developer-logo splash (GAME ARTS / SETA / studio anima, GP_TITLE entries 11/14). With names attached, its paint order 0 2 4 6 1 3 5 reads as the full-screen .prm, then all three _eff glows, then all three base logos — glow behind, logo on top. That refines the earlier "grouped by sprite" reading and partly withdraws it: here every element has its own sprite, so the grouping is by ROLE, not sprite identity. The title build could not tell the two apart because its repeated logo instances share both. Still not derived: where the role comes from. No decoded field carries it, and sorting on a "_eff" name suffix would be an odd thing for a loader to do.
225 lines
10 KiB
Markdown
225 lines
10 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.
|