diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index c4c96792..35b69b21 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -51,6 +51,55 @@ screen, and the per-element visibility toggles isolate a suspect element. otherwise — a screen whose background lands on top is showing you this bug, not a decode failure. +### 2026-08-18 — measured against the running game; three orderings refuted, and half the symptom was a different bug + +**The premise is confirmed by the oracle**, which this entry had not had: a +framebuffer capture of Canary on the title screen +([`captures/title-screen-oracle.png`](captures/title-screen-oracle.png)) shows +the `PROJECT SYLPHEED` wordmarks (elements 0–5) drawn **over** `ptbase2.t32` +(element 13), which is a full-screen background. Declaration order is therefore +not the paint order on this screen, and no reading of the element table changes +that. + +**But part of what the render showed was not the paint order at all.** In the +capture `ptbase2` covers the whole screen; the compositor drew it as a +960×540-visible slab starting at (320,180), because a keyframe's `scale` was +being grown from the keyframe's corner instead of about the declared **pivot**. +Fixed, and pinned against the capture by cross-correlation (peak at (0,0)) — see +[`structures/ui-rat-layout.md`](structures/ui-rat-layout.md). That was a real +defect worth separating out: it moves **865** of the disc's 5 130 resting +placements, on every screen, independently of any ordering question. + +**Three candidate orderings are now dead**, all cheaply: + +- **The placement region is not a second ordering.** Its keyframe groups carry an + explicit element index, so they *could* be stored in a different order — they + are not, on **every** build on the disc (`placement_region_order_is_never_a_second_ordering`, + >500 builds, identity every time). +- **The RATC child order is not it either.** For the title build it is the + declaration order with the `.prm` elements absent — strictly less information, + and it has no place to put `ptbase2` other than where the table already puts it. +- **Reverse declaration order is refuted by the same capture**: it would draw + `ptbase2` (13) over `ptcopyright` (28), and the copyright line is visible. + +**The `0x10`-adjacency first step was run, and it does not survive.** The +background *is* adjacent to a `kind = 0x10` `.prm` element on both screens that +have one — but on **opposite sides**. `GP_TITLE` build 7 is +`12 pteff00.prm (0x10)`, `13 ptbase2.t32`, `14 pteff04.t32`; +`GP_MISSION_SELECT` build 0 is `0 px_mission_base.tbm`, `1 px_mission_eff00.prm +(0x10)`. So "the `.prm` opens a layer that draws beneath what precedes it" cannot +place both, and no rule keyed on the `.prm`'s position orders the background. +`GP_READY_ROOM` and `GP_OPTIONS` turned out not to be the third and fourth +witnesses this entry hoped for: neither of their largest builds carries a `.prm` +or a full-screen background at all, so they cannot discriminate. + +**What is still open, stated plainly:** nothing in the bundle has been found that +orders element 13 behind elements 0–5. Every ordering the file itself carries is +now either identical to the declaration table or refuted by the capture. The next +step is no longer static — it is either the guest code that walks this table, or a +per-draw capture of the title screen showing the order the game submits. + + ## Capital ships assemble wrong in the viewer **Reported:** 2026-07-30, by the user. **Status:** ✅ **format-side cause found and diff --git a/docs/re/structures/ui-rat-layout.md b/docs/re/structures/ui-rat-layout.md index 785ee497..8be7e24c 100644 --- a/docs/re/structures/ui-rat-layout.md +++ b/docs/re/structures/ui-rat-layout.md @@ -58,8 +58,9 @@ out-of-range value anywhere in any bundle), and the tutorial bundle, which has n The table lists **both** sprites and `.rat` records — it is the screen's element list. `pgp_ttrl` declares 11: six `eff*`, `msg`, and four `.rat`s (`title`, `btn10..12`). -✅ **Verified:** for all 7 `.t32` entries the declared pivot is *exactly* half the decoded -texture's dimensions — `eff10` 408×120 → 204,60; `eff21` 428×360 → 214,180; `msg` 381×38 → +✅ **Verified — but see the disc-wide correction at the end of this document, which +demotes this to a property of *this bundle*:** for all 7 `.t32` entries the declared +pivot is *exactly* half the decoded texture's dimensions — `eff10` 408×120 → 204,60; `eff21` 428×360 → 214,180; `msg` 381×38 → 190,19; and so on, 7/7 with no mismatch (`tools/re-capture/ratc_decls.py`). `GP_PAUSE_MENU.pak`'s six bundles are `{in-mission, tutorial} × {English, Japanese}`, with @@ -191,7 +192,11 @@ recording them because a static-only reading would have shipped them: screen's draw list lives. **It is the bundle's own declaration table** (above): that table is not the child listing — the children are grouped by type (every `.t32`, then every `.rat`), while the declaration table names *elements*, in a plausible back-to-front - order, and it is exactly the missing set that appears there. For the in-mission pause + order, and it is exactly the missing set that appears there. **"Plausible" is + as far as that goes** — a framebuffer capture of the title screen refutes it on + that screen (its background is element 13 and it is drawn behind elements 0–5), + and no other ordering in the bundle replaces it; see + [`BACKLOG.md`](../BACKLOG.md). For the in-mission pause menu it lists 23: `eff11/12/10`, `eff02(+a)`, `eff03(+a)`, `eff01`, `title.rat`, `btn00/01/04/05.rat`, `msg`, `deli1…4`, `eff30…33`, `loop1.rat`. Note what it does **not** list: the focused button variants, which are reached through each base record's @@ -338,3 +343,82 @@ and that sprite exists **nowhere** — not as a pak member, not as a child of th bundles. It is the second such dangling asset found, after [`SUBTITLE_S12B.tbl`](../movie-subtitle-link.md); a reimplementation should skip a missing sprite rather than treat it as a decode failure. + +## The pivot is the scale centre — measured against the framebuffer (2026-08-18) + +**Status:** ✅ `CONFIRMED` that a keyframe's scale grows the element **about its +declared pivot** rather than about the keyframe's own corner. 🟡 the pivot and +the sprite centre are *not* told apart by this measurement — see the scope below. + +The compositor had been reading a keyframe as `top-left = (X,Y)` and +`size = decoded · scale`, ignoring the pivot entirely. That is right for every +element at 100 %, which is why the pause menu and the ARSENAL rulers above never +caught it, and wrong for every element that is scaled. + +### The measurement + +`GP_TITLE.pak` build 7, element 13: + +``` +ptbase2.t32 decoded 640x360 pivot (320,180) one keyframe: (320,180) 200%,200% +``` + +Composited from the corner that is a 1280×720 rect at 320..1600 × 180..900 — a +quarter-screen slab hanging off the bottom-right, with the top-left quadrant +bare. Anchored at the pivot it is `(320,180) − (320,180)·(2−1) = (0,0)`, i.e. +exactly the screen. + +The oracle is a framebuffer capture of Canary sitting on the title screen +([`captures/title-screen-oracle.png`](../captures/title-screen-oracle.png), the +game surface cropped out of the window at offset (1,45), 1:1, the bottom 45 rows +clipped by the display): + +- the background art reaches all four edges, so the corner reading is refuted + outright; +- normalised cross-correlation of the composite's background against the capture, + searched over ±40 px in both axes, peaks at **(0,0)** — 0.90 on the planet limb, + 0.72 on the lower-left ship. Not "close": the correct shift is the argmax. + +Calibration for that crop is `ptcopyright.t32`, which is unscaled: 694×20 placed +at (293,655), and the capture's glyph run is x 295…986, y 700…718 — inside that +rect to the pixel once the 45 px of window chrome is taken off. So at 100 % the +keyframe really is the top-left, as this document already said. + +Residual, stated rather than glossed: the composite is uniformly brighter than +the capture (about +14 in the dark starfield) and less blue in the planet. That +is **not** placement — it is the two full-screen `PRMD` elements (`pteff00.prm`, +`pteff02.prm` at fade-alpha ≈ 204) and `pteff04.t32` that the compositor does not +draw at all. Their geometry is known (a colour and four corners); their blend is +not. + +### Scope, and what this does *not* settle + +Swept over every screen build on the disc (`tests/ui_paint_order_disc.rs`): + +| | count | of 5 130 resting placements with a decoded sprite | +|---|---|---| +| scaled ≠ 100 % — i.e. moved by this fix | **865** | 17 % | +| of those, where "about the pivot" and "about the sprite centre" differ by ≥2 px | **213** | 4 % | + +`ptbase2`'s pivot **is** half its size, so the capture proves only that scaling is +not about the keyframe corner. Pivot-anchoring is chosen because that is the field +the format carries and this document already called it "a rotation/scale centre"; +the 213 placements that could tell the two apart are **unmeasured**, and a capture +of any one of them mid-animation would settle it. + +### Correction: "pivot = half the texture" does not generalise + +The ✅ above — *"for all 7 `.t32` entries the declared pivot is exactly half the +decoded texture's dimensions, 7/7 with no mismatch"* — is a property of the +tutorial pause bundle, not of the disc. Across all 5 130 placements: + +- **2 521 (49 %)** have `pivot·2` equal to the decoded size within 1 px; +- **1 884 (37 %)** are off by more than 16 px. + +Some of that is already explained here — the `.rat` layer is inherited from the +Japanese master, so its baked pivot belongs to another language's sprite. But it +also happens on `.t32` declarations (`pgpeff02.t32` in the in-mission pause build: +decoded 265×198, declared pivot 19,18), so the rule is not merely a +language-inheritance artefact. **Do not derive a texture size from a pivot**; the +capture backs the decoded size, not the pivot — `ptcopyright` is 694 px wide in +the framebuffer, and `pivot·2` says 618.