re(ui): a RATC child's name is stated, not inferred -- and it was hiding every menu background

`ratc::parse` named each child by scanning backwards for the last printable run
of bytes before its magic. The format states the name explicitly instead, in an
`opt ` block: `"opt " | BE32 len | name | NUL | 3 bytes | magic` -- the same
block `ui_layout::opt_link` already read for a button's focus link.

The scan agrees with it 17 918 times out of 17 942 and is wrong 24 times, every
one the same failure: the 3 trailing payload bytes are themselves printable and
beat the real name. For `pteff05.t32` those bytes are `38 41 58` = `8AX`, so the
full-resolution background of all five menu screens registered under a name no
element declares, resolved to no sprite, and `compose` dropped it through an
early `continue` that -- unlike the two arms above it -- records nothing. The
screen lost its background and `screen render` still reported "all resolved".

`8AX` was never a name. Docs that treated it as one are corrected here.

Disc-wide, and the control is the 17 918 the scan already got right: the `opt `
reading reproduces every one of them. Effect on the five screens is the
signature of the same art at twice the resolution -- mean brightness unmoved,
high-frequency detail x1.15..x1.30 -- which is what the separately-measured
`ui-8ax-fullres-background` result said the game draws.

Also closes a long-standing dangling reference: `pmbase.t32`, recorded as "on
the disc nowhere", is the `GP_STAGE_CLEAR` child the scan called `8AX`. RATC
sibling references now resolve 10 148 of 10 148.

Verified: 114/114 sylpheed-formats unit tests (including two new ones pinning
the `8AX` case byte for byte and the no-block fallback), and every disc-gated
integration suite in sylpheed-formats/sylpheed-cli.
This commit is contained in:
Sylpheed RE agent
2026-08-29 07:26:56 +00:00
parent 49f109deb1
commit 56cc7acfc3
14 changed files with 583 additions and 9 deletions

View File

@@ -0,0 +1,137 @@
# A RATC child's name is stated by an `opt ` block, not by the bytes before it
**Status:**`DECODED`, with a disc-wide check. This fixes a decoder defect that
silently dropped the **full-resolution background from all five menu screens**.
## The field
Immediately before nearly every child of a `RATC` bundle — 17 942 of the disc's
18 002 — sits an `opt ` block:
```text
"opt " | BE32 length | name | NUL | 3 bytes | <child magic>
```
Two children of the main menu bundle (`GP_TITLE.pak` entry 8), raw:
```text
opt 00 00 00 0a p t b a s e . t 3 2 00 0e 10 a4 T8aD
opt 00 00 00 0b p t e f f 0 5 . t 3 2 00 38 41 58 T8aD
^^^^^^^^
"8AX"
```
This is the **same `opt ` block** `ui_layout::opt_link` already decodes for a
button's focus link. Nothing new had to be discovered to read it — only noticed.
## The defect it fixes
`ratc::parse` named each child with `name_before`: the last printable run in the
96 bytes before the child's magic. That is a heuristic, and it is *usually* right
`ptbase.t32`'s three trailing bytes are `0e 10 a4`, not printable, so the scan
walks back to the real name.
But `pteff05.t32`'s trailing three bytes are `38 41 58`, which is `"8AX"` in
ASCII. The scan takes those, the child is registered under the name `8AX`, the
element that declares `pteff05.t32` matches nothing in the sprite table, and
`compose` drops it through
```rust
let Some(sprite) = el.sprite.as_ref() else { continue };
```
*before* the arm that records a `missing` sprite. So the screen's background
vanished with **no diagnostic at all**: `screen render` reported "all resolved".
⚠️ `8AX` was carried in `docs/` as though it were a name the game uses — the old
text read "the `T8aD` behind its `opt ` link is registered under the name `8AX`".
It is not a name. It is three bytes of the preceding record's payload.
## The disc-wide check
[`examples/ratc_child_names.rs`](../../../crates/sylpheed-formats/examples/ratc_child_names.rs)
compares the two readings for every RATC child in all 33 `dat/*.pak`
([data](../data/ratc-child-name-audit.txt)):
| | |
|---|---|
| RATC children scanned | **18 002** |
| carrying an `opt ` block | 17 942 |
| scanned name **agrees** with it | **17 918** |
| scanned name **differs** | **24** |
Every one of the 24 is the same failure: a 3-byte printable tail beating a real
name.
| scanned | actual | count | where |
|---|---|---|---|
| `8AX` | `pbbg.t32` | 12 | `GP_READY_ROOM` |
| `8AX` | `pteff05.t32` | 4 | `GP_TITLE` 5, 6, 8, 9 — the menus |
| `8AX` | `pmbase.t32` | 4 | `GP_STAGE_CLEAR` |
| `8AX` | `pteff04.t32` | 2 | `GP_TITLE` 4, 7 — the title |
| `'OX` | `po_keys_win1.t32` | 2 | `GP_OPTIONS` |
⚠️ **What I checked about the 24, precisely.** That the recovered name is the one
the bundle actually wants is verified for the ten `GP_TITLE` and `GP_STAGE_CLEAR`
cases: on the title screens `pteff04.t32`/`pteff05.t32` are *declared elements*
that previously resolved to nothing and now resolve, and `pmbase.t32` is the
target of `GP_STAGE_CLEAR`'s long-standing dangling reference (below). For the 12
`GP_READY_ROOM` (`pbbg.t32`) and 2 `GP_OPTIONS` (`po_keys_win1.t32`) cases I
checked only that no element in those bundles is left unresolved afterwards —
which is consistent with, not proof of, the same story. None of the 14 is on the
five menu screens.
**The control is the 17 918 the heuristic already got right**: the `opt ` reading
reproduces every one of them. A reading that fixed the 24 but disturbed the rest
would be a different rule, not this one.
🟡 **Reach.** 60 children (0.3 %) have **no** `opt ` block within 128 bytes and
still fall back to the scan. None of them is on the five menu screens. Whether
they genuinely lack the block or sit past the search window is not established.
## What it changes in the composite
Resolving the name makes the element resolve, so `compose` now draws it. On all
five port screens ([before/after](../data/ratc-name-fix-render-effect.txt)):
| build | screen | mean brightness | high-frequency detail |
|---|---|---|---|
| 4 | title | 72.77 → 72.81 | **×1.15** |
| 5 | main menu (JP) | 42.73 → 42.74 | **×1.27** |
| 6 | `EXTRAS` (JP) | 44.08 → 44.09 | **×1.27** |
| 8 | main menu | 41.57 → 41.58 | **×1.30** |
| 9 | `EXTRAS` | 42.99 → 43.00 | **×1.29** |
The brightness is unmoved and the detail is up by a quarter — which is exactly
the signature of *the same artwork at twice the resolution* replacing a 2×
upscale, and not of new content appearing. That it *should* be the full-res art
was settled separately and against the running game, in
[`ui-8ax-fullres-background.md`](ui-8ax-fullres-background.md); this page only
supplies the name that lets the renderer find it.
**Nothing is covered.** The background is opaque and full-screen, and on the
menu it paints 4th of 16, so the worry is real. Measured at each element's
resting rect, before vs after: `ptbtn01` sd 59.98 → 60.03, `ptbtn03` 63.40 →
63.45, `ptbtn05` 66.54 → 66.58, `ptframe1` 51.16 → 51.20, `ptmsg` 56.11 → 56.12.
Everything survives; only the two `loop*` elements paint beneath it, and those
are excluded from the default composite anyway.
⚠️ **Both backgrounds are now drawn**`ptbase.t32` upscaled 2×, then the
full-res one opaquely over it. Correct output, wasted fill. The port should draw
only the full-res one, and take its *timing* from `ptbase`'s element, which is
the one carrying the keyframes.
## What is NOT decoded
**No declaration word points at the child.** Before reading the bytes I tested
whether the 60-byte element declaration indexes the T8aD child table. Its unread
words are `+28`, `+36`, `+44` and `+56`; the control is the resolved elements,
whose child index is known. On the main menu, of 13 controls the words reproduce
the child index **1, 0, 0 and 1** times — and both 1s are the trivial index-0
case. There is no pointer; the association is by name, and the name is the `opt `
string. [`decl_word_probe.rs`](../../../crates/sylpheed-formats/examples/decl_word_probe.rs)
✅ Incidental, from the same probe: **`+44` is a button ordinal.** It is `1…5` on
exactly the five `ptbtn0N.rat` elements of the main menu, in order, and `1` on
every other element. Not needed for anything open, and recorded rather than
chased.

View File

@@ -1,8 +1,26 @@
# 🟡 `pteff04` / `pteff05` are dropped — the texture is registered as `8AX`
# `pteff04` / `pteff05` — the full-resolution background, once dropped as `8AX`
**Status:** 🟡 a real name-resolution gap, **currently harmless to look at**, and
the obvious fix would make the render *worse*. Found by auditing what
`screen render` silently omits on the port's five screens.
**Status:** **RESOLVED 2026-08-29.** This page's two questions are both closed
and it is kept for the evidence, not as an open item.
* *Which of the two backgrounds does the game draw?* — the **full-resolution**
one. Measured against a capture; the section below stands unchanged.
* *Why did ours drop it?* — because `8AX` **is not a name.** It is three bytes of
the preceding record's payload (`38 41 58`) that happen to be printable ASCII,
and our backwards printable-run scan preferred them to the name the format
states in its `opt ` block. Decoded, with a disc-wide check, in
[`ratc-child-names.md`](ratc-child-names.md); `ratc::parse` now reads the
stated name and `screen render` draws the background on all five screens.
⚠️ **Read the rest of this page with that correction in mind.** It was written
while `8AX` was believed to be a name the game uses, and says so in several
places — "the texture is registered as `8AX`", "the `T8aD` behind its `opt ` link
is registered under the name `8AX`". The `opt ` link was never the problem; the
`opt ` block was the answer, sitting unread three bytes away.
<sub>Original status line: 🟡 a real name-resolution gap, currently harmless to
look at, and the obvious fix would make the render *worse*. Found by auditing
what `screen render` silently omits on the port's five screens.</sub>
## What `screen render` drops, and why

View File

@@ -330,8 +330,14 @@ parser limitation. It is not — **there is nothing deeper on the disc**:
- The children that are themselves RATC (the `.rat` layout records) are **leaf
records**: they carry no child list and instead **reference their siblings by
name** — the sprite they place and, via `opt `, their focused variant.
**3 311** such leaves, every one embedding sibling names, and **10 144 of
**3 311** such leaves, every one embedding sibling names, and **10 148 of
10 148** references resolve to a sibling of the same bundle.
⚠️ It read **10 144 of 10 148** until 2026-08-29. The 4 misses were
`pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`, written up as an asset
that is "on the disc nowhere". It was on the disc: it is the child our name
scan called `8AX`. See [ratc-child-names](ratc-child-names.md) — the same
defect that hid the menu backgrounds. Nothing about the reference was wrong;
the thing it pointed at had the wrong name in our index.
That is the same by-name convention used one level up, where a screen's config
names `.prt` components, and one level up again, where the movie table names