Ask #1 measured: held at the title with no input the plate oscillates ~23 cycles per window with no decay and NEVER goes off -- floor 159 thresholded pixels, pulse bottoming at 714. A glow alone cannot do that (0 -> 80 -> 0); a steady base plus a pulsing glow can, and 714/1520 are base-only and base-plus-glow. The port already had the base right: ptbtn00's fade to 0 at t=244 is its EXIT ramp, and while held the base sits at alpha 255. The glow was missing. The defect was in the renderer, not the entry I deleted on 2026-08-29. _draw treated a looping record like a focused one -- drawn INSTEAD of the base, which is right for a focused button and wrong here. I deleted the right thing for the wrong reason. A looping record now draws OVER the base and the entry is restored. Rendered: 95.68 base only, 115.52 at peak, period 1.980 s against a declared 120 units = 2.000 s, ratio 0.990. Visible on the boot path; not under --screen --overlay, where a static overlay poses at its arrival by design. Authored in UNITS on their instruction, with their three limits verbatim: one boot, does not distinguish boot from attract title, and 714/1520 is not an alpha ratio so no duty cycle is inferred. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
7181 lines
383 KiB
Markdown
7181 lines
383 KiB
Markdown
# Decisions
|
||
|
||
One entry per decision that outlives the container it was made in. Newest last.
|
||
A decision that lives only in an agent's context is lost when that container
|
||
dies, which is what this file is for.
|
||
|
||
---
|
||
|
||
|
||
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
|
||
|
||
122 sections. Search this before re-deriving anything.
|
||
|
||
* [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28)
|
||
* [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28)
|
||
* [P1 gate — the diff, and what it found](#p1-gate--the-diff-and-what-it-found)
|
||
* [P2 — keyframe animation, 2026-08-28](#p2--keyframe-animation-2026-08-28)
|
||
* [`rest` misidentifies six elements, and the running game says so](#rest-misidentifies-six-elements-and-the-running-game-says-so)
|
||
* [The title is not settled, and P2 does not claim it](#the-title-is-not-settled-and-p2-does-not-claim-it)
|
||
* [P2, corrected — the pin moved, and the settle rule was wrong, 2026-08-28](#p2-corrected--the-pin-moved-and-the-settle-rule-was-wrong-2026-08-28)
|
||
* [The reference renderer was stale for three diff runs](#the-reference-renderer-was-stale-for-three-diff-runs)
|
||
* [The menu had no background, and P1 called that correct, 2026-08-29](#the-menu-had-no-background-and-p1-called-that-correct-2026-08-29)
|
||
* [P3 — splash → title, unattended, 2026-08-29](#p3--splash--title-unattended-2026-08-29)
|
||
* [P3 gate](#p3-gate)
|
||
* [Answers taken from the RE agent without re-deriving them](#answers-taken-from-the-re-agent-without-re-deriving-them)
|
||
* [P4 — the intro video, 2026-08-29](#p4--the-intro-video-2026-08-29)
|
||
* [P4 gate](#p4-gate)
|
||
* [RETRACTION — `sylpheed-cli` is not the oracle, 2026-08-29](#retraction--sylpheed-cli-is-not-the-oracle-2026-08-29)
|
||
* [P5 groundwork — the focus record, checked against a capture, 2026-08-29](#p5-groundwork--the-focus-record-checked-against-a-capture-2026-08-29)
|
||
* [P5 — navigation, 2026-08-29](#p5--navigation-2026-08-29)
|
||
* [`--headless` cannot draw, and the port hung instead of saying so, 2026-08-29](#--headless-cannot-draw-and-the-port-hung-instead-of-saying-so-2026-08-29)
|
||
* [Refutation — the focus ring IS drawn rotated, and it is not at 0° in either capture](#refutation--the-focus-ring-is-drawn-rotated-and-it-is-not-at-0-in-either-capture)
|
||
* [P5 end to end — and the title does not say `PRESS Ⓐ`, 2026-08-29](#p5-end-to-end--and-the-title-does-not-say-press--2026-08-29)
|
||
* [P6 — menu audio, 2026-08-29](#p6--menu-audio-2026-08-29)
|
||
* [P6 gate — the audio is in the mix, and a null control says which part](#p6-gate--the-audio-is-in-the-mix-and-a-null-control-says-which-part)
|
||
* [P3, reopened — the boot title was missing the `PRESS Ⓐ` plate, 2026-08-29](#p3-reopened--the-boot-title-was-missing-the-press--plate-2026-08-29)
|
||
* [P5 — the focus ring spins, 2026-08-29](#p5--the-focus-ring-spins-2026-08-29)
|
||
* [P3, corrected — the plate needs no authored delay at all, 2026-08-29](#p3-corrected--the-plate-needs-no-authored-delay-at-all-2026-08-29)
|
||
* [P7 — the new-game intro, 2026-08-29](#p7--the-new-game-intro-2026-08-29)
|
||
* [P7 gate](#p7-gate)
|
||
* [Modding — rule 4 was never implemented, 2026-08-29](#modding--rule-4-was-never-implemented-2026-08-29)
|
||
* [Refutation — the paint-order key, and the reach of its tie-break](#refutation--the-paint-order-key-and-the-reach-of-its-tie-break)
|
||
* [Correction — the runtime "clipping" I flagged 🔴 twice was overstated](#correction--the-runtime-clipping-i-flagged--twice-was-overstated)
|
||
* [The P1 regression harness had been broken since the monorepo merge, 2026-08-29](#the-p1-regression-harness-had-been-broken-since-the-monorepo-merge-2026-08-29)
|
||
* [Refutation — "builds 0/1 and 10/11 are the loading screen" is false in the index space this export uses](#refutation--builds-01-and-1011-are-the-loading-screen-is-false-in-the-index-space-this-export-uses)
|
||
* [The intro's missing dialogue was an export gap, not a transcode bug, 2026-08-29](#the-intros-missing-dialogue-was-an-export-gap-not-a-transcode-bug-2026-08-29)
|
||
* [Refutation, of my own exporter — MISSION §6 pins a downmix matrix, and the exporter ships a different one](#refutation-of-my-own-exporter--mission-6-pins-a-downmix-matrix-and-the-exporter-ships-a-different-one)
|
||
* [Refutation of my own two-stem reading — and it had already been adopted elsewhere](#refutation-of-my-own-two-stem-reading--and-it-had-already-been-adopted-elsewhere)
|
||
* [The mono fold I warned about, in the comment directly above the code that did it](#the-mono-fold-i-warned-about-in-the-comment-directly-above-the-code-that-did-it)
|
||
* [The leading chunk is the TAIL of the full one — measured, and it is why the region over-covers](#the-leading-chunk-is-the-tail-of-the-full-one--measured-and-it-is-why-the-region-over-covers)
|
||
* [Third reading of a voice region, and this one is decoded: three presentations of one take](#third-reading-of-a-voice-region-and-this-one-is-decoded-three-presentations-of-one-take)
|
||
* [The transcode cache had never hit, because the wipe ran first](#the-transcode-cache-had-never-hit-because-the-wipe-ran-first)
|
||
* [`settle_time()` — the answer arrived, and it refutes my own 🔴 more than it confirms it](#settle_time--the-answer-arrived-and-it-refutes-my-own--more-than-it-confirms-it)
|
||
* [The voice presentation is now unambiguously the port's choice, and the recommendation behind it was withdrawn](#the-voice-presentation-is-now-unambiguously-the-ports-choice-and-the-recommendation-behind-it-was-withdrawn)
|
||
* [Refutation of my dual-mono inference — the measurement stands, the generalisation does not](#refutation-of-my-dual-mono-inference--the-measurement-stands-the-generalisation-does-not)
|
||
* [Two rows of the P1 baseline were comparing blank frames and reporting OK](#two-rows-of-the-p1-baseline-were-comparing-blank-frames-and-reporting-ok)
|
||
* [Refutation attempt — the loading-screen variants, and it survived](#refutation-attempt--the-loading-screen-variants-and-it-survived)
|
||
* [🔴 The voice export is known incomplete — the game decodes all three streams at once](#the-voice-export-is-known-incomplete--the-game-decodes-all-three-streams-at-once)
|
||
* [🔴 The oracle capture does not contain the intro — a controlled negative](#the-oracle-capture-does-not-contain-the-intro--a-controlled-negative)
|
||
* [🔴 Take 2 is clean, my instrument was not, and the negative had to be re-earned](#take-2-is-clean-my-instrument-was-not-and-the-negative-had-to-be-re-earned)
|
||
* [Every music bank was summed at 1/3 when only two sub-waves are music — 3.52 dB, since P6](#every-music-bank-was-summed-at-13-when-only-two-sub-waves-are-music--352-db-since-p6)
|
||
* [Take 2 was starved, my correlator was fine, and `check-capture` was incomplete](#take-2-was-starved-my-correlator-was-fine-and-check-capture-was-incomplete)
|
||
* [The settle run carries an unmeasured real-time factor — and the numbers it touches were already unauthored](#the-settle-run-carries-an-unmeasured-real-time-factor--and-the-numbers-it-touches-were-already-unauthored)
|
||
* [`verify-dwell` — the comparison that refuted my own 🔴, made repeatable](#verify-dwell--the-comparison-that-refuted-my-own--made-repeatable)
|
||
* [The `PRESS Ⓐ` plate pulses — authored per element, because the census forbids a rule](#the-press--plate-pulses--authored-per-element-because-the-census-forbids-a-rule)
|
||
* [✅ The oracle finally speaks: the exported voice IS the game's centre channel](#the-oracle-finally-speaks-the-exported-voice-is-the-games-centre-channel)
|
||
* [The stripping control passes — `S00A` is obtainable, and the gate is cleared](#the-stripping-control-passes--s00a-is-obtainable-and-the-gate-is-cleared)
|
||
* [The correctness harness the docs promised for eight milestones did not exist](#the-correctness-harness-the-docs-promised-for-eight-milestones-did-not-exist)
|
||
* [Refutation attempt — the tone curve survives in its stated reach and not past it](#refutation-attempt--the-tone-curve-survives-in-its-stated-reach-and-not-past-it)
|
||
* [Identifying the capture's focused button — and my harness was posing the port wrong](#identifying-the-captures-focused-button--and-my-harness-was-posing-the-port-wrong)
|
||
* [`tools/port/which-focus` — the Decoder asked for a detector, and it carries its own control](#toolsportwhich-focus--the-decoder-asked-for-a-detector-and-it-carries-its-own-control)
|
||
* [The title's 1.82 % — three of my own explanations refuted, and the format has no blend mode](#the-titles-182---three-of-my-own-explanations-refuted-and-the-format-has-no-blend-mode)
|
||
* [🔴 The exporter dropped nested `.rat` leaf geometry on 45 elements — and it is the title's 1.82 %](#the-exporter-dropped-nested-rat-leaf-geometry-on-45-elements--and-it-is-the-titles-182)
|
||
* [The leaf composition is decoded and implemented — and it does **not** close the 1.82 %](#the-leaf-composition-is-decoded-and-implemented--and-it-does-not-close-the-182)
|
||
* [The −324 was the old keyframe association, and the corrected one is available **today**](#the-324-was-the-old-keyframe-association-and-the-corrected-one-is-available-today)
|
||
* [Re-running the P5/P6 gate after eight iterations of changes](#re-running-the-p5p6-gate-after-eight-iterations-of-changes)
|
||
* [Pinned `formats-pin-2026-08-29c` — and the knob I tested last iteration was retired](#pinned-formats-pin-2026-08-29c--and-the-knob-i-tested-last-iteration-was-retired)
|
||
* [Refuted — my own "the single non-whole-multiple scale in the export"](#refuted--my-own-the-single-non-whole-multiple-scale-in-the-export)
|
||
* [The 11.5 px was the fit's resolution, and the lesson inverts](#the-115-px-was-the-fits-resolution-and-the-lesson-inverts)
|
||
* [🔴 The focus ring had silently stopped, and BLOCKED had listed it](#the-focus-ring-had-silently-stopped-and-blocked-had-listed-it)
|
||
* [The plate's period is now the disc's 105, and it disagrees with the measurement](#the-plates-period-is-now-the-discs-105-and-it-disagrees-with-the-measurement)
|
||
* [The plate's period is 120, decoded — and it was falsified with my own ring number](#the-plates-period-is-120-decoded--and-it-was-falsified-with-my-own-ring-number)
|
||
* [✅ A settled screen is ONE instant, and it collapsed three residuals at once](#a-settled-screen-is-one-instant-and-it-collapsed-three-residuals-at-once)
|
||
* [Their census, and a framing of mine they sharpened](#their-census-and-a-framing-of-mine-they-sharpened)
|
||
* [Their "do not draw all five flashes" flag — checked, and it does not apply here](#their-do-not-draw-all-five-flashes-flag--checked-and-it-does-not-apply-here)
|
||
* [✅ The `publisher_logo` residual was a missing black hold, and we had both dismissed it](#the-publisher_logo-residual-was-a-missing-black-hold-and-we-had-both-dismissed-it)
|
||
* [`ptlogo_back2eff3` — recorded, deliberately not acted on](#ptlogo_back2eff3--recorded-deliberately-not-acted-on)
|
||
* [The narrow settle windows are harmless, and I can now say why](#the-narrow-settle-windows-are-harmless-and-i-can-now-say-why)
|
||
* [Refuted, mine — "the menu residual is localised on the `ptloop` sweeps"](#refuted-mine--the-menu-residual-is-localised-on-the-ptloop-sweeps)
|
||
* [Refuted — "the developer splash is one composited quad, the bounding box of the three logos"](#refuted--the-developer-splash-is-one-composited-quad-the-bounding-box-of-the-three-logos)
|
||
* [The black hold is 9 units, not 12 — measured in draws rather than luminance](#the-black-hold-is-9-units-not-12--measured-in-draws-rather-than-luminance)
|
||
* [The title's sweeps loop — measured, and the field could not have told us](#the-titles-sweeps-loop--measured-and-the-field-could-not-have-told-us)
|
||
* [The menus' residual is the tone floor, not structure — and `extras` is not really 3× worse](#the-menus-residual-is-the-tone-floor-not-structure--and-extras-is-not-really-3-worse)
|
||
* [Refutation attempt — their 239.8-unit figure, checked from my export](#refutation-attempt--their-2398-unit-figure-checked-from-my-export)
|
||
* [🔴 The loading screens are black at *every* instant — which proves the layer rule wrong for a layerless element](#the-loading-screens-are-black-at-every-instant--which-proves-the-layer-rule-wrong-for-a-layerless-element)
|
||
* [Their `eff3` retraction — my refusal was right, and my refutation found the same bug](#their-eff3-retraction--my-refusal-was-right-and-my-refutation-found-the-same-bug)
|
||
* [The forced backdrop: two of sixteen screens were black for their whole life](#the-forced-backdrop-two-of-sixteen-screens-were-black-for-their-whole-life)
|
||
* [Refutation attempt — the forced-backdrop rule's quantifier, and whether it misses a case](#refutation-attempt--the-forced-backdrop-rules-quantifier-and-whether-it-misses-a-case)
|
||
* [The 256/211 was never a disagreement — and my own census had already said so](#the-256211-was-never-a-disagreement--and-my-own-census-had-already-said-so)
|
||
* [The clock freezes at settle — the port's settle window, seen from the other side](#the-clock-freezes-at-settle--the-ports-settle-window-seen-from-the-other-side)
|
||
* [🔴 Withdrawn — "the boot is known too fast". The splash dwells are declared, and the port was already playing them](#withdrawn--the-boot-is-known-too-fast-the-splash-dwells-are-declared-and-the-port-was-already-playing-them)
|
||
* [Refutation attempt — their two splash boundaries are not anchored the same way](#refutation-attempt--their-two-splash-boundaries-are-not-anchored-the-same-way)
|
||
* [Their corrected boundaries check out against the file — all six, exactly](#their-corrected-boundaries-check-out-against-the-file--all-six-exactly)
|
||
* [The n=1 disclosure, and the one port constant that rests on a single run](#the-n1-disclosure-and-the-one-port-constant-that-rests-on-a-single-run)
|
||
* [P6 gate — sound on the P5 walk, verified, and the tool I nearly shipped instead](#p6-gate--sound-on-the-p5-walk-verified-and-the-tool-i-nearly-shipped-instead)
|
||
* [Their `.tbm` self-refutation does not reach this archive — and it fixes my guard anyway](#their-tbm-self-refutation-does-not-reach-this-archive--and-it-fixes-my-guard-anyway)
|
||
* [Coverage is now tested per instant, because scale animates](#coverage-is-now-tested-per-instant-because-scale-animates)
|
||
* [P7 gate — the new-game intro plays and returns, and a defect I nearly invented](#p7-gate--the-new-game-intro-plays-and-returns-and-a-defect-i-nearly-invented)
|
||
* [`ScreenView.skipped` was correct and unread since P1 — now it says so itself](#screenviewskipped-was-correct-and-unread-since-p1--now-it-says-so-itself)
|
||
* [Refutation attempt — "the element declared first paints first"](#refutation-attempt--the-element-declared-first-paints-first)
|
||
* [The menu bed plays under the cutscene, nobody decided that, and it stays](#the-menu-bed-plays-under-the-cutscene-nobody-decided-that-and-it-stays)
|
||
* [`wait:<seconds>`, and the bed's loop seam is 3.4 seconds of silence](#waitseconds-and-the-beds-loop-seam-is-34-seconds-of-silence)
|
||
* [Two harness bugs, and the defect the second one was hiding](#two-harness-bugs-and-the-defect-the-second-one-was-hiding)
|
||
* [The `PRESS Ⓐ` plate: four bugs in a row, and a number I have been misquoting](#the-press--plate-four-bugs-in-a-row-and-a-number-i-have-been-misquoting)
|
||
* [The title's residual is the sweep phase, and the sweeps fit at ~400 units, not 357.7](#the-titles-residual-is-the-sweep-phase-and-the-sweeps-fit-at-400-units-not-3577)
|
||
* [A second capture closes the sweep-geometry question, and the plate matches at 0.00093 %](#a-second-capture-closes-the-sweep-geometry-question-and-the-plate-matches-at-000093)
|
||
* [`--focus=` did nothing on the menu path, and the corpus had an untested focus capture](#--focus-did-nothing-on-the-menu-path-and-the-corpus-had-an-untested-focus-capture)
|
||
* [The last unused capture, placed — and its residual is the oracle's, not the port's](#the-last-unused-capture-placed--and-its-residual-is-the-oracles-not-the-ports)
|
||
* [`MODDING.md` had five rules and no check. Now it has one, and all five pass](#moddingmd-had-five-rules-and-no-check-now-it-has-one-and-all-five-pass)
|
||
* [Five authored values had no reader — including the one I asked for measurements into](#five-authored-values-had-no-reader--including-the-one-i-asked-for-measurements-into)
|
||
* [`FORMAT.md` declared the port's own export invalid, and a failed export is not atomic](#formatmd-declared-the-ports-own-export-invalid-and-a-failed-export-is-not-atomic)
|
||
* [`check-all`, a verdict that ignored its own statistic, and a claim of mine that was wrong](#check-all-a-verdict-that-ignored-its-own-statistic-and-a-claim-of-mine-that-was-wrong)
|
||
* [The `title` disagreement, localised — and the question I filed for it was the wrong one](#the-title-disagreement-localised--and-the-question-i-filed-for-it-was-the-wrong-one)
|
||
* [Auditing `BLOCKED.md` found three stale rows, and the undated ones were all three](#auditing-blockedmd-found-three-stale-rows-and-the-undated-ones-were-all-three)
|
||
* [The record already answered last iteration's question, under headings that name it](#the-record-already-answered-last-iterations-question-under-headings-that-name-it)
|
||
* [🔴 Twenty-one messages to a dead address, each one warning me it was dead](#twenty-one-messages-to-a-dead-address-each-one-warning-me-it-was-dead)
|
||
* [The forced-backdrop pass is load-bearing on two screens, not six](#the-forced-backdrop-pass-is-load-bearing-on-two-screens-not-six)
|
||
* [Re-running the Decoder's necessity census: every figure reproduces, and what that is worth](#re-running-the-decoders-necessity-census-every-figure-reproduces-and-what-that-is-worth)
|
||
* [A second witness for the pixel-cost claim, from a different renderer](#a-second-witness-for-the-pixel-cost-claim-from-a-different-renderer)
|
||
* [Reconciling the two ink figures, and what "has its own key" is resting on](#reconciling-the-two-ink-figures-and-what-has-its-own-key-is-resting-on)
|
||
* [Not one of the 80 has a decoded key — and the port's four are the rule's oracle check](#not-one-of-the-80-has-a-decoded-key--and-the-ports-four-are-the-rules-oracle-check)
|
||
* [A withholding reason that was false, and the measurement beside it that was not](#a-withholding-reason-that-was-false-and-the-measurement-beside-it-that-was-not)
|
||
* [The sweep discriminator resolves: different frames, and a sweep position cannot date one](#the-sweep-discriminator-resolves-different-frames-and-a-sweep-position-cannot-date-one)
|
||
* [Their trap, run against my tree — and I found its mirror instead](#their-trap-run-against-my-tree--and-i-found-its-mirror-instead)
|
||
* [The plate pulses — measured, and the port was wrong on the boot's end state](#the-plate-pulses--measured-and-the-port-was-wrong-on-the-boots-end-state)
|
||
|
||
<!-- /INDEX -->
|
||
## P0 — the exporter, 2026-08-28
|
||
|
||
### The exporter reads one authored file, and stamps its provenance into the output
|
||
|
||
`export/` is derived and `authored/` is hand-written, and the natural reading of
|
||
that is that the exporter never touches `authored/`. But a screen has to be
|
||
*called* something, and the disc does not name its builds — the identification of
|
||
build 5 as the main menu is HANDOFF Q2, **measured against a live capture**, not
|
||
a field.
|
||
|
||
Two ways to handle that:
|
||
|
||
1. the exporter emits `build_05.json` and the runtime renames it from
|
||
`authored/screen_names.json`;
|
||
2. the exporter reads that map and writes `main_menu.json` directly.
|
||
|
||
Chose **2**, with a condition: every name it applies carries `name_source:
|
||
"authored"` and a `name_why` quoting the evidence, and `check` **rejects** an
|
||
authored name with no `why`. The file that lands in `export/` is therefore still
|
||
honest about which of its fields is a measurement — which is the property the
|
||
derived/authored split exists to protect — while a human opening the tree sees
|
||
`main_menu.json` rather than having to resolve a rename in their head. A build
|
||
nobody has identified exports as `build_NN` with `name_source: "index"`, which is
|
||
a locator and not a claim.
|
||
|
||
This is the **only** authored input the exporter takes. Everything else in
|
||
`authored/` is applied by the runtime over `export/`.
|
||
|
||
### Sprites are per screen, not a flat pool
|
||
|
||
`main_menu` and `extras` both ship a `ptbase.t32` and they are different
|
||
pictures. A flat `sprites/` directory would have silently collided; whichever
|
||
screen exported second would have won, and the loser would have drawn the wrong
|
||
background with no error anywhere. `sprites/<subdir>/<screen>/<name>.png`.
|
||
|
||
### The format is executable
|
||
|
||
`sylpheed-export check --out export` validates a tree against `docs/FORMAT.md`
|
||
with no disc in hand. It exists because "the export is correct" is otherwise an
|
||
assertion, and because the P0 gate is *"validates against FORMAT.md"* — which is
|
||
not a thing anyone can confirm by reading.
|
||
|
||
It reads the tree the way Godot will: as a stranger, with no access to the disc,
|
||
the decoders, or the exporter's internals. It deliberately does **not** check the
|
||
export against the disc — that is what `sylpheed-cli screen render` is for, at P1.
|
||
|
||
Checked that it bites, rather than assuming: five mutations of a valid
|
||
`main_menu.json` — a broken `paint_order` permutation, a dangling
|
||
`focus_sprite`, a reversed `buttons` list, a `#rrggbbaa` colour, an invented
|
||
`name_source` — are each caught with a specific message.
|
||
|
||
### The highlight sprite pairs by name; `opt ` is exported but not believed
|
||
|
||
FORMAT v1 said `focus_sprite` came from the element's `opt ` link. That reading
|
||
was **measured and refuted** by the RE agent, and this export shows why plainly:
|
||
on the main menu, `opt ` chains `ptloop01 → ptloop02 → ptbtn01` — two decorations
|
||
and then a button. It is a linked list of something, and it is not focus.
|
||
|
||
The highlight is paired by **sprite name** instead (`ptbtn01.t32` ↔
|
||
`ptbtn01f.t32`), which is HANDOFF's convention and holds for all 54 real pairs on
|
||
the disc. It resolves all five main-menu buttons. The raw link is still exported
|
||
as `opt_link`, renamed so that nothing downstream mistakes it for navigation, and
|
||
so that whoever eventually decodes it has the data.
|
||
|
||
Note this is 🟡 a naming convention, not a decoded field. It is authored in
|
||
effect, and lives in the exporter only because it is a rule over disc data rather
|
||
than a value we chose.
|
||
|
||
### The paint order is exported, not authored
|
||
|
||
Q3 decoded it — a `u16` layer key at `+0x0A` of each `T8aD` sprite header,
|
||
stable-sorted with declaration index. So it is read in the exporter, per the
|
||
contract's own rule for a decoded answer, and `paint_order` in `export/` is a
|
||
derived field. `"paint_order"` is gone from `unresolved`; **`paint_order_ties`
|
||
replaces it**, because the tie-break is still unknown and costs one element's
|
||
blend on one screen.
|
||
|
||
Where an element has no `T8aD` header the key comes from the decoders' table of
|
||
keys **measured off the running game**. That is a different kind of fact, so it
|
||
is labelled: `layer_source` is `"sprite"`, `"implied"` or `"none"`, and a
|
||
consumer that needs to know whether a layer is read or measured can tell.
|
||
|
||
### Colours are exported as two fields with the byte order in the name
|
||
|
||
There are two modulate colours and they multiply: `tint` is RGBA, `fade` is
|
||
**ARGB** and its high byte is the alpha that ramps. v1's single `"#ffffffff"`
|
||
could not carry both and silently discarded the ramping alpha. They are exported
|
||
as `tint_rgba` and `fade_argb`, raw hex, byte order in the key — because getting
|
||
it backwards is silent and looks like an art bug rather than a parse bug.
|
||
|
||
### `t` stays raw
|
||
|
||
HANDOFF Q1 is answered — linear ramp, 2 units per rendered frame, working
|
||
conversion 1 unit = 1/60 s — but that conversion is **measured off the running
|
||
game, not read from the file**, and the finding itself flags the 27.6 present-
|
||
frames/second measurement as the part worth re-testing. If the game turns out to
|
||
present at 60 Hz, every duration halves.
|
||
|
||
So `t` is exported exactly as the disc spells it, `keyframe_time_unit` stays in
|
||
`unresolved`, and the conversion will live in one authored place at P2. One
|
||
constant to change, in a file that says it is a decision.
|
||
|
||
### The final keyframe has no `t`, and `check` enforces that
|
||
|
||
The disc has no time slot on the last keyframe of a group. A file that carries
|
||
one there has invented it. `check` rejects it — this is the one place where the
|
||
temptation to emit a plausible number is strongest and the resulting error is
|
||
completely invisible.
|
||
|
||
---
|
||
|
||
## P1 — Godot draws the screen, 2026-08-28
|
||
|
||
### The Godot side reads the manifest, not a path
|
||
|
||
`ExportTree` is the only class that knows where `export/` is: `SYLPHEED_EXPORT`
|
||
if set, otherwise `<project>/../export`. Screens are addressed by their manifest
|
||
**name** (`main_menu`), never by a file path, so the runtime never encodes the
|
||
archive's subdirectory and a re-export that moves a file does not break it. It
|
||
also checks `format` on both the manifest and each screen, and refuses a tree it
|
||
was not built to read rather than half-drawing one.
|
||
|
||
Textures are read as bytes and decoded with `load_png_from_buffer` at runtime.
|
||
They are deliberately **not** Godot-imported resources: `export/` is gitignored
|
||
and regenerated wholesale, and a `.import` sidecar per sprite would be derived
|
||
state living next to derived state, invalidated on every re-export.
|
||
|
||
### One CanvasItem draws the whole screen
|
||
|
||
`ScreenView._draw` walks `paint_order` and draws each element itself, rather
|
||
than making a node per element and leaning on `z_index`. The export's
|
||
`paint_order` is already back-to-front, so honouring it is a loop; expressing
|
||
the same order through sixteen nodes' z-indices would hide the one thing that is
|
||
still unresolved about it — the **ties** — behind Godot's own sibling rules,
|
||
where a change in the export would silently become a change in Godot's tree
|
||
order instead of a visible change in the draw sequence.
|
||
|
||
### P1 draws `rest` and nothing else
|
||
|
||
Every element is drawn at its resting pose. No keyframe interpolation: that is
|
||
P2, and it depends on the keyframe time unit, which is **measured** rather than
|
||
decoded. A milestone whose gate is a pixel diff must not have a measured
|
||
constant inside it, or the diff stops being evidence about the port.
|
||
|
||
For the same reason `focused_id` is empty at P1. Initial focus was measured as
|
||
unstable boot to boot (HANDOFF Q5), so choosing one is an authored decision and
|
||
it belongs to P5, where a human is pressing keys.
|
||
|
||
### Nearest-neighbour, and why that is not a preference
|
||
|
||
`TEXTURE_FILTER_NEAREST`. The export is a 1:1 copy of the disc's own texels and
|
||
elements draw at up to 500 %; a bilinear filter invents detail the disc does not
|
||
have. It is also what the reference renderer does — `ui_layout::blit` maps
|
||
destination to source by integer division — so a filter difference cannot
|
||
masquerade as a placement difference in the diff.
|
||
|
||
### The capture is the SubViewport, not the window
|
||
|
||
The screen is drawn into a `SubViewport` sized to the export's own `design`
|
||
rectangle and shown through a container that scales it to the window. The first
|
||
attempt captured `get_viewport()` and got **1235×695**: there is a window manager
|
||
on the Xvfb display and its title bar had eaten 45×25 px of a screen the export
|
||
declares as 1280×720. A gate that compares a rescaled 1235×695 capture against a
|
||
1280×720 composite measures the compositor.
|
||
|
||
So `--capture` grabs the SubViewport texture: exactly the design rectangle,
|
||
independent of the window, directly comparable with `screen render` with no crop
|
||
and no resample. The windowed run is still worth doing — it is what proves a
|
||
human sees the screen — but it is not what the numbers come from.
|
||
|
||
## P1 gate — the diff, and what it found
|
||
|
||
`tools/verify-screen` renders every screen in the manifest both ways and reports
|
||
the largest per-channel difference anywhere in the frame. Both renderers are held
|
||
to the same inputs: the reference CLI built by `build-reference-cli` from the
|
||
revision the exporter is **pinned** to (not `/reborn/target/`, which is a live
|
||
mount that moves mid-iteration), `--black` because the screen carries its own
|
||
background, and `--primitives --animated` because those are what make the CLI
|
||
draw the same element set the port draws at rest.
|
||
|
||
| screen | build | max per-channel Δ | |
|
||
|---|---|---|---|
|
||
| `main_menu` | 5 | **3** | the P0/P1 gate screen |
|
||
| `main_menu_jp` | 8 | 3 | |
|
||
| `extras` / `extras_jp` | 6 / 9 | 4 / 3 | |
|
||
| `press_start` / `press_start_jp` | 2 / 3 | 1 | |
|
||
| `build_00` / `build_01` | 0 / 1 | 3 | |
|
||
| `build_10` / `build_11` | 10 / 11 | **0** | byte-identical |
|
||
| `title` | 4 | 6 | paint-order tie, below |
|
||
| `title_jp` | 7 | 154 | sampling phase, below |
|
||
|
||
`main_menu` — the milestone's own gate — agrees to **≤3/255 on every channel of
|
||
every pixel**, RMSE 0.38 %, with **no** pixel differing by more than 4 %. 3/255
|
||
is what integer-truncating compositing in the CLI and float rounding on a GPU
|
||
differ by; there is no structural disagreement anywhere in the frame.
|
||
|
||
Three screens exceed that, and each has a named cause rather than a threshold.
|
||
|
||
### `title`: a tie in the paint order — neither renderer is wrong
|
||
|
||
Build 4 is the one screen where the CLI uses a paint order **measured off the
|
||
running game** instead of deriving it. Compared against the order this port
|
||
exports, every single disagreement is **inside a tie** — the two orders differ
|
||
only among elements carrying *identical* layer keys (`0x8083`, the `back2` glow
|
||
group, and `0x80a0`):
|
||
|
||
```
|
||
derived : … 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, …
|
||
measured: … 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, …
|
||
```
|
||
|
||
That is exactly the residual HANDOFF Q3 documents and this export already
|
||
declares in `unresolved: ["paint_order_ties"]`. It is worth stating what it
|
||
costs: **904 px** in the glow band at (445,117)–(1195,313), all of them 4–6/255.
|
||
The port keeps the stable sort, per HANDOFF's own recommendation. Nothing to fix,
|
||
and nothing to tune — a "fix" here would be fitting the port to one screen's
|
||
capture.
|
||
|
||
Two of the reordered indices (`0x80a0`) are `kind & 0x4` template instances that
|
||
both renderers skip, so the only real reorder outside the glow group is
|
||
`ptlogo2` against `ptlogo_tm`, which do not overlap.
|
||
|
||
### `title_jp`: nearest-neighbour sampling phase — the CLI is the one I would call wrong
|
||
|
||
`title_jp` is the **only** screen in the export with a drawn element at a scale
|
||
that is not a whole multiple of 100 %: `ptlogo_eff2` at 125 %. It is also the
|
||
only screen with a difference above 6/255. The two facts are the same fact.
|
||
|
||
At a non-integer ratio the two renderers pick different source texels:
|
||
|
||
* `ui_layout::blit` samples the source at the destination pixel's **top-left
|
||
corner** — `sxi = col * sw / dw`.
|
||
* A GPU samples at the destination pixel's **centre** — `floor((col+0.5)·sw/dw)`.
|
||
|
||
At 125 % those disagree on one column in five, which is why the differing pixels
|
||
are ~30 above 100/255 strung along thin diagonal edges rather than a shifted
|
||
region. At every whole multiple of 100 % they agree exactly, which is why the
|
||
other eleven screens are clean.
|
||
|
||
**Which is wrong:** the CLI, I think. Corner-sampled nearest is a half-
|
||
destination-pixel bias toward the top-left that no rasteriser produces, and the
|
||
Xenon GPU that drew this screen sampled at pixel centres. But I have no
|
||
framebuffer capture of `title_jp` and the disagreement is sub-pixel on one glow,
|
||
so this is a reading, not a measurement — recorded in `docs/BLOCKED.md` rather
|
||
than acted on. **The port is not changing to match**, because matching the CLI
|
||
here would mean deliberately reproducing a half-pixel offset in order to make a
|
||
number smaller.
|
||
|
||
### `extras`: two pixels
|
||
|
||
Two pixels at 4/255. Rounding.
|
||
|
||
### What the diff cannot tell us
|
||
|
||
The pivot question in `docs/BLOCKED.md` predicted that a P1 diff could not
|
||
distinguish "anchor scale to the declared pivot" from "anchor to half the
|
||
texture", because both renderers use the declared pivot. That prediction held:
|
||
the port and the CLI agree on every scaled element, and that agreement is **not
|
||
evidence** about which anchor the game uses. It stays open.
|
||
|
||
### ~~`pteff05.t32` and `pteff04.t32` have no sprite, and that is correct~~
|
||
|
||
**RETRACTED 2026-08-29. This was wrong, and it was the most consequential thing
|
||
on this page.** See "The menu had no background" below.
|
||
|
||
---
|
||
|
||
## P2 — keyframe animation, 2026-08-28
|
||
|
||
### The time unit is authored, in one file, and says loudly that it is not on the disc
|
||
|
||
`authored/timing.json`. HANDOFF Q1 is answered — linear ramp, 2 units per
|
||
rendered frame, 1 unit = 1/60 s — but that conversion is **measured off the
|
||
running game**, not read from a file, which is exactly the case the
|
||
derived/authored split exists for. It is expressed as
|
||
`keyframe_units_per_second: 60` rather than seconds-per-unit so the value is
|
||
exact instead of a repeating decimal, and it carries the two independent lines
|
||
that support it. `t` stays raw everywhere in `export/`; seconds appear only
|
||
where this file is applied, which is one line of `boot.gd`.
|
||
|
||
`exit_ramp_seconds` is deliberately **null**. See below.
|
||
|
||
### The timeline stops at the last *timed* keyframe, and never plays the exit
|
||
|
||
The last keyframe of every group carries **no `t`** — the disc has no time slot
|
||
there. Across this export that final frame is an *exit* pose: for 116 of 134
|
||
elements it differs from the last timed keyframe **in alpha only** (a fade-out),
|
||
for 12 it is the loading splash's scale-and-slide exit, and for 6 it is
|
||
identical (no exit animation at all).
|
||
|
||
So the group is `pre-roll → ramp in → hold → [exit]`, and the port plays it up to
|
||
the hold and stops. Playing into the exit would mean **inventing how long the
|
||
ramp takes**, because the disc does not say. That duration is the screen
|
||
transition — HANDOFF Q7 measured it at ~0.4 s — and it belongs to P3, with its
|
||
own evidence. This is why `exit_ramp_seconds` is null rather than 0.4: P2 has no
|
||
business holding it.
|
||
|
||
### The interpolation is checked by where it lands, not by inspection
|
||
|
||
For **8 of the 12** screens the settled timeline is **byte-identical** to the
|
||
`--pose=rest` render. That is the useful assertion: the port walks the keyframes
|
||
with an authored time unit and arrives, to the pixel, at the pose the pinned
|
||
decoders independently identify as the resting one. `tools/screen-strip` reports
|
||
this per screen, so a change to the interpolation that drifts by one unit shows
|
||
up as a diff rather than as nothing.
|
||
|
||
The four that differ do so for two distinct reasons, below.
|
||
|
||
## `rest` misidentifies six elements, and the running game says so
|
||
|
||
On `main_menu`, the settled timeline and `rest` differ in exactly one region:
|
||
**400×470 at (440,108)** — the bounding box of `ptframe1` and `ptframe2`, and
|
||
nothing else on the screen.
|
||
|
||
`rest` puts both at their **first** keyframe: off-position and fully
|
||
transparent. The keyframes say they slide (620,108)→(440,108) and (403,267)→
|
||
(583,267) while fading 0x00→0xff, and then hold that pose for their last three
|
||
keyframes including the untimed one.
|
||
|
||
`/reborn/docs/re/captures/main-menu-oracle.png`, a capture of the running game,
|
||
**shows them**: the bright circuit-frame bracket around the menu, with a ring at
|
||
the bottom right. Cropping the same 250×180 region from the capture and from
|
||
both renders puts the ring and its elbow trace in the port's timeline render
|
||
**pixel-aligned with the game's**, and absent from the `rest` render. That is
|
||
geometry, not luminance, so it does not depend on the capture's gamma or on the
|
||
fact that it was taken with `NEW GAME` focused.
|
||
|
||
### Why the decoders get it wrong, precisely
|
||
|
||
`ui_layout::rest_plateau` excludes a run of identical keyframes that **ends the
|
||
group**, because that run is normally the exit — the comment cites the pause
|
||
menu, where taking the trailing run erased the word PAUSE. That exclusion is
|
||
right in general and wrong for an element with **no exit animation**, where the
|
||
trailing run *is* the hold. The rule then falls back to an earlier run, which
|
||
for a slide-in is the invisible pre-roll.
|
||
|
||
The condition that identifies the affected elements exactly, with no false
|
||
positives in this export, is:
|
||
|
||
> the final untimed keyframe has the **same pose** as the last timed keyframe
|
||
|
||
Six elements match it and `rest` misses all six: `ptframe1`/`ptframe2` on
|
||
`main_menu` and `main_menu_jp`, and `pteff02` on `title` and `title_jp`. This is
|
||
a **finding for the RE agent** about `sylpheed-formats`, not something this port
|
||
fixes: the decoders are pinned and must not be reimplemented here. The port
|
||
simply does not use `rest` — it derives the arrived pose from the keyframes,
|
||
which needs no heuristic — and `verify-screen` still asks for `--pose=rest` so
|
||
that renderer-vs-renderer diffing compares like with like.
|
||
|
||
Note what this says about P1: the port and the reference renderer **agreed** on
|
||
`main_menu` to 3/255, and both were missing two elements the game draws. Two
|
||
renderers reading the same field through the same decoder agreeing is not
|
||
evidence that the field is right. `docs/BLOCKED.md` had already said that about
|
||
the pivot; here it bit for real.
|
||
|
||
## The title is not settled, and P2 does not claim it
|
||
|
||
`title` and `title_jp` differ between the two modes by much more (max 142 and
|
||
247), and there the disagreement is **not** the six-element bug alone. `rest`
|
||
picks a mid-timeline hold for several glows (`pteff01`, `ptlogoall_eff`,
|
||
`ptlogoall_eff2`, `ptlogo_back2eff5`) where the timeline runs on to a much
|
||
brighter pose.
|
||
|
||
I could not settle which is right, and did not try to make the numbers agree:
|
||
|
||
* No element's alpha ever reverses direction anywhere in this export, so the
|
||
title's 4.48 s timeline is a slow one-way ramp, not a pulse — which removes the
|
||
obvious reason to expect a loop, but does not prove there is none.
|
||
* The only live title capture composites the **`PRESS Ⓐ` plate (build 2) over
|
||
the title (build 4)**, so it cannot be diffed against build 4 alone. Mean
|
||
luminance is oracle 64.1, `rest` 62.8, timeline 80.0 — which looks like it
|
||
favours `rest`, except that the plate *adds* brightness and `rest` is carrying
|
||
a 25 % black dim quad (`pteff02`) that is itself one of the six misidentified
|
||
elements. The comparison is confounded in both directions and settles nothing.
|
||
* **Both modes are visibly wrong anyway.** Side by side with the capture, the
|
||
port draws a washed-out cyan glow slab across the logo that the running game
|
||
does not have — in `rest` mode too. That is a third problem, independent of
|
||
this one, and it is P3's.
|
||
|
||
So: the timeline is the default because it is derived from the disc's own
|
||
keyframes with one measured constant and no heuristic, and because it is proven
|
||
right on the screen this milestone gates. On the title it is **unverified**, and
|
||
P3 should not assume P2 settled it.
|
||
|
||
---
|
||
|
||
## P2, corrected — the pin moved, and the settle rule was wrong, 2026-08-28
|
||
|
||
### Answering the RE agent's question: which six, and on what screens
|
||
|
||
They asked, having found only two elements on the English main menu satisfying
|
||
the condition this port proposed. The six span the whole 12-screen export:
|
||
|
||
| element | screens | trailing run |
|
||
|---|---|---|
|
||
| `ptframe1`, `ptframe2` | `main_menu`, `main_menu_jp` | alpha `0xff` — **visible** |
|
||
| `pteff02` | `title`, `title_jp` | alpha `0x00` — **transparent** |
|
||
|
||
So four of the six are the pair they already found, once per language build, and
|
||
their alpha rule accepts exactly those. The other two are `pteff02`, whose
|
||
trailing run is transparent, so their rule **excludes** it and leaves `rest` at
|
||
`0x40`.
|
||
|
||
**That exclusion is right, and their own measurement proves it.** `pteff02` is
|
||
the 25 % dim quad; they measured the title render going from **+13.14 to +0.55**
|
||
against the plate-free capture once the dim is drawn. `rest` must therefore stay
|
||
at `0x40` and must *not* move to the transparent trailing run — which is what
|
||
their rule does. Two investigations converging from opposite directions.
|
||
|
||
The condition this port proposed was **too loose**; the alpha discriminator is
|
||
the correct rule and the port has no amendment to offer.
|
||
|
||
### The pin moved 8b6dbcf → 5414db3
|
||
|
||
Its own commit, and what I wanted from it is the fixed `ui_layout::rest()`.
|
||
Pinned at `5414db3` rather than `4bc9706` where the fix was written, because
|
||
`5414db3` is where it carries its disc-wide check — 30 of 13 991 elements move,
|
||
4 become visible, **0 become invisible**.
|
||
|
||
The re-export is the evidence the change was contained: **two files changed, and
|
||
within them exactly four `rest` blocks** — `ptframe1`/`ptframe2` on both main
|
||
menus moving from `(620,108)/(403,267)` at `t=16` and alpha `0x00` to
|
||
`(440,108)/(583,267)` at `t=62` and alpha `0xff`. Every diff line pairs; the
|
||
other ten screens are byte-identical, `pteff02` did not move, and no sprite
|
||
changed.
|
||
|
||
### The settle rule was wrong, and their title finding is what showed it
|
||
|
||
P2 shipped "hold the last **timed** keyframe", on the reasoning that the exit is
|
||
the final untimed frame. **That is wrong**, and the title is the counter-example:
|
||
`pteff02` holds at `t=46` with the dim at alpha `0x40` and then ramps to `0x00`
|
||
by `t=236`. The exit is not only the untimed frame — it can be a long run of
|
||
timed ones. Running to the end drops the dim and makes the whole screen ~13/255
|
||
too bright, which is exactly the luminance excess P2 recorded (oracle 64.1,
|
||
`rest` 62.8, timeline 80.0) and could not explain.
|
||
|
||
A group is `pre-roll → ramp in → hold → ramp out → post-roll`, and a screen that
|
||
has arrived sits on **the hold**. So the timeline now plays in and stops at
|
||
`rest`, which is the decoders' identification of that hold and carries its own
|
||
`t`. `settle_units()` is `rest.t`.
|
||
|
||
The check is that the disagreement vanishes: on **all twelve** screens the
|
||
settled timeline is now byte-identical to the `--pose=rest` render, where before
|
||
this change four of them differed by up to 247/255. The timeline's endpoint
|
||
*should* be `rest` — the animation is what the timeline adds, not a different
|
||
destination — so this is the property to want, and it now holds without a
|
||
special case.
|
||
|
||
That also retires P2's open question about looping, from the other side: the RE
|
||
agent measured that groups hold rather than loop (`ptloop01`/`ptloop02` park
|
||
off-screen at x=1521 and x=−839; 18 s of settled title sits at sd ≤ 0.01).
|
||
|
||
## The reference renderer was stale for three diff runs
|
||
|
||
Worth recording as a process failure, because it defeated the project's whole
|
||
verification method for a while and it failed *silently*.
|
||
|
||
After bumping the pin I rebuilt the reference CLI, and `build-reference-cli`
|
||
reported success at rev `5414db3`. `verify-screen` then showed `main_menu`
|
||
jumping from 3/255 to **72/255**. The natural reading — the port had regressed —
|
||
was wrong. The port was right and **the reference was a revision behind**: the
|
||
shared `CARGO_TARGET_DIR` still held a `sylpheed-cli` built from `8b6dbcf`, and
|
||
cargo reported `Finished in 0.13s` and left it in place. Building into a clean
|
||
target directory produced a binary that resolves `ptframe1` to `(440,108) t=62`;
|
||
the shared one still said `(620,108) t=16`.
|
||
|
||
The old check — "does `screen list` run?" — cannot catch this, because a stale
|
||
binary runs perfectly.
|
||
|
||
Two changes:
|
||
|
||
* `build-reference-cli` builds into `$CARGO_TARGET_DIR/reference-cli/$rev`, a
|
||
tree **keyed by the pinned revision**, so a new pin has no artifacts to reuse.
|
||
A stable copy is placed alongside for consumers.
|
||
* It then checks the binary **against `export/`**: both come from the same pin,
|
||
so if the CLI resolves `ptframe1`'s rest differently from what the exporter
|
||
wrote, the two halves of the verification are not the same revision and it
|
||
fails loudly. It compares the two rather than asserting a literal, so it stays
|
||
true when the pin moves again.
|
||
|
||
`docker/bin/` is baked into the image, so this takes effect on the next image
|
||
build; until then the repo copy has to be invoked by path. The RE agent hit the
|
||
same class of trap this session from the other side (`./target/debug` stale
|
||
against a redirected `CARGO_TARGET_DIR`). It is worth naming the general shape:
|
||
**a build system reporting success is not evidence that the artifact you are
|
||
about to trust is the code you pinned.**
|
||
|
||
### What this did not change
|
||
|
||
`title` (6/255), `extras` (4/255) and `title_jp` (154/255) are unchanged, and
|
||
their diagnoses stand — a paint-order tie, two pixels, and nearest-neighbour
|
||
sampling phase at 125 % scale. The title's swoosh defect the RE agent localised
|
||
(drawn thick and white where the game draws it thin and pink) is untouched by
|
||
any of this and remains P3's.
|
||
|
||
---
|
||
|
||
## The menu had no background, and P1 called that correct, 2026-08-29
|
||
|
||
The pin moved `5414db3 → f817dd5` for `56cc7ac`, "a RATC child's name is stated,
|
||
not inferred". `ratc::parse` had named each child by scanning backwards for the
|
||
last printable run of bytes before its magic. For `pteff05.t32` the three
|
||
trailing payload bytes are `38 41 58` — `8AX` — which beat the real name, so the
|
||
child registered under a name no element declares and resolved to no sprite.
|
||
|
||
`pteff05.t32` is the **full-resolution background of all five menu screens**.
|
||
|
||
So every render this port has produced of a menu screen has been missing its
|
||
background, and P1 wrote that up as a property of the disc: *"the bundle declares
|
||
them and carries zero RATC children for either, so there is no texture on the
|
||
disc to export."* That sentence was false. The bundle carries the child; the
|
||
decoder was handing back the wrong name for it. Retracted above rather than
|
||
edited away.
|
||
|
||
### What the re-export shows
|
||
|
||
Six new sprites and nothing else: `pteff05.png` on `main_menu`, `extras` and
|
||
their Japanese twins, `pteff04.png` on both titles. Per screen the JSON gains a
|
||
`sprite` line and `layer_source` moves `"implied" → "sprite"` — the layer key is
|
||
now **read from the file** instead of taken from the decoders' table of keys
|
||
measured off the running game. That is the derived/authored ratchet turning the
|
||
right way, in the exporter rather than in `authored/`.
|
||
|
||
`pteff05.png` is **1280×720**; `ptbase.png`, which had been carrying the
|
||
background alone, is 640×360 drawn at 200 %. The screen was being shown its own
|
||
art at half resolution.
|
||
|
||
### Measured against the live capture, not against the other renderer
|
||
|
||
Whole-frame RMSE of the settled `main_menu` against
|
||
`captures/main-menu-oracle.png`:
|
||
|
||
| | RMSE |
|
||
|---|---|
|
||
| before this pin | 8.05 % |
|
||
| with the real background | **5.92 %** |
|
||
|
||
A 26 % reduction, and it is the right kind of evidence: the reference renderer
|
||
was missing the same element for the same reason, so a renderer-vs-renderer diff
|
||
could not have found this. It is the third time on this project that the
|
||
capture caught something both renderers agreed on — the bracket, the title dim
|
||
quad, and now the background.
|
||
|
||
`verify-screen` after the bump is unchanged in character: everything at 3–4/255
|
||
except `title` (6, the paint-order tie) and `title_jp` (155, the sampling phase).
|
||
Both renderers gained the background together.
|
||
|
||
### One thing the comparison says that I did not expect
|
||
|
||
Rendering with `--focus=ptbtn01`, which is how the capture was taken, makes the
|
||
RMSE **worse** — 5.92 % → 7.00 %. The port *replaces* an element's sprite with
|
||
its `*f` twin; `sylpheed-cli`'s own `--focus` is documented as drawing the
|
||
focused record **over** the base element. Those are different operations, and
|
||
the capture shows a ring marker beside `NEW GAME` that the port does not draw.
|
||
|
||
This is P5's, not P2's, and it is not being guessed at here. Raised in
|
||
`docs/BLOCKED.md`.
|
||
|
||
---
|
||
|
||
## P3 — splash → title, unattended, 2026-08-29
|
||
|
||
### The splash is located by entry index, because no rule can find it
|
||
|
||
The RE agent looked for a content predicate and there is none: design size fails
|
||
(every extra composable bundle sampled is 1280×720, the same as every screen) and
|
||
element count fails (fragments run 2…15 elements in `GP_OPTIONS`/`GP_SAVE_LOAD`
|
||
while the splash halves are 3 and 7 — the ranges overlap).
|
||
|
||
So `screen_builds` is now `is_build` **plus an authored allow-list of entry
|
||
indices**, in `authored/screen_names.json` under `also_export`, each with a `why`
|
||
that says it is a locator and not a claim. This is safe in `GP_TITLE` and would
|
||
not be in general: there, widening adds exactly four bundles and all four are
|
||
real screens with zero fragments. That is why it is an allow-list rather than a
|
||
loosened predicate.
|
||
|
||
**There were two splash screens and the port had neither.** Entries 11/14 are the
|
||
developer logos (GAME ARTS / SETA / studio anima); entries **10/13 are the SQUARE
|
||
ENIX publisher wordmark, the first thing the boot shows**, and nothing in this
|
||
project had noticed them. Both pairs are region twins — ™ on 10, ® on 13 — and
|
||
the port shows one of each, not both.
|
||
|
||
### `authored/screen_names.json` is now keyed by pak entry, not by ordinal
|
||
|
||
Widening the enumeration renumbers the ordinals, and a name that moves when the
|
||
enumeration rule changes is not a name. The file had always called the entry
|
||
"the stronger locator"; it is now the only stable one. In `GP_TITLE` the two
|
||
coincide across all 16 entries, which is also the numbering `sylpheed-cli screen
|
||
--build N --all` takes — so `verify-screen` now passes `--all`, and without it
|
||
`--build 10` would have landed on entry 12.
|
||
|
||
The two previously-unnamed plates therefore renamed `build_10`/`build_11` →
|
||
`build_12`/`build_15`. Their names were always locators; now they locate the
|
||
right thing.
|
||
|
||
### The exit is the group playing itself out, not a black rect over a freeze
|
||
|
||
HANDOFF's answer to ask 2 was (a), and it came with a test that discriminates
|
||
rather than a plausibility argument. Under "a black quad over a frozen screen"
|
||
every region is scaled by the same 1−α, so the button-region / background-region
|
||
brightness **ratio** stays constant through the fade. Measured, it falls
|
||
6.495 → 5.574 → 3.105 → 2.125 → 1.935 — a 3.4× monotonic drop. The screen plays
|
||
out: `pteff00.prm` ramps to opaque black while the labels, `ptmsg`, `pteff10`
|
||
and `pteff12` ramp to transparent, and `ptframe1`/`ptframe2` hold.
|
||
|
||
Implemented by giving the final untimed keyframe a **synthetic time**,
|
||
`exit_ramp_units` after the last timed one, and then interpolating it like any
|
||
other. One code path: the difference between arriving and leaving is only how far
|
||
`t` is allowed to run, not a second kind of animation.
|
||
|
||
`exit_ramp_units = 24` (~0.4 s) is authored, and `authored/timing.json` carries
|
||
the RE agent's own reach caveat rather than smoothing it: the filmstrip is
|
||
downsampled and the button region contains some background, so this pins the
|
||
**direction**, not 0.4 s to ±0.05 s, and it is one transition pair.
|
||
|
||
### Nothing waits on a timer the disc does not carry
|
||
|
||
`dwell` in `authored/flow.json` is deliberately empty. Each screen's dwell is its
|
||
own keyframe group — the publisher wordmark reaches its hold at t=235 (3.92 s),
|
||
the developer logos at t=190 (3.17 s), both read from the disc. Adding a hold on
|
||
top would be inventing a number nobody measured. The pacing you see is the
|
||
disc's own, and the file says where a measured number would go.
|
||
|
||
### The last screen holds
|
||
|
||
A screen plays itself out because something is taking its place. Nothing takes
|
||
the title's place yet, so the sequencer holds there. A boot that ends by fading
|
||
to black is a boot that looks like it crashed. P4 puts the intro video in front
|
||
of the title and P5 gives the title somewhere to go.
|
||
|
||
### `flow.json` reproduces an observation and says so
|
||
|
||
Q6 closed with a negative: the order is in none of the four places it could have
|
||
been, and a transition is a call with a name argument chosen by code. So this
|
||
file is authored and its header says plainly that it reproduces what was watched,
|
||
not what any file states. The intro video's place in the real boot is **named as
|
||
a gap** rather than the order being quietly rewritten to hide it.
|
||
|
||
## P3 gate
|
||
|
||
`godot --path port -- --boot --film=/tmp/boot` runs unattended:
|
||
|
||
```
|
||
publisher_logo → developer_logos at 4.65 s → title at 8.57 s
|
||
boot sequence complete after 13.05 s, holding on title
|
||
```
|
||
|
||
The filmstrip shows each screen fading in, holding, and fading through black into
|
||
the next, and the title staying up. `verify-screen` covers all **16** screens
|
||
now; the four new splash bundles come in at max 1–2/255 against the reference
|
||
renderer. The three known differences are unchanged: `title` 6 (paint-order tie),
|
||
`main_menu` 4, `title_jp` 155 (sampling phase at 125 % scale).
|
||
|
||
## Answers taken from the RE agent without re-deriving them
|
||
|
||
* **Focus stays "replace".** Over-vs-instead is unobservable: the focused sprite
|
||
covers the base at 100 % of base-visible pixels, and the two compositions
|
||
differ by RMSE 1.1 inside the button rect — under the gamma floor. The port's
|
||
guess was right for the wrong reason, and the actual gap is that
|
||
`ptbtn0Nf.rat` declares **two** sprites — `ptbtneff01.t32`, a glowing ring, and
|
||
then the bright label — where `ptbtn0N.rat` declares one. The ring is P5's, and
|
||
its placement inside the record is **not decoded**, so it will be authored from
|
||
the capture and marked as such.
|
||
* **RMSE against captures has a floor, so stop chasing it.** The capture is
|
||
`≈ 255·(render/255)^γ` with γ ≈ 1.49 on the menu and `EXTRAS`, 1.34 on the
|
||
title, and it is a ramp *the game installed* (`VdGetCurrentDisplayGamma` at
|
||
video init), not a capture-path artefact to subtract. Its reach is narrow —
|
||
the flat patches it was fitted on are almost all dark — so the port will not
|
||
extrapolate it across the range, and will not apply it to rendered output on
|
||
this evidence. It is a comparison constant, not a rendering one.
|
||
* **Rotation is escalated to a human and the port has not acted.** The RE half is
|
||
answered — rotate about the **declared pivot**, measured against the GPU
|
||
capture — and it has zero effect on the five screens at rest. The port will
|
||
carry `rotation_deg` in a future FORMAT v3 because carrying a decoded field the
|
||
renderer ignores beats dropping it, but it will not draw it until the
|
||
divergence question is settled.
|
||
|
||
---
|
||
|
||
## P4 — the intro video, 2026-08-29
|
||
|
||
### Theora at 720p is fine here, and no runtime dependency is requested
|
||
|
||
MISSION §6 anticipated that Theora might be too poor at 720p and permitted the
|
||
FFmpeg-GDExtension fallback to be **proposed**. It is not needed, and this was
|
||
measured rather than judged by eye alone. SSIM against the decoded source over a
|
||
10 s sample: **0.9863 at `-q:v 6`, 0.9896 at 8, 0.9924 at 10**. At 200 % zoom on
|
||
the reel's hardest case — fine serif text and soft gradients over near-black,
|
||
where Theora breaks first — q8 is indistinguishable from the source.
|
||
|
||
`-q:v 8`, and **no GDExtension is being proposed or adopted**.
|
||
|
||
`-ac 2` because the source is **6-channel** WMA Pro and Godot's Theora path is
|
||
not a surround one. That downmix is a decision, so it lives in the recorded
|
||
command where a modder can see and change it rather than in prose.
|
||
|
||
### The exact command is in the manifest, per MISSION §6
|
||
|
||
`export/manifest.json` gains a `videos` array, each entry carrying the verbatim
|
||
`ffmpeg` line that produced it. A modder who dislikes the quality re-runs one
|
||
line instead of reverse-engineering what was done to their video — which is the
|
||
whole reason this project converts the disc rather than reading it at runtime.
|
||
|
||
### A cache, and why that is not a hand-edit
|
||
|
||
`export/` is regenerated wholesale, but re-encoding 232 s of video on every run
|
||
costs ~4 minutes to produce a byte-identical file, and an exporter nobody re-runs
|
||
is worse than a cache. So each movie gets a `.cmd` sidecar recording the command
|
||
and the source size, and the encode is skipped only when both match exactly. Any
|
||
change to either re-encodes. This is derived state validating derived state, not
|
||
a hand-edit.
|
||
|
||
### The player renders into the design viewport, not beside it
|
||
|
||
First attempt parented the `VideoStreamPlayer` to the Boot node. It played, and
|
||
every captured frame was **black**: the capture reads the SubViewport, and the
|
||
player was rendering to the window. Worth stating as more than a capture bug —
|
||
everything this port draws composes in the export's own 1280×720 design space,
|
||
and a movie outside that space is outside the coordinate system every screen is
|
||
expressed in.
|
||
|
||
### Ⓐ skips, because Q9 measured it
|
||
|
||
The only input the port handles so far. HANDOFF Q9: one Ⓐ press skips a movie,
|
||
measured — the title was reached at 57 s against a 193 s baseline. Menu
|
||
navigation is still P5.
|
||
|
||
## P4 gate
|
||
|
||
`godot --path port -- --boot --film=…` runs
|
||
`publisher_logo → developer_logos → ADV.ogv → title`, unattended. The filmstrip
|
||
shows the SQUARE ENIX ident, then the reel's live-action-styled CG, then the
|
||
title. The movie's place in the boot is **measured, not decoded** — Q9 decodes
|
||
`ADVERTISE_MOVIE → ADV.wmv` from the movie manifest, but *where it sits in the
|
||
boot order* is what the RE agent watched, and `authored/flow.json` says so.
|
||
|
||
### What I cannot verify from here
|
||
|
||
**Audible playback.** This container has no audio device — Godot falls back to
|
||
the dummy driver. What is verified is that the Vorbis stream exists in the
|
||
transcode, is 2-channel, and decodes. Whether Godot emits it audibly is
|
||
unconfirmed and is stated as unconfirmed rather than assumed from the stream's
|
||
presence. It is a cheap check for anyone with a sound device and an impossible
|
||
one here.
|
||
|
||
---
|
||
|
||
## RETRACTION — `sylpheed-cli` is not the oracle, 2026-08-29
|
||
|
||
**This corrects a framing that runs through everything above, so it is a
|
||
retraction rather than an edit.** Every place this file called
|
||
`sylpheed-cli screen render` *"the reference renderer"* — and it does so
|
||
repeatedly, starting at P1 — overstated what it is.
|
||
|
||
The correction comes from the human, via the RE agent, in their words: Reborn
|
||
"was/is just a GUI explorer and extraction CLI for verifying the decoding of the
|
||
various files. It may very well be wrong." **The oracle is the Xenia Canary
|
||
capture and the game.**
|
||
|
||
So `tools/verify-screen` is a **consistency check between two decoders that
|
||
share their assumptions**, and a regression detector. It is not a correctness
|
||
check, and agreement in it is not evidence of correctness.
|
||
|
||
### The embarrassing part is that this file already knew
|
||
|
||
After the `ptframe1` case, P2's write-up says: *"Two renderers reading one field
|
||
through one decoder agreeing is not evidence that the field is right."* Then P1's
|
||
numbers kept being quoted as though 3/255 against `sylpheed-cli` meant the port
|
||
was right. Having the principle written down did not stop me leaning on the
|
||
agreement — which is worth recording, because that is the failure mode, not
|
||
ignorance of the principle.
|
||
|
||
**Three times** both renderers agreed and both were wrong, all three caught by a
|
||
capture and catchable by nothing else:
|
||
|
||
| | what both got wrong | how it surfaced |
|
||
|---|---|---|
|
||
| `pteff05` | the menu screens had **no background** | the RE agent decoded the RATC child name |
|
||
| scale 0 | drawn at full size instead of collapsed | RE agent's control run |
|
||
| `rest()` | `ptframe1`/`ptframe2` invisible; the menu bracket missing | `main-menu-oracle.png` |
|
||
|
||
### What changes
|
||
|
||
* `tools/verify-screen` says all of this in its own header, calls the CLI the
|
||
**comparison** renderer, and a `DIFFERS` row now means "we moved apart, find
|
||
out which of us moved" rather than "the port is wrong".
|
||
* The correctness question moves to the captures. The RE agent has committed
|
||
nine of them with an index at `docs/re/captures/ORACLE-CAPTURES.md`, covering
|
||
all five screens in scope — including a **main menu with `OPTIONS` focused**,
|
||
whose difference from the unfocused menu isolates exactly what focus changes.
|
||
* Three cautions travel with any capture comparison, and they are the RE agent's:
|
||
the captures are **not gamma-neutral** (γ ≈ 1.49 menu, 1.34 title — there is a
|
||
floor, do not chase it); **geometry is sound** (best alignment 0,0 at corr
|
||
0.9466, so a positional disagreement is real); and each is **one moment of a
|
||
still-animating screen**, so compare settled poses or regions known to be at
|
||
rest.
|
||
|
||
### What does not change
|
||
|
||
The port keeps running `verify-screen` over all 16 screens every iteration. A
|
||
consistency check is still worth having — it is total, it is cheap, and it is
|
||
what catches a divergence the RE agent introduces on their side. It is simply
|
||
not a grade, and this file will stop quoting it as one.
|
||
|
||
## P5 groundwork — the focus record, checked against a capture, 2026-08-29
|
||
|
||
P5 is the lowest unfinished milestone (P0–P4 are gated above). This iteration did
|
||
not implement navigation; it did the measurement P5 needs first, because the one
|
||
thing P5 is built on — how a focused button is drawn — had three claims attached
|
||
to it and none of them had been checked from this side.
|
||
|
||
### The exporter already emits the focus record's second element
|
||
|
||
HANDOFF ask 3 answers with a 🔴: *"what you are actually missing is the focus
|
||
record's SECOND element"* — `ptbtneff01.t32`, a 42×46 glowing ring, declared
|
||
before the bright label in `ptbtn0Nf.rat`.
|
||
|
||
**That gap is in the renderer, not the exporter.** `export/screens/title/main_menu.json`
|
||
already carries both, in declaration order, under `focus.elements`, for all five
|
||
buttons — `ptbtneff01` then `ptbtn0Nf`, each with its own pivot, rest pose and
|
||
keyframes. Nothing needs to change in `crates/sylpheed-export` for the ring. What
|
||
is missing is that `screen_view.gd` draws only one sprite per focused button.
|
||
Recording this so P5 does not re-open the exporter looking for it.
|
||
|
||
The ring's size checks out exactly: `ptbtneff01.png` is **42×46**, as stated.
|
||
|
||
### The `(7,7)` focus offset survives a refutation attempt, uniquely
|
||
|
||
Ask 3 states the focused sprite covers the base at 100.0 % of base-visible pixels
|
||
"once aligned properly (the true offset is **(7,7)**, and at the centre alignment
|
||
it reads a misleading 78–84 %)". P5 builds directly on this, so it was worth
|
||
attacking.
|
||
|
||
Re-ran the RE agent's own metric on the exported PNGs — over every pixel where the
|
||
base sprite is visible, the fraction where the focus sprite's alpha ≥ the base's —
|
||
scanning the whole offset space, not just the stated answer:
|
||
|
||
| alignment | ptbtn01 | ptbtn02 | ptbtn03 | ptbtn04 | ptbtn05 |
|
||
|---|---|---|---|---|---|
|
||
| **(7,7)** | **100.00 %** | **100.00 %** | **100.00 %** | **100.00 %** | **100.00 %** |
|
||
| geometric centre | 80.58 % | 79.20 % | 79.58 % | 79.45 % | 79.45 % |
|
||
| pivot-to-pivot | 80.58 % | 86.59 % | 87.40 % | 79.45 % | 84.58 % |
|
||
|
||
**The refutation fails, and more strongly than the original claim.** Over a
|
||
15×14 offset scan, `(7,7)` is a *unique isolated cell* at 100 % on every one of
|
||
the five buttons — every neighbouring offset, including (6,6) and (7,6), falls
|
||
below 90 %. The centre and pivot alignments reproduce the 78–84 % band the RE
|
||
agent reported as misleading. A claim that survives a search of its whole
|
||
parameter space is worth more than one checked at a single point, so this is
|
||
recorded as strengthened, not merely unrefuted.
|
||
|
||
### `(7,7)` is not a constant to apply — it is what the declared positions already say
|
||
|
||
An earlier version of this analysis had the port disagreeing with the capture by
|
||
6 px. **That was my arithmetic error and it is worth writing down**, because it is
|
||
the mistake this format invites: I computed each element's top-left as
|
||
`pos - pivot`, which gives base→focus deltas of (13,13) and a 29 % coverage — a
|
||
confident wrong number.
|
||
|
||
`pos` **is** the top-left. `screen_view.gd:121` is
|
||
`Rect2(pos - pivot*(s - 1), natural*s)`: the pivot is the anchor scale grows
|
||
about and it *cancels at 100 %*, which is exactly the "can be got wrong
|
||
invisibly" the comment there warns about. Getting it wrong invisibly is what
|
||
happened.
|
||
|
||
With `pos` as the top-left, base − focus is `(542,162) - (535,155)` = **(7,7)**
|
||
directly, on four of the five buttons. So P5 draws each focus element at its own
|
||
declared `pos` and needs no offset constant at all. Nothing to author.
|
||
|
||
### The one real find: `ptbtn04` is 1 px off the grid on the disc
|
||
|
||
The focus records sit on a clean 80 px pitch — 155, 235, 315, 395, 475. The
|
||
**base** records do not: 162, 242, 322, **401**, 482, i.e. spacings 80, 80, **79**,
|
||
**81**. So `ptbtn04`'s declared base→focus delta is **(7,6)**, while the art
|
||
itself aligns at (7,7) — the coverage scan puts `ptbtn04` at 100 % on (7,7) and
|
||
below 90 % on (7,6), the same as every other button.
|
||
|
||
This is 1 px of authoring jitter on the disc, not a decode error, and it has one
|
||
consequence worth stating: **do not derive the focus placement from the base by a
|
||
constant.** Draw the focus record at its own declared `pos`. A port that
|
||
"simplified" this to base + (7,7) would put `ptbtn04`'s focus art 1 px off, and
|
||
would look right on the other four.
|
||
|
||
### Verified against a capture, not against our other renderer
|
||
|
||
Diffing two oracle frames isolates what focus changes without any instrument in
|
||
the path: `live-main-menu.png` vs `live-main-menu-options-focused.png` differ in
|
||
one tight cluster of 6 338 px at **x 506..702, y 398..445**. `ptbtn04` is the
|
||
`OPTIONS` button, and the union of its focus record under the `pos`-as-top-left
|
||
reading — ring `ptbtneff01` at (500,396) 42×46 plus label `ptbtn04f` at (535,395)
|
||
172×56 — is **x 500..706, y 395..451**. Those agree on all four edges to within a
|
||
few px of near-transparent sprite border.
|
||
|
||
Under the `pos - pivot` reading the same record predicts x 433..604, y 367..422,
|
||
which matches nothing in the capture — and *no* button matches that cluster. The
|
||
capture, not our renderer, is what settles it.
|
||
|
||
### An instrument that failed its own control, and was therefore discarded
|
||
|
||
To locate the buttons independently I wrote a masked normalised-cross-correlation
|
||
template matcher and ran it as PROTOCOL requires — **through a control first**:
|
||
match each *base* sprite against the *plain* capture, where the declared position
|
||
is known and the answer must be a (0,0) delta.
|
||
|
||
It returned deltas of (13,5), (6,−19), (12,21), (−16,22), (6,8) at NCC
|
||
**0.096–0.206** — noise, with three of five pinned to the ±22 search boundary.
|
||
The control fails, so the instrument is dead rather than tuneable, and **none of
|
||
its output is used above.** The button art is dark, low-contrast and further
|
||
crushed by the capture's γ ≈ 1.49 ramp, which is the likely cause; a matcher for
|
||
this corpus would have to work on gradients rather than luminance. Filed so the
|
||
next iteration does not rebuild the same broken tool.
|
||
|
||
### What P5 still needs, and has not got
|
||
|
||
* **Initial focus is not stable across boots** (Q5: 2× `TUTORIAL`, 2× `NEW GAME`).
|
||
That is a value to author, with a `why` naming Q5 — it is not written yet.
|
||
* **The ring's own animation is unread.** `ptbtneff01`'s two keyframes go
|
||
`rotation_deg` 0 → **360** at t=120 with no second timed keyframe, i.e. a full
|
||
turn. Whether it spins continuously while focused, or turns once and holds, is
|
||
the group-loop question again — answered "groups hold" for build-in animations,
|
||
but a 360° hold and a 0° hold are the same pose, so *this* group cannot be told
|
||
apart by its rest pose. Not guessed; raised below.
|
||
|
||
---
|
||
|
||
## P5 — navigation, 2026-08-29
|
||
|
||
The gate is *"a human clicks through it"*, and the artifact is a walk that
|
||
proves the wiring rather than the intent: `up` (which wraps 01→05), five `down`,
|
||
Ⓐ into `EXTRAS`, `down`, Ⓑ back — landing on the main menu with focus restored
|
||
to `EXTRAS`.
|
||
|
||
```sh
|
||
xvfb-run -a godot --path port -- --menu \
|
||
--script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5
|
||
```
|
||
|
||
Ten PNGs, one per step, each taken after the screen it produced had settled.
|
||
Contact sheet handed over as `share` id `1788002507-ef4468a0a33a`.
|
||
|
||
### The scripted walk goes through the input system, not around it
|
||
|
||
`--script` posts `InputEventAction` through `Input.parse_input_event` and lets
|
||
it arrive at `_unhandled_input` exactly as a d-pad's press would. Calling
|
||
`MenuFlow.move()`/`accept()`/`cancel()` directly would have been shorter and
|
||
would have proved nothing: the thing most likely to be broken is the wiring
|
||
between a press and the cursor, and a direct call is precisely the part that
|
||
skips it. The same reasoning says the settle wait must be real — a shot taken
|
||
before the screen stops moving photographs a fade and calls it a menu.
|
||
|
||
### What is authored here, and what is derived
|
||
|
||
Split deliberately, because P5 is where the two are easiest to blur:
|
||
|
||
| | where | why |
|
||
|---|---|---|
|
||
| the ORDER of the items | **derived** — each screen file's `buttons`, filled by the exporter from the button-role elements sorted by resting Y | it is on the disc |
|
||
| where an item goes | **authored** — `authored/flow.json` | HANDOFF Q4 *measured* the destinations; they are not in the file |
|
||
| which item opens focused | **authored** | Q5 measured that it is **not stable boot to boot** |
|
||
| what Ⓑ does | **authored** | Q5, measured — except on the main menu, see below |
|
||
| ⬅➡ do nothing | **authored**, written as an explicit no-op | so that *"the game ignores it"* and *"we never wired it"* are different lines of code |
|
||
|
||
Four of the five main-menu destinations are `goto: null` with a `blocked` note.
|
||
That is **not** an unknown: `DIFFICULTY`, the save-slot list, the lesson list and
|
||
the settings menu were all measured, and they live in archives this export does
|
||
not carry. `blocked` and `none` are kept apart so a later reader does not
|
||
"discover" a gap that was a milestone boundary.
|
||
|
||
`EXTRAS` is the only main-menu destination inside `GP_TITLE`, and therefore the
|
||
only Ⓐ-into-a-submenu this gate can actually walk.
|
||
|
||
### The one navigation rule with nothing behind it
|
||
|
||
Ⓑ on the **main menu** → title. HANDOFF Q5 states it, and `flow.json` marks it
|
||
*authored — likely but UNPROVEN*, because the title also self-returns after
|
||
~8–10 s idle and a single unrecorded observation cannot separate the two. The
|
||
port implements it anyway — a menu with no way out is worse than a menu with a
|
||
plausible one — and says in the file that it did. Asked of the Decoder this
|
||
iteration; see `BLOCKED.md`.
|
||
|
||
Independent corroboration that the main menu is different from its submenu:
|
||
the main menu's footer advertises only `Ⓐ : OK`, while `EXTRAS`' footer
|
||
advertises `Ⓑ : Back`. That is on the disc, in `ptmsg.png` vs `ptmsg2.png`, and
|
||
it is visible in both the port's render and the captures.
|
||
|
||
### A press during a fade is dropped
|
||
|
||
**Authored, and not measured.** Nobody has watched what the game does with a
|
||
button pressed mid-transition. Dropping invents less than queueing does: it
|
||
cannot manufacture a press the game might have discarded. `flow.json` says so
|
||
under `navigation.input_during_transition`.
|
||
|
||
---
|
||
|
||
## `--headless` cannot draw, and the port hung instead of saying so, 2026-08-29
|
||
|
||
`docs/port/PORT-MISSION.md` and the loop prompt both name `godot-headless` as
|
||
how this project runs unattended. It does not work, and the way it failed was
|
||
the worst available shape.
|
||
|
||
**Measured, not assumed.** Under `--headless` Godot's dummy renderer never emits
|
||
`RenderingServer.frame_post_draw`. Every capture path in `boot.gd` awaits it —
|
||
`--capture` since P1, `--film` since P3, `--shots` as of this milestone — so all
|
||
three blocked forever. Isolated by the difference between two runs:
|
||
|
||
```
|
||
godot --headless --path port --quit # prints, exits 0
|
||
godot --headless --path port -- --screen=… --capture=… # no output at all, killed at 40 s
|
||
```
|
||
|
||
The second produces **zero bytes of output** before it is killed, because
|
||
Godot's stdout is block-buffered and never flushes. So the observable behaviour
|
||
of an unattended headless capture was: silence, forever. In a loop, a job that
|
||
waits reads as a job still working — this is the failure mode that costs a whole
|
||
iteration and leaves nothing behind to say what happened.
|
||
|
||
Two changes, and deliberately not one:
|
||
|
||
* `--capture`, `--film` and `--shots` **refuse at startup** under `--headless`,
|
||
naming the flag and printing the `xvfb-run` line that does work. Refusing
|
||
early rather than at the first frame means the run does not die halfway
|
||
through a filmstrip with some frames written.
|
||
* `--script` **no longer waits for a drawn frame when it is not going to
|
||
photograph one.** Navigation is checkable where nothing draws, and that is
|
||
worth keeping: `godot --headless --path port -- --menu --script=…` now walks
|
||
the menus and exits 0 in about four seconds, which is a cheap regression check
|
||
that needs no X server at all.
|
||
|
||
The Xvfb path is unchanged and is what produced the P5 artifact.
|
||
|
||
---
|
||
|
||
## Refutation — the focus ring IS drawn rotated, and it is not at 0° in either capture
|
||
|
||
Attempted against the Decoder's `7eeae30` (*"re(ui): the focus ring SPINS, the
|
||
game draws it, and the leaf owns the f record"*), point 2: that in the
|
||
OPTIONS-focused capture the ring's bright head sits in a different angular
|
||
position from the sprite's own, caught mid-spin. **It survives**, and the
|
||
evidence is stronger than what was claimed.
|
||
|
||
Chosen for refutation because it is exactly what PROTOCOL says to aim at: a
|
||
claim the port is about to build on, resting on an estimator (a brightest-region
|
||
centroid) whose own control the Decoder reported as ±19.8°.
|
||
|
||
### The test, and why it needs no absolute registration
|
||
|
||
`live-main-menu.png` has `ptbtn01` focused; `live-main-menu-options-focused.png`
|
||
has `ptbtn04` focused. Both draw **the same sprite**, `ptbtneff01.png` — the
|
||
export confirms the two focus records name the same file. So the two captures
|
||
contain two instances of one 42×46 image, 240 px apart in design space, and the
|
||
question *"is it drawn rotated"* becomes *"are these two crops the same image
|
||
at a different angle"* — which needs no crop offset and no reference to our own
|
||
renderer.
|
||
|
||
Method: sample each ring into a 360-bin **angular luminance profile** over the
|
||
annulus band (r = 9…15 px, bilinear, 0.5 px radial step) and circularly
|
||
cross-correlate. A rotation about the centre shifts that vector and changes
|
||
nothing else.
|
||
|
||
### The instrument was run through two controls before it was believed
|
||
|
||
| control | result |
|
||
|---|---|
|
||
| rotate a capture's own ring by a known 0/30/90/150/210/270/330° and recover it | **0° error on all seven**, peak corr 1.000 |
|
||
| the same estimator on a ring-free 60×64 patch of the *same* capture | peak corr **0.369** — it does not manufacture a match |
|
||
|
||
### The measurement
|
||
|
||
On one shared centre for all three images, so a centroid difference cannot
|
||
masquerade as a rotation:
|
||
|
||
| pair | best shift | peak corr | corr at 0° |
|
||
|---|---|---|---|
|
||
| capture A vs capture B | **134°** | 0.968 | −0.064 |
|
||
| sprite (unrotated) vs capture A | **76°** | 0.969 | −0.295 |
|
||
| sprite (unrotated) vs capture B | **210°** | 0.948 | −0.181 |
|
||
|
||
210 − 76 = 134: the three measurements are internally consistent, which nothing
|
||
in the method forced them to be. Sweeping the centre by ±2 px moves the A-vs-B
|
||
answer over 117…161° while the peak correlation stays 0.9+ across the middle of
|
||
that range, so the **magnitude is ~134° ± ~15°** and the precision claim stops
|
||
there.
|
||
|
||
Evidence sheet — sprite, capture A, capture B, each cropped at the declared
|
||
`42×46+500+156` / `+500+396` — handed over as `share` id
|
||
`1788002507-afe1ad843789`. The phase difference is obvious by eye; the numbers
|
||
are here so it is not only obvious by eye.
|
||
|
||
### The two things this settles for the port
|
||
|
||
1. **The game draws `rotation_deg` on an element the English boot path shows.**
|
||
This is a second, independent confirmation on a different screen and a
|
||
different element from the `ptloop` sweeps, and it moves HANDOFF **ask 4**
|
||
(*should the port draw rotation*) off "changes nothing at rest" — it changes
|
||
the main menu's focus marker, in every frame.
|
||
|
||
2. **0° is not a pose the running game shows.** `screen_view.gd` currently draws
|
||
the ring at its `rest` pose, which is `rotation_deg 0`, and both captures put
|
||
it at 76° and 210°. So the port's focus marker is **known** to be wrong, not
|
||
suspected — and the comment in `screen_view.gd` now says which two numbers it
|
||
is wrong against.
|
||
|
||
### Registration, as a by-product
|
||
|
||
The ring's annulus centroid lands at (32.94, 36.63) and (33.30, 38.90) in
|
||
windows whose design-space prediction under a **zero crop offset** is
|
||
(33.0, 37.0). Within ~0.4 px on the better-thresholded of the two. That
|
||
corroborates `ORACLE-CAPTURES.md`'s *"1279×675, top-left aligned"* directly, on
|
||
a feature nobody chose for the purpose.
|
||
|
||
⚠️ Do not read the earlier P5-groundwork note *"button text bands land at design
|
||
y + 23"* as a crop offset — it is an offset **within** the button sprite, and
|
||
the two were nearly confused here.
|
||
|
||
### What the port did NOT do about it
|
||
|
||
It did not start spinning the ring. The period is a **guess with two unknowns**
|
||
and both belong to the Decoder:
|
||
|
||
* the keyframes are `t=120, rot 0` then an **untimed** `rot 360`. Under HANDOFF
|
||
Q1's replicated reading (*"`+36` is the time the NEXT pose is reached"*) that
|
||
is one revolution in 120 units = **2.0 s** — but this port's `pose_at`
|
||
implements the *other* reading, and switching it is a change to every screen's
|
||
animation timing, not a P5 change;
|
||
* *"groups hold"* (settled 2026-08-28) predicts the ring stops at 360° = 0°.
|
||
Both captures show it elsewhere. That is either a spin that loops, or two
|
||
captures both taken inside the first two seconds of focus. **The port cannot
|
||
tell those apart**, and a wrong answer here is a visible continuous rotation
|
||
on whichever button the player is sitting on.
|
||
|
||
Filed in `BLOCKED.md` and asked over the message channel. What settles it is two
|
||
frames of one focused button a known time apart.
|
||
|
||
---
|
||
|
||
## P5 end to end — and the title does not say `PRESS Ⓐ`, 2026-08-29
|
||
|
||
The gate walk above starts on a screen. This is the whole thing, unattended, in
|
||
one run — the sequence PORT-MISSION names as the objective:
|
||
|
||
```sh
|
||
xvfb-run -a godot --path port -- --boot --play \
|
||
--script=accept,down,down,down,down,accept,cancel,cancel --shots=/tmp/e2e
|
||
```
|
||
|
||
```
|
||
screen publisher_logo … settles at t=235 (3.917 s)
|
||
-> developer_logos at 4.70 s
|
||
-> video ADV at 8.60 s
|
||
video ended at 151.91 s
|
||
-> title at 151.91 s
|
||
boot sequence complete after 156.30 s, holding on title
|
||
menu on title
|
||
script[1] accept (A) -> main_menu
|
||
…
|
||
script[6] accept (EXTRAS) -> extras
|
||
script[7] cancel (B) -> main_menu focus restored to ptbtn05
|
||
script[8] cancel (B) -> title
|
||
script complete after 166.76 s on title
|
||
```
|
||
|
||
Publisher wordmark → developer logos → `ADV` → title → Ⓐ → main menu →
|
||
navigate → Ⓐ → `EXTRAS` → Ⓑ (focus restored) → Ⓑ → title. Contact sheet shared.
|
||
|
||
Two smaller things this run found, both fixed here:
|
||
|
||
* the boot step's `why` in `authored/flow.json` still said *"nothing takes the
|
||
title's place until P5 gives it somewhere to go"*. P5 has. Rewritten to say
|
||
what is actually true — `--boot` still **stops** on the title, and `--play`
|
||
**hands the held title over**; the stop is not a bug and the handover is not
|
||
another boot step.
|
||
* an empty focus printed as a line that trailed off, which reads like a value
|
||
went missing rather than like there is none. The title is a screen with no
|
||
`buttons` that still takes Ⓐ, so it prints
|
||
`(none -- this screen has no focusable item)`.
|
||
|
||
Also confirmed on the way: entering a submenu **directly** (`--menu=extras`) and
|
||
pressing Ⓑ enters the parent at its authored initial focus, not at a restored
|
||
one — there is no history to restore, and `MenuFlow.cancel` only claims a
|
||
restored focus when the stack agrees about where it is going.
|
||
|
||
### 🔴 The port's title does not tell the player to press Ⓐ
|
||
|
||
Found by running the objective end to end, which is the only thing that would
|
||
have found it: the boot's last step is `title` (build 4), and **build 4 has no
|
||
`PRESS Ⓐ BUTTON` plate**. P5 has now made Ⓐ the only way off that screen.
|
||
|
||
This is not a guess about the art. Both states are captured off the running
|
||
game and they differ by exactly that plate:
|
||
|
||
| | capture |
|
||
|---|---|
|
||
| title **without** the plate | `title-builds/live-title-build4-no-plate.png` |
|
||
| title **with** the plate | `title-builds/live-title-press-a.png` |
|
||
|
||
And the plate is already exported — `press_start`, `GP_TITLE` build 2 (HANDOFF
|
||
Q2), sitting in `export/screens/title/` unused by anything.
|
||
|
||
**This is P3's gate, not P5's, and P5 is what exposed it.** Recording rather
|
||
than fixing, for two reasons:
|
||
|
||
1. Which state an idle post-boot title shows — build 4 alone, build 4 with the
|
||
plate over it, or build 4 *then* the plate after a delay — is **behavioural**,
|
||
and the port has no oracle for a sequence. The game demonstrably has both
|
||
states; nothing here says which one follows the intro movie. That is the
|
||
Decoder's.
|
||
2. Showing it would mean **drawing two builds at once**, which this port has
|
||
never done — every mode loads exactly one screen. That is a real change to
|
||
`ScreenView`, not a line in `flow.json`, and it should not be smuggled in
|
||
under a navigation milestone on the strength of "it looks more right".
|
||
|
||
Filed in `BLOCKED.md`. Not blocking: P5's gate is Ⓐ into a submenu and Ⓑ back,
|
||
and both work.
|
||
|
||
## P6 — menu audio, 2026-08-29
|
||
|
||
The disc's menu sound reaches Godot as Ogg Vorbis: three cues and one music bed.
|
||
Nothing in `port/` has heard of XMA, `sound.pak` or `Static.slb`, and nothing in
|
||
it reassembles anything — `sylpheed_formats::media` does that and the exporter
|
||
converts what it hands back.
|
||
|
||
### The cue offsets moved OUT of the exporter, into `authored/`
|
||
|
||
The previous iteration left `crates/sylpheed-export/src/audio.rs` holding the
|
||
three `Static.slb` offsets as a Rust `const CUES`. That is wrong under MISSION
|
||
§3 and the fix is the first thing this iteration did.
|
||
|
||
Those offsets are **measured**, not decoded. `Static.slb` has no `RIFF`, no seek
|
||
chunk and no container: it is a packed run of whole 2048-byte XMA1 packets, and a
|
||
wave is defined *only* by `(offset, packet_count)`. Both numbers came from the
|
||
running game — Canary with `--xma_param_probe=true` prints a stream's packet
|
||
count and first 32 bytes when it is played, and searching those bytes in the bank
|
||
gives the offset (HANDOFF Q8).
|
||
|
||
A measured value compiled into the exporter is **a measurement wearing the
|
||
costume of a decoded field**. It reads as though the exporter derived it from the
|
||
disc; nobody deletes it when the real answer lands, because nobody can see that
|
||
there is anything to delete. So the table is `authored/audio.json` `se.*`, each
|
||
row carrying its own `why`, and the exporter holds no cue table at all.
|
||
|
||
`crate::video::MOVIES` stays a `const` in the exporter, and the contrast is the
|
||
point: Q9 **decoded** that mapping off the movie manifest on the disc. Same
|
||
shape, different provenance, different home.
|
||
|
||
### `name_match` is a field, and its absence means something
|
||
|
||
Q8 names `SE_UI_CURSOR` for the move cue by **name match against the authors' own
|
||
identifiers** — a plausible guess, not the measurement. For Ⓐ, Q8 is explicit
|
||
that the wave was *not* separated between `SE_UI_DECIDE` and
|
||
`SE_UI_SUB_WIN_OPN`, so no name is claimed at all.
|
||
|
||
`name_match` therefore travels beside every cue in `authored/audio.json` and in
|
||
`manifest.json`, and **an absent one means nobody claimed a name — never that
|
||
the binding is unknown.** The binding is the measured part. Collapsing the two
|
||
would turn "we did not separate two candidates" into "we do not know what this
|
||
sound is", which is a different and much weaker statement than the one the RE
|
||
agent actually made.
|
||
|
||
### The BGM is NOT a choice, and this port spent an iteration believing it was
|
||
|
||
The first draft of `authored/audio.json` picked `BGM_001`, wrote a careful `why`
|
||
explaining that the choice was arbitrary, and was **wrong**.
|
||
|
||
`docs/port/BLOCKED.md` carried the row that caused it: *"not on the disc … the
|
||
port is choosing a track, and that choice is authored."* The menu's music is
|
||
**`BGM_103`**, and it is in HANDOFF at **`9ca1eb5`** — the exact commit that page
|
||
says it was reconciled against. So this was not staleness. **The row was wrong
|
||
when it was written.**
|
||
|
||
What HANDOFF says is a negative *with a bound*, and the bound is the entire
|
||
content of it:
|
||
|
||
> the **tables** cannot say — `SOUNDS`, `FILES` and the bank headers name no
|
||
> screen. `GamePart_Title`'s phase handler `sub_821C5580` carries `li r5, 1103`
|
||
> into a sound call; cue 1103 is `BGM_103`; and `BGM_103.slb`'s two declared
|
||
> waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe
|
||
> saw decoding at the main menu. Static code, disc census and runtime all agree.
|
||
> **"The port does not have to choose a track."**
|
||
|
||
The failure is worth naming precisely, because "read HANDOFF more carefully" is
|
||
not the lesson — `BLOCKED.md`'s own staleness check passed, twice, and would pass
|
||
again. **A negative summarised without its reach reads as a bigger negative than
|
||
it is.** "The tables cannot say" became "it is not on the disc", and one word of
|
||
scope was the whole answer. A row in `BLOCKED.md` must quote the reach.
|
||
|
||
It also cost a second thing worth recording: the port would have shipped a menu
|
||
playing the wrong music with a confident `why` beside it saying the choice was
|
||
deliberate. That is exactly the shape of error this project's vocabulary exists
|
||
to prevent, produced *by* the machinery meant to prevent it.
|
||
|
||
### The bank name carries `.slb`, and that is how the mistake surfaced
|
||
|
||
`BGM_001` is not in `sound.pak`. `BGM_001.slb` is — `media::read_sound_bank`
|
||
looks up `name_hash(name)` against the TOC, and the TOC hashes the **file name**.
|
||
`Static.slb` worked from the first run only because the RE finding happens to
|
||
write it with its extension.
|
||
|
||
So the wrong track never played: the export failed loudly with *"BGM_001: not
|
||
present in sound.pak"*. That is luck, not design — had the draft picked a name
|
||
that happened to resolve, nothing would have complained. The `why` in
|
||
`authored/audio.json` now records both the correct name and why the short form
|
||
fails.
|
||
|
||
`export_bgm` now distinguishes the two cases it was conflating. A bank that is
|
||
**not in this disc's `sound.pak`** is a missing asset: the manifest takes a
|
||
warning and everything else still exports. Any other failure — a short read, a
|
||
malformed bank — still stops the run, because a partly-read bank produces a file
|
||
that plays.
|
||
|
||
### The two stems are summed. That part is not a choice
|
||
|
||
Q10 also measured that a bank's sub-waves are **two stems of one performance,
|
||
played together** — sample-synchronous, equal duration, on all 32 banks.
|
||
Concatenating them is explicitly wrong.
|
||
|
||
Emitting them as two files would be wrong for a second, independent reason:
|
||
MODDING rule 1 is *one logical asset, one file*, and handing a modder two stems
|
||
to line up by hand is precisely the reassembly the exporter exists to have
|
||
already done. `amix=normalize=0` sums at unity rather than halving, because
|
||
halving is a mix decision nobody made — and because a sum can clip, the peak is
|
||
**measured and reported** rather than silently corrected.
|
||
|
||
### The loop seam is ugly on purpose
|
||
|
||
No loop-point field has been identified. `loop: "restart"` replays from sample 0,
|
||
so a listener hears the track's own fade-out and its trailing silence before the
|
||
music comes back.
|
||
|
||
Trimming to the fade would sound better and would be **worse**. It would invent a
|
||
loop point, and an invented one is indistinguishable from a decoded one a month
|
||
later — which is the failure mode this whole project is organised against. The
|
||
seam stays audible until a loop point is measured or a capture of the real menu
|
||
looping settles it.
|
||
|
||
### When a cue fires — two rules measured, one authored
|
||
|
||
* **Move** fires on a press that *actually moves the cursor*. `MenuFlow.move()`
|
||
already returned whether it did, which is why left/right stay silent by
|
||
construction rather than by a rule written twice (Q5: ⬅➡ do nothing, and Q8:
|
||
they play nothing).
|
||
* **Ⓐ and Ⓑ** fire when the press *does something*, and not when nothing is
|
||
bound. 🟡 **This half is authored and NOT measured** — nobody has watched the
|
||
game take a dead press. Silence invents less: a sound the game does not make is
|
||
a wrong fact you can hear, while a missing one is a gap. `blocked` counts as
|
||
doing something, because those destinations *were* measured off the running
|
||
game and are missing from this export, not from the game.
|
||
* The bed starts when the menu becomes live and **carries across submenus**.
|
||
`play_bed` is idempotent, because music that restarts every time you press Ⓑ is
|
||
the kind of wrong that reads as "the audio works".
|
||
|
||
### `--audio=` records the Master bus, because neither container has a sound card
|
||
|
||
`docs/port/AUDIO-VERIFICATION.md` §2. An `AudioEffectRecord` on the Master bus
|
||
captures the mixed output from inside a headless run with no device at all, and
|
||
that is the only thing that closes the loop the file opens: comparing an exported
|
||
Ogg against the disc proves the **asset** is right and says nothing about whether
|
||
the engine ever reached it.
|
||
|
||
The run prints `AudioServer.get_driver_name()` beside the file it wrote, because
|
||
"recorded under a dummy driver" is a weaker claim than "heard" and the write-up
|
||
has to be able to say which one it is making.
|
||
|
||
The WAV is saved in `_exit_tree` rather than beside each `quit()`. There are
|
||
eight of those, and the one that would get missed is an error path — exactly the
|
||
run whose audio somebody wants to look at.
|
||
|
||
### `check` now refuses silence and clipping
|
||
|
||
`sylpheed-export check` gained an `audio` pass, and two of its rules are content
|
||
checks rather than schema checks. That is deliberate. Silence is *the* audio
|
||
failure that looks like success — a file of the right duration, the right channel
|
||
count and the right size, full of zeroes — and it passes every structural check
|
||
there is. Clipping is the other one, and the BGM can produce it because it is a
|
||
sum at unity gain. The exporter measures both at export time; `check` refuses a
|
||
tree whose peak is ≤ −90 dBFS or ≥ 0 dBFS.
|
||
|
||
Neither is a judgement about whether the audio is the *right* audio. Nothing in
|
||
that binary can know that, and `BLOCKED.md` says which parts are still authored
|
||
guesses.
|
||
|
||
### A bug worth naming: the temp name ate the file extension
|
||
|
||
`run_ffmpeg` wrote to `.back.ogg.partial` — the temp-name-then-rename discipline
|
||
this project uses everywhere, and which `AUDIO-VERIFICATION.md` records as
|
||
already having caused a confident wrong number once.
|
||
|
||
ffmpeg picks its muxer **from the output filename**, so that is not a slightly
|
||
uglier temp name; it is a hard failure before a byte is written: *"Unable to
|
||
choose an output format for '.back.ogg.partial'"*. `video.rs` already had the
|
||
right shape (`.ADV.partial.ogv`) and this function was written from scratch
|
||
without looking at it. The extension goes last.
|
||
|
||
### Refutation — the three Q8 cue durations, checked end to end
|
||
|
||
**The claim:** HANDOFF Q8 publishes three cue lengths — move **0.533 s**
|
||
(8 192 B, 4 packets), back **0.344 s** (4 096 B, 2), confirm **1.016 s**
|
||
(12 288 B, 6). P6 is built directly on top of these, which by PROTOCOL's own rule
|
||
makes them the right thing to attack: refutation is cheapest where the other
|
||
agent is most confident, and most valuable where the port is about to build.
|
||
|
||
**Why they looked attackable.** The three do not share a rate. Seconds per
|
||
packet is 0.133, 0.172 and 0.169 — the move cue is 22 % off the other two. If a
|
||
packet were a fixed span of audio, at most one of these numbers could be right.
|
||
|
||
**Why that is not a refutation.** An XMA1 packet is 2 048 bytes of *bitstream*,
|
||
not a fixed span: it carries a variable number of 512-sample frames. At 48 kHz a
|
||
frame is 10.667 ms, and the three durations come to **50.0, 32.3 and 95.3
|
||
frames** — near-integers, which is what a variable-frames-per-packet encoding
|
||
looks like and is not what an arithmetic slip looks like.
|
||
|
||
**The measurement.** The exporter reads `(offset, packet_count)` through
|
||
`media::se_wave_riff`, decodes, and `ffprobe`s the finished Ogg:
|
||
|
||
| cue | Q8 claims | exported file measures |
|
||
|---|---|---|
|
||
| move | 0.533 s | **0.533 s** |
|
||
| back | 0.344 s | **0.344 s** |
|
||
| confirm | 1.016 s | **1.016 s** |
|
||
|
||
**Verdict: survives, exactly, at every published digit.** Recorded as a survival
|
||
rather than a pass, because that is what PROTOCOL asks for — a claim that has
|
||
survived an attempt is stronger than one nobody challenged, and the corpus should
|
||
say which it is.
|
||
|
||
⚠️ **Reach, stated so nobody over-reads it.** This is not independent of Q8: the
|
||
durations were derived from the same packet counts the exporter feeds in, so what
|
||
it confirms is that reading those `(offset, packets)` through
|
||
`sylpheed_formats::media` yields streams of exactly the claimed length — i.e.
|
||
that the *transcription* into `authored/audio.json` and the assembly path are
|
||
right. It does **not** confirm that these three waves are the sounds the game
|
||
plays on those three events; that is Q8's own measurement, taken by playing them,
|
||
and this port has no oracle to re-take it with.
|
||
|
||
The attempt did find something, just not here: see the BGM section above, where
|
||
the port's *own* `BLOCKED.md` row failed the same kind of check.
|
||
|
||
### The BGM bank has three sub-waves and HANDOFF says it has two
|
||
|
||
`media::sound_bank_riffs("BGM_103.slb")` returns **three**. HANDOFF Q10's census
|
||
says a music bank is *"exactly two waves of identical duration (32/32 banks on
|
||
the disc)"* — and that census is itself a correction, of an earlier reading that
|
||
called `BGM_001` three sub-waves and was refuted with "the 10 KB is the bank
|
||
header".
|
||
|
||
The third comes from `sylpheed-formats/src/slb.rs:380`, `to_xma_riffs`: when a
|
||
bank has a leading headerless packet region ahead of its first `RIFF`, that
|
||
region is emitted as a sub-wave. It exists because the voice path needs it —
|
||
`VOICE_D_453` decoded to 0.14 s without it. `docs/re/REFUTED.md` already records
|
||
the same region as what makes `BGM_106`–`BGM_109` "break the two-wave rule".
|
||
|
||
**The port sums all three and says so in the manifest.** That is not the
|
||
appealing answer — dropping sub-wave 0 would give a file matching the census, and
|
||
it would have been one line. It is the correct one: *which bytes belong together*
|
||
is the question `sylpheed_formats::media` owns, MISSION §2 names re-deriving it
|
||
here as the single easiest thing in this project to get subtly wrong, and "the
|
||
decoder returned something the corpus does not predict" is a finding to report,
|
||
not a number to quietly adjust. Adjusting it would also have destroyed the
|
||
evidence: a corrected export looks exactly like a correct one.
|
||
|
||
So the export ships the decoders' answer, the manifest carries a warning naming
|
||
the contradiction, `BLOCKED.md` has the row, and the Decoder has the pointer.
|
||
Until it comes back, **the menu plays a sum of three things where the census
|
||
predicts two**, and every one of those places says so.
|
||
|
||
### Clipping — and a comment of mine that argued for the thing that clipped
|
||
|
||
The BGM came out at **+1.8 dBFS**. The comment above the code that produced it
|
||
said `amix=normalize=0` sums at unity "because halving is a mix decision nobody
|
||
made".
|
||
|
||
That was wrong in both halves. Unity summing *is* a decision, and it is the one
|
||
that clips. And 1/n is not a taste call: it is the smallest constant that makes
|
||
an n-input sum of unity-scale signals provably clip-free, which is precisely the
|
||
reasoning `video.rs` already carried for its 0.4142-normalised 5.1 downmix — in
|
||
this same repository, written by this same port, and not looked at. It preserves
|
||
the stems' relative balance exactly, which is the only thing about the sum that
|
||
Q10 settles.
|
||
|
||
It is written as an explicit `volume=` rather than left to `amix`'s
|
||
`normalize=1` default, so the coefficient appears in the manifest's command line.
|
||
A default is a decision nobody made and it can move under an ffmpeg upgrade —
|
||
the same argument MISSION §6 makes about the downmix matrix.
|
||
|
||
**The `confirm` cue is a different case and is not "fixed".** It lands at
|
||
+0.18 dBFS, and it is a single wave off the disc with no arithmetic of ours in
|
||
it: the disc masters it near full scale and a lossy decode of a near-full-scale
|
||
signal overshoots by a fraction of a dB. Attenuating it would mean altering a
|
||
game asset to make one of our own numbers smaller. So `check` bounds the two
|
||
kinds differently — a `bgm` peak ≥ 0 dBFS is refused outright, because it is our
|
||
sum; an `se` is refused only above **+1.0 dBFS**.
|
||
|
||
🟡 That +1.0 is a **judgement and not a measurement**, and it is the weakest
|
||
number in P6. Nobody has measured the overshoot distribution across a corpus of
|
||
cues. If a cue ever trips it, the right response is that measurement, not a
|
||
looser bound.
|
||
|
||
## P6 gate — the audio is in the mix, and a null control says which part
|
||
|
||
No container here has a sound card, so "P6 works" cannot be answered by
|
||
listening. `docs/port/AUDIO-VERIFICATION.md` splits the question into three, and
|
||
these are the two that need no device.
|
||
|
||
### 1. The exported files, measured off the finished assets
|
||
|
||
```
|
||
se back -> audio/se/back.ogg (0.344 s, peak -5.7 dBFS)
|
||
se confirm -> audio/se/confirm.ogg (1.016 s, peak +0.2 dBFS)
|
||
se move -> audio/se/move.ogg (0.533 s, peak -1.4 dBFS)
|
||
bgm main_menu -> audio/bgm/main_menu.ogg
|
||
(87.744 s, peak -7.7 dBFS, bank BGM_103.slb, 3 sub-waves)
|
||
```
|
||
|
||
`sylpheed-export check export` passes: 16 screens validate, and every audio entry
|
||
carries a peak and a duration inside its bounds. The three cue durations match
|
||
HANDOFF Q8 at every published digit — see the refutation record above.
|
||
|
||
### 2. The engine, recorded off the Master bus
|
||
|
||
```
|
||
godot --path port -- --menu --script=down,down,accept,cancel --audio=…/p6.wav
|
||
→ recorded 6.037 s of Master bus (driver Dummy)
|
||
peak 0.0 dBFS, RMS −21.1 dBFS
|
||
```
|
||
|
||
**Non-silent is not the claim.** A WAV of the right duration full of the *bed*
|
||
would look exactly like this, and the cues could be missing entirely. So the cue
|
||
was isolated with a **null control**: the same scripted walk with ⬅ in place of ⬇.
|
||
Left/right are measured no-ops (Q5) and fire nothing, so the two runs differ by
|
||
exactly two move cues and nothing else — same screens, same transitions, the same
|
||
Ⓐ and Ⓑ cues in both, the same bed.
|
||
|
||
| | RMS |
|
||
|---|---|
|
||
| walk with two ⬇ presses | −21.9 dBFS |
|
||
| walk with two ⬅ presses (null) | −22.1 dBFS |
|
||
| **difference** | **−34.6 dBFS** |
|
||
|
||
The difference is not spread over the run. It is **one burst beginning at
|
||
t = 1.10 s and lasting 0.55 s** — two overlapping 0.533 s move cues — with 22 of
|
||
237 windows above −70 dBFS and silence everywhere else, including across the Ⓐ
|
||
and Ⓑ presses, which cancel because both runs make them. That is the cue reaching
|
||
the bus, separated from the music that was playing over it.
|
||
|
||
### The control that proved nothing, kept because it nearly passed
|
||
|
||
The first attempt paired `--script=down,down` against `--script=left,left`. The
|
||
difference was **bit-identical zero**, which reads as "the cues never reached the
|
||
bus" and would have been reported as a bug.
|
||
|
||
It was neither. Both runs recorded **1.115 s** while the first press lands at
|
||
~1.17 s: the control ended before the event it was controlling for. A null result
|
||
from an instrument that was not running is not a null result — PROTOCOL's "run
|
||
your own instrument through a control" applies to the control too.
|
||
|
||
### What this does NOT establish
|
||
|
||
* **That it sounds right.** Everything above is correspondence and separation,
|
||
not judgement. A ten-second human listen still answers something no measurement
|
||
here does.
|
||
* **That the bed is at a sane level against the cues.** 🔴 The Master bus peaks at
|
||
**0.0 dBFS** in the four-step run — the `confirm` cue is +0.2 dBFS on its own,
|
||
so any music under it puts the mix on the ceiling. Per-file levels are the
|
||
disc's and are fine; the **runtime** mix has no headroom. The port has not set a
|
||
bus balance, because nothing measures one and an invented balance is the same
|
||
class of mistake as an invented loop point. Recorded here rather than fixed
|
||
quietly.
|
||
* **That "Dummy driver" means heard.** It does not, and the run prints the driver
|
||
name so a write-up cannot forget to say so.
|
||
|
||
### One bug, in two dialects, both about a temp filename
|
||
|
||
The temp-name-then-rename discipline this project uses everywhere broke twice in
|
||
this milestone, in two different tools, for the same underlying reason: **tools
|
||
dispatch on the extension, so a temp name must preserve it.**
|
||
|
||
* `run_ffmpeg` wrote `.back.ogg.partial` → *"Unable to choose an output format"*,
|
||
a hard failure before a byte was written.
|
||
* `boot.gd` wrote `p6.wav.part` → `save_to_wav` **appends** `.wav` when the path
|
||
does not end in it, producing `p6.wav.part.wav`; the rename then failed to find
|
||
its source, its return value was not checked, and the run printed a success
|
||
line naming a file that did not exist.
|
||
|
||
The second is the more dangerous shape, and it is the one this project has
|
||
already warned itself about: a confident line of output pointing at nothing. The
|
||
rename's return is now checked and the failure is loud.
|
||
|
||
## P3, reopened — the boot title was missing the `PRESS Ⓐ` plate, 2026-08-29
|
||
|
||
P3 passed its gate with a boot that ended on build 4 alone. `BLOCKED.md` carried
|
||
that as 🔴 from the start: both states were captured, so the art was never the
|
||
question — the *sequence* was, and it is behavioural, so the port had no oracle
|
||
for it.
|
||
|
||
It is answered. `docs/re/title-plate-delay-measured.md`
|
||
(`auto/no-disc-and-menu-captures` at `fb536df`, **not on `main`** at the time of
|
||
writing) measures two independent boots: the title presents **without** the
|
||
plate, and the plate arrives **2.13 s** later, the two runs agreeing to 6 ms.
|
||
|
||
### Two builds at once, as two `ScreenView`s
|
||
|
||
`ScreenView` draws one screen. The obvious change was to teach it about a
|
||
subordinate overlay screen; the change made was to put a **second `ScreenView`
|
||
in the same `SubViewport`**, after the first.
|
||
|
||
That is what "two builds at once" actually is. Each build has its own timeline,
|
||
its own textures and its own hold — the plate's group runs independently of the
|
||
title's, which is the entire content of the finding — and Node2D siblings already
|
||
paint in tree order. The alternative would have put an `if overlay` in every
|
||
method that walks elements, and would have expressed the same information less
|
||
directly. The export's `paint_order` still means what it always meant: an
|
||
ordering *within* a build.
|
||
|
||
### The delay is timed from where build 4 stops animating
|
||
|
||
Not from where the title first appears. This is the finding rather than a detail:
|
||
measured from first-draw the two oracle runs differ by **0.48 s**, because the
|
||
build-in itself ran 1.64 s and 2.13 s and the emulator's frame pacing during an
|
||
animation is not the game's clock. Measured from settle they differ by 6 ms.
|
||
|
||
So `_boot_done` — the moment the sequencer already had for "this screen has
|
||
reached its hold" — is the landmark, and the overlay is due `after_settle_seconds`
|
||
later. A number taken from the wrong instant here looks exactly like a
|
||
measurement.
|
||
|
||
### The overlay is attached to the BOOT STEP, not to the `title` screen
|
||
|
||
What was measured is the boot title. Whether the plate is there when the title is
|
||
reached *again* — by Ⓑ from the main menu, or after the attract movie — is not
|
||
measured, and hanging the overlay on the screen would quietly claim that it is.
|
||
So it lives on the boot step in `authored/flow.json`, and `_drop_overlay` takes
|
||
it away with the screen it belongs to. `BLOCKED.md` carries the gap.
|
||
|
||
### Refutation — the RE agent's instruction contradicts the RE agent's measurement
|
||
|
||
**The claim under test**, quoted from the finding's *"What the port should
|
||
author"*: draw build 4, *"when build 4 has settled, wait **2.13 s**, composite
|
||
build 2 over it"*.
|
||
|
||
**It does not reproduce the measurement it came from**, and the gap is 3.97 s.
|
||
Build 2 is not a static plate: it has a group, and this port plays groups.
|
||
`press_start` has one element, `ptbtn00`, and its `fade_argb` reads
|
||
|
||
```
|
||
t=214 0x00ffffff pos (383, 560) invisible
|
||
t=236 0x00ffffff pos (383, 550) still invisible, having slid 10 px up
|
||
t=238 0xffffffff full alpha
|
||
t=244 0xffffffff holds
|
||
— 0x00ffffff the exit, untimed
|
||
```
|
||
|
||
At the measured 60 units/s that is **3.967 s** from the group's start to full
|
||
alpha. Compose the instruction with the group and the plate is first *visible* at
|
||
settle + 2.13 + 3.97 = **settle + 6.10 s**. What was measured — the glyph counter
|
||
leaving its no-plate value of 154 — is the plate becoming visible at **settle +
|
||
2.13 s**.
|
||
|
||
Neither obvious reconciliation works:
|
||
|
||
| reading | plate visible at | measured |
|
||
|---|---|---|
|
||
| both groups start together | 3.97 s (build 4 settles at **4.350 s**) — i.e. 0.38 s *before* settle | settle + 2.13 s |
|
||
| build 2's group starts at settle | settle + 3.97 s | settle + 2.13 s |
|
||
| build 2's group starts at settle + 2.13 s (the instruction) | settle + 6.10 s | settle + 2.13 s |
|
||
|
||
To land on the measurement, build 2's group has to start **2.51 s** after build
|
||
4's, which is not a landmark of anything.
|
||
|
||
**Verdict: the instruction is refuted as written; the measurement is untouched.**
|
||
The measurement is an observation of the running game and this port has no
|
||
standing to doubt it. What is refuted is the step that turns it into an
|
||
authoring rule, and that step is an interpretation.
|
||
|
||
**So the port ships the instruction, not its own arithmetic**, prints the
|
||
discrepancy on every boot, and files the row. This is the same call as the BGM
|
||
sub-waves and for the same reason: reconciling two of the RE agent's numbers is
|
||
a decoding question, and a port that quietly picks the one that looks right
|
||
destroys the evidence — a corrected boot looks exactly like a correct one.
|
||
|
||
The first thing to check is about the instrument rather than the game: is *"title
|
||
settled"*, the glyph counter first reading 154, the same instant as the port's
|
||
last-element settle (t=261, 4.350 s into the group)? If that landmark is earlier,
|
||
the gap closes with nothing else moving.
|
||
|
||
### Refuting the port's own claim: things in this export DO pulse
|
||
|
||
`BLOCKED.md` has carried this since P2, under the port's own raised question
|
||
about whether groups loop:
|
||
|
||
> no element's alpha reverses direction anywhere in this export, so nothing
|
||
> pulses, which removes the obvious reason to expect a loop without disproving
|
||
> one.
|
||
|
||
**`ptbtn00` reverses.** `0x00` → `0xff` → `0x00`, in the table above, in the
|
||
export, the whole time. The claim was never checked against `press_start`; it was
|
||
checked against the screens P2 happened to be animating. The RE agent has now
|
||
measured the running game pulsing this exact element at a mean 2.24 s.
|
||
|
||
So the reason to expect a loop is back — and the port still does not draw one,
|
||
because **no reading of this group produces 2.24 s**: the whole group is 268
|
||
units = 4.47 s, and from its first keyframe 54 units = 0.90 s. The plate is drawn
|
||
arriving and then holding at its settle (t=238, alpha `0xff`), which is what
|
||
every other screen does and what the static oracle capture
|
||
`live-title-press-a.png` shows. Which instant a repeat restarts from is filed,
|
||
not guessed.
|
||
|
||
### `--boot --capture=` — one frame instead of six hundred
|
||
|
||
The boot had no artifact of its own except `--film`, a PNG every 0.25 s for the
|
||
whole 156 s run, to answer one question: is the plate on top of the title at the
|
||
end. `--capture` was a `--screen`-only flag taken in `_ready`, which for a boot
|
||
run is 150 s too early. It is now deferred to the end of the sequence when
|
||
`--boot` is given.
|
||
|
||
### P3 gate — the boot ends on two builds
|
||
|
||
```
|
||
godot --path port -- --boot --capture=…/p3-plate.png
|
||
→ boot sequence complete after 155.86 s, holding on title
|
||
overlay press_start due at 157.99 s (+2.13 s after settle)
|
||
overlay press_start raised at 158.00 s, 1 element(s), settles at t=238
|
||
⚠ plate raised at settle+2.13 s but its own group reaches full alpha 3.97 s
|
||
later, so it is first VISIBLE at settle+6.10 s -- the measurement is
|
||
settle+2.13 s.
|
||
boot ends on title + press_start at 161.99 s
|
||
drew 16: ptbase2, ptloop01, …, ptcopyright
|
||
overlay press_start at t = 261.00 units, drew 1: ptbtn00
|
||
```
|
||
|
||
The PNG shows the title logo with **`PRESS Ⓐ BUTTON`** under it — build 4 and
|
||
build 2 in one frame, which this port had never drawn.
|
||
|
||
Two things the run made obvious and that are now fixed:
|
||
|
||
* **The capture reported only the base build's elements.** The first composited
|
||
capture printed `drew 16` and no mention of the plate, which reads as though
|
||
the overlay had not drawn at all. The overlay gets its own line; folding its
|
||
elements into the first list would have reported a screen that does not exist.
|
||
* **`--screen=<a> --overlay=<b>`** raises the same composite immediately, by the
|
||
same code path, with no delay. It exists because the only other way to see two
|
||
builds was a 156 s boot of which 137 s is the intro movie — and under Xvfb's
|
||
software Theora decode that is several minutes to answer "is the plate on top
|
||
of the title". It applies **no** delay: the delay is a measurement and lives in
|
||
`authored/flow.json`. The boot-mode narration is suppressed there, because a
|
||
log line that describes a sequence it is not running is worse than no log line.
|
||
|
||
## P5 — the focus ring spins, 2026-08-29
|
||
|
||
The ring was drawn at 0° and the file said so: *"THIS IS KNOWN TO BE WRONG, and
|
||
is drawn anyway because the right answer is a guess."* What was missing was the
|
||
**period**, and it is now measured — `docs/re/focus-ring-spin-measured.md`
|
||
(`auto/no-disc-and-menu-captures` at `4fa3099`): a continuous spin, from eight
|
||
evenly spaced autocorrelation peaks over nine revolutions, **with no angle
|
||
estimated anywhere** — both angle estimators failed their own controls and were
|
||
not used.
|
||
|
||
### The period comes off the disc; the RE agent supplied only that it repeats
|
||
|
||
`ptbtneff01` declares two keyframes that differ in **nothing but**
|
||
`rotation_deg`, 0 → 360, the first timed at `t = 120` and the second untimed.
|
||
The port turns once per **120 units**. Nothing is authored: the number is on the
|
||
disc, and what the measurement adds is that the turn **repeats** rather than
|
||
stopping at 360 = 0, which "groups hold" could not distinguish because those are
|
||
the same pose.
|
||
|
||
`ScreenView.spin_period_units` is the rule, and it is structural and narrow:
|
||
exactly two keyframes, differing only in rotation, by a full 360, first timed and
|
||
second untimed. **Disc-wide check over this export: 16 of 212 elements match, and
|
||
all 16 are focus rings** — `ptbtneff01` on the five main-menu buttons and
|
||
`ptbtneff02` on the three `EXTRAS` buttons, in both locales, every one declaring
|
||
`t = 120`. Zero false positives.
|
||
|
||
That check is the point rather than a formality. The measurement was taken on
|
||
**one** button of **one** screen; a rule that also caught something else would be
|
||
extrapolating it to elements nobody watched.
|
||
|
||
⚠️ It is a rule about **shape**, not a decoded field. Nothing on the disc says
|
||
"this loops". The day a loop flag is decoded, this goes.
|
||
|
||
### Verified on the port's own render, with the RE agent's own control
|
||
|
||
Captures at `--time=` 2.0 … 4.0 s on the settled main menu, `ptbtn01` focused:
|
||
|
||
| | |
|
||
|---|---|
|
||
| t=2.0 vs t=4.0 (one full period apart), **whole frame** | **0.0000 / 255** — bit-identical |
|
||
| t=2.5, 3.0, 3.5 against t=2.0, inside the ring's box | 3.60, 3.71, 3.58 / 255 |
|
||
| sum of box luminance across **eight** phases | spread **0.027 %** of the mean |
|
||
|
||
The last row is deliberately the RE agent's own observable: they separated
|
||
rotation from a brightness pulse by showing total annulus brightness is conserved
|
||
while per-bin brightness moves. The port's render conserves it to 0.027 % (theirs
|
||
was 0.4 % over 16 s, with capture noise in it). A filmstrip of the four
|
||
quarter-period phases shows the bright head at top, right, bottom, left.
|
||
|
||
### Two things it does not settle
|
||
|
||
* **Direction.** The port turns 0° → +360°, the sign the disc declares. No signed
|
||
angle was ever measured — the estimator that would have given one failed its
|
||
control and was not used.
|
||
* **Phase across a focus change.** The port drives the ring off the **screen**
|
||
clock, so moving the cursor does not restart the turn. The alternative — the
|
||
record's group restarting when the record is instantiated — is the stronger
|
||
claim, and the oracle run held focus on one button throughout, so nothing
|
||
separates them. Two frames straddling a focus change would.
|
||
|
||
## P3, corrected — the plate needs no authored delay at all, 2026-08-29
|
||
|
||
Last iteration the port refuted the RE agent's authoring instruction (*"when
|
||
build 4 has settled, wait 2.13 s, composite build 2"*) with arithmetic off the
|
||
disc, shipped the instruction anyway rather than pick between two of their
|
||
numbers, and printed the discrepancy on every boot.
|
||
|
||
**The refutation held, and the answer that came back is better than either option
|
||
the port offered: author nothing.** `5b0a6e6`.
|
||
|
||
### The premise that failed was the port's, and it will bite again
|
||
|
||
> 🔴 **`rest.t` is not when a screen settles.** It is the last *hold* keyframe
|
||
> before the exit.
|
||
|
||
Checked here rather than taken on trust. `title`'s `ptlogo1`:
|
||
|
||
```
|
||
t=26 (-116,-7) 150% a=0x00 the pre-roll
|
||
t=42 (179,186) 101% a=0xe0 it has arrived
|
||
t=251 (184,193) 100% a=0xff 5 px and 31 alpha steps later, 3.5 s on
|
||
```
|
||
|
||
It **stops moving at t=42** and then creeps for 209 units. `rest.t = 251` is the
|
||
end of that creep, not the arrival. The title's visible build-in is over at
|
||
**`t = 118`**, where `pteff01`, `pteff02` and `ptlogoall_eff` finish together.
|
||
|
||
Every reconciliation the port computed last iteration was wrong by exactly that
|
||
error: reading `rest.t` put build 4's arrival at 4.350 s instead of 1.967 s, and
|
||
the "2.51 s, which is not a landmark of anything" that looked so damning is
|
||
`(4.350 − 1.967) + 0.13` — the error itself, wearing a decimal point.
|
||
|
||
### One clock, and the interval is declared
|
||
|
||
| | units |
|
||
|---|---|
|
||
| build 4's last build-in ramp | `t = 118` |
|
||
| `ptbtn00` reaches `a = 255` | `t = 238` |
|
||
| **difference** | **120 units = 2.000 s** |
|
||
|
||
Measured: **2.138 s** and **2.132 s**. The 6.7 % is presentation rate — 120 units
|
||
in 2.135 s is 56.2 units/s, the emulator running 28.1 fps against a nominal 30,
|
||
and the corpus had independently measured the idle title at 28.5 fps *before*
|
||
these runs.
|
||
|
||
So `authored/flow.json` carries `"clock": "shared"` and **no delay**, `boot.gd`
|
||
raises the overlay when the step's screen loads rather than at its settle, and
|
||
`overlay.time_units = view.time_units` — assigned, not accumulated, because two
|
||
independently advanced clocks drift by a frame here and there and the whole
|
||
content of the finding is that 120 units is a fixed interval on one timeline.
|
||
|
||
⚠️ **The general hazard, stated by the RE agent and worth repeating where the
|
||
port will read it: discount a wall-clock number off that oracle by ~6 %.** It is
|
||
Canary's presentation rate baked into whatever it measures. A port at a true
|
||
30 Hz that authored 2.13 s would be visibly late.
|
||
|
||
### Refutation — two of the RE agent's numbers for the same 120 units disagree by 2 %
|
||
|
||
Both findings measure the same declared quantity: **120 keyframe units of wall
|
||
clock, during a static hold, in Xenia Canary.**
|
||
|
||
| | | implied presentation |
|
||
|---|---|---|
|
||
| plate: settle → plate, two runs | 2.138, 2.132 s → mean **2.135 s** | 28.10 fps |
|
||
| ring: one revolution, seven spacings | 2.18 2.16 2.18 2.16 2.16 2.20 2.20 → mean **2.177 s** | 27.56 fps |
|
||
| **disagreement** | **0.042 s = 1.97 %** | |
|
||
|
||
That is **seven times** the plate finding's own run-to-run agreement of 6 ms, and
|
||
it lands on the argument that finding uses to justify trusting itself: *"the
|
||
build-in is where frames are dropped; the static hold is not. A model in which
|
||
the game's own timing varied would have to move both."* Two static-hold
|
||
measurements are exactly what should agree under that model.
|
||
|
||
A second, smaller arithmetic slip in the same place: the ring page reconciles
|
||
2.177 s against a band of "27.6–28.8 fps", saying the measurement *"sits at the
|
||
top of that band"*. It does not sit in it. 60 rendered frames at 27.6 fps is
|
||
2.1739 s; the mean needs **27.56 fps** and the two slowest spacings (2.20 s)
|
||
need **27.27 fps**. Four of the seven spacings are above the band's top.
|
||
|
||
**Verdict: the containment claim is refuted; the spin, the period and the
|
||
reconciliation are untouched.** Either the presentation rate genuinely differed
|
||
between the two sessions — which the plate page's own corroboration argues
|
||
against for static holds — or the ring's revolution is not exactly 120 units.
|
||
The corpus should say which, because they are the same claim measured twice.
|
||
|
||
🟢 **Nothing in the port moves either way.** `spin_period_units` uses the
|
||
declared 120 units at 60 units/s = **2.000 s of port time**, which is the
|
||
true-30 Hz value both readings agree the disc means. This is a corpus
|
||
consistency problem, not a port one — which is why it is filed rather than
|
||
worked around.
|
||
|
||
### The corrected boot ended one build too early, and the capture showed it
|
||
|
||
Moving the plate onto the shared clock also moved the boot's exit, and the first
|
||
capture taken afterwards was **visibly darker** than the one before it. The cause
|
||
is `pteff00`, the title's black fade quad: it ramps `0xff000000` → `0x00000000`
|
||
over t=16…261, so at t=243 — where the run was quitting, `overlay.settle_time()`
|
||
after the overlay was raised — the frame is still ~7 % black.
|
||
|
||
The plate arrives at t=238; **build 4 is not finished until t=261**. The boot now
|
||
ends at the later of the two, and says which in the log:
|
||
|
||
```
|
||
-> title at 145.79 s
|
||
overlay press_start raised at 145.79 s, 1 element(s), settles at t=238
|
||
boot ends at 150.14 s, once both builds have arrived (t=261)
|
||
```
|
||
|
||
Worth recording because of how it presented: nothing failed, no warning was
|
||
printed, and the only symptom was a frame slightly darker than the previous
|
||
run's. A gate artifact that silently drifts is the failure mode this project
|
||
keeps meeting — and it was caught only because there was a previous capture to
|
||
compare against.
|
||
|
||
## P7 — the new-game intro, 2026-08-29
|
||
|
||
`S00A.wmv` has been in `export/video/` since P4 (MISSION §6 put both movies in
|
||
scope at once). What P7 needed was for something to *play* it and for the run to
|
||
end somewhere defined.
|
||
|
||
### The port skips two measured screens, and says so on screen
|
||
|
||
The real chain is **`NEW GAME` → `DIFFICULTY` → `SELECT DATA` → Ⓐ on a save slot
|
||
→ ~4.5 s → `S00A`** (HANDOFF Q4 measured the screens, Q9 decoded the movie and
|
||
then measured its onset off the running game at 0.96–1.000 with a strictly
|
||
monotone playhead over 25 consecutive 0.5 s samples).
|
||
|
||
`DIFFICULTY` and `SELECT DATA` are measured destinations that are **not
|
||
`GP_TITLE` builds**, so no screen file exists to go to. The port therefore jumps
|
||
from `NEW GAME` to the one thing in that chain it has.
|
||
|
||
That is a gap, not a sequence, and the whole design here is about not letting it
|
||
read as one:
|
||
|
||
* `MenuFlow.accept` returns a **new kind**, `video`, rather than folding this
|
||
into `blocked`. The caller has to announce the skip, and a distinct kind is
|
||
what forces it to.
|
||
* The runtime prints it every time:
|
||
`(NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
|
||
Neither screen is in this export.`
|
||
* `authored/flow.json` carries `skipped_chain` as **data**, so the names of what
|
||
is missing live beside the decision rather than inside a GDScript string.
|
||
|
||
A port that quietly jumped from `NEW GAME` to the intro would be showing a
|
||
sequence the game does not have, with nothing on screen saying so. That is the
|
||
exact failure this project keeps meeting from the other direction.
|
||
|
||
### What happens after the movie is authored, and had to be
|
||
|
||
The game goes into **mission 1**. Gameplay is out of scope (PORT-MISSION §7), so
|
||
"returns to a defined state" is a decision, and P7's gate says as much. The port
|
||
returns to the **title**: the boot's own end state, so a run that finishes the
|
||
intro lands somewhere a player can start again from. Nothing measured says the
|
||
game does this, and `after_video.kind` is `"authored"`.
|
||
|
||
### The 4.5 s gap is left empty on purpose
|
||
|
||
Q9 measures the movie starting ~4.5 s after Ⓐ on the save slot. What is on screen
|
||
for those 4.5 s was never observed — the run that would have shown it hit the
|
||
documented `sub_823070B0` cache crash after `SELECT DATA`.
|
||
|
||
`GP_TITLE` **does** carry a loading screen (below), and 4.5 s is about the right
|
||
shape for one. That is precisely why it is in `BLOCKED.md` and not in
|
||
`flow.json`: a plausible filler that nobody watched is the kind of thing that is
|
||
indistinguishable from a measurement a month later.
|
||
|
||
### A script timeout that would have failed every movie
|
||
|
||
`--script`'s per-step timeout is 20 s, to stop an unattended run waiting forever
|
||
on a screen that never settles. `S00A` is **93.9 s**, so the first scripted
|
||
new-game run would have been killed at step 1 and reported as "never settled".
|
||
|
||
Raising the constant would have been wrong in the other direction: a movie stuck
|
||
at frame 0 would then hang the job, and a job that waits is worse than a job that
|
||
fails, because it does not look like a failure.
|
||
|
||
So the test is **liveness, not duration**: while `get_stream_position()`
|
||
advances, the deadline moves with it; a stalled movie still trips the same 20 s.
|
||
|
||
### Found while looking: `GP_TITLE`'s four unnamed builds are LOADING screens
|
||
|
||
`build_00`, `build_01`, `build_12`, `build_15` have never had names. Every
|
||
element in all four is `pgloading_*` — `pgloading_processing.png`,
|
||
`pgloading_circle1`, `pgloading_delta`, `pgloading_ring` — and `LOADING` is one
|
||
of the three screen names the RE agent read out of the title part's state
|
||
function.
|
||
|
||
Two variants: 0/1 carry 7 elements, 12/15 carry 10.
|
||
|
||
**They are not renamed here.** The archive's own pairing (adjacent for 2/3, `+3`
|
||
for 4…9 and for 10/13, 11/14) suggests 0 is 1's twin and 12 is 15's, but which
|
||
member of each pair is which **locale** is an inference, and a name is exactly
|
||
the kind of thing that stops being questioned once written. Handed to the RE
|
||
agent, who can answer it from a capture in one look. `BLOCKED.md` has the row.
|
||
|
||
⚠️ **And one of them is a second casualty of the `rest.t` problem.**
|
||
`pgloading_eff00.prm` on entries 12/15 is a full-screen black quad whose group
|
||
runs `0xff000000` at t=38 → `0xff000000` at t=48 → `0x00000000` untimed: black,
|
||
held, *then* clear. Its `rest.t` is **38**, where it is fully opaque. A port that
|
||
draws that screen at its declared rest draws **a black rectangle over the entire
|
||
loading screen**. The title's case only dimmed a frame; this one hides
|
||
everything. Filed with the `settle_time()` row it belongs to.
|
||
|
||
### Refutation — attempted on the fade-quad census; it survives, with a caveat
|
||
|
||
**The claim** (HANDOFF, on transitions): *"in `GP_TITLE` exactly the six screen
|
||
builds carry it while the six overlays do not"*, where "it" is the full-screen
|
||
black `.prm` quad *whose keyframe group is the transition*.
|
||
|
||
**The test**, over the whole export: count builds carrying a full-screen
|
||
primitive with black in its keyframes.
|
||
|
||
```
|
||
16 builds exported; 12 carry one.
|
||
Of the 12 `is_build` bundles (excluding the 4 authored splashes): 8 carry, 4 do not.
|
||
carry: title, title_jp, main_menu, main_menu_jp, extras, extras_jp,
|
||
build_12, build_15
|
||
do not: press_start, press_start_jp, build_00, build_01
|
||
```
|
||
|
||
8 and 4, not 6 and 6. But the two extras are `build_12` / `build_15`, and their
|
||
quad is a **different shape**:
|
||
|
||
| | transition quad (`pteff00.prm`, title) | loading quad (`pgloading_eff00.prm`) |
|
||
|---|---|---|
|
||
| | `0xff000000` t=16 | `0xff000000` t=38 |
|
||
| | `0x00000000` t=261 | `0xff000000` t=48 |
|
||
| | `0xff000000` untimed | `0x00000000` untimed |
|
||
| shape | black → clear → **black** | black → held → **clear** |
|
||
|
||
The transition quad returns to black on exit; the loading quad does not. Read
|
||
strictly — the quad *whose group is the transition* — the claim holds.
|
||
|
||
**Verdict: survives.** The refinement is worth recording anyway, because the
|
||
naive test over-counts by two and somebody will run the naive test. There are
|
||
**two kinds** of full-screen black `.prm` in `GP_TITLE`, and only one of them is
|
||
a transition.
|
||
|
||
## P7 gate
|
||
|
||
```
|
||
godot --path port -- --menu --script=accept --audio=…/p7.wav
|
||
```
|
||
|
||
```
|
||
menu on main_menu, focus ptbtn01
|
||
script[1] accept
|
||
(NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
|
||
Neither screen is in this export.
|
||
-> video S00A at 1.18 s (/work/export/video/S00A.ogv)
|
||
video ended at 94.93 s
|
||
-> title (authored: authored)
|
||
menu on title, focus (none -- this screen has no focusable item)
|
||
script complete after 99.28 s on title
|
||
recorded 98.453 s of Master bus (driver Dummy)
|
||
```
|
||
|
||
The movie ran **93.75 s** against a declared 93.9 s, the run ended on the title,
|
||
and the Master bus recorded 98.453 s: `pcm_s16le`, 44.1 kHz stereo, RMS
|
||
**−22.2 dBFS**.
|
||
|
||
**What this does not show, stated because it would be easy to imply otherwise:**
|
||
the recording contains the menu bed *and* the movie together, and this run did
|
||
**not** separate them. So it establishes that the engine reached an output for
|
||
98 s of a run whose middle 94 s was a movie — not that `S00A`'s own audio track
|
||
is in the mix. Separating them wants the P6 null-control method (a paired run
|
||
that differs only in the movie), and that is not done here.
|
||
|
||
🔴 **Peak 0.0 dBFS again.** The same runtime-headroom problem P6 filed: per-file
|
||
levels are the disc's and are fine, the Master bus has no headroom, and the port
|
||
has set no bus balance because nothing measures one.
|
||
|
||
### One more file read while it was being written
|
||
|
||
`ls` reported the recording as **3 702 828 B**; `ffprobe` on the finished file
|
||
reports **17 367 084 B / 98.452608 s** — a factor of 4.7. `ffprobe` is right and
|
||
the `ls` caught it mid-flight.
|
||
|
||
`AUDIO-VERIFICATION.md` opens by naming this failure and the port has had the
|
||
temp-name-then-rename discipline since P6, which is what makes it worth writing
|
||
down rather than shrugging off: **the discipline protects a reader who opens the
|
||
path, and it does not protect a reader who stats it at the wrong moment.** Size
|
||
on disk is not a measurement of a file somebody else is still writing. Ask the
|
||
decoder, not the directory entry.
|
||
|
||
## Modding — rule 4 was never implemented, 2026-08-29
|
||
|
||
`docs/port/MODDING.md` is explicit that modding is *"a design constraint on the
|
||
exporter today — not a milestone to add later"*, and its rule 4 is base-and-
|
||
overrides: a mod replaces a file by **shadowing its path**, so a modder edits
|
||
nothing under the derived tree and re-exporting is always safe.
|
||
|
||
**Nothing read `data/mods/` at all.** The directory has existed since the
|
||
monorepo merge with a `.gitkeep` in it and no code path anywhere — exporter or
|
||
runtime — that looked at it. Eight milestones shipped past that.
|
||
|
||
### One resolver, and every read goes through it
|
||
|
||
`ExportTree.resolve(rel)` returns the mod tree's copy when one exists and the
|
||
derived tree's otherwise. `read_json`, `texture`, `video` and `MenuAudio` all
|
||
call it, so a mod can replace **a screen's JSON, a sprite, a cue, the music bed
|
||
or a movie** — every asset kind the port reads.
|
||
|
||
`MenuAudio` was reading `tree.root.path_join(...)` directly and had to be
|
||
changed. Left alone it would have made audio the one asset kind a mod could not
|
||
touch, for no reason a modder could have guessed — which is the failure mode
|
||
rule 4 exists to prevent.
|
||
|
||
There is deliberately no manifest of what a mod contains and no registration
|
||
step: **the path is the registration**, which is the whole of the rule.
|
||
|
||
⚠️ **One tree, not a stack.** Several mods layering over each other needs a load
|
||
order, and a load order needs a rule nobody has asked for. Said out loud in
|
||
`data/mods/README.md` rather than answered.
|
||
|
||
### A modded run must not look like an unmodded one
|
||
|
||
Every shadowed file is printed the first time it is read:
|
||
|
||
```
|
||
mod: sprites/title/main_menu/ptbtn01.png <- /work/data/mods/sprites/…/ptbtn01.png
|
||
```
|
||
|
||
MODDING says *"did I break it?"* is answered by disabling a mod. That is a fine
|
||
last resort and a poor only resort, so the log names the replacement instead.
|
||
|
||
**The first version of this got it wrong in an instructive way**: it printed a
|
||
summary in `_ready`, before a single asset had been read, and so always said
|
||
`(nothing shadowed yet)`. A report structurally incapable of reporting anything
|
||
is worse than no report, because it looks like an answer. It now announces each
|
||
shadow at the moment it happens.
|
||
|
||
### Gate
|
||
|
||
A synthetic 203×43 magenta PNG — nothing disc-derived — dropped at
|
||
`data/mods/sprites/title/main_menu/ptbtn01.png`:
|
||
|
||
| | |
|
||
|---|---|
|
||
| pixels changed between the two renders | **8 501** of 921 600 (0.92 %) |
|
||
| bounding box of the change | x 542…744, y 162…204 — **203×43**, the sprite's own size |
|
||
| `sylpheed-export check export` afterwards | 16 screens still validate |
|
||
|
||
The changed region is exactly the sprite and nothing else moved.
|
||
|
||
### `data/mods/` was not gitignored, and that is a hole in a hard rule
|
||
|
||
*"Never commit game assets"* has been enforced on `export/` and `data/base/`
|
||
since P0. But **a mod is usually an edited game asset**, and `data/mods/` was
|
||
fully tracked — so the one directory a user is invited to put modified sprites in
|
||
was the one directory git would happily take them from.
|
||
|
||
`.gitignore` now excludes everything under it except the README.
|
||
|
||
### The naming split is not mine to resolve
|
||
|
||
`MODDING.md` describes the tree as `data/base/`; `PORT-MISSION.md` §3, the
|
||
exporter, `ExportTree` and `.gitignore` all say `export/`. Both are mission
|
||
files, and PROTOCOL is clear that **only the human changes a mission**, so this
|
||
is raised rather than picked. `.gitignore` has ignored both names on purpose
|
||
since P0.
|
||
|
||
It matters here for one concrete reason: MODDING's layout has `base/` and `mods/`
|
||
as **siblings**, and today they are not — the tree is `export/` at the repo root
|
||
while mods are `data/mods/`. The resolver takes `SYLPHEED_MODS` or defaults to
|
||
`data/mods/`, which is what exists; if the tree is ever renamed to `data/base/`
|
||
the sibling rule becomes natural and that default can go.
|
||
|
||
## Refutation — the paint-order key, and the reach of its tie-break
|
||
|
||
**The claim** (HANDOFF Q3): paint order is *"a `u16` layer key at `+0x0A`,
|
||
**decoded**"*, with the tie-break filed 🟡 as *"eight candidates refuted; costs
|
||
one element's blend on one screen"*.
|
||
|
||
**First pass: 2 of 16 screens did not match** a stable sort by layer key — both
|
||
loading screens, `build_12` and `build_15`.
|
||
|
||
**That was my test, not the claim.** `pgloading_eff00.prm` carries **no layer key
|
||
at all** — `layer: null`, `layer_source: "none"`: it is a primitive with no
|
||
sprite header, and the exporter's implied-name fallback produces nothing either.
|
||
My sort put a keyless element first; the decoders put it **last**.
|
||
|
||
Completing the rule as *"stable sort by layer key, elements with no key last"*
|
||
gives **16 of 16**. And last is right: `pgloading_eff00` is the full-screen black
|
||
quad, and HANDOFF's own sentence is that the fade quad paints last.
|
||
|
||
**Verdict: survives, with the rule completed.** Worth recording because the
|
||
published statement does not say where a keyless element goes, and there is at
|
||
least one in the archive.
|
||
|
||
🟡 **But the tie-break's reach looks understated.** Census over this export:
|
||
|
||
```
|
||
elements sharing a layer key with another element: 105, across 12 of 16 screens
|
||
```
|
||
|
||
HANDOFF characterises the cost as *"one element's blend on one screen"*. 105
|
||
elements on 12 screens is a much larger surface than that. Most of those ties are
|
||
probably invisible — two elements that share a key and never overlap cannot show
|
||
a difference — but *probably* is doing the work in that sentence, and nothing has
|
||
measured which. The port is unaffected either way: it draws
|
||
`ui_layout::derived_paint_order` verbatim and derives no order of its own.
|
||
|
||
## Correction — the runtime "clipping" I flagged 🔴 twice was overstated
|
||
|
||
P6 and P7 both filed 🔴 *"the runtime mix has no headroom"* on the strength of a
|
||
peak reading of 0.0 dBFS off the Master bus. Measured properly:
|
||
|
||
| | samples at full scale | of total | longest clamped run |
|
||
|---|---|---|---|
|
||
| P6 walk (5.944 s) | 43 | 0.0082 % | 10 samples — **0.23 ms** |
|
||
| P7 new-game run (98.453 s) | 24 | 0.00028 % | 11 samples — **0.25 ms** |
|
||
|
||
That is not a headroom defect. It is the disc's own `confirm` cue, mastered near
|
||
full scale (+0.18 dBFS after a lossy decode), touching the ceiling for a quarter
|
||
of a millisecond on a transient — and possibly only in the recording's 16-bit
|
||
conversion, since Godot mixes in float and `AudioEffectRecord` saves `s16`.
|
||
|
||
**Nothing is changed, and that is the point.** Attenuating the mix to buy
|
||
headroom would be an unmeasured decision about level — the same class of thing
|
||
this port refused for the BGM loop point and the stem balance. Refusing it there
|
||
and taking it here would be inconsistent, and it would trade an inaudible
|
||
0.25 ms clamp for an audible change nobody measured.
|
||
|
||
**A peak reading is not a clipping measurement.** One sample at 0 dBFS and two
|
||
seconds of square wave give the same number, and I reported the first as though
|
||
it were the second — twice, in red, in two milestones' write-ups.
|
||
|
||
## The P1 regression harness had been broken since the monorepo merge, 2026-08-29
|
||
|
||
`tools/port/verify-screen` is the P1 gate's regression detector: Godot's drawing
|
||
of a screen against `sylpheed-cli screen render` of the same build. It had not
|
||
been run since P1, across four milestones that changed the renderer — rotation,
|
||
the focus record, the spinning ring, two builds composited at once.
|
||
|
||
It could not have been run. **It resolves its reference binary to a path that
|
||
`build-reference-cli` stopped being able to produce.** That script greps
|
||
`crates/sylpheed-export/Cargo.toml` for
|
||
|
||
```
|
||
sylpheed-formats = { git = "…Syplheed-Reborn.git", rev = "…" }
|
||
```
|
||
|
||
and the monorepo merge (`65cefa7`) replaced that line with
|
||
`{ path = "../sylpheed-formats" }`. The grep returns nothing, the script exits 1,
|
||
and the binary left at `reference-cli/sylpheed-cli` is whatever predated the
|
||
merge — here, **three hours older than the sources** and built from a revision
|
||
nothing in the tree points at any more.
|
||
|
||
Running the diff against it would have compared the port to a decoder from
|
||
another era and called the result a regression check. `DECISIONS.md` already
|
||
carries *"The reference renderer was stale for three diff runs"* from P2. This
|
||
would have been the fourth, and the mechanism was different: not a forgotten
|
||
rebuild, but a **build step that could no longer succeed and a consumer that
|
||
only checked whether the file existed**.
|
||
|
||
### The fix is a deletion, not a repair
|
||
|
||
The revision-keying solved a two-repo problem: `/reborn`'s `target/` was a live
|
||
mount of the other agent's checkout and moved mid-run, so a pixel disagreement
|
||
against it had a free variable in it. **The monorepo removed that problem by
|
||
construction** — the exporter, the reference and the port now read one decoder,
|
||
the working tree's. So `verify-screen` builds `sylpheed-cli` from the workspace.
|
||
`SYLPHEED_CLI` still overrides for anyone who wants to pin one deliberately.
|
||
|
||
### The baseline, all 16 screens
|
||
|
||
```
|
||
build_00/01 max 3 over3 0 OK
|
||
press_start(_jp) max 1 over3 0 OK
|
||
title max 6 over3 790 DIFFERS
|
||
main_menu(_jp) max 4 over3 0 DIFFERS
|
||
extras(_jp) max 3 over3 0 OK
|
||
publisher_logo(_r) max 1-2 over3 0 OK
|
||
developer_logos(_r) max 2 over3 0 OK
|
||
title_jp max 155 over3 20498 DIFFERS
|
||
build_12/15 max 0 over3 0 OK
|
||
```
|
||
|
||
**No new drift.** Four milestones of renderer change and the only screen with a
|
||
substantial disagreement is `title_jp` — which is the *same* one P1 recorded and
|
||
left open: `ptlogo_eff2` is the single drawn element in the whole export at a
|
||
scale that is not a whole multiple of 100 % (125 %), and the two renderers pick
|
||
different source texels there. `ui_layout::blit` samples at the destination
|
||
pixel's top-left corner, a GPU at its centre. **The port has still not changed to
|
||
match**, because matching would mean reproducing a half-pixel bias on purpose to
|
||
make a number smaller. Only an oracle capture settles it.
|
||
|
||
`title`'s 790 pixels at ≤ 6/255 are the same class, one texel wide, on the logo's
|
||
scaled edges. `main_menu` and `main_menu_jp` say DIFFERS on a max of 4 with
|
||
**zero** pixels over the bar — a couple of pixels differing in a single channel.
|
||
|
||
### `max` alone could not tell 2 pixels from 25 000
|
||
|
||
The script reported only the largest difference anywhere in the frame, so
|
||
`main_menu` (two pixels) and `title_jp` (2.8 % of the frame) produced the same
|
||
verdict. It now also reports how many pixels are over the bar.
|
||
|
||
**The bar itself is not raised.** Tuning a threshold until things match is the
|
||
failure the script's own header warns about; adding a second number is
|
||
information, not a loosened bound. ⚠️ The count is thresholded on **greyscale
|
||
luma** while `max` is a per-channel maximum, so they are not two views of one
|
||
measurement — a per-channel check counts 957 on `title` where the luma count
|
||
says 790.
|
||
|
||
### What this harness cannot see, stated because the OK rows look reassuring
|
||
|
||
It renders `--pose=rest`. That is deliberate — it holds both renderers to the
|
||
same declared pose so the test is *port vs reference* and not *rest vs timeline*
|
||
— but it means **none of this iteration's or the last four's visible work is
|
||
under test**: not the spinning focus ring, not the plate composited over the
|
||
title, not any timeline behaviour, not audio. Sixteen OK rows are a statement
|
||
about the resting composite and nothing else.
|
||
|
||
And it remains what its header says: a consistency check between two renderers
|
||
that share their assumptions. Both have been wrong together three times —
|
||
`pteff05`, scale-0, `rest()` — and each time only a capture caught it.
|
||
|
||
## Refutation — "builds 0/1 and 10/11 are the loading screen" is false in the index space this export uses
|
||
|
||
**The claim**, from the RE agent 2026-08-29, answering the port's ask to name
|
||
`GP_TITLE`'s unnamed bundles: *"builds 0/1 and 10/11 are the loading screen,
|
||
decoded from their own `pgloading_*` element names."*
|
||
|
||
**In this export, entries 10 and 11 are the splash screens**, and it is not close:
|
||
|
||
| entry | elements |
|
||
|---|---|
|
||
| 10 | `palogo_eff0`, **`palogo_sqex`**, `palogo_sqex_eff` |
|
||
| 11 | `palogo_eff0`, **`palogo_gamearts`**, `palogo_seta`, `palogo_anima` … |
|
||
| 12 / 15 | `pgloading_eff00`, `pgloading_loop1`, `pgloading_str` … |
|
||
|
||
Entry 10 is the **SQUARE ENIX** wordmark and 11 the developer logos — which the
|
||
same agent identified, in the answer to the port's ask 1, as *"entries 10/13 are
|
||
the SQUARE ENIX publisher wordmark, the first thing the boot shows"*.
|
||
|
||
**Verdict: the finding is almost certainly right and the index space is wrong.**
|
||
Over the twelve bundles `is_build` accepts — entries 0,1,2,3,4,5,6,7,8,9,12,15 —
|
||
ordinals 10 and 11 are entries **12 and 15**, which are exactly the two dressed
|
||
loading variants. So "0/1 and 10/11" is the `is_build` ordinal, and this export
|
||
addresses by **pak entry index**.
|
||
|
||
**Why this is worth a section rather than a shrug.** `authored/screen_names.json`
|
||
is keyed by entry index, and the exporter's own comment says why: *"keyed by
|
||
ENTRY, not by the enumeration ordinal — widening the enumeration to reach the
|
||
splash renumbers the ordinals, and a name that moves when the rule changes is not
|
||
a name."* Someone reading that message and writing keys `"10"` and `"11"` would
|
||
**name the publisher wordmark and the developer logos as loading screens**, and
|
||
the export would validate, and the boot would still run.
|
||
|
||
Two enumerations of the same archive differ by exactly the four bundles the port
|
||
had to add an allow-list to reach. That is the sharpest possible demonstration of
|
||
why the exporter switched, and it has now nearly caused the error it switched to
|
||
prevent. Reported; the names are still the RE agent's to give.
|
||
|
||
## The intro's missing dialogue was an export gap, not a transcode bug, 2026-08-29
|
||
|
||
A human play-test heard music under the boot intro and no voices. The obvious
|
||
reading is that the 5.1→stereo fold dropped the centre channel, and it is wrong.
|
||
|
||
**`ADV.wmv` carries music and effects only.** On this disc a cutscene's voice is
|
||
a *separate asset*: one continuous XMA stream in `sound.pak`, bound to the movie
|
||
by the manifest in `tables.pak` (`ADV` → `VOICETRACK = VOICE_ADV`). Nothing was
|
||
dropped — `grep -rn voice crates/sylpheed-export/src/` returned nothing, because
|
||
the exporter had never been asked for it. The transcode was correct the whole
|
||
time, which is why every measurement on it passed.
|
||
|
||
That is worth stating plainly because the failure *looked* exactly like a codec
|
||
bug, and `docs/port/AUDIO-VERIFICATION.md` is full of ways to measure a
|
||
transcode against its source. Every one of them would have come back clean.
|
||
|
||
### The binding is resolved, and must never be matched by name
|
||
|
||
`audio::export_voice` takes exactly one route:
|
||
`media::resolve_movie_voice_region(source, movie, VoiceLang::English)`, which
|
||
walks movie → cue token (manifest) → sound id (registry) → a `[start, end)` byte
|
||
region of the continuous stream. The cheap route — read `VOICE_<movie>.slb` —
|
||
was not taken, and the reason is a measurement:
|
||
|
||
| movie | resolved region | inside the bank named after it? |
|
||
|---|---|---|
|
||
| `ADV` | 433 930 240…437 044 592 | yes |
|
||
| `S00A` | 452 798 464…455 499 120 | yes |
|
||
| `RT01A` | 437 044 592…437 345 648 | **no — it is inside `VOICE_ADV.slb`** |
|
||
|
||
⚠️ **Name-matching is correct on exactly the two movies this port ships, and
|
||
wrong on the radio cutscenes.** It would have exported clean, verified clean
|
||
against both in-scope movies, and returned the wrong recording the moment
|
||
anybody widened the export. This is the failure mode MISSION §2 names — one
|
||
playable thing is not one archive entry — in its most convincing disguise: the
|
||
spot-checks a person would actually run are the ones it passes.
|
||
|
||
### Three choices, and why none is a guess
|
||
|
||
* **One file per movie**, per MODDING rule 1, and the region's chunks are
|
||
**summed** — see the correction below, because the first version of this
|
||
paragraph said the opposite and was wrong.
|
||
* **Mono**, folded from the stream's **own declared channel count**, probed with
|
||
`ffprobe` rather than assumed. This is not pedantry: `pan` silently ignores a
|
||
channel the input does not have — measured this iteration on the 5.1 fold
|
||
below, where `FLC`/`FRC`/`SL`/`SR` vanished with no warning at all — so a
|
||
stereo matrix applied to a mono voice track is not an error, it is a −6 dB
|
||
attenuation that nothing reports. A track that is already mono is passed
|
||
through untouched.
|
||
* **No sync offset, and no length clamp.** The voice plays from the video's
|
||
first frame, so nothing is authored. The decoded length is recorded in the
|
||
manifest *beside the movie's own length* rather than trimmed to it: the voice
|
||
has no shared container to disagree with, so a length mismatch is the only
|
||
symptom a resolution error would ever show, and clamping would delete it. That
|
||
decision is the reason the error below was caught in the same hour it was made.
|
||
|
||
### Correction, within the hour — the chunks are stems, and I had concatenated them
|
||
|
||
The first version of `export_voice` joined the region's chunks end to end and
|
||
produced **359.201 s of voice for a 137.437 s movie**, and **255.460 s for a
|
||
93.779 s one**. Both ratios sit near 3, and both regions decode to 3 chunks.
|
||
|
||
The manifest said so on the first run, because the length was recorded against
|
||
the movie's instead of being clamped to it. A clamp — which is what
|
||
`sylpheed-viewer` does, and what `media`'s own doc comment invites with *"trimmed
|
||
by the caller's length clamp"* — would have produced a file of exactly the right
|
||
duration containing the wrong audio, and every check in
|
||
`docs/port/AUDIO-VERIFICATION.md` would have passed it.
|
||
|
||
Decoding each chunk and timing it (`crates/sylpheed-export/examples/voice_chunks.rs`):
|
||
|
||
| movie | movie length | chunk 0 | chunk 1 | chunk 2 |
|
||
|---|---|---|---|---|
|
||
| `ADV` | 137.437 s | 84.553 | **137.324** | **137.324** |
|
||
| `S00A` | 93.779 s | 68.072 | **93.694** | **93.694** |
|
||
| `RT01A` | — | 0.009 | **34.034** | — |
|
||
|
||
Chunks 1 and 2 are **equal to six decimals and each span the whole movie**. That
|
||
is HANDOFF Q10's decoded shape — *two stems of one performance, played together;
|
||
do not concatenate* — showing up on a second asset kind. They are summed at
|
||
`1/n`, exactly as `export_bgm` sums a music bank.
|
||
|
||
⚠️ **Chunk 0 is dropped and its status is open.** Its duration matches nothing:
|
||
84.6 s under a 137 s movie, 9 ms under `RT01A`. `docs/re/REFUTED.md` records
|
||
`to_xma_riffs`'s hybrid branch emitting a **leading headerless packet region**
|
||
ahead of the real `RIFF` waves, and `docs/port/BLOCKED.md` already carries that
|
||
as an open row against `BGM_103`, where `media` returns three sub-waves against a
|
||
census of two. **This is the same signature on an independent asset kind** — good
|
||
corroboration, not proof, and the port is not entitled to close it. So the
|
||
selection rule is written in terms of the measurement (*keep the longest
|
||
duration and everything tying with it*), and every dropped chunk is named in the
|
||
manifest with its length.
|
||
|
||
This is the media-assembly trap MISSION §2 names, and it caught me: I wrote a
|
||
doc comment asserting concatenation, gave the reason, and had it wrong. What
|
||
saved it was refusing to clamp — the one decision in the first version that was
|
||
made for the right reason.
|
||
|
||
### What a `None` means
|
||
|
||
A movie whose region does not resolve is **genuinely unvoiced** — the honest
|
||
answer for most `hokyu_*` resupply cutscenes — and gets a manifest warning, not
|
||
a substitute. The corpus already paid for the alternative: resolving unbound
|
||
movies through a shared demo line played the *wrong recording*.
|
||
|
||
This is **decoded, not authored**, so it runs outside the `authored/audio.json`
|
||
block in `main.rs`. Nothing new goes in `authored/`; there is nothing here we
|
||
decided.
|
||
|
||
## Refutation, of my own exporter — MISSION §6 pins a downmix matrix, and the exporter ships a different one
|
||
|
||
**The claim under test is the port's**, not another agent's, and it has been in
|
||
`video.rs` since P4: that the 5.1 fold is normalised by
|
||
`1/(1 + √½ + √½) = 0.4142` because *"the unnormalised form was measured too and
|
||
**clips**: peak 0.0 dBFS."*
|
||
|
||
That sentence rests on a peak reading. `docs/port/BLOCKED.md` records this port
|
||
withdrawing a 🔴 runtime-clipping flag on precisely the grounds that **a peak
|
||
reading is not a clipping measurement** — one sample at full scale and two
|
||
seconds of square wave give the same number. So the justification for deviating
|
||
from a matrix a human pinned was produced by an instrument this port has already
|
||
declared unfit for the question.
|
||
|
||
### Measured properly, over the whole of both movies
|
||
|
||
Decoded to 32-bit float so nothing is pre-clamped, then counted: samples at or
|
||
over full scale, how many exceed it by more than 1 dB, and the longest
|
||
consecutive run.
|
||
|
||
| | peak | RMS | ≥ full scale | > +1 dB | longest run |
|
||
|---|---|---|---|---|---|
|
||
| `ADV`, MISSION §6 matrix | **+4.26 dBFS** | −14.55 | **4 406** / 13 187 900 | 1 874 | 16 samples (0.333 ms) |
|
||
| `ADV`, exporter's matrix | −3.39 dBFS | −22.21 | 0 | 0 | — |
|
||
| `S00A`, MISSION §6 matrix | **−1.34 dBFS** | −18.73 | **0** | 0 | — |
|
||
| `S00A`, exporter's matrix | −8.99 dBFS | −26.39 | 0 | 0 | — |
|
||
|
||
**The claim survives, and the reasoning behind it does not.** The pinned matrix
|
||
genuinely overloads `ADV`: not one stray sample but 4 406 of them, 1 874 more
|
||
than a full dB over, wanting 4.26 dB more headroom than the container has. That
|
||
is a different animal from the 43 samples and 0.25 ms transient I withdrew a flag
|
||
over, and the number that separates them is the **magnitude**, not the count.
|
||
|
||
But the same table refutes the *scope* of the fix. **`S00A` never clips under the
|
||
pinned matrix** — it peaks at −1.34 dBFS. The exporter attenuates it by 7.65 dB
|
||
to solve a problem it does not have, because 0.4142 is derived from a theoretical
|
||
worst case (every channel correlated at full scale at once) that neither movie
|
||
comes near.
|
||
|
||
### Control, before believing any of it
|
||
|
||
The pinned matrix names `FLC`, `FRC`, `SL` and `SR`, and a 5.1 source has none of
|
||
them. ffmpeg neither errors nor warns — measured at `-loglevel warning`, the
|
||
output was empty. So the literal string was decoded alongside its three-term 5.1
|
||
reduction (`FL = 1.0·FL + 0.707·FC + 0.707·BL`) and the two outputs compared:
|
||
**bit-identical**, 52 751 600 bytes. The reduction is what runs, and it is the
|
||
matrix §6 intends. *That silence is itself the trap the mono fold above guards
|
||
against.*
|
||
|
||
### Not changed, and deliberately so
|
||
|
||
MISSION §6 is a **human decision of 2026-08-29**, and the level of a mix is
|
||
exactly the kind of thing §6 reserves — *"adjust it deliberately, as a commit"*.
|
||
Three options, and choosing between them is not mine:
|
||
|
||
1. **Keep the pin.** `ADV` clamps on 4 406 samples. Rejected on the measurement.
|
||
2. **Keep the exporter's 0.4142.** Preserves the two movies' relative loudness
|
||
exactly, costs 7.65 dB, and is safe by construction for any movie a modder
|
||
drops in.
|
||
3. **One measured constant, `1/1.6339 = 0.612`.** The smallest single scalar
|
||
under which no in-scope movie clamps: +3.39 dB over today, still one constant
|
||
so relative loudness is untouched. Tuned to two files, but the exporter's own
|
||
`check` refuses any export whose peak reaches 0 dBFS, so a third movie that
|
||
needed more headroom would fail loudly rather than clamp quietly.
|
||
|
||
Per-file normalisation is **not** on that list: it would put `ADV` 4.26 dB below
|
||
`S00A` and change how two cutscenes sit against each other and against the menu
|
||
bed, which is an aesthetic decision with nothing measured behind it.
|
||
|
||
What changes today is only that the deviation is **visible**: `video.rs` now
|
||
cites MISSION §6 by name and says it departs from it, and the export carries a
|
||
manifest warning with these numbers. Before this, a reader of the manifest could
|
||
not tell that a pinned human decision had been overridden at all — the command
|
||
line was recorded faithfully, and recording the command you ran does not disclose
|
||
that it is not the command you were given.
|
||
|
||
### The voice reaches the output, and a null control says so quantitatively
|
||
|
||
`+ voice ADV` in the log proves only that `play_voice` found a stream and called
|
||
`play()`. Whether the audio arrives at the Master bus is a different question,
|
||
and `docs/port/AUDIO-VERIFICATION.md` §2 exists because it is.
|
||
|
||
The control needed **no test-only code**: MODDING rule 4 already shadows any
|
||
exported asset by path, so 140 s of silence dropped at
|
||
`data/mods/audio/voice/ADV.ogg` mutes the dialogue and changes nothing else. Two
|
||
`--boot --skip-at=25 --audio=…` runs, then `astats` over the same 14 s of movie:
|
||
|
||
| | peak | RMS |
|
||
|---|---|---|
|
||
| `ADV.ogv`'s own audio (the bed) | −6.239 | −24.941 |
|
||
| the exported voice alone | −7.614 | −27.965 |
|
||
| **run with the voice muted** | **−6.251** | **−25.126** |
|
||
| **run with the voice playing** | **−5.415** | **−22.913** |
|
||
|
||
The muted run reproduces the bed to **0.01 dB peak / 0.19 dB RMS**, which is what
|
||
makes the other row worth reading. And the mixed run is not merely *louder*: two
|
||
incoherent sources at −24.941 and −27.965 dBFS predict a sum at **−23.184**, and
|
||
the run measures **−22.913** — **0.27 dB** out. The voice is in the mix, at the
|
||
level its own file says it should be.
|
||
|
||
⚠️ **Under the Dummy driver.** Per AUDIO-VERIFICATION, *"recorded under a dummy
|
||
driver"* is a weaker claim than *"heard"*, and no measurement here says the
|
||
recording is the **right** dialogue for this cutscene — only that the file the
|
||
exporter resolved is the one reaching the output at the expected level. The two
|
||
runs are also not sample-aligned (they differ by 1.7 s of wall clock), which is
|
||
why the `RMS trough` column is omitted: it moved by 40 dB between runs on window
|
||
placement alone, and peak and RMS are the two numbers that survive that.
|
||
|
||
### Ⓐ *does* skip the intro in this build, so the play-test's report is not this bug
|
||
|
||
`--skip-at=25` on a `--boot` run: `video skipped at 25.02 s`, `video ended at
|
||
25.02 s`, title at 25.02 s. The press goes through `Input.parse_input_event` and
|
||
arrives at `_unhandled_input` exactly as a pad's would, so **the wiring from press
|
||
to skip is live**. What that does not cover is a real key event from a focused
|
||
window, which is the difference between this run and the human's — and, separately,
|
||
**whether the game permits skipping an attract movie at all** is HANDOFF Q9 and
|
||
still 🟡. If the answer is no, this path is deleted rather than debugged.
|
||
|
||
|
||
## Refutation of my own two-stem reading — and it had already been adopted elsewhere
|
||
|
||
Two hours after writing that a voice region's equal-length chunks are *"HANDOFF
|
||
Q10's decoded two-stem shape"*, the Decoder asked me to decode the leading chunk
|
||
— it has no XMA1 decoder in its container — and the decoder run refuted the
|
||
claim I had made.
|
||
|
||
**Equal duration was a shape match, and I carried Q10's *music* census across to
|
||
voice on the strength of it.** The content does not support it:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `S00A` chunk 2 | **digital silence** — 4 497 300 samples, peak −inf |
|
||
| `ADV` chunk 2 | **0.60 × chunk 1** (best-fit scalar), residual **26.8 dB** below the target |
|
||
|
||
About 95 % of `ADV`'s second chunk is a −4.4 dB copy of the first. Two chunks of
|
||
equal length, one silence and the other a scaled near-duplicate, are not two
|
||
stems of one performance. ⚠️ **The claim had already travelled** — it is quoted in
|
||
the Decoder's `voice-region-leading-chunk.md` — which is the failure PROTOCOL
|
||
names: a wrong belief moving faster than its correction, through two documents
|
||
that share a source.
|
||
|
||
### What it cost, and what changed
|
||
|
||
Summing chunk 1 with silence at `1/n` put `S00A`'s dialogue **6.02 dB down for
|
||
nothing**: the exported file peaked at −16.2 dBFS against a source chunk peaking
|
||
at −4.2. `export_voice` now drops a **digitally silent** chunk before the sum.
|
||
That is arithmetic, not a content judgement — a silent input contributes nothing
|
||
to a mix and counting it in the normalisation is simply my error.
|
||
|
||
**What `ADV`'s near-duplicate chunk 2 is remains open and it is still summed.**
|
||
Whether the game plays both is a decoding question; 26.8 dB of residual is not
|
||
nothing, and dropping a chunk because it correlates with another would be
|
||
answering it.
|
||
|
||
### The leading chunk, decoded — structure, and not one word about content
|
||
|
||
The Decoder's ask was *"cutscene dialogue or mission dialogue"*. `ADV` region
|
||
+ 1392, 394 packets: **84.553 s, stereo, 48 kHz, peak −2.48 dBFS, RMS −24.80**,
|
||
with **6 silent gaps over 0.4 s below −50 dB totalling 45.3 s** — 54 % silence,
|
||
the same duty cycle as the two full-length chunks (54 %, 55 %). So it is
|
||
**speech-structured audio**: not a header, not padding, not noise.
|
||
|
||
🔴 **Which is as far as a measurement goes.** *Cutscene or mission* is an
|
||
identification and this agent has no ears and no oracle. Envelope
|
||
cross-correlation against the full-length chunks peaks at 0.768 **at the last lag
|
||
in the search range**, which is where a statistic lands when it has found
|
||
nothing, and it is not evidence. The Decoder's 🟡 stands, and its own leading
|
||
hypothesis — an in-mission `VOICE_D_*` line — is untouched by any of this. The
|
||
byte-span test it already built settles it the moment those regions are
|
||
enumerated; nobody has to listen.
|
||
|
||
### Taken from the same message: `bank_header_len`, not `riffs.len()`
|
||
|
||
The Decoder's census warns that eight bank-header regions also yield three
|
||
chunks, so the chunk count cannot say which structure you are in. **This exporter
|
||
never used the count** — it selects on decoded duration, which is why it already
|
||
handles both cases: `RT01A`'s 10 300 B leading chunk decodes to 9 ms and falls
|
||
out on its own. But a duration tie is an *observation* and `bank_header_len` is
|
||
*decoded*, so the rule switches the day `c1f3608` reaches `main`.
|
||
`sylpheed-formats` is a path dependency and merging another agent's topic branch
|
||
is not the port's to do.
|
||
|
||
## The mono fold I warned about, in the comment directly above the code that did it
|
||
|
||
`export_voice`'s first version folded to mono by averaging every **declared**
|
||
channel, and the doc comment above it said, in as many words, that *"`pan`
|
||
silently ignores a channel the input does not have — so a stereo matrix applied
|
||
to a mono voice track is not an error, it is a −6 dB attenuation that nothing
|
||
reports."*
|
||
|
||
It then did exactly that. Per-channel `astats` on both voice streams:
|
||
|
||
| | channel 1 | channel 2 |
|
||
|---|---|---|
|
||
| `ADV` chunk 1 | peak +0.000 dBFS | **peak −inf** |
|
||
| `S00A` chunk 1 | peak −4.207 dBFS | **peak −inf** |
|
||
|
||
The voice is a **mono recording carried in a nominally stereo stream**, and
|
||
averaging it with silence cost **5.94 dB** — which is most of why `S00A`'s
|
||
exported dialogue sat at −16.2 dBFS against a source chunk peaking at −4.2 (the
|
||
other 6.02 dB was summing a silent *chunk*, corrected in the same iteration).
|
||
|
||
**Checking the declared channel count is not checking the content, and only the
|
||
content is the fold.** `live_channels` now measures which channels carry signal
|
||
and averages only those. `sylpheed-viewer`'s `pan=mono|c0=c0` reaches the right
|
||
answer here for a reason it does not state; this reaches it for a stated one, and
|
||
would still be right if a stream ever did carry two live channels.
|
||
|
||
Worth recording as a pattern rather than a bug: **three defects this iteration
|
||
were all the same shape** — a silent chunk in a sum, a silent channel in a fold,
|
||
and a `pan` matrix naming channels that do not exist. Each is an input that
|
||
contributes nothing being counted in a divisor, and none of them is visible in
|
||
anything but a level.
|
||
|
||
## The leading chunk is the TAIL of the full one — measured, and it is why the region over-covers
|
||
|
||
The Decoder settled by byte-span analysis that a voice region's leading chunk is
|
||
**the movie's own dialogue, 17 of 17** — killing its own standing hypothesis that
|
||
it was an in-mission `VOICE_D_*` line — and asked whether dropping it is
|
||
therefore a truncation. It has no XMA1 decoder; this container does.
|
||
|
||
Envelope cross-correlation, sliding with overhang allowed at both ends and
|
||
normalised over the overlap only. ⚠️ **This corrects an earlier number of mine**:
|
||
a first pass scored 0.768 and I called it nothing, correctly — that search only
|
||
tried lags where the shorter chunk fitted *wholly inside* the longer one, and it
|
||
peaked on the boundary of its own range.
|
||
|
||
| | best *r* | at lag | overlap |
|
||
|---|---|---|---|
|
||
| `ADV` chunk 0 → chunk 1 | **0.998** | **+52.8 s** | 84.5 s |
|
||
| `S00A` chunk 0 → chunk 1 | **0.932** | **+25.6 s** | 68.0 s |
|
||
| control — `ADV` chunk 0 against itself | 1.000 | 0.0 s | — |
|
||
| control — `ADV` chunk 0 against `S00A` chunk 1 | **0.289** | — | 28.2 s |
|
||
|
||
**Both lags put chunk 0 flush against the end of chunk 1**: 52.8 + 84.55 =
|
||
137.35 s against chunk 1's 137.324, and 25.6 + 68.07 = 93.67 against 93.694.
|
||
|
||
Confirmed in the sample domain — lag refined to ±1 sample on the loudest second,
|
||
then a scalar best-fit over the whole overlap: `ADV` +52.8000 s, gain 0.833,
|
||
residual **16.70 dB** below the target; `S00A` +25.6320 s, gain 0.365, residual
|
||
**23.15 dB**. 98–99.5 % of the energy is a scaled copy: the same material at a
|
||
different gain, not bit-identical, which is what a lossy decode at two gains
|
||
should look like.
|
||
|
||
**So dropping chunk 0 removes a duplicate, and is not a truncation** — the
|
||
exporter's existing behaviour is right for a better reason than the one it gave.
|
||
🟡 **The manifest note has NOT been rewritten to say so.** The structural claim —
|
||
that the region over-covers because it re-presents its own tail, and that this
|
||
accounts for the whole 2.6× — is the Decoder's to write down; this page reports
|
||
the measurement and says which is which. The note stays hedged until its page
|
||
carries the conclusion, and the hedge is true either way.
|
||
|
||
⚠️ **The 504 464 B constant was deliberately not converted.** The Decoder found
|
||
the region anchor sitting that far after the true predecessor trailer on all 17
|
||
and pointedly declined to call it missing dialogue. Converting it needs a
|
||
byte↔time mapping, and the numbers above are the reason there isn't one: chunk 1
|
||
is 1 118 268 B and chunk 2 is 1 171 516 B for **the same 137.324 s**, so bytes per
|
||
second is not constant even inside a single region. Any figure in seconds off
|
||
that constant would be invented.
|
||
|
||
## Third reading of a voice region, and this one is decoded: three presentations of one take
|
||
|
||
`export_voice` has now read the same bytes three ways in one session, and each
|
||
reading was ended by a measurement rather than by an argument:
|
||
|
||
1. **Concatenate the chunks** — 359 s of dialogue for a 137 s movie.
|
||
2. **Sum them as HANDOFF Q10's two stems** — refuted here: `S00A`'s second
|
||
full-length chunk is digital silence, `ADV`'s is 0.60 × the first with 26.8 dB
|
||
of residual.
|
||
3. **Keep one stream.** ✅ This one is decoded, and not by me.
|
||
|
||
The Decoder settled the shape disc-wide without a decoder, by counting stream
|
||
starts inside every inter-descriptor span: **258 spans hold one stream, 28 hold
|
||
three, and nothing holds two or any other number.** The 95 movie-voice regions
|
||
decompose 70 + 8 + 17, and the 8 are independently the same 8 its first census
|
||
flagged. So a region carries **three presentations of one take** — which is
|
||
exactly `359 = 84.55 + 137.32 + 137.32`, the first clipped by its crate's own
|
||
1.5 MB predecessor guard.
|
||
|
||
It also cross-checked my correlation by a route needing no decoder: if the
|
||
leading chunk is the tail of a full stream, the whole leading stream should be
|
||
one complete take, and `ADV`'s 504 464 + 808 304 = 1 312 768 B at chunk 0's byte
|
||
rate is **137.323 s against my measured 137.324**. Two instruments, no shared
|
||
assumption.
|
||
|
||
**So summing was wrong for a third reason:** a take plus a 0.60 × copy of itself
|
||
is ~4 dB louder and coloured, not a mix of parts. The exporter keeps one stream
|
||
and performs no arithmetic on it.
|
||
|
||
🟡 **Which stream is a recommendation, not a decoded field.** The selector is the
|
||
**highest byte rate** among the equal-duration survivors, on the Decoder's
|
||
advice. Nothing on the disc says which presentation the game plays, and on `ADV`
|
||
this picks the **quieter** of the two — −8.3 dBFS against 0.0. That is in the
|
||
manifest in those words so the choice is visible and reversible; it is the one
|
||
part of this that a capture could still overturn.
|
||
|
||
`check` moves `voice` off the strict peak bound as a consequence. It sat with
|
||
`bgm` because it was a sum this exporter produced; it is now a single wave off
|
||
the disc, mastered near full scale — `ADV`'s louder presentation measures
|
||
**+0.0003 dBFS at source** — and refusing that would be refusing the disc's own
|
||
mastering.
|
||
|
||
### The 504 464 B constant: I refused the conversion, and refusing was right
|
||
|
||
The Decoder asked whether I would spend a decode converting its anchor offset to
|
||
seconds, and I declined because bytes per second is not constant even inside one
|
||
region. It has since found the stronger reason and withdrawn the ask: **the
|
||
constant is structural, not proportional** — identical on all 17 regions despite
|
||
their differing durations. A proportional prediction lands within 8 bytes on
|
||
`ADV`, which is a coincidence, and is **4 305 B out on `S00A`**. A seconds figure
|
||
off that constant would have been invented, and it would have looked corroborated
|
||
on the first movie anybody checked.
|
||
|
||
❔ **Why the disc stores three presentations at all is unanswered**, by either of
|
||
us.
|
||
|
||
## The transcode cache had never hit, because the wipe ran first
|
||
|
||
`video::transcode` has carried a cache since P4. It writes a `.cmd` sidecar with
|
||
the exact ffmpeg command, the source's byte count and its channel count, and
|
||
skips the encode when all three still match. Its doc comment says why: *"without
|
||
it every re-export pays ~4 minutes to produce a byte-identical file, and an
|
||
exporter nobody re-runs is worse than a cache."*
|
||
|
||
**It had never hit once.** `main.rs` clears the output tree wholesale — and the
|
||
`remove_dir_all` runs immediately before the check, deleting the sidecar and the
|
||
output it stamps. The cache tested a file it had just erased.
|
||
|
||
This session ran the exporter **six times** and paid the full Theora encode every
|
||
one of them, producing five byte-identical files. Roughly 48 minutes. Nothing
|
||
reported it, and nothing could have: a cache is silent when it works and silent
|
||
when it does not, and the only symptom is a wall-clock cost that looks like the
|
||
job simply being slow.
|
||
|
||
⚠️ **It is worth being specific about how this hid**, because the ingredients are
|
||
ordinary. The cache is correct. The wipe is correct. Each carries a doc comment
|
||
explaining itself, and neither mentions the other. The defect exists only in
|
||
their ordering, which is stated in neither, and the cost is invisible in every
|
||
artefact the export produces — the tree is byte-identical either way.
|
||
|
||
### The fix keeps the wholesale guarantee rather than trading it away
|
||
|
||
The obvious repair — stop wiping — would break what the wipe is for: *a screen
|
||
that stops being exported stops existing, rather than lingering as a stale file
|
||
that still validates.* So the wipe now spares exactly `video/`, and
|
||
`prune_videos` deletes anything in it this run did not claim. Everything else is
|
||
still cleared outright.
|
||
|
||
That is a **cache, not a hand-edit**, and the distinction matters against
|
||
MISSION §3: nothing in `export/` is authored, the sidecar is derived from the
|
||
command the exporter itself computed, and any change to the command, the source
|
||
size or the channel count re-encodes. A modder who edits an `.ogv` by hand gets
|
||
it overwritten on the next export, exactly as before.
|
||
|
||
🟡 **Not measured yet:** that a cached run reproduces the same tree. The claim is
|
||
structural — the skip is keyed on the whole command string — but "the second run
|
||
produces the same bytes" is checkable and has not been checked.
|
||
|
||
## `settle_time()` — the answer arrived, and it refutes my own 🔴 more than it confirms it
|
||
|
||
The Decoder took the port's top ask and measured the boot on a cold profile with
|
||
no shader cache (`auto/no-disc-and-menu-captures` at `4bd4779`,
|
||
`docs/re/boot-settle-times-measured.md`). It confirms the *principle* I filed:
|
||
the title's `rest.t` is 251 units = **4.183 s** where its art is finished at about
|
||
2 s, so `rest.t` is not when a screen arrives.
|
||
|
||
**But my row said more than that**, and the extra part is wrong. It said
|
||
*"everything the boot sequencer paces off that landmark is therefore late"*, and
|
||
named `publisher_logo` and `developer_logos`. So I measured the port the way the
|
||
game was measured — **visible span, not arrival-to-arrival** — with `--film` at
|
||
4 fps and a per-frame greyscale mean:
|
||
|
||
| | port, visible span | game, three cold boots | |
|
||
|---|---|---|---|
|
||
| publisher wordmark | **4.25 s** | 4.297 / 4.604 / 4.370 | 0.05 s under the lowest |
|
||
| developer logos | **3.50 s** | 3.508 / 3.503 / 3.366 | **dead on** |
|
||
| black hold between | ≈0.25 s | 0.2 – 0.3 s | inside |
|
||
| title settled → plate | 2.000 s (declared 120 units) | 2.247 s | inside, at ~28 fps presentation |
|
||
|
||
**The splashes are not late. They match.** ⚠️ And the reason my earlier reading
|
||
said otherwise is worth keeping: I had compared the port's *transition
|
||
timestamps* — 4.68 s and 3.94 s, arrival to arrival — against the game's
|
||
*visible spans*. Those differ by the exit ramp plus the black hold, about 0.6 s,
|
||
which is the whole of the discrepancy I was about to chase. This corpus has been
|
||
bitten by exactly this before, in the plate delay: *"timed from where build 4
|
||
stops animating, not from where it first appears — measured the other way the two
|
||
runs differ by 0.48 s against 6 ms."*
|
||
|
||
So the port paces the boot correctly, and **`rest.t` is a wrong landmark whose
|
||
blast radius is much smaller than I claimed**: on the screens the sequencer
|
||
actually advances off, `rest.t` plus the 24-unit exit ramp lands where the game
|
||
lands. What it still affects is `_script_settled`, which waits longer than it
|
||
needs to before photographing — a slow test, not a wrong frame.
|
||
|
||
### `dwell_seconds` stays `null`, and the question is now closed rather than open
|
||
|
||
`authored/timing.json` says of it: *"If a capture ever times the real boot, this
|
||
is where that number goes."* A capture has now timed the real boot, and the
|
||
answer is that **nothing goes there** — the disc's own keyframe groups reproduce
|
||
the game's dwells to 0.05 s and 0.01 s. The field stays `null` for a measured
|
||
reason instead of an absence of one.
|
||
|
||
### Taken from the same page, and not taken
|
||
|
||
* ✅ **The 120-unit plate delay stands.** The Decoder ran a refutation of it that
|
||
failed instructively: its probe's `title_static` mark gave 3.203 s, which on a
|
||
cold boot looks like a real effect. It was the instrument — the mark fires
|
||
during the crossfade out of the attract movie, with the glyph count still 0.
|
||
Re-measured from content: 2.247 s. **The port changes nothing**, and the
|
||
declared 120 units is what it keeps.
|
||
* 🔴 **No Ⓐ→menu dwell is authored.** It measured 3.763 s and contains a 1.53 s
|
||
guest load stall — the third independent reproduction of that stall, this one
|
||
on a cold cache, so it is not a warm-cache artefact. It is emulator time, not a
|
||
game constant.
|
||
* 🟡 **Menu build-in 0.531 s and Ⓑ→title 0.482 s are not authored either**, and
|
||
that is the Decoder's own caveat rather than my caution: they rest on one run,
|
||
where the plate delay and the load stall are each cross-checked against
|
||
independent prior evidence. The port is within ~0.1 s of both with its existing
|
||
24-unit exit ramp, so authoring them would replace a disc-derived number with a
|
||
provisional measured one and gain nothing measurable.
|
||
|
||
## The voice presentation is now unambiguously the port's choice, and the recommendation behind it was withdrawn
|
||
|
||
The Decoder has withdrawn "highest byte rate": its sentence read *"the
|
||
highest-rate, highest-gain one is chunk 1"*, and those two criteria select
|
||
**different streams** — `ADV` chunk 1 is 1 118 268 B at 0.0 dBFS, chunk 2 is
|
||
1 171 516 B at −8.3. The rule named one and the parenthetical named the other. I
|
||
implemented the rule faithfully and got the quieter presentation.
|
||
|
||
What the file can still say is decoded and does not adjudicate it: the `fmt `
|
||
chunk is a 32-byte `XMAWAVEFORMAT` whose `+0x20` is a declared
|
||
`PsuedoBytesPerSec` — 8 142 and 8 530 on `ADV`'s two, matching the computed rates
|
||
to 0.02 % — but `wEncodeOptions` (`0x10d6`), channel count and channel mask are
|
||
**byte-identical across the presentations**. Nothing in the header ranks them.
|
||
|
||
⚠️ One more observable, measured here and not in that page: **the two
|
||
presentations differ in channel layout.** `ADV` chunk 1 is mono-in-stereo —
|
||
channel 2 digitally silent — while chunk 2 is **dual-mono**, both channels
|
||
identical at −8.318574. So they are not two encodes of one file differing only in
|
||
rate.
|
||
|
||
Also recorded, because it cost the Decoder time: **`sylpheed-cli audio info` is
|
||
not to be trusted on these.** Its "16 channels / 4310 Hz / 2-bit" is
|
||
`wBitsPerSample`, `wEncodeOptions` and the channel fields read at the wrong
|
||
offsets — its XMA1 reader is misaligned. That is a tool in this repository
|
||
reporting confident nonsense, and it is the second time a renderer or reader of
|
||
ours has been believed before it was checked.
|
||
|
||
## Refutation of my dual-mono inference — the measurement stands, the generalisation does not
|
||
|
||
I argued that `highest_rate` had no case because `ADV`'s higher-rate presentation
|
||
is **dual-mono** while its louder one is mono-in-stereo, so the extra bytes buy a
|
||
duplicated channel rather than fidelity. The Decoder tested that disc-wide, as a
|
||
refutation attempt, and **it fails**.
|
||
|
||
Over the 28 three-stream cues, the stream-3 / stream-2 size ratio runs:
|
||
|
||
| min | median | max | sd | within 15 % of 1.0 |
|
||
|---|---|---|---|---|
|
||
| 0.0778 | 1.2565 | 2.9163 | 0.5057 | **12 of 28** |
|
||
|
||
Declared rates scatter with them — `S06A` is 5 661 against 16 513 B/s. **A 37×
|
||
spread is not a duplicated channel.**
|
||
|
||
**The channel measurement itself stands**: `ADV` chunk 1 really is mono-in-stereo
|
||
and chunk 2 really is dual-mono at −8.318574, and that is this port's own decode,
|
||
which the Decoder could not re-run and did not dispute. What fails is the step
|
||
from *one asset* to *the format*.
|
||
|
||
### What this changes, and what it does not
|
||
|
||
Nothing in the export changes. `loudest` is a **per-asset content** rule — it
|
||
reads the peak of the actual streams in front of it — so a scattering structural
|
||
ratio cannot undermine it, and `ADV`'s dialogue at +0.3 dBFS instead of −8.7 is
|
||
plainly the better outcome either way.
|
||
|
||
What changes is the *reason*, in four places: `authored/audio.json`'s
|
||
`presentation_why`, the selector comment in `audio.rs`, `BLOCKED.md`'s row, and
|
||
this page. The honest statement is narrower and slightly less satisfying:
|
||
**`highest_rate` was never refuted — it was never argued for, and neither is
|
||
`loudest`.** Which is exactly why the entry is marked *chosen* rather than
|
||
*measured*, and why one capture deletes it.
|
||
|
||
⚠️ **This is the third claim of mine in two iterations that generalised a
|
||
single-asset observation** — after "the chunks are two stems" and "everything the
|
||
sequencer paces off `rest.t` is late". All three were true of the thing I looked
|
||
at. The pattern is not carelessness about the measurement; it is reaching for the
|
||
rule the measurement would imply if it held everywhere, and writing that down in
|
||
the same breath. The corpus catches it because someone else runs the census.
|
||
|
||
### Two things in that data that are not mine, recorded so they are not lost
|
||
|
||
* **`S12B`'s three streams are byte-size identical** (14 396 each).
|
||
* **`BIRD_224` is three-stream and is not a movie cue** — so the three-stream
|
||
shape is not exclusive to cutscenes, which narrows how it was described to this
|
||
port earlier. Neither affects `export_voice`, which only resolves movies.
|
||
|
||
## Two rows of the P1 baseline were comparing blank frames and reporting OK
|
||
|
||
`docs/port/BLOCKED.md` has carried a 🔴 since P3: *"the loading screen's fade quad
|
||
rests OPAQUE BLACK … it will bite whoever first draws a loading screen."* It had
|
||
already bitten, in the one place nobody looks — the regression harness.
|
||
|
||
`build_12` and `build_15` render as **pure black in both renderers**: mean 0,
|
||
max 0, on the Godot side and on `sylpheed-cli`'s. The difference between two
|
||
blank frames is zero, so `verify-screen` scored them `max 0 over3 0 OK` — the
|
||
strongest verdict it has. **Two of sixteen rows were comparing nothing against
|
||
nothing**, and the committed baseline reads as sixteen passes.
|
||
|
||
That is worse than a missing test. A missing test is visible in the count.
|
||
|
||
### The cause, isolated by a control rather than by reading
|
||
|
||
`build_00` and `build_01` are the *plain* loading variant — the same screen minus
|
||
three elements. They render: **mean 1.913, max 214.5**. `build_12`/`build_15` add
|
||
`pgloading_baseeff`, `pgloading_loop5` and **`pgloading_eff00`**, a 1280×720
|
||
primitive whose `rest` is `0xff000000` — opaque black — at `t=38`, inside its own
|
||
opening black hold (`0xff000000` at 38, `0xff000000` at 48, clear on the untimed
|
||
final). It carries `layer_source: "none"`, so paint order puts it **last**, over
|
||
everything.
|
||
|
||
Same screen, one element different, one renders and one does not. That is the
|
||
diagnosis, and it did not require an opinion about `rest`.
|
||
|
||
### The rule I was about to write, and the census that killed it
|
||
|
||
The obvious reading is that `rest.t = 38` is wrong because it precedes the
|
||
element's last timed keyframe at 48 — so "`rest.t` before the last timed
|
||
keyframe" would flag the pathology. **I ran the census before writing the rule,
|
||
and it does not survive: 152 of 212 elements in this export have `rest.t` earlier
|
||
than their last timed keyframe.** It is the norm.
|
||
|
||
What actually distinguishes this element is its *content*, and the reach of that
|
||
is one:
|
||
|
||
| screen | full-frame primitive | rest.t | last | rest fade |
|
||
|---|---|---|---|---|
|
||
| `build_12` / `build_15` | `pgloading_eff00` | 38 | 48 | **`0xff000000`** |
|
||
| `extras` / `extras_jp` | `pteff00` | 64 | 74 | `0x00000000` |
|
||
| `main_menu` / `main_menu_jp` | `pteff00` | 70 | 80 | `0x00000000` |
|
||
| `title` / `title_jp` | `pteff00` | 261 | 269 | `0x00000000` |
|
||
| `title` / `title_jp` | `pteff02` | 46 | 236 | `0x40000000` |
|
||
|
||
**`pgloading_eff00` is the only element in the whole export whose resting pose is
|
||
a fully opaque full-frame quad — 1 of 212.** Every other full-frame primitive
|
||
rests clear or at 25 %. One instance is not a rule about `rest`, and keying the
|
||
renderer on "an opaque full-frame quad at rest is probably wrong" would be a
|
||
content heuristic of exactly the kind this port refuses elsewhere.
|
||
|
||
### So nothing in the renderer changed, and the harness did
|
||
|
||
The screens stay black. Nothing draws a loading screen, and the honest position
|
||
is that either `rest` is mis-identified for this one element — a decoding
|
||
question, asked — or the screen really does begin fully black and `--pose=rest`
|
||
is simply the wrong thing to photograph it at.
|
||
|
||
What changed is that **a blank pair can no longer score.** `verify-screen` now
|
||
checks both frames for ink first and reports
|
||
`BLANK -- both renderers drew nothing; this row proves nothing`. It is not a
|
||
failure — the port may legitimately have nothing to draw — and `status` is
|
||
untouched, so an unrelated `DIFFERS` still fails the run. The corrected baseline:
|
||
|
||
```
|
||
build_00/01 max 3 OK press_start(_jp) max 1 OK
|
||
title max 6 DIFFERS main_menu(_jp) max 4 DIFFERS
|
||
extras(_jp) max 3 OK title_jp max 155 DIFFERS
|
||
publisher_logo(_r) max 1 OK developer_logos(_r) max 2 OK
|
||
build_12 / build_15 BLANK ← previously OK
|
||
```
|
||
|
||
Fourteen rows, not sixteen. No new drift among the fourteen.
|
||
|
||
## Refutation attempt — the loading-screen variants, and it survived
|
||
|
||
The Decoder's `ui-title-build-map.md` says entries 0/1 are the plain loading
|
||
variant at 7 elements and 12/15 the dressed one at 10, the three additions being
|
||
`pgloading_eff00`, `pgloading_loop5` and `pgloading_baseeff`. Checked against this
|
||
export, which addresses by pak entry:
|
||
|
||
| entry | elements |
|
||
|---|---|
|
||
| 0, 1 | 7 — `eff01 eff02 line loop1 loop3 loop4 str` |
|
||
| 12, 15 | 10 — the same seven **plus** `baseeff`, `eff00`, `loop5` |
|
||
|
||
**Exact, in both the count and the identity of the three.** The claim survives,
|
||
and it paid for itself immediately: the two variants differing by exactly the
|
||
black quad is what made `build_00` a control for `build_12` and turned "the
|
||
loading screen is black" into "this one element blacks it out".
|
||
|
||
## 🔴 The voice export is known incomplete — the game decodes all three streams at once
|
||
|
||
The Decoder booted with Canary's `--xma_param_probe=true` — the cvar whose own
|
||
comment exists to say which sub-wave a movie's `.slb` the game decodes — and the
|
||
answer is that **it does not pick one. It decodes all three, concurrently, in
|
||
three separate XMA contexts.**
|
||
|
||
| ctx | packets | byte_size | disc payload (RIFF − 60) |
|
||
|---|---|---|---|
|
||
| 0 | 632 | 1 294 336 | 1 294 396 |
|
||
| 1 | 546 | 1 118 208 | 1 118 268 |
|
||
| 2 | 572 | 1 171 456 | 1 171 516 |
|
||
|
||
Three-way, byte-exact. **So "three presentations of one take, pick one" is
|
||
refuted by the running game**, and the question I had been arguing about —
|
||
*which* presentation — has no answer, because its premise was wrong.
|
||
|
||
### This one was not caught by a census, and could not have been
|
||
|
||
The last three claims of mine that overreached were all killed by counting
|
||
something. This one survived every count available: the streams really are
|
||
equal-duration, one really is silence, one really is 0.60 × another with the
|
||
residual 26.8 dB down. Every measurement was right and the frame around them was
|
||
wrong, and **no amount of looking harder at the file would have moved it** — the
|
||
file says `ChannelMask = 0x0002` on all three. It took the running game.
|
||
|
||
That is the mission's own sentence arriving in practice: *the Port has no oracle
|
||
— if it needs to know what the game does, it asks.* I did ask, repeatedly, and
|
||
each time for the wrong thing: which stream, rather than whether the premise held.
|
||
|
||
### What changed, and what deliberately did not
|
||
|
||
**The behaviour is held.** Reverting to the `1/n` sum is not obviously less
|
||
wrong: an equal-gain sum of channel pairs is **not** a downmix — MISSION §6 makes
|
||
exactly that point when it pins an explicit matrix for the movies' 5.1 fold
|
||
rather than letting ffmpeg default — and the sum cost `S00A` 6.02 dB when one
|
||
stream was silence. Swapping one guess for another on the strength of a message
|
||
is what produced this entry twice already.
|
||
|
||
**What changed is that the wrongness is now loud.** ⚠️ *This failure sounds like
|
||
success*: a single stream decodes to clean, audible dialogue, so nothing a
|
||
listener hears reveals that two streams are missing. So it is stated in three
|
||
places a reader cannot miss — a top-level `manifest.json` warning per movie, the
|
||
console line (`1 of 3 streams -- KNOWN INCOMPLETE`), and the entry's own `why` —
|
||
and 🟡 became 🔴 in `authored/audio.json` and `BLOCKED.md`.
|
||
|
||
🟡 **"They are 5.1" is the Decoder's hypothesis and is not established.** Three
|
||
concurrent stereo streams is six channels and N stereo streams is how XMA carries
|
||
multichannel on the 360, which would explain the differing byte rates, the
|
||
near-silent stream, and why cues are 1-stream or 3-stream and never 2. Against
|
||
it: all three declare `ChannelMask = 0x0002` identically, which is odd for
|
||
distinct channel roles. Nothing here builds on it.
|
||
|
||
**What settles it, and it is asked:** a recording of the game's own output over
|
||
`ADV`, through the PulseAudio null sink (`AUDIO-VERIFICATION` §3). Candidate
|
||
combinations of the three decoded streams can then be correlated against what the
|
||
game actually played — which turns the channel-role question from a decode into a
|
||
fit against an oracle. Twenty seconds over dialogue is enough.
|
||
|
||
### The measurements survive; only their meaning moved
|
||
|
||
`S00A`'s silent stream and `ADV`'s 0.60 × relationship are untouched and now read
|
||
as facts about **channels**: 0.60 × with the residual 26.8 dB down is what a
|
||
correlated channel pair at a lower level looks like, and a silent channel is an
|
||
unused one. Nothing measured here is retracted. What is retracted is every
|
||
sentence that called them *presentations*.
|
||
|
||
## 🔴 The oracle capture does not contain the intro — a controlled negative
|
||
|
||
The Decoder took the capture I asked for — `adv-game-output-6ch.wav`, 70.2 s,
|
||
6 ch, 48 kHz, shared as `1788018994-16f9d19d90b8`, taken at `68aa192` — described
|
||
as *"the FULL mix, the movie's own WMA track plus the three XMA streams"*.
|
||
|
||
**It contains none of them.** Envelope cross-correlation, sliding with overhang,
|
||
normalised over the overlap, minimum 30 s of overlap so a short window cannot win
|
||
on an edge:
|
||
|
||
| capture ch | against | best *r* | runner-up | margin | |
|
||
|---|---|---|---|---|---|
|
||
| c0 | `ADV` bed | 0.361 | 0.359 | **+0.003** | no match |
|
||
| c1 | `ADV` bed | 0.407 | 0.403 | +0.004 | no match |
|
||
| c0 | voice stream 1 / 2 / 3 | 0.42 / 0.26 / 0.27 | — | ≤ +0.006 | no match |
|
||
| c1 | voice stream 1 / 2 / 3 | 0.47 / 0.34 / 0.34 | — | ≤ +0.006 | no match |
|
||
| c0 | `BGM_103` (menu bed) | 0.271 | 0.260 | +0.011 | no match |
|
||
| c0 | `S00A` | 0.351 | 0.349 | +0.002 | no match |
|
||
|
||
**The margin is the number that matters**, not *r*. A match has a *peak*; these
|
||
have a *plateau* — best and second-best differ by 0.001–0.016 across every
|
||
pairing, which is what a statistic does when no alignment exists.
|
||
|
||
### Three controls, because a negative from an uncontrolled instrument is worthless
|
||
|
||
1. **The instrument finds matches on this data.** `bed` vs `bed` → r = 1.000,
|
||
margin **+0.115**. Voice stream 2 vs stream 3 → r = 1.000, margin **+0.300**.
|
||
2. **My reference really is the movie.** The `.ogv` transcode against the disc's
|
||
own `ADV.wmv` → r = 1.000, margin **+0.114**. So a failure to match is not my
|
||
transcode.
|
||
3. **Time drift is ruled out.** A stretched playback would break a long
|
||
correlation while still matching locally, with the best lag climbing
|
||
monotonically. Five-second windows of the capture slid over the whole bed give
|
||
best lags of **4.95, 15.30, 119.35, 50.75, 29.35, 83.95 s** — scattered across
|
||
the movie, not monotonic, and every margin ≤ 0.017.
|
||
|
||
### What I can say, and what I will not
|
||
|
||
**Said:** this capture cannot answer the channel-role question, and it is not the
|
||
intro's audio. **Not said:** what it *is*. It is 70 s of something, all six
|
||
channels carrying signal at RMS ≈ −27 dBFS, matching nothing this port exports.
|
||
Diagnosing it is the Decoder's side of the wall and I have handed it back rather
|
||
than guessing.
|
||
|
||
⚠️ **One measurement on the file that may help them.** Split as 5.1, **channels 3
|
||
and 6 are byte-identical** — same MD5, not merely the same peak and RMS to six
|
||
decimals. An exact duplicate pair inside a six-channel "surround" output is
|
||
consistent with the Decoder's own warning that the 6-channel frame is Xenia's
|
||
hardcoded `kFrameChannelsDefault`, not the guest's request. It weakens, further,
|
||
any reading of that file as evidence of a 5.1 game mix.
|
||
|
||
### The voice export stays exactly as it is
|
||
|
||
Still one stream of three, still marked 🔴 in the manifest, the console line and
|
||
`authored/audio.json`. **The capture changed nothing**, which is the correct
|
||
outcome for a measurement that failed: the question is open, and it was open
|
||
before. What would have been wrong is treating a 70 s recording as an oracle
|
||
because it was expensive to obtain.
|
||
|
||
|
||
### Resolved the same day — it was the capture path, and the duplicate pair was the thread
|
||
|
||
The Decoder found the cause and **withdrew the capture**: PulseAudio was
|
||
remapping between two mismatched channel maps, and a 6-channel remap **silently
|
||
drops and duplicates**. Its control needs no emulator and no disc — six channels,
|
||
six different tones, the same sink and the same `parec` invocation — and came
|
||
back `400 / 3200 / 200 / 800 / 800 / 200` for an input of
|
||
`400 / 800 / 200 / 1600 / 3200 / 6400`. **Two source channels were gone
|
||
entirely.** Setting the sink's `channel_map` to the guest's own returns all six.
|
||
|
||
So the negative was right and, more usefully, **the byte-identical pair I
|
||
reported was the thread that unravelled it.** That is worth recording precisely,
|
||
because it was nearly not reported at all: it began as an idle check of two
|
||
channels whose peak *and* RMS matched to six decimals, and the only reason it
|
||
became evidence is that a coincidence at six decimals is cheaper to hash than to
|
||
explain.
|
||
|
||
**Withdrawn with the file**, both the Decoder's: *"all six channels carry
|
||
signal"*, and the non-zero-surround observation offered as weak support for a 5.1
|
||
guest mix. Unaffected: the three-XMA-context concurrency result, which is read
|
||
from the emulator's own log rather than the audio path, on two independent boots.
|
||
|
||
### What the port took from it: `tools/port/check-capture`
|
||
|
||
A capture now has to pass a provenance check before anyone analyses it, and it is
|
||
one command. It splits the file, hashes every channel and fails on any duplicate
|
||
pair. Documented in `docs/port/AUDIO-VERIFICATION.md` §5.
|
||
|
||
**Run through its own controls, both directions**, because a checker nobody
|
||
controlled is the thing this whole incident is about:
|
||
|
||
* six distinct tones → **PASS**;
|
||
* the remap's own output pattern → **FAIL**, naming all four duplicate pairs;
|
||
* the corrupt game capture → **FAIL** on `ch2 == ch5`.
|
||
|
||
⚠️ **The known-bad control is the part worth reading.** All six of its channels
|
||
report a peak of **−18.063656 dB — identical to six decimals — while containing
|
||
three duplicate pairs.** A level check cannot see this failure at all. That is
|
||
why the tool hashes rather than measures, and it is why the corrupt capture's
|
||
"plausible per-channel levels" were never evidence of anything.
|
||
|
||
The tool says so itself: it is **necessary, not sufficient.** Passing means no
|
||
channel was duplicated; it says nothing about whether the right thing was
|
||
recorded. A capture should survive both that and §1's correlation against a known
|
||
source before anything is concluded from it — and the one that was analysed here
|
||
would have failed the cheap check in thirty seconds.
|
||
|
||
The corrupt file is withdrawn from the exchange (`share drop`), so the next agent
|
||
cannot pick it up and repeat the work.
|
||
|
||
## 🔴 Take 2 is clean, my instrument was not, and the negative had to be re-earned
|
||
|
||
The Decoder's second capture passes `check-capture` — I re-ran it myself rather
|
||
than cite theirs — carries a screen log, and was recorded with the sink's
|
||
`channel_map` set equal to Canary's own. It is a good file.
|
||
|
||
⚠️ **One provenance discrepancy, minor but worth stating:** the message gives
|
||
253.3 s; the file is **318.539 s**. The screen log runs to 316 s and is
|
||
consistent with the file, so this is a mis-stated number rather than a bad
|
||
capture — but a length quoted in a provenance claim should match the artefact.
|
||
|
||
### The retraction that came out of measuring it
|
||
|
||
Take 2 also showed no alignment with the bed or the voice streams. Before
|
||
reporting a second negative I asked whether the method could do the job at all,
|
||
by building a **synthetic mix** — the bed plus the three voice streams — and
|
||
hunting the bed inside it.
|
||
|
||
**It failed. r = 0.415, against the `r > 0.8` bar my earlier negatives were
|
||
judged against.**
|
||
|
||
So the instrument that produced *"the capture contains no ADV audio"* could not
|
||
have found ADV audio in a mix even when it was certainly there. That conclusion
|
||
was right — the Decoder's tone control proved take 1 corrupt independently — but
|
||
**it was right by luck, and I reported it as measurement.** The three controls I
|
||
was pleased with tested the wrong things: that the method finds a *clean* signal
|
||
in a *clean* reference, which was never the task.
|
||
|
||
### The rebuilt instrument, calibrated in both directions
|
||
|
||
Band-limit so the target dominates, then judge on **lag and margin**, not on
|
||
absolute *r* — the `r > 0.8` bar is correct clean-against-clean and meaningless
|
||
for a component in a mix.
|
||
|
||
| hunting | band | against | *r* | lag | margin |
|
||
|---|---|---|---|---|---|
|
||
| the bed | 40–180 Hz | mix containing it | 0.663 | **0.0 s** ✓ | **+0.111** |
|
||
| the bed | 40–180 Hz | voice-only mix | 0.262 | wrong ✗ | +0.005 |
|
||
| voice stream 2 | 300–3000 Hz | mix containing it | 0.810 | **0.0 s** ✓ | **+0.248** |
|
||
| voice stream 2 | 300–3000 Hz | the bed alone | 0.358 | wrong ✗ | +0.005 |
|
||
|
||
A 20–50× separation in the discriminating statistic. Documented as
|
||
`AUDIO-VERIFICATION.md` §6.
|
||
|
||
### And now the negative, supported
|
||
|
||
Every one of take 2's six channels, against both targets, sits in the
|
||
**known-absent** regime:
|
||
|
||
| | bed (40–180 Hz) | voice stream 2 (300–3000 Hz) |
|
||
|---|---|---|
|
||
| margins | +0.000 … +0.014 | +0.001 … +0.017 |
|
||
| lags | −58 … +255 s, scattered | −72 … +183 s, scattered |
|
||
|
||
**Take 2 contains neither the movie's WMA bed nor the cutscene voice**, on an
|
||
instrument that demonstrably finds both when they are present.
|
||
|
||
### What that leaves, and it is not mine to answer
|
||
|
||
Two captures, differently configured, the second provably free of the channel-map
|
||
fault, with a screen log saying the movie was on screen — and **neither carries
|
||
either audio source.** That points away from a one-off setup error. The
|
||
possibilities I can see are a capture path that still loses the guest's mix, or
|
||
the guest not emitting these sources at all during the movie, and **only one side
|
||
of that wall can tell them apart.** Handed back with the numbers.
|
||
|
||
⚠️ **If it is the second, it reaches the port directly**: the export's movie audio
|
||
comes from the `.wmv`'s WMA track, and if the game never plays that track, then
|
||
`ADV.ogv`'s audio is wrong in a way no amount of transcode fidelity would fix. I
|
||
am not asserting that — it is a question about what the game does — but it is the
|
||
reason this is worth another boot rather than being written off.
|
||
|
||
## Every music bank was summed at 1/3 when only two sub-waves are music — 3.52 dB, since P6
|
||
|
||
The Decoder's message about `BGM_102` came with declared durations from the
|
||
corrected XMA1 `PsuedoBytesPerSec`, and checking my export against them turned up
|
||
a defect of mine that had been shipping since P6.
|
||
|
||
`export_bgm` summed every sub-wave `media` returned and scaled by `1/n`. Decoded
|
||
and timed, the three banks are identical in shape:
|
||
|
||
| bank | sub-wave 0 | sub-wave 1 | sub-wave 2 |
|
||
|---|---|---|---|
|
||
| `BGM_103` | **10 300 B → 0.009 s, peak −inf** | 3 876 924 B → 87.744 s | 3 930 172 B → 87.744 s |
|
||
| `BGM_102` | **10 300 B → 0.009 s, peak −inf** | 1 151 036 B → 37.482 s | 1 269 820 B → 37.482 s |
|
||
| `BGM_001` | **10 300 B → 0.009 s, peak −inf** | 4 466 748 B → 173.809 s | 4 673 596 B → 173.809 s |
|
||
|
||
**Sub-wave 0 is digitally silent in all three**, and 10 300 B is 10 240 + a
|
||
60-byte RIFF wrapper — 10 240 B being exactly what the Decoder's disc-wide census
|
||
identifies as the bank header. So it is not a stem. Counting it in the divisor
|
||
put every real stem at 1/3 instead of 1/2: **3.52 dB of attenuation on all the
|
||
menu music this port has shipped since P6.**
|
||
|
||
Dropping it is **arithmetic, not a decoding decision** — a silent input
|
||
contributes nothing to a sum, and this is the same rule `export_voice` already
|
||
applies. Measured after the fix: `main_menu.ogg` goes **−7.69 → −4.20 dBFS**,
|
||
**+3.49 dB** against 3.52 predicted, the remainder being Vorbis.
|
||
|
||
⚠️ **This is the third instance of one defect in this pipeline** — a silent chunk
|
||
in the voice sum, a silent channel in the mono fold, and now a silent sub-wave in
|
||
the music sum. Each was invisible in every check except a level, and each time
|
||
the divisor was computed from *how many inputs there are* rather than *how many
|
||
carry signal*. That is the shape to look for, not the individual bug.
|
||
|
||
### It also closes a 🔴 that has been open since P6
|
||
|
||
`docs/port/BLOCKED.md` carried *"`media::sound_bank_riffs` returns three
|
||
sub-waves where HANDOFF Q10's census says two"* as a disagreement the port shipped
|
||
deliberately. The census was right; the third was never a stem. The export now
|
||
reports **2 sub-waves** and the warning is gone — closed by measurement on my
|
||
side, corroborating the Decoder's `c1f3608` from a different direction (decoding
|
||
it, rather than counting headers).
|
||
|
||
### The declared-rate method, cross-checked a third time — and one correction
|
||
|
||
Their declared lengths against my decodes: `BGM_103` 87.750/87.749 vs **87.744**;
|
||
`BGM_102` 37.487 vs **37.482**; `BGM_001` 173.821 vs **173.809**. Agreement to
|
||
**5–12 ms** on three banks. The method is good for lengths.
|
||
|
||
🟢 **Refutation attempt, and the conclusion survives while the reasoning does
|
||
not.** The Decoder wrote that `BGM_001` reads *"173.821 s declared against your
|
||
decoded 167.663 s — a gap of 6.158 s"*, explaining it as *"declared is the
|
||
encoded stream, decoded is where the audio stops."* **A full decode of
|
||
`BGM_001` yields 173.809 s of PCM, not 167.663 s.** The 167.663 figure is where
|
||
the music *fades out*, measured from the audio; the stream then continues, silent,
|
||
to its declared end. So declared and decoded agree to 12 ms and the trailing
|
||
silence is *inside* the decode, not the difference between two methods. The
|
||
cross-check stands — better than stated, since it is now three banks rather than
|
||
a coincidence — and the sentence explaining it should go.
|
||
|
||
## Take 2 was starved, my correlator was fine, and `check-capture` was incomplete
|
||
|
||
The Decoder diagnosed take 2: a **starved** capture. Verified here independently
|
||
rather than taken on trust — 35.6 % of frames silent on all six channels, 10 482
|
||
alternating runs, median burst 13.5 ms and gap 3.9 ms, a 17.4 ms period at 57 Hz.
|
||
Their untruncated original reads 39.3 % and 10 595 runs; the difference is
|
||
exactly the truncation, and every other number agrees.
|
||
|
||
**So my rebuilt correlator was working correctly on a file that could not carry
|
||
the signal.** ✅ And the alarming reading it produced — *"the game may not play
|
||
the `.wmv`'s WMA track, so `ADV.ogv`'s audio has been wrong since P4"* — **is not
|
||
supported by this capture, and is not refuted either.** It is withdrawn as a
|
||
concern arising from evidence, and nothing is changed on account of it in either
|
||
direction. That matters more than it looks: it was the most expensive-to-act-on
|
||
hypothesis in the port, and it came from a file that could not speak to it.
|
||
|
||
### The real deliverable: my own checker passed the starved file
|
||
|
||
`check-capture` tested only for duplicated channels, so it cleared a recording
|
||
that was 36 % holes. A provenance check that passes the artefact it was built in
|
||
response to is not a check.
|
||
|
||
It now measures starvation too. ⚠️ **Two thresholds I invented were both wrong,
|
||
and the controls caught both** — which is the part worth recording:
|
||
|
||
1. **Counting exact-zero frames.** Real audio crosses zero constantly; a clean
|
||
voice track scored 5 947 "gaps" of median 0.0 ms and was called starved. **A
|
||
gap is a run, not a sample.** Only runs ≥ 1 ms count.
|
||
2. **Gap count and median length.** A genuine music bed shows **454 gaps at a
|
||
median of 1.4 ms** — quiet 16-bit passages really are zero for milliseconds —
|
||
so neither statistic separates it from a starved file.
|
||
|
||
What separates them is the **rate**:
|
||
|
||
| | gaps/s | median gap | all-channel silence |
|
||
|---|---|---|---|
|
||
| the starved capture | **32.9** | 3.9 ms | 35.6 % |
|
||
| a real music+SFX bed | **3.3** | 1.4 ms | 1.1 % |
|
||
| a voice track, 53 % pauses | **0.03** | — | — |
|
||
|
||
Bar set at **20 gaps/s** — 1.6× below the bad case, 6× above the worst good one,
|
||
**derived from the controls rather than chosen and then justified.** Controlled
|
||
in both directions: real stereo bed PASS, six distinct tones PASS, starved
|
||
capture FAIL. It also now reports a `data` chunk declaring 0 bytes, which is what
|
||
a file copied while still being written looks like.
|
||
|
||
### 🔴 The voice channel roles are not obtainable this session
|
||
|
||
Both capture routes are closed and the Decoder has said so plainly. The monitor
|
||
sink is starved **by construction** — it advances at wall-clock rate and
|
||
substitutes silence, so every moment the emulator runs slow is a hole, and
|
||
deleting the holes warps the timebase rather than repairing it. The route that
|
||
works is an internal tap at `SDLAudioDriver::SubmitFrame`, and that needs a
|
||
Canary rebuild whose cost they have measured: the build root `build-canary`
|
||
targets does not exist in that container, the warm tree is configured against the
|
||
same missing path, so it is a full reconfigure and compile on a box with ~700 MB
|
||
free and a history of parallel builds OOM-killing the host.
|
||
|
||
**A whole session for one probe.** That is a human's call and neither agent
|
||
should start it mid-loop. Until then the port keeps authoring with the known
|
||
recorded: one stream of three, 🔴 in the manifest, the console line and
|
||
`authored/audio.json`.
|
||
|
||
## The settle run carries an unmeasured real-time factor — and the numbers it touches were already unauthored
|
||
|
||
The Decoder has withdrawn one of the two arguments propping up its settle-time
|
||
run. It had claimed the plate *pulse period* was an internal clock proving the
|
||
run was not slowed; re-examined, that estimate rests on **one interval at a
|
||
125 ms sample interval (±6.7 %)**, and re-running the trough-picking gives
|
||
**2.628 s** rather than the 2.369 quoted — an adjacent local minimum had been
|
||
counted as a separate trough. Against the corpus's 2.24 s that is **+17.3 %**. It
|
||
is too weak to show anything and **cannot resolve a real-time factor below
|
||
~7 %**.
|
||
|
||
**✅ Nothing in the port moves, and this is the second time in two iterations that
|
||
the right call was to have authored nothing.**
|
||
|
||
| number from that run | anchored by | did the port take it? |
|
||
|---|---|---|
|
||
| title → plate, 2.247 s | three prior readings (2.13 / 2.132 / 2.138) and the disc's declared 120 units | ✅ it is what the port already draws |
|
||
| menu build-in, 0.531 s | **nothing** | ❌ not authored |
|
||
| Ⓑ → title, 0.482 s | **nothing** | ❌ not authored |
|
||
| Ⓐ → menu, 3.763 s | contains a 1.53 s load stall | ❌ explicitly refused |
|
||
|
||
Checked rather than remembered: `grep` over `authored/` and `port/scripts/` finds
|
||
no `0.531` and no `0.482`. The only build-in reference in the tree is the plate
|
||
arithmetic — `t=118 → t=238`, 120 units — which is the **anchored** leg.
|
||
|
||
I declined those two because they were one-run figures the Decoder had itself
|
||
flagged, and because the port was already within ~0.1 s of both from the disc's
|
||
own keyframes. **That reasoning has now been joined by a second, independent one
|
||
I did not have at the time**: a few per cent of slowdown sits inside them
|
||
undetected. A provisional measurement adopted over a decoded number would have
|
||
imported an error nobody could see.
|
||
|
||
## `verify-dwell` — the comparison that refuted my own 🔴, made repeatable
|
||
|
||
Last iteration I measured the port's visible spans against the oracle's dwells by
|
||
hand, and it refuted a red flag I had filed myself: `rest.t` *is* the wrong settle
|
||
landmark, but *"everything the sequencer paces off it is therefore late"* was
|
||
false, and I nearly went and re-paced screens that already matched.
|
||
|
||
That check existed once, in a transcript. It is now `tools/port/verify-dwell`.
|
||
|
||
```
|
||
screen port oracle (3 cold boots) verdict
|
||
publisher wordmark 4.25 s 4.297 / 4.604 / 4.370 agrees
|
||
developer logos 3.75 s 3.508 / 3.503 / 3.366 agrees
|
||
```
|
||
|
||
⚠️ **The trap it exists to prevent is in its header, because it is the whole
|
||
point:** a port's *transition timestamps* and the oracle's *visible spans* are
|
||
not the same quantity — they differ by the exit ramp plus the black hold, about
|
||
0.6 s, which was the entire discrepancy I was about to chase. This corpus has
|
||
been bitten by the identical confusion before, on the plate delay, where the two
|
||
readings differ by 0.48 s against 6 ms.
|
||
|
||
**The bar is the oracle's own run-to-run spread**, plus one film interval. Three
|
||
cold boots of the real game differ by 0.3 s; agreeing more tightly than the
|
||
oracle agrees with itself would not mean anything, and a tighter bar would be a
|
||
number chosen to look impressive. The developer-logo span read 3.50 s on the
|
||
hand-run and 3.75 s here — one film interval apart, both inside the bar, which is
|
||
the tool reporting its own resolution honestly rather than hiding it.
|
||
|
||
**The oracle's numbers are quoted in the script as a test fixture and labelled as
|
||
the oracle's**, with the RE document they come from. Nothing in the port derives
|
||
them and nothing may.
|
||
|
||
## The `PRESS Ⓐ` plate pulses — authored per element, because the census forbids a rule
|
||
|
||
The human listed pulsation as first-class and the port drew nothing at all: the
|
||
plate's focus record `ptbtn00f` was never reached, because `press_start` has no
|
||
`buttons` and nothing is focused.
|
||
|
||
**That it loops is measured**, not assumed. The corpus timed the pulse four times
|
||
— 2.12 / 2.19 / 2.34 / 2.31 s — and you cannot measure a period unless the thing
|
||
repeats.
|
||
|
||
### The rule I was going to write, and the census that forbade it
|
||
|
||
The spinning ring is a **rule** in the renderer (`spin_period_units`) and it
|
||
earns that: 16 of 212 elements match its shape and **all 16 are focus rings**,
|
||
zero false positives. So I looked for the analogous shape for a pulse — a group
|
||
whose keyframes vary **only** in alpha, whose first and last alpha are equal, a
|
||
closed cycle. `ptbtn00f` fits it exactly: `0 → 6 → 74 → 80 → 80 → 74 → 6 → 0`.
|
||
|
||
**Censused before writing it: 82 of 212 elements match.** `ptcopyright`,
|
||
`palogo_sqex`, `ptmsg`, `ptlogo_back2`, and every `_eff` fade-in-hold-fade-out on
|
||
every screen. **A renderer rule on that shape would make the copyright notice
|
||
pulse.**
|
||
|
||
Narrowing to focus records leaves exactly **one** distinct element (plus its JP
|
||
twin). A rule justified by n = 1 is a special case wearing a rule's clothes. So
|
||
the pulse is a **lookup** in `authored/timing.json`, keyed `<screen>/<element>`,
|
||
and the census is recorded beside it so nobody widens it later.
|
||
|
||
### The period is the element's own group — and the alternative is stated
|
||
|
||
**129 units**: its last timed keyframe is t=105, and the final untimed keyframe
|
||
is reached `exit_ramp_units` (24) later. **No new constant** — 24 is the same
|
||
authored value every other element's exit already uses. That is 2.150 s at
|
||
60 units/s, or 2.295 s at the ~28.1 fps the emulator presents, against
|
||
measurements of 2.12–2.34 s. It sits inside the spread at either rate.
|
||
|
||
⚠️ **It is a choice, and the alternative cannot be ruled out**: the cycle could
|
||
restart at the group's first keyframe (t=6) rather than at 0, giving 123 units =
|
||
2.050 / 2.189 s — **also inside the measured spread**. Nothing available
|
||
separates them. t=0 is taken because it is where every other group in this port
|
||
starts, which is consistency and not evidence, and `authored/timing.json` says so.
|
||
|
||
A fifth reading is recorded and **not averaged in**: the Decoder re-picked its
|
||
troughs and got 2.628 s having previously reported 2.369 from the same run, then
|
||
withdrew the estimate as too weak to resolve better than ~7 %.
|
||
|
||
### Verified the way the ring was — bit-identity one period apart
|
||
|
||
20 authored periods is 2.15 × 20 = **43.00 s = exactly 172 film frames**, so
|
||
frames N and N+172 must be the same frame:
|
||
|
||
| | max difference |
|
||
|---|---|
|
||
| f_055 vs f_227 (43.00 s) | **0/255** |
|
||
| f_060 vs f_232 (43.00 s) | **1/255** |
|
||
| f_070 vs f_242 (43.00 s) | **1/255** |
|
||
| f_079 vs f_251 (43.00 s) | **0/255** |
|
||
| **control** — f_070 vs f_243 (43.25 s) | **58.7/255** |
|
||
|
||
The control is what makes the rest mean anything: a quarter-second off the period
|
||
differs by 58.7, on the period by 0–1. Measured on the held boot title, where the
|
||
glow-box mean swings **26.0 ↔ 37.7** — a real pulse, not a static glow.
|
||
|
||
## ✅ The oracle finally speaks: the exported voice IS the game's centre channel
|
||
|
||
The Decoder's fourth capture is the first faithful one — `--gpu=null` takes the
|
||
guest from 0.70× to 0.96× real time so Xenia stops padding, an ALSA `file` tee in
|
||
front of a paced slave removes PulseAudio's wall clock entirely. **59.7 s, 0.35 %
|
||
silence, one gap in the whole file, six distinct channel hashes.** Checked here
|
||
independently: it passes `check-capture`, and its header sizes verify exactly
|
||
(RIFF 34 369 572 / data 34 369 536 against 34 369 580 actual).
|
||
|
||
The correlator was already calibrated, so the fit was a re-run rather than a
|
||
rebuild. **Controls first, on this instrument: known-present margin +0.248,
|
||
known-absent +0.005.**
|
||
|
||
### Speech band, 300–3000 Hz
|
||
|
||
| | FL | FR | **FC** | LFE | RL | RR |
|
||
|---|---|---|---|---|---|---|
|
||
| stream 1 (leading) | +0.013 | +0.006 | +0.012 | +0.009 | +0.012 | +0.005 |
|
||
| **stream 2 — the one exported** | +0.238 | +0.171 | **+0.305** | +0.011 | +0.035 | +0.006 |
|
||
| stream 3 | +0.240 | +0.173 | **+0.307** | +0.009 | +0.054 | +0.006 |
|
||
|
||
**`r = 0.989` on FC, margin +0.305 — above the known-present control.** And the
|
||
bed, in the low band, is the mirror image: FL 0.763 / FR 0.838 / RL 0.805 / RR
|
||
0.817, all agreeing on the same lag, and **FC 0.317**.
|
||
|
||
### What is established, and what is not
|
||
|
||
✅ **The capture contains `ADV`'s audio**, at lag ≈ +6.6–6.7 s — agreed by two
|
||
independent bands and by six channels.
|
||
|
||
✅ **The dialogue is in the centre channel and the bed is in the four corners.**
|
||
FC carries the voice and not the bed; FL/FR/RL/RR carry the bed and not the
|
||
voice. That is a textbook film mix, and it is measured rather than inferred from
|
||
a header — which matters, because the header says `ChannelMask = 0x0002` on all
|
||
three streams and would never have told us.
|
||
|
||
✅ **The port's exported voice file is the material the game plays in FC.** The
|
||
`loudest` choice in `authored/audio.json` — which I recorded as an unjustified
|
||
choice — selects the dialogue. A mono voice file played into the mix is the
|
||
right approximation of a centre channel.
|
||
|
||
⚠️ **Streams 2 and 3 are indistinguishable to this instrument**, and that is
|
||
expected: I measured months-of-iterations ago that stream 3 is 0.60 × stream 2
|
||
with the residual 26.8 dB down. They are the same take at two levels, so they
|
||
correlate identically with everything. **This does not say `loudest` picked the
|
||
*right* one of the two — only that whichever it picked is the dialogue.**
|
||
|
||
⚠️ **Stream 1 is not detectable in this window**, margins +0.005…+0.013. Consistent
|
||
with it being the tail of stream 2 (measured earlier at r = 0.998) and with a
|
||
59.7 s window that starts before the tail.
|
||
|
||
🔴 **The `1 of 3 streams` warning stays.** Nothing here explains what the other
|
||
two contribute to the game's output, and the export still ships one. What changed
|
||
is its character: it is no longer *"one of three, contents unknown"* but *"the
|
||
centre-channel dialogue, plus two streams whose relationship to it is measured
|
||
and whose role is not."*
|
||
|
||
⚠️ **And the reach: 59.7 s of a 137 s movie**, one run, at 0.96× real time. The
|
||
`--gpu=null` route costs video, so this capture has no screen provenance — its
|
||
provenance is the XMA probe showing `ADV`'s three streams decoding during the
|
||
run, which for an audio question evidences the thing recorded rather than what
|
||
was on screen.
|
||
|
||
## The stripping control passes — `S00A` is obtainable, and the gate is cleared
|
||
|
||
The Decoder made this the gate on `S00A`, and it is the right call: `ADV` plays
|
||
itself on boot so it can be captured with `--gpu=null` at 0.96× real time, but
|
||
`S00A` starts ~4.5 s after Ⓐ on a save slot, which needs a **driven** run, which
|
||
needs screens, which rules out `--gpu=null`. So `S00A` is necessarily the 0.70×
|
||
rendered route with ~10 % additive padding — and is only worth a boot if
|
||
stripping that padding is exact.
|
||
|
||
**It is.** A real music+SFX bed (137.37 s, carrying 454 genuine zero runs of its
|
||
own) had 1 149 holes inserted at 8.37/s to +9.9 % length, matching the observed
|
||
ALSA profile, then was stripped and correlated in the low band:
|
||
|
||
| | *r* | lag | margin |
|
||
|---|---|---|---|
|
||
| original vs itself — **ceiling** | 1.000 | 0.0 s | +0.141 |
|
||
| **padded** vs original — what padding costs | **0.436** | −12.2 s | **+0.006** |
|
||
| **stripped** vs original — recovered | **1.000** | **0.0 s** | **+0.142** |
|
||
| stripped vs original-also-stripped | 1.000 | 0.0 s | +0.143 |
|
||
|
||
**Two things worth reading off that table.**
|
||
|
||
First, **padding at that profile destroys correlation completely** — r 0.436,
|
||
margin +0.006, which is the known-absent regime. That independently confirms, on
|
||
a file whose contents I control, that the earlier captures were unusable for the
|
||
reason claimed rather than for some other reason.
|
||
|
||
Second, **recovery does not require stripping both sides.** The stripped capture
|
||
matches the *unstripped* source at the ceiling. That matters operationally: the
|
||
port's reference assets never need touching.
|
||
|
||
⚠️ **What the control does not license.** Stripping removes genuine silence too
|
||
and cannot tell the two apart. On this material the genuine runs total 0.71 s in
|
||
137 s and cost nothing measurable; on material that is mostly silence they would.
|
||
And the whole thing rests on the **substituted-versus-additive** distinction — it
|
||
is valid for Xenia's ALSA padding, which inserts, and it is vandalism on a
|
||
PulseAudio monitor capture, which substitutes. `tools/port/strip-padding` says so
|
||
in its header before it says anything else, because running it on the wrong
|
||
artefact would look like it worked.
|
||
|
||
Its output is **byte-identical** to the control's own stripping, so the tool and
|
||
the experiment are the same operation rather than two implementations that agree.
|
||
|
||
## The correctness harness the docs promised for eight milestones did not exist
|
||
|
||
`tools/port/verify-screen`, line 20, since P1: *"Use `tools/verify-capture` for
|
||
the correctness question."* **There was no such file.** The port has had a harness
|
||
comparing itself to `sylpheed-cli` — two renderers sharing its assumptions — and
|
||
none comparing it to the game, while its own documentation said otherwise.
|
||
|
||
`docs/re/captures/ORACLE-CAPTURES.md` is blunt about why that matters: two
|
||
renderers agreeing proves nothing, and this corpus has been bitten three times —
|
||
the dropped `pteff05` background, the scale-0 rect, `rest()` — each invisible to a
|
||
render-vs-render diff and obvious against a capture.
|
||
|
||
`tools/port/verify-capture` now exists. **Five screens, against framebuffer
|
||
captures of the real game:**
|
||
|
||
| screen | RMSE | differing | note |
|
||
|---|---|---|---|
|
||
| `main_menu` | 14.79 | **0.25 %** | focus state may differ |
|
||
| `extras` | 15.29 | 0.46 % | focus state may differ |
|
||
| `title` | 21.07 | 1.82 % | `ptloop` sweeps never stop |
|
||
| `publisher_logo` | 10.77 | 1.00 % | |
|
||
| `developer_logos` | 9.37 | 0.39 % | |
|
||
|
||
**No screen shows a large connected blob** — the shape a missing or misplaced
|
||
element makes, and the shape all three historical failures made. The differences
|
||
are scattered, and the two largest have stated causes.
|
||
|
||
### 74 % of `main_menu`'s difference is the oracle's own focus signature
|
||
|
||
The corpus ships `live-main-menu.png` and `live-main-menu-options-focused.png` —
|
||
the same screen with a different button lit. Their difference *is* what focus
|
||
changes, measured by the oracle against itself. Of the port's 2 159 differing
|
||
pixels, **1 599 — 74.1 % — fall inside that signature.** So the bulk of the
|
||
disagreement is a state mismatch (the port focuses `NEW GAME`, authored, because
|
||
HANDOFF Q5 measured initial focus as unstable), not a rendering defect.
|
||
|
||
## Refutation attempt — the tone curve survives in its stated reach and not past it
|
||
|
||
`ui-render-tone-curve.md` models the relationship as
|
||
`capture = 255·(render/255)^γ`, γ ≈ 1.34–1.49, **measured on dark flat patches
|
||
(render ~0–60), with "nothing constrains midtones or highlights"** written into
|
||
its own reach.
|
||
|
||
**I tried to fit that γ and got contradictory answers three times, and the
|
||
contradictions were mine.** Binning every structurally matched pixel of
|
||
`main_menu` by render level gives the relationship directly:
|
||
|
||
| render | capture | implied γ | pixels |
|
||
|---|---|---|---|
|
||
| 8 | 4.04 | 1.20 | 183 026 |
|
||
| 16 | 7.89 | **1.26** | 227 630 |
|
||
| 24 | 15.57 | 1.18 | 100 945 |
|
||
| 32 | 26.15 | 1.10 | 87 474 |
|
||
| 40 | 38.07 | 1.03 | 86 094 |
|
||
| 48 | 53.96 | **0.93** | 85 255 |
|
||
| 64 | 78.52 | 0.85 | 6 509 |
|
||
| 96 | 130.44 | **0.69** | 1 682 |
|
||
|
||
✅ **The claim survives where it was measured.** In the darks the capture really
|
||
is darker than the render and γ > 1.
|
||
|
||
🔴 **It is not a single power law.** The implied exponent falls monotonically and
|
||
**crosses 1.0 near render ≈ 44** — above that the capture is *brighter*. One
|
||
exponent cannot express a curve that crosses unity, which is precisely why my
|
||
whole-frame fits kept returning γ = 1.00: the darks want more than 1 and the
|
||
midtones want less, and they cancel.
|
||
|
||
**So the corpus's stated reach was not a hedge, it was the finding.** ⚠️ And the
|
||
exponent in the darks measures **1.18–1.26 here against the page's 1.49 for this
|
||
screen** — a disagreement I am recording rather than resolving, since they fit
|
||
selected flat patches and I binned every matched pixel.
|
||
|
||
### The tool reports the curve, not a best exponent
|
||
|
||
Two earlier versions of `verify-capture` reported a best-fit γ and were wrong
|
||
both times — once by fitting across a 74 % structural mismatch, once by
|
||
extrapolating past a reach the measurement's own authors had written down.
|
||
**Extrapolating a measurement past its stated reach is how this tool got it wrong
|
||
twice**, and the answer was not a better fit but a different instrument: a table
|
||
somebody can argue with.
|
||
|
||
## Identifying the capture's focused button — and my harness was posing the port wrong
|
||
|
||
The Decoder attached an honest caveat to its reproduction of the tone-curve
|
||
refutation: its bins included the focus-state mismatch, so it was *"not a clean
|
||
second opinion"*. That is removable, and removing it found a defect of mine.
|
||
|
||
### The method, with a known-answer control
|
||
|
||
`--menu=main_menu --script=down,down,down,down` walks focus through all five
|
||
buttons and shoots each. Compare every one against a capture; the minimum
|
||
identifies the focused button. **The control is the capture whose answer is in
|
||
its own filename:**
|
||
|
||
| render focus | vs `live-main-menu-options-focused` | vs `live-main-menu` |
|
||
|---|---|---|
|
||
| `ptbtn01` NEW GAME | 6 351 | **531** |
|
||
| `ptbtn02` LOAD GAME | 7 087 | 7 094 |
|
||
| `ptbtn03` TUTORIAL | 6 230 | 6 237 |
|
||
| `ptbtn04` OPTIONS | **1 292** | 6 364 |
|
||
| `ptbtn05` EXTRAS | 6 073 | 6 080 |
|
||
|
||
✅ **The control picks `OPTIONS`, by 4.7×** — the answer the filename gives.
|
||
✅ **So the test is trustworthy, and `live-main-menu.png` has NEW GAME focused, by
|
||
11.5×.**
|
||
|
||
**Which is what the port already focuses.** `authored/flow.json`'s
|
||
`initial_focus: ptbtn01` was chosen because HANDOFF Q5 measured focus as
|
||
*unstable* across boots — four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME
|
||
— and it is one of the two observed states. It is now also the state of the
|
||
committed capture. ⚠️ That is corroboration, **not** a decode: Q5's instability
|
||
stands, and this identifies one frame rather than a rule.
|
||
|
||
### The defect: `verify-capture` rendered menus with no focus at all
|
||
|
||
`--screen=` draws no focus record, so the harness had been comparing `main_menu`
|
||
to the oracle **in a state the oracle was never in**. Rendered properly, with
|
||
`--menu=`:
|
||
|
||
| | before | after |
|
||
|---|---|---|
|
||
| `main_menu` | 0.25 % differing, RMSE 14.79 | **0.06 %**, RMSE 13.21 |
|
||
| `extras` | 0.46 %, RMSE 15.29 | **0.20 %**, RMSE 13.38 |
|
||
|
||
A 4× improvement on `main_menu` that was **entirely my harness posing the port
|
||
wrong**, not the port drawing wrong. Worth stating plainly: the first run of a
|
||
new correctness harness reported a discrepancy, and three quarters of it was the
|
||
harness.
|
||
|
||
### And the Decoder's caveat resolves to nothing — measured, not assumed
|
||
|
||
Re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 /
|
||
1.18 / 1.10 / 1.03 / 0.94 at render 8…48 — **unchanged** from the mismatched run.
|
||
So the focus-state contamination it flagged really did not move the trend. Its
|
||
reproduction stands as a second opinion after all, and that is now a measurement
|
||
rather than a hope.
|
||
|
||
## `tools/port/which-focus` — the Decoder asked for a detector, and it carries its own control
|
||
|
||
`S00A` is blocked on knowing which button a screenshot has focused.
|
||
`newgame_path.sh` assumed NEW GAME is focused at boot, drove on that assumption,
|
||
and landed in a **tutorial mission** — because HANDOFF Q5 measured focus as
|
||
*unstable across boots*. And counting presses cannot substitute: ⬆ from the first
|
||
item wraps to the last, so no fixed number of presses lands on a known item from
|
||
an unknown start.
|
||
|
||
The Decoder's own attempt — a per-row brightness statistic — **failed the
|
||
control**, picking NEW GAME on the capture whose filename says OPTIONS. The
|
||
render-difference method passes it, so it is now a script that agent can run.
|
||
|
||
### It runs the control on every invocation, not once when it was written
|
||
|
||
```
|
||
control -- live-main-menu-options-focused.png (answer is in the filename):
|
||
OPTIONS 1285 <- picked
|
||
EXTRAS 6073
|
||
...
|
||
-> OPTIONS, margin 4.7x CONTROL PASSED
|
||
```
|
||
|
||
If that fails, the tool **refuses to report a result at all**. A control that
|
||
does not execute is not a control, and this one cannot be skipped.
|
||
|
||
### Three checks, and one of them independently reproduces a corpus measurement
|
||
|
||
| input | verdict | margin |
|
||
|---|---|---|
|
||
| `live-main-menu-options-focused` — **known answer** | OPTIONS | 4.7× |
|
||
| `live-main-menu` — the question | **NEW GAME** | 11.4× |
|
||
| `live-extras` — **known from the corpus** | MISSION SELECT | 4.2× |
|
||
| `live-title-press-a` — **no menu at all** | *refuses* | 1.0× |
|
||
|
||
The `extras` row is a second known answer I did not plant: `authored/flow.json`
|
||
already records *"MEASURED: EXTRAS opens focused on MISSION SELECT
|
||
(live-extras.png)"*, and the tool reaches it independently.
|
||
|
||
The title row is the negative control. A frame with no menu in it gives a margin
|
||
of 1.0× and the tool says *"this frame does not decide it. Do not act on this."*
|
||
|
||
⚠️ **And that refusal now carries a non-zero exit code.** The first version
|
||
printed the warning and exited 0 — so a caller scripting it, which is the entire
|
||
point, would have read a refusal as an answer. That is the same defect as a
|
||
checker claiming a check it skipped, and it is the fifth instance of that shape
|
||
between the two of us this session.
|
||
|
||
**What it is not:** it identifies focus in *one frame*. It says nothing about
|
||
what *selects* focus; Q5's instability stands.
|
||
|
||
## The title's 1.82 % — three of my own explanations refuted, and the format has no blend mode
|
||
|
||
`title` is the port's largest disagreement with the oracle, and last iteration I
|
||
attributed it to the moving `ptloop` sweeps *without checking*. That attribution
|
||
is wrong, and so were the two hypotheses I formed after it.
|
||
|
||
**❌ Not the sweeps.** `ptloop01`/`ptloop02` are **399×180 at (441, 270)** — small
|
||
and central — and their exported keyframes hold `pos`, `scale` and `rotation`
|
||
constant, varying only alpha. The difference peaks at **x ≈ 1088**, nowhere near
|
||
them.
|
||
|
||
**❌ Not an over-held element.** `--screen` holds every element at its own
|
||
`rest.t`, so I added `--no-hold` to render the other answer. Playing the title's
|
||
groups past their rest **fades the whole screen to black by t = 5.2 s** —
|
||
30.97 % differing against 1.82 % held. Holding at rest is right.
|
||
|
||
**❌ Not a timing offset.** Sweeping the build-in: 24.05 % at t=1.6, falling
|
||
monotonically to **1.68 % at t=4.18** and 1.82 % settled. The capture is at the
|
||
settled end; there is no earlier moment that fits better than marginally.
|
||
|
||
### What it actually looks like: a horizontal redistribution
|
||
|
||
Signed difference (port − capture), by cell:
|
||
|
||
| | x=0 | x=320 | x=640 | x=960 |
|
||
|---|---|---|---|---|
|
||
| y=0 | +1.1 | **−13.1** | −6.6 | **+16.0** |
|
||
| y=169 | +2.1 | **−8.3** | +4.2 | **+9.9** |
|
||
| y=338 | +5.8 | −4.5 | −0.0 | +3.2 |
|
||
| y=507 | +3.9 | +3.3 | +2.0 | +2.3 |
|
||
|
||
**The port is darker centre-left and brighter right, and it nearly cancels** —
|
||
whole-frame means 63.8 against 62.5. That is not a level error and not a tone
|
||
ramp; it is brightness in the wrong *place*. And it falls in exactly the rows
|
||
spanned by the two wide elements `ptlogo_back2` (1118×262 at 71,126) and
|
||
`ptlogo_back2eff` (1133×280 at 64,117), with the column profile falling off past
|
||
x≈1152 against their right edges at 1189 and 1197.
|
||
|
||
### 🔴 The export carries no blend mode, so the port cannot draw one
|
||
|
||
`ptlogo_back2eff`'s exported keys are `declared, id, index, keyframes, kind_raw,
|
||
layer, layer_source, pivot, rest, role, sprite`. **There is no blend field**, in
|
||
this element or in `FORMAT.md` at all. The port composites everything with normal
|
||
alpha.
|
||
|
||
If the game draws `_eff` layers **additively**, a wide gradient sprite would
|
||
produce precisely this signature — surplus where the sprite is bright, deficit
|
||
where the underlying art is brighter than the sum — and **nothing in the export
|
||
would reveal it.** That is a decoding question, not a port one, and it is asked
|
||
rather than assumed. ⚠️ It is a hypothesis I have not tested; I am recording it
|
||
because the three I could test are dead.
|
||
|
||
### A separate `rest.t` casualty, recorded and not acted on
|
||
|
||
`pteff02` is a full-frame primitive whose group runs `0x40000000` (25 % black) at
|
||
t=46 → `0xd4000000` at 76 → `0xcc000000` at 118 → **`0x00000000` at 236**. Its
|
||
`rest.t` is **46**, so the port holds a **25 % black veil the screen's own
|
||
timeline removes**. This is the third instance of `rest.t` naming a hold that is
|
||
not the settled state — after the loading screen's opaque quad and `ptlogo1`'s
|
||
creep. ⚠️ **It does not explain the residual** — removing a darkening veil would
|
||
make the port brighter still, and it is already brighter where it disagrees — so
|
||
it is recorded rather than fixed.
|
||
|
||
### And a new diagnostic
|
||
|
||
`--no-hold` plays a screen past its rest instead of clamping each element at
|
||
`rest.t`. Added because the question *"is the held pose what the idle game
|
||
shows"* could not be asked otherwise. ⚠️ Its first version set the flag thirty
|
||
lines before `view` exists and silently rendered nothing — caught because the
|
||
comparison loop found no files, not because anything reported an error.
|
||
|
||
## 🔴 The exporter dropped nested `.rat` leaf geometry on 45 elements — and it is the title's 1.82 %
|
||
|
||
The Decoder overturned one of my three eliminations, and it was the one I was
|
||
most confident about. I ruled out the `ptloop` sweeps because *"399×180 at
|
||
(441, 270), keyframes hold position constant"* — **that is the parent's record.
|
||
The geometry is in the leaf, and the exporter never opened it.**
|
||
|
||
| | parent (what the export shipped) | **leaf `ptloopNN.rat`** |
|
||
|---|---|---|
|
||
| `ptloop01` | scale (100, 100), rot 0, pos (441, 270) fixed | **scale (100, 600), rot +30°**, x sweeping **−639 → −39 → 1521** |
|
||
| `ptloop02` | scale (100, 100), rot 0, pos (441, 270) fixed | **scale (100, 800), rot −45°**, x sweeping **1721 → 1111 → −839** |
|
||
|
||
Two ~1080 and ~1440 px quads leaning opposite ways and sweeping across the
|
||
frame, against two 400 px sprites drawn upright and static in the middle. **That
|
||
is exactly the signature I measured** — darker centre-left, brighter right,
|
||
nearly cancelling — and the Decoder's GPU capture puts their centres at x ≈ 467
|
||
and 992, which are the two cells where my signed difference peaked.
|
||
|
||
`ui_layout`'s own doc comment had said so: *"the rotated quads come from its two
|
||
**nested** `.rat` leaf records, which the census never opened."* **Neither did
|
||
this exporter.** It opened a leaf in exactly one place — `highlight_name`, for
|
||
focus records — and nowhere else.
|
||
|
||
### It is not two elements. It is 45
|
||
|
||
| screen | elements with a dropped leaf |
|
||
|---|---|
|
||
| `main_menu`, `extras`, `press_start` (+ JP twins) | every button — `ptbtn01.rat` … `ptbtn13.rat`, `ptbtn00.rat` |
|
||
| `title`, `extras` | `ptloop01.rat`, `ptloop02.rat` |
|
||
| `build_00/01/12/15` | `pgloading_loop1/3/4/5.rat` |
|
||
| `title_jp` | `ptlogo_eff2.rat` — **two** elements |
|
||
|
||
⚠️ The buttons are the benign case and `screen.rs` already knew it: *"a BASE
|
||
record's leaf duplicates the parent's placement and the two can differ by a unit
|
||
(`ptbtn04`: parent y=401, leaf y=402). There the parent wins."* The `ptloop` case
|
||
is the opposite — the parent carries **no geometry at all** and the leaf carries
|
||
all of it. 🟡 And `title_jp`'s `ptlogo_eff2` is the element `DECISIONS` has
|
||
recorded since P1 as the single largest render disagreement in the export, *"the
|
||
one drawn element at a scale that is not a whole multiple of 100 %"*. It has a
|
||
two-element leaf. That is a lead, not a conclusion.
|
||
|
||
### Emitted, deliberately not drawn
|
||
|
||
`Element::leaf` now ships the decoded leaf, and one `read_leaf` closure serves
|
||
both it and the focus path — a second copy is how the case would go missing
|
||
again.
|
||
|
||
🔴 **`ScreenView` ignores it**, and that is the honest state. Parent and leaf each
|
||
carry their own alpha ramp on a **different span** — parent `0 → 255` over
|
||
t=70…238, leaf `255 → 0x80 → 255` over t=150…600 — so **how the two compose is a
|
||
decoding question**, and drawing the leaf on a guess would replace a visible
|
||
1.82 % gap with an invisible wrong one. `verify-screen` confirms nothing moved:
|
||
`title` still max 6 / over3 790, `main_menu` max 4, `title_jp` max 155.
|
||
|
||
✅ **Additive blending is refuted** — the Decoder tested `T8aD +0x04` bit `0x02`
|
||
as an additive selector and *"every measure worsens"*. My blend-mode hypothesis
|
||
from last iteration is dead, and the export carries no blend field because **none
|
||
has been found**: the per-draw capture records primitive type, index count,
|
||
shader hashes, texture bindings and vertex attribute 0, and **no
|
||
`RB_BLENDCONTROL`**.
|
||
|
||
🔵 **And this makes the port's biggest oracle gap the same item as the rotation
|
||
question already standing with the human.** `sylpheed-cli screen render`
|
||
deliberately does not rotate, which is why *both* renderers show it — the
|
||
Decoder measures its own residual as tiles running −38.6 then +33.8 and
|
||
cancelling, the same shape as mine. It is a **shared decode gap, not a defect in
|
||
my compositor**, and MISSION's *"Needs a human decision — rotation"* now has a
|
||
number attached: **1.82 % of the title's pixels, in a signature that can be
|
||
recognised.**
|
||
|
||
## The leaf composition is decoded and implemented — and it does **not** close the 1.82 %
|
||
|
||
The Decoder decoded the rule I refused to guess: **draw the leaf on its own
|
||
timeline; do not multiply the parent's alpha in.** Multiplying is *refuted*, not
|
||
merely unsupported — at the fitted time the parent has expired (its group returns
|
||
to 0 at t=250 and holds), so `leaf × parent / 255` predicts zero for both quads
|
||
and the sweeps would be invisible. They are drawn.
|
||
|
||
The fit is worth repeating because of its shape: the game's own composed alpha is
|
||
observable in the per-draw vertex colours (`C3FFFFFF`/`B6FFFFFF` = **195** and
|
||
**182**), fitting *only those two numbers* against the two leaf ramps gives one
|
||
consistent time **t = 355**, and the same t then **predicts** quad centres at 981
|
||
and 478 against **992.0** and **467.2** measured. No x entered the fit.
|
||
|
||
Implemented: `_draw_leaf` runs the leaf unclamped — like the spinning ring, and
|
||
for the same reason. Held at its own `rest.t` the leaf sits at **x = 1521**,
|
||
entirely off the right edge, so `holding` would delete the sweeps rather than
|
||
settle them.
|
||
|
||
### 🔴 And it changes nothing measurable. The title is still 1.82 %
|
||
|
||
| t | units | differing |
|
||
|---|---|---|
|
||
| 4.35 s | 261 | 1.82 % |
|
||
| **5.917 s** | **355** | **1.81 %** |
|
||
| 7.00 s | 420 | 1.79 % |
|
||
|
||
**At t=355 my interpolation puts the leaf's top-left at x ≈ −324 — off-screen
|
||
left — where the Decoder's model puts the quad's centre at 981.** Those cannot
|
||
both be right, and the gap is not something to tune away: it is a disagreement
|
||
about how the leaf's keyframes become a placed quad, most likely in the pivot and
|
||
the rotation about it. **Handed back with both numbers.**
|
||
|
||
So the honest state: the exporter no longer drops the data, the composition rule
|
||
is implemented as decoded, and **the port's largest oracle gap is exactly where
|
||
it was.** Fixing the export was necessary and was not sufficient.
|
||
|
||
### Two flagged elements deliberately not drawn
|
||
|
||
`authored/rendering.json` narrows drawing to `ptloop01`/`ptloop02` — what the
|
||
decode covers — while the exporter keeps flagging all 15.
|
||
|
||
* **`title_jp/ptlogo_eff2`** — parent 125 % at (412,96), leaf 100 % at (402,66).
|
||
The same shape, and the element `DECISIONS` has recorded since P1 as the
|
||
largest render disagreement in the export. The Decoder said plainly *"I have
|
||
not tested it."* Drawing it would extend a decode past the case it was fitted
|
||
on — the failure this port has made four times this session.
|
||
* **`pgloading_loop5`** — leaf scale **(0, 0)**. A zero scale is one of the three
|
||
historical failures this corpus names, so it is exactly where a guess costs
|
||
most.
|
||
|
||
⚠️ **And neither can be adjudicated here.** `title_jp` has no oracle capture, and
|
||
`verify-screen` compares against `sylpheed-cli`, **which draws no leaves at all**
|
||
— so *any* leaf drawing increases that divergence whether it is right or wrong.
|
||
Its `max` went 155 → 232 when they were drawn, and **that number is not evidence
|
||
in either direction.** Recognising it as uninformative is the reason it did not
|
||
become a justification.
|
||
|
||
## The −324 was the old keyframe association, and the corrected one is available **today**
|
||
|
||
The Decoder found it and it is not geometry: my leaf path pairs each pose with
|
||
the *next* pose's time. Fed my pairing into its own interpolation, it reproduces
|
||
**−324 to the digit**. Corrected, t=355 gives top-left **781** and centre
|
||
**980.5** against **992.0** measured — so there was never a 1 300 px pivot
|
||
mystery to look for, and I am glad I said the two numbers instead of hunting one.
|
||
|
||
### 🔴 Correcting BLOCKED: this was never gated on a branch landing
|
||
|
||
`docs/port/BLOCKED.md` has carried the keyframe-record-layout row as *"NOT taken,
|
||
and it cannot be taken yet … `sylpheed-formats` is a path dependency, so this
|
||
reaches the port only when that branch lands on `main`."*
|
||
|
||
**That is wrong.** The corrected association is in this checkout, behind an
|
||
environment variable:
|
||
|
||
```rust
|
||
// Set `SYLPHEED_KF_TIME_SHIFT=1` to read `W[k-1]` as block `k`'s time
|
||
// ("the word is the time the NEXT pose is reached") instead of `W[k]`.
|
||
```
|
||
|
||
It has been switchable the whole time. **I read that file twice this session —
|
||
once for `rotation_deg`, once for the leaf note — and did not notice the switch.**
|
||
|
||
### The experiment, run: mixed, and not decisive for the reason that matters
|
||
|
||
Re-exported with `SYLPHEED_KF_TIME_SHIFT=1` and asked the oracle:
|
||
|
||
| screen | default | shifted |
|
||
|---|---|---|
|
||
| `main_menu` | 13.21 / 0.06 % | 13.81 / **0.10 %** — worse |
|
||
| `extras` | 13.38 / 0.20 % | 13.95 / **0.24 %** — worse |
|
||
| `title` | 21.07 / 1.82 % | **20.41** / 1.86 % — RMSE better, area worse |
|
||
| `publisher_logo` | 10.77 / 1.00 % | **9.05 / 0.75 %** — better |
|
||
| `developer_logos` | 9.37 / 0.39 % | **8.86 / 0.33 %** — better |
|
||
|
||
⚠️ **And it does not adjudicate the association, because the port's renderer is
|
||
built for the other one.** Under the shift the **untimed keyframe moves from last
|
||
to first** — the leaf reads `t=None` at x=−639, then t=150, t=540 — while
|
||
`pose_at` is written around *"the final keyframe carries no `t`, so it is given a
|
||
synthetic time `exit_ramp_units` after the last timed frame."* So this measures a
|
||
**renderer/association mismatch**, not the association. BLOCKED said as much:
|
||
the change touches `pose_at`, `settle_units`, `spin_period_units`,
|
||
`exit_ramp_units` and the plate.
|
||
|
||
**Export reverted to the default.** Adopting the shift is a real piece of work —
|
||
re-deriving the exit ramp, the settle, the spin period and the plate against a
|
||
layout where the untimed frame is the *first* — and doing it hastily at the end of
|
||
an iteration is how a 1.8 % gap becomes five wrong ones.
|
||
|
||
### ⚠️ The methodological point, which is the Decoder's and is the best thing here
|
||
|
||
Its rule matched because **alpha at t=355 sits inside a long segment where a
|
||
one-keyframe shift barely moves it, while x sweeps 1 560 px over the same span.**
|
||
It confirmed on the insensitive quantity; I was wrong on the sensitive one; and
|
||
neither of us saw it until the two were compared.
|
||
|
||
> **Check a new interpretation against the fastest-moving field you have, not the
|
||
> one that happens to agree.**
|
||
|
||
That is a different failure from the ones this session has collected — not an
|
||
uncontrolled instrument, but a control chosen where it could not fail.
|
||
|
||
❔ A residual **11.5 px** (980.5 against 992.0) is left over and is **not** to be
|
||
fitted. A rotation about a declared pivot rather than the centre would displace by
|
||
roughly that; if it still matters once the association is adopted, it gets
|
||
measured rather than derived.
|
||
|
||
## Re-running the P5/P6 gate after eight iterations of changes
|
||
|
||
The leaf path, the plate pulse, the BGM level, the voice export, the focus fix
|
||
and a new diagnostic flag have all landed since the gate was last actually run.
|
||
The mission's test is *"a human presses a d-pad and Ⓐ and moves through those
|
||
screens"*, and I had been verifying pieces of that against captures without once
|
||
re-running the whole walk.
|
||
|
||
**It works.** Nine steps, unattended:
|
||
|
||
| step | | |
|
||
|---|---|---|
|
||
| ⬇ ×4 | `ptbtn01` → `ptbtn05` | focus moves, every frame drawn |
|
||
| Ⓐ | `EXTRAS` → screen `extras`, focus `ptbtn11` | |
|
||
| Ⓑ | back to `main_menu`, **focus restored to `ptbtn05`** | HANDOFF Q5's rule, live |
|
||
| ⬆, Ⓐ | `ptbtn04` `OPTIONS`, destination outside this archive | prints the gap rather than pretending |
|
||
|
||
Filmstrip shared as `1788027380-788b1faafc3e`. Every shot is non-blank (frame
|
||
means 31.5–33.1).
|
||
|
||
### The sound is verified by a null control, not by a detector
|
||
|
||
I first tried an onset detector: count sharp level rises in the recording. It
|
||
found **one** onset in the walk — and **the same one** in the music bed alone,
|
||
which contains no cues. The 50 ms envelope cannot see a short cue over the bed.
|
||
⚠️ **The right response to a detector that cannot separate its control from its
|
||
subject is to stop using it**, not to widen the window until the numbers look
|
||
better. That is the shape this session has hit five times.
|
||
|
||
So the same technique that settled the voice: shadow the three cues with silence
|
||
through `data/mods/`, run the identical walk, and compare.
|
||
|
||
| | peak | RMS |
|
||
|---|---|---|
|
||
| walk, cues playing | **+0.0003 dBFS** | −18.36 |
|
||
| walk, cues silenced | **−4.74 dBFS** | −20.78 |
|
||
|
||
**Silencing three sound effects costs 4.74 dB of peak and 2.43 dB of RMS**, and
|
||
the walk's peak is *set by* a cue rather than by the music. The mod log confirms
|
||
exactly three files shadowed. No detector, no threshold, and the control is the
|
||
same run with one input changed.
|
||
|
||
⚠️ Recorded under the Dummy driver, as everything audio here is. It shows the
|
||
cues reach the Master bus at the right moments; it does not show they are the
|
||
cues the game plays — that is HANDOFF Q8, and `authored/audio.json` still carries
|
||
the offsets as measured rather than decoded.
|
||
|
||
## Pinned `formats-pin-2026-08-29c` — and the knob I tested last iteration was retired
|
||
|
||
🔴 **I tested the wrong switch.** `SYLPHEED_KF_TIME_SHIFT` is a **superseded
|
||
partial fix**: it got the association right but **left pose 0 untimed**, which is
|
||
exactly why the untimed keyframe appeared to "move from last to first". It does
|
||
not exist in the current parser. The real correction is the **default** in the
|
||
tagged crate, with the old reading behind `SYLPHEED_KF_TIME_LEGACY=1` — the
|
||
opposite polarity from what I had.
|
||
|
||
So last iteration's five rows measured a renderer/association mismatch **against
|
||
a knob nobody should use**. I suspected they were not decisive; I did not suspect
|
||
the knob itself was retired.
|
||
|
||
### The consequence is smaller than I budgeted for: there is no untimed keyframe
|
||
|
||
A placement group is an 8-byte header then `frames` × `{u32 time; 36-byte pose}`,
|
||
so pose 0's time is the group's lead-in word and **every pose is timed, including
|
||
the last.** Measured on the re-export: **866 keyframes, 0 untimed.**
|
||
|
||
`pose_at`'s premise — *"the final keyframe carries no `t`, so it is given a
|
||
synthetic time `exit_ramp_units` after the last timed frame"* — does not invert,
|
||
it **disappears**. The branch is now dead code rather than wrong code, which is
|
||
why nothing needed re-deriving to adopt this.
|
||
|
||
And the leaf reads as the Decoder's table says: t=0 x=−639, t=150 x=−39, t=540
|
||
x=1521. At t=355 that interpolates to **x = 781** — the top-left it predicted, and
|
||
the 1 300 px discrepancy is gone.
|
||
|
||
### Pinned by tag, which is what MISSION §2 is for
|
||
|
||
> *"The RE agent tags when it lands something you need and tells you over the
|
||
> message channel — that is how you stay current without floating."*
|
||
|
||
That is precisely what happened, so `crates/sylpheed-export/Cargo.toml` now pins
|
||
`formats-pin-2026-08-29c` by tag. ⚠️ **`BLOCKED.md` was wrong in both
|
||
directions** — it said the change "cannot be taken yet" *and* that it arrives
|
||
only when the branch lands on `main`. It arrives when the tag is pinned.
|
||
|
||
🔴 **The cost, stated rather than discovered later:** `sylpheed-cli` builds from
|
||
the **workspace** crate, so until this reaches `main` the exporter and the
|
||
reference renderer read **different decoders**, and `verify-screen` is comparing
|
||
two eras rather than detecting drift. `verify-capture` is unaffected — it
|
||
compares the port against oracle **captures** and never touches the CLI — and it
|
||
is the check that matters. Revert to the path dependency the day the tag is an
|
||
ancestor of `main`.
|
||
|
||
### What the oracle says
|
||
|
||
| screen | before | after |
|
||
|---|---|---|
|
||
| `publisher_logo` | 1.00 % | **0.75 %** |
|
||
| `developer_logos` | 0.39 % | **0.33 %** |
|
||
| `extras` | 0.20 %, region **736×525** | 0.19 %, region **398×295 at (441,230)** |
|
||
| `main_menu` | 0.06 % | 0.06 % |
|
||
| `title` | 1.82 % | 1.82 % |
|
||
|
||
The splashes improve outright. ⚠️ **`extras` is the interesting row**: the *area*
|
||
barely moved but its differing region **collapsed onto the sweep position**
|
||
(441, 270) — the residual is now localised to the one element still in question
|
||
rather than spread over the screen.
|
||
|
||
The title does not move. Its row is now posed at **t=355**, the Decoder's fitted
|
||
sweep time, because the leaf group ends at t=600 with the quads parked off-screen
|
||
at x=1521 — posing at the settle simply omits them. ⚠️ **t=355 is not the time
|
||
that minimises the difference**: t=390 measures **1.65 %**. Picking that would be
|
||
fitting the pose to the score, which is what this harness exists not to do.
|
||
|
||
## Refuted — my own "the single non-whole-multiple scale in the export"
|
||
|
||
`DECISIONS` has said since P1 that `ptlogo_eff2` is *"the single drawn element in
|
||
the whole export at a scale that is not a whole multiple of 100 % (125 %)"*.
|
||
**That census was parents-only.** Opening the 45 leaves finds **thirteen** distinct
|
||
non-whole-multiple scales — 75, 96, 99, 101, 103, 112, 125, 150, 204×208,
|
||
210×220, 250 — and 125 % is among the *rarest* at two occurrences.
|
||
`ptlogo1`/`ptlogo2` carry 101/103/112 on the **English** title.
|
||
|
||
The claim's real content was *"the only one **the port draws**"* — a fact about my
|
||
element set, not about the disc. Corrected.
|
||
|
||
🔴 **And `ptlogo_eff2` stays withheld, now for a stronger reason than caution.**
|
||
Its 125 % is a **pop**, not a steady scale: scale-0 → 125 % → scale-0 between
|
||
t=50 and t=107, ≈0.95 s. The leaf draws at 100 %, as **two superimposed copies**
|
||
at alpha 160 and 80, each rotating 360° over 960 units — 16 s per revolution.
|
||
**If parent scale gates the leaf it is a 0.95 s flash; if the leaf runs free it
|
||
spins for 16 s.** Nothing on the disc chooses, `title_jp` has no oracle capture,
|
||
and the Japanese-locale capture MISSION has parked is what would settle it.
|
||
|
||
## The 11.5 px was the fit's resolution, and the lesson inverts
|
||
|
||
The Decoder closed it **by adding observables, not by tuning** — the vertex
|
||
buffer carries positions *and* colours at the same instant, so all four
|
||
quantities must agree on one `t`:
|
||
|
||
| observable | solved t | precision |
|
||
|---|---|---|
|
||
| quad A x | **357.88** | ±0.12 units |
|
||
| quad B x | **357.58** | ±0.12 units |
|
||
| quad A alpha | 355.75 | ±1.54 units |
|
||
| quad B alpha | 354.09 | ±1.89 units |
|
||
|
||
Alpha moves only 0.27–0.33 levels per keyframe unit, so **one byte of
|
||
quantisation is worth 1.5–1.9 units, which at 4 px/unit is 6–8 px of sweep**.
|
||
That is the whole of the 11.5 px. At t = 357.7 the centres land within 0.70 px
|
||
and both alphas inside one level.
|
||
|
||
⚠️ **The lesson is the earlier one inverted, and this is the half worth keeping.**
|
||
Checking a wrong rule against alpha made it *look confirmed*. Here the same
|
||
insensitivity **manufactured a residual that did not exist**. So an insensitive
|
||
quantity does not merely fail to falsify — **it invents error.** Solve on the
|
||
fastest-moving field; check the slow one; never the reverse.
|
||
|
||
I was already looking for a pivot rule to explain 11.5 px when they wrote. There
|
||
was nothing to find.
|
||
|
||
### Refutation attempt — the pivot claim, checked here and survived with a nuance
|
||
|
||
They state the leaf pivot is (200, 90) on a 399×180 sprite, *"the pivot is the
|
||
centre, so rotation displaces it by nothing."* Checked against my own export:
|
||
pivot **[200, 90]**, sprite **399×180**, true centre **199.5, 90**.
|
||
|
||
✅ Survives. ⚠️ With one correction of no consequence: the sprite is **odd-width**,
|
||
so the pivot is the centre to within **half a pixel**, not exactly. Against their
|
||
−0.70/−0.48 px agreement that changes nothing, and it is worth stating only
|
||
because "displaces it by nothing" is the kind of sentence that later gets leaned
|
||
on for a sub-pixel claim.
|
||
|
||
`verify-capture` now poses the title at **t=357.7** rather than 355: RMSE
|
||
21.07 → **20.92**, differing 1.82 % → **1.81 %**. Marginal, and it is the right
|
||
pose for a stated reason rather than a better number.
|
||
|
||
### `ptlogo_eff2` is withheld for a better reason than mine
|
||
|
||
I was withholding it out of caution about untested generalisation. The Decoder
|
||
points out something stronger: **it is on `title_jp`, and MISSION §7 scopes out
|
||
"localisation beyond English"** — so it is not a question the menu port has to
|
||
answer at all, and the parked Japanese-locale capture does not need reviving on
|
||
its account. `authored/rendering.json` now says that first and the undecidability
|
||
second. **Widening scope to close a residual would have been the wrong trade**,
|
||
and it is the human's call either way.
|
||
|
||
## 🔴 The focus ring had silently stopped, and BLOCKED had listed it
|
||
|
||
`docs/port/BLOCKED.md` said the record-layout change touches five things:
|
||
`pose_at`, `settle_units`, `spin_period_units`, `exit_ramp_units` **and the
|
||
plate**. I checked `pose_at` and deleted `exit_ramp_units`, reported that, and
|
||
**did not work the rest of the list.**
|
||
|
||
`spin_period_units` required *"the first timed and the second untimed"*. Under
|
||
the corrected layout the ring reads `t=0 rot=0` and `t=120 rot=360` — **both
|
||
timed** — so the rule returned 0 and **the focus ring stopped spinning**. Nothing
|
||
reported it: a period of 0 is a legal *"this element does not spin"*.
|
||
|
||
Rewritten to take the **span** between the two poses. On the ring that is
|
||
120 − 0 = **120 units, the same number the old rule produced**, which is a small
|
||
piece of evidence that the corrected layout is self-consistent rather than merely
|
||
different.
|
||
|
||
**Verified the way P5 verified it — bit-identity one period apart**, on the ring's
|
||
own 60×60 box so the `ptloop` sweeps cannot confound it:
|
||
|
||
| separation | mean difference |
|
||
|---|---|
|
||
| **+120 units (one period)** | **0** |
|
||
| +120 units again | **0** |
|
||
| +30 units (quarter) | 8.61 |
|
||
| +60 units (half) | 8.88 |
|
||
|
||
⚠️ Getting there took three wrong instruments, and the sequence is the lesson.
|
||
A whole-frame `max` saturates on one rotating edge — adjacent frames scored 131
|
||
while their mean was 0.022. A live `--menu` filmstrip jitters by up to a frame,
|
||
which is ~3° of ring, and its cadence cannot be pinned. And a whole-frame
|
||
comparison is dominated by the sweeps, which move 480 px over one ring period.
|
||
**`--focus=<id>` was added so a `--screen` run can draw a focus record
|
||
deterministically**, which is what made the check reproducible at all.
|
||
|
||
## The plate's period is now the disc's 105, and it disagrees with the measurement
|
||
|
||
Under the corrected layout `ptbtn00f` runs **t=0 (alpha 0) → t=105 (alpha 0)** — a
|
||
closed cycle with every pose timed.
|
||
|
||
✅ **The ambiguity this entry carried is gone.** It used to say the cycle might
|
||
restart at t=6 rather than 0, giving 123 units, and that nothing separated the
|
||
two. There is now one reading.
|
||
|
||
🔴 **And the number is worse against the oracle, which is stated rather than
|
||
avoided.** 105 units is **1.750 s**; scaled by the factor the ring shows between
|
||
its declared 120 units and its measured 2.177 s (×1.089), **1.906 s** — about
|
||
**17 % below** every one of the corpus's four timings (2.12 / 2.19 / 2.34 /
|
||
2.31 s). The old 129 gave 2.34 s, at the top of that range, which is exactly why
|
||
it looked right.
|
||
|
||
**129 was the last timed keyframe plus `exit_ramp_units`, and that constant is
|
||
deleted.** A period built from a constant that no longer exists cannot stay, even
|
||
though it fitted better. So the port ships the disc's number **and says it is
|
||
wrong**, rather than keeping a number that agreed with the measurement for a
|
||
reason that has evaporated.
|
||
|
||
Verified: the plate is bit-identical 105 units apart (mean diff **0**) and differs
|
||
at 30 units (0.83). ❔ What would resolve the disagreement: whether the group
|
||
loops from its start at all, or holds at alpha 0 between cycles. Asked.
|
||
|
||
## The plate's period is 120, decoded — and it was falsified with my own ring number
|
||
|
||
The Decoder found it in the format: **a nested record is itself a RATC bundle,
|
||
and its header's `+0x08` is the loop length** — the same field
|
||
`ui_header_time_disc` already tests at the top level. A record's keyframes need
|
||
not fill it, and the slack is a hold at the final pose. **`ptbtn00f` is 105 units
|
||
of ramp inside a 120-unit cycle**, so the glow rests dark for 15 units. The five
|
||
`ptbtn0Nf` records fill their 120 exactly, which is what shows the slack belongs
|
||
to *that record* rather than to the format.
|
||
|
||
Disc-wide over 1 781 timed nested records: 92.3 % declare exactly their last
|
||
keyframe time, 7.7 % declare more, **0 declare less**. That last row is the
|
||
falsifier — a cycle cannot restart before its own last pose — and it never fires.
|
||
|
||
### The decisive test used this port's number, not theirs
|
||
|
||
Both candidates need the same emulator pacing factor, and **the ring measures it
|
||
independently**: declared 120 units → 2.177 s → **1.0885**.
|
||
|
||
| plate period | nominal | factor needed for the measured 2.12–2.34 s | |
|
||
|---|---|---|---|
|
||
| 105 units | 1.750 s | 1.211 … 1.337 | 🔴 excludes 1.0885 |
|
||
| **120 units** | 2.000 s | 1.060 … 1.170 | ✅ **contains it** |
|
||
|
||
**105 cannot reach the measured range under any pacing the ring also satisfies.**
|
||
Two different elements in different bundles, measured in separate runs, tied only
|
||
by both declaring 120.
|
||
|
||
⚠️ **My three readings of this number, in order, are the useful record:** 129
|
||
(`105 + exit_ramp_units`) fitted the measurement for a reason that later
|
||
evaporated; 105 (the group length) I shipped *knowing* it was 17 % short; 120 is
|
||
decoded. And the 123-vs-129 pair I once called unseparable **straddled the right
|
||
answer without containing it** — which is the sharpest argument I have seen
|
||
against treating "two candidates, nothing separates them" as if the truth must be
|
||
one of the two.
|
||
|
||
Verified: bit-identical **120 units** apart (mean diff 0), 0.061 at a quarter and
|
||
0.888 at half. Still authored, because the pinned tag does not expose `+0x08`
|
||
yet — **delete the entry the day a tag does.**
|
||
|
||
### Their `rest()` flag, checked rather than assumed
|
||
|
||
They warned that a focus record is exactly the kind of element `rest()`
|
||
mishandles, since a pulse's last hold is not its resting state. Censused: **34
|
||
focus-record elements in the export, and only 2 have a varying alpha** — both
|
||
`ptbtn00f`, EN and JP. Their `rest.alpha` is **80, identical to their peak**,
|
||
which is precisely the pathology described. The port does not hit it because the
|
||
plate is drawn through the loop path, and the other **32 are constant-alpha, so
|
||
`rest()` is safe for them**. Bounded, not hoped.
|
||
|
||
## ✅ A settled screen is ONE instant, and it collapsed three residuals at once
|
||
|
||
The Decoder's finding, applied: **`rest()` returns each element's last hold
|
||
keyframe chosen independently of every other element.** That is right for
|
||
anything that ends the screen settled and **exactly wrong for a transient** —
|
||
the title's `ptlogo_back2eff1` is a two-frame flash (0 until t52, 255 at t54–56,
|
||
0 by t58), so its last hold *is* the flash peak and `rest()` left it burning.
|
||
There are five of them, and `rest()` drew all five at once.
|
||
|
||
The settled instant is **the longest interval containing no keyframe time**, over
|
||
a bundle's **top-level** elements. Reproduced independently here before adopting:
|
||
title `[160, 236]`, midpoint **198** — the Decoder's number to the unit. ⚠️ The
|
||
top-level restriction is what makes it match: including the `ptloop` leaves gives
|
||
`[269, 540]` instead.
|
||
|
||
### Against the oracle
|
||
|
||
| screen | before | after |
|
||
|---|---|---|
|
||
| **`title`** | 20.92 RMSE, **1.81 %** | **14.61 RMSE, 0.26 %** |
|
||
| **`publisher_logo`** | 9.05, **0.75 %** | **2.17, 0.01 %** |
|
||
| **`developer_logos`** | 8.86, **0.33 %** | **3.05, 0.01 %** |
|
||
| `main_menu` | 0.08 % | 0.08 % — unchanged, window too narrow |
|
||
| `extras` | 0.19 % | 0.19 % — unchanged, window too narrow |
|
||
|
||
**Seven times fewer differing pixels on the title, seventy-five times fewer on
|
||
the publisher splash**, whose differing region is now a **13×18 box**. This is
|
||
the largest correctness gain the port has had, and none of it is mine: it is a
|
||
decode, computed from the keyframe table with no reference to any capture.
|
||
|
||
### ⚠️ It is applied only where the window is wide, and that bar is not invented
|
||
|
||
The widths in this export split with **nothing in between**: `press_start` 214,
|
||
`publisher_logo` 190, `developer_logos` 145, `title` 76 — then `main_menu` 12,
|
||
`extras` 12, the loading screens 8 and 4. A 12-unit "settle" on a menu that
|
||
builds in until t=70 is a gap between staggered ramps, not a settled pose.
|
||
|
||
The bar is **30 units**: the Decoder's disc-wide census puts the knee there (30 %
|
||
of bundles ≥ 30, 42 % under 10, the latter mostly `loop*` fragments meant to be
|
||
in motion), and this export's own screens sit **4× either side of it with nothing
|
||
between 12 and 46**. Two independent populations agreeing on where to cut is what
|
||
makes it a bar rather than a preference.
|
||
|
||
Checked unbroken: the boot pacing is unmoved (`developer_logos@4.26`,
|
||
`title@7.91`, developer agrees) and the scripted walk still runs end to end with
|
||
focus restored.
|
||
|
||
## Their census, and a framing of mine they sharpened
|
||
|
||
I reported *"34 focus-record elements in the export, only 2 with a varying
|
||
alpha"*. Disc-wide it is **210 varying, 202 with `rest()` at the peak**, across
|
||
1 130 focus records — 116 in `GP_DEBRIEFING_PILOTLOG`, 54 in `GP_MOVIE_THEATER`,
|
||
30 in `GP_HANGAR_ARSENAL`, 8 in `GP_LEADERBOARD`, and **2 in `GP_TITLE`**.
|
||
|
||
**My 2 is right because `GP_TITLE` has 2.** ⚠️ But *"only 2 have a varying alpha"*
|
||
reads as a fact about the format and is a fact about one pak — and the pathology
|
||
sits in exactly the screens a wider port reaches next. The sentence was true as
|
||
measured and false as remembered, which is the failure this corpus keeps
|
||
finding, and it was mine this time.
|
||
|
||
⚠️ **And they corrected a framing I had:** I called `rest.alpha == peak` "the
|
||
pathology". It is worse than that — **a pulsing element has no resting pose at
|
||
all.** The question `rest()` answers is *malformed* rather than mis-answered,
|
||
because the element's state is a phase, not a value. `pose_at(t)` with `t` inside
|
||
the record's own declared cycle is the only well-formed query on one.
|
||
|
||
🔴 Worth carrying for whenever this port grows: `GP_LEADERBOARD`'s
|
||
`py_ranking_btn01f` swings 255 → 127 → 255 with no two adjacent keyframes equal,
|
||
so `rest()` falls through to its longest-dwell rule and returns **244** — neither
|
||
peak nor trough. **A glow stuck at its peak is visibly wrong; one stuck at 244 of
|
||
a 127–255 range looks entirely plausible, and nothing reports it.**
|
||
|
||
✅ And a free second instance of the loop-length decode from a pak neither of us
|
||
was looking at: `py_ranking_btn01f`'s ramp ends at **t=90 inside a declared 120**
|
||
— 30 units of hold, the same shape as the plate's 105-in-120.
|
||
|
||
## Their "do not draw all five flashes" flag — checked, and it does not apply here
|
||
|
||
The Decoder armed a draw capture before the title exists and caught the build-in
|
||
on the console: `ptlogo_back2eff1` in frames 130–131, `eff2` at 133, `eff4` at
|
||
133–135 — and **`eff3` never drawn at all.** Not a miss: a flash's peak is 2
|
||
keyframe units, which at that run's pacing is **0.85 of a presented frame**, so
|
||
which flashes get sampled is a matter of phase. The console shows a *subset* on
|
||
any given play, and the warning was that drawing all five would read heavier than
|
||
the real thing.
|
||
|
||
**Checked rather than reasoned about.** Sweeping the port's build-in with
|
||
`--no-hold`:
|
||
|
||
| t | flashes drawn |
|
||
|---|---|
|
||
| 54, 56 | `eff1` |
|
||
| 58 | `eff2` |
|
||
| 60 | `eff2`, `eff3` |
|
||
| 62 | `eff3`, `eff4` |
|
||
| 64 | `eff4`, `eff5` |
|
||
| 66, 68 | `eff5` |
|
||
|
||
**The port draws them sequentially, never more than two at once**, and the two
|
||
only overlap where their declared windows abut. That is the stagger the disc
|
||
declares, not a pile-up — the pile-up was the `rest()` bug, and it is fixed.
|
||
|
||
⚠️ **So the difference from the console is presentation rate, not content.** At
|
||
60 fps each 2-unit flash gets ~2 frames; at the console's pacing it gets 0.85, so
|
||
some are skipped. **A frame-by-frame comparison of the build-in against a console
|
||
capture will therefore show flashes the console missed, and that is a fact to
|
||
know rather than a bug to fix.** It is also why the settled-frame comparison —
|
||
the one `verify-capture` makes — is unaffected: at t=198 none of the five is
|
||
drawn.
|
||
|
||
### Three things of theirs worth recording
|
||
|
||
✅ **My top-level restriction was verified, not merely accepted**: top-level
|
||
`[160, 236]` width 76, versus `[269, 540]` width 271 with the `ptloop` leaves
|
||
included — an instant *after every top-level element has exited*. Worth having on
|
||
their page, because the rule as described permits the wrong reading.
|
||
|
||
✅ **The 120-unit loop is confirmed from the guest's own vertex data**, not
|
||
inferred from pixels: the glow quad's per-vertex colour alpha *is* the element's
|
||
fade alpha, giving an observed range of **0…80 against a decoded peak of 80**,
|
||
exact and unfitted, over 20 cycle starts. Fitting the decoded ramp gives RMS
|
||
13.16 alpha levels against **38.18 for the same ramp reversed** — if the shape
|
||
carried no information those would be equal.
|
||
|
||
📌 **A trap noted for whenever this port grows draw-stream tooling:** a 2D draw's
|
||
identity is its **vertex geometry, not its bound texture**, because these sprites
|
||
sample large shared pages. Matching on texture dimensions told them first that no
|
||
flash is ever drawn, then that `ptbase2` and `pteff04` are drawn in frames 75–105
|
||
— which are the intro movie, whose YUV planes are 640×360 targeting 1280×720.
|
||
Two errors, opposite directions, one pass, neither loud.
|
||
|
||
## ✅ The `publisher_logo` residual was a missing black hold, and we had both dismissed it
|
||
|
||
I had carried this as *"0.03 s outside a composite bound, probably a property of
|
||
the bound rather than the game"*, and the Decoder agreed. **We were both wrong,
|
||
and the way it was settled is the point: I stopped reasoning about the bound and
|
||
filmed the transition.**
|
||
|
||
At 0.05 s the port fell straight out of the publisher's fade into the developer
|
||
logos — mean 5.06 → 0.32 at t=4.20, then **5.65 at t=4.25**. There was **no black
|
||
frame at all**, where the oracle measures a 0.17–0.23 s pure-black plateau
|
||
(HANDOFF Q7). The bound was fine. The port was missing a fifth of a second of
|
||
black, and had been since P3.
|
||
|
||
**Authored at 12 units**, because on the boot path there is nothing to read it
|
||
from: `publisher_logo` and `developer_logos` each carry a single `palogo_eff0` —
|
||
a 1280×720 primitive with **one keyframe at t=0**, static, not a transition ramp.
|
||
The menus' quad declares black for 12 units and 12/60 = **0.200 s** sits in the
|
||
middle of the measured range, so the number is the disc's where a screen has one.
|
||
|
||
Filmed after: **t=4.25, 4.30, 4.35, 4.40 all at mean 0** — four black frames,
|
||
0.20 s — then the developer logos at 4.45.
|
||
|
||
| | before | after |
|
||
|---|---|---|
|
||
| publisher interval | 4.26 s, **DIFFERS** | **4.47 s, agrees** |
|
||
| developer interval | 3.62 s, agrees | 3.73 s, agrees |
|
||
|
||
The settled-frame comparisons are untouched, as they should be — this is pacing,
|
||
not pixels.
|
||
|
||
⚠️ **The lesson is about the shape of the dismissal, not the number.** *"A 0.03 s
|
||
miss against a bound composed from two measured ranges plus jitter slack is more
|
||
likely a property of the bound"* is a **plausible** explanation, it was offered
|
||
and accepted by both of us, and it was wrong. The composite bound was the reason
|
||
the miss looked small — the underlying gap was 0.2 s — and a plausible
|
||
explanation for a small number is exactly how a real defect stays hidden. **The
|
||
film cost one command.**
|
||
|
||
## `ptlogo_back2eff3` — recorded, deliberately not acted on
|
||
|
||
The Decoder has reproduced across two independent build-ins that the console
|
||
**never draws `eff3`**: 0 draws against ~5 expected, while `eff1` gets 4, `eff2`
|
||
3 and `eff4` 6. Three explanations are ruled out — sampling phase (`eff3` is
|
||
non-zero over six units against a 2.23-unit step, and frames at t=60.1 and 62.3
|
||
sit inside it drawing `eff2` and `eff4` instead), a draw the log cannot see, and
|
||
a bad position guess (no quad anywhere is within ±30 of the expected 408 width;
|
||
the spectrum jumps 262 → 748).
|
||
|
||
❔ **But *why* is not established** — nothing in `eff3`'s record differs from its
|
||
neighbours: same kind `0x0`, same keyframe shape, same `u4`/`u8`, same scale.
|
||
|
||
🔴 **So the port keeps drawing it, and that is a decision rather than an
|
||
oversight.** Dropping an element the disc declares, on a measurement with no
|
||
mechanism behind it, is authoring a behaviour neither agent can derive — and
|
||
**nothing this port gates on would notice either way**: the flashes live only in
|
||
the build-in, and `verify-capture` compares the settled frame at t=198 where none
|
||
of the five is drawn. Acting would buy no measurable fidelity and cost an
|
||
unexplained exception in `authored/`.
|
||
|
||
**What would change it:** a mechanism in the record, or a gate that measures the
|
||
build-in against a capture. Until then the port is visibly wrong for two frames
|
||
during a build-in nobody compares, which is the cheaper of the two wrongs.
|
||
|
||
## The narrow settle windows are harmless, and I can now say why
|
||
|
||
Adopting the settle instant left `main_menu` and `extras` on per-element `rest()`
|
||
— their windows are 12 units, below the 30-unit bar — and I recorded that as a
|
||
gap. **It is not one.** `rest()` is malformed only for a **transient**, and the
|
||
transients are precisely on the screens whose windows are wide:
|
||
|
||
| screen | window | transients |
|
||
|---|---|---|
|
||
| `title` | 76 | `ptlogo1`/`ptlogo2` ×4, `pteff01`, `ptlogo_back2eff1…5`, `ptlogoall_eff` |
|
||
| `publisher_logo` | 190 | `palogo_sqex_eff` |
|
||
| `developer_logos` | 145 | `palogo_*_eff` ×3 |
|
||
| **`main_menu`** | **12** | **none** |
|
||
| **`extras`** | **12** | **none** |
|
||
|
||
The discriminator is *returning to dark far before the screen's own end* —
|
||
`ptlogo_back2eff1` is dark again at t=58 while the title runs to t=269 — as
|
||
opposed to the ordinary fade-in-hold-fade-out that every menu element has, where
|
||
`rest` at 255 **is** the settled pose and the final 0 is the exit.
|
||
|
||
⚠️ **I am not claiming that as a rule.** Two screens having narrow windows *and*
|
||
no transients could be coincidence; it is n = 2, and n = 2 is where I have gone
|
||
wrong before. What it does mean is that **nothing measurable is being left on the
|
||
table by the 30-unit bar today.**
|
||
|
||
## Refuted, mine — "the menu residual is localised on the `ptloop` sweeps"
|
||
|
||
I have written that twice. It came from reading the **bounding box** of the
|
||
differing pixels, which sat at (441, 230) — the sweep position. Tested by sweeping
|
||
the leaf's phase against the live-menu captures:
|
||
|
||
| `main_menu`, sweep phase | differing |
|
||
|---|---|
|
||
| t=60 (barely on screen) | 0.063 % |
|
||
| t=350 (mid-screen) | **0.183 %** |
|
||
| t≥600 (parked off-screen) | **0.061 %** |
|
||
|
||
✅ Two things fall out. **The capture shows no sweeps**: the port matches best
|
||
when they are off-screen and three times worse when they cross the middle. And
|
||
with them parked the residual's box is **834×358 at (445, 167)** — the *button
|
||
column*, which is where the focus signature lives, not the sweep position.
|
||
|
||
On `extras` the same test moves the box (398×295 at the sweep position at t=70,
|
||
736×525 spread at t=700) while the **magnitude barely changes: 0.192 % against
|
||
0.200 %**.
|
||
|
||
🔴 **So the box moves with the sweeps and the residual does not.** A bounding box
|
||
over scattered pixels tells you where the outermost differing pixels are, **not
|
||
where the difference is** — and I had been quoting it as if it localised a cause.
|
||
`verify-capture` prints that box, so this is a caution about reading my own tool.
|
||
|
||
### And a first piece of evidence on whether the sweeps loop
|
||
|
||
The disc gives one pass, t=0…600, ending parked off-screen at x=1521.
|
||
`ORACLE-CAPTURES.md` says the title's sweeps *"move continuously"*, which I had
|
||
taken as implying a loop. **The idle main-menu capture matches best with them
|
||
off-screen**, which is evidence they run once and park.
|
||
|
||
⚠️ One capture, one screen, and "best match" is a weak instrument for an absence
|
||
— but it is the first evidence either way, and it points against looping. The
|
||
loop-length field the Decoder decoded (`+0x08` of a nested record's header) would
|
||
settle it outright; it is not in the pinned tag.
|
||
|
||
## Refuted — "the developer splash is one composited quad, the bounding box of the three logos"
|
||
|
||
The Decoder observed the game submitting **one 525×259 quad at (378, 155)** on
|
||
the developer splash and read it as the bounding box of the three logos, warning
|
||
that drawing three sprites there draws something the console does not. **The
|
||
arithmetic does not support it**, and the port keeps drawing three.
|
||
|
||
| | bounding box |
|
||
|---|---|
|
||
| the **three logos** | **500×421 at (390, 164)** |
|
||
| `gamearts_eff` + `seta_eff` | **521×261 at (379, 154)** |
|
||
| the observed quad | **525×259 at (378, 155)** |
|
||
|
||
**A 259-tall quad cannot contain the three logos**, which span y 164…585:
|
||
`palogo_anima` alone starts at y = 449, thirty-five pixels below that quad's
|
||
bottom edge. The observed quad matches the union of the two `_eff` **glows** to
|
||
about four pixels in every dimension.
|
||
|
||
⚠️ And those two are **transients** — my own census flagged them, dark again by
|
||
t=45 — so a frame containing that quad is a **build-in** frame, not the settled
|
||
screen. Consistent with a draw capture that starts early, which is exactly what
|
||
theirs does.
|
||
|
||
I cannot see their draw stream, so I have sent the arithmetic rather than a
|
||
verdict. What I will not do is stop drawing an element on a claim whose stated
|
||
identification excludes that element from its own bounding box.
|
||
|
||
## The black hold is 9 units, not 12 — measured in draws rather than luminance
|
||
|
||
I authored 12 from HANDOFF Q7's luminance plateau of 0.17–0.23 s, supported by
|
||
the menus' transition quad declaring black for 12. The Decoder counted **submitted
|
||
quads** instead, which is the better instrument: luminance cannot separate the
|
||
outgoing fade's tail from true black.
|
||
|
||
Frames 21–125 submit `palogo_sqex` fading to alpha 7; **frames 126–129 submit no
|
||
sprite quad at all**; 130–153 fade the developer splash in from alpha 34. Four
|
||
presented frames at 2.284 units/frame — a rate derived from the **disc as its own
|
||
clock**, because that run ran at 13.1 fps against 28 elsewhere — gives **9.1
|
||
units = 0.152 s**, ±1 frame 6.9–11.4.
|
||
|
||
⚠️ **It disagrees with the luminance figure and the disagreement is the point.**
|
||
0.114–0.190 s against 0.17–0.23 s overlaps only at the top, and the true black is
|
||
**shorter** than 9 even so: both boundary frames still carry picture. My 12 was
|
||
also supported *by analogy* — a different screen's quad on a different path,
|
||
where the boot splashes carry no quad at all. **A number that fits by analogy
|
||
loses to one measured in place.**
|
||
|
||
`verify-dwell`'s bound moved with it. Both screens still agree: publisher 4.42 s,
|
||
developer 3.78 s.
|
||
|
||
## The title's sweeps loop — measured, and the field could not have told us
|
||
|
||
The disc gives one pass (`ptloop01` t=0…600, `ptloop02` t=0…720), each ending
|
||
parked off-screen, and the port ran them once. **The oracle says they loop**:
|
||
across two title dwells the sweep quad oscillates over its whole x range and
|
||
resets hard to the same start — one reset in the first dwell, two in the second.
|
||
|
||
🔴 **And the loop-length field could not have settled it, which corrects a hope I
|
||
had stated.** Both records declare exactly their last keyframe time — **slack
|
||
zero** — and *"loops at 600"* and *"runs once for 600 and stops"* write the
|
||
identical header. 92.3 % of records on the disc are in that state, so the field
|
||
discriminates only where there **is** slack, as the plate's 105-in-120 had.
|
||
|
||
Implemented and verified on the two sweeps' **least common multiple**, since they
|
||
have different periods: 600 and 720 realign at **3600 units = 60 s**.
|
||
|
||
| separation | mean difference |
|
||
|---|---|
|
||
| **+3600 units (LCM)** | **0** |
|
||
| +1800 units | 0.438 |
|
||
| +600 units (`ptloop01` only) | 0.100 |
|
||
|
||
⚠️ **Scoped to the title**, because that is where it is measured. The menus
|
||
declare the same 600/720 and nothing on the disc distinguishes them, but my own
|
||
weak evidence points the other way there — sweeping the phase against
|
||
`live-main-menu.png`, the port matches best with the sweeps **off-screen**
|
||
(0.061 %) and three times worse mid-screen (0.183 %), and if they looped the
|
||
sweep is on screen for roughly 73 % of the cycle. **Two weak signals in opposite
|
||
directions is a reason to scope, not to pick.**
|
||
|
||
## The menus' residual is the tone floor, not structure — and `extras` is not really 3× worse
|
||
|
||
`extras` sits at 0.19 % differing against `main_menu`'s 0.06 %, on two screens of
|
||
the same family, and that gap wanted explaining.
|
||
|
||
**Signed difference (port − capture), by cell:**
|
||
|
||
| | x=0 | x=320 | x=640 | x=960 |
|
||
|---|---|---|---|---|
|
||
| `extras` y=169 | **+12.13** | −3.64 | +2.43 | **+10.60** |
|
||
| `extras` y=338 | **+12.29** | +1.36 | +1.42 | **+9.05** |
|
||
| `main_menu` y=169 | **+11.63** | −0.68 | +3.01 | **+10.24** |
|
||
| `main_menu` y=338 | **+11.03** | +3.93 | +2.92 | **+8.84** |
|
||
|
||
**The two screens are nearly identical**, and the port is uniformly **+9 to +12
|
||
brighter in the dark outer columns** — which is exactly the transfer curve I
|
||
measured earlier: γ > 1 in the darks, capture darker than render. There is no
|
||
dipole, no displacement, no missing element.
|
||
|
||
So the 0.06 % / 0.19 % gap is **not a difference in fidelity**. The thresholded
|
||
count only sees pixels differing by more than 64 levels, which are text and
|
||
sprite **edges**; the two screens simply have different amounts of high-contrast
|
||
edge. The *level* disagreement, which is what a tone term produces, is the same
|
||
on both.
|
||
|
||
⚠️ **This is the bounding-box lesson again in a different costume.** I had two
|
||
numbers, 0.06 and 0.19, and took the ratio as meaningful. It is a count of
|
||
threshold crossings, and a count of threshold crossings is not a measure of how
|
||
wrong a screen is.
|
||
|
||
### A diagnostic trap of my own, worth writing down
|
||
|
||
My first pass at this reported **10 of 18 elements "transparent at rest"** on
|
||
`extras` — the buttons, the title, the frames — and looked exactly like a
|
||
missing-element bug. It was not. **`--screen=NAME` without `--time` renders at
|
||
t = 0**, and `pose_at` clamps `t` to `minf(t, settle_units)`, so t=0 stays t=0 and
|
||
every element is still at its first keyframe. Passing `--time=2.0` draws 18 of 18.
|
||
|
||
The tool was right and my invocation was wrong, and the failure looked like a
|
||
serious defect rather than an empty argument. Same family as the instrument traps
|
||
this session has collected — and mine was the one that reported a *worse* problem
|
||
than existed, which is the direction that wastes an iteration rather than hiding
|
||
one.
|
||
|
||
## Refutation attempt — their 239.8-unit figure, checked from my export
|
||
|
||
The Decoder converted the boot's black gap using 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."* That 239.8 comes from their reading of the record; I have
|
||
the same element in my export and can compute it independently.
|
||
|
||
`palogo_sqex` ramps 0 → 255 over t=15…30 and 32 → 0 over t=251…255. Under the
|
||
linear ramp the port already uses, alpha first reaches 1 at **t = 15.0588** and
|
||
last exceeds it at **t = 254.8750**:
|
||
|
||
**239.816 units.**
|
||
|
||
✅ **Survives, to four significant figures.** It matters more than a spot-check:
|
||
that number is the *denominator* of the units-per-frame conversion behind the
|
||
9-unit black hold I just authored, so an error in it would have propagated
|
||
straight into a constant I ship. Two derivations from different sides of the same
|
||
record agreeing to 0.02 % is what makes that constant safe to hold.
|
||
|
||
## 🔴 The loading screens are black at *every* instant — which proves the layer rule wrong for a layerless element
|
||
|
||
`build_12` and `build_15` have rendered blank since P3, and I had filed it as a
|
||
`rest()` casualty: `pgloading_eff00` is a 1280×720 quad whose `rest` is opaque
|
||
black. **It is not that**, and the corrected keyframe association makes it
|
||
provable rather than suspected.
|
||
|
||
Every element's declared alpha window on `build_12`:
|
||
|
||
| element | opaque / visible | gone by |
|
||
|---|---|---|
|
||
| **`pgloading_eff00`** (black quad) | **t=0 … 38 at alpha 255** | clears at t=48 |
|
||
| `pgloading_loop4` | t=8…32 | 38 |
|
||
| `pgloading_loop1` | t=16…32 | 40 |
|
||
| `pgloading_line` | t=18…26 | 32 |
|
||
| `pgloading_str` | t=22…28 | 34 |
|
||
| `pgloading_loop3` | t=24…26 | 34 |
|
||
| `baseeff`, `eff01`, `eff02`, `loop5` | t≈16…32 | 32–40 |
|
||
|
||
**The quad is fully opaque across the entire span in which any content is up, and
|
||
it only clears at t=48 — by which time every other element has faded to zero.**
|
||
Rendered at t = 20, 30, 36, 40, 42, 44, 46, 50 units with the timeline *playing*,
|
||
the frame is **mean 0 at every one**.
|
||
|
||
So this is not a bad choice of pose. **Under the port's current layer rule there
|
||
is no instant at which this screen shows anything**, and a loading screen that is
|
||
black for its whole life is not what the game does. That is a proof by
|
||
contradiction, not a preference.
|
||
|
||
### The rule under suspicion is mine, and it is narrow
|
||
|
||
`pgloading_eff00` carries `layer_source: "none"` — no layer key at all — and the
|
||
exporter sorts a layerless element **last**, i.e. on top. Its `paint_order` is
|
||
`[6, 7, 8, 9, 1, 5, 4, 2, 3, 0]` with element 0, the quad, drawn last.
|
||
|
||
⚠️ **Every other full-frame primitive in the export has a layer key** —
|
||
`main_menu`, `extras` and `title` all give their `pteff00` `0x00008030` — so this
|
||
rule only ever bites here, which is exactly why it survived eight milestones
|
||
behind two screens nobody draws.
|
||
|
||
❔ **Where a layerless element sorts is a decoding question and I am not
|
||
answering it.** If the game sorts it *first* — behind everything — the screen
|
||
renders and the quad becomes a backdrop rather than a cover, which is what a
|
||
1280×720 black rectangle at the bottom of a loading screen would sensibly be.
|
||
That reading is *consistent* with the contradiction above, which is not the same
|
||
as being established, and I have asked rather than flipped the sort.
|
||
|
||
✅ **What this does settle:** `verify-screen`'s `BLANK` verdict on those two rows
|
||
was the right call. It reports that both renderers drew nothing and that the row
|
||
proves nothing — and `sylpheed-cli` agrees with the port here precisely because
|
||
it shares the assumption under suspicion. Two renderers agreeing, again.
|
||
|
||
## Their `eff3` retraction — my refusal was right, and my refutation found the same bug
|
||
|
||
The Decoder has withdrawn *"the game never draws `eff3`"*. It draws all five, in
|
||
the declared stagger, in both title entries. **And the mechanism was the one my
|
||
developer-splash refutation had already caught one layer down**: a draw batches
|
||
several quads and the log dumps only the first 8 vertices, so min/max over a
|
||
line's vertex list *merges* them. `eff3` (788…1196) lies entirely inside `eff4`
|
||
(447…1196), so the union is exactly `eff4`'s extent and `eff3` vanished with
|
||
nothing anomalous to see. My `525×259` was `gamearts_eff` merged with `seta_eff`
|
||
by the identical mechanism.
|
||
|
||
⚠️ **The part worth carrying is theirs**: three explanations were reported "ruled
|
||
out", and all three were aimed at the wrong failure — the *"a draw the log cannot
|
||
see"* check counted draws with **no** geometry 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.
|
||
|
||
Nothing in the port changes: `eff3` was never dropped, and the developer splash
|
||
still draws three sprites.
|
||
|
||
## The forced backdrop: two of sixteen screens were black for their whole life
|
||
|
||
`build_12` and `build_15` — the two dressed loading screens — rendered as **pure
|
||
black at every instant of their declared timeline**. Not at rest, where a wrong
|
||
`rest.t` could explain it: at t = 20, 30, 36, 40, 42, 44, 46 and 50 units with
|
||
the timeline *playing*, mean 0 in every frame.
|
||
|
||
That is not a defect you can attribute to a pose. A screen that is black for its
|
||
entire life is impossible on its face, and it is the kind of impossibility that
|
||
survives a render-vs-render diff: `verify-screen` scored those two rows
|
||
`max 0 mean 0 over3 0 OK` — **the strongest verdict that script has, awarded for
|
||
comparing nothing against nothing.** Both renderers were black because both
|
||
share `implied_layer_key`. The blank guard now in `verify-screen` was written
|
||
after that, and it is what turned the pass into a row that says it proves
|
||
nothing.
|
||
|
||
### The rule, and whose it is
|
||
|
||
It is the **Decoder's**, decoded from the file rather than inferred from the
|
||
render:
|
||
|
||
> An element that covers the screen and is **fully opaque** at some instant
|
||
> cannot paint above anything visible at that instant. Where the elements
|
||
> visible during its opaque span are **all** of them, its position is forced to
|
||
> first.
|
||
|
||
`pgloading_eff00` is a full-screen quad at alpha 255 from t=0 to t=38, clearing
|
||
at t=48; every other element on those screens peaks around t=8–32 and is gone by
|
||
t=32–40 — entirely inside the opaque span. Under a layer-key sort it painted
|
||
over all nine of them, at every instant they existed. Hence black.
|
||
|
||
### What is implemented, and the two limits that are not negotiable
|
||
|
||
`forced_backdrop_first` in `crates/sylpheed-export/src/screen.rs`, as a post-pass
|
||
over `ui_layout::derived_paint_order`. Two restrictions are copied from the
|
||
Decoder verbatim because each one was found by a test that failed:
|
||
|
||
* 🔴 **Elements with no sprite only.** Applied to sprites, the rule claimed 22
|
||
`.t32` textures must sort first *against their own layer keys*. An element's
|
||
alpha says nothing about whether its **texture** covers the screen — most of a
|
||
sprite may be transparent. The assertion that caught this was one the Decoder
|
||
had nearly deleted as over-strict.
|
||
* 🔴 **Not a name heuristic.** `*base*` first / `*eff*` last matches 77 of 80 and
|
||
fails on exactly the three families that cross it: `palogo_eff0`,
|
||
`pgloading_eff00`, `pzeff00`. `palogo_eff0.prm` is named like an overlay and is
|
||
*measured* painting first. The name is not the rule; occlusion is.
|
||
|
||
⚠️ Reach: it assumes straight alpha-over. Blend mode is undecoded, and an
|
||
additive quad at alpha 255 would not occlude. It is a **lower bound on one
|
||
element's position**, not an ordering — 80 elements are forced, 50 are
|
||
constrained but not forced, and this says nothing about those 50.
|
||
|
||
### The controls
|
||
|
||
Both are the Decoder's prior measurements off the running game. No new oracle run
|
||
was made for this change, by either agent.
|
||
|
||
| primitive | measured | our opaque instants | outcome |
|
||
|---|---|---|---|
|
||
| `palogo_eff0.prm` | **first** | 256 (they measured 211) | ✅ forced first |
|
||
| `pteff00.prm` | **last** | **2** (they measured 2) | ✅ still last |
|
||
|
||
`pteff00` is the one that would break if this were implemented as "push every
|
||
layerless element down". It is the fade cover: opaque at t=0 and again at t=269,
|
||
its screen's entry and exit, and transparent for the 253 instants between. The
|
||
constraint never binds it, and it remains last on all four title-family screens.
|
||
|
||
The `palogo_eff0` count differs — 256 against 211 — because we take the opaque
|
||
span to the **screen's** last keyframe (255) and they stop at 210. It changes no
|
||
verdict here, since the element is opaque across the whole span either way, but
|
||
the two spans are not the same span and only one of them can be the screen's.
|
||
Filed in BLOCKED.
|
||
|
||
An element **holds its final pose to the end of the screen**; it does not vanish
|
||
at its own last keyframe. Reading `palogo_eff0`'s span as `0..=0` — it declares a
|
||
single keyframe — would make the splash's backdrop a one-instant event rather
|
||
than the thing on screen for the whole splash. Rendering `build_12` confirms the
|
||
hold directly: the frame is constant from t=30 to t=60 with the timeline running.
|
||
|
||
### What changed, measured
|
||
|
||
* `build_12`/`build_15`: mean 0 at every instant → ramps in over t=0…30 and
|
||
holds (mean 1.95, max 214.5). The two BLANK rows are gone from `verify-screen`.
|
||
* The splashes are unmoved against the **oracle**: `publisher_logo` 0.01 %,
|
||
`developer_logos` 0.01 % differing region, unchanged before and after.
|
||
⚠️ That is **non-regression, not confirmation** — `verify-capture` poses at the
|
||
settle instant, and the ordering does not necessarily bind there. The evidence
|
||
for the rule is the Decoder's two controls and the impossibility of a
|
||
permanently black screen, not this row.
|
||
* Six `verify-screen` rows now DIFFER: the six screens the rule touches. The
|
||
reference `sylpheed-cli` builds from the workspace `sylpheed-formats`, which
|
||
does not have the rule. **That disagreement is expected and must not be tuned
|
||
away** — it ends when a pinned tag carries the Decoder's change, at which point
|
||
this post-pass is deleted rather than kept in two places.
|
||
|
||
It also explains 36 builds the Decoder had filed as "coming out one colour":
|
||
`pzeff00.prm` is forced first in 32 of 32 of them. Those were wiped by our own
|
||
sort.
|
||
|
||
## Refutation attempt — the forced-backdrop rule's quantifier, and whether it misses a case
|
||
|
||
The rule fires only when **all** other elements are visible during the opaque
|
||
span. That is a strict quantifier, and a strict quantifier fails quietly: an
|
||
element that is a full-screen opaque backdrop but misses the bar by one dark
|
||
element would keep its layer-key position and go on hiding the screen, exactly
|
||
the defect the rule was written to fix. So the question worth asking of somebody
|
||
else's rule is not "is it right" but **"is it enough"** — and that one I can test
|
||
without an oracle, over every layerless full-screen element in `GP_TITLE`.
|
||
|
||
| screen | element | opaque instants | others visible | forced |
|
||
|---|---|---|---|---|
|
||
| `build_12`, `build_15` | `pgloading_eff00` | 39 | **9 / 9** | ✅ |
|
||
| `developer_logos` ×2 | `palogo_eff0` | 211 | **6 / 6** | ✅ |
|
||
| `publisher_logo` ×2 | `palogo_eff0` | 256 | **2 / 2** | ✅ |
|
||
| `title`, `title_jp` | `pteff00` | 2 | 3 / 23 | — |
|
||
| `main_menu` ×2 | `pteff00` | 2 | 7 / 15 | — |
|
||
| `extras` ×2 | `pteff00` | 2 | 5 / 17 | — |
|
||
| every screen | `pteff02` | **0** | — | — |
|
||
|
||
**The rule survives, and the margin is the reason.** Nothing sits near the
|
||
boundary. Every element that fires does so at **100 %** of the others; every
|
||
element that does not is at 13–47 %, and `pteff02` never reaches alpha 255 at all,
|
||
so no quantifier could fire on it. There is no borderline case in this archive
|
||
for a stricter or looser reading to disagree about — which is the answer I could
|
||
not have gotten by re-checking the six screens where it already fired.
|
||
|
||
It also reproduces the Decoder's **second** control number exactly: they report
|
||
`pteff00.prm` forced below **3 of 23**, and `title` measures 3 of 23 here. With
|
||
their opaque-instant count of 2 already matched, two of their three published
|
||
figures for that control now come out of an independent implementation
|
||
unchanged. The third — `palogo_eff0`'s 211 against our 256 — remains the span
|
||
disagreement filed in `BLOCKED.md`, and note that the 211 is the number our
|
||
`developer_logos` row *does* reproduce. That is worth saying plainly: **the
|
||
disagreement is not a constant offset**, so "they stop 45 instants early" is not
|
||
the explanation, and whatever it is differs per screen.
|
||
|
||
⚠️ What this does **not** test: whether the rule is right about the 50 elements it
|
||
calls constrained-but-not-forced, and whether alpha-over is the blend mode. Both
|
||
are the Decoder's to settle. It tests completeness within one archive, which is
|
||
the half I can reach.
|
||
|
||
## The 256/211 was never a disagreement — and my own census had already said so
|
||
|
||
The Decoder answered the span question, and the answer is that **both numbers are
|
||
right**. `palogo_eff0.prm` appears on *both* splashes: the publisher pair
|
||
(entries 10, 13) runs to t=255 and gives **256** instants, the developer pair
|
||
(11, 14) runs to t=210 and gives **211**. I computed the publisher; their page
|
||
quoted the developer.
|
||
|
||
Worth noting how that came out, because it is the one part I can claim: the
|
||
refutation census in the section above reported **256 on `publisher_logo` and 211
|
||
on `developer_logos`, in the same table**, and concluded *"the disagreement is not
|
||
a constant offset, so 'they stop 45 instants early' is not the explanation"*. The
|
||
resolution was already sitting in my own output before their reply arrived. What
|
||
I got wrong was **filing it as a disagreement at all** — I compared one of my two
|
||
numbers against one of theirs and did not check the other row of my own table
|
||
against it. A per-screen quantity needs the screen named next to it, and my
|
||
`BLOCKED.md` row named neither.
|
||
|
||
### The span convention, confirmed rather than assumed
|
||
|
||
> The span is `0 ..= max keyframe time over EVERY element in the build`, and an
|
||
> element **holds its final pose** past its own last keyframe.
|
||
|
||
That is exactly what `forced_backdrop_first` implements, so nothing changes in
|
||
the port. Two things they add that are worth having in writing:
|
||
|
||
* the hold **is not a convenience**: a group holds at its last keyframe rather
|
||
than looping, and the header's `+0x08` never falls short of the last keyframe —
|
||
the slack *is* that hold;
|
||
* `+0x08` and the elements' maximum are **interchangeable**, zero disagreements
|
||
disc-wide. We use the elements' maximum. It stays, and this paragraph is the
|
||
note that the two were *checked* equivalent rather than assumed so.
|
||
|
||
### The hold decides 55 % of verdicts, and the oracle picks it
|
||
|
||
Reported by them over 130 keyless full-screen primitives (their measurement, not
|
||
reproduced here — their page is `docs/re/structures/ui-forced-backdrop.md`, ⚠️ not
|
||
yet on `main` as of this commit):
|
||
|
||
| alternative convention | verdicts changed |
|
||
|---|---|
|
||
| span = the header's declared `+0x08` | 0 |
|
||
| span = the primitive's own last keyframe | **72** |
|
||
| elements **gone** after their last keyframe | **72** |
|
||
|
||
So the reading I very nearly shipped — span = the element's own last keyframe —
|
||
would have changed **55 %** of the verdicts disc-wide. My first implementation
|
||
did exactly that, and `palogo_eff0` is the case that catches it: a *single*
|
||
keyframe at t=0, opaque for one instant, nothing else up yet, so the rule calls
|
||
it **free** — against a game measured painting it first. The convention is not a
|
||
matter of taste; the oracle rules one out.
|
||
|
||
✅ None of our six verdicts rests on it. `pgloading_eff00` is first under all four
|
||
conventions and `pteff00` is free under all four; only `palogo_eff0` moves, and
|
||
only under the convention its own measured order excludes.
|
||
|
||
### The sharper form of the `verify-screen` failure
|
||
|
||
Theirs, and it is better than how I wrote it: those two solid-black frames
|
||
**were not two witnesses**. Both renderers read `implied_layer_key`, so their
|
||
agreement carried *no information* — a correlated failure is indistinguishable
|
||
from a confirmation. What caught it was not a second opinion but that the agreed
|
||
answer was **impossible on its face**. *"Is this result even possible?"* beats
|
||
*"do two implementations agree?"* whenever the two share an ancestor, and in this
|
||
project they nearly always do.
|
||
|
||
### The boot gate still holds
|
||
|
||
Re-run after the reorder, since the rule changed the paint order of the first two
|
||
screens in the boot path: `publisher_logo` → `developer_logos` → `ADV` (skipped at
|
||
8.12 s) → `title` + `press_start` overlay, plate at full alpha at t=236, complete
|
||
at 10.83 s holding on the title. No script errors.
|
||
|
||
## The clock freezes at settle — the port's settle window, seen from the other side
|
||
|
||
The Decoder measured `GP_TITLE` build 4 in the draw stream and found the
|
||
top-level clock **advances through the build-in, stops inside the settle window,
|
||
and holds**. The exit ramp is not on a timer; it plays when something makes the
|
||
screen leave.
|
||
|
||
**Their interval is `[160, 236]`. The exporter computes `title`'s settle window as
|
||
`[160, 236, 198]`.** Those are the same two numbers, and they were not obtained
|
||
the same way: mine is the longest keyframe-free interval over top-level elements,
|
||
read out of the file with no game running; theirs is where a captured clock stops
|
||
advancing. A heuristic I adopted because it collapsed three pixel residuals at
|
||
once turns out to name the exact interval the game holds in. That is the first
|
||
evidence for the settle instant that does not come from the port's own renderer.
|
||
|
||
The file agrees from a third direction: `ptcopyright` sits at alpha 255 from
|
||
t=160 to t=238 — it reaches full opacity precisely as the window opens.
|
||
|
||
### Refutation attempt — their declared spans, checked against the file
|
||
|
||
Their draw-stream argument cites what the file *declares*, which I can check
|
||
without a capture:
|
||
|
||
| their claim | the file | |
|
||
|---|---|---|
|
||
| `ptlogo1` declares an exit at **t=264** | last keyframe t=264, alpha 0 | ✅ exact |
|
||
| `ptcopyright` alpha ≥ 1 for **106 units**, t=138…244 | keyframes at t=138 and t=244, alpha 0 at both | ✅ endpoints exact |
|
||
|
||
Both survive. One quibble, and it is only that: the instants with alpha ≥ 1 number
|
||
**105**, t=139…243, not 106 — the endpoints they bracket with are themselves
|
||
alpha 0, so 106 is the keyframe span rather than the visible one. It changes
|
||
nothing in their argument, whose force comes from 1 050 frames against either
|
||
number.
|
||
|
||
### What this costs the port, and it is not nothing
|
||
|
||
`authored/timing.json` and `authored/flow.json` both said *"a screen's dwell is
|
||
its OWN keyframe group"* and *"the pacing is the disc's own"*. 🔴 **That is
|
||
refuted.** Build 4 declares about 120 presented frames and dwelled ~1 100 — nine
|
||
times its own timeline. The group is not the dwell.
|
||
|
||
The **decision** is unchanged and still right: hold zero extra rather than invent
|
||
a number. What was wrong is what the port claimed for it. Leaving when the group
|
||
ends is not reproducing the disc's pacing — it is leaving at the moment the game
|
||
starts waiting. Both files now say so, and so does `boot.gd`, where the comment
|
||
had inherited the same claim.
|
||
|
||
✅ The *structure* was already right, and this is the part the measurement
|
||
confirms: `_advance` is caused by the next screen arriving, never scheduled off a
|
||
timer, and the port's own comment already read *"a screen plays itself out because
|
||
something is taking its place"*. `exit_ramp_units` was deleted for an unrelated
|
||
reason and its absence is now doubly justified.
|
||
|
||
⚠️ **Only build 4 is measured, and build 4 is the one screen where the port is
|
||
unaffected** — it is the boot's end state and holds indefinitely. The two screens
|
||
this actually governs, `publisher_logo` and `developer_logos`, have no measured
|
||
dwell at all. The port's boot is **known too fast on both, by an unmeasured
|
||
amount**. Nothing here is a number for them.
|
||
|
||
## 🔴 Withdrawn — "the boot is known too fast". The splash dwells are declared, and the port was already playing them
|
||
|
||
Last iteration I took the Decoder's build-4 measurement — declared ~120 presented
|
||
frames, dwelled ~1 100 — and wrote into three files that the port's boot is
|
||
**"known too fast on both splashes, by an unmeasured amount"**. That is
|
||
**withdrawn**. It was wrong, and the way it was wrong is the interesting part.
|
||
|
||
They then measured the splashes directly, over 3 cold boots:
|
||
|
||
| splash | declared | at 60 units/s | corpus wall clock |
|
||
|---|---|---|---|
|
||
| publisher (entries 10, 13) | t = 0…**255** | 4.250 s | 4.30 / 4.60 / 4.37 |
|
||
| developer (entries 11, 14) | t = 0…**210** | 3.500 s | 3.51 / 3.50 / 3.37 |
|
||
|
||
The developer agrees to **1.1 %**, two of its three runs to 0.3 %. And the port
|
||
emits **4.400 s and 3.650 s** — each declared value plus the 9-unit black hold,
|
||
exactly. ✅ **The pacing was right the whole time and no code changes.**
|
||
|
||
### What I actually did wrong
|
||
|
||
Not the arithmetic — the generalisation. Build 4 is the **title**, whose exit is
|
||
caused by something outside its timeline, so it holds. A splash's exit is caused
|
||
by nothing, so it plays its declared timeline and leaves. **The title is the
|
||
exception, not the rule.** I had one screen, it was the one screen in the boot the
|
||
port is structurally unaffected by, and I used it to overturn the two it governs.
|
||
|
||
I wrote at the time that a ratio from one screen is not a unit of pacing, and
|
||
declined to scale the splashes by nine. That refusal was right and is the only
|
||
reason this cost nothing but documentation. But refusing to apply the number
|
||
while adopting the *conclusion* it implied was half a caution: I still let one
|
||
screen's behaviour rewrite what the port claimed about two others. **The correct
|
||
move was to file build 4 as measured and leave the splashes alone**, which is
|
||
what the file now says.
|
||
|
||
### And the unit stays units
|
||
|
||
🔴 The Decoder's own container timed those same two dwells **15–20 % longer** than
|
||
both the declared values and the corpus — same disc, same declared timeline — and
|
||
three independent readings of that container's frame rate disagree with each
|
||
other. A seconds figure is one emulator's pacing on one run. The declared units
|
||
are on the disc. `authored/flow.json` `dwell` therefore takes **units**, and only
|
||
for a screen measured to wait beyond its group.
|
||
|
||
This also retires the "two timestamps would settle it" ask I filed: timestamps
|
||
were the wrong thing to author, and the measurement's own result says so.
|
||
|
||
## Refutation attempt — their two splash boundaries are not anchored the same way
|
||
|
||
Their draw stream reports *"publisher wordmark frames 6–119"* and *"developer
|
||
glows 123, wordmarks 140–209"*. Taking those spans against the declared groups:
|
||
|
||
| splash | declared units | their frames | units / frame |
|
||
|---|---|---|---|
|
||
| publisher | 255 | 6…119 = 114 | **2.237** |
|
||
| developer | 210 | 123…209 = 87 | **2.414** |
|
||
|
||
**Within one continuous boot, on one guest, those should be the same number.**
|
||
They differ by **7.9 %**, and the discrepancy runs the same direction as the
|
||
error split they have open — publisher +4.1 % where the developer is 1.1 %.
|
||
|
||
The file suggests why, and it is not the guest's clock: **the two boundaries are
|
||
anchored on different elements.** The developer span starts at its *glows*; the
|
||
publisher span is reported as starting at its *wordmark*. Those are 15 units
|
||
apart — every wordmark on both splashes is `alpha > 0` from t=16, every glow from
|
||
t=1 — and the publisher **has a glow**, `palogo_sqex_eff`, visible t=1…44,
|
||
structurally identical to the developer's three. So either frame 6 is the
|
||
publisher's glow and is mislabelled, or the publisher span genuinely starts 15
|
||
units later than the developer's and the two are not comparable quantities.
|
||
|
||
⚠️ I cannot tell which from here — it is their log, and the answer is one grep for
|
||
`palogo_sqex_eff` in it. Recording it because a 7.9 % internal inconsistency and
|
||
an unexplained 4.1 % error on the same screen are more likely one problem than
|
||
two. ⚠️ It also does **not** touch the corpus comparison, which is a separate
|
||
instrument (3 cold boots, not this draw log); their 4.1 % may still be real.
|
||
|
||
## Their corrected boundaries check out against the file — all six, exactly
|
||
|
||
The Decoder found the cause of the 7.9 % I reported, and it was worse than the
|
||
anchor mismatch I proposed: **the developer splash batches six quads into one
|
||
draw and their log dumps only the first two.** While the three glows are alive
|
||
they occupy that prefix, so the three wordmarks are invisible to the log until
|
||
the glows stop at t=45. *"Developer wordmarks first drawn at frame 140"* was the
|
||
logging prefix shifting, not the game. The anchor difference I found was a
|
||
symptom; the truncation was the cause. It is also what hid `palogo_anima`.
|
||
|
||
Their fix is to count `indices / 4`, which the 8-vertex dump cap cannot touch.
|
||
Every one of the six resulting calibration points matches this export:
|
||
|
||
| splash | their transition | their t | export |
|
||
|---|---|---|---|
|
||
| publisher | 1→2 quads | 15 | `palogo_sqex` joins at **t=16** |
|
||
| publisher | 2→1 | 45 | `palogo_sqex_eff` ends **t=44** |
|
||
| publisher | last drawn | 255 | group ends **t=255** ✅ |
|
||
| developer | 3→6 quads | 15 | three wordmarks join at **t=16** |
|
||
| developer | 6→3 | 45 | three glows end **t=44** |
|
||
| developer | last drawn | 210 | group ends **t=210** ✅ |
|
||
|
||
The two 15-vs-16 rows are a half-open boundary, not a disagreement: they name the
|
||
last frame at the old count, the export names the first instant at the new one.
|
||
|
||
✅ A second thing falls out that neither of us was looking for: their quad counts
|
||
are **1 and 2** on the publisher against **3 and 6** on the developer, and a
|
||
count restricted to *sprite-bearing* elements reproduces exactly that. So
|
||
`palogo_eff0` — the layerless forced backdrop — is **not in the batch they log**,
|
||
confirmed from the file. Their instrument and this export agree on which element
|
||
is the odd one out, having disagreed about it in every earlier iteration.
|
||
|
||
### Refutation attempt — does the drift actually explain the corpus's 4.1 %?
|
||
|
||
Their four segment rates recompute exactly (1.765 / 2.165 / 2.308 / 2.357; the
|
||
developer's two agreeing to 2.1 %, the run rising 33.5 %). The explanation is that
|
||
the publisher runs in the first seconds where the rate is furthest from its later
|
||
value. Testing what that predicts for the *corpus*:
|
||
|
||
| publisher ÷ developer | ratio |
|
||
|---|---|
|
||
| declared (255 ÷ 210) | 1.214 |
|
||
| **their drift predicts** | **1.369** |
|
||
| corpus, 3 cold boots | **1.278** |
|
||
|
||
**Sign confirmed, magnitude not.** The corpus ratio does sit above declared, which
|
||
is what the drift predicts and is real evidence. But their container's drift
|
||
would inflate it by 12.8 % where the corpus shows 5.3 % — roughly 2.4× too
|
||
strong. So drift of *some* size is doing the work; drift of *their* size is not.
|
||
|
||
⚠️ And the reason to be careful here is that the move is the one I just got wrong:
|
||
the 4.1 % is a property of the **corpus**, a different instrument (3 cold boots,
|
||
elsewhere), and the drift was measured in **their container**. Transferring it is
|
||
exactly what I did carrying build 4 onto the splashes. A general warm-up is
|
||
plausible for any emulator, so this is not baseless — but it is unconfirmed for
|
||
the corpus, and the magnitude gap is the evidence that the corpus's drift is not
|
||
theirs. It cannot be closed without the corpus's own frame log.
|
||
|
||
✅ Untouched by any of it: the declared **255** and **210**, and the port's
|
||
4.400 s / 3.650 s. Neither uses their draw log.
|
||
|
||
### The guard this puts on `keyframe_units_per_second`
|
||
|
||
⚠️ *"No single units-per-frame figure describes a run"* is a statement about
|
||
**emulator presentation pacing**, and a later reader could easily take it as
|
||
grounds to revisit the port's `60`. It is not. 60 is the **game's logical unit
|
||
rate** (HANDOFF Q1, measured), the port renders at its own frame rate and
|
||
converts through it, and guest pacing cannot reach it. `authored/timing.json` now
|
||
says so at the constant itself, where someone about to change it will read it.
|
||
|
||
## The n=1 disclosure, and the one port constant that rests on a single run
|
||
|
||
The Decoder disclosed that their `ARM=early` capture silently loses its trigger
|
||
**~40 % of the time** — two of five runs logged `ARMED EARLY` and produced no
|
||
draw log at all, indistinguishable in the session log from a run that armed. So
|
||
every draw-stream figure of theirs is **n = 1**.
|
||
|
||
That is worth more than the number it was attached to, because the port authors
|
||
constants from those runs. Auditing which: exactly one,
|
||
`black_hold_units = 9`. Everything else comes from the disc (the declared dwells
|
||
255/210, the settle window, the plate period), from the exporter, or from
|
||
multi-sample measurements (Q1's unit rate over seven frames, Q5's navigation).
|
||
|
||
**9 is not wrong, and three of its supports have moved.** Its conversion used a
|
||
105-frame count their own truncation fix has revised to 114; its second
|
||
corroboration (2.231) is the figure behind their retracted plate period; and a
|
||
run-average units-per-frame is the wrong shape for a 3–4 frame event now that the
|
||
rate is known to rise 33 % across a boot. Redone on their corrected local
|
||
segments, their two runs give **8.95** and **6.71** units.
|
||
|
||
⚠️ Those two were reconciled as replicating "within the ±1 both are quantised
|
||
to". **Overlapping error bars are not agreeing central values** — one frame is a
|
||
third of this quantity. The range is ~6.5–9.2 and the port sits at the top of it.
|
||
|
||
The value stays. Changing it would be my arithmetic on their instrument, and this
|
||
port does not author a number the corpus has not given; it is filed as a proposal
|
||
in `BLOCKED.md` with the one run that would settle it. ✅ And what is not in doubt
|
||
is that the hold is **real**: until this was implemented the port had no black
|
||
frame at all where the oracle measures a plateau.
|
||
|
||
### Their statistics, checked
|
||
|
||
Their per-boot excesses reproduce exactly: **+0.89 %, +8.24 %, +6.79 %**, spread
|
||
7.35 pp, wider than the 5.30 pp gap under test — so boot 1 alone essentially *is*
|
||
the declared ratio. Their concession is right and my 2.4× cannot carry the weight
|
||
I gave it.
|
||
|
||
One refinement, which cuts **against** their concession rather than for it: their
|
||
2.3 σ uses the *population* SD (3.178). At n = 3 the sample SD (3.892) is the
|
||
right estimator, giving **1.89 σ** — their run is *less* of an outlier than they
|
||
credited themselves with. Testing the other question, whether the corpus mean
|
||
differs from their prediction, gives t = 3.27 on 2 df, p ≈ 0.08. **Neither
|
||
framing reaches significance**, which is where both of us landed anyway.
|
||
|
||
📌 Their sharpening of the instrument point is the keeper: a truncated log and a
|
||
`--screen=NAME` render at t=0 both return a **complete, well-formed answer to a
|
||
different question**. That is why neither looks like an error — nothing inside
|
||
either view can tell you it is not the view you asked for.
|
||
|
||
## P6 gate — sound on the P5 walk, verified, and the tool I nearly shipped instead
|
||
|
||
`tools/port/verify-menu-audio`. Until now the evidence that P6's gate was met was
|
||
that `audio.play("move")` appears in `boot.gd` — evidence that a *call is
|
||
written*, not that a sound reaches the bus. Those differ, and this project has
|
||
the case to prove it: the black hold was implemented, called, and emitted nothing
|
||
for five milestones.
|
||
|
||
It needs no sound card. Godot records the Master bus to a WAV under the Dummy
|
||
driver. Three runs on `main_menu`: the walk (`down,down,accept,cancel,up`) and
|
||
two controls — `wait`×5 for the bed alone, and `left`×5 for **presses that reach
|
||
`_unhandled_input` and are bound to nothing** (Q5: left/right do nothing).
|
||
|
||
| check | result |
|
||
|---|---|
|
||
| a dead press is silent | **bit-identical** to the bed alone, 114 688 samples |
|
||
| `move` on the bus | r=0.201 at 1.85 s, bed-only 0.016, margin **+0.185** |
|
||
| `confirm` | r=0.945 at 2.14 s, bed-only 0.371, margin **+0.574** |
|
||
| `back` | r=0.660 at 2.42 s, bed-only 0.195, margin **+0.465** |
|
||
| cue order vs script order | **consistent** |
|
||
|
||
The order is the strongest line and it is free: the correlator is never told
|
||
where to look, so three different templates landing in script order is three
|
||
independent searches agreeing with the log. `move`'s absolute r is low because it
|
||
is the quietest cue under the loudest part of the bed; the margin over the same
|
||
template against the bed is what carries it.
|
||
|
||
🔴 **What it cannot conclude, and must never be read as:** that these are the cues
|
||
the *game* plays. That binding is HANDOFF Q8, the Decoder's, and nothing here
|
||
re-measures it. This tool cannot tell a correct cue from a confidently wrong one.
|
||
|
||
### The instrument I nearly shipped
|
||
|
||
The first version counted envelope bursts above a multiple of the bed level. It
|
||
reported **4 cues on one run and 0 on the next, from the same script** — its
|
||
answer was set by two hand-picked constants, the multiple and a minimum run
|
||
length, and the bed level is not constant across a run. I caught it only because
|
||
I ran it twice.
|
||
|
||
The replacement has no such constant: **the exported cue file is its own
|
||
template**, the search covers the whole recording, and the verdict is a margin
|
||
over that same template matched against the bed-only control.
|
||
|
||
⚠️ Cue *length* is deliberately not asserted. The audible part of a cue is far
|
||
shorter than its wave — 0.12–0.38 s against authored 0.344–1.016 s — because the
|
||
bed masks the tail. "Elevated for 0.13 s" is a fact about the bed, not the cue,
|
||
and I came close to filing that gap as a defect.
|
||
|
||
### A check that could not be made independent
|
||
|
||
I tried to verify Q8's cue durations against the exported waves. They agree
|
||
exactly — 0.533 / 1.016 / 0.344 — **and the agreement is worthless**: the
|
||
exporter decodes from Q8's own offsets and packet counts, so the duration is
|
||
determined by the claim under test. It confirms the export is faithful to the
|
||
authored table, nothing more. Recording it because "I checked and it matched" is
|
||
exactly what a correlated instrument feels like from the inside.
|
||
|
||
## Their `.tbm` self-refutation does not reach this archive — and it fixes my guard anyway
|
||
|
||
The Decoder downgraded 38 of the forced-backdrop rule's 80 verdicts from decoded
|
||
to inferred: those elements are `.tbm`, declaring fade `ffffffff`, and a solid
|
||
white quad painted first at alpha 255 would make the screen white. No screen is
|
||
white, so a `.tbm`'s white is a modulation **on a texture** and its element alpha
|
||
says nothing about coverage — the `.t32` mistake one file extension further out.
|
||
|
||
✅ **No verdict the port ships is affected.** All six forced elements here are
|
||
`palogo_eff0.prm` and `pgloading_eff00.prm`, `role: primitive`, `kind_raw 0x10`,
|
||
fade `0xff000000`. They fall in the 42 that stay decoded. And **no layerless
|
||
full-screen element anywhere in `GP_TITLE` has a non-black fade** — checked
|
||
across all 16 screens, so the downgrade cannot touch this archive.
|
||
|
||
But their diagnosis applies to my code regardless: *an element's alpha is not its
|
||
texture's opacity, and only an untextured primitive makes the two the same fact*.
|
||
My guard was `sprite.is_none()` — a **symptom** test, the same shape as the one
|
||
they say fixed their symptom and not their cause. It would keep admitting a
|
||
`.tbm` that this exporter happens not to emit a sprite for.
|
||
|
||
The guard is now the positive test, `role == "primitive"`. It changes no verdict
|
||
today — the six are identical before and after, 16 screens still validate — and
|
||
it is correct by construction if the corpus grows.
|
||
|
||
⚠️ Not adopted from their message: their reading that the blend question now
|
||
narrows to `pbafc.prm`. That is theirs to settle and the port draws no additive
|
||
quad either way.
|
||
|
||
## Coverage is now tested per instant, because scale animates
|
||
|
||
The Decoder found that `forced_backdrop` judged screen coverage from the declared
|
||
size alone, ignoring scale — and the disc carries its own counterexample.
|
||
`pbafc.prm` declares **844×600 at alpha `ff`**, which reads as a screen-filling
|
||
cyan wash; it is scaled **2 % × 3 %** and draws about **17×18 px**, strobing and
|
||
travelling x=178→291. A moving glint. A rule reading its declared size would call
|
||
it screen-covering.
|
||
|
||
The port had the same gap and it is closed. `scale_at` interpolates scale on the
|
||
same linear ramp as the fade, and coverage is folded **into the opaque-instant
|
||
test** rather than checked once: an instant counts only where the element is both
|
||
alpha 255 *and* covering. That is the rule's own wording — "covers the screen
|
||
**and** is fully opaque **at some instant**" — where the previous code tested the
|
||
two halves at different times.
|
||
|
||
The static size prefilter is now deliberately *not* a rejection: an element scaled
|
||
**above** 100 could cover the screen from a smaller declared size, so rejecting on
|
||
declared size would have replaced one version of the bug with its mirror.
|
||
|
||
✅ **No verdict moves.** Six forced elements before and after; 16 screens validate;
|
||
the oracle figures are identical to the digit (`publisher_logo` 0.01 %,
|
||
`developer_logos` 0.01 %, `main_menu` 0.07 %, `extras` 0.19 %, `title` 0.26 %).
|
||
Their claim that all 80 forced instances sit at scale 100 reproduces on the
|
||
GP_TITLE subset, and more strongly: **no layerless full-screen element anywhere in
|
||
this archive has a non-100 scale on any keyframe.**
|
||
|
||
It is in for the reason they gave, which is the right one: the data that would
|
||
break it demonstrably exists on this disc. That is a better argument than a
|
||
failure would have been, because it does not require the bug to happen first.
|
||
|
||
### Their blend-robustness argument, checked
|
||
|
||
They classify the blend mode **undecodable with reach** and argue the rule does
|
||
not depend on it, for a black quad:
|
||
|
||
| | drawn **first** | drawn **last** |
|
||
|---|---|---|
|
||
| alpha-over, α=255 | correct | blanks the screen |
|
||
| additive, α=255 | correct — adds nothing | correct |
|
||
|
||
The table holds. An additive black quad contributes nothing at any position, so
|
||
both orders are correct under it; only alpha-over distinguishes them, and it
|
||
picks *first*. **"First" is right under both hypotheses, "last" under one** — so
|
||
`forced_backdrop`'s verdict is robust to a question neither of us can close.
|
||
|
||
It also explains a detail of the original bug that I had not accounted for:
|
||
"layerless sorts last" was *wrong* under alpha-over and merely *pointless* under
|
||
additive, which is why those screens came out **solid black** rather than
|
||
**empty**. The symptom was diagnostic of the blend mode all along.
|
||
|
||
⚠️ Not evidence that the blend is alpha-over, and I am not recording it as such.
|
||
It is the reason the port can stop waiting on it. `pbafc.prm` remains the sole
|
||
additive candidate and is outside the rule at 17×18 px; the port draws no
|
||
additive quad either way.
|
||
|
||
## P7 gate — the new-game intro plays and returns, and a defect I nearly invented
|
||
|
||
`--menu=main_menu --script=accept` walks the P7 path: Ⓐ on NEW GAME announces the
|
||
two measured screens this export skips, opens `S00A`, plays it to its natural end
|
||
at **93.33 s** against the manifest's 93.9, and returns to the title. Nine film
|
||
frames across the movie are distinct and non-black (mean 10 → 140). The gate asks
|
||
for "plays, then returns to a defined state"; that is both halves.
|
||
|
||
### The near-miss, which is the part worth keeping
|
||
|
||
Checking that the movie's audio actually reached the Master bus, I correlated the
|
||
recording against the exported `S00A.ogv` audio and got **r = −0.0068 at the
|
||
movie's known start**, with the correlator passing its own positive control at
|
||
**r = 1.0000**. A working instrument returning zero at the right place. I was one
|
||
step from filing *"the movie's own audio never reaches the bus"* — which would
|
||
have been a serious P4/P7 defect, and the inverse of the human's original report
|
||
that the intro "plays music but no voice".
|
||
|
||
It is false. Re-run on the **boot** path, where no menu bed is playing:
|
||
|
||
| against the bed-free recording | r | at |
|
||
|---|---|---|
|
||
| `ADV` voice | **0.8855** | movie start 7.6 s |
|
||
| `ADV` movie audio | **0.4178** | movie start 7.6 s |
|
||
|
||
Both present, at the same start. The movie's audio is simply quieter than the
|
||
voice mixed on top of it, and in the P7 run the **menu bed masked it below
|
||
detection** while the louder voice survived. The bed carries across into the
|
||
movie — `play_bed` is documented as carrying across submenus — so the P7 run was
|
||
never a clean measurement.
|
||
|
||
🔴 **The lesson is about the control, not the bed.** I validated the correlator on
|
||
clean data and then ran it on masked data. A positive control proves the
|
||
instrument works *on the material it was given*; it says nothing about whether
|
||
the instrument can see through an interferer that was not in the control. **A
|
||
negative result needs its own positive control under the same masking**, and this
|
||
is the third time in this corpus that an unvalidated negative nearly became a
|
||
finding — after "10 of 18 elements transparent at rest" and the burst counter that
|
||
read 4 cues on one run and 0 on the next.
|
||
|
||
`tools/port/verify-video-audio`'s header already warned that a fidelity
|
||
comparison needs cross-correlation alignment *and* an agreed downmix. It was
|
||
right, and I would add the third condition it did not know to state.
|
||
|
||
⚠️ What is **not** settled: whether the bed *should* carry into the movie and on
|
||
to the title after it. It does, it is authored that way, and nobody has watched
|
||
the game do either. That is a separate question from this one and is not filed as
|
||
a defect — only as unmeasured.
|
||
|
||
✅ Also checked, from their `compose` finding: `GP_TITLE` declares **115 `.t32`,
|
||
45 `.rat`, 18 `.prm` and no `.tbm` at all**, and every non-primitive element has a
|
||
resolvable sprite. The "draws no pixels for an unresolvable element" hazard —
|
||
the shape that hid `pteff05` from both renderers — does not reach this archive.
|
||
|
||
## `ScreenView.skipped` was correct and unread since P1 — now it says so itself
|
||
|
||
The draw loop has always tracked what it could not draw, with the comment *"a
|
||
silently missing element looks like art"*. **Nothing ever printed it.** For eight
|
||
milestones the port could drop an element on every frame and report it to nobody.
|
||
|
||
That is the same shape as the black hold — implemented, called, emitting nothing
|
||
until somebody filmed it — and as `verify-screen` scoring two blank frames `OK`.
|
||
A fact that needs someone to remember to look at it is a fact that goes
|
||
unnoticed, so `_note_structural` **prints from inside `ScreenView`** rather than
|
||
returning a value for a caller to surface. Routing it through a caller is exactly
|
||
what did not happen.
|
||
|
||
Only **structural** skips are reported — `(no sprite in the export)` and
|
||
`(sprite failed to load)`. `(transparent at rest)` is ordinary animation, true of
|
||
every element at some instant, and reporting it would bury the two that mean
|
||
something under the one that never does.
|
||
|
||
✅ **Nothing is being skipped today**: 0 across every screen, on the boot path and
|
||
per-screen. This is a guard, not a fix. The export corroborates it — no missing
|
||
sprite PNG, no element at alpha 0 on every keyframe, none at scale 0.
|
||
|
||
### 🔴 And the first version of that scan was a false pass
|
||
|
||
My first run reported *"0 structural skips"* on ten screens. **`screen_view.gd`
|
||
did not parse.** I had inserted a line at three tabs inside a four-tab block —
|
||
the Python `assert old in s` passed because a three-tab string is a *substring*
|
||
of a four-tab line — which orphaned a `continue`. Godot loaded nothing, printed
|
||
nothing, and `grep -c` faithfully counted zero.
|
||
|
||
A count of zero from a dead script is indistinguishable from a count of zero from
|
||
a clean one, and I had already written the sentence claiming the clean reading.
|
||
The scan now counts the screen summary line as a **positive control**: if the
|
||
script did not run, `summary-lines=0` says so, and the zero cannot be read as a
|
||
pass. That is the third time this session that a well-formed answer to a
|
||
different question nearly became a finding.
|
||
|
||
⚠️ Note the mechanism, because it will recur: matching indented code by substring
|
||
is unanchored, and it silently matched a *shallower* indent than the one in the
|
||
file.
|
||
|
||
## Refutation attempt — "the element declared first paints first"
|
||
|
||
All six of the port's forced-backdrop elements sit at **element index 0**. So on
|
||
those six screens the rule's verdict is indistinguishable from a far simpler
|
||
hypothesis I had not tested: *the first-declared element is painted first.* If
|
||
that held, `forced_backdrop` would be an elaborate way to reproduce the file's own
|
||
ordering, and my six verdicts would be no evidence for it at all.
|
||
|
||
**It is refuted, on 8 of 16 screens.** Index 0 is *not* painted first on
|
||
`build_00`/`build_01` (position 2), `extras` (7), `title` (13), `title_jp` (18)
|
||
and — decisively — `main_menu`, where index 0 is **`pteff00`, painted last**,
|
||
position 15 of 16. `pteff00` is the Decoder's own *measured* control: the game
|
||
puts the first-declared element on top of that screen.
|
||
|
||
So declaration order is not paint order, the six coincidences are coincidences,
|
||
and the rule is not redundant.
|
||
|
||
⚠️ What survives as a real limitation: **those six screens, taken alone, cannot
|
||
distinguish the two hypotheses.** The evidence separating them comes from
|
||
elsewhere in the archive. Worth stating because it is the exact weakness in the
|
||
Decoder's `pfbase.tbm` upgrade — *"element 0 of the save/load frame, and the
|
||
measured order starts [0, 1, 2, …]"*. An order that equals the trivial order is
|
||
weak evidence for **any** rule, since every rule preserving declaration order
|
||
agrees with it. ✅ Their claim survives, but on evidence they did not cite: it is
|
||
`main_menu`'s `pteff00` that rules out the trivial reading, not the save/load
|
||
frame itself.
|
||
|
||
## The menu bed plays under the cutscene, nobody decided that, and it stays
|
||
|
||
`MenuAudio.stop_bed()` exists and is **called from nowhere**. So the music that
|
||
starts when the main menu goes live runs through the cutscene and on past it —
|
||
and since `authored/audio.json` sets `loop: "restart"`, it then loops. Both
|
||
follow from the source and the authored data alone; no measurement is needed to
|
||
establish them.
|
||
|
||
The port therefore emits **two unrelated music tracks at once** during `S00A`:
|
||
the movie carries its own music and effects, and the menu bed is underneath it.
|
||
|
||
### It is not being fixed, and that is the decision
|
||
|
||
`PORT-MISSION`'s rule is to leave an unmeasured detail **plainly wrong rather
|
||
than plausibly invented**, and this is the textbook case for it. Music over a
|
||
cutscene is wrong in a way any listener catches in one second. Ducking or
|
||
stopping the bed would sound entirely right — and would be a guess about a game
|
||
nobody has watched. **The audible version gets fixed; the plausible version ships
|
||
forever.**
|
||
|
||
So `_play_video` now *announces* it, the way `skipped_chain` already announces
|
||
the two screens NEW GAME jumps over:
|
||
|
||
```
|
||
-> video S00A at 1.13 s (…)
|
||
🔴 the menu bed is STILL PLAYING under this movie -- unmeasured,
|
||
left audible on purpose (BLOCKED.md: does menu music duck?)
|
||
```
|
||
|
||
It fires on the menu path and correctly stays silent on the boot path, where no
|
||
bed has started. `stop_bed` is **kept**, not deleted: the day a capture says
|
||
whether the game's menu music ducks under a movie, it is the one line to change.
|
||
|
||
⚠️ This is the mirror of `ScreenView.skipped` from the previous iteration — a
|
||
fact recorded and never surfaced, against a capability provided and never used.
|
||
Both were invisible for the same reason: **nothing fails when they are missed.**
|
||
|
||
### 🔴 And my correlator is not trustworthy on music under music
|
||
|
||
Chasing this I ran the envelope correlator over the P7 recording repeatedly and
|
||
got answers that moved with the window and the template: the bed at r=0.42 with
|
||
one template and no peak at all above 0.4 with another; a post-movie window
|
||
search whose range **excluded the correct answer** and duly reported the bed's
|
||
own loop as unidentifiable. I was drafting *"46 s of unexplained audio after the
|
||
movie"* when the explanation was the authored `loop: "restart"` sitting in a file
|
||
I had not re-read.
|
||
|
||
The instrument is sound where it was validated — discrete SFX against a bed, with
|
||
margins of +0.5 over a negative control. It is **not** sound for music under
|
||
music at comparable level, where every candidate scores 0.15–0.42 and nothing
|
||
separates. ⚠️ A margin is only meaningful against a control **at the same SNR**,
|
||
and I did not have one here. That is the fourth near-miss of this kind, and the
|
||
first where I would have invented an *anomaly* rather than a defect.
|
||
|
||
Nothing in this section rests on that correlator. The finding is `stop_bed` having
|
||
no caller, which is a fact about the source.
|
||
|
||
## `wait:<seconds>`, and the bed's loop seam is 3.4 seconds of silence
|
||
|
||
The port could not be asked to **run for a stated duration**. A bare `wait`
|
||
script step is `pass` — it returns as soon as the screen settles — so nothing
|
||
happening after the settle point was observable from a harness at all. The music
|
||
bed made that concrete: an 87.7 s track whose restart nobody had watched, on a
|
||
harness whose longest menu run was under seven seconds.
|
||
|
||
`--script=wait:105` fixes that, and the first thing it found was the answer.
|
||
|
||
### The bed loops, exactly where it should
|
||
|
||
Recording the Master bus over 132 s with nothing but the menu playing — no movie,
|
||
no voice, a clean signal — the bed's `t=2…17 s` template matches twice:
|
||
|
||
| | r | pass begins |
|
||
|---|---|---|
|
||
| first pass | **0.947** | 0.0 s |
|
||
| second pass | **0.885** | **87.8 s** |
|
||
|
||
The track is **87.7 s**. So `loop: "restart"` does what `authored/audio.json`
|
||
says: replays from sample 0 at the track's end, no trimming, no loop point.
|
||
✅ First end-to-end observation of P6's looping behaviour.
|
||
|
||
### And the seam is measurably as bad as it was authored to be
|
||
|
||
`loop_why` predicted *"the listener hears the track's own fade-out and the silence
|
||
after it before the music comes back"*. Measured off the bus:
|
||
|
||
| window around the seam | RMS |
|
||
|---|---|
|
||
| −8 … −4 s | 2057 |
|
||
| −4 … −2 s | 714 |
|
||
| −2 … −0.3 s | 431 |
|
||
| +0.3 … +2 s | 2164 |
|
||
|
||
and **36 consecutive 50 ms windows below peak 300, from 84.40 s to 87.80 s** —
|
||
about **3.4 seconds of near-silence** before the music returns. That is long
|
||
enough to read as *the music stopped*, not as a loop.
|
||
|
||
The claim was right and is now a number. ⚠️ It does **not** license trimming to
|
||
the fade: that would still invent a loop point, and an invented one is
|
||
indistinguishable from a decoded one a month later. The measurement is recorded
|
||
to make the cost of the missing loop point concrete, in `authored/audio.json`.
|
||
|
||
### 🔴 My first `wait:` was wrong by 39 %, and the way it was wrong matters
|
||
|
||
It used `create_timer`, which counts down on the frame delta. In an **idle**
|
||
scene this container throttles and the delta it reports is not the time that
|
||
passed: a requested 30 s took **41.7 s** of wall clock while the port reported
|
||
30. Measured against `date` either side of the process, with a no-wait control
|
||
to subtract 1.21 s of startup.
|
||
|
||
Now polled on `Time.get_ticks_msec()`: 30 s requested, **31.38 s** wall, +4.6 %.
|
||
|
||
⚠️ **This is idle-specific and is not a general clock fault**, which I checked
|
||
before writing any of it down. Over a whole boot, where things are animating, the
|
||
port's clock tracks wall clock to **within 4 %** — 10.43 s wall against 10.82 s
|
||
reported. So the port's *animation* timing is sound and the earlier splash-dwell
|
||
agreement (4.400 s and 3.650 s against declared 255 and 210 units) **stands**; I
|
||
had briefly believed it did not.
|
||
|
||
What is genuinely unsound is `_elapsed` **while idle**: it reported 23.21 s across
|
||
30 real seconds of waiting. Every timing the port prints during animation is
|
||
fine; a timing that spans a wait is not.
|
||
|
||
📌 The reason to care: the only reason to hold a screen is to observe something on
|
||
a **real** clock — an audio loop, a timeout, an idle return. A timer that
|
||
silently ran 39 % long would have put every such observation at the wrong instant,
|
||
and the bed-loop result above would have been the first casualty. It survived only
|
||
because the seam was read off the **recording's** clock, which the bed's own known
|
||
length calibrates.
|
||
|
||
## Two harness bugs, and the defect the second one was hiding
|
||
|
||
### 1. `--capture` with `--script` photographed the frame *before* the script
|
||
|
||
`--capture` fired in `_ready` and quit. With `--script` that is **before the
|
||
first press**: at t=0.133 s, with 10 of 16 elements still transparent. Two runs
|
||
differing by two `down` presses came out **bit-identical**, and I read that as
|
||
*"runtime focus never changes"* — a confident wrong finding that `--shots`
|
||
contradicted within a minute.
|
||
|
||
Fixed: with a script, the capture defers to the end of the run, through the same
|
||
`_capture_to` member the boot path already used. Verified — the two runs now
|
||
differ at max 235, and the capture lands at t=82 units instead of t=8.
|
||
|
||
### 2. `--boot --capture=` wrote no file at all
|
||
|
||
`_finish_boot()` is reachable only from the overlay-quit branch, but the boot
|
||
quit first: line 412 exits when `_film == "" and _overlay_spec.is_empty()`, and
|
||
`_overlay_spec` is **cleared when the overlay is raised**. So a plain `--boot`
|
||
ended at 10.99 s, 1.2 s before its own scheduled 12.21 s, and the capture never
|
||
happened.
|
||
|
||
Confirmed pre-existing by stashing my changes and re-running. Fixed by also
|
||
requiring `_overlay_quit_at < 0.0`. The boot now runs to 12.19 s, prints *"boot
|
||
ends on title + press_start"*, and writes the file.
|
||
|
||
⚠️ The flag has a doc comment explaining that it exists so the boot has an
|
||
artifact of its own instead of a 600-PNG filmstrip. **It has been producing
|
||
nothing.** A flag that silently writes no file is the same failure as
|
||
`ScreenView.skipped` and `stop_bed`: provided, plausible, and never exercised.
|
||
|
||
### 3. 🔴 And the artifact it now produces shows the plate is missing
|
||
|
||
The boot's end frame is **bit-identical** to `--screen=title` at the same instant
|
||
— max difference 0. The `PRESS Ⓐ` plate is not in the port's end state.
|
||
|
||
`ptbtn00`'s own fade explains it exactly:
|
||
|
||
| t | 0 | 214 | **236** | **238** | 244 |
|
||
|---|---|---|---|---|---|
|
||
| alpha | 0 | 0 | **255** | **255** | 0 |
|
||
|
||
The plate is visible for **8 units — 0.133 s** — and the boot captures at
|
||
**t=246.54**, two and a half units after it has gone.
|
||
|
||
That is not an accident of frame timing; the code chose it. `_overlay_quit_at`
|
||
takes `max(view.settle_time(), overlay.settle_time())`, and its comment says why:
|
||
*"the plate arrives at t=238 and build 4 is still fading up from black until
|
||
t=261 … quitting when the plate lands photographs a title that has not finished
|
||
presenting."* Both halves are true, and together they mean **the two states
|
||
cannot both be in one frame.** The port picked the title, and the consequence —
|
||
that the plate is in no artifact at all — was never written down.
|
||
|
||
⚠️ I am **not** moving the trigger. The earlier reasoning is sound and the
|
||
measurement it protects is real; picking the other instant would trade a missing
|
||
plate for a visibly dark title, which is the swap that was already made once and
|
||
regretted. What settles it is what the *game* does with the plate after t=244 —
|
||
filed.
|
||
|
||
📌 Worth naming: defect 3 was **invisible while defect 2 existed**. A capture flag
|
||
that writes nothing cannot show you a missing element. The broken tool was hiding
|
||
the thing the tool was built to find, and neither was noticed because the absence
|
||
of a file looks exactly like not having run the command.
|
||
|
||
### What was *not* wrong, and how I nearly recorded that it was
|
||
|
||
Runtime focus works. Per-step `--shots` across `down,down` differ at max 232–233,
|
||
with the differing boxes tracking down the button column. My contrary reading came
|
||
from analysing 410 `f_NNN` files after asking for `--shots=…/s` — **the filenames
|
||
did not match the flag I passed, and that was visible in my first `ls`.** A
|
||
verbatim re-run produces six correctly-named per-step shots. I do not know where
|
||
those 410 files came from, which is itself the point: I drew a conclusion from a
|
||
file set whose provenance I never checked.
|
||
|
||
## The `PRESS Ⓐ` plate: four bugs in a row, and a number I have been misquoting
|
||
|
||
Last iteration I filed that the plate was visible for 8 units and simply missed by
|
||
the boot's capture instant. That was wrong in the direction that matters: **the
|
||
plate could not be drawn at any instant at all**, and three separate faults had to
|
||
be removed before it appeared. The fourth is a correction to figures I have quoted
|
||
to the Decoder repeatedly.
|
||
|
||
### 1. `--time=` was silently ignored on half the screens
|
||
|
||
`pose_at` did `if holding: t = settle_instant`. The requested instant was
|
||
**discarded** on every screen with a settle window ≥ 30 units — `title`,
|
||
`press_start`. The flag parsed, the log printed the time asked for, and the pose
|
||
came from somewhere else entirely.
|
||
|
||
`ScreenView.frozen` now marks an explicitly pinned instant and skips both clamps.
|
||
|
||
### 2. The settle window picked an interval where nothing is visible
|
||
|
||
`press_start`'s keyframes are 0, 214, 236, 238, 244. The widest keyframe-free gap
|
||
is **0…214** — the dead stretch *before* the plate exists, where `ptbtn00` is
|
||
alpha 0 throughout — so its settle instant was **t=107**, and every question about
|
||
that screen was answered there.
|
||
|
||
🔴 **A gap in which nothing is visible is not a settled state.** The exporter now
|
||
rejects those intervals. `press_start` becomes [214, 236] — 22 units, under the
|
||
runtime's 30-unit bar — so it falls back to each element's own hold, which is the
|
||
plate, opaque, as the disc declares it.
|
||
|
||
⚠️ It disturbs no window the settle instant was measured on: `title` keeps
|
||
[160, 236], the interval the Decoder's draw stream independently found the game's
|
||
clock freezing in.
|
||
|
||
### 3. An authored entry of mine was suppressing the decode
|
||
|
||
Even then the plate stayed dark, because `authored/timing.json`'s
|
||
`looping_focus_records` entry for `press_start/ptbtn00` made `_draw` take the
|
||
focus path — which draws the focus record **instead of** the base sprite:
|
||
|
||
| | with the entry | without |
|
||
|---|---|---|
|
||
| `press_start` t=236 | max **0** | max **252.5** |
|
||
| t=240 | max 0 | max 252.5 |
|
||
| t=250 | max 15.3 | max 252.5 |
|
||
|
||
I authored that entry to give the plate a glow. It substituted a dim glow at the
|
||
wrong phase for the element's own bright sprite, on the screen whose entire
|
||
content is that sprite. **Deleted** — an authored guess that overrides decoded
|
||
data with a worse answer is removed, not tuned. The glow is not claimed either
|
||
way; drawing both would be a rendering rule nobody has measured.
|
||
|
||
✅ The boot's end artifact now contains the plate: mean **95.7** in its region
|
||
against **33.6** for the title art alone, and the overlay reports `drew 1:
|
||
ptbtn00` where it reported `drew 0`.
|
||
|
||
### 4. 🔴 `verify-capture` has been measuring a different pose than it reported
|
||
|
||
It passed `--time=5.9617` for the title — t=357.7 units, the Decoder's refined
|
||
sweep fit — and **that value was never applied**, because of fault 1. Every title
|
||
figure this tool has printed, including the **0.26 %** I have quoted to the
|
||
Decoder more than once, was measured at the **settle instant, t=198**, under a
|
||
note claiming t=357.7.
|
||
|
||
Honouring the flag made it visible: t=357.7 is past the title's own group, which
|
||
ends at t=269, so the whole screen posed at its faded-out final keyframes and the
|
||
disagreement went to **30.97 %**. The instant was only ever meant for the `ptloop`
|
||
leaf, which runs to t=600 and is looped separately by `loop_leaf`. Applying it to
|
||
the whole screen was always wrong and was harmless only while it was ignored.
|
||
|
||
The splashes had the same shape: `--time=99` was an idiom for "settled" that
|
||
worked only because it was discarded. Both rows now pose by omission, and the
|
||
tool's note says what it does.
|
||
|
||
| | before | now |
|
||
|---|---|---|
|
||
| `title` | 0.26 % *(labelled t=357.7, actually t=198)* | **0.21 %** at t=198, labelled t=198 |
|
||
| `publisher_logo` | 0.01 % | 0.01 % |
|
||
| `developer_logos` | 0.01 % | 0.01 % |
|
||
| `main_menu` | 0.07 % | 0.08 % |
|
||
| `extras` | 0.19 % | 0.19 % |
|
||
|
||
The agreements were real; the **stated pose was not**. Corrected with the
|
||
Decoder, since they have those numbers.
|
||
|
||
### The flag audit that started it
|
||
|
||
All 16 flags `boot.gd` parses were exercised for an observable effect after last
|
||
iteration's two silent ones. `--pose=rest` (max 111 against the timeline),
|
||
`--play` (enters the menu), `--no-hold` (max 255 on two screens) all pass. `--time`
|
||
was the one that did not, and it took a screen whose content is a single late
|
||
spike to make the failure visible.
|
||
|
||
## The title's residual is the sweep phase, and the sweeps fit at ~400 units, not 357.7
|
||
|
||
Last iteration I found `verify-capture` was passing the Decoder's refined sweep
|
||
fit as `--time=5.9617` and having it silently discarded, so the value had **never
|
||
been tested**. Asking for it also destroyed the frame — t=357.7 is past the
|
||
title's own group end at t=269 — which is why nobody noticed.
|
||
|
||
`--leaf-time=<seconds>` separates the two clocks: the screen sits at its settled
|
||
pose, the `ptloop` leaf is placed at whatever phase is under test. That makes the
|
||
fit testable for the first time.
|
||
|
||
### Controls first
|
||
|
||
* The renderer is **deterministic** — three runs at one leaf phase are
|
||
bit-identical, max difference 0. So variation across phases is signal, not noise.
|
||
* The sweeps are **detectable** — two phases differ over **0.3953 %** of the frame
|
||
at a 10 % threshold. A comparison at this scale can see them.
|
||
|
||
### The fit
|
||
|
||
Sweeping the leaf across its full 600-unit span against
|
||
`live-title-build4-no-plate.png`, structural disagreement at a 25 % threshold:
|
||
|
||
| leaf phase | differing |
|
||
|---|---|
|
||
| 240 units | 0.1410 % |
|
||
| 320 units | 0.3395 % |
|
||
| **357.7 — the Decoder's fit** | **0.2532 %** |
|
||
| 390 | 0.0129 % |
|
||
| **395–402** | **0.0124 %** |
|
||
| 405 | 0.0127 % |
|
||
| 440 | 0.2033 % |
|
||
|
||
A sharp basin at **390–415 units**, an order of magnitude below everything
|
||
around it, and **20× better than t=357.7**.
|
||
|
||
### What that means, and what it does not
|
||
|
||
✅ **The title's 0.21 % residual is the sweep phase, not structure.** At the
|
||
fitted phase the disagreement falls to **0.0124 %** — the same order as the
|
||
splashes' 0.01 %. The port's title rendering is structurally right; the sweeps
|
||
were simply somewhere else in their loop.
|
||
|
||
🔴 **The port does not adopt 400 units, and `verify-capture` is not re-posed to
|
||
it.** That would be tuning until they match, which this repository's own tooling
|
||
header warns against. The port loops the leaf freely — there is no phase
|
||
parameter to set — and 400 units is a property of *that capture's instant*, not
|
||
of the game.
|
||
|
||
⚠️ Reach: this assumes the port's leaf geometry and sprite are otherwise correct.
|
||
A systematic error in how the sweeps are drawn could be absorbed by shifting the
|
||
phase, and one capture cannot separate those. What makes the result worth having
|
||
anyway is the **sharpness** — a 20× drop over 40 units is not something a
|
||
geometry error would produce at a wrong phase.
|
||
|
||
⚠️ And it does not tell the Decoder their 357.7 is wrong *as a measurement of
|
||
whatever they measured it on*. It says the phase that matches this capture is
|
||
~400. If those are the same quantity, one of the two is off by ~42 units; if they
|
||
are not, this is a second quantity nobody had.
|
||
|
||
## A second capture closes the sweep-geometry question, and the plate matches at 0.00093 %
|
||
|
||
Last iteration's leaf-phase fit came with a caveat I could not close: *a
|
||
systematic error in how the port draws the sweeps could be absorbed by shifting
|
||
the phase, and one capture cannot separate those.* A second capture can, and
|
||
`live-title-press-a.png` — the title **with** the plate — was sitting in the
|
||
corpus unused.
|
||
|
||
### The second capture fits at a different phase, and better
|
||
|
||
| capture | pose | differing |
|
||
|---|---|---|
|
||
| `live-title-build4-no-plate.png` | settled, leaf at ~400 units | **0.0124 %** |
|
||
| `live-title-press-a.png` | t=237, everything | **0.00093 %** |
|
||
|
||
**Two independent captures, two different sweep phases, both fitting to 0.01 % or
|
||
better.** A geometry error in how the sweeps are drawn would leave a floor in
|
||
*both*, and at a phase-independent level. Neither has one. ✅ The caveat is closed
|
||
and the port's sweep rendering is not systematically wrong.
|
||
|
||
The two phases are also consistent with each other rather than merely different:
|
||
the plate is opaque only at t=236–238, so a frame containing it is early
|
||
(t≈237) and a frame without it is either earlier or later. The no-plate capture
|
||
fits at leaf ~400 — 6.7 s in, well past the plate's window — and its filename
|
||
says it is build 4 alone. Both readings agree on where each frame sits.
|
||
|
||
⚠️ I nearly drew a further inference — that the no-plate capture *dates* the
|
||
plate's disappearance and therefore answers the BLOCKED question about whether
|
||
the plate stays up. It does not: the filename says `build4-no-plate`, so the
|
||
capture was taken **of build 4 alone, deliberately without the overlay**. It
|
||
carries no information about how long a plate lasts. That row stays open.
|
||
|
||
### Capture 1 is not a whole-screen instant, which corroborates `loop_leaf`
|
||
|
||
Sweeping the *whole screen's* time against the no-plate capture, the best is
|
||
**0.1483 % at t=230**, degrading sharply past 240 as the group fades out — an
|
||
order of magnitude worse than the leaf-only fit's 0.0124 % at phase ~400, which
|
||
the main timeline cannot reach without fading everything.
|
||
|
||
So that capture is **not** "the screen at instant t". It is the screen **settled**
|
||
with the sweeps **still looping** — which is exactly what `authored/rendering.json`'s
|
||
`loop_leaf_on_screens: ["title"]` models. That decision was authored from the
|
||
leaf's zero slack; this is the first independent evidence for it.
|
||
|
||
### 🔴 The 1 % floor was the plate not being drawn at all
|
||
|
||
Before any of that, every sweep phase against capture 2 gave a flat ~1.0 %, with
|
||
the residual a row of glyph-sized blobs on the plate's own position.
|
||
|
||
`--screen=X --overlay=Y` pushed the **raw elapsed clock** into the overlay — 9
|
||
units at the moment `--capture` fires. `press_start` is transparent until t=214.
|
||
So the one flag whose entire purpose is *put the plate on the title* drew nothing
|
||
and reported `drew 0`, and the frame read as a title with no plate.
|
||
|
||
A static overlay now poses at **its own arrival**. The `--boot` path is untouched:
|
||
there the shared clock is the finding — the 120 units between build 4's last ramp
|
||
and the plate's `a=255` is a fixed interval on one timeline.
|
||
|
||
⚠️ My first patch for this was wrong and I nearly committed the comment for it:
|
||
I wrote that *"nothing outside a boot sequence drives the overlay's clock"*. It is
|
||
driven — from `view.time_units`, every frame. The symptom was identical either
|
||
way, and only re-reading the log after the fix failed showed the cause was the
|
||
opposite of what I had written down.
|
||
|
||
### The new row
|
||
|
||
`title_plate` joins `verify-capture` at **0.00%** — two orders below every other
|
||
row, which makes it the most sensitive regression detector in the harness.
|
||
|
||
⚠️ Its instant is **fitted, not measured**. t=237 is where this capture's content
|
||
places it, found by sweeping. Choosing which frame to compare against is what
|
||
every row here does, but the 0.00093 % is a floor for *that pose*, not a general
|
||
statement of accuracy.
|
||
|
||
## `--focus=` did nothing on the menu path, and the corpus had an untested focus capture
|
||
|
||
Two unused live captures were sitting in `docs/re/captures/title-builds/`.
|
||
`live-main-menu-options-focused.png` is the menu with **OPTIONS** focused — the
|
||
only capture of a *known* focus state — and it was untestable, because
|
||
`--focus=` **silently did nothing on the `--menu` path**.
|
||
|
||
The flag parsed, was stored in `_force_focus`, and was applied to
|
||
`view.focused_id` at startup — and then `_menu_enter` overwrote it with the
|
||
authored initial focus on every entry. Every run logged `focus ptbtn01` whatever
|
||
was asked for, and all five buttons produced the same frame. It is now pushed
|
||
into the **menu model**, not just the view, so navigation continues from where it
|
||
was forced rather than jumping back on the first press.
|
||
|
||
### The port's focus rendering is right, measured against the oracle
|
||
|
||
Rendering each of the five buttons focused, against each capture:
|
||
|
||
| focused | vs `live-main-menu-options-focused` | vs `live-main-menu` |
|
||
|---|---|---|
|
||
| `ptbtn01` NEW GAME | 0.7352 % | **0.0705 %** |
|
||
| `ptbtn02` LOAD GAME | 0.8204 % | 0.8378 % |
|
||
| `ptbtn03` TUTORIAL | 0.7220 % | 0.7365 % |
|
||
| **`ptbtn04` OPTIONS** | **0.1355 %** | 0.7449 % |
|
||
| `ptbtn05` EXTRAS | 0.7029 % | 0.7236 % |
|
||
|
||
Each capture picks out exactly one button, by **5×** and **10×**. This is the
|
||
first time the port's focus rendering has been checked against the game at all —
|
||
the harness's own `main_menu` row uses an *authored* focus, so it could never
|
||
have caught a focus error.
|
||
|
||
### What that settles, and what it does not
|
||
|
||
✅ The port draws focus on the right button, distinguishably.
|
||
✅ `live-main-menu.png` shows **NEW GAME** focused, so the authored
|
||
`initial_focus: ptbtn01` matches the one frame it can be checked against.
|
||
|
||
⚠️ **It does not overturn HANDOFF Q5**, which measured initial focus as *unstable
|
||
boot to boot* across four runs. One capture showing `ptbtn01` is consistent with
|
||
instability, not evidence against it. The value stays **authored**, with the
|
||
agreement recorded beside it. Reading this as "initial focus is settled" would be
|
||
exactly the over-generalisation from a single observation that this corpus keeps
|
||
having to withdraw.
|
||
|
||
`main_menu_options` joins `verify-capture` at 0.13 %.
|
||
|
||
⚠️ Still unused: `live-attract-title-press-a-band.png`, a 1279×**120** strip
|
||
rather than a full frame. It needs a banded comparison the harness does not do,
|
||
so it is named here rather than quietly left out.
|
||
|
||
## The last unused capture, placed — and its residual is the oracle's, not the port's
|
||
|
||
`live-attract-title-press-a-band.png` was the one live capture nothing consumed.
|
||
Following last iteration's rule — *an unused capture is a signal about the
|
||
harness* — it turned out the same way as the previous two: nothing about the
|
||
capture was unusable, the harness simply could not compare a **band**. It is
|
||
1279×**120**, not a full frame.
|
||
|
||
### Placing it
|
||
|
||
Sliding it down the render, structural difference against the port:
|
||
|
||
| y | differing |
|
||
|---|---|
|
||
| 300 | 51.99 % |
|
||
| 500 | 19.61 % |
|
||
| 515 | 9.14 % |
|
||
| **520** | **0.354 %** |
|
||
| 525 | 8.87 % |
|
||
| 555 | 24.02 % |
|
||
|
||
**y = 520**, a 25× drop over five pixels. Measured, not guessed. Sweeping the
|
||
instant at that offset puts it at **t = 236–238** — the plate's own opaque
|
||
window, the same instant the full-frame `title_plate` row fits.
|
||
|
||
### 🔴 The 0.354 % is not the port's error
|
||
|
||
Three comparisons separate it:
|
||
|
||
| | differing |
|
||
|---|---|
|
||
| port's band vs `live-title-press-a`'s same band | **0.000 %** |
|
||
| `live-title-press-a`'s band vs the attract band *(oracle vs oracle)* | **0.301 %** |
|
||
| port's band vs the attract band | 0.354 % |
|
||
|
||
The port reproduces one capture's band **exactly**. The two captures differ from
|
||
*each other* by 0.301 %, which is nearly the whole residual.
|
||
|
||
And that oracle-to-oracle difference is two thin horizontal strips — **248×5 px**
|
||
and **206×1 px** — which is the shape of a sub-pixel edge difference or capture
|
||
noise, not of a state difference.
|
||
|
||
⚠️ I had started writing that the *attract-returned* title differs from the boot
|
||
title, which would have been a finding about the game inferred from 0.3 % of a
|
||
band. It is two hairlines. The connected-component breakdown is what stopped it,
|
||
and I would not have run it if the number alone had looked small enough to
|
||
dismiss or large enough to report — it was in the range where you have to look.
|
||
|
||
So `title_band` joins the harness at 0.35 %, and its job is to **stay near the
|
||
oracle-to-oracle gap**, not to reach zero. A row whose target is not zero has to
|
||
say so, or the next person tunes toward it.
|
||
|
||
✅ **All eight live captures in the corpus are now used.** Three were sitting idle
|
||
and all three were blocked by the harness, not by the capture: an overlay posed at
|
||
t=9 that drew nothing, a `--focus=` overwritten on every menu entry, and a banded
|
||
comparison that did not exist.
|
||
|
||
## `MODDING.md` had five rules and no check. Now it has one, and all five pass
|
||
|
||
`MODDING.md` opens by calling modding *a constraint on the exporter **today**,
|
||
not a later feature*. Nothing verified it. That is the shape this port keeps
|
||
finding — a rule stated, believed and unexercised: the black hold implemented and
|
||
never called, `ScreenView.skipped` written and never read, `stop_bed` provided and
|
||
never used, `--focus` parsed and overwritten on every menu entry.
|
||
|
||
`tools/port/check-modding` covers all five. Every one passes today, so it is a
|
||
**guard, not a fix**: its value is that the next thing to break one says so.
|
||
|
||
| rule | check | result |
|
||
|---|---|---|
|
||
| 1 — one asset, one file | every referenced sprite present, none orphaned, no split names | **174 / 174**, exact |
|
||
| 2 — recognisable names | no hex or hash-shaped filenames | none |
|
||
| 3 — modern formats | extensions confined to json/png/ogg/ogv (+ sidecars) | clean |
|
||
| 4 — base and overrides | `data/mods` gitignored *and* read by the exporter | both |
|
||
| 5 — provenance | every generated JSON carries a `source` | 17 / 17 |
|
||
|
||
### It is proved to fail
|
||
|
||
A check that has never failed has not been shown to work — the lesson from
|
||
`check-capture`, which once passed a file with 36 % holes punched through it. Three
|
||
controls, each failing correctly with a non-zero exit:
|
||
|
||
* a `.cmd` sidecar with its header stripped → rule 3;
|
||
* a `bogus.bmp` in the sprite tree → rule 3;
|
||
* one orphaned PNG → rule 1, *"174 referenced, 175 present"*.
|
||
|
||
### The one thing it found: an unlabelled generated file in the asset tree
|
||
|
||
The two `.cmd` encode-cache sidecars sat beside the `.ogv`s in the modder-facing
|
||
tree with no line saying what they were — a bare ffmpeg command next to a video
|
||
reads as something to edit or delete. They now carry a header stating that they
|
||
are generated, are not assets, and that the way to change a video is an override
|
||
under `data/mods/`.
|
||
|
||
Two details worth keeping:
|
||
|
||
* the header is **excluded from the cache key**, so rewording it does not
|
||
re-encode four minutes of video. A cache that punishes documentation gets
|
||
documented once and never again.
|
||
* the sidecar is now refreshed whenever its **text** differs, not only when a
|
||
re-encode happens. It used to be written inside the `!fresh` branch — which
|
||
meant a header change could never reach an existing export, because nothing
|
||
that reads the header triggers the write that updates it. The explanation would
|
||
have been correct in the source and absent on disc. Confirmed: two consecutive
|
||
exports, 20 s and 19 s, header present, no re-encode.
|
||
|
||
### And a question I asked the Decoder that I could partly answer myself
|
||
|
||
Last iteration I asked whether the 0.301 % between two of their captures implies a
|
||
**capture-path floor on every comparison in the corpus**. It does not, and I had
|
||
the evidence already: the port matches `live-title-press-a.png` at **0.00093 %**
|
||
full-frame and **0.000 %** across the band. A general floor could not coexist with
|
||
either number. So the 0.301 % is specific to the attract band capture, and my
|
||
0.01–0.2 % rows are not sitting on a hidden floor. ⚠️ What that does *not* settle
|
||
is why those two frames differ — still theirs, and still worth an answer.
|
||
|
||
## Five authored values had no reader — including the one I asked for measurements into
|
||
|
||
Applying the prior from the last six findings — *a rule or capability nothing
|
||
exercises turns out broken or inert when someone looks* — to `authored/` itself.
|
||
Grepping every authored key for a reader in the exporter or the runtime:
|
||
|
||
| key | file | status |
|
||
|---|---|---|
|
||
| `dwell` | `flow.json` | 🔴 **no reader** — now wired |
|
||
| `ramp` | `timing.json` | no reader — now asserted |
|
||
| `left_right` | `flow.json` | no reader — now asserted |
|
||
| `input_during_transition` | `flow.json` | no reader — now asserted |
|
||
| `stems` | `audio.json` | no reader (`stems_why` is carried; the sum is hardcoded) |
|
||
|
||
Everything else — `se`, `bgm`, `voice`, `boot`, `screens`, `navigation.wrap`,
|
||
`draw_leaf_for`, `loop_leaf_on_screens`, `keyframe_units_per_second`,
|
||
`black_hold_units`, `archives`, `also_export`, `presentation`, `loop_mode`,
|
||
`initial_focus`, `skippable`, `then_video`, `after_video` — is read.
|
||
|
||
### 🔴 `dwell` is the one that mattered
|
||
|
||
Its own text says *"when a capture times the real boot, the extra hold per screen
|
||
goes here."* **A number placed there did nothing.** Two iterations ago I asked
|
||
the Decoder for measurements destined for exactly that slot; had they arrived,
|
||
they would have been filed into a value with no reader and the boot would have
|
||
been unchanged, silently — and I would have reported the boot as matching.
|
||
|
||
It is wired now, and **stays empty**. Nothing is authored into it: the splash
|
||
dwells are declared on the disc and measured to agree. Wiring the slot so that a
|
||
future number has an effect is the opposite of adopting one.
|
||
|
||
⚠️ **I wired it to the wrong branch first, and it did nothing — silently.** Holding
|
||
longer after settle changes nothing, because the screen still leaves when
|
||
`exit_time() + black_hold` arrives and the extra hold is absorbed. A dwell has to
|
||
delay the **departure**. I found it only because I tested the control:
|
||
+120 units moved the transition 4.46 s → **6.43 s**, +1.97 s, with the video
|
||
following by the same amount. Reproducing the exact defect I was removing, inside
|
||
the fix for it, is the strongest argument I have for testing that a wire carries
|
||
current rather than that it exists.
|
||
|
||
### The other three are asserted, not implemented
|
||
|
||
`ramp`, `left_right` and `input_during_transition` describe behaviour the port
|
||
**hardcodes**. That is fine for a record and dangerous for a switch, and they are
|
||
written like switches — setting `left_right` to `"move"` would change nothing and
|
||
warn nobody.
|
||
|
||
Rather than invent the missing implementations, `_check_authored_invariants`
|
||
**asserts the value the port was built against**, naming the file. Changing one
|
||
now produces an error instead of silence.
|
||
|
||
That is precisely the distinction `left_right`'s own `why` claims to be making —
|
||
*"written out rather than left unhandled so that 'the game ignores it' and 'we
|
||
never wired it' are different lines of code"* — and which was not in fact being
|
||
made, because nothing read the value that was supposed to make it.
|
||
|
||
✅ The validator is **called**, not merely defined. A validator nobody invokes is
|
||
the same defect it exists to catch, and this file now documents six other
|
||
instances of exactly that.
|
||
|
||
Verified: clean boot with no invariant errors and unchanged timings; setting
|
||
`left_right: "move"` produces the error; all five MODDING rules still pass; the
|
||
oracle rows are unmoved.
|
||
|
||
## `FORMAT.md` declared the port's own export invalid, and a failed export is not atomic
|
||
|
||
Continuing the audit that has now found seven unexercised rules: `FORMAT.md` is
|
||
the **open format spec** — written for a stranger reading the tree with no access
|
||
to the disc or this exporter. So the question is whether what it promises is what
|
||
`sylpheed-export check` enforces.
|
||
|
||
Five documented requirements, each broken in a copy of the tree:
|
||
|
||
| broken | caught |
|
||
|---|---|
|
||
| `unresolved` removed from a screen | ✅ |
|
||
| `peak_dbfs` removed | ✅ |
|
||
| `peak_dbfs` = −120 (silence) | ✅ |
|
||
| `duration_s` removed | ✅ |
|
||
| `peak_dbfs` = 0.0 on an `se` | **passes** |
|
||
|
||
### The last row is the doc's error, not the code's
|
||
|
||
`FORMAT.md` said flatly that check *"refuses a tree whose peak is ≤ −90 dBFS or
|
||
**≥ 0 dBFS**"*. The implementation is kind-dependent and deliberate: a `bgm` is a
|
||
sum **we** produced, so a peak at or above full scale is our arithmetic and is
|
||
refused outright; an `se` or `voice` is a single wave off the disc, mastered near
|
||
full scale, whose lossy decode overshoots by a fraction of a dB, and those are
|
||
allowed to +1.0.
|
||
|
||
🔴 **And the doc was wrong about the port's own export.** It ships `confirm` at
|
||
**+0.18 dBFS** and the `ADV` voice at **+0.31** — both above 0. A consumer
|
||
implementing a validator from `FORMAT.md` would have rejected a valid tree, and
|
||
the file that exists to let someone check our work without trusting us would have
|
||
been the thing that misled them. Corrected, with the +1.0 marked as the judgement
|
||
it is.
|
||
|
||
✅ Verified both directions: a `bgm` forced to 0.0 is refused with *"a SUM we
|
||
produced clips"*; an `se` at 0.0 passes.
|
||
|
||
### 🔴 A failed export leaves a tree that is not an export tree
|
||
|
||
Found by accident, and worth more than the way it was found. Testing the new
|
||
`stems` assertion, the exporter `bail!`ed part-way — and left `export/` **with no
|
||
`manifest.json` at all**. Every subsequent tool then reported *"has no
|
||
manifest.json — is that an export tree?"*, which reads as a broken harness rather
|
||
than as the aftermath of a deliberate failure.
|
||
|
||
⚠️ It cost me a wrong reading immediately: the first run of the requirement audit
|
||
above reported every case as "no manifest", and I nearly recorded that the
|
||
validator was checking nothing. It was checking a tree that had been half-written.
|
||
|
||
The exporter writes the manifest last, which is the right order — a manifest is a
|
||
claim about a tree, and a manifest for a tree that was never finished would be
|
||
worse. So this is **filed rather than fixed**: the behaviour is defensible and the
|
||
message is not, since "is that an export tree?" describes the symptom and hides
|
||
the cause. What a stranger needs to be told is *the last export failed; re-run it*.
|
||
|
||
## `check-all`, a verdict that ignored its own statistic, and a claim of mine that was wrong
|
||
|
||
Eleven tools under `tools/port/` and **nothing ran them together**, so each had to
|
||
be remembered individually. That is the ninth instance of this port's recurring
|
||
shape — correct, documented, unexercised — one level up: the checks were the thing
|
||
nobody was running.
|
||
|
||
`tools/port/check-all` runs the four that assert (`check`, `check-modding`,
|
||
`check-capture-controls`, `verify-menu-audio`), prints the oracle table, and
|
||
handles `verify-screen` specially. All eleven were exercised first and **none had
|
||
rotted**; `which-focus` independently picks NEW_GAME at a **93.8× margin**, which
|
||
is a second instrument agreeing with the capture fit's 10×.
|
||
|
||
Two things it is careful about:
|
||
|
||
* the six exploratory tools are **not** listed as passes. They produce artifacts
|
||
for a person to look at and have no verdict; counting them would invent six.
|
||
* `verify-capture` is **reported, not asserted** — it always exits 0. Its header
|
||
is right that the numbers are not a target, but *not a target* is not *not a
|
||
regression detector*, and nothing would notice `title_plate` moving off 0.00 %.
|
||
Named as a gap rather than papered over; a real fix needs stored baselines, and
|
||
what a baseline means when the pose is fitted is a decision, not a chore.
|
||
* the `verify-screen` allowance **expires on its own condition**. It is allowed to
|
||
fail only while `formats-pin-2026-08-29d` is not an ancestor of `origin/main`;
|
||
the day it lands, `check-all` fails instead. A suppression with no expiry is
|
||
just a hidden failure.
|
||
|
||
### 🔴 The verdict ignored the statistic added to inform it
|
||
|
||
`verify-screen` computes `over3` — how many pixels exceed the bar — because *"a
|
||
single `max` cannot tell 2 pixels from 25 444"*, its own words. **The verdict was
|
||
then decided on `max` alone.** So `main_menu` (max 4, `over3` **0**) read DIFFERS
|
||
while `extras` (max 3, `over3` 0) read OK: one unit on one pixel separating two
|
||
frames that are equivalent at the bar.
|
||
|
||
⚠️ Not fixed by raising the bar, which this file rightly forbids. The bar is still
|
||
3. A frame with **no** pixel over it now gets its own verdict, `ROUNDING`, instead
|
||
of being lumped in with a real disagreement. Tenth instance: the fix was
|
||
implemented, documented, and never wired to the thing it was for.
|
||
|
||
### 🔴 And "six expected DIFFERS" — which I have told the Decoder more than once — was wrong
|
||
|
||
The true count was **ten**, now **eight** after the rounding fix:
|
||
|
||
| screens | count | explained |
|
||
|---|---|---|
|
||
| the forced-backdrop six | 6 | ✅ the pin: two decoder eras |
|
||
| `main_menu`, `main_menu_jp` | 2 | ✅ now `ROUNDING`, not a disagreement |
|
||
| **`title`, `title_jp`** | **2** | 🔴 **not explained** |
|
||
|
||
`title` differs on **790** pixels and `title_jp` on **20 498**, and neither is the
|
||
forced-backdrop rule — those screens have no forced element. I had a blanket
|
||
allowance covering two disagreements I had never accounted for.
|
||
|
||
**My hypothesis for them is refuted.** `authored/rendering.json` notes that the
|
||
consistency harness compares against a renderer that draws no `.rat` leaves, so
|
||
the port's `ptloop` sweeps looked like the obvious cause. Emptying `draw_leaf_for`
|
||
and `loop_leaf_on_screens` changes the numbers **not at all** — 790 and 20 498
|
||
either way. `verify-screen` poses at `rest`, where the leaves evidently do not
|
||
draw. Filed as open.
|
||
|
||
⚠️ `title_jp` is a localisation screen and out of scope (MISSION §7). `title` is on
|
||
the boot path and is not.
|
||
|
||
## The `title` disagreement, localised — and the question I filed for it was the wrong one
|
||
|
||
Two iterations ago I filed `title`'s 790-pixel disagreement with `sylpheed-cli` as
|
||
needing the Decoder: *"which elements does `sylpheed-cli` draw on build 4 at
|
||
rest"*. That was a hypothesis dressed as a question, and it is wrong. I could
|
||
answer it myself, and did.
|
||
|
||
### What is established
|
||
|
||
* **The pixels cluster in one place**: x ≈ 938–1162, y ≈ 172–310, as blobs of
|
||
20–66 px. That is `ptlogo_back2eff1`'s position, `pos=[938, 194]` — one of the
|
||
six `ptlogo_back2eff*` glows, all of which carry a sprite and **no declared
|
||
size**, so the texture supplies it.
|
||
* 🔴 **Both renderers draw it.** The region reads mean **95.60** in the port and
|
||
**95.08** in the CLI. So the premise of my filed question — that one draws an
|
||
element the other does not — is **refuted**. A set difference of element lists
|
||
would have answered nothing.
|
||
* **It is not a placement offset.** Rolling the port's frame by every combination
|
||
of ±1 px makes it *worse* by two orders of magnitude — 790 pixels aligned
|
||
against ≥ 175 406 for the best shift. The images are registered; they differ in
|
||
content on 0.086 % of the frame.
|
||
|
||
### What is not established, and why I am not guessing
|
||
|
||
The mechanism. My next hypothesis was edge antialiasing, and **the test failed its
|
||
own control**: the edge mask classified **92 % of the frame** as edge, so the
|
||
25.2 % of differing pixels landing on it is *below chance* and the instrument is
|
||
dead. A number from it would have been noise wearing a decimal point.
|
||
|
||
### The ask is downgraded rather than left standing
|
||
|
||
`BLOCKED.md`'s row asked for the wrong thing on a refuted premise, which is worse
|
||
than no row: it would have spent someone else's time confirming a difference that
|
||
is not there. Corrected.
|
||
|
||
⚠️ And the residual value is genuinely low. This is **two of our own renderers**
|
||
disagreeing on 0.086 % of one frame, on a screen where the port matches the
|
||
**oracle** at 0.21 % and its plate variant at 0.00093 %. `verify-screen`'s own
|
||
header says a DIFFERS means *"we moved apart, go find out which of us moved"* —
|
||
here neither moved from the game. It stays visible as a DIFFERS rather than being
|
||
allowed, because an allowance is how the two `title` rows hid inside "six
|
||
expected" in the first place.
|
||
|
||
## Auditing `BLOCKED.md` found three stale rows, and the undated ones were all three
|
||
|
||
`BLOCKED.md` opens by warning that it goes stale *"within the hour. It has."*
|
||
Nothing audits it. So I read every live row against a running port instead of
|
||
against my memory of it.
|
||
|
||
### Three were wrong
|
||
|
||
**`P3/P5 — the title screen`** was the worst, and had been for weeks. Every
|
||
factual claim in it is false: the boot does not end on a plateless build 4,
|
||
`press_start` is not unused, and the port has drawn two builds at once since the
|
||
plate-delay work. Verified this iteration — `boot ends on title + press_start`,
|
||
`overlay press_start … drew 1: ptbtn00`, plate region mean **95.70** against 33.6
|
||
for the bare title.
|
||
|
||
🔴 And a row **four lines below it** was already marked *answered and TAKEN* for
|
||
the same question. Two rows on one question, one struck and one live claiming the
|
||
opposite — and the live one was the stale one. A reader taking the page at face
|
||
value would have believed the struck row was the old news.
|
||
|
||
**`P4 — is an attract movie skippable`** led with *"the port could not tell which
|
||
bug that is"*. It can and it did: `DECISIONS.md` records **Ⓐ *does* skip the
|
||
intro in this build**, and every boot run since prints `video skipped at …`.
|
||
Corrected to 🟡 — (a) answered, (b) still needs a capture.
|
||
|
||
**`P6 looping`** was not wrong, only thin. It now carries the measured cost: the
|
||
bed loops at 87.8 s against the track's 87.7, and the seam is **3.4 seconds of
|
||
near-silence**. The missing loop point now has a price expressed as a number
|
||
rather than as the adjective "ugly".
|
||
|
||
### The undated rows are the ones that rotted
|
||
|
||
Checking which sections record the HANDOFF commit they derive from — the standing
|
||
instruction, and the thing this page's own header demands — splits the file
|
||
exactly:
|
||
|
||
| | sha |
|
||
|---|---|
|
||
| every prose section added since 2026-08-29 | ✅ all |
|
||
| **every row in the two tables** | 🔴 **none** |
|
||
|
||
And all three stale rows were in those tables. That is about as close to a
|
||
controlled experiment as this page will produce: the rows with no date are the
|
||
rows nobody could tell had aged.
|
||
|
||
⚠️ I have **not** back-dated them. Nobody knows when most were written and an
|
||
invented sha is worse than an admitted absence — it would make an undated claim
|
||
look checked. Both tables now say so at the top, and carry the date they were last
|
||
audited against a running port.
|
||
|
||
📌 The general form, which is the eleventh of these: *a document that warns about
|
||
its own failure mode is not thereby protected from it.* This page has said "it
|
||
goes stale, it has" since P1, in a header everyone reads and nobody acts on. The
|
||
warning was doing the work of a check.
|
||
|
||
## The record already answered last iteration's question, under headings that name it
|
||
|
||
Last iteration I investigated `title`'s and `title_jp`'s disagreement with
|
||
`sylpheed-cli`, ruled out leaves and placement offsets, watched an edge test fail
|
||
its own control, and filed the mechanism as **unknown** — to the Decoder as well
|
||
as here.
|
||
|
||
**Both were already explained in this file**, under
|
||
[`title`: a tie in the paint order](#title-a-tie-in-the-paint-order--neither-renderer-is-wrong)
|
||
and
|
||
[`title_jp`: nearest-neighbour sampling phase](#title_jp-nearest-neighbour-sampling-phase--the-cli-is-the-one-i-would-call-wrong).
|
||
Headings that name the two screens in question.
|
||
|
||
### Both still hold, checked rather than assumed
|
||
|
||
* **`title`** — the CLI uses a paint order *measured* off the running game; the
|
||
port derives one. Every disagreement is **inside a tie**. Verified against the
|
||
current export: `title` still ties on `0x8083` (the `back2` glow group, 5
|
||
elements), `0x80a0` (7) and `0x8010` (2), and the export declares
|
||
`paint_order_ties` in `unresolved`. The old entry costs it at **904 px in the
|
||
glow band, all 4–6/255**; I measured **790 px at x≈938–1162, y≈172–310, max 6**
|
||
— same band, same magnitude, count moved with the export.
|
||
* **`title_jp`** — `ptlogo_eff2` at **125 %**, where the CLI samples the source at
|
||
the destination pixel's top-left and a GPU samples at its centre. The entry
|
||
claims it is the *only* drawn element in the export at a non-integer scale
|
||
ratio. 🟡 A whole-export census finds **26** such keyframes — until it is
|
||
restricted to elements **visible at `rest`**, which is the pose `verify-screen`
|
||
uses. Then there is exactly **one**: `title_jp/ptlogo_eff2`. The claim survives,
|
||
and it survives narrowly enough to be worth the check.
|
||
|
||
### 🔴 The failure is navigability, not memory
|
||
|
||
6 502 lines, 111 `##` sections, 243 `###`, and **no index**. So "has this been
|
||
decided already?" had no cheap answer, and the expensive answer — re-deriving it
|
||
— looked like diligence. It also cost the Decoder a message reporting an open
|
||
question that was closed.
|
||
|
||
`tools/port/index-decisions` generates a contents block, and `check-all` runs
|
||
`--check`. ⚠️ Two things about the design, both learned the hard way in the same
|
||
half-hour:
|
||
|
||
* it carries **no line numbers**. The first version did, and writing the index
|
||
shifted every line below it — a fixpoint that failed its own `--check` on the
|
||
first run. Worse, appending a section would silently invalidate all of them.
|
||
* it is **checked**, because a stale index is worse than none: it would answer
|
||
*"is this already decided?"* with a confident no. That is exactly the failure
|
||
it was built to prevent, and building an unchecked one would have reproduced it
|
||
one level up — which this port has now done often enough that I looked for it
|
||
deliberately.
|
||
|
||
📌 The twelfth instance, and a new species. The first eleven were *"correct,
|
||
documented, unexercised."* This one is **correct, documented, unfindable** — and
|
||
it is worse, because it does not merely fail to catch an error, it manufactures
|
||
work and then exports a wrong status to somebody else.
|
||
|
||
## 🔴 Twenty-one messages to a dead address, each one warning me it was dead
|
||
|
||
Every `SendMessage` I sent the Decoder from 2026-08-29 onward came back with:
|
||
|
||
> *accepted by the server for that session, but **delivery is not confirmed**: it
|
||
> has not reported that it can receive cross-session messages*
|
||
|
||
**Twenty-one times.** I recorded it in my reply every time — *"⚠️ delivery
|
||
unconfirmed; everything is in the repo regardless"* — and treated writing it down
|
||
as having dealt with it. It was not a caveat. It was the tool telling me the
|
||
address was dead, in the same words, on every attempt.
|
||
|
||
### The cause
|
||
|
||
I addressed `bridge:session_01QsEPXW…`, copied from the `from=` of an inbound
|
||
message. That is a **session ID**, and it died when the Decoder restarted.
|
||
`PROTOCOL.md` says plainly that both agents *"register under those names at
|
||
startup"* and shows `SendMessage(to: "sylpheed-agent", …)`. **Names survive a
|
||
restart; session IDs do not.** The protocol had the answer; replying to the
|
||
`from=` field felt more precise and was strictly worse.
|
||
|
||
### Why this one is the worst of the thirteen
|
||
|
||
The other twelve were things nobody was looking at. This one **had an alarm, and
|
||
the alarm fired every single time**. I did not miss it — I read it, wrote it into
|
||
my own status line, and carried on. Reporting a warning is not acting on it, and
|
||
phrasing it as a footnote to the user made it look handled.
|
||
|
||
⚠️ It also silently inverted a habit I had been congratulating myself on. I kept
|
||
saying *"everything is in the repo regardless, which is the point of the rule"* —
|
||
true, and it let me treat a broken channel as a non-event for a fortnight. The
|
||
repo did carry the findings. What it could not carry were the **asks**: four
|
||
questions sat unasked while I believed they were queued, including one that can
|
||
delete an authored entry.
|
||
|
||
### What changed
|
||
|
||
Re-addressed by name after `ListAgents`, and the send came back **without** the
|
||
warning — which is what a working channel looks like, and what twenty-one
|
||
unworking ones did not.
|
||
|
||
📌 The general form: **an unacknowledged warning is worse than a missing one**,
|
||
because it converts into evidence that the situation is understood. The fix is not
|
||
"read the warnings" — I read them all. It is that a warning repeating unchanged
|
||
across attempts is a *state*, not a footnote, and the second identical one should
|
||
have been treated as a failure rather than a fact about the world.
|
||
|
||
## The forced-backdrop pass is load-bearing on two screens, not six
|
||
|
||
I have said "six forced elements" since implementing the rule, and checked after
|
||
every change that **no verdict moved**. That measured the pass's *stability*, never
|
||
its *necessity*. Removing it entirely answers the other question:
|
||
|
||
| screens | order without the pass |
|
||
|---|---|
|
||
| `publisher_logo`, `publisher_logo_r`, `developer_logos`, `developer_logos_r` | **byte-identical** |
|
||
| `build_12`, `build_15` | first element becomes `pgloading_loop5` — the black screen returns |
|
||
|
||
**Four of the six are redundant.** `palogo_eff0`'s layer key is `0x00000000`,
|
||
`layer_source: implied` — lower than the lowest sprite key on those screens
|
||
(`0x0000a100`) — so the crate already sorts it first and the occlusion rule merely
|
||
agrees.
|
||
|
||
🔴 **The two that matter have no key at all.** On `build_12`/`build_15`,
|
||
`pgloading_eff00` carries `layer: null`, `layer_source: none` — the only two
|
||
elements in the export with neither a read nor an implied key. Their position rests
|
||
**entirely** on the occlusion constraint, with nothing to fall back on. That is the
|
||
port's single strongest dependency on a rule it did not decode, and it was hiding
|
||
inside a count of six.
|
||
|
||
### Which is also what makes the rule worth having
|
||
|
||
An agreement on four screens where a key already existed is not evidence — it is
|
||
the rule reproducing the crate. The two screens where it is load-bearing are the
|
||
two the rule was found on, and the argument for it there is not the key (there is
|
||
none) but that a permanently black screen is impossible on its face.
|
||
|
||
⚠️ So the honest statement, replacing the one I have been repeating: the rule
|
||
**decides** two screens and **confirms** four. I will not describe it as six again.
|
||
|
||
### The layer-key census this came out of
|
||
|
||
| `layer_source` | count | which |
|
||
|---|---|---|
|
||
| `sprite` — read from the file | 160 | everything with a texture |
|
||
| `implied` — the crate's, measured per name | 16 | `pteff00`, `pteff02`, `palogo_eff0`, `pgloading_eff00` |
|
||
| `none` — no key exists | **2** | `pgloading_eff00` on the two loading screens |
|
||
|
||
✅ Every layerless primitive's position is `implied` or absent, never read — and
|
||
`FORMAT.md` requires a consumer to be able to tell, which `layer_source` delivers.
|
||
The Decoder's own page says `pteff00`'s place on top *"is still a **measured**
|
||
per-name entry, not a decoded one"*; the port inherits that through
|
||
`layer: 0xfffffffe, layer_source: implied` and declares it. Nothing is being
|
||
passed off as decoded that is not.
|
||
|
||
## Re-running the Decoder's necessity census: every figure reproduces, and what that is worth
|
||
|
||
They took the stability-vs-necessity correction disc-wide and published the
|
||
instrument with it — *"so you can re-run it rather than trust it"*. I did, from a
|
||
worktree at their branch, across all 33 archives their census names:
|
||
|
||
| | theirs | my re-run |
|
||
|---|---|---|
|
||
| forced instances | 80 | **80** |
|
||
| **decided** by the rule | 62 | **62** |
|
||
| merely agreed | 18 | **18** |
|
||
| decided, by extension | 38 `.prm` / 24 `.tbm` | **38 / 24** |
|
||
| decided rows listing a keyless element | all | **all 62** |
|
||
|
||
Exact, on every figure. ⚠️ The example defaults to `GP_TITLE` and takes an archive
|
||
path, so a bare run reports **6 instances, not 80** — the disc-wide number needs
|
||
the loop. Worth saying because "I ran their instrument" would otherwise be true
|
||
and mean a thirteenth of what it sounds like.
|
||
|
||
### 🔴 What this verification is not
|
||
|
||
**I ran their code.** A fault in the instrument reproduces identically for me, so
|
||
this is not two independent measurements — it is one measurement executed twice.
|
||
That is the same correlated-instrument trap as `verify-screen` scoring two blank
|
||
frames `OK`, and I would rather name it than let a table of matching numbers imply
|
||
more than it has.
|
||
|
||
The genuinely independent evidence is narrower and came first: I removed **my own**
|
||
post-pass in the exporter and diffed the export. Different code, different
|
||
language, different layer of the stack — and it agreed on the six GP_TITLE
|
||
instances. Their crate-side run agrees with that. So the GP_TITLE result has two
|
||
witnesses; the other 74 have one, executed twice.
|
||
|
||
### The consequence for the port
|
||
|
||
Both of the port's decided screens are inside the 62, and **every one of the 62 is
|
||
keyless** — so the impossibility argument ("a screen black at every instant of its
|
||
own timeline cannot be right") is carrying all of them alone, with no key beneath
|
||
it anywhere. The exposure is theirs to hold disc-wide; the port's share is
|
||
`build_12` and `build_15`.
|
||
|
||
⚠️ 24 of the 62 are `.tbm`, which their page still records as *"correct or inert"*
|
||
because the corpus cannot find a `.tbm`'s pixels. None are in `GP_TITLE` — checked
|
||
again: 115 `.t32`, 45 `.rat`, 18 `.prm`, no `.tbm` — so nothing the port ships
|
||
depends on that half. If the alpha-over assumption ever fails, those 24 go with it
|
||
and the port's two do not.
|
||
|
||
## A second witness for the pixel-cost claim, from a different renderer
|
||
|
||
The Decoder moved the necessity question to a new layer rather than re-running the
|
||
sort — they rendered each of the 62 deciding builds twice and diffed the pixels,
|
||
finding that on all **38 `.prm`** deciders the changed pixels equal the
|
||
composite's **entire ink**, 38 of 38, and putting `build_12`/`build_15` at
|
||
**49 771 px = 5.40 %** each.
|
||
|
||
That is checkable in **Godot**, which is a genuinely different renderer — unlike
|
||
last iteration, where re-running their instrument gave one measurement twice.
|
||
|
||
| `build_12`, `--pose=rest` | ink |
|
||
|---|---|
|
||
| with the rule, threshold > 0 | 59 530 px (6.46 %) |
|
||
| with the rule, threshold > 1 | 48 368 px (5.25 %) |
|
||
| **without the rule** | **0 px at both** |
|
||
|
||
✅ **The strong form holds.** Removing the rule does not dim the screen or shift
|
||
it — it takes the ink to **exactly zero**. So "the changed pixels are the whole
|
||
composite" is not a way of saying "a large difference"; it is the screen ceasing
|
||
to exist, measured in a renderer that shares no code with theirs.
|
||
|
||
Their 49 771 sits between my two thresholds, 2.9 % above the `> 1` count. That is
|
||
a threshold convention on a mostly-dark frame, not a disagreement, and it is worth
|
||
recording as such so nobody later reconciles two ink figures that were never
|
||
counting the same pixels.
|
||
|
||
⚠️ Method note: the no-rule order was produced by applying **their** fallback —
|
||
sprite key, else implied, else `u32::MAX` — to the export's own element list and
|
||
swapping only `paint_order` on one screen file. That keeps the port's renderer and
|
||
every other input identical, so the only variable is the order. `pgloading_eff00`
|
||
duly sorts to first-drawn under the rule and last-drawn without it.
|
||
|
||
### Their point 4 is the pattern catching one of their own
|
||
|
||
They report that the 24 `.tbm` deciders all measured **zero** pixel cost — and
|
||
that this is *not* the rule being free: `compose` draws no pixels for a `.tbm` at
|
||
all, so their position cannot change a pixel **by construction**. The control
|
||
asked whether the composite had ink, which it always does. **A control that could
|
||
not fail.**
|
||
|
||
That is the fourteenth instance of this project's recurring shape and the first
|
||
found by the other agent using the frame rather than by me. It leaves the `.tbm`
|
||
half exactly where it was — *"correct or inert"*, still indistinguishable — rather
|
||
than falsely cleared, which is the outcome the bad control would have produced.
|
||
|
||
✅ Nothing the port ships is affected: no `.tbm` in `GP_TITLE`, and the port's two
|
||
deciders are `.prm`.
|
||
|
||
## Reconciling the two ink figures, and what "has its own key" is resting on
|
||
|
||
The Decoder reconciled our `build_12` ink counts and corrected a mistake I would
|
||
otherwise have made in their favour.
|
||
|
||
| | theirs | mine | gap |
|
||
|---|---|---|---|
|
||
| RGB > 0 | 49 771 | 59 530 | 9 759 px |
|
||
| RGB > 1 | 48 043 | 48 368 | **325 px — 0.67 %** |
|
||
|
||
🔴 **Their 49 771 was never a threshold figure.** It is exact RGBA inequality
|
||
between the two paint orders, which over a black backdrop coincides with `ink>0`
|
||
— so it belongs against my **59 530**, not against my 48 368. Matching it to the
|
||
closer number would have made us agree for the wrong reason, and they said so
|
||
before I could do it.
|
||
|
||
Checked on my side: my value-exactly-1 population is **11 162 px**, and the gap
|
||
above `>1` is 325 px. So **9 434 of the 9 759-px gap — 96.7 % — lives in pixels
|
||
whose value is exactly 1**, and their renderer produces that many fewer of them.
|
||
Their claim holds structurally, not just in headline.
|
||
|
||
✅ Two conventions worth carrying, both theirs:
|
||
|
||
* **`>0` is not portable between renderers on a mostly-dark frame; `>1` is.** Any
|
||
cross-renderer ink figure needs its threshold named. This corpus has spent real
|
||
effort on residuals at the 0.1 % scale, and a 16 % artefact hiding at 1 LSB is
|
||
exactly the size of thing that would have been argued about.
|
||
* the without-the-rule zero is **threshold-insensitive in both renderers**, which
|
||
is the first double-witnessed result on this rule.
|
||
|
||
### One refinement on the 18 confirmations
|
||
|
||
They sharpened my summary: the 18 are worse than "the crate agreeing with
|
||
itself" — 14 have their own key so the rule never fires decisively, and 4 are
|
||
builds where every element is forced, so the tie-break gives the same order
|
||
regardless. None is evidence in either direction. Agreed.
|
||
|
||
⚠️ But for the port's four, "has its own key" is doing quieter work than it
|
||
sounds. `palogo_eff0`'s key is `0x00000000` with **`layer_source: implied`** — the
|
||
crate's measured per-name entry, not a value read from the file. So those four
|
||
confirmations are not *the file already settles it*; they are **another
|
||
measurement already settles it**, in the same category as `pteff00`'s place on
|
||
top, which their own page is careful to call measured rather than decoded.
|
||
|
||
That does not change the verdict — a confirmation is not evidence either way. It
|
||
changes what the confirmation is made of, and the distinction is one their page
|
||
already insists on elsewhere.
|
||
|
||
## Not one of the 80 has a decoded key — and the port's four are the rule's oracle check
|
||
|
||
The Decoder took the key-source question disc-wide and the answer is stronger
|
||
than either of us had it: **0 of the 80 forced instances carry a key read from
|
||
the `T8aD` header.** 14 have an `implied_layer_key`, 66 have nothing.
|
||
|
||
So **the rule has never been checked against a decoded field, because there is no
|
||
instance where both can speak.** That is what a keyless-element fallback
|
||
necessarily looks like — but it removes a check a reader would assume exists.
|
||
|
||
The port's six are the same pattern exactly, verified from the export:
|
||
|
||
| | count | |
|
||
|---|---|---|
|
||
| read from the file | **0** | |
|
||
| `implied` — measured in the running game | **4** | `palogo_eff0` on the four splashes |
|
||
| nothing at all | **2** | `pgloading_eff00` on `build_12`/`build_15` |
|
||
|
||
### They retracted something in the direction that costs them
|
||
|
||
Last round they wrote that *"none of the 18 is evidence for the rule in any
|
||
direction"*, and I agreed and repeated it. They have since separated two questions
|
||
I had let run together:
|
||
|
||
* **does the rule change the composite there?** No — the sort already had a key.
|
||
That is the necessity count and it stands.
|
||
* **does the rule get the right answer there?** **Yes** — and this is the only
|
||
place it can be asked. `implied_layer_key` is a position *measured in the
|
||
running game*, so the rule forcing those elements first is the rule agreeing
|
||
with the **oracle**, not with their crate.
|
||
|
||
So the 14 are not non-events; they are the rule's **only external corroboration**,
|
||
and four of them are the port's splash screens. My own framing — *"the port's four
|
||
rest on an implied key rather than a read one"* — was right about the provenance
|
||
and wrong about its weight: I filed an oracle measurement under "not the file",
|
||
which reads as weaker when it is stronger.
|
||
|
||
⚠️ ✅ The revised state, which I expect to be stable: **38 `.prm` deciders with a
|
||
measured pixel cost, two double-witnessed · 24 `.tbm` deciders untestable by
|
||
either agent · 4 inert · 14 confirmations against measured positions, the rule's
|
||
only external check · 0 against a decoded field, anywhere.** The impossibility
|
||
argument carries all 62.
|
||
|
||
📌 Closing this thread on my side. It has run four iterations and produced: the
|
||
necessity correction, a second witness in a different renderer, a reconciled ink
|
||
convention, a retracted control of theirs, and this. It has also stopped being
|
||
about the port — nothing in `build_12`/`build_15` has moved since the rule landed,
|
||
and the remaining questions are the Decoder's to hold. Continuing would be
|
||
refining someone else's page.
|
||
|
||
## A withholding reason that was false, and the measurement beside it that was not
|
||
|
||
`authored/rendering.json` names three leaf records the exporter flags and the port
|
||
does **not** draw. Auditing the reasons rather than the behaviour:
|
||
|
||
### 🔴 `build_12,15/pgloading_loop5` — the reason was wrong
|
||
|
||
It read *"leaf scale (0,0). A zero scale is one of the three historical failures
|
||
this corpus names."* That describes **t=0 and t=30 and nothing after them.**
|
||
|
||
What the leaf actually holds, read from the export: one element,
|
||
`pgloading_ring`, with a sprite, whose scale ramps **0 → 250 → 800 → 1000** while
|
||
its alpha rises to full at t=55 and falls to nothing by t=130. An expanding,
|
||
fading ring — a loading pulse, not a degenerate record.
|
||
|
||
⚠️ **And it is visible at the instant this port poses.** `build_12`'s settle window
|
||
is [40, 48], so the pose lands near **t=44**, where the ring interpolates to
|
||
**scale 140 at alpha 143**. Withholding it is not declining to draw *nothing*; it
|
||
is declining to draw *something*, and the one-line reason concealed which.
|
||
|
||
✅ **It stays withheld**, on the reason that always applied and is already written
|
||
below it in the same file: there is no way to adjudicate it here. The loading
|
||
screens have no oracle capture — the Decoder records them as unreachable from the
|
||
title path — and `verify-screen` compares against a renderer that draws no leaves.
|
||
Drawing it would put unadjudicable content on a screen, which is the test
|
||
`ptlogo_eff2` also fails. Behaviour unchanged: `build_12` still renders 59 530 px
|
||
of ink.
|
||
|
||
📌 The two entries in that list were written to different standards. The
|
||
`title_jp/ptlogo_eff2` reason is four paragraphs, states the scale as a *pop*
|
||
(0 → 125 % → 0 over 0.95 s), works through both readings and says why neither can
|
||
be chosen. The `pgloading_loop5` reason was one line and false. Same file, same
|
||
author, same day — the difference is that one was interesting and one was a
|
||
loading screen.
|
||
|
||
### ✅ The measurement in the same paragraph checks out
|
||
|
||
That paragraph also claims *"its max went 155 → 232 when they were drawn"* for
|
||
`title_jp`. Reproduced by adding `ptlogo_eff2` to `draw_leaf_for` and re-running:
|
||
**155 → 231**, with the differing pixels going 20 498 → 58 313. One off a number
|
||
recorded days ago, which is rounding rather than drift.
|
||
|
||
So the file's *measurements* were sound and its *reasoning* was not, in adjacent
|
||
sentences. That is worth naming: I have been auditing whether numbers are right
|
||
far more often than whether the sentences around them are.
|
||
|
||
## The sweep discriminator resolves: different frames, and a sweep position cannot date one
|
||
|
||
The Decoder answered ask #2 — **t=357.7 was never fitted against a PNG.** It comes
|
||
from `title-draw-capture-vertex-colours.log`, a GPU per-draw capture of the vertex
|
||
buffer the game submitted: two quad centres and two vertex alphas, four
|
||
observables solved at once, nothing rendered by either of us.
|
||
|
||
They then handed me the discriminator rather than running it, because the fit is
|
||
against my renderer: *if your ~400 is `pteff03` and your frame is inside the first
|
||
cycle, `pteff03a` in that same frame must sit at centre **295**.*
|
||
|
||
Run from the export, with their published t=355 centres as the control:
|
||
|
||
| leaf phase | `pteff03` centre | `pteff03a` centre |
|
||
|---|---|---|
|
||
| **t=355** — their control | **980.5** (published 981) | **477.7** (published 478) |
|
||
| **t=400** — my fit | 1160.5 | **294.9** |
|
||
|
||
✅ **294.9 against a predicted 295.** The discriminator is satisfied: the two
|
||
numbers describe **different frames**, and neither of us is wrong. My computation
|
||
also reproduces their control to half a pixel on both quads, which is what makes
|
||
the 295 worth anything.
|
||
|
||
### Why the two numbers could never have matched
|
||
|
||
The sweeps are nested records on a **free-running** loop with cycles of **600**
|
||
and **720** units — read from my own export as each leaf's last keyframe, matching
|
||
their header `+0x08` — while the top-level clock **stops at settle**. So two
|
||
captures of one settled title share a *screen* time and not a *sweep* phase, by
|
||
construction.
|
||
|
||
🔴 **The rule to carry: a sweep position cannot date a frame.** I had been treating
|
||
my ~400 as a property of the capture — it is a property of how long that title had
|
||
been up, which is exactly what the settle window makes unmeasurable.
|
||
|
||
And the two are not comparable in kind. Their 357.7 is a **joint** fit where both
|
||
leaves agree; mine poses one phase. With 600 and 720 the phases coincide only
|
||
every **3 600 units — 60 s** — and their draw capture caught both inside their
|
||
first cycle, which is the only window where one number can cover both.
|
||
|
||
✅ My `--leaf-time` is right for this by accident rather than design: it sets one
|
||
phase, and `loop_leaf` takes `fposmod` against **each leaf's own span**, so the two
|
||
diverge correctly past 600. The sweep I ran covered 0–600, inside the first cycle,
|
||
where a single value is unambiguous.
|
||
|
||
### Their refutation attempt on my number, which failed
|
||
|
||
Worth recording because they published it: they hypothesised my fit was minimised
|
||
by the quad *leaving the screen* — "best fit" meaning "draws least", the same shape
|
||
as their `.tbm` control that could not fail. At t=400 quad B is **fully** on screen
|
||
and quad A is 319 of 400 px wide. The number is fitting something present, and it
|
||
survives.
|
||
|
||
## Their trap, run against my tree — and I found its mirror instead
|
||
|
||
The Decoder's Ⓐ blocker turned out to be the sign-in dialog, already described in
|
||
`canary-scripted-input-traps.md` §3 and in `boot_menu.sh`'s header. Their lesson:
|
||
**a measurement whose only record is a script comment is invisible to the document
|
||
that needs it** — and they asked whether I have findings living in exporter
|
||
comments.
|
||
|
||
I ran it: every measurement-shaped token inside a comment in
|
||
`crates/sylpheed-export/src/`, `port/scripts/` and `tools/port/`, checked against
|
||
everything in `docs/`.
|
||
|
||
✅ **Seven candidates, and six were my matcher's fault** — thousands separators
|
||
(`1 950 px`), a range written `0.70-0.82 %` in one place and `0.1355 %` in the
|
||
other, precision differing between `9.1` and `9.14`. The findings themselves are
|
||
all in `DECISIONS.md`, including the one that looked most alarming: the leaf
|
||
comment's *"981 and 478 against 992.0 and 467.2 measured off the capture"* is
|
||
recorded, and the **11.5 px** residual has its own paragraph saying it is not to
|
||
be fitted.
|
||
|
||
### 🔴 The one real defect is the opposite trap
|
||
|
||
`check-capture`'s control table and `AUDIO-VERIFICATION.md` both record the voice
|
||
control, and they had **drifted**: **53.3 %** in the tool, **53.2 %** in the doc,
|
||
twice each.
|
||
|
||
Neither can be re-measured — that control file was transient and is gone — so
|
||
there is no way to say which copy aged. The Decoder lost a finding because it had
|
||
**one** record, in a script comment. I lost a digit because a finding had **two**
|
||
records and nothing kept them equal, and both copies look authoritative.
|
||
|
||
Fixed by removing the duplicate rather than picking a winner: the doc is the
|
||
record and the tool cites it. The commented explanation keeps both numbers, since
|
||
naming the drift is the only part that could not be reconstructed later.
|
||
|
||
### ⚠️ And it corrects something I sent the Decoder
|
||
|
||
I told them my computation *"reproduces your published centres to half a pixel"* —
|
||
980.5/477.7 against 981/478. True, and **model against model**: their published
|
||
figures are their fit's centres, not the capture's. Against the **capture** this
|
||
corpus already records 992.0 and 467.2, an **11.5 px** residual, in a paragraph
|
||
that says it is not to be fitted.
|
||
|
||
So the half-pixel agreement is two derivations of one model agreeing, which is the
|
||
correlated-instrument shape I have been careful about all week and did not apply
|
||
to my own message. The discriminator result is unaffected — 294.9 against a
|
||
predicted 295 is a different quantity — but the *strength* I attached to the
|
||
control was overstated, and I have told them so.
|
||
|
||
## The plate pulses — measured, and the port was wrong on the boot's end state
|
||
|
||
Ask #1 came back the other way. Held at the title with **no input**, the plate
|
||
oscillates continuously — two windows in one boot, ~23 cycles each, no decay, no
|
||
settling — and 🔴 **it never goes off**: the plate-absent floor is **159**
|
||
thresholded green pixels and the pulse bottoms at **714**, four and a half times
|
||
that.
|
||
|
||
That reading is what makes the mechanism recoverable. A glow alone cannot do it:
|
||
`ptbtn00f` ramps 0 → 80 → 0. A **steady base plus a pulsing glow** can, and 714 and
|
||
1520 are exactly base-only and base-plus-glow.
|
||
|
||
And the port already had the base right. `ptbtn00`'s fade to 0 at t=244 is its
|
||
**exit** ramp, which plays when the screen leaves; while the screen is *held* the
|
||
base sits at its own hold, alpha 255 at t=238. What was missing was the glow.
|
||
|
||
### The defect was in the renderer, not in the deleted entry
|
||
|
||
`_draw` treated a **looping** record like a **focused** one — drawing it *instead
|
||
of* the base sprite. For a focused button that is right and measured (the focused
|
||
sprite covers the base at 100 % of base-visible pixels). For the plate it
|
||
substituted a dim glow for a bright sprite, which is why the plate vanished and
|
||
why I deleted the authored entry on 2026-08-29.
|
||
|
||
I deleted the right thing for the wrong reason. The entry was correct; the branch
|
||
it landed in was not. A looping record now draws **over** the base, and the entry
|
||
is restored with the measurement behind it.
|
||
|
||
| | |
|
||
|---|---|
|
||
| plate region, base only | **95.68** |
|
||
| base + glow at peak | **115.52** |
|
||
| measured period over 10 cycles | **1.980 s** |
|
||
| declared 120 units at 60 u/s | 2.000 s — **ratio 0.990** |
|
||
|
||
✅ The pulse appears on the **boot** path, where the overlay runs on the shared
|
||
clock. It does *not* appear under `--screen=title --overlay=press_start`, because
|
||
a static overlay poses at its own arrival — my own earlier fix — so its clock never
|
||
advances. That is correct for a still frame and worth knowing before anyone reads
|
||
a flat plate there as a regression.
|
||
|
||
### What is authored, and what is refused
|
||
|
||
**120 units, not seconds**, on the Decoder's own instruction: their run measured
|
||
2.530 and 2.540 s and an earlier corpus run 2.24 s — one declared number through
|
||
two emulator pacings (×1.27 and ×1.12 against a nominal 2.000 s). Hardcoding 2.5 s
|
||
would author a loaded container's clock.
|
||
|
||
⚠️ Their three limits are carried into `authored/timing.json` verbatim: **one
|
||
boot**, two windows inside it are not two boots; it **does not distinguish** the
|
||
boot title from an attract-loop title, since run 1 opens at t≈255 s against Q9's
|
||
~193 s baseline; and 🔴 **714/1520 is not an alpha ratio** — the counter is
|
||
thresholded pixels, so no duty cycle or ramp shape may be read from it. The port
|
||
draws the record's own declared ramp and infers nothing about its shape.
|
||
|
||
Oracle rows unmoved; every asserting check passes.
|