# 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. --- 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:`, 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) ## 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///.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 `/../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= --overlay=`** 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_.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 `/`, 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=` 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:`, 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=` 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.