diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 9007652d..9c7354fb 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -2028,8 +2028,9 @@ decoded rule about vertex colour. ## 2026-08-29 β€” `ptlogo_back2eff3` is declared on the disc and never drawn by the game -βœ… **measured**, and this one is actionable: **you draw `eff3` at t=60–62 and the -console does not.** +πŸ”΄ ~~**measured**, and this one is actionable: **you draw `eff3` at t=60–62 and the +console does not.**~~ **RETRACTED β€” see the correction at the end of this file. +The console DOES draw it. You were right to keep drawing all five.** Last time I called `eff3`'s absence unexplained and withdrew a bad explanation for it. Re-examined against the **second** title build-in in the same capture β€” the @@ -2142,3 +2143,63 @@ differing pixels, not the difference. Detail: [`docs/re/structures/boot-splash-gap-measured.md`](../re/structures/boot-splash-gap-measured.md). +## 2026-08-29 β€” RETRACTION: the console draws all five flashes, including `eff3` + +πŸ”΄πŸ”΄ **My "the game never draws `ptlogo_back2eff3`" was wrong.** You declined to act +on it β€” *"I will not stop drawing an element on a claim whose own identification +excludes that element from its bounding box"* β€” and that judgement was correct +twice over: the identification was broken, and so was the finding built on the +same instrument. + +Parsed properly, **all five flashes fire in both title entries in the declared +stagger**: + +| element | entry 1 | entry 2 | declared | +|---|---|---|---| +| `eff1` | 130–131 | 5953–5955 | flash t54–58 | +| `eff2` | 133 | 5955–5957 | flash t58–62 | +| **`eff3`** | **133–134** | **5957–5958** | **flash t60–64** | +| `eff4` | 133–135 | 5957–5959 | flash t62–66 | +| `eff`/`eff5` | 134 β†’ | 5958 β†’ | holds | +| `ptlogo_back2` | 136 β†’ | 5962 β†’ | holds | + +Frames 133/134 are t = 60.1 and 62.3 β€” inside `eff3`'s declared window. **The disc +was right about every element; my reading of the oracle was wrong.** + +### The mechanism, because it invalidated your developer-splash correction's twin + +A draw **batches several quads** β€” `indices=4` is one, `indices=8` two, +`indices=24` six β€” and the log dumps only the first **8 vertices**. Min/max over a +line's vertex list therefore *merges* quads. + +`eff3` is batched with `eff4`, and because the wipe family is right-aligned, `eff3` +(788…1196) lies **entirely inside** `eff4` (447…1196). The union is **exactly +`eff4`'s extent** β€” the merged box matched `eff4` to 1 px and `eff3` simply +disappeared, with nothing anomalous to notice. + +πŸ”΄ **And your developer-splash refutation was the same bug**, which you found by +arithmetic before I found it by measurement. `525Γ—259` was `gamearts_eff` merged +with `seta_eff`. The splash draws three logos and three glows as separate quads. +⚠️ The **9-unit black hold is unaffected** β€” those glows are the developer +splash's first draw, so frame 130 is still its first drawn frame. + +### The part worth keeping + +I reported three alternative explanations "ruled out". All three were aimed at the +wrong failure β€” in particular, my "a draw the log cannot see" check counted draws +with **no** geometry line, when the hiding place was draws with **partial** +geometry. **Refuting three wrong hypotheses is not evidence for a fourth**, and a +list of failure modes written by whoever built the instrument is the least likely +to contain that instrument's blind spot. + +`tools/re-capture/quads_per_frame.py` now parses vertices in groups of four and +warns whenever the logged quad count falls short of `indices / 4`. + +βœ… Your independent derivation of **239.816** from the exported keyframes, against +my 239.8 from the draw stream, is the check that conversion needed β€” it is the +denominator of the 9-unit hold you now ship. + +❔ **Still not settled: the main-menu sweeps.** Two capture attempts failed β€” one +crashed the guest (a double β’Ά tap, now guarded), one drifted to a flight screen. +The title answer stands; the menu is unmeasured. + diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 41e8962c..a66968c0 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -171,5 +171,5 @@ files, which is how the same ground got covered twice. | [`structures/ui-tie-break-cost-at-settle.md`](structures/ui-tie-break-cost-at-settle.md) | What the unknown paint-order tie-break costs, in pixels | βœ… **decoded**, closing the open half of Q3: at the settled instant the tie-break costs **at most 1 px at Ξ”1**, on the **Japanese title only** (`ptlogo2`Γ—`ptlogo_tm`, 5 px shared ink); **exactly 0 px on all five port screens**. The earlier 24-pair bound was a `rest()` count β€” and 10 of the title's 11 tied pairs are between `ptlogo_back2eff1`…`eff5`, five transient flashes that are **transparent** on the settled screen ([`ui-settle-time.md`](structures/ui-settle-time.md)). Live pairs at settle: entry 4 β†’ **1**, entry 7 β†’ **2**, the four loading bundles β†’ **0**. βœ… Not a knife-edge β€” sweeping every keyframe time and midpoint, the count is **flat across the whole settle window**, and the loading bundles' tie is live only at t17–t33. βœ… Controls: an overlapping *different*-key swap moves 25 310 / 268 698 / ~765 000 px on the entries reporting zero; zeros are explained by shared-ink counts (the `ptframe` pairs share **0 px** of ink). ⚠️ Entries 0/1/12/15 have **no live control** β€” their zeros rest on keyframe data, not a render. 🟑 Refutation attempt on the corpus's "24 pairs": **survives** as a rest-pose bound, 16/16 on entry 7. ❔ *Why* ties order as they do is still unknown β€” and now worth one pixel | | [`structures/ui-record-loop-length.md`](structures/ui-record-loop-length.md) | Where a looping record's cycle restarts β€” and the `PRESS β’Ά` plate's real period | βœ… **decoded**: a nested record is itself a RATC bundle and its header **`+0x08` is the loop length**; its keyframes need not fill it, and the slack is a hold at the final pose. Disc-wide over **1 781** timed nested records: 92.3 % declare exactly their last keyframe time, **7.7 % declare more**, and **0 declare less** β€” the falsifier (a cycle cannot restart before its own last pose) never fires. πŸ”΄ **The plate's `ptbtn00f` is 105 units of ramp inside a 120-unit cycle, so it holds dark for 15 units** β€” the port was shipping **105**, and the answer is **120**. βœ… Falsification test against the running game, using a pacing factor measured *independently* on the focus ring (declared 120 β†’ **2.177 s**, factor **1.0885**): to reach the corpus's measured 2.12–2.34 s, 105 units needs a factor of **1.211–1.337** (πŸ”΄ excludes the ring's) while 120 needs **1.060–1.170** (βœ… contains it). Different elements, different bundles, separate runs β€” tied only by both declaring 120. ⚠️ Says where a cycle *ends*, not which records cycle. ❔ the **top-level** `+0x08` (300 on every `GP_TITLE` entry, elements ending at 244–269) is a different question, untouched | | [`structures/ui-focus-record-pulse-census.md`](structures/ui-focus-record-pulse-census.md) | Every focus record whose glow pulses, and where `rest()` puts it | βœ… **decoded**, disc-wide: **1 130** focus records, **2 664** timed elements, **210 with a varying alpha** β€” of which **202** have `rest()` == the **peak** (burns bright forever) and **8** land **mid-ramp**. By pak: `PILOTLOG` 116, `MOVIE_THEATER` 54, `HANGAR_ARSENAL` 30, `LEADERBOARD` 8, **`GP_TITLE` 2**. 🟑 Bounds rather than refutes the port's "34 in the export, 2 varying, nothing to fix" β€” correct, and correct *because* `GP_TITLE` has 2; the pathology sits in the screens a wider port needs next. πŸ”΄ The 8 mid-ramp ones are the worse mode: `py_ranking_btn01f` swings 255β†’127β†’255 and `rest()` returns **244**, neither extreme, which looks entirely plausible and nothing reports it. βœ… Control: `ptbtn01f` is genuinely constant (255 throughout) and is **not** flagged; two hits verified keyframe by keyframe. ⚠️ A pulsing element has no resting pose β€” the question is malformed, not mis-answered; `pose_at(t)` inside the record's declared cycle ([`ui-record-loop-length.md`](structures/ui-record-loop-length.md)) is the only well-formed query. ⚠️ 210 is a **floor**: focus records are matched by the `Xf.rat` name rule, and varying scale/rotation/position is not counted | -| [`structures/ui-title-buildin-measured.md`](structures/ui-title-buildin-measured.md) | The title's build-in and the plate glow, read out of the guest's own draw stream | βœ… **measured** (Canary, `ARM=early` draw capture): the decoded *mechanism* is observed, not just its end state. **The five flashes fire in a six-frame window and are absent from all 155 other sampled frames**; `ptlogo_back2eff1` is drawn in exactly 2 frames at **t = 54.0** against a decoded peak of **t54–56**, and `ptlogo1` first appears at **t = 42.2** against a decoded **t42** β€” with units/frame taken from the **glow's period alone**, a different element. The two holders (`ptlogo_back2eff`, `ptlogo_back2`) are continuous from frame 134. βœ… The glow's per-vertex colour alpha IS its fade alpha: **observed range 0…80 against a decoded peak of 80**, exact and unfitted; **period 51.158 presented frames** over 20 cycle starts; fitting the decoded ramp gives RMS **13.16** against **38.18 reversed** (2.9Γ—), so the asymmetry is real and correctly directed. Structure: the settled title is 10–11 draws naming no sprite β€” which is why arming at the title sees nothing. ⚠️ Frame **107** is a 27-draw spike between the movie's last frame and the title's first; calling it "the composite" was an **over-read** β€” it binds **no texture** and only 4 of its 27 draws log geometry. The second title entry has no such frame. ⚠️ The two entries are the same animation at **different sampling phases** (only 4 of 46 aligned frames match), which is what makes the `eff3` result robust. πŸ”΄ **The game does not draw `ptlogo_back2eff3` at all** β€” reproduced over **two** title entries 5 800 frames apart, and three alternative explanations tested and failed: *phase* (its window is **6 units** against a **2.23-unit** step, so it cannot be missed β€” frames 133/134 sit at t=60.1/62.3 inside it and draw `eff2` and `eff4` instead), *an unlogged draw* (exactly 2 blind draws/frame, always the same full-screen-triangle shader, present when no wipe is active), and *a bad position guess* (dropping position entirely, **zero** quads anywhere have a width within Β±30 of 408; the spectrum jumps 262 β†’ 748). Draw counts across both entries: eff1 **4**, eff2 **3**, eff3 **0**, eff4 **6**. πŸ”΄ **The port draws `eff3` at t=60–62 and the console does not.** ❔ Why is not established β€” nothing in its element record differs from its neighbours. ⚠️ An earlier "sub-frame phase" explanation and the advice that drawing all five "shows more sweep than the console" are both **withdrawn**. ⚠️ What a frame-by-frame build-in comparison *will* show is disagreement about which flash lands in which frame β€” 2 units/submitted frame against this run's 2.231 units/presented frame β€” and neither side is wrong. πŸ”΄ **Trap:** matching a bound texture's dimensions to a sprite fails both ways β€” it missed every flash *and* read the intro movie's 640Γ—360 YUV planes as `ptbase2`. βœ… A regression of five events' observed frames against their declared times (residuals ≀0.9 frames) recovers the intercept at frame **106.1** when the composite spike, not in the fit, is frame **107**. ⚠️ Per-vertex alpha = fade alpha holds for the **glow** and does not generalise β€” `eff4` reads 255/127/254 on consecutive frames. ❔ Frame rate not recorded, so nothing is in seconds; the glow's period implies a **114**-unit cycle against a declared 120, unexplained; `eff5` vs `ptlogo_back2eff` not separated | -| [`structures/boot-splash-gap-measured.md`](structures/boot-splash-gap-measured.md) | The black gap between the two boot splashes | βœ… **measured** in the guest's **draw stream**, which separates true black from a fade tail where luminance cannot: the publisher's last sprite is frame 125 (alpha 7), then **frames 126–129 submit NO sprite quad at all**, then the developer fades in at alpha 34. **The gap is 4 presented frames.** Converted with the disc as its own clock β€” `palogo_sqex` declares alphaβ‰₯1 for **239.8 units** and is drawn in **105** frames β†’ **2.284 units/frame** (the title capture independently gave 2.231) β€” that is **~9.1 units β‰ˆ 0.152 s**, against the **12** the port authored; ⚠️ and the true black is *shorter*, since both boundary frames still carry picture. βœ… Second finding: the **developer splash is ONE composited 525Γ—259 quad** at the bounding box of its three declared logos, none of whose individual sizes is ever submitted. ❌ Not declared on the disc: `palogo_eff0.prm` is a single static keyframe, and the top-level `+0x08` is a **family constant** (300 / 60) whose slack ranges 12–226 units. ❔ The executable is **not** looked at β€” named, not claimed. πŸ”΄ The instrument was perturbing the measurement: the capture script taps β’Ά on "screen changed a lot", which is also true of a fading splash β€” it tapped through the publisher and the developer never appeared. `GRACE=1` and `NOTAP=1` knobs added | +| [`structures/ui-title-buildin-measured.md`](structures/ui-title-buildin-measured.md) | The title's build-in and the plate glow, read out of the guest's own draw stream | βœ… **measured** (Canary, `ARM=early` draw capture): the decoded *mechanism* is observed, not just its end state. **The five flashes fire in a six-frame window and are absent from all 155 other sampled frames**; `ptlogo_back2eff1` is drawn in exactly 2 frames at **t = 54.0** against a decoded peak of **t54–56**, and `ptlogo1` first appears at **t = 42.2** against a decoded **t42** β€” with units/frame taken from the **glow's period alone**, a different element. The two holders (`ptlogo_back2eff`, `ptlogo_back2`) are continuous from frame 134. βœ… The glow's per-vertex colour alpha IS its fade alpha: **observed range 0…80 against a decoded peak of 80**, exact and unfitted; **period 51.158 presented frames** over 20 cycle starts; fitting the decoded ramp gives RMS **13.16** against **38.18 reversed** (2.9Γ—), so the asymmetry is real and correctly directed. Structure: the settled title is 10–11 draws naming no sprite β€” which is why arming at the title sees nothing. ⚠️ Frame **107** is a 27-draw spike between the movie's last frame and the title's first; calling it "the composite" was an **over-read** β€” it binds **no texture** and only 4 of its 27 draws log geometry. The second title entry has no such frame. ⚠️ The two entries are the same animation at **different sampling phases** (only 4 of 46 aligned frames match), which is what makes the `eff3` result robust. πŸ”΄πŸ”΄ **RETRACTED β€” the game DOES draw `ptlogo_back2eff3`, and all five flashes fire in both entries in the declared stagger** (`eff3` at frames 133–134 / 5957–5958, i.e. t=60.1 and 62.3, inside its declared t∈(58,64)). The absence was an **instrument artefact**: a draw batches several quads (`indices=8` is two) and the log dumps only the first 8 vertices, so min/max over a line **merges** them β€” and because the wipe is right-aligned, `eff3` (788…1196) lies entirely inside `eff4` (447…1196), making the union *exactly* `eff4`'s extent. The merged box matched `eff4` to 1 px. πŸ”΄ Three explanations had been "ruled out" and all three were aimed at the wrong failure β€” notably the invisible-draw check counted draws with **no** geometry, where the hiding place was **partial** geometry. Superseded text follows: ~~three alternative explanations tested and failed: *phase* (its window is **6 units** against a **2.23-unit** step, so it cannot be missed β€” frames 133/134 sit at t=60.1/62.3 inside it and draw `eff2` and `eff4` instead), *an unlogged draw* (exactly 2 blind draws/frame, always the same full-screen-triangle shader, present when no wipe is active), and *a bad position guess* (dropping position entirely, **zero** quads anywhere have a width within Β±30 of 408; the spectrum jumps 262 β†’ 748). Draw counts across both entries: eff1 **4**, eff2 **3**, eff3 **0**, eff4 **6**.~~ (all from the merged-box parse, and wrong) πŸ”΄ **The port draws `eff3` at t=60–62 and the console does not.** ❔ Why is not established β€” nothing in its element record differs from its neighbours. ⚠️ An earlier "sub-frame phase" explanation and the advice that drawing all five "shows more sweep than the console" are both **withdrawn**. ⚠️ What a frame-by-frame build-in comparison *will* show is disagreement about which flash lands in which frame β€” 2 units/submitted frame against this run's 2.231 units/presented frame β€” and neither side is wrong. πŸ”΄ **Trap:** matching a bound texture's dimensions to a sprite fails both ways β€” it missed every flash *and* read the intro movie's 640Γ—360 YUV planes as `ptbase2`. βœ… A regression of five events' observed frames against their declared times (residuals ≀0.9 frames) recovers the intercept at frame **106.1** when the composite spike, not in the fit, is frame **107**. ⚠️ Per-vertex alpha = fade alpha holds for the **glow** and does not generalise β€” `eff4` reads 255/127/254 on consecutive frames. ❔ Frame rate not recorded, so nothing is in seconds; the glow's period implies a **114**-unit cycle against a declared 120, unexplained; `eff5` vs `ptlogo_back2eff` not separated | +| [`structures/boot-splash-gap-measured.md`](structures/boot-splash-gap-measured.md) | The black gap between the two boot splashes | βœ… **measured** in the guest's **draw stream**, which separates true black from a fade tail where luminance cannot: the publisher's last sprite is frame 125 (alpha 7), then **frames 126–129 submit NO sprite quad at all**, then the developer fades in at alpha 34. **The gap is 4 presented frames.** Converted with the disc as its own clock β€” `palogo_sqex` declares alphaβ‰₯1 for **239.8 units** and is drawn in **105** frames β†’ **2.284 units/frame** (the title capture independently gave 2.231) β€” that is **~9.1 units β‰ˆ 0.152 s**, against the **12** the port authored; ⚠️ and the true black is *shorter*, since both boundary frames still carry picture. πŸ”΄ **RETRACTED**: "the developer splash is ONE composited 525Γ—259 quad" β€” the same batching artefact. It draws three logos and three glows as separate quads in one `indices=24` call; the 525Γ—259 was `gamearts_eff` merged with `seta_eff`. The port refuted it with arithmetic (a 259-tall box cannot hold logos spanning y 164…585) before I checked. ⚠️ The gap measurement is unaffected β€” those glows are the developer splash's first draw. ❌ Not declared on the disc: `palogo_eff0.prm` is a single static keyframe, and the top-level `+0x08` is a **family constant** (300 / 60) whose slack ranges 12–226 units. ❔ The executable is **not** looked at β€” named, not claimed. πŸ”΄ The instrument was perturbing the measurement: the capture script taps β’Ά on "screen changed a lot", which is also true of a fading splash β€” it tapped through the publisher and the developer never appeared. `GRACE=1` and `NOTAP=1` knobs added | diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 84651d9c..d5ca26b2 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -1080,3 +1080,44 @@ title binds only 1280Γ—768 pages β€” both were there to be read first. The general shape: **a coincidence of size is not an identification.** Before matching on one attribute, ask what else in the frame shares it. + +## A batched draw merges quads, and the merge can be invisible + +A GPU draw can carry several quads β€” `indices=4` is one, `indices=8` two, +`indices=24` six β€” and the UI draw log dumps **only the first 8 vertices**. Taking +min/max over a log line's whole vertex list therefore silently *merges* quads into +one bounding box. + +This produced two wrong findings in one session, one of them reported to another +agent with three alternative explanations "ruled out": + +* **`ptlogo_back2eff3` "is never drawn by the game".** It is batched with + `ptlogo_back2eff4`, and because the wipe family is right-aligned, `eff3` + (788…1196) lies **entirely inside** `eff4` (447…1196). The union is *exactly* + `eff4`'s extent β€” so the merged box matched `eff4` to 1 px, `eff3` vanished, and + nothing looked wrong. +* **"the developer splash is one composited quad."** `gamearts_eff` and + `seta_eff` merged into a box that was read as the bounding box of three logos β€” + which it could not have been, since it was 259 px tall and they span 421. + +**Why the checks failed.** Three hypotheses were tested and refuted β€” sampling +phase, a draw with no geometry logged, a bad position guess. All three were aimed +at the wrong failure. In particular the "invisible draw" check counted draws with +**no** geometry line; the hiding place was draws with **partial** geometry, which +was never looked for. + +> πŸ”΄ **Refuting three wrong hypotheses is not evidence for a fourth.** The +> confidence gained from "I ruled out everything I could think of" is worth +> exactly as much as the list was complete, and a list of failure modes assembled +> by the person who built the instrument is the least likely to contain that +> instrument's own blind spot. + +Parse vertices in groups of four, one per quad, and **compare the logged quad +count against `indices / 4`** β€” `tools/re-capture/quads_per_frame.py` does both and +warns on the shortfall. + +⚠️ A related tell that was present and ignored: a merged box carries the *first* +quad's vertex colour, which made one element's alpha read 255 / 127 / 254 on +consecutive frames. That non-monotonicity was noticed, written down as "the +vertex-alpha identity does not generalise", and not chased. **An anomaly you +explain away is cheaper to chase than to re-derive later.** diff --git a/docs/re/structures/boot-splash-gap-measured.md b/docs/re/structures/boot-splash-gap-measured.md index 93b977d3..82c7bcfa 100644 --- a/docs/re/structures/boot-splash-gap-measured.md +++ b/docs/re/structures/boot-splash-gap-measured.md @@ -58,15 +58,24 @@ the last publisher frame still carries alpha 7 and the first developer frame alpha 34, so both boundary frames contain some picture that this count treats as black. -## βœ… A second thing the draw stream shows: the developer splash is ONE quad +## πŸ”΄ RETRACTED: "the developer splash is ONE composited quad" -The developer bundle declares three logos β€” `palogo_gamearts` at (390,164), -`palogo_seta` at (521,316), `palogo_anima` β€” and **none of their sizes is ever -submitted**. What is submitted is a single **525Γ—259 quad at (378,155)**, which is -the bounding box of the three. The game composites them and blits the group. +**It is not.** The three logos and their glows are drawn as separate quads, +**batched into a single draw call** β€” `indices=24` is six quads β€” and the log dumps +only the first 8 vertices. The "525Γ—259 quad at (378,155)" was min/max taken across +two *different* quads: `palogo_gamearts_eff` (525Γ—91 at 378,154) and +`palogo_seta_eff` (262Γ—108 at 512,306). -This is why an earlier pass reported "developer splash: 0 frames" β€” it was -matching individual logo sizes that the game never draws. +The port refuted it with arithmetic before I had checked: a 259-tall box cannot +contain three logos spanning y 164…585, and `palogo_anima` alone starts 35 px below +its bottom edge. They were right, and they were right to keep drawing three. + +⚠️ **The gap measurement is unaffected.** Those glows are the *first* thing the +developer splash draws (`t0 a0 β†’ t15 a255`), so frame 130 is still the developer +screen's first drawn frame, and frames 126–129 still submit nothing at all. + +See [`ui-title-buildin-measured.md`](ui-title-buildin-measured.md) for the same +trap producing a worse error on the title. ## Is the gap declared anywhere? Not that I can find diff --git a/docs/re/structures/ui-title-buildin-measured.md b/docs/re/structures/ui-title-buildin-measured.md index 2bed6a5a..d3f13793 100644 --- a/docs/re/structures/ui-title-buildin-measured.md +++ b/docs/re/structures/ui-title-buildin-measured.md @@ -79,52 +79,58 @@ a different element**, taking the composite spike (frame 107) as t=0: other 155 frames sampled.** The two holders are present continuously from frame 134 onward. That is the decoded mechanism, observed. -## πŸ”΄ The game does not draw `ptlogo_back2eff3` +## πŸ”΄πŸ”΄ RETRACTED: "the game does not draw `ptlogo_back2eff3`" -**Reproduced over two title entries, 5 800 frames apart, in one run.** The disc -declares `ptlogo_back2eff3` β€” alpha 0 at t58, **255 at t60–62**, 0 at t64, at -(788, 117), sprite 408Γ—203 β€” and the running game never submits it. +**It draws it. All five flashes fire, in both title entries, in exactly the +declared stagger.** The claim was an instrument artefact, and the instrument was +mine. -The wipe family is right-aligned: `eff2` 938+258, `eff3` 788+408, `eff4` 447+749, -`eff5` 64+1133 all end at xβ‰ˆ1196. It is a left-growing wipe in four widths, and -**the game draws three of them.** - -| element | width | draws across both build-ins | expected at ~2.23 units/frame | +| element | entry 1 | entry 2 | declared | |---|---|---|---| -| `eff1` | 167 | 4 | ~5 | -| `eff2` | 258 | 3 | ~5 | -| **`eff3`** | **408** | **0** | **~5** | -| `eff4` | 749 | 6 | ~5 | +| `ptlogo_back2eff1` | 130–131 | 5953–5955 | flash t54–58 | +| `ptlogo_back2eff2` | 133 | 5955–5957 | flash t58–62 | +| **`ptlogo_back2eff3`** | **133–134** | **5957–5958** | **flash t60–64** | +| `ptlogo_back2eff4` | 133–135 | 5957–5959 | flash t62–66 | +| `ptlogo_back2eff` / `eff5` | 134 β†’ | 5958 β†’ | holds | +| `ptlogo_back2` | 136 β†’ | 5962 β†’ | holds | -⚠️ An earlier version of this page called this a **sub-frame phase effect**. That -was wrong and is withdrawn. Three explanations were tested and all fail: +Frames 133 and 134 sit at t = 60.1 and 62.3 β€” inside `eff3`'s declared t ∈ (58, 64). +The disc was right about every one of them. -* **Sampling phase.** `eff3`'s non-zero window is t ∈ (58, 64) β€” **six units**, - against a step of **2.23 units per presented frame**. A window wider than the - step *cannot* be missed: at least two frames must land inside it, and frames 133 - (t = 60.1) and 134 (t = 62.3) do. Both draw `eff2` and `eff4`; neither draws - `eff3`. -* **A draw the log cannot see.** Exactly **2** draws per frame carry no geometry, - on all 932 settled title frames β€” always the same full-screen-triangle shader - (`prim=8 indices=3`, `vs=0x72CBCAA6A7984111`), present on frames where no wipe - element is active at all. `eff3` cannot be one of them. -* **A bad position guess on my side.** Dropping the position entirely and - searching *every* quad submitted anywhere on screen during both build-in - windows: **zero** have a width within Β±30 of 408. The width spectrum jumps - straight from 262 to 748. +### Why it looked absent, and why the checks that "ruled that out" did not -❔ **Why the game skips it is not established.** Nothing in its element record -distinguishes it β€” same kind (`0x0`), same keyframe shape, same `u4`/`u8`, same -scale, as `eff2` and `eff4` either side of it. +**A draw can batch several quads.** `indices=4` is one quad, `indices=8` is two, +`indices=24` is six β€” and the log dumps **only the first 8 vertices**. Taking +min/max over a line's whole vertex list therefore *merges* quads into a single +box. -πŸ”΄ **For the port: you draw `eff3` at t=60–62 and the console does not.** That is -one extra frame of wipe, at the widest-but-one step. It is small, and it is real. +`eff3` is batched with `eff4` in an `indices=8` draw. And `eff3` (788…1196) sits +**entirely inside** `eff4`'s x-range (447…1196), because the wipe family is +right-aligned β€” so the union of the two is **exactly `eff4`'s own extent**. The +merged box matched `eff4` to 1 px, `eff3` disappeared, and nothing looked wrong. -πŸ”΄ **A second thing this page previously claimed, also withdrawn: that a port -drawing all five flashes shows "more sweep than the console".** There is no -evidence for that. The port draws them sequentially at their declared times, never -more than two at once, which is what frames 131–135 above show the game doing. -The pile-up worth warning about was the `rest()` bug, and that is fixed. +Every check I ran was aimed at the wrong failure: + +* "sampling phase" β€” correctly refuted, and irrelevant; +* "a draw the log cannot see" β€” I counted draws with **no** geometry line. The + hiding place was draws with **partial** geometry, which I never looked for; +* "a bad position guess" β€” I searched for a 408-wide box. The box did not exist + because it had been merged, not because the quad had not been drawn. + +**Three refutations of the wrong hypothesis do not add up to one confirmation.** + +`tools/re-capture/quads_per_frame.py` now parses vertices in groups of four, one +per quad, and warns when a batch exceeds the 8-vertex cap. + +### What this also explains + +`eff4`'s alpha read 255 / 127 / 254 on consecutive frames β€” non-monotonic, which I +flagged as "the vertex-alpha identity does not generalise". It was not the +identity failing; those were **merged boxes carrying the first quad's colour**. + +πŸ”΄ **A second claim, also withdrawn: that a port drawing all five flashes shows +"more sweep than the console".** The opposite is true β€” the console draws all +five. Drawing them sequentially at their declared times is exactly right. ⚠️ **What a frame-by-frame comparison of the build-in WILL show, and it is not a defect on either side.** The game's timeline is 60 units/s against a 30 Hz diff --git a/tools/re-capture/quads_per_frame.py b/tools/re-capture/quads_per_frame.py new file mode 100755 index 00000000..f2bc8c50 --- /dev/null +++ b/tools/re-capture/quads_per_frame.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Parse a `xenia_re_ui_draws_NN.log` into ONE ROW PER QUAD. + +πŸ”΄ The reason this file exists. A draw can BATCH several quads β€” `indices=8` is +two, `indices=24` is six β€” and the log dumps only the first 8 vertices. Taking +min/max over a line's whole vertex list therefore merges quads into one box. + +That is not a theoretical hazard. It silently produced two wrong findings on +2026-08-29: + + * `ptlogo_back2eff3` (408x203 @ 788,117) is batched with `ptlogo_back2eff4` + (749x203 @ 447,117), and eff3 sits ENTIRELY INSIDE eff4's x-range, so the + union equals eff4 exactly. The merged box matched eff4 to 1 px and eff3 + "was never drawn" β€” reported, with three other explanations ruled out. + * the developer splash's `gamearts_eff` + `seta_eff` merged into a 525x259 + box that was read as "the three logos composited into one quad". + +Vertices come in groups of four, one per quad. Read them that way. +""" +import re, sys, json + +def quads(path, lo=None, hi=None): + """Yield (frame, index_count, logged_quads, expected_quads, x, y, w, h, alpha).""" + frame = None + pend = None + for line in open(path, errors="replace"): + m = re.match(r"--- frame (\d+) ---", line) + if m: + frame = int(m.group(1)); pend = None; continue + if frame is None: + continue + if lo is not None and not (lo <= frame <= hi): + continue + mm = re.match(r"^\s*\d+ prim=(\d+) indices=(\d+)", line) + if mm: + pend = int(mm.group(2)); continue + vs = re.findall(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=[-\d.]+(?:,col=([0-9A-F]{8}))?\]", line) + if not vs or pend is None: + continue + exp = max(1, pend // 4) + got = len(vs) // 4 + for k in range(got): + g = vs[k*4:(k+1)*4] + xs = [(float(a) + 1) / 2 * 1280 for a, b, _ in g] + ys = [(1 - float(b)) / 2 * 720 for a, b, _ in g] + col = next((c for _, _, c in g if c), None) + yield (frame, pend, got, exp, + round(min(xs)), round(min(ys)), + round(max(xs) - min(xs)), round(max(ys) - min(ys)), + int(col[:2], 16) if col else -1) + pend = None + +if __name__ == "__main__": + path = sys.argv[1] + lo, hi = (int(sys.argv[2]), int(sys.argv[3])) if len(sys.argv) > 3 else (None, None) + unlogged = 0 + for q in quads(path, lo, hi): + if q[2] < q[3]: + unlogged += q[3] - q[2] + print(",".join(str(v) for v in q)) + if unlogged: + print(f"# WARNING: {unlogged} quads were batched but NOT logged (8-vertex cap)", + file=sys.stderr) diff --git a/tools/re-capture/ui_draw_capture.sh b/tools/re-capture/ui_draw_capture.sh index 1547322e..57a15ce0 100755 --- a/tools/re-capture/ui_draw_capture.sh +++ b/tools/re-capture/ui_draw_capture.sh @@ -97,10 +97,18 @@ while [ $SECONDS -lt $deadline ]; do echo "MENU CAPTURED at ${SECONDS}s"; break fi echo "F10 pressed on the menu; no log yet" - elif [ "$d" -gt 1500 ]; then + elif [ "$d" -gt 1500 ] && [ "${tapped:-0}" = "0" ]; then # Still in the attract loop. Without this the menu target waits forever: # the attract movie returns to the title and away again, and a run that # only ever watches never gets there. + # + # πŸ”΄ `tapped` guard added 2026-08-29, and it is not cosmetic. The trigger + # here is "the screen changed a lot", which is ALSO true of the + # titleβ†’menu transition this target has just started. A run tapped A on + # the title at t=23 s and again at t=27 s on the transition; the guest + # faulted, and Xenia dumped registers to stdout until the file reached + # **519 MB**. That is the double-tap into the save-data probe this + # script's own header warns about, arriving through the movie branch. echo "movie (rmse $d) -> skip A"; python3 "$SD/pad.py" tap A 0.25; sleep 3 fi sleep 1