Their corrected least-squares fit gives 4.287 and -4.348 px/frame against my declared 4.000 and 4.063 px/unit: 1.072 and 1.070 units/frame, where Q1 establishes 2 for top-level elements. My port drives everything from one clock -- boot.gd:375, time_units += delta * units_per_second at 60 units/s -- and the leaf path reads that same clock. So the title's sweeps cross in 10.0 s where the game takes 18.7, a visible 1.87x defect in what the boot ships. Not changed, and not only out of caution: keyframe_units_per_second is authored from a measurement and governs build-in, transitions and the plate. Changing it globally breaks the top-level timing Q1 measured; a leaf-only clock is a claim about how the game works rather than about my renderer. That is a Q1 sub-question and theirs. Refutation attempt on their strongest argument: two strips agreeing to three significant figures constrains the strips to EACH OTHER, not the absolute rate. Both ratios come from one capture under one fps assumption, so a systematic error scales both identically and the agreement survives. Their own untested candidate -- 1 unit per 1/30 s at 28.5 fps gives 1.053 -- sits within that uncertainty of 1.070. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
10483 lines
558 KiB
Markdown
10483 lines
558 KiB
Markdown
# Decisions
|
||
|
||
One entry per decision that outlives the container it was made in. Newest last.
|
||
A decision that lives only in an agent's context is lost when that container
|
||
dies, which is what this file is for.
|
||
|
||
---
|
||
|
||
|
||
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
|
||
|
||
184 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 [refuted]". The splash dwells are declared, and the port was already playing them](#withdrawn--the-boot-is-known-too-fast-refuted-the-splash-dwells-are-declared-and-the-port-was-already-playing-them)
|
||
* [Refutation attempt — their two splash boundaries are not anchored the same way](#refutation-attempt--their-two-splash-boundaries-are-not-anchored-the-same-way)
|
||
* [Their corrected boundaries check out against the file — all six, exactly](#their-corrected-boundaries-check-out-against-the-file--all-six-exactly)
|
||
* [The n=1 disclosure, and the one port constant that rests on a single run](#the-n1-disclosure-and-the-one-port-constant-that-rests-on-a-single-run)
|
||
* [P6 gate — sound on the P5 walk, verified, and the tool I nearly shipped instead](#p6-gate--sound-on-the-p5-walk-verified-and-the-tool-i-nearly-shipped-instead)
|
||
* [Their `.tbm` self-refutation does not reach this archive — and it fixes my guard anyway](#their-tbm-self-refutation-does-not-reach-this-archive--and-it-fixes-my-guard-anyway)
|
||
* [Coverage is now tested per instant, because scale animates](#coverage-is-now-tested-per-instant-because-scale-animates)
|
||
* [P7 gate — the new-game intro plays and returns, and a defect I nearly invented](#p7-gate--the-new-game-intro-plays-and-returns-and-a-defect-i-nearly-invented)
|
||
* [`ScreenView.skipped` was correct and unread since P1 — now it says so itself](#screenviewskipped-was-correct-and-unread-since-p1--now-it-says-so-itself)
|
||
* [Refutation attempt — "the element declared first paints first"](#refutation-attempt--the-element-declared-first-paints-first)
|
||
* [The menu bed plays under the cutscene, nobody decided that, and it stays](#the-menu-bed-plays-under-the-cutscene-nobody-decided-that-and-it-stays)
|
||
* [`wait:<seconds>`, and the bed's loop seam is 3.4 seconds of silence](#waitseconds-and-the-beds-loop-seam-is-34-seconds-of-silence)
|
||
* [Two harness bugs, and the defect the second one was hiding](#two-harness-bugs-and-the-defect-the-second-one-was-hiding)
|
||
* [The `PRESS Ⓐ` plate: four bugs in a row, and a number I have been misquoting](#the-press--plate-four-bugs-in-a-row-and-a-number-i-have-been-misquoting)
|
||
* [The title's residual is the sweep phase, and the sweeps fit at ~400 units, not 357.7](#the-titles-residual-is-the-sweep-phase-and-the-sweeps-fit-at-400-units-not-3577)
|
||
* [A second capture closes the sweep-geometry question, and the plate matches at 0.00093 %](#a-second-capture-closes-the-sweep-geometry-question-and-the-plate-matches-at-000093)
|
||
* [`--focus=` did nothing on the menu path, and the corpus had an untested focus capture](#--focus-did-nothing-on-the-menu-path-and-the-corpus-had-an-untested-focus-capture)
|
||
* [The last unused capture, placed — and its residual is the oracle's, not the port's](#the-last-unused-capture-placed--and-its-residual-is-the-oracles-not-the-ports)
|
||
* [`MODDING.md` had five rules and no check. Now it has one, and all five pass](#moddingmd-had-five-rules-and-no-check-now-it-has-one-and-all-five-pass)
|
||
* [Five authored values had no reader — including the one I asked for measurements into](#five-authored-values-had-no-reader--including-the-one-i-asked-for-measurements-into)
|
||
* [`FORMAT.md` declared the port's own export invalid, and a failed export is not atomic](#formatmd-declared-the-ports-own-export-invalid-and-a-failed-export-is-not-atomic)
|
||
* [`check-all`, a verdict that ignored its own statistic, and a claim of mine that was wrong](#check-all-a-verdict-that-ignored-its-own-statistic-and-a-claim-of-mine-that-was-wrong)
|
||
* [The `title` disagreement, localised — and the question I filed for it was the wrong one](#the-title-disagreement-localised--and-the-question-i-filed-for-it-was-the-wrong-one)
|
||
* [Auditing `BLOCKED.md` found three stale rows, and the undated ones were all three](#auditing-blockedmd-found-three-stale-rows-and-the-undated-ones-were-all-three)
|
||
* [The record already answered last iteration's question, under headings that name it](#the-record-already-answered-last-iterations-question-under-headings-that-name-it)
|
||
* [🔴 Twenty-one messages to a dead address, each one warning me it was dead](#twenty-one-messages-to-a-dead-address-each-one-warning-me-it-was-dead)
|
||
* [The forced-backdrop pass is load-bearing on two screens, not six](#the-forced-backdrop-pass-is-load-bearing-on-two-screens-not-six)
|
||
* [Re-running the Decoder's necessity census: every figure reproduces, and what that is worth](#re-running-the-decoders-necessity-census-every-figure-reproduces-and-what-that-is-worth)
|
||
* [A second witness for the pixel-cost claim, from a different renderer](#a-second-witness-for-the-pixel-cost-claim-from-a-different-renderer)
|
||
* [Reconciling the two ink figures, and what "has its own key" is resting on](#reconciling-the-two-ink-figures-and-what-has-its-own-key-is-resting-on)
|
||
* [Not one of the 80 has a decoded key — and the port's four are the rule's oracle check](#not-one-of-the-80-has-a-decoded-key--and-the-ports-four-are-the-rules-oracle-check)
|
||
* [A withholding reason that was false, and the measurement beside it that was not](#a-withholding-reason-that-was-false-and-the-measurement-beside-it-that-was-not)
|
||
* [The sweep discriminator resolves: different frames, and a sweep position cannot date one](#the-sweep-discriminator-resolves-different-frames-and-a-sweep-position-cannot-date-one)
|
||
* [Their trap, run against my tree — and I found its mirror instead](#their-trap-run-against-my-tree--and-i-found-its-mirror-instead)
|
||
* [The plate pulses — measured, and the port was wrong on the boot's end state](#the-plate-pulses--measured-and-the-port-was-wrong-on-the-boots-end-state)
|
||
* [A static overlay now advances, and a refutation attempt on the pulse floor](#a-static-overlay-now-advances-and-a-refutation-attempt-on-the-pulse-floor)
|
||
* [Their pulse floor reproduces exactly once the predicate is named — 159, to the pixel](#their-pulse-floor-reproduces-exactly-once-the-predicate-is-named--159-to-the-pixel)
|
||
* [My rendered pulse, counted in their units — and #4 refutes the voice value without fixing it](#my-rendered-pulse-counted-in-their-units--and-4-refutes-the-voice-value-without-fixing-it)
|
||
* [The voice export now carries every qualifying stream — and a unity sum was refused by our own check](#the-voice-export-now-carries-every-qualifying-stream--and-a-unity-sum-was-refused-by-our-own-check)
|
||
* [Their stream assignment does not fit my region — weights NOT applied](#their-stream-assignment-does-not-fit-my-region--weights-not-applied)
|
||
* [The resolver starts late, and my "duplicate tail" was a real stream all along](#the-resolver-starts-late-and-my-duplicate-tail-was-a-real-stream-all-along)
|
||
* [The export knew the voice was incomplete; the runtime did not say so](#the-export-knew-the-voice-was-incomplete-the-runtime-did-not-say-so)
|
||
* [The voice export is complete — new pin, and the cause was a "within one bank" cap](#the-voice-export-is-complete--new-pin-and-the-cause-was-a-within-one-bank-cap)
|
||
* [The positional weights are applied — keyed by byte size, so the key is a check](#the-positional-weights-are-applied--keyed-by-byte-size-so-the-key-is-a-check)
|
||
* [🔴 Unexplained: `verify-menu-audio`'s dead-press check has started failing](#unexplained-verify-menu-audios-dead-press-check-has-started-failing)
|
||
* [External ground truth for every three-chunk region — the movies' own durations](#external-ground-truth-for-every-three-chunk-region--the-movies-own-durations)
|
||
* [The menu bed loops at 61.93 s — and my 3.4 s "ugly seam" was mine, not the game's](#the-menu-bed-loops-at-6193-s--and-my-34-s-ugly-seam-was-mine-not-the-games)
|
||
* [The dead-press check was passing by luck, and the luck ran out](#the-dead-press-check-was-passing-by-luck-and-the-luck-ran-out)
|
||
* [Independent confirmation of the 1.5 MB cap — the mechanism, not just the conclusion](#independent-confirmation-of-the-15-mb-cap--the-mechanism-not-just-the-conclusion)
|
||
* [The loop is a runtime field, the two readings conflict, and the port keeps what it shipped](#the-loop-is-a-runtime-field-the-two-readings-conflict-and-the-port-keeps-what-it-shipped)
|
||
* [The duration is confirmed and the window is wrong — and the start is now a visible field](#the-duration-is-confirmed-and-the-window-is-wrong--and-the-start-is-now-a-visible-field)
|
||
* [The loop window is measured — `-ss 9.44 -t 61.87` — and the near-silence count tracked the error](#the-loop-window-is-measured---ss-944--t-6187--and-the-near-silence-count-tracked-the-error)
|
||
* [Applying "grep the corpus for the claim" to my own corpus](#applying-grep-the-corpus-for-the-claim-to-my-own-corpus)
|
||
* [A refuted-claim register, because the audit found what the audit found](#a-refuted-claim-register-because-the-audit-found-what-the-audit-found)
|
||
* [State of the port, and a claim I built on for a week without checking](#state-of-the-port-and-a-claim-i-built-on-for-a-week-without-checking)
|
||
* [Identifying their submenu capture: edges where intensity could not](#identifying-their-submenu-capture-edges-where-intensity-could-not)
|
||
* [`on_cancel`: one half measured, and a MEASURED stamp removed from the other](#on_cancel-one-half-measured-and-a-measured-stamp-removed-from-the-other)
|
||
* [BLOCKED.md's five "blocking" rows were all answered, some days ago](#blockedmds-five-blocking-rows-were-all-answered-some-days-ago)
|
||
* [The plate came back in the game and not in the port](#the-plate-came-back-in-the-game-and-not-in-the-port)
|
||
* [🔴 `verify-screen` was nondeterministic, and it looked fine most of the time](#verify-screen-was-nondeterministic-and-it-looked-fine-most-of-the-time)
|
||
* [The JP title capture adjudicates `title_jp` — and it goes against the port](#the-jp-title-capture-adjudicates-title_jp--and-it-goes-against-the-port)
|
||
* [🔴 CORRECTION: the port did not move away from the game — I scored the wrong frame](#correction-the-port-did-not-move-away-from-the-game--i-scored-the-wrong-frame)
|
||
* [The `rest()` flash defect reaches four screens I ship — and the port already survives it](#the-rest-flash-defect-reaches-four-screens-i-ship--and-the-port-already-survives-it)
|
||
* [Correction: those two are the *sound* path, which makes the rule stronger](#correction-those-two-are-the-sound-path-which-makes-the-rule-stronger)
|
||
* [The two loading screens are no longer black, and it was the paint order](#the-two-loading-screens-are-no-longer-black-and-it-was-the-paint-order)
|
||
* [Adjudicating the Decoder's `rest()` replacement against the game](#adjudicating-the-decoders-rest-replacement-against-the-game)
|
||
* [The boot's own end frame, scored against the game for the first time](#the-boots-own-end-frame-scored-against-the-game-for-the-first-time)
|
||
* [Refutation attempt: the settle-instant candidate is **not** uniformly better](#refutation-attempt-the-settle-instant-candidate-is-not-uniformly-better)
|
||
* [My own predictor holds — and the evidence that made it *better than width* does not](#my-own-predictor-holds--and-the-evidence-that-made-it-better-than-width-does-not)
|
||
* [Checking my own tree for the ordinal foot-gun that just voided three of theirs](#checking-my-own-tree-for-the-ordinal-foot-gun-that-just-voided-three-of-theirs)
|
||
* [Looking for a case that separates width from mid-ramp — there is none, and I nearly invented one](#looking-for-a-case-that-separates-width-from-mid-ramp--there-is-none-and-i-nearly-invented-one)
|
||
* [Auditing my tree for the disc-wide ordinal foot-gun](#auditing-my-tree-for-the-disc-wide-ordinal-foot-gun)
|
||
* [Their withdrawn "~14 units of black hold" — my authored 9 survives it](#their-withdrawn-14-units-of-black-hold--my-authored-9-survives-it)
|
||
* [🔴 CORRECTION: my 18-vs-19 "agreement" compared two different intervals](#correction-my-18-vs-19-agreement-compared-two-different-intervals)
|
||
* [`check-all` passes — after an hour-long hang that was the suite's own fault](#check-all-passes--after-an-hour-long-hang-that-was-the-suites-own-fault)
|
||
* [Ⓐ and Ⓑ are not the same shape, and my `black_hold` treats them as if they were](#and--are-not-the-same-shape-and-my-black_hold-treats-them-as-if-they-were)
|
||
* [🔴 `check-all` excused two failing rows with a reason that is measurably false](#check-all-excused-two-failing-rows-with-a-reason-that-is-measurably-false)
|
||
* [`black_hold_units` 9 → 0, and why not the value that fits best](#black_hold_units-9--0-and-why-not-the-value-that-fits-best)
|
||
* ["Already up to date" is not evidence that I am current](#already-up-to-date-is-not-evidence-that-i-am-current)
|
||
* [Re-deriving `black_hold_units` against four measurements, not three](#re-deriving-black_hold_units-against-four-measurements-not-three)
|
||
* [🔴 CORRECTION: my "the eras render identically" measurement was void](#correction-my-the-eras-render-identically-measurement-was-void)
|
||
* [🔴 CORRECTION: my branch *is* the stale era, and the reference binary was never the workspace build](#correction-my-branch-is-the-stale-era-and-the-reference-binary-was-never-the-workspace-build)
|
||
* [`exit_ramp_units`: the refuted constant was living in a default](#exit_ramp_units-the-refuted-constant-was-living-in-a-default)
|
||
* [Auditing the whole tree for "a deleted value that something still supplies"](#auditing-the-whole-tree-for-a-deleted-value-that-something-still-supplies)
|
||
* [Counting the fallbacks instead of inspecting them — and one I had misjudged](#counting-the-fallbacks-instead-of-inspecting-them--and-one-i-had-misjudged)
|
||
* [The oracle harness was nondeterministic, and I quoted its numbers for a dozen iterations](#the-oracle-harness-was-nondeterministic-and-i-quoted-its-numbers-for-a-dozen-iterations)
|
||
* [Answering "an unenumerated set" — don't enumerate, test](#answering-an-unenumerated-set--dont-enumerate-test)
|
||
* [🔴 The third clock was in my own list, and I did not wire it](#the-third-clock-was-in-my-own-list-and-i-did-not-wire-it)
|
||
* [The leaf phase was an arbitrary choice; the capture turns out to determine it](#the-leaf-phase-was-an-arbitrary-choice-the-capture-turns-out-to-determine-it)
|
||
* [Cross-checking their leaf reading against my export — it reconciles](#cross-checking-their-leaf-reading-against-my-export--it-reconciles)
|
||
* [Replicating the phase result on the title — it fails, and the failure is the finding](#replicating-the-phase-result-on-the-title--it-fails-and-the-failure-is-the-finding)
|
||
* [Their masking rule, implemented — and it does not transfer to my screens](#their-masking-rule-implemented--and-it-does-not-transfer-to-my-screens)
|
||
* [Their "the game may not draw these leaves" hypothesis — my curves say *sometimes*](#their-the-game-may-not-draw-these-leaves-hypothesis--my-curves-say-sometimes)
|
||
* [Using the clean splash rows to measure the tone curve — and repeating a documented mistake](#using-the-clean-splash-rows-to-measure-the-tone-curve--and-repeating-a-documented-mistake)
|
||
* [Localising the 1.92 splash floor: it is glyph edges, and off them the port is ~1 RMSE from the game](#localising-the-192-splash-floor-it-is-glyph-edges-and-off-them-the-port-is-1-rmse-from-the-game)
|
||
* [Their draw-stream result checked against my export — three confirmations and one correction](#their-draw-stream-result-checked-against-my-export--three-confirmations-and-one-correction)
|
||
* [Nested leaves may advance at half rate — a quantified defect in shipped output](#nested-leaves-may-advance-at-half-rate--a-quantified-defect-in-shipped-output)
|
||
|
||
<!-- /INDEX -->
|
||
## P0 — the exporter, 2026-08-28
|
||
|
||
### The exporter reads one authored file, and stamps its provenance into the output
|
||
|
||
`export/` is derived and `authored/` is hand-written, and the natural reading of
|
||
that is that the exporter never touches `authored/`. But a screen has to be
|
||
*called* something, and the disc does not name its builds — the identification of
|
||
build 5 as the main menu is HANDOFF Q2, **measured against a live capture**, not
|
||
a field.
|
||
|
||
Two ways to handle that:
|
||
|
||
1. the exporter emits `build_05.json` and the runtime renames it from
|
||
`authored/screen_names.json`;
|
||
2. the exporter reads that map and writes `main_menu.json` directly.
|
||
|
||
Chose **2**, with a condition: every name it applies carries `name_source:
|
||
"authored"` and a `name_why` quoting the evidence, and `check` **rejects** an
|
||
authored name with no `why`. The file that lands in `export/` is therefore still
|
||
honest about which of its fields is a measurement — which is the property the
|
||
derived/authored split exists to protect — while a human opening the tree sees
|
||
`main_menu.json` rather than having to resolve a rename in their head. A build
|
||
nobody has identified exports as `build_NN` with `name_source: "index"`, which is
|
||
a locator and not a claim.
|
||
|
||
This is the **only** authored input the exporter takes. Everything else in
|
||
`authored/` is applied by the runtime over `export/`.
|
||
|
||
### Sprites are per screen, not a flat pool
|
||
|
||
`main_menu` and `extras` both ship a `ptbase.t32` and they are different
|
||
pictures. A flat `sprites/` directory would have silently collided; whichever
|
||
screen exported second would have won, and the loser would have drawn the wrong
|
||
background with no error anywhere. `sprites/<subdir>/<screen>/<name>.png`.
|
||
|
||
### The format is executable
|
||
|
||
`sylpheed-export check --out export` validates a tree against `docs/FORMAT.md`
|
||
with no disc in hand. It exists because "the export is correct" is otherwise an
|
||
assertion, and because the P0 gate is *"validates against FORMAT.md"* — which is
|
||
not a thing anyone can confirm by reading.
|
||
|
||
It reads the tree the way Godot will: as a stranger, with no access to the disc,
|
||
the decoders, or the exporter's internals. It deliberately does **not** check the
|
||
export against the disc — that is what `sylpheed-cli screen render` is for, at P1.
|
||
|
||
Checked that it bites, rather than assuming: five mutations of a valid
|
||
`main_menu.json` — a broken `paint_order` permutation, a dangling
|
||
`focus_sprite`, a reversed `buttons` list, a `#rrggbbaa` colour, an invented
|
||
`name_source` — are each caught with a specific message.
|
||
|
||
### The highlight sprite pairs by name; `opt ` is exported but not believed
|
||
|
||
FORMAT v1 said `focus_sprite` came from the element's `opt ` link. That reading
|
||
was **measured and refuted** by the RE agent, and this export shows why plainly:
|
||
on the main menu, `opt ` chains `ptloop01 → ptloop02 → ptbtn01` — two decorations
|
||
and then a button. It is a linked list of something, and it is not focus.
|
||
|
||
The highlight is paired by **sprite name** instead (`ptbtn01.t32` ↔
|
||
`ptbtn01f.t32`), which is HANDOFF's convention and holds for all 54 real pairs on
|
||
the disc. It resolves all five main-menu buttons. The raw link is still exported
|
||
as `opt_link`, renamed so that nothing downstream mistakes it for navigation, and
|
||
so that whoever eventually decodes it has the data.
|
||
|
||
Note this is 🟡 a naming convention, not a decoded field. It is authored in
|
||
effect, and lives in the exporter only because it is a rule over disc data rather
|
||
than a value we chose.
|
||
|
||
### The paint order is exported, not authored
|
||
|
||
Q3 decoded it — a `u16` layer key at `+0x0A` of each `T8aD` sprite header,
|
||
stable-sorted with declaration index. So it is read in the exporter, per the
|
||
contract's own rule for a decoded answer, and `paint_order` in `export/` is a
|
||
derived field. `"paint_order"` is gone from `unresolved`; **`paint_order_ties`
|
||
replaces it**, because the tie-break is still unknown and costs one element's
|
||
blend on one screen.
|
||
|
||
Where an element has no `T8aD` header the key comes from the decoders' table of
|
||
keys **measured off the running game**. That is a different kind of fact, so it
|
||
is labelled: `layer_source` is `"sprite"`, `"implied"` or `"none"`, and a
|
||
consumer that needs to know whether a layer is read or measured can tell.
|
||
|
||
### Colours are exported as two fields with the byte order in the name
|
||
|
||
There are two modulate colours and they multiply: `tint` is RGBA, `fade` is
|
||
**ARGB** and its high byte is the alpha that ramps. v1's single `"#ffffffff"`
|
||
could not carry both and silently discarded the ramping alpha. They are exported
|
||
as `tint_rgba` and `fade_argb`, raw hex, byte order in the key — because getting
|
||
it backwards is silent and looks like an art bug rather than a parse bug.
|
||
|
||
### `t` stays raw
|
||
|
||
HANDOFF Q1 is answered — linear ramp, 2 units per rendered frame, working
|
||
conversion 1 unit = 1/60 s — but that conversion is **measured off the running
|
||
game, not read from the file**, and the finding itself flags the 27.6 present-
|
||
frames/second measurement as the part worth re-testing. If the game turns out to
|
||
present at 60 Hz, every duration halves.
|
||
|
||
So `t` is exported exactly as the disc spells it, `keyframe_time_unit` stays in
|
||
`unresolved`, and the conversion will live in one authored place at P2. One
|
||
constant to change, in a file that says it is a decision.
|
||
|
||
### The final keyframe has no `t`, and `check` enforces that
|
||
|
||
The disc has no time slot on the last keyframe of a group. A file that carries
|
||
one there has invented it. `check` rejects it — this is the one place where the
|
||
temptation to emit a plausible number is strongest and the resulting error is
|
||
completely invisible.
|
||
|
||
---
|
||
|
||
## P1 — Godot draws the screen, 2026-08-28
|
||
|
||
### The Godot side reads the manifest, not a path
|
||
|
||
`ExportTree` is the only class that knows where `export/` is: `SYLPHEED_EXPORT`
|
||
if set, otherwise `<project>/../export`. Screens are addressed by their manifest
|
||
**name** (`main_menu`), never by a file path, so the runtime never encodes the
|
||
archive's subdirectory and a re-export that moves a file does not break it. It
|
||
also checks `format` on both the manifest and each screen, and refuses a tree it
|
||
was not built to read rather than half-drawing one.
|
||
|
||
Textures are read as bytes and decoded with `load_png_from_buffer` at runtime.
|
||
They are deliberately **not** Godot-imported resources: `export/` is gitignored
|
||
and regenerated wholesale, and a `.import` sidecar per sprite would be derived
|
||
state living next to derived state, invalidated on every re-export.
|
||
|
||
### One CanvasItem draws the whole screen
|
||
|
||
`ScreenView._draw` walks `paint_order` and draws each element itself, rather
|
||
than making a node per element and leaning on `z_index`. The export's
|
||
`paint_order` is already back-to-front, so honouring it is a loop; expressing
|
||
the same order through sixteen nodes' z-indices would hide the one thing that is
|
||
still unresolved about it — the **ties** — behind Godot's own sibling rules,
|
||
where a change in the export would silently become a change in Godot's tree
|
||
order instead of a visible change in the draw sequence.
|
||
|
||
### P1 draws `rest` and nothing else
|
||
|
||
Every element is drawn at its resting pose. No keyframe interpolation: that is
|
||
P2, and it depends on the keyframe time unit, which is **measured** rather than
|
||
decoded. A milestone whose gate is a pixel diff must not have a measured
|
||
constant inside it, or the diff stops being evidence about the port.
|
||
|
||
For the same reason `focused_id` is empty at P1. Initial focus was measured as
|
||
unstable boot to boot (HANDOFF Q5), so choosing one is an authored decision and
|
||
it belongs to P5, where a human is pressing keys.
|
||
|
||
### Nearest-neighbour, and why that is not a preference
|
||
|
||
`TEXTURE_FILTER_NEAREST`. The export is a 1:1 copy of the disc's own texels and
|
||
elements draw at up to 500 %; a bilinear filter invents detail the disc does not
|
||
have. It is also what the reference renderer does — `ui_layout::blit` maps
|
||
destination to source by integer division — so a filter difference cannot
|
||
masquerade as a placement difference in the diff.
|
||
|
||
### The capture is the SubViewport, not the window
|
||
|
||
The screen is drawn into a `SubViewport` sized to the export's own `design`
|
||
rectangle and shown through a container that scales it to the window. The first
|
||
attempt captured `get_viewport()` and got **1235×695**: there is a window manager
|
||
on the Xvfb display and its title bar had eaten 45×25 px of a screen the export
|
||
declares as 1280×720. A gate that compares a rescaled 1235×695 capture against a
|
||
1280×720 composite measures the compositor.
|
||
|
||
So `--capture` grabs the SubViewport texture: exactly the design rectangle,
|
||
independent of the window, directly comparable with `screen render` with no crop
|
||
and no resample. The windowed run is still worth doing — it is what proves a
|
||
human sees the screen — but it is not what the numbers come from.
|
||
|
||
## P1 gate — the diff, and what it found
|
||
|
||
`tools/verify-screen` renders every screen in the manifest both ways and reports
|
||
the largest per-channel difference anywhere in the frame. Both renderers are held
|
||
to the same inputs: the reference CLI built by `build-reference-cli` from the
|
||
revision the exporter is **pinned** to (not `/reborn/target/`, which is a live
|
||
mount that moves mid-iteration), `--black` because the screen carries its own
|
||
background, and `--primitives --animated` because those are what make the CLI
|
||
draw the same element set the port draws at rest.
|
||
|
||
| screen | build | max per-channel Δ | |
|
||
|---|---|---|---|
|
||
| `main_menu` | 5 | **3** | the P0/P1 gate screen |
|
||
| `main_menu_jp` | 8 | 3 | |
|
||
| `extras` / `extras_jp` | 6 / 9 | 4 / 3 | |
|
||
| `press_start` / `press_start_jp` | 2 / 3 | 1 | |
|
||
| `build_00` / `build_01` | 0 / 1 | 3 | |
|
||
| `build_10` / `build_11` | 10 / 11 | **0** | byte-identical |
|
||
| `title` | 4 | 6 | paint-order tie, below |
|
||
| `title_jp` | 7 | 154 | sampling phase, below |
|
||
|
||
`main_menu` — the milestone's own gate — agrees to **≤3/255 on every channel of
|
||
every pixel**, RMSE 0.38 %, with **no** pixel differing by more than 4 %. 3/255
|
||
is what integer-truncating compositing in the CLI and float rounding on a GPU
|
||
differ by; there is no structural disagreement anywhere in the frame.
|
||
|
||
Three screens exceed that, and each has a named cause rather than a threshold.
|
||
|
||
### `title`: a tie in the paint order — neither renderer is wrong
|
||
|
||
Build 4 is the one screen where the CLI uses a paint order **measured off the
|
||
running game** instead of deriving it. Compared against the order this port
|
||
exports, every single disagreement is **inside a tie** — the two orders differ
|
||
only among elements carrying *identical* layer keys (`0x8083`, the `back2` glow
|
||
group, and `0x80a0`):
|
||
|
||
```
|
||
derived : … 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, …
|
||
measured: … 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, …
|
||
```
|
||
|
||
That is exactly the residual HANDOFF Q3 documents and this export already
|
||
declares in `unresolved: ["paint_order_ties"]`. It is worth stating what it
|
||
costs: **904 px** in the glow band at (445,117)–(1195,313), all of them 4–6/255.
|
||
The port keeps the stable sort, per HANDOFF's own recommendation. Nothing to fix,
|
||
and nothing to tune — a "fix" here would be fitting the port to one screen's
|
||
capture.
|
||
|
||
Two of the reordered indices (`0x80a0`) are `kind & 0x4` template instances that
|
||
both renderers skip, so the only real reorder outside the glow group is
|
||
`ptlogo2` against `ptlogo_tm`, which do not overlap.
|
||
|
||
### `title_jp`: nearest-neighbour sampling phase — the CLI is the one I would call wrong
|
||
|
||
`title_jp` is the **only** screen in the export with a drawn element at a scale
|
||
that is not a whole multiple of 100 %: `ptlogo_eff2` at 125 %. It is also the
|
||
only screen with a difference above 6/255. The two facts are the same fact.
|
||
|
||
At a non-integer ratio the two renderers pick different source texels:
|
||
|
||
* `ui_layout::blit` samples the source at the destination pixel's **top-left
|
||
corner** — `sxi = col * sw / dw`.
|
||
* A GPU samples at the destination pixel's **centre** — `floor((col+0.5)·sw/dw)`.
|
||
|
||
At 125 % those disagree on one column in five, which is why the differing pixels
|
||
are ~30 above 100/255 strung along thin diagonal edges rather than a shifted
|
||
region. At every whole multiple of 100 % they agree exactly, which is why the
|
||
other eleven screens are clean.
|
||
|
||
**Which is wrong:** the CLI, I think. Corner-sampled nearest is a half-
|
||
destination-pixel bias toward the top-left that no rasteriser produces, and the
|
||
Xenon GPU that drew this screen sampled at pixel centres. But I have no
|
||
framebuffer capture of `title_jp` and the disagreement is sub-pixel on one glow,
|
||
so this is a reading, not a measurement — recorded in `docs/BLOCKED.md` rather
|
||
than acted on. **The port is not changing to match**, because matching the CLI
|
||
here would mean deliberately reproducing a half-pixel offset in order to make a
|
||
number smaller.
|
||
|
||
### `extras`: two pixels
|
||
|
||
Two pixels at 4/255. Rounding.
|
||
|
||
### What the diff cannot tell us
|
||
|
||
The pivot question in `docs/BLOCKED.md` predicted that a P1 diff could not
|
||
distinguish "anchor scale to the declared pivot" from "anchor to half the
|
||
texture", because both renderers use the declared pivot. That prediction held:
|
||
the port and the CLI agree on every scaled element, and that agreement is **not
|
||
evidence** about which anchor the game uses. It stays open.
|
||
|
||
### ~~`pteff05.t32` and `pteff04.t32` have no sprite, and that is correct~~
|
||
|
||
**RETRACTED 2026-08-29. This was wrong, and it was the most consequential thing
|
||
on this page.** See "The menu had no background" below.
|
||
|
||
---
|
||
|
||
## P2 — keyframe animation, 2026-08-28
|
||
|
||
### The time unit is authored, in one file, and says loudly that it is not on the disc
|
||
|
||
`authored/timing.json`. HANDOFF Q1 is answered — linear ramp, 2 units per
|
||
rendered frame, 1 unit = 1/60 s — but that conversion is **measured off the
|
||
running game**, not read from a file, which is exactly the case the
|
||
derived/authored split exists for. It is expressed as
|
||
`keyframe_units_per_second: 60` rather than seconds-per-unit so the value is
|
||
exact instead of a repeating decimal, and it carries the two independent lines
|
||
that support it. `t` stays raw everywhere in `export/`; seconds appear only
|
||
where this file is applied, which is one line of `boot.gd`.
|
||
|
||
`exit_ramp_seconds` is deliberately **null**. See below.
|
||
|
||
### The timeline stops at the last *timed* keyframe, and never plays the exit
|
||
|
||
The last keyframe of every group carries **no `t`** — the disc has no time slot
|
||
there. Across this export that final frame is an *exit* pose: for 116 of 134
|
||
elements it differs from the last timed keyframe **in alpha only** (a fade-out),
|
||
for 12 it is the loading splash's scale-and-slide exit, and for 6 it is
|
||
identical (no exit animation at all).
|
||
|
||
So the group is `pre-roll → ramp in → hold → [exit]`, and the port plays it up to
|
||
the hold and stops. Playing into the exit would mean **inventing how long the
|
||
ramp takes**, because the disc does not say. That duration is the screen
|
||
transition — HANDOFF Q7 measured it at ~0.4 s — and it belongs to P3, with its
|
||
own evidence. This is why `exit_ramp_seconds` is null rather than 0.4: P2 has no
|
||
business holding it.
|
||
|
||
### The interpolation is checked by where it lands, not by inspection
|
||
|
||
For **8 of the 12** screens the settled timeline is **byte-identical** to the
|
||
`--pose=rest` render. That is the useful assertion: the port walks the keyframes
|
||
with an authored time unit and arrives, to the pixel, at the pose the pinned
|
||
decoders independently identify as the resting one. `tools/screen-strip` reports
|
||
this per screen, so a change to the interpolation that drifts by one unit shows
|
||
up as a diff rather than as nothing.
|
||
|
||
The four that differ do so for two distinct reasons, below.
|
||
|
||
## `rest` misidentifies six elements, and the running game says so
|
||
|
||
On `main_menu`, the settled timeline and `rest` differ in exactly one region:
|
||
**400×470 at (440,108)** — the bounding box of `ptframe1` and `ptframe2`, and
|
||
nothing else on the screen.
|
||
|
||
`rest` puts both at their **first** keyframe: off-position and fully
|
||
transparent. The keyframes say they slide (620,108)→(440,108) and (403,267)→
|
||
(583,267) while fading 0x00→0xff, and then hold that pose for their last three
|
||
keyframes including the untimed one.
|
||
|
||
`/reborn/docs/re/captures/main-menu-oracle.png`, a capture of the running game,
|
||
**shows them**: the bright circuit-frame bracket around the menu, with a ring at
|
||
the bottom right. Cropping the same 250×180 region from the capture and from
|
||
both renders puts the ring and its elbow trace in the port's timeline render
|
||
**pixel-aligned with the game's**, and absent from the `rest` render. That is
|
||
geometry, not luminance, so it does not depend on the capture's gamma or on the
|
||
fact that it was taken with `NEW GAME` focused.
|
||
|
||
### Why the decoders get it wrong, precisely
|
||
|
||
`ui_layout::rest_plateau` excludes a run of identical keyframes that **ends the
|
||
group**, because that run is normally the exit — the comment cites the pause
|
||
menu, where taking the trailing run erased the word PAUSE. That exclusion is
|
||
right in general and wrong for an element with **no exit animation**, where the
|
||
trailing run *is* the hold. The rule then falls back to an earlier run, which
|
||
for a slide-in is the invisible pre-roll.
|
||
|
||
The condition that identifies the affected elements exactly, with no false
|
||
positives in this export, is:
|
||
|
||
> the final untimed keyframe has the **same pose** as the last timed keyframe
|
||
|
||
Six elements match it and `rest` misses all six: `ptframe1`/`ptframe2` on
|
||
`main_menu` and `main_menu_jp`, and `pteff02` on `title` and `title_jp`. This is
|
||
a **finding for the RE agent** about `sylpheed-formats`, not something this port
|
||
fixes: the decoders are pinned and must not be reimplemented here. The port
|
||
simply does not use `rest` — it derives the arrived pose from the keyframes,
|
||
which needs no heuristic — and `verify-screen` still asks for `--pose=rest` so
|
||
that renderer-vs-renderer diffing compares like with like.
|
||
|
||
Note what this says about P1: the port and the reference renderer **agreed** on
|
||
`main_menu` to 3/255, and both were missing two elements the game draws. Two
|
||
renderers reading the same field through the same decoder agreeing is not
|
||
evidence that the field is right. `docs/BLOCKED.md` had already said that about
|
||
the pivot; here it bit for real.
|
||
|
||
## The title is not settled, and P2 does not claim it
|
||
|
||
`title` and `title_jp` differ between the two modes by much more (max 142 and
|
||
247), and there the disagreement is **not** the six-element bug alone. `rest`
|
||
picks a mid-timeline hold for several glows (`pteff01`, `ptlogoall_eff`,
|
||
`ptlogoall_eff2`, `ptlogo_back2eff5`) where the timeline runs on to a much
|
||
brighter pose.
|
||
|
||
I could not settle which is right, and did not try to make the numbers agree:
|
||
|
||
* No element's alpha ever reverses direction anywhere in this export, so the
|
||
title's 4.48 s timeline is a slow one-way ramp, not a pulse — which removes the
|
||
obvious reason to expect a loop, but does not prove there is none.
|
||
* The only live title capture composites the **`PRESS Ⓐ` plate (build 2) over
|
||
the title (build 4)**, so it cannot be diffed against build 4 alone. Mean
|
||
luminance is oracle 64.1, `rest` 62.8, timeline 80.0 — which looks like it
|
||
favours `rest`, except that the plate *adds* brightness and `rest` is carrying
|
||
a 25 % black dim quad (`pteff02`) that is itself one of the six misidentified
|
||
elements. The comparison is confounded in both directions and settles nothing.
|
||
* **Both modes are visibly wrong anyway.** Side by side with the capture, the
|
||
port draws a washed-out cyan glow slab across the logo that the running game
|
||
does not have — in `rest` mode too. That is a third problem, independent of
|
||
this one, and it is P3's.
|
||
|
||
So: the timeline is the default because it is derived from the disc's own
|
||
keyframes with one measured constant and no heuristic, and because it is proven
|
||
right on the screen this milestone gates. On the title it is **unverified**, and
|
||
P3 should not assume P2 settled it.
|
||
|
||
---
|
||
|
||
## P2, corrected — the pin moved, and the settle rule was wrong, 2026-08-28
|
||
|
||
### Answering the RE agent's question: which six, and on what screens
|
||
|
||
They asked, having found only two elements on the English main menu satisfying
|
||
the condition this port proposed. The six span the whole 12-screen export:
|
||
|
||
| element | screens | trailing run |
|
||
|---|---|---|
|
||
| `ptframe1`, `ptframe2` | `main_menu`, `main_menu_jp` | alpha `0xff` — **visible** |
|
||
| `pteff02` | `title`, `title_jp` | alpha `0x00` — **transparent** |
|
||
|
||
So four of the six are the pair they already found, once per language build, and
|
||
their alpha rule accepts exactly those. The other two are `pteff02`, whose
|
||
trailing run is transparent, so their rule **excludes** it and leaves `rest` at
|
||
`0x40`.
|
||
|
||
**That exclusion is right, and their own measurement proves it.** `pteff02` is
|
||
the 25 % dim quad; they measured the title render going from **+13.14 to +0.55**
|
||
against the plate-free capture once the dim is drawn. `rest` must therefore stay
|
||
at `0x40` and must *not* move to the transparent trailing run — which is what
|
||
their rule does. Two investigations converging from opposite directions.
|
||
|
||
The condition this port proposed was **too loose**; the alpha discriminator is
|
||
the correct rule and the port has no amendment to offer.
|
||
|
||
### The pin moved 8b6dbcf → 5414db3
|
||
|
||
Its own commit, and what I wanted from it is the fixed `ui_layout::rest()`.
|
||
Pinned at `5414db3` rather than `4bc9706` where the fix was written, because
|
||
`5414db3` is where it carries its disc-wide check — 30 of 13 991 elements move,
|
||
4 become visible, **0 become invisible**.
|
||
|
||
The re-export is the evidence the change was contained: **two files changed, and
|
||
within them exactly four `rest` blocks** — `ptframe1`/`ptframe2` on both main
|
||
menus moving from `(620,108)/(403,267)` at `t=16` and alpha `0x00` to
|
||
`(440,108)/(583,267)` at `t=62` and alpha `0xff`. Every diff line pairs; the
|
||
other ten screens are byte-identical, `pteff02` did not move, and no sprite
|
||
changed.
|
||
|
||
### The settle rule was wrong, and their title finding is what showed it
|
||
|
||
P2 shipped "hold the last **timed** keyframe", on the reasoning that the exit is
|
||
the final untimed frame. **That is wrong**, and the title is the counter-example:
|
||
`pteff02` holds at `t=46` with the dim at alpha `0x40` and then ramps to `0x00`
|
||
by `t=236`. The exit is not only the untimed frame — it can be a long run of
|
||
timed ones. Running to the end drops the dim and makes the whole screen ~13/255
|
||
too bright, which is exactly the luminance excess P2 recorded (oracle 64.1,
|
||
`rest` 62.8, timeline 80.0) and could not explain.
|
||
|
||
A group is `pre-roll → ramp in → hold → ramp out → post-roll`, and a screen that
|
||
has arrived sits on **the hold**. So the timeline now plays in and stops at
|
||
`rest`, which is the decoders' identification of that hold and carries its own
|
||
`t`. `settle_units()` is `rest.t`.
|
||
|
||
The check is that the disagreement vanishes: on **all twelve** screens the
|
||
settled timeline is now byte-identical to the `--pose=rest` render, where before
|
||
this change four of them differed by up to 247/255. The timeline's endpoint
|
||
*should* be `rest` — the animation is what the timeline adds, not a different
|
||
destination — so this is the property to want, and it now holds without a
|
||
special case.
|
||
|
||
That also retires P2's open question about looping, from the other side: the RE
|
||
agent measured that groups hold rather than loop (`ptloop01`/`ptloop02` park
|
||
off-screen at x=1521 and x=−839; 18 s of settled title sits at sd ≤ 0.01).
|
||
|
||
## The reference renderer was stale for three diff runs
|
||
|
||
Worth recording as a process failure, because it defeated the project's whole
|
||
verification method for a while and it failed *silently*.
|
||
|
||
After bumping the pin I rebuilt the reference CLI, and `build-reference-cli`
|
||
reported success at rev `5414db3`. `verify-screen` then showed `main_menu`
|
||
jumping from 3/255 to **72/255**. The natural reading — the port had regressed —
|
||
was wrong. The port was right and **the reference was a revision behind**: the
|
||
shared `CARGO_TARGET_DIR` still held a `sylpheed-cli` built from `8b6dbcf`, and
|
||
cargo reported `Finished in 0.13s` and left it in place. Building into a clean
|
||
target directory produced a binary that resolves `ptframe1` to `(440,108) t=62`;
|
||
the shared one still said `(620,108) t=16`.
|
||
|
||
The old check — "does `screen list` run?" — cannot catch this, because a stale
|
||
binary runs perfectly.
|
||
|
||
Two changes:
|
||
|
||
* `build-reference-cli` builds into `$CARGO_TARGET_DIR/reference-cli/$rev`, a
|
||
tree **keyed by the pinned revision**, so a new pin has no artifacts to reuse.
|
||
A stable copy is placed alongside for consumers.
|
||
* It then checks the binary **against `export/`**: both come from the same pin,
|
||
so if the CLI resolves `ptframe1`'s rest differently from what the exporter
|
||
wrote, the two halves of the verification are not the same revision and it
|
||
fails loudly. It compares the two rather than asserting a literal, so it stays
|
||
true when the pin moves again.
|
||
|
||
`docker/bin/` is baked into the image, so this takes effect on the next image
|
||
build; until then the repo copy has to be invoked by path. The RE agent hit the
|
||
same class of trap this session from the other side (`./target/debug` stale
|
||
against a redirected `CARGO_TARGET_DIR`). It is worth naming the general shape:
|
||
**a build system reporting success is not evidence that the artifact you are
|
||
about to trust is the code you pinned.**
|
||
|
||
### What this did not change
|
||
|
||
`title` (6/255), `extras` (4/255) and `title_jp` (154/255) are unchanged, and
|
||
their diagnoses stand — a paint-order tie, two pixels, and nearest-neighbour
|
||
sampling phase at 125 % scale. The title's swoosh defect the RE agent localised
|
||
(drawn thick and white where the game draws it thin and pink) is untouched by
|
||
any of this and remains P3's.
|
||
|
||
---
|
||
|
||
## The menu had no background, and P1 called that correct, 2026-08-29
|
||
|
||
The pin moved `5414db3 → f817dd5` for `56cc7ac`, "a RATC child's name is stated,
|
||
not inferred". `ratc::parse` had named each child by scanning backwards for the
|
||
last printable run of bytes before its magic. For `pteff05.t32` the three
|
||
trailing payload bytes are `38 41 58` — `8AX` — which beat the real name, so the
|
||
child registered under a name no element declares and resolved to no sprite.
|
||
|
||
`pteff05.t32` is the **full-resolution background of all five menu screens**.
|
||
|
||
So every render this port has produced of a menu screen has been missing its
|
||
background, and P1 wrote that up as a property of the disc: *"the bundle declares
|
||
them and carries zero RATC children for either, so there is no texture on the
|
||
disc to export."* That sentence was false. The bundle carries the child; the
|
||
decoder was handing back the wrong name for it. Retracted above rather than
|
||
edited away.
|
||
|
||
### What the re-export shows
|
||
|
||
Six new sprites and nothing else: `pteff05.png` on `main_menu`, `extras` and
|
||
their Japanese twins, `pteff04.png` on both titles. Per screen the JSON gains a
|
||
`sprite` line and `layer_source` moves `"implied" → "sprite"` — the layer key is
|
||
now **read from the file** instead of taken from the decoders' table of keys
|
||
measured off the running game. That is the derived/authored ratchet turning the
|
||
right way, in the exporter rather than in `authored/`.
|
||
|
||
`pteff05.png` is **1280×720**; `ptbase.png`, which had been carrying the
|
||
background alone, is 640×360 drawn at 200 %. The screen was being shown its own
|
||
art at half resolution.
|
||
|
||
### Measured against the live capture, not against the other renderer
|
||
|
||
Whole-frame RMSE of the settled `main_menu` against
|
||
`captures/main-menu-oracle.png`:
|
||
|
||
| | RMSE |
|
||
|---|---|
|
||
| before this pin | 8.05 % |
|
||
| with the real background | **5.92 %** |
|
||
|
||
A 26 % reduction, and it is the right kind of evidence: the reference renderer
|
||
was missing the same element for the same reason, so a renderer-vs-renderer diff
|
||
could not have found this. It is the third time on this project that the
|
||
capture caught something both renderers agreed on — the bracket, the title dim
|
||
quad, and now the background.
|
||
|
||
`verify-screen` after the bump is unchanged in character: everything at 3–4/255
|
||
except `title` (6, the paint-order tie) and `title_jp` (155, the sampling phase).
|
||
Both renderers gained the background together.
|
||
|
||
### One thing the comparison says that I did not expect
|
||
|
||
Rendering with `--focus=ptbtn01`, which is how the capture was taken, makes the
|
||
RMSE **worse** — 5.92 % → 7.00 %. The port *replaces* an element's sprite with
|
||
its `*f` twin; `sylpheed-cli`'s own `--focus` is documented as drawing the
|
||
focused record **over** the base element. Those are different operations, and
|
||
the capture shows a ring marker beside `NEW GAME` that the port does not draw.
|
||
|
||
This is P5's, not P2's, and it is not being guessed at here. Raised in
|
||
`docs/BLOCKED.md`.
|
||
|
||
---
|
||
|
||
## P3 — splash → title, unattended, 2026-08-29
|
||
|
||
### The splash is located by entry index, because no rule can find it
|
||
|
||
The RE agent looked for a content predicate and there is none: design size fails
|
||
(every extra composable bundle sampled is 1280×720, the same as every screen) and
|
||
element count fails (fragments run 2…15 elements in `GP_OPTIONS`/`GP_SAVE_LOAD`
|
||
while the splash halves are 3 and 7 — the ranges overlap).
|
||
|
||
So `screen_builds` is now `is_build` **plus an authored allow-list of entry
|
||
indices**, in `authored/screen_names.json` under `also_export`, each with a `why`
|
||
that says it is a locator and not a claim. This is safe in `GP_TITLE` and would
|
||
not be in general: there, widening adds exactly four bundles and all four are
|
||
real screens with zero fragments. That is why it is an allow-list rather than a
|
||
loosened predicate.
|
||
|
||
**There were two splash screens and the port had neither.** Entries 11/14 are the
|
||
developer logos (GAME ARTS / SETA / studio anima); entries **10/13 are the SQUARE
|
||
ENIX publisher wordmark, the first thing the boot shows**, and nothing in this
|
||
project had noticed them. Both pairs are region twins — ™ on 10, ® on 13 — and
|
||
the port shows one of each, not both.
|
||
|
||
### `authored/screen_names.json` is now keyed by pak entry, not by ordinal
|
||
|
||
Widening the enumeration renumbers the ordinals, and a name that moves when the
|
||
enumeration rule changes is not a name. The file had always called the entry
|
||
"the stronger locator"; it is now the only stable one. In `GP_TITLE` the two
|
||
coincide across all 16 entries, which is also the numbering `sylpheed-cli screen
|
||
--build N --all` takes — so `verify-screen` now passes `--all`, and without it
|
||
`--build 10` would have landed on entry 12.
|
||
|
||
The two previously-unnamed plates therefore renamed `build_10`/`build_11` →
|
||
`build_12`/`build_15`. Their names were always locators; now they locate the
|
||
right thing.
|
||
|
||
### The exit is the group playing itself out, not a black rect over a freeze
|
||
|
||
HANDOFF's answer to ask 2 was (a), and it came with a test that discriminates
|
||
rather than a plausibility argument. Under "a black quad over a frozen screen"
|
||
every region is scaled by the same 1−α, so the button-region / background-region
|
||
brightness **ratio** stays constant through the fade. Measured, it falls
|
||
6.495 → 5.574 → 3.105 → 2.125 → 1.935 — a 3.4× monotonic drop. The screen plays
|
||
out: `pteff00.prm` ramps to opaque black while the labels, `ptmsg`, `pteff10`
|
||
and `pteff12` ramp to transparent, and `ptframe1`/`ptframe2` hold.
|
||
|
||
Implemented by giving the final untimed keyframe a **synthetic time**,
|
||
`exit_ramp_units` after the last timed one, and then interpolating it like any
|
||
other. One code path: the difference between arriving and leaving is only how far
|
||
`t` is allowed to run, not a second kind of animation.
|
||
|
||
`exit_ramp_units = 24` (~0.4 s) is authored, and `authored/timing.json` carries
|
||
the RE agent's own reach caveat rather than smoothing it: the filmstrip is
|
||
downsampled and the button region contains some background, so this pins the
|
||
**direction**, not 0.4 s to ±0.05 s, and it is one transition pair.
|
||
|
||
### Nothing waits on a timer the disc does not carry
|
||
|
||
`dwell` in `authored/flow.json` is deliberately empty. Each screen's dwell is its
|
||
own keyframe group — the publisher wordmark reaches its hold at t=235 (3.92 s),
|
||
the developer logos at t=190 (3.17 s), both read from the disc. Adding a hold on
|
||
top would be inventing a number nobody measured. The pacing you see is the
|
||
disc's own, and the file says where a measured number would go.
|
||
|
||
### The last screen holds
|
||
|
||
A screen plays itself out because something is taking its place. Nothing takes
|
||
the title's place yet, so the sequencer holds there. A boot that ends by fading
|
||
to black is a boot that looks like it crashed. P4 puts the intro video in front
|
||
of the title and P5 gives the title somewhere to go.
|
||
|
||
### `flow.json` reproduces an observation and says so
|
||
|
||
Q6 closed with a negative: the order is in none of the four places it could have
|
||
been, and a transition is a call with a name argument chosen by code. So this
|
||
file is authored and its header says plainly that it reproduces what was watched,
|
||
not what any file states. The intro video's place in the real boot is **named as
|
||
a gap** rather than the order being quietly rewritten to hide it.
|
||
|
||
## P3 gate
|
||
|
||
`godot --path port -- --boot --film=/tmp/boot` runs unattended:
|
||
|
||
```
|
||
publisher_logo → developer_logos at 4.65 s → title at 8.57 s
|
||
boot sequence complete after 13.05 s, holding on title
|
||
```
|
||
|
||
The filmstrip shows each screen fading in, holding, and fading through black into
|
||
the next, and the title staying up. `verify-screen` covers all **16** screens
|
||
now; the four new splash bundles come in at max 1–2/255 against the reference
|
||
renderer. The three known differences are unchanged: `title` 6 (paint-order tie),
|
||
`main_menu` 4, `title_jp` 155 (sampling phase at 125 % scale).
|
||
|
||
## Answers taken from the RE agent without re-deriving them
|
||
|
||
* **Focus stays "replace".** Over-vs-instead is unobservable: the focused sprite
|
||
covers the base at 100 % of base-visible pixels, and the two compositions
|
||
differ by RMSE 1.1 inside the button rect — under the gamma floor. The port's
|
||
guess was right for the wrong reason, and the actual gap is that
|
||
`ptbtn0Nf.rat` declares **two** sprites — `ptbtneff01.t32`, a glowing ring, and
|
||
then the bright label — where `ptbtn0N.rat` declares one. The ring is P5's, and
|
||
its placement inside the record is **not decoded**, so it will be authored from
|
||
the capture and marked as such.
|
||
* **RMSE against captures has a floor, so stop chasing it.** The capture is
|
||
`≈ 255·(render/255)^γ` with γ ≈ 1.49 on the menu and `EXTRAS`, 1.34 on the
|
||
title, and it is a ramp *the game installed* (`VdGetCurrentDisplayGamma` at
|
||
video init), not a capture-path artefact to subtract. Its reach is narrow —
|
||
the flat patches it was fitted on are almost all dark — so the port will not
|
||
extrapolate it across the range, and will not apply it to rendered output on
|
||
this evidence. It is a comparison constant, not a rendering one.
|
||
* **Rotation is escalated to a human and the port has not acted.** The RE half is
|
||
answered — rotate about the **declared pivot**, measured against the GPU
|
||
capture — and it has zero effect on the five screens at rest. The port will
|
||
carry `rotation_deg` in a future FORMAT v3 because carrying a decoded field the
|
||
renderer ignores beats dropping it, but it will not draw it until the
|
||
divergence question is settled.
|
||
|
||
---
|
||
|
||
## P4 — the intro video, 2026-08-29
|
||
|
||
### Theora at 720p is fine here, and no runtime dependency is requested
|
||
|
||
MISSION §6 anticipated that Theora might be too poor at 720p and permitted the
|
||
FFmpeg-GDExtension fallback to be **proposed**. It is not needed, and this was
|
||
measured rather than judged by eye alone. SSIM against the decoded source over a
|
||
10 s sample: **0.9863 at `-q:v 6`, 0.9896 at 8, 0.9924 at 10**. At 200 % zoom on
|
||
the reel's hardest case — fine serif text and soft gradients over near-black,
|
||
where Theora breaks first — q8 is indistinguishable from the source.
|
||
|
||
`-q:v 8`, and **no GDExtension is being proposed or adopted**.
|
||
|
||
`-ac 2` because the source is **6-channel** WMA Pro and Godot's Theora path is
|
||
not a surround one. That downmix is a decision, so it lives in the recorded
|
||
command where a modder can see and change it rather than in prose.
|
||
|
||
### The exact command is in the manifest, per MISSION §6
|
||
|
||
`export/manifest.json` gains a `videos` array, each entry carrying the verbatim
|
||
`ffmpeg` line that produced it. A modder who dislikes the quality re-runs one
|
||
line instead of reverse-engineering what was done to their video — which is the
|
||
whole reason this project converts the disc rather than reading it at runtime.
|
||
|
||
### A cache, and why that is not a hand-edit
|
||
|
||
`export/` is regenerated wholesale, but re-encoding 232 s of video on every run
|
||
costs ~4 minutes to produce a byte-identical file, and an exporter nobody re-runs
|
||
is worse than a cache. So each movie gets a `.cmd` sidecar recording the command
|
||
and the source size, and the encode is skipped only when both match exactly. Any
|
||
change to either re-encodes. This is derived state validating derived state, not
|
||
a hand-edit.
|
||
|
||
### The player renders into the design viewport, not beside it
|
||
|
||
First attempt parented the `VideoStreamPlayer` to the Boot node. It played, and
|
||
every captured frame was **black**: the capture reads the SubViewport, and the
|
||
player was rendering to the window. Worth stating as more than a capture bug —
|
||
everything this port draws composes in the export's own 1280×720 design space,
|
||
and a movie outside that space is outside the coordinate system every screen is
|
||
expressed in.
|
||
|
||
### Ⓐ skips, because Q9 measured it
|
||
|
||
The only input the port handles so far. HANDOFF Q9: one Ⓐ press skips a movie,
|
||
measured — the title was reached at 57 s against a 193 s baseline. Menu
|
||
navigation is still P5.
|
||
|
||
## P4 gate
|
||
|
||
`godot --path port -- --boot --film=…` runs
|
||
`publisher_logo → developer_logos → ADV.ogv → title`, unattended. The filmstrip
|
||
shows the SQUARE ENIX ident, then the reel's live-action-styled CG, then the
|
||
title. The movie's place in the boot is **measured, not decoded** — Q9 decodes
|
||
`ADVERTISE_MOVIE → ADV.wmv` from the movie manifest, but *where it sits in the
|
||
boot order* is what the RE agent watched, and `authored/flow.json` says so.
|
||
|
||
### What I cannot verify from here
|
||
|
||
**Audible playback.** This container has no audio device — Godot falls back to
|
||
the dummy driver. What is verified is that the Vorbis stream exists in the
|
||
transcode, is 2-channel, and decodes. Whether Godot emits it audibly is
|
||
unconfirmed and is stated as unconfirmed rather than assumed from the stream's
|
||
presence. It is a cheap check for anyone with a sound device and an impossible
|
||
one here.
|
||
|
||
---
|
||
|
||
## RETRACTION — `sylpheed-cli` is not the oracle, 2026-08-29
|
||
|
||
**This corrects a framing that runs through everything above, so it is a
|
||
retraction rather than an edit.** Every place this file called
|
||
`sylpheed-cli screen render` *"the reference renderer"* — and it does so
|
||
repeatedly, starting at P1 — overstated what it is.
|
||
|
||
The correction comes from the human, via the RE agent, in their words: Reborn
|
||
"was/is just a GUI explorer and extraction CLI for verifying the decoding of the
|
||
various files. It may very well be wrong." **The oracle is the Xenia Canary
|
||
capture and the game.**
|
||
|
||
So `tools/verify-screen` is a **consistency check between two decoders that
|
||
share their assumptions**, and a regression detector. It is not a correctness
|
||
check, and agreement in it is not evidence of correctness.
|
||
|
||
### The embarrassing part is that this file already knew
|
||
|
||
After the `ptframe1` case, P2's write-up says: *"Two renderers reading one field
|
||
through one decoder agreeing is not evidence that the field is right."* Then P1's
|
||
numbers kept being quoted as though 3/255 against `sylpheed-cli` meant the port
|
||
was right. Having the principle written down did not stop me leaning on the
|
||
agreement — which is worth recording, because that is the failure mode, not
|
||
ignorance of the principle.
|
||
|
||
**Three times** both renderers agreed and both were wrong, all three caught by a
|
||
capture and catchable by nothing else:
|
||
|
||
| | what both got wrong | how it surfaced |
|
||
|---|---|---|
|
||
| `pteff05` | the menu screens had **no background** | the RE agent decoded the RATC child name |
|
||
| scale 0 | drawn at full size instead of collapsed | RE agent's control run |
|
||
| `rest()` | `ptframe1`/`ptframe2` invisible; the menu bracket missing | `main-menu-oracle.png` |
|
||
|
||
### What changes
|
||
|
||
* `tools/verify-screen` says all of this in its own header, calls the CLI the
|
||
**comparison** renderer, and a `DIFFERS` row now means "we moved apart, find
|
||
out which of us moved" rather than "the port is wrong".
|
||
* The correctness question moves to the captures. The RE agent has committed
|
||
nine of them with an index at `docs/re/captures/ORACLE-CAPTURES.md`, covering
|
||
all five screens in scope — including a **main menu with `OPTIONS` focused**,
|
||
whose difference from the unfocused menu isolates exactly what focus changes.
|
||
* Three cautions travel with any capture comparison, and they are the RE agent's:
|
||
the captures are **not gamma-neutral** (γ ≈ 1.49 menu, 1.34 title — there is a
|
||
floor, do not chase it); **geometry is sound** (best alignment 0,0 at corr
|
||
0.9466, so a positional disagreement is real); and each is **one moment of a
|
||
still-animating screen**, so compare settled poses or regions known to be at
|
||
rest.
|
||
|
||
### What does not change
|
||
|
||
The port keeps running `verify-screen` over all 16 screens every iteration. A
|
||
consistency check is still worth having — it is total, it is cheap, and it is
|
||
what catches a divergence the RE agent introduces on their side. It is simply
|
||
not a grade, and this file will stop quoting it as one.
|
||
|
||
## P5 groundwork — the focus record, checked against a capture, 2026-08-29
|
||
|
||
P5 is the lowest unfinished milestone (P0–P4 are gated above). This iteration did
|
||
not implement navigation; it did the measurement P5 needs first, because the one
|
||
thing P5 is built on — how a focused button is drawn — had three claims attached
|
||
to it and none of them had been checked from this side.
|
||
|
||
### The exporter already emits the focus record's second element
|
||
|
||
HANDOFF ask 3 answers with a 🔴: *"what you are actually missing is the focus
|
||
record's SECOND element"* — `ptbtneff01.t32`, a 42×46 glowing ring, declared
|
||
before the bright label in `ptbtn0Nf.rat`.
|
||
|
||
**That gap is in the renderer, not the exporter.** `export/screens/title/main_menu.json`
|
||
already carries both, in declaration order, under `focus.elements`, for all five
|
||
buttons — `ptbtneff01` then `ptbtn0Nf`, each with its own pivot, rest pose and
|
||
keyframes. Nothing needs to change in `crates/sylpheed-export` for the ring. What
|
||
is missing is that `screen_view.gd` draws only one sprite per focused button.
|
||
Recording this so P5 does not re-open the exporter looking for it.
|
||
|
||
The ring's size checks out exactly: `ptbtneff01.png` is **42×46**, as stated.
|
||
|
||
### The `(7,7)` focus offset survives a refutation attempt, uniquely
|
||
|
||
Ask 3 states the focused sprite covers the base at 100.0 % of base-visible pixels
|
||
"once aligned properly (the true offset is **(7,7)**, and at the centre alignment
|
||
it reads a misleading 78–84 %)". P5 builds directly on this, so it was worth
|
||
attacking.
|
||
|
||
Re-ran the RE agent's own metric on the exported PNGs — over every pixel where the
|
||
base sprite is visible, the fraction where the focus sprite's alpha ≥ the base's —
|
||
scanning the whole offset space, not just the stated answer:
|
||
|
||
| alignment | ptbtn01 | ptbtn02 | ptbtn03 | ptbtn04 | ptbtn05 |
|
||
|---|---|---|---|---|---|
|
||
| **(7,7)** | **100.00 %** | **100.00 %** | **100.00 %** | **100.00 %** | **100.00 %** |
|
||
| geometric centre | 80.58 % | 79.20 % | 79.58 % | 79.45 % | 79.45 % |
|
||
| pivot-to-pivot | 80.58 % | 86.59 % | 87.40 % | 79.45 % | 84.58 % |
|
||
|
||
**The refutation fails, and more strongly than the original claim.** Over a
|
||
15×14 offset scan, `(7,7)` is a *unique isolated cell* at 100 % on every one of
|
||
the five buttons — every neighbouring offset, including (6,6) and (7,6), falls
|
||
below 90 %. The centre and pivot alignments reproduce the 78–84 % band the RE
|
||
agent reported as misleading. A claim that survives a search of its whole
|
||
parameter space is worth more than one checked at a single point, so this is
|
||
recorded as strengthened, not merely unrefuted.
|
||
|
||
### `(7,7)` is not a constant to apply — it is what the declared positions already say
|
||
|
||
An earlier version of this analysis had the port disagreeing with the capture by
|
||
6 px. **That was my arithmetic error and it is worth writing down**, because it is
|
||
the mistake this format invites: I computed each element's top-left as
|
||
`pos - pivot`, which gives base→focus deltas of (13,13) and a 29 % coverage — a
|
||
confident wrong number.
|
||
|
||
`pos` **is** the top-left. `screen_view.gd:121` is
|
||
`Rect2(pos - pivot*(s - 1), natural*s)`: the pivot is the anchor scale grows
|
||
about and it *cancels at 100 %*, which is exactly the "can be got wrong
|
||
invisibly" the comment there warns about. Getting it wrong invisibly is what
|
||
happened.
|
||
|
||
With `pos` as the top-left, base − focus is `(542,162) - (535,155)` = **(7,7)**
|
||
directly, on four of the five buttons. So P5 draws each focus element at its own
|
||
declared `pos` and needs no offset constant at all. Nothing to author.
|
||
|
||
### The one real find: `ptbtn04` is 1 px off the grid on the disc
|
||
|
||
The focus records sit on a clean 80 px pitch — 155, 235, 315, 395, 475. The
|
||
**base** records do not: 162, 242, 322, **401**, 482, i.e. spacings 80, 80, **79**,
|
||
**81**. So `ptbtn04`'s declared base→focus delta is **(7,6)**, while the art
|
||
itself aligns at (7,7) — the coverage scan puts `ptbtn04` at 100 % on (7,7) and
|
||
below 90 % on (7,6), the same as every other button.
|
||
|
||
This is 1 px of authoring jitter on the disc, not a decode error, and it has one
|
||
consequence worth stating: **do not derive the focus placement from the base by a
|
||
constant.** Draw the focus record at its own declared `pos`. A port that
|
||
"simplified" this to base + (7,7) would put `ptbtn04`'s focus art 1 px off, and
|
||
would look right on the other four.
|
||
|
||
### Verified against a capture, not against our other renderer
|
||
|
||
Diffing two oracle frames isolates what focus changes without any instrument in
|
||
the path: `live-main-menu.png` vs `live-main-menu-options-focused.png` differ in
|
||
one tight cluster of 6 338 px at **x 506..702, y 398..445**. `ptbtn04` is the
|
||
`OPTIONS` button, and the union of its focus record under the `pos`-as-top-left
|
||
reading — ring `ptbtneff01` at (500,396) 42×46 plus label `ptbtn04f` at (535,395)
|
||
172×56 — is **x 500..706, y 395..451**. Those agree on all four edges to within a
|
||
few px of near-transparent sprite border.
|
||
|
||
Under the `pos - pivot` reading the same record predicts x 433..604, y 367..422,
|
||
which matches nothing in the capture — and *no* button matches that cluster. The
|
||
capture, not our renderer, is what settles it.
|
||
|
||
### An instrument that failed its own control, and was therefore discarded
|
||
|
||
To locate the buttons independently I wrote a masked normalised-cross-correlation
|
||
template matcher and ran it as PROTOCOL requires — **through a control first**:
|
||
match each *base* sprite against the *plain* capture, where the declared position
|
||
is known and the answer must be a (0,0) delta.
|
||
|
||
It returned deltas of (13,5), (6,−19), (12,21), (−16,22), (6,8) at NCC
|
||
**0.096–0.206** — noise, with three of five pinned to the ±22 search boundary.
|
||
The control fails, so the instrument is dead rather than tuneable, and **none of
|
||
its output is used above.** The button art is dark, low-contrast and further
|
||
crushed by the capture's γ ≈ 1.49 ramp, which is the likely cause; a matcher for
|
||
this corpus would have to work on gradients rather than luminance. Filed so the
|
||
next iteration does not rebuild the same broken tool.
|
||
|
||
### What P5 still needs, and has not got
|
||
|
||
* **Initial focus is not stable across boots** (Q5: 2× `TUTORIAL`, 2× `NEW GAME`).
|
||
That is a value to author, with a `why` naming Q5 — it is not written yet.
|
||
* **The ring's own animation is unread.** `ptbtneff01`'s two keyframes go
|
||
`rotation_deg` 0 → **360** at t=120 with no second timed keyframe, i.e. a full
|
||
turn. Whether it spins continuously while focused, or turns once and holds, is
|
||
the group-loop question again — answered "groups hold" for build-in animations,
|
||
but a 360° hold and a 0° hold are the same pose, so *this* group cannot be told
|
||
apart by its rest pose. Not guessed; raised below.
|
||
|
||
---
|
||
|
||
## P5 — navigation, 2026-08-29
|
||
|
||
The gate is *"a human clicks through it"*, and the artifact is a walk that
|
||
proves the wiring rather than the intent: `up` (which wraps 01→05), five `down`,
|
||
Ⓐ into `EXTRAS`, `down`, Ⓑ back — landing on the main menu with focus restored
|
||
to `EXTRAS`.
|
||
|
||
```sh
|
||
xvfb-run -a godot --path port -- --menu \
|
||
--script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5
|
||
```
|
||
|
||
Ten PNGs, one per step, each taken after the screen it produced had settled.
|
||
Contact sheet handed over as `share` id `1788002507-ef4468a0a33a`.
|
||
|
||
### The scripted walk goes through the input system, not around it
|
||
|
||
`--script` posts `InputEventAction` through `Input.parse_input_event` and lets
|
||
it arrive at `_unhandled_input` exactly as a d-pad's press would. Calling
|
||
`MenuFlow.move()`/`accept()`/`cancel()` directly would have been shorter and
|
||
would have proved nothing: the thing most likely to be broken is the wiring
|
||
between a press and the cursor, and a direct call is precisely the part that
|
||
skips it. The same reasoning says the settle wait must be real — a shot taken
|
||
before the screen stops moving photographs a fade and calls it a menu.
|
||
|
||
### What is authored here, and what is derived
|
||
|
||
Split deliberately, because P5 is where the two are easiest to blur:
|
||
|
||
| | where | why |
|
||
|---|---|---|
|
||
| the ORDER of the items | **derived** — each screen file's `buttons`, filled by the exporter from the button-role elements sorted by resting Y | it is on the disc |
|
||
| where an item goes | **authored** — `authored/flow.json` | HANDOFF Q4 *measured* the destinations; they are not in the file |
|
||
| which item opens focused | **authored** | Q5 measured that it is **not stable boot to boot** |
|
||
| what Ⓑ does | **authored** | Q5, measured — except on the main menu, see below |
|
||
| ⬅➡ do nothing | **authored**, written as an explicit no-op | so that *"the game ignores it"* and *"we never wired it"* are different lines of code |
|
||
|
||
Four of the five main-menu destinations are `goto: null` with a `blocked` note.
|
||
That is **not** an unknown: `DIFFICULTY`, the save-slot list, the lesson list and
|
||
the settings menu were all measured, and they live in archives this export does
|
||
not carry. `blocked` and `none` are kept apart so a later reader does not
|
||
"discover" a gap that was a milestone boundary.
|
||
|
||
`EXTRAS` is the only main-menu destination inside `GP_TITLE`, and therefore the
|
||
only Ⓐ-into-a-submenu this gate can actually walk.
|
||
|
||
### The one navigation rule with nothing behind it
|
||
|
||
Ⓑ on the **main menu** → title. HANDOFF Q5 states it, and `flow.json` marks it
|
||
*authored — likely but UNPROVEN*, because the title also self-returns after
|
||
~8–10 s idle and a single unrecorded observation cannot separate the two. The
|
||
port implements it anyway — a menu with no way out is worse than a menu with a
|
||
plausible one — and says in the file that it did. Asked of the Decoder this
|
||
iteration; see `BLOCKED.md`.
|
||
|
||
Independent corroboration that the main menu is different from its submenu:
|
||
the main menu's footer advertises only `Ⓐ : OK`, while `EXTRAS`' footer
|
||
advertises `Ⓑ : Back`. That is on the disc, in `ptmsg.png` vs `ptmsg2.png`, and
|
||
it is visible in both the port's render and the captures.
|
||
|
||
### A press during a fade is dropped
|
||
|
||
**Authored, and not measured.** Nobody has watched what the game does with a
|
||
button pressed mid-transition. Dropping invents less than queueing does: it
|
||
cannot manufacture a press the game might have discarded. `flow.json` says so
|
||
under `navigation.input_during_transition`.
|
||
|
||
---
|
||
|
||
## `--headless` cannot draw, and the port hung instead of saying so, 2026-08-29
|
||
|
||
`docs/port/PORT-MISSION.md` and the loop prompt both name `godot-headless` as
|
||
how this project runs unattended. It does not work, and the way it failed was
|
||
the worst available shape.
|
||
|
||
**Measured, not assumed.** Under `--headless` Godot's dummy renderer never emits
|
||
`RenderingServer.frame_post_draw`. Every capture path in `boot.gd` awaits it —
|
||
`--capture` since P1, `--film` since P3, `--shots` as of this milestone — so all
|
||
three blocked forever. Isolated by the difference between two runs:
|
||
|
||
```
|
||
godot --headless --path port --quit # prints, exits 0
|
||
godot --headless --path port -- --screen=… --capture=… # no output at all, killed at 40 s
|
||
```
|
||
|
||
The second produces **zero bytes of output** before it is killed, because
|
||
Godot's stdout is block-buffered and never flushes. So the observable behaviour
|
||
of an unattended headless capture was: silence, forever. In a loop, a job that
|
||
waits reads as a job still working — this is the failure mode that costs a whole
|
||
iteration and leaves nothing behind to say what happened.
|
||
|
||
Two changes, and deliberately not one:
|
||
|
||
* `--capture`, `--film` and `--shots` **refuse at startup** under `--headless`,
|
||
naming the flag and printing the `xvfb-run` line that does work. Refusing
|
||
early rather than at the first frame means the run does not die halfway
|
||
through a filmstrip with some frames written.
|
||
* `--script` **no longer waits for a drawn frame when it is not going to
|
||
photograph one.** Navigation is checkable where nothing draws, and that is
|
||
worth keeping: `godot --headless --path port -- --menu --script=…` now walks
|
||
the menus and exits 0 in about four seconds, which is a cheap regression check
|
||
that needs no X server at all.
|
||
|
||
The Xvfb path is unchanged and is what produced the P5 artifact.
|
||
|
||
---
|
||
|
||
## Refutation — the focus ring IS drawn rotated, and it is not at 0° in either capture
|
||
|
||
Attempted against the Decoder's `7eeae30` (*"re(ui): the focus ring SPINS, the
|
||
game draws it, and the leaf owns the f record"*), point 2: that in the
|
||
OPTIONS-focused capture the ring's bright head sits in a different angular
|
||
position from the sprite's own, caught mid-spin. **It survives**, and the
|
||
evidence is stronger than what was claimed.
|
||
|
||
Chosen for refutation because it is exactly what PROTOCOL says to aim at: a
|
||
claim the port is about to build on, resting on an estimator (a brightest-region
|
||
centroid) whose own control the Decoder reported as ±19.8°.
|
||
|
||
### The test, and why it needs no absolute registration
|
||
|
||
`live-main-menu.png` has `ptbtn01` focused; `live-main-menu-options-focused.png`
|
||
has `ptbtn04` focused. Both draw **the same sprite**, `ptbtneff01.png` — the
|
||
export confirms the two focus records name the same file. So the two captures
|
||
contain two instances of one 42×46 image, 240 px apart in design space, and the
|
||
question *"is it drawn rotated"* becomes *"are these two crops the same image
|
||
at a different angle"* — which needs no crop offset and no reference to our own
|
||
renderer.
|
||
|
||
Method: sample each ring into a 360-bin **angular luminance profile** over the
|
||
annulus band (r = 9…15 px, bilinear, 0.5 px radial step) and circularly
|
||
cross-correlate. A rotation about the centre shifts that vector and changes
|
||
nothing else.
|
||
|
||
### The instrument was run through two controls before it was believed
|
||
|
||
| control | result |
|
||
|---|---|
|
||
| rotate a capture's own ring by a known 0/30/90/150/210/270/330° and recover it | **0° error on all seven**, peak corr 1.000 |
|
||
| the same estimator on a ring-free 60×64 patch of the *same* capture | peak corr **0.369** — it does not manufacture a match |
|
||
|
||
### The measurement
|
||
|
||
On one shared centre for all three images, so a centroid difference cannot
|
||
masquerade as a rotation:
|
||
|
||
| pair | best shift | peak corr | corr at 0° |
|
||
|---|---|---|---|
|
||
| capture A vs capture B | **134°** | 0.968 | −0.064 |
|
||
| sprite (unrotated) vs capture A | **76°** | 0.969 | −0.295 |
|
||
| sprite (unrotated) vs capture B | **210°** | 0.948 | −0.181 |
|
||
|
||
210 − 76 = 134: the three measurements are internally consistent, which nothing
|
||
in the method forced them to be. Sweeping the centre by ±2 px moves the A-vs-B
|
||
answer over 117…161° while the peak correlation stays 0.9+ across the middle of
|
||
that range, so the **magnitude is ~134° ± ~15°** and the precision claim stops
|
||
there.
|
||
|
||
Evidence sheet — sprite, capture A, capture B, each cropped at the declared
|
||
`42×46+500+156` / `+500+396` — handed over as `share` id
|
||
`1788002507-afe1ad843789`. The phase difference is obvious by eye; the numbers
|
||
are here so it is not only obvious by eye.
|
||
|
||
### The two things this settles for the port
|
||
|
||
1. **The game draws `rotation_deg` on an element the English boot path shows.**
|
||
This is a second, independent confirmation on a different screen and a
|
||
different element from the `ptloop` sweeps, and it moves HANDOFF **ask 4**
|
||
(*should the port draw rotation*) off "changes nothing at rest" — it changes
|
||
the main menu's focus marker, in every frame.
|
||
|
||
2. **0° is not a pose the running game shows.** `screen_view.gd` currently draws
|
||
the ring at its `rest` pose, which is `rotation_deg 0`, and both captures put
|
||
it at 76° and 210°. So the port's focus marker is **known** to be wrong, not
|
||
suspected — and the comment in `screen_view.gd` now says which two numbers it
|
||
is wrong against.
|
||
|
||
### Registration, as a by-product
|
||
|
||
The ring's annulus centroid lands at (32.94, 36.63) and (33.30, 38.90) in
|
||
windows whose design-space prediction under a **zero crop offset** is
|
||
(33.0, 37.0). Within ~0.4 px on the better-thresholded of the two. That
|
||
corroborates `ORACLE-CAPTURES.md`'s *"1279×675, top-left aligned"* directly, on
|
||
a feature nobody chose for the purpose.
|
||
|
||
⚠️ Do not read the earlier P5-groundwork note *"button text bands land at design
|
||
y + 23"* as a crop offset — it is an offset **within** the button sprite, and
|
||
the two were nearly confused here.
|
||
|
||
### What the port did NOT do about it
|
||
|
||
It did not start spinning the ring. The period is a **guess with two unknowns**
|
||
and both belong to the Decoder:
|
||
|
||
* the keyframes are `t=120, rot 0` then an **untimed** `rot 360`. Under HANDOFF
|
||
Q1's replicated reading (*"`+36` is the time the NEXT pose is reached"*) that
|
||
is one revolution in 120 units = **2.0 s** — but this port's `pose_at`
|
||
implements the *other* reading, and switching it is a change to every screen's
|
||
animation timing, not a P5 change;
|
||
* *"groups hold"* (settled 2026-08-28) predicts the ring stops at 360° = 0°.
|
||
Both captures show it elsewhere. That is either a spin that loops, or two
|
||
captures both taken inside the first two seconds of focus. **The port cannot
|
||
tell those apart**, and a wrong answer here is a visible continuous rotation
|
||
on whichever button the player is sitting on.
|
||
|
||
Filed in `BLOCKED.md` and asked over the message channel. What settles it is two
|
||
frames of one focused button a known time apart.
|
||
|
||
---
|
||
|
||
## P5 end to end — and the title does not say `PRESS Ⓐ`, 2026-08-29
|
||
|
||
The gate walk above starts on a screen. This is the whole thing, unattended, in
|
||
one run — the sequence PORT-MISSION names as the objective:
|
||
|
||
```sh
|
||
xvfb-run -a godot --path port -- --boot --play \
|
||
--script=accept,down,down,down,down,accept,cancel,cancel --shots=/tmp/e2e
|
||
```
|
||
|
||
```
|
||
screen publisher_logo … settles at t=235 (3.917 s)
|
||
-> developer_logos at 4.70 s
|
||
-> video ADV at 8.60 s
|
||
video ended at 151.91 s
|
||
-> title at 151.91 s
|
||
boot sequence complete after 156.30 s, holding on title
|
||
menu on title
|
||
script[1] accept (A) -> main_menu
|
||
…
|
||
script[6] accept (EXTRAS) -> extras
|
||
script[7] cancel (B) -> main_menu focus restored to ptbtn05
|
||
script[8] cancel (B) -> title
|
||
script complete after 166.76 s on title
|
||
```
|
||
|
||
Publisher wordmark → developer logos → `ADV` → title → Ⓐ → main menu →
|
||
navigate → Ⓐ → `EXTRAS` → Ⓑ (focus restored) → Ⓑ → title. Contact sheet shared.
|
||
|
||
Two smaller things this run found, both fixed here:
|
||
|
||
* the boot step's `why` in `authored/flow.json` still said *"nothing takes the
|
||
title's place until P5 gives it somewhere to go"*. P5 has. Rewritten to say
|
||
what is actually true — `--boot` still **stops** on the title, and `--play`
|
||
**hands the held title over**; the stop is not a bug and the handover is not
|
||
another boot step.
|
||
* an empty focus printed as a line that trailed off, which reads like a value
|
||
went missing rather than like there is none. The title is a screen with no
|
||
`buttons` that still takes Ⓐ, so it prints
|
||
`(none -- this screen has no focusable item)`.
|
||
|
||
Also confirmed on the way: entering a submenu **directly** (`--menu=extras`) and
|
||
pressing Ⓑ enters the parent at its authored initial focus, not at a restored
|
||
one — there is no history to restore, and `MenuFlow.cancel` only claims a
|
||
restored focus when the stack agrees about where it is going.
|
||
|
||
### 🔴 The port's title does not tell the player to press Ⓐ
|
||
|
||
Found by running the objective end to end, which is the only thing that would
|
||
have found it: the boot's last step is `title` (build 4), and **build 4 has no
|
||
`PRESS Ⓐ BUTTON` plate**. P5 has now made Ⓐ the only way off that screen.
|
||
|
||
This is not a guess about the art. Both states are captured off the running
|
||
game and they differ by exactly that plate:
|
||
|
||
| | capture |
|
||
|---|---|
|
||
| title **without** the plate | `title-builds/live-title-build4-no-plate.png` |
|
||
| title **with** the plate | `title-builds/live-title-press-a.png` |
|
||
|
||
And the plate is already exported — `press_start`, `GP_TITLE` build 2 (HANDOFF
|
||
Q2), sitting in `export/screens/title/` unused by anything.
|
||
|
||
**This is P3's gate, not P5's, and P5 is what exposed it.** Recording rather
|
||
than fixing, for two reasons:
|
||
|
||
1. Which state an idle post-boot title shows — build 4 alone, build 4 with the
|
||
plate over it, or build 4 *then* the plate after a delay — is **behavioural**,
|
||
and the port has no oracle for a sequence. The game demonstrably has both
|
||
states; nothing here says which one follows the intro movie. That is the
|
||
Decoder's.
|
||
2. Showing it would mean **drawing two builds at once**, which this port has
|
||
never done — every mode loads exactly one screen. That is a real change to
|
||
`ScreenView`, not a line in `flow.json`, and it should not be smuggled in
|
||
under a navigation milestone on the strength of "it looks more right".
|
||
|
||
Filed in `BLOCKED.md`. Not blocking: P5's gate is Ⓐ into a submenu and Ⓑ back,
|
||
and both work.
|
||
|
||
## P6 — menu audio, 2026-08-29
|
||
|
||
The disc's menu sound reaches Godot as Ogg Vorbis: three cues and one music bed.
|
||
Nothing in `port/` has heard of XMA, `sound.pak` or `Static.slb`, and nothing in
|
||
it reassembles anything — `sylpheed_formats::media` does that and the exporter
|
||
converts what it hands back.
|
||
|
||
### The cue offsets moved OUT of the exporter, into `authored/`
|
||
|
||
The previous iteration left `crates/sylpheed-export/src/audio.rs` holding the
|
||
three `Static.slb` offsets as a Rust `const CUES`. That is wrong under MISSION
|
||
§3 and the fix is the first thing this iteration did.
|
||
|
||
Those offsets are **measured**, not decoded. `Static.slb` has no `RIFF`, no seek
|
||
chunk and no container: it is a packed run of whole 2048-byte XMA1 packets, and a
|
||
wave is defined *only* by `(offset, packet_count)`. Both numbers came from the
|
||
running game — Canary with `--xma_param_probe=true` prints a stream's packet
|
||
count and first 32 bytes when it is played, and searching those bytes in the bank
|
||
gives the offset (HANDOFF Q8).
|
||
|
||
A measured value compiled into the exporter is **a measurement wearing the
|
||
costume of a decoded field**. It reads as though the exporter derived it from the
|
||
disc; nobody deletes it when the real answer lands, because nobody can see that
|
||
there is anything to delete. So the table is `authored/audio.json` `se.*`, each
|
||
row carrying its own `why`, and the exporter holds no cue table at all.
|
||
|
||
`crate::video::MOVIES` stays a `const` in the exporter, and the contrast is the
|
||
point: Q9 **decoded** that mapping off the movie manifest on the disc. Same
|
||
shape, different provenance, different home.
|
||
|
||
### `name_match` is a field, and its absence means something
|
||
|
||
Q8 names `SE_UI_CURSOR` for the move cue by **name match against the authors' own
|
||
identifiers** — a plausible guess, not the measurement. For Ⓐ, Q8 is explicit
|
||
that the wave was *not* separated between `SE_UI_DECIDE` and
|
||
`SE_UI_SUB_WIN_OPN`, so no name is claimed at all.
|
||
|
||
`name_match` therefore travels beside every cue in `authored/audio.json` and in
|
||
`manifest.json`, and **an absent one means nobody claimed a name — never that
|
||
the binding is unknown.** The binding is the measured part. Collapsing the two
|
||
would turn "we did not separate two candidates" into "we do not know what this
|
||
sound is", which is a different and much weaker statement than the one the RE
|
||
agent actually made.
|
||
|
||
### The BGM is NOT a choice, and this port spent an iteration believing it was
|
||
|
||
The first draft of `authored/audio.json` picked `BGM_001`, wrote a careful `why`
|
||
explaining that the choice was arbitrary, and was **wrong**.
|
||
|
||
`docs/port/BLOCKED.md` carried the row that caused it: *"not on the disc … the
|
||
port is choosing a track, and that choice is authored."* The menu's music is
|
||
**`BGM_103`**, and it is in HANDOFF at **`9ca1eb5`** — the exact commit that page
|
||
says it was reconciled against. So this was not staleness. **The row was wrong
|
||
when it was written.**
|
||
|
||
What HANDOFF says is a negative *with a bound*, and the bound is the entire
|
||
content of it:
|
||
|
||
> the **tables** cannot say — `SOUNDS`, `FILES` and the bank headers name no
|
||
> screen. `GamePart_Title`'s phase handler `sub_821C5580` carries `li r5, 1103`
|
||
> into a sound call; cue 1103 is `BGM_103`; and `BGM_103.slb`'s two declared
|
||
> waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe
|
||
> saw decoding at the main menu. Static code, disc census and runtime all agree.
|
||
> **"The port does not have to choose a track."**
|
||
|
||
The failure is worth naming precisely, because "read HANDOFF more carefully" is
|
||
not the lesson — `BLOCKED.md`'s own staleness check passed, twice, and would pass
|
||
again. **A negative summarised without its reach reads as a bigger negative than
|
||
it is.** "The tables cannot say" became "it is not on the disc", and one word of
|
||
scope was the whole answer. A row in `BLOCKED.md` must quote the reach.
|
||
|
||
It also cost a second thing worth recording: the port would have shipped a menu
|
||
playing the wrong music with a confident `why` beside it saying the choice was
|
||
deliberate. That is exactly the shape of error this project's vocabulary exists
|
||
to prevent, produced *by* the machinery meant to prevent it.
|
||
|
||
### The bank name carries `.slb`, and that is how the mistake surfaced
|
||
|
||
`BGM_001` is not in `sound.pak`. `BGM_001.slb` is — `media::read_sound_bank`
|
||
looks up `name_hash(name)` against the TOC, and the TOC hashes the **file name**.
|
||
`Static.slb` worked from the first run only because the RE finding happens to
|
||
write it with its extension.
|
||
|
||
So the wrong track never played: the export failed loudly with *"BGM_001: not
|
||
present in sound.pak"*. That is luck, not design — had the draft picked a name
|
||
that happened to resolve, nothing would have complained. The `why` in
|
||
`authored/audio.json` now records both the correct name and why the short form
|
||
fails.
|
||
|
||
`export_bgm` now distinguishes the two cases it was conflating. A bank that is
|
||
**not in this disc's `sound.pak`** is a missing asset: the manifest takes a
|
||
warning and everything else still exports. Any other failure — a short read, a
|
||
malformed bank — still stops the run, because a partly-read bank produces a file
|
||
that plays.
|
||
|
||
### The two stems are summed. That part is not a choice
|
||
|
||
Q10 also measured that a bank's sub-waves are **two stems of one performance,
|
||
played together** — sample-synchronous, equal duration, on all 32 banks.
|
||
Concatenating them is explicitly wrong.
|
||
|
||
Emitting them as two files would be wrong for a second, independent reason:
|
||
MODDING rule 1 is *one logical asset, one file*, and handing a modder two stems
|
||
to line up by hand is precisely the reassembly the exporter exists to have
|
||
already done. `amix=normalize=0` sums at unity rather than halving, because
|
||
halving is a mix decision nobody made — and because a sum can clip, the peak is
|
||
**measured and reported** rather than silently corrected.
|
||
|
||
### The loop seam is ugly on purpose
|
||
|
||
No loop-point field has been identified. `loop: "restart"` replays from sample 0,
|
||
so a listener hears the track's own fade-out and its trailing silence before the
|
||
music comes back.
|
||
|
||
Trimming to the fade would sound better and would be **worse**. It would invent a
|
||
loop point, and an invented one is indistinguishable from a decoded one a month
|
||
later — which is the failure mode this whole project is organised against. The
|
||
seam stays audible until a loop point is measured or a capture of the real menu
|
||
looping settles it.
|
||
|
||
### When a cue fires — two rules measured, one authored
|
||
|
||
* **Move** fires on a press that *actually moves the cursor*. `MenuFlow.move()`
|
||
already returned whether it did, which is why left/right stay silent by
|
||
construction rather than by a rule written twice (Q5: ⬅➡ do nothing, and Q8:
|
||
they play nothing).
|
||
* **Ⓐ and Ⓑ** fire when the press *does something*, and not when nothing is
|
||
bound. 🟡 **This half is authored and NOT measured** — nobody has watched the
|
||
game take a dead press. Silence invents less: a sound the game does not make is
|
||
a wrong fact you can hear, while a missing one is a gap. `blocked` counts as
|
||
doing something, because those destinations *were* measured off the running
|
||
game and are missing from this export, not from the game.
|
||
* The bed starts when the menu becomes live and **carries across submenus**.
|
||
`play_bed` is idempotent, because music that restarts every time you press Ⓑ is
|
||
the kind of wrong that reads as "the audio works".
|
||
|
||
### `--audio=` records the Master bus, because neither container has a sound card
|
||
|
||
`docs/port/AUDIO-VERIFICATION.md` §2. An `AudioEffectRecord` on the Master bus
|
||
captures the mixed output from inside a headless run with no device at all, and
|
||
that is the only thing that closes the loop the file opens: comparing an exported
|
||
Ogg against the disc proves the **asset** is right and says nothing about whether
|
||
the engine ever reached it.
|
||
|
||
The run prints `AudioServer.get_driver_name()` beside the file it wrote, because
|
||
"recorded under a dummy driver" is a weaker claim than "heard" and the write-up
|
||
has to be able to say which one it is making.
|
||
|
||
The WAV is saved in `_exit_tree` rather than beside each `quit()`. There are
|
||
eight of those, and the one that would get missed is an error path — exactly the
|
||
run whose audio somebody wants to look at.
|
||
|
||
### `check` now refuses silence and clipping
|
||
|
||
`sylpheed-export check` gained an `audio` pass, and two of its rules are content
|
||
checks rather than schema checks. That is deliberate. Silence is *the* audio
|
||
failure that looks like success — a file of the right duration, the right channel
|
||
count and the right size, full of zeroes — and it passes every structural check
|
||
there is. Clipping is the other one, and the BGM can produce it because it is a
|
||
sum at unity gain. The exporter measures both at export time; `check` refuses a
|
||
tree whose peak is ≤ −90 dBFS or ≥ 0 dBFS.
|
||
|
||
Neither is a judgement about whether the audio is the *right* audio. Nothing in
|
||
that binary can know that, and `BLOCKED.md` says which parts are still authored
|
||
guesses.
|
||
|
||
### A bug worth naming: the temp name ate the file extension
|
||
|
||
`run_ffmpeg` wrote to `.back.ogg.partial` — the temp-name-then-rename discipline
|
||
this project uses everywhere, and which `AUDIO-VERIFICATION.md` records as
|
||
already having caused a confident wrong number once.
|
||
|
||
ffmpeg picks its muxer **from the output filename**, so that is not a slightly
|
||
uglier temp name; it is a hard failure before a byte is written: *"Unable to
|
||
choose an output format for '.back.ogg.partial'"*. `video.rs` already had the
|
||
right shape (`.ADV.partial.ogv`) and this function was written from scratch
|
||
without looking at it. The extension goes last.
|
||
|
||
### Refutation — the three Q8 cue durations, checked end to end
|
||
|
||
**The claim:** HANDOFF Q8 publishes three cue lengths — move **0.533 s**
|
||
(8 192 B, 4 packets), back **0.344 s** (4 096 B, 2), confirm **1.016 s**
|
||
(12 288 B, 6). P6 is built directly on top of these, which by PROTOCOL's own rule
|
||
makes them the right thing to attack: refutation is cheapest where the other
|
||
agent is most confident, and most valuable where the port is about to build.
|
||
|
||
**Why they looked attackable.** The three do not share a rate. Seconds per
|
||
packet is 0.133, 0.172 and 0.169 — the move cue is 22 % off the other two. If a
|
||
packet were a fixed span of audio, at most one of these numbers could be right.
|
||
|
||
**Why that is not a refutation.** An XMA1 packet is 2 048 bytes of *bitstream*,
|
||
not a fixed span: it carries a variable number of 512-sample frames. At 48 kHz a
|
||
frame is 10.667 ms, and the three durations come to **50.0, 32.3 and 95.3
|
||
frames** — near-integers, which is what a variable-frames-per-packet encoding
|
||
looks like and is not what an arithmetic slip looks like.
|
||
|
||
**The measurement.** The exporter reads `(offset, packet_count)` through
|
||
`media::se_wave_riff`, decodes, and `ffprobe`s the finished Ogg:
|
||
|
||
| cue | Q8 claims | exported file measures |
|
||
|---|---|---|
|
||
| move | 0.533 s | **0.533 s** |
|
||
| back | 0.344 s | **0.344 s** |
|
||
| confirm | 1.016 s | **1.016 s** |
|
||
|
||
**Verdict: survives, exactly, at every published digit.** Recorded as a survival
|
||
rather than a pass, because that is what PROTOCOL asks for — a claim that has
|
||
survived an attempt is stronger than one nobody challenged, and the corpus should
|
||
say which it is.
|
||
|
||
⚠️ **Reach, stated so nobody over-reads it.** This is not independent of Q8: the
|
||
durations were derived from the same packet counts the exporter feeds in, so what
|
||
it confirms is that reading those `(offset, packets)` through
|
||
`sylpheed_formats::media` yields streams of exactly the claimed length — i.e.
|
||
that the *transcription* into `authored/audio.json` and the assembly path are
|
||
right. It does **not** confirm that these three waves are the sounds the game
|
||
plays on those three events; that is Q8's own measurement, taken by playing them,
|
||
and this port has no oracle to re-take it with.
|
||
|
||
The attempt did find something, just not here: see the BGM section above, where
|
||
the port's *own* `BLOCKED.md` row failed the same kind of check.
|
||
|
||
### The BGM bank has three sub-waves and HANDOFF says it has two
|
||
|
||
`media::sound_bank_riffs("BGM_103.slb")` returns **three**. HANDOFF Q10's census
|
||
says a music bank is *"exactly two waves of identical duration (32/32 banks on
|
||
the disc)"* — and that census is itself a correction, of an earlier reading that
|
||
called `BGM_001` three sub-waves and was refuted with "the 10 KB is the bank
|
||
header".
|
||
|
||
The third comes from `sylpheed-formats/src/slb.rs:380`, `to_xma_riffs`: when a
|
||
bank has a leading headerless packet region ahead of its first `RIFF`, that
|
||
region is emitted as a sub-wave. It exists because the voice path needs it —
|
||
`VOICE_D_453` decoded to 0.14 s without it. `docs/re/REFUTED.md` already records
|
||
the same region as what makes `BGM_106`–`BGM_109` "break the two-wave rule".
|
||
|
||
**The port sums all three and says so in the manifest.** That is not the
|
||
appealing answer — dropping sub-wave 0 would give a file matching the census, and
|
||
it would have been one line. It is the correct one: *which bytes belong together*
|
||
is the question `sylpheed_formats::media` owns, MISSION §2 names re-deriving it
|
||
here as the single easiest thing in this project to get subtly wrong, and "the
|
||
decoder returned something the corpus does not predict" is a finding to report,
|
||
not a number to quietly adjust. Adjusting it would also have destroyed the
|
||
evidence: a corrected export looks exactly like a correct one.
|
||
|
||
So the export ships the decoders' answer, the manifest carries a warning naming
|
||
the contradiction, `BLOCKED.md` has the row, and the Decoder has the pointer.
|
||
Until it comes back, **the menu plays a sum of three things where the census
|
||
predicts two**, and every one of those places says so.
|
||
|
||
### Clipping — and a comment of mine that argued for the thing that clipped
|
||
|
||
The BGM came out at **+1.8 dBFS**. The comment above the code that produced it
|
||
said `amix=normalize=0` sums at unity "because halving is a mix decision nobody
|
||
made".
|
||
|
||
That was wrong in both halves. Unity summing *is* a decision, and it is the one
|
||
that clips. And 1/n is not a taste call: it is the smallest constant that makes
|
||
an n-input sum of unity-scale signals provably clip-free, which is precisely the
|
||
reasoning `video.rs` already carried for its 0.4142-normalised 5.1 downmix — in
|
||
this same repository, written by this same port, and not looked at. It preserves
|
||
the stems' relative balance exactly, which is the only thing about the sum that
|
||
Q10 settles.
|
||
|
||
It is written as an explicit `volume=` rather than left to `amix`'s
|
||
`normalize=1` default, so the coefficient appears in the manifest's command line.
|
||
A default is a decision nobody made and it can move under an ffmpeg upgrade —
|
||
the same argument MISSION §6 makes about the downmix matrix.
|
||
|
||
**The `confirm` cue is a different case and is not "fixed".** It lands at
|
||
+0.18 dBFS, and it is a single wave off the disc with no arithmetic of ours in
|
||
it: the disc masters it near full scale and a lossy decode of a near-full-scale
|
||
signal overshoots by a fraction of a dB. Attenuating it would mean altering a
|
||
game asset to make one of our own numbers smaller. So `check` bounds the two
|
||
kinds differently — a `bgm` peak ≥ 0 dBFS is refused outright, because it is our
|
||
sum; an `se` is refused only above **+1.0 dBFS**.
|
||
|
||
🟡 That +1.0 is a **judgement and not a measurement**, and it is the weakest
|
||
number in P6. Nobody has measured the overshoot distribution across a corpus of
|
||
cues. If a cue ever trips it, the right response is that measurement, not a
|
||
looser bound.
|
||
|
||
## P6 gate — the audio is in the mix, and a null control says which part
|
||
|
||
No container here has a sound card, so "P6 works" cannot be answered by
|
||
listening. `docs/port/AUDIO-VERIFICATION.md` splits the question into three, and
|
||
these are the two that need no device.
|
||
|
||
### 1. The exported files, measured off the finished assets
|
||
|
||
```
|
||
se back -> audio/se/back.ogg (0.344 s, peak -5.7 dBFS)
|
||
se confirm -> audio/se/confirm.ogg (1.016 s, peak +0.2 dBFS)
|
||
se move -> audio/se/move.ogg (0.533 s, peak -1.4 dBFS)
|
||
bgm main_menu -> audio/bgm/main_menu.ogg
|
||
(87.744 s, peak -7.7 dBFS, bank BGM_103.slb, 3 sub-waves)
|
||
```
|
||
|
||
`sylpheed-export check export` passes: 16 screens validate, and every audio entry
|
||
carries a peak and a duration inside its bounds. The three cue durations match
|
||
HANDOFF Q8 at every published digit — see the refutation record above.
|
||
|
||
### 2. The engine, recorded off the Master bus
|
||
|
||
```
|
||
godot --path port -- --menu --script=down,down,accept,cancel --audio=…/p6.wav
|
||
→ recorded 6.037 s of Master bus (driver Dummy)
|
||
peak 0.0 dBFS, RMS −21.1 dBFS
|
||
```
|
||
|
||
**Non-silent is not the claim.** A WAV of the right duration full of the *bed*
|
||
would look exactly like this, and the cues could be missing entirely. So the cue
|
||
was isolated with a **null control**: the same scripted walk with ⬅ in place of ⬇.
|
||
Left/right are measured no-ops (Q5) and fire nothing, so the two runs differ by
|
||
exactly two move cues and nothing else — same screens, same transitions, the same
|
||
Ⓐ and Ⓑ cues in both, the same bed.
|
||
|
||
| | RMS |
|
||
|---|---|
|
||
| walk with two ⬇ presses | −21.9 dBFS |
|
||
| walk with two ⬅ presses (null) | −22.1 dBFS |
|
||
| **difference** | **−34.6 dBFS** |
|
||
|
||
The difference is not spread over the run. It is **one burst beginning at
|
||
t = 1.10 s and lasting 0.55 s** — two overlapping 0.533 s move cues — with 22 of
|
||
237 windows above −70 dBFS and silence everywhere else, including across the Ⓐ
|
||
and Ⓑ presses, which cancel because both runs make them. That is the cue reaching
|
||
the bus, separated from the music that was playing over it.
|
||
|
||
### The control that proved nothing, kept because it nearly passed
|
||
|
||
The first attempt paired `--script=down,down` against `--script=left,left`. The
|
||
difference was **bit-identical zero**, which reads as "the cues never reached the
|
||
bus" and would have been reported as a bug.
|
||
|
||
It was neither. Both runs recorded **1.115 s** while the first press lands at
|
||
~1.17 s: the control ended before the event it was controlling for. A null result
|
||
from an instrument that was not running is not a null result — PROTOCOL's "run
|
||
your own instrument through a control" applies to the control too.
|
||
|
||
### What this does NOT establish
|
||
|
||
* **That it sounds right.** Everything above is correspondence and separation,
|
||
not judgement. A ten-second human listen still answers something no measurement
|
||
here does.
|
||
* **That the bed is at a sane level against the cues.** 🔴 The Master bus peaks at
|
||
**0.0 dBFS** in the four-step run — the `confirm` cue is +0.2 dBFS on its own,
|
||
so any music under it puts the mix on the ceiling. Per-file levels are the
|
||
disc's and are fine; the **runtime** mix has no headroom. The port has not set a
|
||
bus balance, because nothing measures one and an invented balance is the same
|
||
class of mistake as an invented loop point. Recorded here rather than fixed
|
||
quietly.
|
||
* **That "Dummy driver" means heard.** It does not, and the run prints the driver
|
||
name so a write-up cannot forget to say so.
|
||
|
||
### One bug, in two dialects, both about a temp filename
|
||
|
||
The temp-name-then-rename discipline this project uses everywhere broke twice in
|
||
this milestone, in two different tools, for the same underlying reason: **tools
|
||
dispatch on the extension, so a temp name must preserve it.**
|
||
|
||
* `run_ffmpeg` wrote `.back.ogg.partial` → *"Unable to choose an output format"*,
|
||
a hard failure before a byte was written.
|
||
* `boot.gd` wrote `p6.wav.part` → `save_to_wav` **appends** `.wav` when the path
|
||
does not end in it, producing `p6.wav.part.wav`; the rename then failed to find
|
||
its source, its return value was not checked, and the run printed a success
|
||
line naming a file that did not exist.
|
||
|
||
The second is the more dangerous shape, and it is the one this project has
|
||
already warned itself about: a confident line of output pointing at nothing. The
|
||
rename's return is now checked and the failure is loud.
|
||
|
||
## P3, reopened — the boot title was missing the `PRESS Ⓐ` plate, 2026-08-29
|
||
|
||
P3 passed its gate with a boot that ended on build 4 alone. `BLOCKED.md` carried
|
||
that as 🔴 from the start: both states were captured, so the art was never the
|
||
question — the *sequence* was, and it is behavioural, so the port had no oracle
|
||
for it.
|
||
|
||
It is answered. `docs/re/title-plate-delay-measured.md`
|
||
(`auto/no-disc-and-menu-captures` at `fb536df`, **not on `main`** at the time of
|
||
writing) measures two independent boots: the title presents **without** the
|
||
plate, and the plate arrives **2.13 s** later, the two runs agreeing to 6 ms.
|
||
|
||
### Two builds at once, as two `ScreenView`s
|
||
|
||
`ScreenView` draws one screen. The obvious change was to teach it about a
|
||
subordinate overlay screen; the change made was to put a **second `ScreenView`
|
||
in the same `SubViewport`**, after the first.
|
||
|
||
That is what "two builds at once" actually is. Each build has its own timeline,
|
||
its own textures and its own hold — the plate's group runs independently of the
|
||
title's, which is the entire content of the finding — and Node2D siblings already
|
||
paint in tree order. The alternative would have put an `if overlay` in every
|
||
method that walks elements, and would have expressed the same information less
|
||
directly. The export's `paint_order` still means what it always meant: an
|
||
ordering *within* a build.
|
||
|
||
### The delay is timed from where build 4 stops animating
|
||
|
||
Not from where the title first appears. This is the finding rather than a detail:
|
||
measured from first-draw the two oracle runs differ by **0.48 s**, because the
|
||
build-in itself ran 1.64 s and 2.13 s and the emulator's frame pacing during an
|
||
animation is not the game's clock. Measured from settle they differ by 6 ms.
|
||
|
||
So `_boot_done` — the moment the sequencer already had for "this screen has
|
||
reached its hold" — is the landmark, and the overlay is due `after_settle_seconds`
|
||
later. A number taken from the wrong instant here looks exactly like a
|
||
measurement.
|
||
|
||
### The overlay is attached to the BOOT STEP, not to the `title` screen
|
||
|
||
What was measured is the boot title. Whether the plate is there when the title is
|
||
reached *again* — by Ⓑ from the main menu, or after the attract movie — is not
|
||
measured, and hanging the overlay on the screen would quietly claim that it is.
|
||
So it lives on the boot step in `authored/flow.json`, and `_drop_overlay` takes
|
||
it away with the screen it belongs to. `BLOCKED.md` carries the gap.
|
||
|
||
### Refutation — the RE agent's instruction contradicts the RE agent's measurement
|
||
|
||
**The claim under test**, quoted from the finding's *"What the port should
|
||
author"*: draw build 4, *"when build 4 has settled, wait **2.13 s**, composite
|
||
build 2 over it"*.
|
||
|
||
**It does not reproduce the measurement it came from**, and the gap is 3.97 s.
|
||
Build 2 is not a static plate: it has a group, and this port plays groups.
|
||
`press_start` has one element, `ptbtn00`, and its `fade_argb` reads
|
||
|
||
```
|
||
t=214 0x00ffffff pos (383, 560) invisible
|
||
t=236 0x00ffffff pos (383, 550) still invisible, having slid 10 px up
|
||
t=238 0xffffffff full alpha
|
||
t=244 0xffffffff holds
|
||
— 0x00ffffff the exit, untimed
|
||
```
|
||
|
||
At the measured 60 units/s that is **3.967 s** from the group's start to full
|
||
alpha. Compose the instruction with the group and the plate is first *visible* at
|
||
settle + 2.13 + 3.97 = **settle + 6.10 s**. What was measured — the glyph counter
|
||
leaving its no-plate value of 154 — is the plate becoming visible at **settle +
|
||
2.13 s**.
|
||
|
||
Neither obvious reconciliation works:
|
||
|
||
| reading | plate visible at | measured |
|
||
|---|---|---|
|
||
| both groups start together | 3.97 s (build 4 settles at **4.350 s**) — i.e. 0.38 s *before* settle | settle + 2.13 s |
|
||
| build 2's group starts at settle | settle + 3.97 s | settle + 2.13 s |
|
||
| build 2's group starts at settle + 2.13 s (the instruction) | settle + 6.10 s | settle + 2.13 s |
|
||
|
||
To land on the measurement, build 2's group has to start **2.51 s** after build
|
||
4's, which is not a landmark of anything.
|
||
|
||
**Verdict: the instruction is refuted as written; the measurement is untouched.**
|
||
The measurement is an observation of the running game and this port has no
|
||
standing to doubt it. What is refuted is the step that turns it into an
|
||
authoring rule, and that step is an interpretation.
|
||
|
||
**So the port ships the instruction, not its own arithmetic**, prints the
|
||
discrepancy on every boot, and files the row. This is the same call as the BGM
|
||
sub-waves and for the same reason: reconciling two of the RE agent's numbers is
|
||
a decoding question, and a port that quietly picks the one that looks right
|
||
destroys the evidence — a corrected boot looks exactly like a correct one.
|
||
|
||
The first thing to check is about the instrument rather than the game: is *"title
|
||
settled"*, the glyph counter first reading 154, the same instant as the port's
|
||
last-element settle (t=261, 4.350 s into the group)? If that landmark is earlier,
|
||
the gap closes with nothing else moving.
|
||
|
||
### Refuting the port's own claim: things in this export DO pulse
|
||
|
||
`BLOCKED.md` has carried this since P2, under the port's own raised question
|
||
about whether groups loop:
|
||
|
||
> no element's alpha reverses direction anywhere in this export, so nothing
|
||
> pulses, which removes the obvious reason to expect a loop without disproving
|
||
> one.
|
||
|
||
**`ptbtn00` reverses.** `0x00` → `0xff` → `0x00`, in the table above, in the
|
||
export, the whole time. The claim was never checked against `press_start`; it was
|
||
checked against the screens P2 happened to be animating. The RE agent has now
|
||
measured the running game pulsing this exact element at a mean 2.24 s.
|
||
|
||
So the reason to expect a loop is back — and the port still does not draw one,
|
||
because **no reading of this group produces 2.24 s**: the whole group is 268
|
||
units = 4.47 s, and from its first keyframe 54 units = 0.90 s. The plate is drawn
|
||
arriving and then holding at its settle (t=238, alpha `0xff`), which is what
|
||
every other screen does and what the static oracle capture
|
||
`live-title-press-a.png` shows. Which instant a repeat restarts from is filed,
|
||
not guessed.
|
||
|
||
### `--boot --capture=` — one frame instead of six hundred
|
||
|
||
The boot had no artifact of its own except `--film`, a PNG every 0.25 s for the
|
||
whole 156 s run, to answer one question: is the plate on top of the title at the
|
||
end. `--capture` was a `--screen`-only flag taken in `_ready`, which for a boot
|
||
run is 150 s too early. It is now deferred to the end of the sequence when
|
||
`--boot` is given.
|
||
|
||
### P3 gate — the boot ends on two builds
|
||
|
||
```
|
||
godot --path port -- --boot --capture=…/p3-plate.png
|
||
→ boot sequence complete after 155.86 s, holding on title
|
||
overlay press_start due at 157.99 s (+2.13 s after settle)
|
||
overlay press_start raised at 158.00 s, 1 element(s), settles at t=238
|
||
⚠ plate raised at settle+2.13 s but its own group reaches full alpha 3.97 s
|
||
later, so it is first VISIBLE at settle+6.10 s -- the measurement is
|
||
settle+2.13 s.
|
||
boot ends on title + press_start at 161.99 s
|
||
drew 16: ptbase2, ptloop01, …, ptcopyright
|
||
overlay press_start at t = 261.00 units, drew 1: ptbtn00
|
||
```
|
||
|
||
The PNG shows the title logo with **`PRESS Ⓐ BUTTON`** under it — build 4 and
|
||
build 2 in one frame, which this port had never drawn.
|
||
|
||
Two things the run made obvious and that are now fixed:
|
||
|
||
* **The capture reported only the base build's elements.** The first composited
|
||
capture printed `drew 16` and no mention of the plate, which reads as though
|
||
the overlay had not drawn at all. The overlay gets its own line; folding its
|
||
elements into the first list would have reported a screen that does not exist.
|
||
* **`--screen=<a> --overlay=<b>`** raises the same composite immediately, by the
|
||
same code path, with no delay. It exists because the only other way to see two
|
||
builds was a 156 s boot of which 137 s is the intro movie — and under Xvfb's
|
||
software Theora decode that is several minutes to answer "is the plate on top
|
||
of the title". It applies **no** delay: the delay is a measurement and lives in
|
||
`authored/flow.json`. The boot-mode narration is suppressed there, because a
|
||
log line that describes a sequence it is not running is worse than no log line.
|
||
|
||
## P5 — the focus ring spins, 2026-08-29
|
||
|
||
The ring was drawn at 0° and the file said so: *"THIS IS KNOWN TO BE WRONG, and
|
||
is drawn anyway because the right answer is a guess."* What was missing was the
|
||
**period**, and it is now measured — `docs/re/focus-ring-spin-measured.md`
|
||
(`auto/no-disc-and-menu-captures` at `4fa3099`): a continuous spin, from eight
|
||
evenly spaced autocorrelation peaks over nine revolutions, **with no angle
|
||
estimated anywhere** — both angle estimators failed their own controls and were
|
||
not used.
|
||
|
||
### The period comes off the disc; the RE agent supplied only that it repeats
|
||
|
||
`ptbtneff01` declares two keyframes that differ in **nothing but**
|
||
`rotation_deg`, 0 → 360, the first timed at `t = 120` and the second untimed.
|
||
The port turns once per **120 units**. Nothing is authored: the number is on the
|
||
disc, and what the measurement adds is that the turn **repeats** rather than
|
||
stopping at 360 = 0, which "groups hold" could not distinguish because those are
|
||
the same pose.
|
||
|
||
`ScreenView.spin_period_units` is the rule, and it is structural and narrow:
|
||
exactly two keyframes, differing only in rotation, by a full 360, first timed and
|
||
second untimed. **Disc-wide check over this export: 16 of 212 elements match, and
|
||
all 16 are focus rings** — `ptbtneff01` on the five main-menu buttons and
|
||
`ptbtneff02` on the three `EXTRAS` buttons, in both locales, every one declaring
|
||
`t = 120`. Zero false positives.
|
||
|
||
That check is the point rather than a formality. The measurement was taken on
|
||
**one** button of **one** screen; a rule that also caught something else would be
|
||
extrapolating it to elements nobody watched.
|
||
|
||
⚠️ It is a rule about **shape**, not a decoded field. Nothing on the disc says
|
||
"this loops". The day a loop flag is decoded, this goes.
|
||
|
||
### Verified on the port's own render, with the RE agent's own control
|
||
|
||
Captures at `--time=` 2.0 … 4.0 s on the settled main menu, `ptbtn01` focused:
|
||
|
||
| | |
|
||
|---|---|
|
||
| t=2.0 vs t=4.0 (one full period apart), **whole frame** | **0.0000 / 255** — bit-identical |
|
||
| t=2.5, 3.0, 3.5 against t=2.0, inside the ring's box | 3.60, 3.71, 3.58 / 255 |
|
||
| sum of box luminance across **eight** phases | spread **0.027 %** of the mean |
|
||
|
||
The last row is deliberately the RE agent's own observable: they separated
|
||
rotation from a brightness pulse by showing total annulus brightness is conserved
|
||
while per-bin brightness moves. The port's render conserves it to 0.027 % (theirs
|
||
was 0.4 % over 16 s, with capture noise in it). A filmstrip of the four
|
||
quarter-period phases shows the bright head at top, right, bottom, left.
|
||
|
||
### Two things it does not settle
|
||
|
||
* **Direction.** The port turns 0° → +360°, the sign the disc declares. No signed
|
||
angle was ever measured — the estimator that would have given one failed its
|
||
control and was not used.
|
||
* **Phase across a focus change.** The port drives the ring off the **screen**
|
||
clock, so moving the cursor does not restart the turn. The alternative — the
|
||
record's group restarting when the record is instantiated — is the stronger
|
||
claim, and the oracle run held focus on one button throughout, so nothing
|
||
separates them. Two frames straddling a focus change would.
|
||
|
||
## P3, corrected — the plate needs no authored delay at all, 2026-08-29
|
||
|
||
Last iteration the port refuted the RE agent's authoring instruction (*"when
|
||
build 4 has settled, wait 2.13 s, composite build 2"*) with arithmetic off the
|
||
disc, shipped the instruction anyway rather than pick between two of their
|
||
numbers, and printed the discrepancy on every boot.
|
||
|
||
**The refutation held, and the answer that came back is better than either option
|
||
the port offered: author nothing.** `5b0a6e6`.
|
||
|
||
### The premise that failed was the port's, and it will bite again
|
||
|
||
> 🔴 **`rest.t` is not when a screen settles.** It is the last *hold* keyframe
|
||
> before the exit.
|
||
|
||
Checked here rather than taken on trust. `title`'s `ptlogo1`:
|
||
|
||
```
|
||
t=26 (-116,-7) 150% a=0x00 the pre-roll
|
||
t=42 (179,186) 101% a=0xe0 it has arrived
|
||
t=251 (184,193) 100% a=0xff 5 px and 31 alpha steps later, 3.5 s on
|
||
```
|
||
|
||
It **stops moving at t=42** and then creeps for 209 units. `rest.t = 251` is the
|
||
end of that creep, not the arrival. The title's visible build-in is over at
|
||
**`t = 118`**, where `pteff01`, `pteff02` and `ptlogoall_eff` finish together.
|
||
|
||
Every reconciliation the port computed last iteration was wrong by exactly that
|
||
error: reading `rest.t` put build 4's arrival at 4.350 s instead of 1.967 s, and
|
||
the "2.51 s, which is not a landmark of anything" that looked so damning is
|
||
`(4.350 − 1.967) + 0.13` — the error itself, wearing a decimal point.
|
||
|
||
### One clock, and the interval is declared
|
||
|
||
| | units |
|
||
|---|---|
|
||
| build 4's last build-in ramp | `t = 118` |
|
||
| `ptbtn00` reaches `a = 255` | `t = 238` |
|
||
| **difference** | **120 units = 2.000 s** |
|
||
|
||
Measured: **2.138 s** and **2.132 s**. The 6.7 % is presentation rate — 120 units
|
||
in 2.135 s is 56.2 units/s, the emulator running 28.1 fps against a nominal 30,
|
||
and the corpus had independently measured the idle title at 28.5 fps *before*
|
||
these runs.
|
||
|
||
So `authored/flow.json` carries `"clock": "shared"` and **no delay**, `boot.gd`
|
||
raises the overlay when the step's screen loads rather than at its settle, and
|
||
`overlay.time_units = view.time_units` — assigned, not accumulated, because two
|
||
independently advanced clocks drift by a frame here and there and the whole
|
||
content of the finding is that 120 units is a fixed interval on one timeline.
|
||
|
||
⚠️ **The general hazard, stated by the RE agent and worth repeating where the
|
||
port will read it: discount a wall-clock number off that oracle by ~6 %.** It is
|
||
Canary's presentation rate baked into whatever it measures. A port at a true
|
||
30 Hz that authored 2.13 s would be visibly late.
|
||
|
||
### Refutation — two of the RE agent's numbers for the same 120 units disagree by 2 %
|
||
|
||
Both findings measure the same declared quantity: **120 keyframe units of wall
|
||
clock, during a static hold, in Xenia Canary.**
|
||
|
||
| | | implied presentation |
|
||
|---|---|---|
|
||
| plate: settle → plate, two runs | 2.138, 2.132 s → mean **2.135 s** | 28.10 fps |
|
||
| ring: one revolution, seven spacings | 2.18 2.16 2.18 2.16 2.16 2.20 2.20 → mean **2.177 s** | 27.56 fps |
|
||
| **disagreement** | **0.042 s = 1.97 %** | |
|
||
|
||
That is **seven times** the plate finding's own run-to-run agreement of 6 ms, and
|
||
it lands on the argument that finding uses to justify trusting itself: *"the
|
||
build-in is where frames are dropped; the static hold is not. A model in which
|
||
the game's own timing varied would have to move both."* Two static-hold
|
||
measurements are exactly what should agree under that model.
|
||
|
||
A second, smaller arithmetic slip in the same place: the ring page reconciles
|
||
2.177 s against a band of "27.6–28.8 fps", saying the measurement *"sits at the
|
||
top of that band"*. It does not sit in it. 60 rendered frames at 27.6 fps is
|
||
2.1739 s; the mean needs **27.56 fps** and the two slowest spacings (2.20 s)
|
||
need **27.27 fps**. Four of the seven spacings are above the band's top.
|
||
|
||
**Verdict: the containment claim is refuted; the spin, the period and the
|
||
reconciliation are untouched.** Either the presentation rate genuinely differed
|
||
between the two sessions — which the plate page's own corroboration argues
|
||
against for static holds — or the ring's revolution is not exactly 120 units.
|
||
The corpus should say which, because they are the same claim measured twice.
|
||
|
||
🟢 **Nothing in the port moves either way.** `spin_period_units` uses the
|
||
declared 120 units at 60 units/s = **2.000 s of port time**, which is the
|
||
true-30 Hz value both readings agree the disc means. This is a corpus
|
||
consistency problem, not a port one — which is why it is filed rather than
|
||
worked around.
|
||
|
||
### The corrected boot ended one build too early, and the capture showed it
|
||
|
||
Moving the plate onto the shared clock also moved the boot's exit, and the first
|
||
capture taken afterwards was **visibly darker** than the one before it. The cause
|
||
is `pteff00`, the title's black fade quad: it ramps `0xff000000` → `0x00000000`
|
||
over t=16…261, so at t=243 — where the run was quitting, `overlay.settle_time()`
|
||
after the overlay was raised — the frame is still ~7 % black.
|
||
|
||
The plate arrives at t=238; **build 4 is not finished until t=261**. The boot now
|
||
ends at the later of the two, and says which in the log:
|
||
|
||
```
|
||
-> title at 145.79 s
|
||
overlay press_start raised at 145.79 s, 1 element(s), settles at t=238
|
||
boot ends at 150.14 s, once both builds have arrived (t=261)
|
||
```
|
||
|
||
Worth recording because of how it presented: nothing failed, no warning was
|
||
printed, and the only symptom was a frame slightly darker than the previous
|
||
run's. A gate artifact that silently drifts is the failure mode this project
|
||
keeps meeting — and it was caught only because there was a previous capture to
|
||
compare against.
|
||
|
||
## P7 — the new-game intro, 2026-08-29
|
||
|
||
`S00A.wmv` has been in `export/video/` since P4 (MISSION §6 put both movies in
|
||
scope at once). What P7 needed was for something to *play* it and for the run to
|
||
end somewhere defined.
|
||
|
||
### The port skips two measured screens, and says so on screen
|
||
|
||
The real chain is **`NEW GAME` → `DIFFICULTY` → `SELECT DATA` → Ⓐ on a save slot
|
||
→ ~4.5 s → `S00A`** (HANDOFF Q4 measured the screens, Q9 decoded the movie and
|
||
then measured its onset off the running game at 0.96–1.000 with a strictly
|
||
monotone playhead over 25 consecutive 0.5 s samples).
|
||
|
||
`DIFFICULTY` and `SELECT DATA` are measured destinations that are **not
|
||
`GP_TITLE` builds**, so no screen file exists to go to. The port therefore jumps
|
||
from `NEW GAME` to the one thing in that chain it has.
|
||
|
||
That is a gap, not a sequence, and the whole design here is about not letting it
|
||
read as one:
|
||
|
||
* `MenuFlow.accept` returns a **new kind**, `video`, rather than folding this
|
||
into `blocked`. The caller has to announce the skip, and a distinct kind is
|
||
what forces it to.
|
||
* The runtime prints it every time:
|
||
`(NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
|
||
Neither screen is in this export.`
|
||
* `authored/flow.json` carries `skipped_chain` as **data**, so the names of what
|
||
is missing live beside the decision rather than inside a GDScript string.
|
||
|
||
A port that quietly jumped from `NEW GAME` to the intro would be showing a
|
||
sequence the game does not have, with nothing on screen saying so. That is the
|
||
exact failure this project keeps meeting from the other direction.
|
||
|
||
### What happens after the movie is authored, and had to be
|
||
|
||
The game goes into **mission 1**. Gameplay is out of scope (PORT-MISSION §7), so
|
||
"returns to a defined state" is a decision, and P7's gate says as much. The port
|
||
returns to the **title**: the boot's own end state, so a run that finishes the
|
||
intro lands somewhere a player can start again from. Nothing measured says the
|
||
game does this, and `after_video.kind` is `"authored"`.
|
||
|
||
### The 4.5 s gap is left empty on purpose
|
||
|
||
Q9 measures the movie starting ~4.5 s after Ⓐ on the save slot. What is on screen
|
||
for those 4.5 s was never observed — the run that would have shown it hit the
|
||
documented `sub_823070B0` cache crash after `SELECT DATA`.
|
||
|
||
`GP_TITLE` **does** carry a loading screen (below), and 4.5 s is about the right
|
||
shape for one. That is precisely why it is in `BLOCKED.md` and not in
|
||
`flow.json`: a plausible filler that nobody watched is the kind of thing that is
|
||
indistinguishable from a measurement a month later.
|
||
|
||
### A script timeout that would have failed every movie
|
||
|
||
`--script`'s per-step timeout is 20 s, to stop an unattended run waiting forever
|
||
on a screen that never settles. `S00A` is **93.9 s**, so the first scripted
|
||
new-game run would have been killed at step 1 and reported as "never settled".
|
||
|
||
Raising the constant would have been wrong in the other direction: a movie stuck
|
||
at frame 0 would then hang the job, and a job that waits is worse than a job that
|
||
fails, because it does not look like a failure.
|
||
|
||
So the test is **liveness, not duration**: while `get_stream_position()`
|
||
advances, the deadline moves with it; a stalled movie still trips the same 20 s.
|
||
|
||
### Found while looking: `GP_TITLE`'s four unnamed builds are LOADING screens
|
||
|
||
`build_00`, `build_01`, `build_12`, `build_15` have never had names. Every
|
||
element in all four is `pgloading_*` — `pgloading_processing.png`,
|
||
`pgloading_circle1`, `pgloading_delta`, `pgloading_ring` — and `LOADING` is one
|
||
of the three screen names the RE agent read out of the title part's state
|
||
function.
|
||
|
||
Two variants: 0/1 carry 7 elements, 12/15 carry 10.
|
||
|
||
**They are not renamed here.** The archive's own pairing (adjacent for 2/3, `+3`
|
||
for 4…9 and for 10/13, 11/14) suggests 0 is 1's twin and 12 is 15's, but which
|
||
member of each pair is which **locale** is an inference, and a name is exactly
|
||
the kind of thing that stops being questioned once written. Handed to the RE
|
||
agent, who can answer it from a capture in one look. `BLOCKED.md` has the row.
|
||
|
||
⚠️ **And one of them is a second casualty of the `rest.t` problem.**
|
||
`pgloading_eff00.prm` on entries 12/15 is a full-screen black quad whose group
|
||
runs `0xff000000` at t=38 → `0xff000000` at t=48 → `0x00000000` untimed: black,
|
||
held, *then* clear. Its `rest.t` is **38**, where it is fully opaque. A port that
|
||
draws that screen at its declared rest draws **a black rectangle over the entire
|
||
loading screen**. The title's case only dimmed a frame; this one hides
|
||
everything. Filed with the `settle_time()` row it belongs to.
|
||
|
||
### Refutation — attempted on the fade-quad census; it survives, with a caveat
|
||
|
||
**The claim** (HANDOFF, on transitions): *"in `GP_TITLE` exactly the six screen
|
||
builds carry it while the six overlays do not"*, where "it" is the full-screen
|
||
black `.prm` quad *whose keyframe group is the transition*.
|
||
|
||
**The test**, over the whole export: count builds carrying a full-screen
|
||
primitive with black in its keyframes.
|
||
|
||
```
|
||
16 builds exported; 12 carry one.
|
||
Of the 12 `is_build` bundles (excluding the 4 authored splashes): 8 carry, 4 do not.
|
||
carry: title, title_jp, main_menu, main_menu_jp, extras, extras_jp,
|
||
build_12, build_15
|
||
do not: press_start, press_start_jp, build_00, build_01
|
||
```
|
||
|
||
8 and 4, not 6 and 6. But the two extras are `build_12` / `build_15`, and their
|
||
quad is a **different shape**:
|
||
|
||
| | transition quad (`pteff00.prm`, title) | loading quad (`pgloading_eff00.prm`) |
|
||
|---|---|---|
|
||
| | `0xff000000` t=16 | `0xff000000` t=38 |
|
||
| | `0x00000000` t=261 | `0xff000000` t=48 |
|
||
| | `0xff000000` untimed | `0x00000000` untimed |
|
||
| shape | black → clear → **black** | black → held → **clear** |
|
||
|
||
The transition quad returns to black on exit; the loading quad does not. Read
|
||
strictly — the quad *whose group is the transition* — the claim holds.
|
||
|
||
**Verdict: survives.** The refinement is worth recording anyway, because the
|
||
naive test over-counts by two and somebody will run the naive test. There are
|
||
**two kinds** of full-screen black `.prm` in `GP_TITLE`, and only one of them is
|
||
a transition.
|
||
|
||
## P7 gate
|
||
|
||
```
|
||
godot --path port -- --menu --script=accept --audio=…/p7.wav
|
||
```
|
||
|
||
```
|
||
menu on main_menu, focus ptbtn01
|
||
script[1] accept
|
||
(NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
|
||
Neither screen is in this export.
|
||
-> video S00A at 1.18 s (/work/export/video/S00A.ogv)
|
||
video ended at 94.93 s
|
||
-> title (authored: authored)
|
||
menu on title, focus (none -- this screen has no focusable item)
|
||
script complete after 99.28 s on title
|
||
recorded 98.453 s of Master bus (driver Dummy)
|
||
```
|
||
|
||
The movie ran **93.75 s** against a declared 93.9 s, the run ended on the title,
|
||
and the Master bus recorded 98.453 s: `pcm_s16le`, 44.1 kHz stereo, RMS
|
||
**−22.2 dBFS**.
|
||
|
||
**What this does not show, stated because it would be easy to imply otherwise:**
|
||
the recording contains the menu bed *and* the movie together, and this run did
|
||
**not** separate them. So it establishes that the engine reached an output for
|
||
98 s of a run whose middle 94 s was a movie — not that `S00A`'s own audio track
|
||
is in the mix. Separating them wants the P6 null-control method (a paired run
|
||
that differs only in the movie), and that is not done here.
|
||
|
||
🔴 **Peak 0.0 dBFS again.** The same runtime-headroom problem P6 filed: per-file
|
||
levels are the disc's and are fine, the Master bus has no headroom, and the port
|
||
has set no bus balance because nothing measures one.
|
||
|
||
### One more file read while it was being written
|
||
|
||
`ls` reported the recording as **3 702 828 B**; `ffprobe` on the finished file
|
||
reports **17 367 084 B / 98.452608 s** — a factor of 4.7. `ffprobe` is right and
|
||
the `ls` caught it mid-flight.
|
||
|
||
`AUDIO-VERIFICATION.md` opens by naming this failure and the port has had the
|
||
temp-name-then-rename discipline since P6, which is what makes it worth writing
|
||
down rather than shrugging off: **the discipline protects a reader who opens the
|
||
path, and it does not protect a reader who stats it at the wrong moment.** Size
|
||
on disk is not a measurement of a file somebody else is still writing. Ask the
|
||
decoder, not the directory entry.
|
||
|
||
## Modding — rule 4 was never implemented, 2026-08-29
|
||
|
||
`docs/port/MODDING.md` is explicit that modding is *"a design constraint on the
|
||
exporter today — not a milestone to add later"*, and its rule 4 is base-and-
|
||
overrides: a mod replaces a file by **shadowing its path**, so a modder edits
|
||
nothing under the derived tree and re-exporting is always safe.
|
||
|
||
**Nothing read `data/mods/` at all.** The directory has existed since the
|
||
monorepo merge with a `.gitkeep` in it and no code path anywhere — exporter or
|
||
runtime — that looked at it. Eight milestones shipped past that.
|
||
|
||
### One resolver, and every read goes through it
|
||
|
||
`ExportTree.resolve(rel)` returns the mod tree's copy when one exists and the
|
||
derived tree's otherwise. `read_json`, `texture`, `video` and `MenuAudio` all
|
||
call it, so a mod can replace **a screen's JSON, a sprite, a cue, the music bed
|
||
or a movie** — every asset kind the port reads.
|
||
|
||
`MenuAudio` was reading `tree.root.path_join(...)` directly and had to be
|
||
changed. Left alone it would have made audio the one asset kind a mod could not
|
||
touch, for no reason a modder could have guessed — which is the failure mode
|
||
rule 4 exists to prevent.
|
||
|
||
There is deliberately no manifest of what a mod contains and no registration
|
||
step: **the path is the registration**, which is the whole of the rule.
|
||
|
||
⚠️ **One tree, not a stack.** Several mods layering over each other needs a load
|
||
order, and a load order needs a rule nobody has asked for. Said out loud in
|
||
`data/mods/README.md` rather than answered.
|
||
|
||
### A modded run must not look like an unmodded one
|
||
|
||
Every shadowed file is printed the first time it is read:
|
||
|
||
```
|
||
mod: sprites/title/main_menu/ptbtn01.png <- /work/data/mods/sprites/…/ptbtn01.png
|
||
```
|
||
|
||
MODDING says *"did I break it?"* is answered by disabling a mod. That is a fine
|
||
last resort and a poor only resort, so the log names the replacement instead.
|
||
|
||
**The first version of this got it wrong in an instructive way**: it printed a
|
||
summary in `_ready`, before a single asset had been read, and so always said
|
||
`(nothing shadowed yet)`. A report structurally incapable of reporting anything
|
||
is worse than no report, because it looks like an answer. It now announces each
|
||
shadow at the moment it happens.
|
||
|
||
### Gate
|
||
|
||
A synthetic 203×43 magenta PNG — nothing disc-derived — dropped at
|
||
`data/mods/sprites/title/main_menu/ptbtn01.png`:
|
||
|
||
| | |
|
||
|---|---|
|
||
| pixels changed between the two renders | **8 501** of 921 600 (0.92 %) |
|
||
| bounding box of the change | x 542…744, y 162…204 — **203×43**, the sprite's own size |
|
||
| `sylpheed-export check export` afterwards | 16 screens still validate |
|
||
|
||
The changed region is exactly the sprite and nothing else moved.
|
||
|
||
### `data/mods/` was not gitignored, and that is a hole in a hard rule
|
||
|
||
*"Never commit game assets"* has been enforced on `export/` and `data/base/`
|
||
since P0. But **a mod is usually an edited game asset**, and `data/mods/` was
|
||
fully tracked — so the one directory a user is invited to put modified sprites in
|
||
was the one directory git would happily take them from.
|
||
|
||
`.gitignore` now excludes everything under it except the README.
|
||
|
||
### The naming split is not mine to resolve
|
||
|
||
`MODDING.md` describes the tree as `data/base/`; `PORT-MISSION.md` §3, the
|
||
exporter, `ExportTree` and `.gitignore` all say `export/`. Both are mission
|
||
files, and PROTOCOL is clear that **only the human changes a mission**, so this
|
||
is raised rather than picked. `.gitignore` has ignored both names on purpose
|
||
since P0.
|
||
|
||
It matters here for one concrete reason: MODDING's layout has `base/` and `mods/`
|
||
as **siblings**, and today they are not — the tree is `export/` at the repo root
|
||
while mods are `data/mods/`. The resolver takes `SYLPHEED_MODS` or defaults to
|
||
`data/mods/`, which is what exists; if the tree is ever renamed to `data/base/`
|
||
the sibling rule becomes natural and that default can go.
|
||
|
||
## Refutation — the paint-order key, and the reach of its tie-break
|
||
|
||
**The claim** (HANDOFF Q3): paint order is *"a `u16` layer key at `+0x0A`,
|
||
**decoded**"*, with the tie-break filed 🟡 as *"eight candidates refuted; costs
|
||
one element's blend on one screen"*.
|
||
|
||
**First pass: 2 of 16 screens did not match** a stable sort by layer key — both
|
||
loading screens, `build_12` and `build_15`.
|
||
|
||
**That was my test, not the claim.** `pgloading_eff00.prm` carries **no layer key
|
||
at all** — `layer: null`, `layer_source: "none"`: it is a primitive with no
|
||
sprite header, and the exporter's implied-name fallback produces nothing either.
|
||
My sort put a keyless element first; the decoders put it **last**.
|
||
|
||
Completing the rule as *"stable sort by layer key, elements with no key last"*
|
||
gives **16 of 16**. And last is right: `pgloading_eff00` is the full-screen black
|
||
quad, and HANDOFF's own sentence is that the fade quad paints last.
|
||
|
||
**Verdict: survives, with the rule completed.** Worth recording because the
|
||
published statement does not say where a keyless element goes, and there is at
|
||
least one in the archive.
|
||
|
||
🟡 **But the tie-break's reach looks understated.** Census over this export:
|
||
|
||
```
|
||
elements sharing a layer key with another element: 105, across 12 of 16 screens
|
||
```
|
||
|
||
HANDOFF characterises the cost as *"one element's blend on one screen"*. 105
|
||
elements on 12 screens is a much larger surface than that. Most of those ties are
|
||
probably invisible — two elements that share a key and never overlap cannot show
|
||
a difference — but *probably* is doing the work in that sentence, and nothing has
|
||
measured which. The port is unaffected either way: it draws
|
||
`ui_layout::derived_paint_order` verbatim and derives no order of its own.
|
||
|
||
## Correction — the runtime "clipping" I flagged 🔴 twice was overstated
|
||
|
||
P6 and P7 both filed 🔴 *"the runtime mix has no headroom"* on the strength of a
|
||
peak reading of 0.0 dBFS off the Master bus. Measured properly:
|
||
|
||
| | samples at full scale | of total | longest clamped run |
|
||
|---|---|---|---|
|
||
| P6 walk (5.944 s) | 43 | 0.0082 % | 10 samples — **0.23 ms** |
|
||
| P7 new-game run (98.453 s) | 24 | 0.00028 % | 11 samples — **0.25 ms** |
|
||
|
||
That is not a headroom defect. It is the disc's own `confirm` cue, mastered near
|
||
full scale (+0.18 dBFS after a lossy decode), touching the ceiling for a quarter
|
||
of a millisecond on a transient — and possibly only in the recording's 16-bit
|
||
conversion, since Godot mixes in float and `AudioEffectRecord` saves `s16`.
|
||
|
||
**Nothing is changed, and that is the point.** Attenuating the mix to buy
|
||
headroom would be an unmeasured decision about level — the same class of thing
|
||
this port refused for the BGM loop point and the stem balance. Refusing it there
|
||
and taking it here would be inconsistent, and it would trade an inaudible
|
||
0.25 ms clamp for an audible change nobody measured.
|
||
|
||
**A peak reading is not a clipping measurement.** One sample at 0 dBFS and two
|
||
seconds of square wave give the same number, and I reported the first as though
|
||
it were the second — twice, in red, in two milestones' write-ups.
|
||
|
||
## The P1 regression harness had been broken since the monorepo merge, 2026-08-29
|
||
|
||
`tools/port/verify-screen` is the P1 gate's regression detector: Godot's drawing
|
||
of a screen against `sylpheed-cli screen render` of the same build. It had not
|
||
been run since P1, across four milestones that changed the renderer — rotation,
|
||
the focus record, the spinning ring, two builds composited at once.
|
||
|
||
It could not have been run. **It resolves its reference binary to a path that
|
||
`build-reference-cli` stopped being able to produce.** That script greps
|
||
`crates/sylpheed-export/Cargo.toml` for
|
||
|
||
```
|
||
sylpheed-formats = { git = "…Syplheed-Reborn.git", rev = "…" }
|
||
```
|
||
|
||
and the monorepo merge (`65cefa7`) replaced that line with
|
||
`{ path = "../sylpheed-formats" }`. The grep returns nothing, the script exits 1,
|
||
and the binary left at `reference-cli/sylpheed-cli` is whatever predated the
|
||
merge — here, **three hours older than the sources** and built from a revision
|
||
nothing in the tree points at any more.
|
||
|
||
Running the diff against it would have compared the port to a decoder from
|
||
another era and called the result a regression check. `DECISIONS.md` already
|
||
carries *"The reference renderer was stale for three diff runs"* from P2. This
|
||
would have been the fourth, and the mechanism was different: not a forgotten
|
||
rebuild, but a **build step that could no longer succeed and a consumer that
|
||
only checked whether the file existed**.
|
||
|
||
### The fix is a deletion, not a repair
|
||
|
||
The revision-keying solved a two-repo problem: `/reborn`'s `target/` was a live
|
||
mount of the other agent's checkout and moved mid-run, so a pixel disagreement
|
||
against it had a free variable in it. **The monorepo removed that problem by
|
||
construction** — the exporter, the reference and the port now read one decoder,
|
||
the working tree's. So `verify-screen` builds `sylpheed-cli` from the workspace.
|
||
`SYLPHEED_CLI` still overrides for anyone who wants to pin one deliberately.
|
||
|
||
### The baseline, all 16 screens
|
||
|
||
```
|
||
build_00/01 max 3 over3 0 OK
|
||
press_start(_jp) max 1 over3 0 OK
|
||
title max 6 over3 790 DIFFERS
|
||
main_menu(_jp) max 4 over3 0 DIFFERS
|
||
extras(_jp) max 3 over3 0 OK
|
||
publisher_logo(_r) max 1-2 over3 0 OK
|
||
developer_logos(_r) max 2 over3 0 OK
|
||
title_jp max 155 over3 20498 DIFFERS
|
||
build_12/15 max 0 over3 0 OK
|
||
```
|
||
|
||
**No new drift.** Four milestones of renderer change and the only screen with a
|
||
substantial disagreement is `title_jp` — which is the *same* one P1 recorded and
|
||
left open: `ptlogo_eff2` is the single drawn element in the whole export at a
|
||
scale that is not a whole multiple of 100 % (125 %), and the two renderers pick
|
||
different source texels there. `ui_layout::blit` samples at the destination
|
||
pixel's top-left corner, a GPU at its centre. **The port has still not changed to
|
||
match**, because matching would mean reproducing a half-pixel bias on purpose to
|
||
make a number smaller. Only an oracle capture settles it.
|
||
|
||
`title`'s 790 pixels at ≤ 6/255 are the same class, one texel wide, on the logo's
|
||
scaled edges. `main_menu` and `main_menu_jp` say DIFFERS on a max of 4 with
|
||
**zero** pixels over the bar — a couple of pixels differing in a single channel.
|
||
|
||
### `max` alone could not tell 2 pixels from 25 000
|
||
|
||
The script reported only the largest difference anywhere in the frame, so
|
||
`main_menu` (two pixels) and `title_jp` (2.8 % of the frame) produced the same
|
||
verdict. It now also reports how many pixels are over the bar.
|
||
|
||
**The bar itself is not raised.** Tuning a threshold until things match is the
|
||
failure the script's own header warns about; adding a second number is
|
||
information, not a loosened bound. ⚠️ The count is thresholded on **greyscale
|
||
luma** while `max` is a per-channel maximum, so they are not two views of one
|
||
measurement — a per-channel check counts 957 on `title` where the luma count
|
||
says 790.
|
||
|
||
### What this harness cannot see, stated because the OK rows look reassuring
|
||
|
||
It renders `--pose=rest`. That is deliberate — it holds both renderers to the
|
||
same declared pose so the test is *port vs reference* and not *rest vs timeline*
|
||
— but it means **none of this iteration's or the last four's visible work is
|
||
under test**: not the spinning focus ring, not the plate composited over the
|
||
title, not any timeline behaviour, not audio. Sixteen OK rows are a statement
|
||
about the resting composite and nothing else.
|
||
|
||
And it remains what its header says: a consistency check between two renderers
|
||
that share their assumptions. Both have been wrong together three times —
|
||
`pteff05`, scale-0, `rest()` — and each time only a capture caught it.
|
||
|
||
## Refutation — "builds 0/1 and 10/11 are the loading screen" is false in the index space this export uses
|
||
|
||
**The claim**, from the RE agent 2026-08-29, answering the port's ask to name
|
||
`GP_TITLE`'s unnamed bundles: *"builds 0/1 and 10/11 are the loading screen,
|
||
decoded from their own `pgloading_*` element names."*
|
||
|
||
**In this export, entries 10 and 11 are the splash screens**, and it is not close:
|
||
|
||
| entry | elements |
|
||
|---|---|
|
||
| 10 | `palogo_eff0`, **`palogo_sqex`**, `palogo_sqex_eff` |
|
||
| 11 | `palogo_eff0`, **`palogo_gamearts`**, `palogo_seta`, `palogo_anima` … |
|
||
| 12 / 15 | `pgloading_eff00`, `pgloading_loop1`, `pgloading_str` … |
|
||
|
||
Entry 10 is the **SQUARE ENIX** wordmark and 11 the developer logos — which the
|
||
same agent identified, in the answer to the port's ask 1, as *"entries 10/13 are
|
||
the SQUARE ENIX publisher wordmark, the first thing the boot shows"*.
|
||
|
||
**Verdict: the finding is almost certainly right and the index space is wrong.**
|
||
Over the twelve bundles `is_build` accepts — entries 0,1,2,3,4,5,6,7,8,9,12,15 —
|
||
ordinals 10 and 11 are entries **12 and 15**, which are exactly the two dressed
|
||
loading variants. So "0/1 and 10/11" is the `is_build` ordinal, and this export
|
||
addresses by **pak entry index**.
|
||
|
||
**Why this is worth a section rather than a shrug.** `authored/screen_names.json`
|
||
is keyed by entry index, and the exporter's own comment says why: *"keyed by
|
||
ENTRY, not by the enumeration ordinal — widening the enumeration to reach the
|
||
splash renumbers the ordinals, and a name that moves when the rule changes is not
|
||
a name."* Someone reading that message and writing keys `"10"` and `"11"` would
|
||
**name the publisher wordmark and the developer logos as loading screens**, and
|
||
the export would validate, and the boot would still run.
|
||
|
||
Two enumerations of the same archive differ by exactly the four bundles the port
|
||
had to add an allow-list to reach. That is the sharpest possible demonstration of
|
||
why the exporter switched, and it has now nearly caused the error it switched to
|
||
prevent. Reported; the names are still the RE agent's to give.
|
||
|
||
## The intro's missing dialogue was an export gap, not a transcode bug, 2026-08-29
|
||
|
||
A human play-test heard music under the boot intro and no voices. The obvious
|
||
reading is that the 5.1→stereo fold dropped the centre channel, and it is wrong.
|
||
|
||
**`ADV.wmv` carries music and effects only.** On this disc a cutscene's voice is
|
||
a *separate asset*: one continuous XMA stream in `sound.pak`, bound to the movie
|
||
by the manifest in `tables.pak` (`ADV` → `VOICETRACK = VOICE_ADV`). Nothing was
|
||
dropped — `grep -rn voice crates/sylpheed-export/src/` returned nothing, because
|
||
the exporter had never been asked for it. The transcode was correct the whole
|
||
time, which is why every measurement on it passed.
|
||
|
||
That is worth stating plainly because the failure *looked* exactly like a codec
|
||
bug, and `docs/port/AUDIO-VERIFICATION.md` is full of ways to measure a
|
||
transcode against its source. Every one of them would have come back clean.
|
||
|
||
### The binding is resolved, and must never be matched by name
|
||
|
||
`audio::export_voice` takes exactly one route:
|
||
`media::resolve_movie_voice_region(source, movie, VoiceLang::English)`, which
|
||
walks movie → cue token (manifest) → sound id (registry) → a `[start, end)` byte
|
||
region of the continuous stream. The cheap route — read `VOICE_<movie>.slb` —
|
||
was not taken, and the reason is a measurement:
|
||
|
||
| movie | resolved region | inside the bank named after it? |
|
||
|---|---|---|
|
||
| `ADV` | 433 930 240…437 044 592 | yes |
|
||
| `S00A` | 452 798 464…455 499 120 | yes |
|
||
| `RT01A` | 437 044 592…437 345 648 | **no — it is inside `VOICE_ADV.slb`** |
|
||
|
||
⚠️ **Name-matching is correct on exactly the two movies this port ships, and
|
||
wrong on the radio cutscenes.** It would have exported clean, verified clean
|
||
against both in-scope movies, and returned the wrong recording the moment
|
||
anybody widened the export. This is the failure mode MISSION §2 names — one
|
||
playable thing is not one archive entry — in its most convincing disguise: the
|
||
spot-checks a person would actually run are the ones it passes.
|
||
|
||
### Three choices, and why none is a guess
|
||
|
||
* **One file per movie**, per MODDING rule 1, and the region's chunks are
|
||
**summed** — see the correction below, because the first version of this
|
||
paragraph said the opposite and was wrong.
|
||
* **Mono**, folded from the stream's **own declared channel count**, probed with
|
||
`ffprobe` rather than assumed. This is not pedantry: `pan` silently ignores a
|
||
channel the input does not have — measured this iteration on the 5.1 fold
|
||
below, where `FLC`/`FRC`/`SL`/`SR` vanished with no warning at all — so a
|
||
stereo matrix applied to a mono voice track is not an error, it is a −6 dB
|
||
attenuation that nothing reports. A track that is already mono is passed
|
||
through untouched.
|
||
* **No sync offset, and no length clamp.** The voice plays from the video's
|
||
first frame, so nothing is authored. The decoded length is recorded in the
|
||
manifest *beside the movie's own length* rather than trimmed to it: the voice
|
||
has no shared container to disagree with, so a length mismatch is the only
|
||
symptom a resolution error would ever show, and clamping would delete it. That
|
||
decision is the reason the error below was caught in the same hour it was made.
|
||
|
||
### Correction, within the hour — the chunks are stems, and I had concatenated them
|
||
|
||
The first version of `export_voice` joined the region's chunks end to end and
|
||
produced **359.201 s of voice for a 137.437 s movie**, and **255.460 s for a
|
||
93.779 s one**. Both ratios sit near 3, and both regions decode to 3 chunks.
|
||
|
||
The manifest said so on the first run, because the length was recorded against
|
||
the movie's instead of being clamped to it. A clamp — which is what
|
||
`sylpheed-viewer` does, and what `media`'s own doc comment invites with *"trimmed
|
||
by the caller's length clamp"* — would have produced a file of exactly the right
|
||
duration containing the wrong audio, and every check in
|
||
`docs/port/AUDIO-VERIFICATION.md` would have passed it.
|
||
|
||
Decoding each chunk and timing it (`crates/sylpheed-export/examples/voice_chunks.rs`):
|
||
|
||
| movie | movie length | chunk 0 | chunk 1 | chunk 2 |
|
||
|---|---|---|---|---|
|
||
| `ADV` | 137.437 s | 84.553 | **137.324** | **137.324** |
|
||
| `S00A` | 93.779 s | 68.072 | **93.694** | **93.694** |
|
||
| `RT01A` | — | 0.009 | **34.034** | — |
|
||
|
||
Chunks 1 and 2 are **equal to six decimals and each span the whole movie**. That
|
||
is HANDOFF Q10's decoded shape — *two stems of one performance, played together;
|
||
do not concatenate* — showing up on a second asset kind. They are summed at
|
||
`1/n`, exactly as `export_bgm` sums a music bank.
|
||
|
||
⚠️ **Chunk 0 is dropped and its status is open.** Its duration matches nothing:
|
||
84.6 s under a 137 s movie, 9 ms under `RT01A`. `docs/re/REFUTED.md` records
|
||
`to_xma_riffs`'s hybrid branch emitting a **leading headerless packet region**
|
||
ahead of the real `RIFF` waves, and `docs/port/BLOCKED.md` already carries that
|
||
as an open row against `BGM_103`, where `media` returns three sub-waves against a
|
||
census of two. **This is the same signature on an independent asset kind** — good
|
||
corroboration, not proof, and the port is not entitled to close it. So the
|
||
selection rule is written in terms of the measurement (*keep the longest
|
||
duration and everything tying with it*), and every dropped chunk is named in the
|
||
manifest with its length.
|
||
|
||
This is the media-assembly trap MISSION §2 names, and it caught me: I wrote a
|
||
doc comment asserting concatenation, gave the reason, and had it wrong. What
|
||
saved it was refusing to clamp — the one decision in the first version that was
|
||
made for the right reason.
|
||
|
||
### What a `None` means
|
||
|
||
A movie whose region does not resolve is **genuinely unvoiced** — the honest
|
||
answer for most `hokyu_*` resupply cutscenes — and gets a manifest warning, not
|
||
a substitute. The corpus already paid for the alternative: resolving unbound
|
||
movies through a shared demo line played the *wrong recording*.
|
||
|
||
This is **decoded, not authored**, so it runs outside the `authored/audio.json`
|
||
block in `main.rs`. Nothing new goes in `authored/`; there is nothing here we
|
||
decided.
|
||
|
||
## Refutation, of my own exporter — MISSION §6 pins a downmix matrix, and the exporter ships a different one
|
||
|
||
**The claim under test is the port's**, not another agent's, and it has been in
|
||
`video.rs` since P4: that the 5.1 fold is normalised by
|
||
`1/(1 + √½ + √½) = 0.4142` because *"the unnormalised form was measured too and
|
||
**clips**: peak 0.0 dBFS."*
|
||
|
||
That sentence rests on a peak reading. `docs/port/BLOCKED.md` records this port
|
||
withdrawing a 🔴 runtime-clipping flag on precisely the grounds that **a peak
|
||
reading is not a clipping measurement** — one sample at full scale and two
|
||
seconds of square wave give the same number. So the justification for deviating
|
||
from a matrix a human pinned was produced by an instrument this port has already
|
||
declared unfit for the question.
|
||
|
||
### Measured properly, over the whole of both movies
|
||
|
||
Decoded to 32-bit float so nothing is pre-clamped, then counted: samples at or
|
||
over full scale, how many exceed it by more than 1 dB, and the longest
|
||
consecutive run.
|
||
|
||
| | peak | RMS | ≥ full scale | > +1 dB | longest run |
|
||
|---|---|---|---|---|---|
|
||
| `ADV`, MISSION §6 matrix | **+4.26 dBFS** | −14.55 | **4 406** / 13 187 900 | 1 874 | 16 samples (0.333 ms) |
|
||
| `ADV`, exporter's matrix | −3.39 dBFS | −22.21 | 0 | 0 | — |
|
||
| `S00A`, MISSION §6 matrix | **−1.34 dBFS** | −18.73 | **0** | 0 | — |
|
||
| `S00A`, exporter's matrix | −8.99 dBFS | −26.39 | 0 | 0 | — |
|
||
|
||
**The claim survives, and the reasoning behind it does not.** The pinned matrix
|
||
genuinely overloads `ADV`: not one stray sample but 4 406 of them, 1 874 more
|
||
than a full dB over, wanting 4.26 dB more headroom than the container has. That
|
||
is a different animal from the 43 samples and 0.25 ms transient I withdrew a flag
|
||
over, and the number that separates them is the **magnitude**, not the count.
|
||
|
||
But the same table refutes the *scope* of the fix. **`S00A` never clips under the
|
||
pinned matrix** — it peaks at −1.34 dBFS. The exporter attenuates it by 7.65 dB
|
||
to solve a problem it does not have, because 0.4142 is derived from a theoretical
|
||
worst case (every channel correlated at full scale at once) that neither movie
|
||
comes near.
|
||
|
||
### Control, before believing any of it
|
||
|
||
The pinned matrix names `FLC`, `FRC`, `SL` and `SR`, and a 5.1 source has none of
|
||
them. ffmpeg neither errors nor warns — measured at `-loglevel warning`, the
|
||
output was empty. So the literal string was decoded alongside its three-term 5.1
|
||
reduction (`FL = 1.0·FL + 0.707·FC + 0.707·BL`) and the two outputs compared:
|
||
**bit-identical**, 52 751 600 bytes. The reduction is what runs, and it is the
|
||
matrix §6 intends. *That silence is itself the trap the mono fold above guards
|
||
against.*
|
||
|
||
### Not changed, and deliberately so
|
||
|
||
MISSION §6 is a **human decision of 2026-08-29**, and the level of a mix is
|
||
exactly the kind of thing §6 reserves — *"adjust it deliberately, as a commit"*.
|
||
Three options, and choosing between them is not mine:
|
||
|
||
1. **Keep the pin.** `ADV` clamps on 4 406 samples. Rejected on the measurement.
|
||
2. **Keep the exporter's 0.4142.** Preserves the two movies' relative loudness
|
||
exactly, costs 7.65 dB, and is safe by construction for any movie a modder
|
||
drops in.
|
||
3. **One measured constant, `1/1.6339 = 0.612`.** The smallest single scalar
|
||
under which no in-scope movie clamps: +3.39 dB over today, still one constant
|
||
so relative loudness is untouched. Tuned to two files, but the exporter's own
|
||
`check` refuses any export whose peak reaches 0 dBFS, so a third movie that
|
||
needed more headroom would fail loudly rather than clamp quietly.
|
||
|
||
Per-file normalisation is **not** on that list: it would put `ADV` 4.26 dB below
|
||
`S00A` and change how two cutscenes sit against each other and against the menu
|
||
bed, which is an aesthetic decision with nothing measured behind it.
|
||
|
||
What changes today is only that the deviation is **visible**: `video.rs` now
|
||
cites MISSION §6 by name and says it departs from it, and the export carries a
|
||
manifest warning with these numbers. Before this, a reader of the manifest could
|
||
not tell that a pinned human decision had been overridden at all — the command
|
||
line was recorded faithfully, and recording the command you ran does not disclose
|
||
that it is not the command you were given.
|
||
|
||
### The voice reaches the output, and a null control says so quantitatively
|
||
|
||
`+ voice ADV` in the log proves only that `play_voice` found a stream and called
|
||
`play()`. Whether the audio arrives at the Master bus is a different question,
|
||
and `docs/port/AUDIO-VERIFICATION.md` §2 exists because it is.
|
||
|
||
The control needed **no test-only code**: MODDING rule 4 already shadows any
|
||
exported asset by path, so 140 s of silence dropped at
|
||
`data/mods/audio/voice/ADV.ogg` mutes the dialogue and changes nothing else. Two
|
||
`--boot --skip-at=25 --audio=…` runs, then `astats` over the same 14 s of movie:
|
||
|
||
| | peak | RMS |
|
||
|---|---|---|
|
||
| `ADV.ogv`'s own audio (the bed) | −6.239 | −24.941 |
|
||
| the exported voice alone | −7.614 | −27.965 |
|
||
| **run with the voice muted** | **−6.251** | **−25.126** |
|
||
| **run with the voice playing** | **−5.415** | **−22.913** |
|
||
|
||
The muted run reproduces the bed to **0.01 dB peak / 0.19 dB RMS**, which is what
|
||
makes the other row worth reading. And the mixed run is not merely *louder*: two
|
||
incoherent sources at −24.941 and −27.965 dBFS predict a sum at **−23.184**, and
|
||
the run measures **−22.913** — **0.27 dB** out. The voice is in the mix, at the
|
||
level its own file says it should be.
|
||
|
||
⚠️ **Under the Dummy driver.** Per AUDIO-VERIFICATION, *"recorded under a dummy
|
||
driver"* is a weaker claim than *"heard"*, and no measurement here says the
|
||
recording is the **right** dialogue for this cutscene — only that the file the
|
||
exporter resolved is the one reaching the output at the expected level. The two
|
||
runs are also not sample-aligned (they differ by 1.7 s of wall clock), which is
|
||
why the `RMS trough` column is omitted: it moved by 40 dB between runs on window
|
||
placement alone, and peak and RMS are the two numbers that survive that.
|
||
|
||
### Ⓐ *does* skip the intro in this build, so the play-test's report is not this bug
|
||
|
||
`--skip-at=25` on a `--boot` run: `video skipped at 25.02 s`, `video ended at
|
||
25.02 s`, title at 25.02 s. The press goes through `Input.parse_input_event` and
|
||
arrives at `_unhandled_input` exactly as a pad's would, so **the wiring from press
|
||
to skip is live**. What that does not cover is a real key event from a focused
|
||
window, which is the difference between this run and the human's — and, separately,
|
||
**whether the game permits skipping an attract movie at all** is HANDOFF Q9 and
|
||
still 🟡. If the answer is no, this path is deleted rather than debugged.
|
||
|
||
|
||
## Refutation of my own two-stem reading — and it had already been adopted elsewhere
|
||
|
||
Two hours after writing that a voice region's equal-length chunks are *"HANDOFF
|
||
Q10's decoded two-stem shape"*, the Decoder asked me to decode the leading chunk
|
||
— it has no XMA1 decoder in its container — and the decoder run refuted the
|
||
claim I had made.
|
||
|
||
**Equal duration was a shape match, and I carried Q10's *music* census across to
|
||
voice on the strength of it.** The content does not support it:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `S00A` chunk 2 | **digital silence** — 4 497 300 samples, peak −inf |
|
||
| `ADV` chunk 2 | **0.60 × chunk 1** (best-fit scalar), residual **26.8 dB** below the target |
|
||
|
||
About 95 % of `ADV`'s second chunk is a −4.4 dB copy of the first. Two chunks of
|
||
equal length, one silence and the other a scaled near-duplicate, are not two
|
||
stems of one performance. ⚠️ **The claim had already travelled** — it is quoted in
|
||
the Decoder's `voice-region-leading-chunk.md` — which is the failure PROTOCOL
|
||
names: a wrong belief moving faster than its correction, through two documents
|
||
that share a source.
|
||
|
||
### What it cost, and what changed
|
||
|
||
Summing chunk 1 with silence at `1/n` put `S00A`'s dialogue **6.02 dB down for
|
||
nothing**: the exported file peaked at −16.2 dBFS against a source chunk peaking
|
||
at −4.2. `export_voice` now drops a **digitally silent** chunk before the sum.
|
||
That is arithmetic, not a content judgement — a silent input contributes nothing
|
||
to a mix and counting it in the normalisation is simply my error.
|
||
|
||
**What `ADV`'s near-duplicate chunk 2 is remains open and it is still summed.**
|
||
Whether the game plays both is a decoding question; 26.8 dB of residual is not
|
||
nothing, and dropping a chunk because it correlates with another would be
|
||
answering it.
|
||
|
||
### The leading chunk, decoded — structure, and not one word about content
|
||
|
||
The Decoder's ask was *"cutscene dialogue or mission dialogue"*. `ADV` region
|
||
+ 1392, 394 packets: **84.553 s, stereo, 48 kHz, peak −2.48 dBFS, RMS −24.80**,
|
||
with **6 silent gaps over 0.4 s below −50 dB totalling 45.3 s** — 54 % silence,
|
||
the same duty cycle as the two full-length chunks (54 %, 55 %). So it is
|
||
**speech-structured audio**: not a header, not padding, not noise.
|
||
|
||
🔴 **Which is as far as a measurement goes.** *Cutscene or mission* is an
|
||
identification and this agent has no ears and no oracle. Envelope
|
||
cross-correlation against the full-length chunks peaks at 0.768 **at the last lag
|
||
in the search range**, which is where a statistic lands when it has found
|
||
nothing, and it is not evidence. The Decoder's 🟡 stands, and its own leading
|
||
hypothesis — an in-mission `VOICE_D_*` line — is untouched by any of this. The
|
||
byte-span test it already built settles it the moment those regions are
|
||
enumerated; nobody has to listen.
|
||
|
||
### Taken from the same message: `bank_header_len`, not `riffs.len()`
|
||
|
||
The Decoder's census warns that eight bank-header regions also yield three
|
||
chunks, so the chunk count cannot say which structure you are in. **This exporter
|
||
never used the count** — it selects on decoded duration, which is why it already
|
||
handles both cases: `RT01A`'s 10 300 B leading chunk decodes to 9 ms and falls
|
||
out on its own. But a duration tie is an *observation* and `bank_header_len` is
|
||
*decoded*, so the rule switches the day `c1f3608` reaches `main`.
|
||
`sylpheed-formats` is a path dependency and merging another agent's topic branch
|
||
is not the port's to do.
|
||
|
||
## The mono fold I warned about, in the comment directly above the code that did it
|
||
|
||
`export_voice`'s first version folded to mono by averaging every **declared**
|
||
channel, and the doc comment above it said, in as many words, that *"`pan`
|
||
silently ignores a channel the input does not have — so a stereo matrix applied
|
||
to a mono voice track is not an error, it is a −6 dB attenuation that nothing
|
||
reports."*
|
||
|
||
It then did exactly that. Per-channel `astats` on both voice streams:
|
||
|
||
| | channel 1 | channel 2 |
|
||
|---|---|---|
|
||
| `ADV` chunk 1 | peak +0.000 dBFS | **peak −inf** |
|
||
| `S00A` chunk 1 | peak −4.207 dBFS | **peak −inf** |
|
||
|
||
The voice is a **mono recording carried in a nominally stereo stream**, and
|
||
averaging it with silence cost **5.94 dB** — which is most of why `S00A`'s
|
||
exported dialogue sat at −16.2 dBFS against a source chunk peaking at −4.2 (the
|
||
other 6.02 dB was summing a silent *chunk*, corrected in the same iteration).
|
||
|
||
**Checking the declared channel count is not checking the content, and only the
|
||
content is the fold.** `live_channels` now measures which channels carry signal
|
||
and averages only those. `sylpheed-viewer`'s `pan=mono|c0=c0` reaches the right
|
||
answer here for a reason it does not state; this reaches it for a stated one, and
|
||
would still be right if a stream ever did carry two live channels.
|
||
|
||
Worth recording as a pattern rather than a bug: **three defects this iteration
|
||
were all the same shape** — a silent chunk in a sum, a silent channel in a fold,
|
||
and a `pan` matrix naming channels that do not exist. Each is an input that
|
||
contributes nothing being counted in a divisor, and none of them is visible in
|
||
anything but a level.
|
||
|
||
## The leading chunk is the TAIL of the full one — measured, and it is why the region over-covers
|
||
|
||
The Decoder settled by byte-span analysis that a voice region's leading chunk is
|
||
**the movie's own dialogue, 17 of 17** — killing its own standing hypothesis that
|
||
it was an in-mission `VOICE_D_*` line — and asked whether dropping it is
|
||
therefore a truncation. It has no XMA1 decoder; this container does.
|
||
|
||
Envelope cross-correlation, sliding with overhang allowed at both ends and
|
||
normalised over the overlap only. ⚠️ **This corrects an earlier number of mine**:
|
||
a first pass scored 0.768 and I called it nothing, correctly — that search only
|
||
tried lags where the shorter chunk fitted *wholly inside* the longer one, and it
|
||
peaked on the boundary of its own range.
|
||
|
||
| | best *r* | at lag | overlap |
|
||
|---|---|---|---|
|
||
| `ADV` chunk 0 → chunk 1 | **0.998** | **+52.8 s** | 84.5 s |
|
||
| `S00A` chunk 0 → chunk 1 | **0.932** | **+25.6 s** | 68.0 s |
|
||
| control — `ADV` chunk 0 against itself | 1.000 | 0.0 s | — |
|
||
| control — `ADV` chunk 0 against `S00A` chunk 1 | **0.289** | — | 28.2 s |
|
||
|
||
**Both lags put chunk 0 flush against the end of chunk 1**: 52.8 + 84.55 =
|
||
137.35 s against chunk 1's 137.324, and 25.6 + 68.07 = 93.67 against 93.694.
|
||
|
||
Confirmed in the sample domain — lag refined to ±1 sample on the loudest second,
|
||
then a scalar best-fit over the whole overlap: `ADV` +52.8000 s, gain 0.833,
|
||
residual **16.70 dB** below the target; `S00A` +25.6320 s, gain 0.365, residual
|
||
**23.15 dB**. 98–99.5 % of the energy is a scaled copy: the same material at a
|
||
different gain, not bit-identical, which is what a lossy decode at two gains
|
||
should look like.
|
||
|
||
**So dropping chunk 0 removes a duplicate, and is not a truncation** — the
|
||
exporter's existing behaviour is right for a better reason than the one it gave.
|
||
🟡 **The manifest note has NOT been rewritten to say so.** The structural claim —
|
||
that the region over-covers because it re-presents its own tail, and that this
|
||
accounts for the whole 2.6× — is the Decoder's to write down; this page reports
|
||
the measurement and says which is which. The note stays hedged until its page
|
||
carries the conclusion, and the hedge is true either way.
|
||
|
||
⚠️ **The 504 464 B constant was deliberately not converted.** The Decoder found
|
||
the region anchor sitting that far after the true predecessor trailer on all 17
|
||
and pointedly declined to call it missing dialogue. Converting it needs a
|
||
byte↔time mapping, and the numbers above are the reason there isn't one: chunk 1
|
||
is 1 118 268 B and chunk 2 is 1 171 516 B for **the same 137.324 s**, so bytes per
|
||
second is not constant even inside a single region. Any figure in seconds off
|
||
that constant would be invented.
|
||
|
||
## Third reading of a voice region, and this one is decoded: three presentations of one take
|
||
|
||
`export_voice` has now read the same bytes three ways in one session, and each
|
||
reading was ended by a measurement rather than by an argument:
|
||
|
||
1. **Concatenate the chunks** — 359 s of dialogue for a 137 s movie.
|
||
2. **Sum them as HANDOFF Q10's two stems** — refuted here: `S00A`'s second
|
||
full-length chunk is digital silence, `ADV`'s is 0.60 × the first with 26.8 dB
|
||
of residual.
|
||
3. **Keep one stream.** ✅ This one is decoded, and not by me.
|
||
|
||
The Decoder settled the shape disc-wide without a decoder, by counting stream
|
||
starts inside every inter-descriptor span: **258 spans hold one stream, 28 hold
|
||
three, and nothing holds two or any other number.** The 95 movie-voice regions
|
||
decompose 70 + 8 + 17, and the 8 are independently the same 8 its first census
|
||
flagged. So a region carries **three presentations of one take** — which is
|
||
exactly `359 = 84.55 + 137.32 + 137.32`, the first clipped by its crate's own
|
||
1.5 MB predecessor guard.
|
||
|
||
It also cross-checked my correlation by a route needing no decoder: if the
|
||
leading chunk is the tail of a full stream, the whole leading stream should be
|
||
one complete take, and `ADV`'s 504 464 + 808 304 = 1 312 768 B at chunk 0's byte
|
||
rate is **137.323 s against my measured 137.324**. Two instruments, no shared
|
||
assumption.
|
||
|
||
**So summing was wrong for a third reason:** a take plus a 0.60 × copy of itself
|
||
is ~4 dB louder and coloured, not a mix of parts. The exporter keeps one stream
|
||
and performs no arithmetic on it.
|
||
|
||
🟡 **Which stream is a recommendation, not a decoded field.** The selector is the
|
||
**highest byte rate** among the equal-duration survivors, on the Decoder's
|
||
advice. Nothing on the disc says which presentation the game plays, and on `ADV`
|
||
this picks the **quieter** of the two — −8.3 dBFS against 0.0. That is in the
|
||
manifest in those words so the choice is visible and reversible; it is the one
|
||
part of this that a capture could still overturn.
|
||
|
||
`check` moves `voice` off the strict peak bound as a consequence. It sat with
|
||
`bgm` because it was a sum this exporter produced; it is now a single wave off
|
||
the disc, mastered near full scale — `ADV`'s louder presentation measures
|
||
**+0.0003 dBFS at source** — and refusing that would be refusing the disc's own
|
||
mastering.
|
||
|
||
### The 504 464 B constant: I refused the conversion, and refusing was right
|
||
|
||
The Decoder asked whether I would spend a decode converting its anchor offset to
|
||
seconds, and I declined because bytes per second is not constant even inside one
|
||
region. It has since found the stronger reason and withdrawn the ask: **the
|
||
constant is structural, not proportional** — identical on all 17 regions despite
|
||
their differing durations. A proportional prediction lands within 8 bytes on
|
||
`ADV`, which is a coincidence, and is **4 305 B out on `S00A`**. A seconds figure
|
||
off that constant would have been invented, and it would have looked corroborated
|
||
on the first movie anybody checked.
|
||
|
||
❔ **Why the disc stores three presentations at all is unanswered**, by either of
|
||
us.
|
||
|
||
## The transcode cache had never hit, because the wipe ran first
|
||
|
||
`video::transcode` has carried a cache since P4. It writes a `.cmd` sidecar with
|
||
the exact ffmpeg command, the source's byte count and its channel count, and
|
||
skips the encode when all three still match. Its doc comment says why: *"without
|
||
it every re-export pays ~4 minutes to produce a byte-identical file, and an
|
||
exporter nobody re-runs is worse than a cache."*
|
||
|
||
**It had never hit once.** `main.rs` clears the output tree wholesale — and the
|
||
`remove_dir_all` runs immediately before the check, deleting the sidecar and the
|
||
output it stamps. The cache tested a file it had just erased.
|
||
|
||
This session ran the exporter **six times** and paid the full Theora encode every
|
||
one of them, producing five byte-identical files. Roughly 48 minutes. Nothing
|
||
reported it, and nothing could have: a cache is silent when it works and silent
|
||
when it does not, and the only symptom is a wall-clock cost that looks like the
|
||
job simply being slow.
|
||
|
||
⚠️ **It is worth being specific about how this hid**, because the ingredients are
|
||
ordinary. The cache is correct. The wipe is correct. Each carries a doc comment
|
||
explaining itself, and neither mentions the other. The defect exists only in
|
||
their ordering, which is stated in neither, and the cost is invisible in every
|
||
artefact the export produces — the tree is byte-identical either way.
|
||
|
||
### The fix keeps the wholesale guarantee rather than trading it away
|
||
|
||
The obvious repair — stop wiping — would break what the wipe is for: *a screen
|
||
that stops being exported stops existing, rather than lingering as a stale file
|
||
that still validates.* So the wipe now spares exactly `video/`, and
|
||
`prune_videos` deletes anything in it this run did not claim. Everything else is
|
||
still cleared outright.
|
||
|
||
That is a **cache, not a hand-edit**, and the distinction matters against
|
||
MISSION §3: nothing in `export/` is authored, the sidecar is derived from the
|
||
command the exporter itself computed, and any change to the command, the source
|
||
size or the channel count re-encodes. A modder who edits an `.ogv` by hand gets
|
||
it overwritten on the next export, exactly as before.
|
||
|
||
🟡 **Not measured yet:** that a cached run reproduces the same tree. The claim is
|
||
structural — the skip is keyed on the whole command string — but "the second run
|
||
produces the same bytes" is checkable and has not been checked.
|
||
|
||
## `settle_time()` — the answer arrived, and it refutes my own 🔴 more than it confirms it
|
||
|
||
The Decoder took the port's top ask and measured the boot on a cold profile with
|
||
no shader cache (`auto/no-disc-and-menu-captures` at `4bd4779`,
|
||
`docs/re/boot-settle-times-measured.md`). It confirms the *principle* I filed:
|
||
the title's `rest.t` is 251 units = **4.183 s** where its art is finished at about
|
||
2 s, so `rest.t` is not when a screen arrives.
|
||
|
||
**But my row said more than that**, and the extra part is wrong. It said
|
||
*"everything the boot sequencer paces off that landmark is therefore late"*, and
|
||
named `publisher_logo` and `developer_logos`. So I measured the port the way the
|
||
game was measured — **visible span, not arrival-to-arrival** — with `--film` at
|
||
4 fps and a per-frame greyscale mean:
|
||
|
||
| | port, visible span | game, three cold boots | |
|
||
|---|---|---|---|
|
||
| publisher wordmark | **4.25 s** | 4.297 / 4.604 / 4.370 | 0.05 s under the lowest |
|
||
| developer logos | **3.50 s** | 3.508 / 3.503 / 3.366 | **dead on** |
|
||
| black hold between | ≈0.25 s | 0.2 – 0.3 s | inside |
|
||
| title settled → plate | 2.000 s (declared 120 units) | 2.247 s | inside, at ~28 fps presentation |
|
||
|
||
**The splashes are not late. They match.** ⚠️ And the reason my earlier reading
|
||
said otherwise is worth keeping: I had compared the port's *transition
|
||
timestamps* — 4.68 s and 3.94 s, arrival to arrival — against the game's
|
||
*visible spans*. Those differ by the exit ramp plus the black hold, about 0.6 s,
|
||
which is the whole of the discrepancy I was about to chase. This corpus has been
|
||
bitten by exactly this before, in the plate delay: *"timed from where build 4
|
||
stops animating, not from where it first appears — measured the other way the two
|
||
runs differ by 0.48 s against 6 ms."*
|
||
|
||
So the port paces the boot correctly, and **`rest.t` is a wrong landmark whose
|
||
blast radius is much smaller than I claimed**: on the screens the sequencer
|
||
actually advances off, `rest.t` plus the 24-unit exit ramp lands where the game
|
||
lands. What it still affects is `_script_settled`, which waits longer than it
|
||
needs to before photographing — a slow test, not a wrong frame.
|
||
|
||
### `dwell_seconds` stays `null`, and the question is now closed rather than open
|
||
|
||
`authored/timing.json` says of it: *"If a capture ever times the real boot, this
|
||
is where that number goes."* A capture has now timed the real boot, and the
|
||
answer is that **nothing goes there** — the disc's own keyframe groups reproduce
|
||
the game's dwells to 0.05 s and 0.01 s. The field stays `null` for a measured
|
||
reason instead of an absence of one.
|
||
|
||
### Taken from the same page, and not taken
|
||
|
||
* ✅ **The 120-unit plate delay stands.** The Decoder ran a refutation of it that
|
||
failed instructively: its probe's `title_static` mark gave 3.203 s, which on a
|
||
cold boot looks like a real effect. It was the instrument — the mark fires
|
||
during the crossfade out of the attract movie, with the glyph count still 0.
|
||
Re-measured from content: 2.247 s. **The port changes nothing**, and the
|
||
declared 120 units is what it keeps.
|
||
* 🔴 **No Ⓐ→menu dwell is authored.** It measured 3.763 s and contains a 1.53 s
|
||
guest load stall — the third independent reproduction of that stall, this one
|
||
on a cold cache, so it is not a warm-cache artefact. It is emulator time, not a
|
||
game constant.
|
||
* 🟡 **Menu build-in 0.531 s and Ⓑ→title 0.482 s are not authored either**, and
|
||
that is the Decoder's own caveat rather than my caution: they rest on one run,
|
||
where the plate delay and the load stall are each cross-checked against
|
||
independent prior evidence. The port is within ~0.1 s of both with its existing
|
||
24-unit exit ramp, so authoring them would replace a disc-derived number with a
|
||
provisional measured one and gain nothing measurable.
|
||
|
||
## The voice presentation is now unambiguously the port's choice, and the recommendation behind it was withdrawn
|
||
|
||
The Decoder has withdrawn "highest byte rate": its sentence read *"the
|
||
highest-rate, highest-gain one is chunk 1"*, and those two criteria select
|
||
**different streams** — `ADV` chunk 1 is 1 118 268 B at 0.0 dBFS, chunk 2 is
|
||
1 171 516 B at −8.3. The rule named one and the parenthetical named the other. I
|
||
implemented the rule faithfully and got the quieter presentation.
|
||
|
||
What the file can still say is decoded and does not adjudicate it: the `fmt `
|
||
chunk is a 32-byte `XMAWAVEFORMAT` whose `+0x20` is a declared
|
||
`PsuedoBytesPerSec` — 8 142 and 8 530 on `ADV`'s two, matching the computed rates
|
||
to 0.02 % — but `wEncodeOptions` (`0x10d6`), channel count and channel mask are
|
||
**byte-identical across the presentations**. Nothing in the header ranks them.
|
||
|
||
⚠️ One more observable, measured here and not in that page: **the two
|
||
presentations differ in channel layout.** `ADV` chunk 1 is mono-in-stereo —
|
||
channel 2 digitally silent — while chunk 2 is **dual-mono**, both channels
|
||
identical at −8.318574. So they are not two encodes of one file differing only in
|
||
rate.
|
||
|
||
Also recorded, because it cost the Decoder time: **`sylpheed-cli audio info` is
|
||
not to be trusted on these.** Its "16 channels / 4310 Hz / 2-bit" is
|
||
`wBitsPerSample`, `wEncodeOptions` and the channel fields read at the wrong
|
||
offsets — its XMA1 reader is misaligned. That is a tool in this repository
|
||
reporting confident nonsense, and it is the second time a renderer or reader of
|
||
ours has been believed before it was checked.
|
||
|
||
## Refutation of my dual-mono inference — the measurement stands, the generalisation does not
|
||
|
||
I argued that `highest_rate` had no case because `ADV`'s higher-rate presentation
|
||
is **dual-mono** while its louder one is mono-in-stereo, so the extra bytes buy a
|
||
duplicated channel rather than fidelity. The Decoder tested that disc-wide, as a
|
||
refutation attempt, and **it fails**.
|
||
|
||
Over the 28 three-stream cues, the stream-3 / stream-2 size ratio runs:
|
||
|
||
| min | median | max | sd | within 15 % of 1.0 |
|
||
|---|---|---|---|---|
|
||
| 0.0778 | 1.2565 | 2.9163 | 0.5057 | **12 of 28** |
|
||
|
||
Declared rates scatter with them — `S06A` is 5 661 against 16 513 B/s. **A 37×
|
||
spread is not a duplicated channel.**
|
||
|
||
**The channel measurement itself stands**: `ADV` chunk 1 really is mono-in-stereo
|
||
and chunk 2 really is dual-mono at −8.318574, and that is this port's own decode,
|
||
which the Decoder could not re-run and did not dispute. What fails is the step
|
||
from *one asset* to *the format*.
|
||
|
||
### What this changes, and what it does not
|
||
|
||
Nothing in the export changes. `loudest` is a **per-asset content** rule — it
|
||
reads the peak of the actual streams in front of it — so a scattering structural
|
||
ratio cannot undermine it, and `ADV`'s dialogue at +0.3 dBFS instead of −8.7 is
|
||
plainly the better outcome either way.
|
||
|
||
What changes is the *reason*, in four places: `authored/audio.json`'s
|
||
`presentation_why`, the selector comment in `audio.rs`, `BLOCKED.md`'s row, and
|
||
this page. The honest statement is narrower and slightly less satisfying:
|
||
**`highest_rate` was never refuted — it was never argued for, and neither is
|
||
`loudest`.** Which is exactly why the entry is marked *chosen* rather than
|
||
*measured*, and why one capture deletes it.
|
||
|
||
⚠️ **This is the third claim of mine in two iterations that generalised a
|
||
single-asset observation** — after "the chunks are two stems" and "everything the
|
||
sequencer paces off `rest.t` is late". All three were true of the thing I looked
|
||
at. The pattern is not carelessness about the measurement; it is reaching for the
|
||
rule the measurement would imply if it held everywhere, and writing that down in
|
||
the same breath. The corpus catches it because someone else runs the census.
|
||
|
||
### Two things in that data that are not mine, recorded so they are not lost
|
||
|
||
* **`S12B`'s three streams are byte-size identical** (14 396 each).
|
||
* **`BIRD_224` is three-stream and is not a movie cue** — so the three-stream
|
||
shape is not exclusive to cutscenes, which narrows how it was described to this
|
||
port earlier. Neither affects `export_voice`, which only resolves movies.
|
||
|
||
## Two rows of the P1 baseline were comparing blank frames and reporting OK
|
||
|
||
`docs/port/BLOCKED.md` has carried a 🔴 since P3: *"the loading screen's fade quad
|
||
rests OPAQUE BLACK … it will bite whoever first draws a loading screen."* It had
|
||
already bitten, in the one place nobody looks — the regression harness.
|
||
|
||
`build_12` and `build_15` render as **pure black in both renderers**: mean 0,
|
||
max 0, on the Godot side and on `sylpheed-cli`'s. The difference between two
|
||
blank frames is zero, so `verify-screen` scored them `max 0 over3 0 OK` — the
|
||
strongest verdict it has. **Two of sixteen rows were comparing nothing against
|
||
nothing**, and the committed baseline reads as sixteen passes.
|
||
|
||
That is worse than a missing test. A missing test is visible in the count.
|
||
|
||
### The cause, isolated by a control rather than by reading
|
||
|
||
`build_00` and `build_01` are the *plain* loading variant — the same screen minus
|
||
three elements. They render: **mean 1.913, max 214.5**. `build_12`/`build_15` add
|
||
`pgloading_baseeff`, `pgloading_loop5` and **`pgloading_eff00`**, a 1280×720
|
||
primitive whose `rest` is `0xff000000` — opaque black — at `t=38`, inside its own
|
||
opening black hold (`0xff000000` at 38, `0xff000000` at 48, clear on the untimed
|
||
final). It carries `layer_source: "none"`, so paint order puts it **last**, over
|
||
everything.
|
||
|
||
Same screen, one element different, one renders and one does not. That is the
|
||
diagnosis, and it did not require an opinion about `rest`.
|
||
|
||
### The rule I was about to write, and the census that killed it
|
||
|
||
The obvious reading is that `rest.t = 38` is wrong because it precedes the
|
||
element's last timed keyframe at 48 — so "`rest.t` before the last timed
|
||
keyframe" would flag the pathology. **I ran the census before writing the rule,
|
||
and it does not survive: 152 of 212 elements in this export have `rest.t` earlier
|
||
than their last timed keyframe.** It is the norm.
|
||
|
||
What actually distinguishes this element is its *content*, and the reach of that
|
||
is one:
|
||
|
||
| screen | full-frame primitive | rest.t | last | rest fade |
|
||
|---|---|---|---|---|
|
||
| `build_12` / `build_15` | `pgloading_eff00` | 38 | 48 | **`0xff000000`** |
|
||
| `extras` / `extras_jp` | `pteff00` | 64 | 74 | `0x00000000` |
|
||
| `main_menu` / `main_menu_jp` | `pteff00` | 70 | 80 | `0x00000000` |
|
||
| `title` / `title_jp` | `pteff00` | 261 | 269 | `0x00000000` |
|
||
| `title` / `title_jp` | `pteff02` | 46 | 236 | `0x40000000` |
|
||
|
||
**`pgloading_eff00` is the only element in the whole export whose resting pose is
|
||
a fully opaque full-frame quad — 1 of 212.** Every other full-frame primitive
|
||
rests clear or at 25 %. One instance is not a rule about `rest`, and keying the
|
||
renderer on "an opaque full-frame quad at rest is probably wrong" would be a
|
||
content heuristic of exactly the kind this port refuses elsewhere.
|
||
|
||
### So nothing in the renderer changed, and the harness did
|
||
|
||
The screens stay black. Nothing draws a loading screen, and the honest position
|
||
is that either `rest` is mis-identified for this one element — a decoding
|
||
question, asked — or the screen really does begin fully black and `--pose=rest`
|
||
is simply the wrong thing to photograph it at.
|
||
|
||
What changed is that **a blank pair can no longer score.** `verify-screen` now
|
||
checks both frames for ink first and reports
|
||
`BLANK -- both renderers drew nothing; this row proves nothing`. It is not a
|
||
failure — the port may legitimately have nothing to draw — and `status` is
|
||
untouched, so an unrelated `DIFFERS` still fails the run. The corrected baseline:
|
||
|
||
```
|
||
build_00/01 max 3 OK press_start(_jp) max 1 OK
|
||
title max 6 DIFFERS main_menu(_jp) max 4 DIFFERS
|
||
extras(_jp) max 3 OK title_jp max 155 DIFFERS
|
||
publisher_logo(_r) max 1 OK developer_logos(_r) max 2 OK
|
||
build_12 / build_15 BLANK ← previously OK
|
||
```
|
||
|
||
Fourteen rows, not sixteen. No new drift among the fourteen.
|
||
|
||
## Refutation attempt — the loading-screen variants, and it survived
|
||
|
||
The Decoder's `ui-title-build-map.md` says entries 0/1 are the plain loading
|
||
variant at 7 elements and 12/15 the dressed one at 10, the three additions being
|
||
`pgloading_eff00`, `pgloading_loop5` and `pgloading_baseeff`. Checked against this
|
||
export, which addresses by pak entry:
|
||
|
||
| entry | elements |
|
||
|---|---|
|
||
| 0, 1 | 7 — `eff01 eff02 line loop1 loop3 loop4 str` |
|
||
| 12, 15 | 10 — the same seven **plus** `baseeff`, `eff00`, `loop5` |
|
||
|
||
**Exact, in both the count and the identity of the three.** The claim survives,
|
||
and it paid for itself immediately: the two variants differing by exactly the
|
||
black quad is what made `build_00` a control for `build_12` and turned "the
|
||
loading screen is black" into "this one element blacks it out".
|
||
|
||
## 🔴 The voice export is known incomplete — the game decodes all three streams at once
|
||
|
||
The Decoder booted with Canary's `--xma_param_probe=true` — the cvar whose own
|
||
comment exists to say which sub-wave a movie's `.slb` the game decodes — and the
|
||
answer is that **it does not pick one. It decodes all three, concurrently, in
|
||
three separate XMA contexts.**
|
||
|
||
| ctx | packets | byte_size | disc payload (RIFF − 60) |
|
||
|---|---|---|---|
|
||
| 0 | 632 | 1 294 336 | 1 294 396 |
|
||
| 1 | 546 | 1 118 208 | 1 118 268 |
|
||
| 2 | 572 | 1 171 456 | 1 171 516 |
|
||
|
||
Three-way, byte-exact. **So "three presentations of one take, pick one" is
|
||
refuted by the running game**, and the question I had been arguing about —
|
||
*which* presentation — has no answer, because its premise was wrong.
|
||
|
||
### This one was not caught by a census, and could not have been
|
||
|
||
The last three claims of mine that overreached were all killed by counting
|
||
something. This one survived every count available: the streams really are
|
||
equal-duration, one really is silence, one really is 0.60 × another with the
|
||
residual 26.8 dB down. Every measurement was right and the frame around them was
|
||
wrong, and **no amount of looking harder at the file would have moved it** — the
|
||
file says `ChannelMask = 0x0002` on all three. It took the running game.
|
||
|
||
That is the mission's own sentence arriving in practice: *the Port has no oracle
|
||
— if it needs to know what the game does, it asks.* I did ask, repeatedly, and
|
||
each time for the wrong thing: which stream, rather than whether the premise held.
|
||
|
||
### What changed, and what deliberately did not
|
||
|
||
**The behaviour is held.** Reverting to the `1/n` sum is not obviously less
|
||
wrong: an equal-gain sum of channel pairs is **not** a downmix — MISSION §6 makes
|
||
exactly that point when it pins an explicit matrix for the movies' 5.1 fold
|
||
rather than letting ffmpeg default — and the sum cost `S00A` 6.02 dB when one
|
||
stream was silence. Swapping one guess for another on the strength of a message
|
||
is what produced this entry twice already.
|
||
|
||
**What changed is that the wrongness is now loud.** ⚠️ *This failure sounds like
|
||
success*: a single stream decodes to clean, audible dialogue, so nothing a
|
||
listener hears reveals that two streams are missing. So it is stated in three
|
||
places a reader cannot miss — a top-level `manifest.json` warning per movie, the
|
||
console line (`1 of 3 streams [refuted] -- KNOWN INCOMPLETE`), and the entry's own `why` —
|
||
and 🟡 became 🔴 in `authored/audio.json` and `BLOCKED.md`.
|
||
|
||
🟡 **"They are 5.1" is the Decoder's hypothesis and is not established.** Three
|
||
concurrent stereo streams is six channels and N stereo streams is how XMA carries
|
||
multichannel on the 360, which would explain the differing byte rates, the
|
||
near-silent stream, and why cues are 1-stream or 3-stream and never 2. Against
|
||
it: all three declare `ChannelMask = 0x0002` identically, which is odd for
|
||
distinct channel roles. Nothing here builds on it.
|
||
|
||
**What settles it, and it is asked:** a recording of the game's own output over
|
||
`ADV`, through the PulseAudio null sink (`AUDIO-VERIFICATION` §3). Candidate
|
||
combinations of the three decoded streams can then be correlated against what the
|
||
game actually played — which turns the channel-role question from a decode into a
|
||
fit against an oracle. Twenty seconds over dialogue is enough.
|
||
|
||
### The measurements survive; only their meaning moved
|
||
|
||
`S00A`'s silent stream and `ADV`'s 0.60 × relationship are untouched and now read
|
||
as facts about **channels**: 0.60 × with the residual 26.8 dB down is what a
|
||
correlated channel pair at a lower level looks like, and a silent channel is an
|
||
unused one. Nothing measured here is retracted. What is retracted is every
|
||
sentence that called them *presentations*.
|
||
|
||
## 🔴 The oracle capture does not contain the intro — a controlled negative
|
||
|
||
The Decoder took the capture I asked for — `adv-game-output-6ch.wav`, 70.2 s,
|
||
6 ch, 48 kHz, shared as `1788018994-16f9d19d90b8`, taken at `68aa192` — described
|
||
as *"the FULL mix, the movie's own WMA track plus the three XMA streams"*.
|
||
|
||
**It contains none of them.** Envelope cross-correlation, sliding with overhang,
|
||
normalised over the overlap, minimum 30 s of overlap so a short window cannot win
|
||
on an edge:
|
||
|
||
| capture ch | against | best *r* | runner-up | margin | |
|
||
|---|---|---|---|---|---|
|
||
| c0 | `ADV` bed | 0.361 | 0.359 | **+0.003** | no match |
|
||
| c1 | `ADV` bed | 0.407 | 0.403 | +0.004 | no match |
|
||
| c0 | voice stream 1 / 2 / 3 | 0.42 / 0.26 / 0.27 | — | ≤ +0.006 | no match |
|
||
| c1 | voice stream 1 / 2 / 3 | 0.47 / 0.34 / 0.34 | — | ≤ +0.006 | no match |
|
||
| c0 | `BGM_103` (menu bed) | 0.271 | 0.260 | +0.011 | no match |
|
||
| c0 | `S00A` | 0.351 | 0.349 | +0.002 | no match |
|
||
|
||
**The margin is the number that matters**, not *r*. A match has a *peak*; these
|
||
have a *plateau* — best and second-best differ by 0.001–0.016 across every
|
||
pairing, which is what a statistic does when no alignment exists.
|
||
|
||
### Three controls, because a negative from an uncontrolled instrument is worthless
|
||
|
||
1. **The instrument finds matches on this data.** `bed` vs `bed` → r = 1.000,
|
||
margin **+0.115**. Voice stream 2 vs stream 3 → r = 1.000, margin **+0.300**.
|
||
2. **My reference really is the movie.** The `.ogv` transcode against the disc's
|
||
own `ADV.wmv` → r = 1.000, margin **+0.114**. So a failure to match is not my
|
||
transcode.
|
||
3. **Time drift is ruled out.** A stretched playback would break a long
|
||
correlation while still matching locally, with the best lag climbing
|
||
monotonically. Five-second windows of the capture slid over the whole bed give
|
||
best lags of **4.95, 15.30, 119.35, 50.75, 29.35, 83.95 s** — scattered across
|
||
the movie, not monotonic, and every margin ≤ 0.017.
|
||
|
||
### What I can say, and what I will not
|
||
|
||
**Said:** this capture cannot answer the channel-role question, and it is not the
|
||
intro's audio. **Not said:** what it *is*. It is 70 s of something, all six
|
||
channels carrying signal at RMS ≈ −27 dBFS, matching nothing this port exports.
|
||
Diagnosing it is the Decoder's side of the wall and I have handed it back rather
|
||
than guessing.
|
||
|
||
⚠️ **One measurement on the file that may help them.** Split as 5.1, **channels 3
|
||
and 6 are byte-identical** — same MD5, not merely the same peak and RMS to six
|
||
decimals. An exact duplicate pair inside a six-channel "surround" output is
|
||
consistent with the Decoder's own warning that the 6-channel frame is Xenia's
|
||
hardcoded `kFrameChannelsDefault`, not the guest's request. It weakens, further,
|
||
any reading of that file as evidence of a 5.1 game mix.
|
||
|
||
### The voice export stays exactly as it is
|
||
|
||
Still one stream of three, still marked 🔴 in the manifest, the console line and
|
||
`authored/audio.json`. **The capture changed nothing**, which is the correct
|
||
outcome for a measurement that failed: the question is open, and it was open
|
||
before. What would have been wrong is treating a 70 s recording as an oracle
|
||
because it was expensive to obtain.
|
||
|
||
|
||
### Resolved the same day — it was the capture path, and the duplicate pair was the thread
|
||
|
||
The Decoder found the cause and **withdrew the capture**: PulseAudio was
|
||
remapping between two mismatched channel maps, and a 6-channel remap **silently
|
||
drops and duplicates**. Its control needs no emulator and no disc — six channels,
|
||
six different tones, the same sink and the same `parec` invocation — and came
|
||
back `400 / 3200 / 200 / 800 / 800 / 200` for an input of
|
||
`400 / 800 / 200 / 1600 / 3200 / 6400`. **Two source channels were gone
|
||
entirely.** Setting the sink's `channel_map` to the guest's own returns all six.
|
||
|
||
So the negative was right and, more usefully, **the byte-identical pair I
|
||
reported was the thread that unravelled it.** That is worth recording precisely,
|
||
because it was nearly not reported at all: it began as an idle check of two
|
||
channels whose peak *and* RMS matched to six decimals, and the only reason it
|
||
became evidence is that a coincidence at six decimals is cheaper to hash than to
|
||
explain.
|
||
|
||
**Withdrawn with the file**, both the Decoder's: *"all six channels carry
|
||
signal"*, and the non-zero-surround observation offered as weak support for a 5.1
|
||
guest mix. Unaffected: the three-XMA-context concurrency result, which is read
|
||
from the emulator's own log rather than the audio path, on two independent boots.
|
||
|
||
### What the port took from it: `tools/port/check-capture`
|
||
|
||
A capture now has to pass a provenance check before anyone analyses it, and it is
|
||
one command. It splits the file, hashes every channel and fails on any duplicate
|
||
pair. Documented in `docs/port/AUDIO-VERIFICATION.md` §5.
|
||
|
||
**Run through its own controls, both directions**, because a checker nobody
|
||
controlled is the thing this whole incident is about:
|
||
|
||
* six distinct tones → **PASS**;
|
||
* the remap's own output pattern → **FAIL**, naming all four duplicate pairs;
|
||
* the corrupt game capture → **FAIL** on `ch2 == ch5`.
|
||
|
||
⚠️ **The known-bad control is the part worth reading.** All six of its channels
|
||
report a peak of **−18.063656 dB — identical to six decimals — while containing
|
||
three duplicate pairs.** A level check cannot see this failure at all. That is
|
||
why the tool hashes rather than measures, and it is why the corrupt capture's
|
||
"plausible per-channel levels" were never evidence of anything.
|
||
|
||
The tool says so itself: it is **necessary, not sufficient.** Passing means no
|
||
channel was duplicated; it says nothing about whether the right thing was
|
||
recorded. A capture should survive both that and §1's correlation against a known
|
||
source before anything is concluded from it — and the one that was analysed here
|
||
would have failed the cheap check in thirty seconds.
|
||
|
||
The corrupt file is withdrawn from the exchange (`share drop`), so the next agent
|
||
cannot pick it up and repeat the work.
|
||
|
||
## 🔴 Take 2 is clean, my instrument was not, and the negative had to be re-earned
|
||
|
||
The Decoder's second capture passes `check-capture` — I re-ran it myself rather
|
||
than cite theirs — carries a screen log, and was recorded with the sink's
|
||
`channel_map` set equal to Canary's own. It is a good file.
|
||
|
||
⚠️ **One provenance discrepancy, minor but worth stating:** the message gives
|
||
253.3 s; the file is **318.539 s**. The screen log runs to 316 s and is
|
||
consistent with the file, so this is a mis-stated number rather than a bad
|
||
capture — but a length quoted in a provenance claim should match the artefact.
|
||
|
||
### The retraction that came out of measuring it
|
||
|
||
Take 2 also showed no alignment with the bed or the voice streams. Before
|
||
reporting a second negative I asked whether the method could do the job at all,
|
||
by building a **synthetic mix** — the bed plus the three voice streams — and
|
||
hunting the bed inside it.
|
||
|
||
**It failed. r = 0.415, against the `r > 0.8` bar my earlier negatives were
|
||
judged against.**
|
||
|
||
So the instrument that produced *"the capture contains no ADV audio"* could not
|
||
have found ADV audio in a mix even when it was certainly there. That conclusion
|
||
was right — the Decoder's tone control proved take 1 corrupt independently — but
|
||
**it was right by luck, and I reported it as measurement.** The three controls I
|
||
was pleased with tested the wrong things: that the method finds a *clean* signal
|
||
in a *clean* reference, which was never the task.
|
||
|
||
### The rebuilt instrument, calibrated in both directions
|
||
|
||
Band-limit so the target dominates, then judge on **lag and margin**, not on
|
||
absolute *r* — the `r > 0.8` bar is correct clean-against-clean and meaningless
|
||
for a component in a mix.
|
||
|
||
| hunting | band | against | *r* | lag | margin |
|
||
|---|---|---|---|---|---|
|
||
| the bed | 40–180 Hz | mix containing it | 0.663 | **0.0 s** ✓ | **+0.111** |
|
||
| the bed | 40–180 Hz | voice-only mix | 0.262 | wrong ✗ | +0.005 |
|
||
| voice stream 2 | 300–3000 Hz | mix containing it | 0.810 | **0.0 s** ✓ | **+0.248** |
|
||
| voice stream 2 | 300–3000 Hz | the bed alone | 0.358 | wrong ✗ | +0.005 |
|
||
|
||
A 20–50× separation in the discriminating statistic. Documented as
|
||
`AUDIO-VERIFICATION.md` §6.
|
||
|
||
### And now the negative, supported
|
||
|
||
Every one of take 2's six channels, against both targets, sits in the
|
||
**known-absent** regime:
|
||
|
||
| | bed (40–180 Hz) | voice stream 2 (300–3000 Hz) |
|
||
|---|---|---|
|
||
| margins | +0.000 … +0.014 | +0.001 … +0.017 |
|
||
| lags | −58 … +255 s, scattered | −72 … +183 s, scattered |
|
||
|
||
**Take 2 contains neither the movie's WMA bed nor the cutscene voice**, on an
|
||
instrument that demonstrably finds both when they are present.
|
||
|
||
### What that leaves, and it is not mine to answer
|
||
|
||
Two captures, differently configured, the second provably free of the channel-map
|
||
fault, with a screen log saying the movie was on screen — and **neither carries
|
||
either audio source.** That points away from a one-off setup error. The
|
||
possibilities I can see are a capture path that still loses the guest's mix, or
|
||
the guest not emitting these sources at all during the movie, and **only one side
|
||
of that wall can tell them apart.** Handed back with the numbers.
|
||
|
||
⚠️ **If it is the second, it reaches the port directly**: the export's movie audio
|
||
comes from the `.wmv`'s WMA track, and if the game never plays that track, then
|
||
`ADV.ogv`'s audio is wrong in a way no amount of transcode fidelity would fix. I
|
||
am not asserting that — it is a question about what the game does — but it is the
|
||
reason this is worth another boot rather than being written off.
|
||
|
||
## Every music bank was summed at 1/3 when only two sub-waves are music — 3.52 dB, since P6
|
||
|
||
The Decoder's message about `BGM_102` came with declared durations from the
|
||
corrected XMA1 `PsuedoBytesPerSec`, and checking my export against them turned up
|
||
a defect of mine that had been shipping since P6.
|
||
|
||
`export_bgm` summed every sub-wave `media` returned and scaled by `1/n`. Decoded
|
||
and timed, the three banks are identical in shape:
|
||
|
||
| bank | sub-wave 0 | sub-wave 1 | sub-wave 2 |
|
||
|---|---|---|---|
|
||
| `BGM_103` | **10 300 B → 0.009 s, peak −inf** | 3 876 924 B → 87.744 s | 3 930 172 B → 87.744 s |
|
||
| `BGM_102` | **10 300 B → 0.009 s, peak −inf** | 1 151 036 B → 37.482 s | 1 269 820 B → 37.482 s |
|
||
| `BGM_001` | **10 300 B → 0.009 s, peak −inf** | 4 466 748 B → 173.809 s | 4 673 596 B → 173.809 s |
|
||
|
||
**Sub-wave 0 is digitally silent in all three**, and 10 300 B is 10 240 + a
|
||
60-byte RIFF wrapper — 10 240 B being exactly what the Decoder's disc-wide census
|
||
identifies as the bank header. So it is not a stem. Counting it in the divisor
|
||
put every real stem at 1/3 instead of 1/2: **3.52 dB of attenuation on all the
|
||
menu music this port has shipped since P6.**
|
||
|
||
Dropping it is **arithmetic, not a decoding decision** — a silent input
|
||
contributes nothing to a sum, and this is the same rule `export_voice` already
|
||
applies. Measured after the fix: `main_menu.ogg` goes **−7.69 → −4.20 dBFS**,
|
||
**+3.49 dB** against 3.52 predicted, the remainder being Vorbis.
|
||
|
||
⚠️ **This is the third instance of one defect in this pipeline** — a silent chunk
|
||
in the voice sum, a silent channel in the mono fold, and now a silent sub-wave in
|
||
the music sum. Each was invisible in every check except a level, and each time
|
||
the divisor was computed from *how many inputs there are* rather than *how many
|
||
carry signal*. That is the shape to look for, not the individual bug.
|
||
|
||
### It also closes a 🔴 that has been open since P6
|
||
|
||
`docs/port/BLOCKED.md` carried *"`media::sound_bank_riffs` returns three
|
||
sub-waves where HANDOFF Q10's census says two"* as a disagreement the port shipped
|
||
deliberately. The census was right; the third was never a stem. The export now
|
||
reports **2 sub-waves** and the warning is gone — closed by measurement on my
|
||
side, corroborating the Decoder's `c1f3608` from a different direction (decoding
|
||
it, rather than counting headers).
|
||
|
||
### The declared-rate method, cross-checked a third time — and one correction
|
||
|
||
Their declared lengths against my decodes: `BGM_103` 87.750/87.749 vs **87.744**;
|
||
`BGM_102` 37.487 vs **37.482**; `BGM_001` 173.821 vs **173.809**. Agreement to
|
||
**5–12 ms** on three banks. The method is good for lengths.
|
||
|
||
🟢 **Refutation attempt, and the conclusion survives while the reasoning does
|
||
not.** The Decoder wrote that `BGM_001` reads *"173.821 s declared against your
|
||
decoded 167.663 s — a gap of 6.158 s"*, explaining it as *"declared is the
|
||
encoded stream, decoded is where the audio stops."* **A full decode of
|
||
`BGM_001` yields 173.809 s of PCM, not 167.663 s.** The 167.663 figure is where
|
||
the music *fades out*, measured from the audio; the stream then continues, silent,
|
||
to its declared end. So declared and decoded agree to 12 ms and the trailing
|
||
silence is *inside* the decode, not the difference between two methods. The
|
||
cross-check stands — better than stated, since it is now three banks rather than
|
||
a coincidence — and the sentence explaining it should go.
|
||
|
||
## Take 2 was starved, my correlator was fine, and `check-capture` was incomplete
|
||
|
||
The Decoder diagnosed take 2: a **starved** capture. Verified here independently
|
||
rather than taken on trust — 35.6 % of frames silent on all six channels, 10 482
|
||
alternating runs, median burst 13.5 ms and gap 3.9 ms, a 17.4 ms period at 57 Hz.
|
||
Their untruncated original reads 39.3 % and 10 595 runs; the difference is
|
||
exactly the truncation, and every other number agrees.
|
||
|
||
**So my rebuilt correlator was working correctly on a file that could not carry
|
||
the signal.** ✅ And the alarming reading it produced — *"the game may not play
|
||
the `.wmv`'s WMA track, so `ADV.ogv`'s audio has been wrong since P4"* — **is not
|
||
supported by this capture, and is not refuted either.** It is withdrawn as a
|
||
concern arising from evidence, and nothing is changed on account of it in either
|
||
direction. That matters more than it looks: it was the most expensive-to-act-on
|
||
hypothesis in the port, and it came from a file that could not speak to it.
|
||
|
||
### The real deliverable: my own checker passed the starved file
|
||
|
||
`check-capture` tested only for duplicated channels, so it cleared a recording
|
||
that was 36 % holes. A provenance check that passes the artefact it was built in
|
||
response to is not a check.
|
||
|
||
It now measures starvation too. ⚠️ **Two thresholds I invented were both wrong,
|
||
and the controls caught both** — which is the part worth recording:
|
||
|
||
1. **Counting exact-zero frames.** Real audio crosses zero constantly; a clean
|
||
voice track scored 5 947 "gaps" of median 0.0 ms and was called starved. **A
|
||
gap is a run, not a sample.** Only runs ≥ 1 ms count.
|
||
2. **Gap count and median length.** A genuine music bed shows **454 gaps at a
|
||
median of 1.4 ms** — quiet 16-bit passages really are zero for milliseconds —
|
||
so neither statistic separates it from a starved file.
|
||
|
||
What separates them is the **rate**:
|
||
|
||
| | gaps/s | median gap | all-channel silence |
|
||
|---|---|---|---|
|
||
| the starved capture | **32.9** | 3.9 ms | 35.6 % |
|
||
| a real music+SFX bed | **3.3** | 1.4 ms | 1.1 % |
|
||
| a voice track, 53 % pauses | **0.03** | — | — |
|
||
|
||
Bar set at **20 gaps/s** — 1.6× below the bad case, 6× above the worst good one,
|
||
**derived from the controls rather than chosen and then justified.** Controlled
|
||
in both directions: real stereo bed PASS, six distinct tones PASS, starved
|
||
capture FAIL. It also now reports a `data` chunk declaring 0 bytes, which is what
|
||
a file copied while still being written looks like.
|
||
|
||
### 🔴 The voice channel roles are not obtainable this session
|
||
|
||
Both capture routes are closed and the Decoder has said so plainly. The monitor
|
||
sink is starved **by construction** — it advances at wall-clock rate and
|
||
substitutes silence, so every moment the emulator runs slow is a hole, and
|
||
deleting the holes warps the timebase rather than repairing it. The route that
|
||
works is an internal tap at `SDLAudioDriver::SubmitFrame`, and that needs a
|
||
Canary rebuild whose cost they have measured: the build root `build-canary`
|
||
targets does not exist in that container, the warm tree is configured against the
|
||
same missing path, so it is a full reconfigure and compile on a box with ~700 MB
|
||
free and a history of parallel builds OOM-killing the host.
|
||
|
||
**A whole session for one probe.** That is a human's call and neither agent
|
||
should start it mid-loop. Until then the port keeps authoring with the known
|
||
recorded: one stream of three, 🔴 in the manifest, the console line and
|
||
`authored/audio.json`.
|
||
|
||
## The settle run carries an unmeasured real-time factor — and the numbers it touches were already unauthored
|
||
|
||
The Decoder has withdrawn one of the two arguments propping up its settle-time
|
||
run. It had claimed the plate *pulse period* was an internal clock proving the
|
||
run was not slowed; re-examined, that estimate rests on **one interval at a
|
||
125 ms sample interval (±6.7 %)**, and re-running the trough-picking gives
|
||
**2.628 s** rather than the 2.369 quoted — an adjacent local minimum had been
|
||
counted as a separate trough. Against the corpus's 2.24 s that is **+17.3 %**. It
|
||
is too weak to show anything and **cannot resolve a real-time factor below
|
||
~7 %**.
|
||
|
||
**✅ Nothing in the port moves, and this is the second time in two iterations that
|
||
the right call was to have authored nothing.**
|
||
|
||
| number from that run | anchored by | did the port take it? |
|
||
|---|---|---|
|
||
| title → plate, 2.247 s | three prior readings (2.13 / 2.132 / 2.138) and the disc's declared 120 units | ✅ it is what the port already draws |
|
||
| menu build-in, 0.531 s | **nothing** | ❌ not authored |
|
||
| Ⓑ → title, 0.482 s | **nothing** | ❌ not authored |
|
||
| Ⓐ → menu, 3.763 s | contains a 1.53 s load stall | ❌ explicitly refused |
|
||
|
||
Checked rather than remembered: `grep` over `authored/` and `port/scripts/` finds
|
||
no `0.531` and no `0.482`. The only build-in reference in the tree is the plate
|
||
arithmetic — `t=118 → t=238`, 120 units — which is the **anchored** leg.
|
||
|
||
I declined those two because they were one-run figures the Decoder had itself
|
||
flagged, and because the port was already within ~0.1 s of both from the disc's
|
||
own keyframes. **That reasoning has now been joined by a second, independent one
|
||
I did not have at the time**: a few per cent of slowdown sits inside them
|
||
undetected. A provisional measurement adopted over a decoded number would have
|
||
imported an error nobody could see.
|
||
|
||
## `verify-dwell` — the comparison that refuted my own 🔴, made repeatable
|
||
|
||
Last iteration I measured the port's visible spans against the oracle's dwells by
|
||
hand, and it refuted a red flag I had filed myself: `rest.t` *is* the wrong settle
|
||
landmark, but *"everything the sequencer paces off it is therefore late"* was
|
||
false, and I nearly went and re-paced screens that already matched.
|
||
|
||
That check existed once, in a transcript. It is now `tools/port/verify-dwell`.
|
||
|
||
```
|
||
screen port oracle (3 cold boots) verdict
|
||
publisher wordmark 4.25 s 4.297 / 4.604 / 4.370 agrees
|
||
developer logos 3.75 s 3.508 / 3.503 / 3.366 agrees
|
||
```
|
||
|
||
⚠️ **The trap it exists to prevent is in its header, because it is the whole
|
||
point:** a port's *transition timestamps* and the oracle's *visible spans* are
|
||
not the same quantity — they differ by the exit ramp plus the black hold, about
|
||
0.6 s, which was the entire discrepancy I was about to chase. This corpus has
|
||
been bitten by the identical confusion before, on the plate delay, where the two
|
||
readings differ by 0.48 s against 6 ms.
|
||
|
||
**The bar is the oracle's own run-to-run spread**, plus one film interval. Three
|
||
cold boots of the real game differ by 0.3 s; agreeing more tightly than the
|
||
oracle agrees with itself would not mean anything, and a tighter bar would be a
|
||
number chosen to look impressive. The developer-logo span read 3.50 s on the
|
||
hand-run and 3.75 s here — one film interval apart, both inside the bar, which is
|
||
the tool reporting its own resolution honestly rather than hiding it.
|
||
|
||
**The oracle's numbers are quoted in the script as a test fixture and labelled as
|
||
the oracle's**, with the RE document they come from. Nothing in the port derives
|
||
them and nothing may.
|
||
|
||
## The `PRESS Ⓐ` plate pulses — authored per element, because the census forbids a rule
|
||
|
||
The human listed pulsation as first-class and the port drew nothing at all: the
|
||
plate's focus record `ptbtn00f` was never reached, because `press_start` has no
|
||
`buttons` and nothing is focused.
|
||
|
||
**That it loops is measured**, not assumed. The corpus timed the pulse four times
|
||
— 2.12 / 2.19 / 2.34 / 2.31 s — and you cannot measure a period unless the thing
|
||
repeats.
|
||
|
||
### The rule I was going to write, and the census that forbade it
|
||
|
||
The spinning ring is a **rule** in the renderer (`spin_period_units`) and it
|
||
earns that: 16 of 212 elements match its shape and **all 16 are focus rings**,
|
||
zero false positives. So I looked for the analogous shape for a pulse — a group
|
||
whose keyframes vary **only** in alpha, whose first and last alpha are equal, a
|
||
closed cycle. `ptbtn00f` fits it exactly: `0 → 6 → 74 → 80 → 80 → 74 → 6 → 0`.
|
||
|
||
**Censused before writing it: 82 of 212 elements match.** `ptcopyright`,
|
||
`palogo_sqex`, `ptmsg`, `ptlogo_back2`, and every `_eff` fade-in-hold-fade-out on
|
||
every screen. **A renderer rule on that shape would make the copyright notice
|
||
pulse.**
|
||
|
||
Narrowing to focus records leaves exactly **one** distinct element (plus its JP
|
||
twin). A rule justified by n = 1 is a special case wearing a rule's clothes. So
|
||
the pulse is a **lookup** in `authored/timing.json`, keyed `<screen>/<element>`,
|
||
and the census is recorded beside it so nobody widens it later.
|
||
|
||
### The period is the element's own group — and the alternative is stated
|
||
|
||
**129 units**: its last timed keyframe is t=105, and the final untimed keyframe
|
||
is reached `exit_ramp_units` (24) later. **No new constant** — 24 is the same
|
||
authored value every other element's exit already uses. That is 2.150 s at
|
||
60 units/s, or 2.295 s at the ~28.1 fps the emulator presents, against
|
||
measurements of 2.12–2.34 s. It sits inside the spread at either rate.
|
||
|
||
⚠️ **It is a choice, and the alternative cannot be ruled out**: the cycle could
|
||
restart at the group's first keyframe (t=6) rather than at 0, giving 123 units =
|
||
2.050 / 2.189 s — **also inside the measured spread**. Nothing available
|
||
separates them. t=0 is taken because it is where every other group in this port
|
||
starts, which is consistency and not evidence, and `authored/timing.json` says so.
|
||
|
||
A fifth reading is recorded and **not averaged in**: the Decoder re-picked its
|
||
troughs and got 2.628 s having previously reported 2.369 from the same run, then
|
||
withdrew the estimate as too weak to resolve better than ~7 %.
|
||
|
||
### Verified the way the ring was — bit-identity one period apart
|
||
|
||
20 authored periods is 2.15 × 20 = **43.00 s = exactly 172 film frames**, so
|
||
frames N and N+172 must be the same frame:
|
||
|
||
| | max difference |
|
||
|---|---|
|
||
| f_055 vs f_227 (43.00 s) | **0/255** |
|
||
| f_060 vs f_232 (43.00 s) | **1/255** |
|
||
| f_070 vs f_242 (43.00 s) | **1/255** |
|
||
| f_079 vs f_251 (43.00 s) | **0/255** |
|
||
| **control** — f_070 vs f_243 (43.25 s) | **58.7/255** |
|
||
|
||
The control is what makes the rest mean anything: a quarter-second off the period
|
||
differs by 58.7, on the period by 0–1. Measured on the held boot title, where the
|
||
glow-box mean swings **26.0 ↔ 37.7** — a real pulse, not a static glow.
|
||
|
||
## ✅ The oracle finally speaks: the exported voice IS the game's centre channel
|
||
|
||
The Decoder's fourth capture is the first faithful one — `--gpu=null` takes the
|
||
guest from 0.70× to 0.96× real time so Xenia stops padding, an ALSA `file` tee in
|
||
front of a paced slave removes PulseAudio's wall clock entirely. **59.7 s, 0.35 %
|
||
silence, one gap in the whole file, six distinct channel hashes.** Checked here
|
||
independently: it passes `check-capture`, and its header sizes verify exactly
|
||
(RIFF 34 369 572 / data 34 369 536 against 34 369 580 actual).
|
||
|
||
The correlator was already calibrated, so the fit was a re-run rather than a
|
||
rebuild. **Controls first, on this instrument: known-present margin +0.248,
|
||
known-absent +0.005.**
|
||
|
||
### Speech band, 300–3000 Hz
|
||
|
||
| | FL | FR | **FC** | LFE | RL | RR |
|
||
|---|---|---|---|---|---|---|
|
||
| stream 1 (leading) | +0.013 | +0.006 | +0.012 | +0.009 | +0.012 | +0.005 |
|
||
| **stream 2 — the one exported** | +0.238 | +0.171 | **+0.305** | +0.011 | +0.035 | +0.006 |
|
||
| stream 3 | +0.240 | +0.173 | **+0.307** | +0.009 | +0.054 | +0.006 |
|
||
|
||
**`r = 0.989` on FC, margin +0.305 — above the known-present control.** And the
|
||
bed, in the low band, is the mirror image: FL 0.763 / FR 0.838 / RL 0.805 / RR
|
||
0.817, all agreeing on the same lag, and **FC 0.317**.
|
||
|
||
### What is established, and what is not
|
||
|
||
✅ **The capture contains `ADV`'s audio**, at lag ≈ +6.6–6.7 s — agreed by two
|
||
independent bands and by six channels.
|
||
|
||
✅ **The dialogue is in the centre channel and the bed is in the four corners.**
|
||
FC carries the voice and not the bed; FL/FR/RL/RR carry the bed and not the
|
||
voice. That is a textbook film mix, and it is measured rather than inferred from
|
||
a header — which matters, because the header says `ChannelMask = 0x0002` on all
|
||
three streams and would never have told us.
|
||
|
||
✅ **The port's exported voice file is the material the game plays in FC.** The
|
||
`loudest` choice in `authored/audio.json` — which I recorded as an unjustified
|
||
choice — selects the dialogue. A mono voice file played into the mix is the
|
||
right approximation of a centre channel.
|
||
|
||
⚠️ **Streams 2 and 3 are indistinguishable to this instrument**, and that is
|
||
expected: I measured months-of-iterations ago that stream 3 is 0.60 × stream 2
|
||
with the residual 26.8 dB down. They are the same take at two levels, so they
|
||
correlate identically with everything. **This does not say `loudest` picked the
|
||
*right* one of the two — only that whichever it picked is the dialogue.**
|
||
|
||
⚠️ **Stream 1 is not detectable in this window**, margins +0.005…+0.013. Consistent
|
||
with it being the tail of stream 2 (measured earlier at r = 0.998) and with a
|
||
59.7 s window that starts before the tail.
|
||
|
||
🔴 **The `1 of 3 streams [refuted]` warning stays.** Nothing here explains what the other
|
||
two contribute to the game's output, and the export still ships one. What changed
|
||
is its character: it is no longer *"one of three, contents unknown"* but *"the
|
||
centre-channel dialogue, plus two streams whose relationship to it is measured
|
||
and whose role is not."*
|
||
|
||
⚠️ **And the reach: 59.7 s of a 137 s movie**, one run, at 0.96× real time. The
|
||
`--gpu=null` route costs video, so this capture has no screen provenance — its
|
||
provenance is the XMA probe showing `ADV`'s three streams decoding during the
|
||
run, which for an audio question evidences the thing recorded rather than what
|
||
was on screen.
|
||
|
||
## The stripping control passes — `S00A` is obtainable, and the gate is cleared
|
||
|
||
The Decoder made this the gate on `S00A`, and it is the right call: `ADV` plays
|
||
itself on boot so it can be captured with `--gpu=null` at 0.96× real time, but
|
||
`S00A` starts ~4.5 s after Ⓐ on a save slot, which needs a **driven** run, which
|
||
needs screens, which rules out `--gpu=null`. So `S00A` is necessarily the 0.70×
|
||
rendered route with ~10 % additive padding — and is only worth a boot if
|
||
stripping that padding is exact.
|
||
|
||
**It is.** A real music+SFX bed (137.37 s, carrying 454 genuine zero runs of its
|
||
own) had 1 149 holes inserted at 8.37/s to +9.9 % length, matching the observed
|
||
ALSA profile, then was stripped and correlated in the low band:
|
||
|
||
| | *r* | lag | margin |
|
||
|---|---|---|---|
|
||
| original vs itself — **ceiling** | 1.000 | 0.0 s | +0.141 |
|
||
| **padded** vs original — what padding costs | **0.436** | −12.2 s | **+0.006** |
|
||
| **stripped** vs original — recovered | **1.000** | **0.0 s** | **+0.142** |
|
||
| stripped vs original-also-stripped | 1.000 | 0.0 s | +0.143 |
|
||
|
||
**Two things worth reading off that table.**
|
||
|
||
First, **padding at that profile destroys correlation completely** — r 0.436,
|
||
margin +0.006, which is the known-absent regime. That independently confirms, on
|
||
a file whose contents I control, that the earlier captures were unusable for the
|
||
reason claimed rather than for some other reason.
|
||
|
||
Second, **recovery does not require stripping both sides.** The stripped capture
|
||
matches the *unstripped* source at the ceiling. That matters operationally: the
|
||
port's reference assets never need touching.
|
||
|
||
⚠️ **What the control does not license.** Stripping removes genuine silence too
|
||
and cannot tell the two apart. On this material the genuine runs total 0.71 s in
|
||
137 s and cost nothing measurable; on material that is mostly silence they would.
|
||
And the whole thing rests on the **substituted-versus-additive** distinction — it
|
||
is valid for Xenia's ALSA padding, which inserts, and it is vandalism on a
|
||
PulseAudio monitor capture, which substitutes. `tools/port/strip-padding` says so
|
||
in its header before it says anything else, because running it on the wrong
|
||
artefact would look like it worked.
|
||
|
||
Its output is **byte-identical** to the control's own stripping, so the tool and
|
||
the experiment are the same operation rather than two implementations that agree.
|
||
|
||
## The correctness harness the docs promised for eight milestones did not exist
|
||
|
||
`tools/port/verify-screen`, line 20, since P1: *"Use `tools/verify-capture` for
|
||
the correctness question."* **There was no such file.** The port has had a harness
|
||
comparing itself to `sylpheed-cli` — two renderers sharing its assumptions — and
|
||
none comparing it to the game, while its own documentation said otherwise.
|
||
|
||
`docs/re/captures/ORACLE-CAPTURES.md` is blunt about why that matters: two
|
||
renderers agreeing proves nothing, and this corpus has been bitten three times —
|
||
the dropped `pteff05` background, the scale-0 rect, `rest()` — each invisible to a
|
||
render-vs-render diff and obvious against a capture.
|
||
|
||
`tools/port/verify-capture` now exists. **Five screens, against framebuffer
|
||
captures of the real game:**
|
||
|
||
| screen | RMSE | differing | note |
|
||
|---|---|---|---|
|
||
| `main_menu` | 14.79 | **0.25 %** | focus state may differ |
|
||
| `extras` | 15.29 | 0.46 % | focus state may differ |
|
||
| `title` | 21.07 | 1.82 % | `ptloop` sweeps never stop |
|
||
| `publisher_logo` | 10.77 | 1.00 % | |
|
||
| `developer_logos` | 9.37 | 0.39 % | |
|
||
|
||
**No screen shows a large connected blob** — the shape a missing or misplaced
|
||
element makes, and the shape all three historical failures made. The differences
|
||
are scattered, and the two largest have stated causes.
|
||
|
||
### 74 % of `main_menu`'s difference is the oracle's own focus signature
|
||
|
||
The corpus ships `live-main-menu.png` and `live-main-menu-options-focused.png` —
|
||
the same screen with a different button lit. Their difference *is* what focus
|
||
changes, measured by the oracle against itself. Of the port's 2 159 differing
|
||
pixels, **1 599 — 74.1 % — fall inside that signature.** So the bulk of the
|
||
disagreement is a state mismatch (the port focuses `NEW GAME`, authored, because
|
||
HANDOFF Q5 measured initial focus as unstable), not a rendering defect.
|
||
|
||
## Refutation attempt — the tone curve survives in its stated reach and not past it
|
||
|
||
`ui-render-tone-curve.md` models the relationship as
|
||
`capture = 255·(render/255)^γ`, γ ≈ 1.34–1.49, **measured on dark flat patches
|
||
(render ~0–60), with "nothing constrains midtones or highlights"** written into
|
||
its own reach.
|
||
|
||
**I tried to fit that γ and got contradictory answers three times, and the
|
||
contradictions were mine.** Binning every structurally matched pixel of
|
||
`main_menu` by render level gives the relationship directly:
|
||
|
||
| render | capture | implied γ | pixels |
|
||
|---|---|---|---|
|
||
| 8 | 4.04 | 1.20 | 183 026 |
|
||
| 16 | 7.89 | **1.26** | 227 630 |
|
||
| 24 | 15.57 | 1.18 | 100 945 |
|
||
| 32 | 26.15 | 1.10 | 87 474 |
|
||
| 40 | 38.07 | 1.03 | 86 094 |
|
||
| 48 | 53.96 | **0.93** | 85 255 |
|
||
| 64 | 78.52 | 0.85 | 6 509 |
|
||
| 96 | 130.44 | **0.69** | 1 682 |
|
||
|
||
✅ **The claim survives where it was measured.** In the darks the capture really
|
||
is darker than the render and γ > 1.
|
||
|
||
🔴 **It is not a single power law.** The implied exponent falls monotonically and
|
||
**crosses 1.0 near render ≈ 44** — above that the capture is *brighter*. One
|
||
exponent cannot express a curve that crosses unity, which is precisely why my
|
||
whole-frame fits kept returning γ = 1.00: the darks want more than 1 and the
|
||
midtones want less, and they cancel.
|
||
|
||
**So the corpus's stated reach was not a hedge, it was the finding.** ⚠️ And the
|
||
exponent in the darks measures **1.18–1.26 here against the page's 1.49 for this
|
||
screen** — a disagreement I am recording rather than resolving, since they fit
|
||
selected flat patches and I binned every matched pixel.
|
||
|
||
### The tool reports the curve, not a best exponent
|
||
|
||
Two earlier versions of `verify-capture` reported a best-fit γ and were wrong
|
||
both times — once by fitting across a 74 % structural mismatch, once by
|
||
extrapolating past a reach the measurement's own authors had written down.
|
||
**Extrapolating a measurement past its stated reach is how this tool got it wrong
|
||
twice**, and the answer was not a better fit but a different instrument: a table
|
||
somebody can argue with.
|
||
|
||
## Identifying the capture's focused button — and my harness was posing the port wrong
|
||
|
||
The Decoder attached an honest caveat to its reproduction of the tone-curve
|
||
refutation: its bins included the focus-state mismatch, so it was *"not a clean
|
||
second opinion"*. That is removable, and removing it found a defect of mine.
|
||
|
||
### The method, with a known-answer control
|
||
|
||
`--menu=main_menu --script=down,down,down,down` walks focus through all five
|
||
buttons and shoots each. Compare every one against a capture; the minimum
|
||
identifies the focused button. **The control is the capture whose answer is in
|
||
its own filename:**
|
||
|
||
| render focus | vs `live-main-menu-options-focused` | vs `live-main-menu` |
|
||
|---|---|---|
|
||
| `ptbtn01` NEW GAME | 6 351 | **531** |
|
||
| `ptbtn02` LOAD GAME | 7 087 | 7 094 |
|
||
| `ptbtn03` TUTORIAL | 6 230 | 6 237 |
|
||
| `ptbtn04` OPTIONS | **1 292** | 6 364 |
|
||
| `ptbtn05` EXTRAS | 6 073 | 6 080 |
|
||
|
||
✅ **The control picks `OPTIONS`, by 4.7×** — the answer the filename gives.
|
||
✅ **So the test is trustworthy, and `live-main-menu.png` has NEW GAME focused, by
|
||
11.5×.**
|
||
|
||
**Which is what the port already focuses.** `authored/flow.json`'s
|
||
`initial_focus: ptbtn01` was chosen because HANDOFF Q5 measured focus as
|
||
*unstable* across boots — four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME
|
||
— and it is one of the two observed states. It is now also the state of the
|
||
committed capture. ⚠️ That is corroboration, **not** a decode: Q5's instability
|
||
stands, and this identifies one frame rather than a rule.
|
||
|
||
### The defect: `verify-capture` rendered menus with no focus at all
|
||
|
||
`--screen=` draws no focus record, so the harness had been comparing `main_menu`
|
||
to the oracle **in a state the oracle was never in**. Rendered properly, with
|
||
`--menu=`:
|
||
|
||
| | before | after |
|
||
|---|---|---|
|
||
| `main_menu` | 0.25 % differing, RMSE 14.79 | **0.06 %**, RMSE 13.21 |
|
||
| `extras` | 0.46 %, RMSE 15.29 | **0.20 %**, RMSE 13.38 |
|
||
|
||
A 4× improvement on `main_menu` that was **entirely my harness posing the port
|
||
wrong**, not the port drawing wrong. Worth stating plainly: the first run of a
|
||
new correctness harness reported a discrepancy, and three quarters of it was the
|
||
harness.
|
||
|
||
### And the Decoder's caveat resolves to nothing — measured, not assumed
|
||
|
||
Re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 /
|
||
1.18 / 1.10 / 1.03 / 0.94 at render 8…48 — **unchanged** from the mismatched run.
|
||
So the focus-state contamination it flagged really did not move the trend. Its
|
||
reproduction stands as a second opinion after all, and that is now a measurement
|
||
rather than a hope.
|
||
|
||
## `tools/port/which-focus` — the Decoder asked for a detector, and it carries its own control
|
||
|
||
`S00A` is blocked on knowing which button a screenshot has focused.
|
||
`newgame_path.sh` assumed NEW GAME is focused at boot, drove on that assumption,
|
||
and landed in a **tutorial mission** — because HANDOFF Q5 measured focus as
|
||
*unstable across boots*. And counting presses cannot substitute: ⬆ from the first
|
||
item wraps to the last, so no fixed number of presses lands on a known item from
|
||
an unknown start.
|
||
|
||
The Decoder's own attempt — a per-row brightness statistic — **failed the
|
||
control**, picking NEW GAME on the capture whose filename says OPTIONS. The
|
||
render-difference method passes it, so it is now a script that agent can run.
|
||
|
||
### It runs the control on every invocation, not once when it was written
|
||
|
||
```
|
||
control -- live-main-menu-options-focused.png (answer is in the filename):
|
||
OPTIONS 1285 <- picked
|
||
EXTRAS 6073
|
||
...
|
||
-> OPTIONS, margin 4.7x CONTROL PASSED
|
||
```
|
||
|
||
If that fails, the tool **refuses to report a result at all**. A control that
|
||
does not execute is not a control, and this one cannot be skipped.
|
||
|
||
### Three checks, and one of them independently reproduces a corpus measurement
|
||
|
||
| input | verdict | margin |
|
||
|---|---|---|
|
||
| `live-main-menu-options-focused` — **known answer** | OPTIONS | 4.7× |
|
||
| `live-main-menu` — the question | **NEW GAME** | 11.4× |
|
||
| `live-extras` — **known from the corpus** | MISSION SELECT | 4.2× |
|
||
| `live-title-press-a` — **no menu at all** | *refuses* | 1.0× |
|
||
|
||
The `extras` row is a second known answer I did not plant: `authored/flow.json`
|
||
already records *"MEASURED: EXTRAS opens focused on MISSION SELECT
|
||
(live-extras.png)"*, and the tool reaches it independently.
|
||
|
||
The title row is the negative control. A frame with no menu in it gives a margin
|
||
of 1.0× and the tool says *"this frame does not decide it. Do not act on this."*
|
||
|
||
⚠️ **And that refusal now carries a non-zero exit code.** The first version
|
||
printed the warning and exited 0 — so a caller scripting it, which is the entire
|
||
point, would have read a refusal as an answer. That is the same defect as a
|
||
checker claiming a check it skipped, and it is the fifth instance of that shape
|
||
between the two of us this session.
|
||
|
||
**What it is not:** it identifies focus in *one frame*. It says nothing about
|
||
what *selects* focus; Q5's instability stands.
|
||
|
||
## The title's 1.82 % — three of my own explanations refuted, and the format has no blend mode
|
||
|
||
`title` is the port's largest disagreement with the oracle, and last iteration I
|
||
attributed it to the moving `ptloop` sweeps *without checking*. That attribution
|
||
is wrong, and so were the two hypotheses I formed after it.
|
||
|
||
**❌ Not the sweeps.** `ptloop01`/`ptloop02` are **399×180 at (441, 270)** — small
|
||
and central — and their exported keyframes hold `pos`, `scale` and `rotation`
|
||
constant, varying only alpha. The difference peaks at **x ≈ 1088**, nowhere near
|
||
them.
|
||
|
||
**❌ Not an over-held element.** `--screen` holds every element at its own
|
||
`rest.t`, so I added `--no-hold` to render the other answer. Playing the title's
|
||
groups past their rest **fades the whole screen to black by t = 5.2 s** —
|
||
30.97 % differing against 1.82 % held. Holding at rest is right.
|
||
|
||
**❌ Not a timing offset.** Sweeping the build-in: 24.05 % at t=1.6, falling
|
||
monotonically to **1.68 % at t=4.18** and 1.82 % settled. The capture is at the
|
||
settled end; there is no earlier moment that fits better than marginally.
|
||
|
||
### What it actually looks like: a horizontal redistribution
|
||
|
||
Signed difference (port − capture), by cell:
|
||
|
||
| | x=0 | x=320 | x=640 | x=960 |
|
||
|---|---|---|---|---|
|
||
| y=0 | +1.1 | **−13.1** | −6.6 | **+16.0** |
|
||
| y=169 | +2.1 | **−8.3** | +4.2 | **+9.9** |
|
||
| y=338 | +5.8 | −4.5 | −0.0 | +3.2 |
|
||
| y=507 | +3.9 | +3.3 | +2.0 | +2.3 |
|
||
|
||
**The port is darker centre-left and brighter right, and it nearly cancels** —
|
||
whole-frame means 63.8 against 62.5. That is not a level error and not a tone
|
||
ramp; it is brightness in the wrong *place*. And it falls in exactly the rows
|
||
spanned by the two wide elements `ptlogo_back2` (1118×262 at 71,126) and
|
||
`ptlogo_back2eff` (1133×280 at 64,117), with the column profile falling off past
|
||
x≈1152 against their right edges at 1189 and 1197.
|
||
|
||
### 🔴 The export carries no blend mode, so the port cannot draw one
|
||
|
||
`ptlogo_back2eff`'s exported keys are `declared, id, index, keyframes, kind_raw,
|
||
layer, layer_source, pivot, rest, role, sprite`. **There is no blend field**, in
|
||
this element or in `FORMAT.md` at all. The port composites everything with normal
|
||
alpha.
|
||
|
||
If the game draws `_eff` layers **additively**, a wide gradient sprite would
|
||
produce precisely this signature — surplus where the sprite is bright, deficit
|
||
where the underlying art is brighter than the sum — and **nothing in the export
|
||
would reveal it.** That is a decoding question, not a port one, and it is asked
|
||
rather than assumed. ⚠️ It is a hypothesis I have not tested; I am recording it
|
||
because the three I could test are dead.
|
||
|
||
### A separate `rest.t` casualty, recorded and not acted on
|
||
|
||
`pteff02` is a full-frame primitive whose group runs `0x40000000` (25 % black) at
|
||
t=46 → `0xd4000000` at 76 → `0xcc000000` at 118 → **`0x00000000` at 236**. Its
|
||
`rest.t` is **46**, so the port holds a **25 % black veil the screen's own
|
||
timeline removes**. This is the third instance of `rest.t` naming a hold that is
|
||
not the settled state — after the loading screen's opaque quad and `ptlogo1`'s
|
||
creep. ⚠️ **It does not explain the residual** — removing a darkening veil would
|
||
make the port brighter still, and it is already brighter where it disagrees — so
|
||
it is recorded rather than fixed.
|
||
|
||
### And a new diagnostic
|
||
|
||
`--no-hold` plays a screen past its rest instead of clamping each element at
|
||
`rest.t`. Added because the question *"is the held pose what the idle game
|
||
shows"* could not be asked otherwise. ⚠️ Its first version set the flag thirty
|
||
lines before `view` exists and silently rendered nothing — caught because the
|
||
comparison loop found no files, not because anything reported an error.
|
||
|
||
## 🔴 The exporter dropped nested `.rat` leaf geometry on 45 elements — and it is the title's 1.82 %
|
||
|
||
The Decoder overturned one of my three eliminations, and it was the one I was
|
||
most confident about. I ruled out the `ptloop` sweeps because *"399×180 at
|
||
(441, 270), keyframes hold position constant"* — **that is the parent's record.
|
||
The geometry is in the leaf, and the exporter never opened it.**
|
||
|
||
| | parent (what the export shipped) | **leaf `ptloopNN.rat`** |
|
||
|---|---|---|
|
||
| `ptloop01` | scale (100, 100), rot 0, pos (441, 270) fixed | **scale (100, 600), rot +30°**, x sweeping **−639 → −39 → 1521** |
|
||
| `ptloop02` | scale (100, 100), rot 0, pos (441, 270) fixed | **scale (100, 800), rot −45°**, x sweeping **1721 → 1111 → −839** |
|
||
|
||
Two ~1080 and ~1440 px quads leaning opposite ways and sweeping across the
|
||
frame, against two 400 px sprites drawn upright and static in the middle. **That
|
||
is exactly the signature I measured** — darker centre-left, brighter right,
|
||
nearly cancelling — and the Decoder's GPU capture puts their centres at x ≈ 467
|
||
and 992, which are the two cells where my signed difference peaked.
|
||
|
||
`ui_layout`'s own doc comment had said so: *"the rotated quads come from its two
|
||
**nested** `.rat` leaf records, which the census never opened."* **Neither did
|
||
this exporter.** It opened a leaf in exactly one place — `highlight_name`, for
|
||
focus records — and nowhere else.
|
||
|
||
### It is not two elements. It is 45
|
||
|
||
| screen | elements with a dropped leaf |
|
||
|---|---|
|
||
| `main_menu`, `extras`, `press_start` (+ JP twins) | every button — `ptbtn01.rat` … `ptbtn13.rat`, `ptbtn00.rat` |
|
||
| `title`, `extras` | `ptloop01.rat`, `ptloop02.rat` |
|
||
| `build_00/01/12/15` | `pgloading_loop1/3/4/5.rat` |
|
||
| `title_jp` | `ptlogo_eff2.rat` — **two** elements |
|
||
|
||
⚠️ The buttons are the benign case and `screen.rs` already knew it: *"a BASE
|
||
record's leaf duplicates the parent's placement and the two can differ by a unit
|
||
(`ptbtn04`: parent y=401, leaf y=402). There the parent wins."* The `ptloop` case
|
||
is the opposite — the parent carries **no geometry at all** and the leaf carries
|
||
all of it. 🟡 And `title_jp`'s `ptlogo_eff2` is the element `DECISIONS` has
|
||
recorded since P1 as the single largest render disagreement in the export, *"the
|
||
one drawn element at a scale that is not a whole multiple of 100 %"*. It has a
|
||
two-element leaf. That is a lead, not a conclusion.
|
||
|
||
### Emitted, deliberately not drawn
|
||
|
||
`Element::leaf` now ships the decoded leaf, and one `read_leaf` closure serves
|
||
both it and the focus path — a second copy is how the case would go missing
|
||
again.
|
||
|
||
🔴 **`ScreenView` ignores it**, and that is the honest state. Parent and leaf each
|
||
carry their own alpha ramp on a **different span** — parent `0 → 255` over
|
||
t=70…238, leaf `255 → 0x80 → 255` over t=150…600 — so **how the two compose is a
|
||
decoding question**, and drawing the leaf on a guess would replace a visible
|
||
1.82 % gap with an invisible wrong one. `verify-screen` confirms nothing moved:
|
||
`title` still max 6 / over3 790, `main_menu` max 4, `title_jp` max 155.
|
||
|
||
✅ **Additive blending is refuted** — the Decoder tested `T8aD +0x04` bit `0x02`
|
||
as an additive selector and *"every measure worsens"*. My blend-mode hypothesis
|
||
from last iteration is dead, and the export carries no blend field because **none
|
||
has been found**: the per-draw capture records primitive type, index count,
|
||
shader hashes, texture bindings and vertex attribute 0, and **no
|
||
`RB_BLENDCONTROL`**.
|
||
|
||
🔵 **And this makes the port's biggest oracle gap the same item as the rotation
|
||
question already standing with the human.** `sylpheed-cli screen render`
|
||
deliberately does not rotate, which is why *both* renderers show it — the
|
||
Decoder measures its own residual as tiles running −38.6 then +33.8 and
|
||
cancelling, the same shape as mine. It is a **shared decode gap, not a defect in
|
||
my compositor**, and MISSION's *"Needs a human decision — rotation"* now has a
|
||
number attached: **1.82 % of the title's pixels, in a signature that can be
|
||
recognised.**
|
||
|
||
## The leaf composition is decoded and implemented — and it does **not** close the 1.82 %
|
||
|
||
The Decoder decoded the rule I refused to guess: **draw the leaf on its own
|
||
timeline; do not multiply the parent's alpha in.** Multiplying is *refuted*, not
|
||
merely unsupported — at the fitted time the parent has expired (its group returns
|
||
to 0 at t=250 and holds), so `leaf × parent / 255` predicts zero for both quads
|
||
and the sweeps would be invisible. They are drawn.
|
||
|
||
The fit is worth repeating because of its shape: the game's own composed alpha is
|
||
observable in the per-draw vertex colours (`C3FFFFFF`/`B6FFFFFF` = **195** and
|
||
**182**), fitting *only those two numbers* against the two leaf ramps gives one
|
||
consistent time **t = 355**, and the same t then **predicts** quad centres at 981
|
||
and 478 against **992.0** and **467.2** measured. No x entered the fit.
|
||
|
||
Implemented: `_draw_leaf` runs the leaf unclamped — like the spinning ring, and
|
||
for the same reason. Held at its own `rest.t` the leaf sits at **x = 1521**,
|
||
entirely off the right edge, so `holding` would delete the sweeps rather than
|
||
settle them.
|
||
|
||
### 🔴 And it changes nothing measurable. The title is still 1.82 %
|
||
|
||
| t | units | differing |
|
||
|---|---|---|
|
||
| 4.35 s | 261 | 1.82 % |
|
||
| **5.917 s** | **355** | **1.81 %** |
|
||
| 7.00 s | 420 | 1.79 % |
|
||
|
||
**At t=355 my interpolation puts the leaf's top-left at x ≈ −324 — off-screen
|
||
left — where the Decoder's model puts the quad's centre at 981.** Those cannot
|
||
both be right, and the gap is not something to tune away: it is a disagreement
|
||
about how the leaf's keyframes become a placed quad, most likely in the pivot and
|
||
the rotation about it. **Handed back with both numbers.**
|
||
|
||
So the honest state: the exporter no longer drops the data, the composition rule
|
||
is implemented as decoded, and **the port's largest oracle gap is exactly where
|
||
it was.** Fixing the export was necessary and was not sufficient.
|
||
|
||
### Two flagged elements deliberately not drawn
|
||
|
||
`authored/rendering.json` narrows drawing to `ptloop01`/`ptloop02` — what the
|
||
decode covers — while the exporter keeps flagging all 15.
|
||
|
||
* **`title_jp/ptlogo_eff2`** — parent 125 % at (412,96), leaf 100 % at (402,66).
|
||
The same shape, and the element `DECISIONS` has recorded since P1 as the
|
||
largest render disagreement in the export. The Decoder said plainly *"I have
|
||
not tested it."* Drawing it would extend a decode past the case it was fitted
|
||
on — the failure this port has made four times this session.
|
||
* **`pgloading_loop5`** — leaf scale **(0, 0)**. A zero scale is one of the three
|
||
historical failures this corpus names, so it is exactly where a guess costs
|
||
most.
|
||
|
||
⚠️ **And neither can be adjudicated here.** `title_jp` has no oracle capture, and
|
||
`verify-screen` compares against `sylpheed-cli`, **which draws no leaves at all**
|
||
— so *any* leaf drawing increases that divergence whether it is right or wrong.
|
||
Its `max` went 155 → 232 when they were drawn, and **that number is not evidence
|
||
in either direction.** Recognising it as uninformative is the reason it did not
|
||
become a justification.
|
||
|
||
## The −324 was the old keyframe association, and the corrected one is available **today**
|
||
|
||
The Decoder found it and it is not geometry: my leaf path pairs each pose with
|
||
the *next* pose's time. Fed my pairing into its own interpolation, it reproduces
|
||
**−324 to the digit**. Corrected, t=355 gives top-left **781** and centre
|
||
**980.5** against **992.0** measured — so there was never a 1 300 px pivot
|
||
mystery to look for, and I am glad I said the two numbers instead of hunting one.
|
||
|
||
### 🔴 Correcting BLOCKED: this was never gated on a branch landing
|
||
|
||
`docs/port/BLOCKED.md` has carried the keyframe-record-layout row as *"NOT taken,
|
||
and it cannot be taken yet … `sylpheed-formats` is a path dependency, so this
|
||
reaches the port only when that branch lands on `main`."*
|
||
|
||
**That is wrong.** The corrected association is in this checkout, behind an
|
||
environment variable:
|
||
|
||
```rust
|
||
// Set `SYLPHEED_KF_TIME_SHIFT=1` to read `W[k-1]` as block `k`'s time
|
||
// ("the word is the time the NEXT pose is reached") instead of `W[k]`.
|
||
```
|
||
|
||
It has been switchable the whole time. **I read that file twice this session —
|
||
once for `rotation_deg`, once for the leaf note — and did not notice the switch.**
|
||
|
||
### The experiment, run: mixed, and not decisive for the reason that matters
|
||
|
||
Re-exported with `SYLPHEED_KF_TIME_SHIFT=1` and asked the oracle:
|
||
|
||
| screen | default | shifted |
|
||
|---|---|---|
|
||
| `main_menu` | 13.21 / 0.06 % | 13.81 / **0.10 %** — worse |
|
||
| `extras` | 13.38 / 0.20 % | 13.95 / **0.24 %** — worse |
|
||
| `title` | 21.07 / 1.82 % | **20.41** / 1.86 % — RMSE better, area worse |
|
||
| `publisher_logo` | 10.77 / 1.00 % | **9.05 / 0.75 %** — better |
|
||
| `developer_logos` | 9.37 / 0.39 % | **8.86 / 0.33 %** — better |
|
||
|
||
⚠️ **And it does not adjudicate the association, because the port's renderer is
|
||
built for the other one.** Under the shift the **untimed keyframe moves from last
|
||
to first** — the leaf reads `t=None` at x=−639, then t=150, t=540 — while
|
||
`pose_at` is written around *"the final keyframe carries no `t`, so it is given a
|
||
synthetic time `exit_ramp_units` after the last timed frame."* So this measures a
|
||
**renderer/association mismatch**, not the association. BLOCKED said as much:
|
||
the change touches `pose_at`, `settle_units`, `spin_period_units`,
|
||
`exit_ramp_units` and the plate.
|
||
|
||
**Export reverted to the default.** Adopting the shift is a real piece of work —
|
||
re-deriving the exit ramp, the settle, the spin period and the plate against a
|
||
layout where the untimed frame is the *first* — and doing it hastily at the end of
|
||
an iteration is how a 1.8 % gap becomes five wrong ones.
|
||
|
||
### ⚠️ The methodological point, which is the Decoder's and is the best thing here
|
||
|
||
Its rule matched because **alpha at t=355 sits inside a long segment where a
|
||
one-keyframe shift barely moves it, while x sweeps 1 560 px over the same span.**
|
||
It confirmed on the insensitive quantity; I was wrong on the sensitive one; and
|
||
neither of us saw it until the two were compared.
|
||
|
||
> **Check a new interpretation against the fastest-moving field you have, not the
|
||
> one that happens to agree.**
|
||
|
||
That is a different failure from the ones this session has collected — not an
|
||
uncontrolled instrument, but a control chosen where it could not fail.
|
||
|
||
❔ A residual **11.5 px** (980.5 against 992.0) is left over and is **not** to be
|
||
fitted. A rotation about a declared pivot rather than the centre would displace by
|
||
roughly that; if it still matters once the association is adopted, it gets
|
||
measured rather than derived.
|
||
|
||
## Re-running the P5/P6 gate after eight iterations of changes
|
||
|
||
The leaf path, the plate pulse, the BGM level, the voice export, the focus fix
|
||
and a new diagnostic flag have all landed since the gate was last actually run.
|
||
The mission's test is *"a human presses a d-pad and Ⓐ and moves through those
|
||
screens"*, and I had been verifying pieces of that against captures without once
|
||
re-running the whole walk.
|
||
|
||
**It works.** Nine steps, unattended:
|
||
|
||
| step | | |
|
||
|---|---|---|
|
||
| ⬇ ×4 | `ptbtn01` → `ptbtn05` | focus moves, every frame drawn |
|
||
| Ⓐ | `EXTRAS` → screen `extras`, focus `ptbtn11` | |
|
||
| Ⓑ | back to `main_menu`, **focus restored to `ptbtn05`** | HANDOFF Q5's rule, live |
|
||
| ⬆, Ⓐ | `ptbtn04` `OPTIONS`, destination outside this archive | prints the gap rather than pretending |
|
||
|
||
Filmstrip shared as `1788027380-788b1faafc3e`. Every shot is non-blank (frame
|
||
means 31.5–33.1).
|
||
|
||
### The sound is verified by a null control, not by a detector
|
||
|
||
I first tried an onset detector: count sharp level rises in the recording. It
|
||
found **one** onset in the walk — and **the same one** in the music bed alone,
|
||
which contains no cues. The 50 ms envelope cannot see a short cue over the bed.
|
||
⚠️ **The right response to a detector that cannot separate its control from its
|
||
subject is to stop using it**, not to widen the window until the numbers look
|
||
better. That is the shape this session has hit five times.
|
||
|
||
So the same technique that settled the voice: shadow the three cues with silence
|
||
through `data/mods/`, run the identical walk, and compare.
|
||
|
||
| | peak | RMS |
|
||
|---|---|---|
|
||
| walk, cues playing | **+0.0003 dBFS** | −18.36 |
|
||
| walk, cues silenced | **−4.74 dBFS** | −20.78 |
|
||
|
||
**Silencing three sound effects costs 4.74 dB of peak and 2.43 dB of RMS**, and
|
||
the walk's peak is *set by* a cue rather than by the music. The mod log confirms
|
||
exactly three files shadowed. No detector, no threshold, and the control is the
|
||
same run with one input changed.
|
||
|
||
⚠️ Recorded under the Dummy driver, as everything audio here is. It shows the
|
||
cues reach the Master bus at the right moments; it does not show they are the
|
||
cues the game plays — that is HANDOFF Q8, and `authored/audio.json` still carries
|
||
the offsets as measured rather than decoded.
|
||
|
||
## Pinned `formats-pin-2026-08-29c` — and the knob I tested last iteration was retired
|
||
|
||
🔴 **I tested the wrong switch.** `SYLPHEED_KF_TIME_SHIFT` is a **superseded
|
||
partial fix**: it got the association right but **left pose 0 untimed**, which is
|
||
exactly why the untimed keyframe appeared to "move from last to first". It does
|
||
not exist in the current parser. The real correction is the **default** in the
|
||
tagged crate, with the old reading behind `SYLPHEED_KF_TIME_LEGACY=1` — the
|
||
opposite polarity from what I had.
|
||
|
||
So last iteration's five rows measured a renderer/association mismatch **against
|
||
a knob nobody should use**. I suspected they were not decisive; I did not suspect
|
||
the knob itself was retired.
|
||
|
||
### The consequence is smaller than I budgeted for: there is no untimed keyframe
|
||
|
||
A placement group is an 8-byte header then `frames` × `{u32 time; 36-byte pose}`,
|
||
so pose 0's time is the group's lead-in word and **every pose is timed, including
|
||
the last.** Measured on the re-export: **866 keyframes, 0 untimed.**
|
||
|
||
`pose_at`'s premise — *"the final keyframe carries no `t`, so it is given a
|
||
synthetic time `exit_ramp_units` after the last timed frame"* — does not invert,
|
||
it **disappears**. The branch is now dead code rather than wrong code, which is
|
||
why nothing needed re-deriving to adopt this.
|
||
|
||
And the leaf reads as the Decoder's table says: t=0 x=−639, t=150 x=−39, t=540
|
||
x=1521. At t=355 that interpolates to **x = 781** — the top-left it predicted, and
|
||
the 1 300 px discrepancy is gone.
|
||
|
||
### Pinned by tag, which is what MISSION §2 is for
|
||
|
||
> *"The RE agent tags when it lands something you need and tells you over the
|
||
> message channel — that is how you stay current without floating."*
|
||
|
||
That is precisely what happened, so `crates/sylpheed-export/Cargo.toml` now pins
|
||
`formats-pin-2026-08-29c` by tag. ⚠️ **`BLOCKED.md` was wrong in both
|
||
directions** — it said the change "cannot be taken yet" *and* that it arrives
|
||
only when the branch lands on `main`. It arrives when the tag is pinned.
|
||
|
||
🔴 **The cost, stated rather than discovered later:** `sylpheed-cli` builds from
|
||
the **workspace** crate, so until this reaches `main` the exporter and the
|
||
reference renderer read **different decoders**, and `verify-screen` is comparing
|
||
two eras rather than detecting drift. `verify-capture` is unaffected — it
|
||
compares the port against oracle **captures** and never touches the CLI — and it
|
||
is the check that matters. Revert to the path dependency the day the tag is an
|
||
ancestor of `main`.
|
||
|
||
### What the oracle says
|
||
|
||
| screen | before | after |
|
||
|---|---|---|
|
||
| `publisher_logo` | 1.00 % | **0.75 %** |
|
||
| `developer_logos` | 0.39 % | **0.33 %** |
|
||
| `extras` | 0.20 %, region **736×525** | 0.19 %, region **398×295 at (441,230)** |
|
||
| `main_menu` | 0.06 % | 0.06 % |
|
||
| `title` | 1.82 % | 1.82 % |
|
||
|
||
The splashes improve outright. ⚠️ **`extras` is the interesting row**: the *area*
|
||
barely moved but its differing region **collapsed onto the sweep position**
|
||
(441, 270) — the residual is now localised to the one element still in question
|
||
rather than spread over the screen.
|
||
|
||
The title does not move. Its row is now posed at **t=355**, the Decoder's fitted
|
||
sweep time, because the leaf group ends at t=600 with the quads parked off-screen
|
||
at x=1521 — posing at the settle simply omits them. ⚠️ **t=355 is not the time
|
||
that minimises the difference**: t=390 measures **1.65 %**. Picking that would be
|
||
fitting the pose to the score, which is what this harness exists not to do.
|
||
|
||
## Refuted — my own "the single non-whole-multiple scale in the export"
|
||
|
||
`DECISIONS` has said since P1 that `ptlogo_eff2` is *"the single drawn element in
|
||
the whole export at a scale that is not a whole multiple of 100 % (125 %)"*.
|
||
**That census was parents-only.** Opening the 45 leaves finds **thirteen** distinct
|
||
non-whole-multiple scales — 75, 96, 99, 101, 103, 112, 125, 150, 204×208,
|
||
210×220, 250 — and 125 % is among the *rarest* at two occurrences.
|
||
`ptlogo1`/`ptlogo2` carry 101/103/112 on the **English** title.
|
||
|
||
The claim's real content was *"the only one **the port draws**"* — a fact about my
|
||
element set, not about the disc. Corrected.
|
||
|
||
🔴 **And `ptlogo_eff2` stays withheld, now for a stronger reason than caution.**
|
||
Its 125 % is a **pop**, not a steady scale: scale-0 → 125 % → scale-0 between
|
||
t=50 and t=107, ≈0.95 s. The leaf draws at 100 %, as **two superimposed copies**
|
||
at alpha 160 and 80, each rotating 360° over 960 units — 16 s per revolution.
|
||
**If parent scale gates the leaf it is a 0.95 s flash; if the leaf runs free it
|
||
spins for 16 s.** Nothing on the disc chooses, `title_jp` has no oracle capture,
|
||
and the Japanese-locale capture MISSION has parked is what would settle it.
|
||
|
||
## The 11.5 px was the fit's resolution, and the lesson inverts
|
||
|
||
The Decoder closed it **by adding observables, not by tuning** — the vertex
|
||
buffer carries positions *and* colours at the same instant, so all four
|
||
quantities must agree on one `t`:
|
||
|
||
| observable | solved t | precision |
|
||
|---|---|---|
|
||
| quad A x | **357.88** | ±0.12 units |
|
||
| quad B x | **357.58** | ±0.12 units |
|
||
| quad A alpha | 355.75 | ±1.54 units |
|
||
| quad B alpha | 354.09 | ±1.89 units |
|
||
|
||
Alpha moves only 0.27–0.33 levels per keyframe unit, so **one byte of
|
||
quantisation is worth 1.5–1.9 units, which at 4 px/unit is 6–8 px of sweep**.
|
||
That is the whole of the 11.5 px. At t = 357.7 the centres land within 0.70 px
|
||
and both alphas inside one level.
|
||
|
||
⚠️ **The lesson is the earlier one inverted, and this is the half worth keeping.**
|
||
Checking a wrong rule against alpha made it *look confirmed*. Here the same
|
||
insensitivity **manufactured a residual that did not exist**. So an insensitive
|
||
quantity does not merely fail to falsify — **it invents error.** Solve on the
|
||
fastest-moving field; check the slow one; never the reverse.
|
||
|
||
I was already looking for a pivot rule to explain 11.5 px when they wrote. There
|
||
was nothing to find.
|
||
|
||
### Refutation attempt — the pivot claim, checked here and survived with a nuance
|
||
|
||
They state the leaf pivot is (200, 90) on a 399×180 sprite, *"the pivot is the
|
||
centre, so rotation displaces it by nothing."* Checked against my own export:
|
||
pivot **[200, 90]**, sprite **399×180**, true centre **199.5, 90**.
|
||
|
||
✅ Survives. ⚠️ With one correction of no consequence: the sprite is **odd-width**,
|
||
so the pivot is the centre to within **half a pixel**, not exactly. Against their
|
||
−0.70/−0.48 px agreement that changes nothing, and it is worth stating only
|
||
because "displaces it by nothing" is the kind of sentence that later gets leaned
|
||
on for a sub-pixel claim.
|
||
|
||
`verify-capture` now poses the title at **t=357.7** rather than 355: RMSE
|
||
21.07 → **20.92**, differing 1.82 % → **1.81 %**. Marginal, and it is the right
|
||
pose for a stated reason rather than a better number.
|
||
|
||
### `ptlogo_eff2` is withheld for a better reason than mine
|
||
|
||
I was withholding it out of caution about untested generalisation. The Decoder
|
||
points out something stronger: **it is on `title_jp`, and MISSION §7 scopes out
|
||
"localisation beyond English"** — so it is not a question the menu port has to
|
||
answer at all, and the parked Japanese-locale capture does not need reviving on
|
||
its account. `authored/rendering.json` now says that first and the undecidability
|
||
second. **Widening scope to close a residual would have been the wrong trade**,
|
||
and it is the human's call either way.
|
||
|
||
## 🔴 The focus ring had silently stopped, and BLOCKED had listed it
|
||
|
||
`docs/port/BLOCKED.md` said the record-layout change touches five things:
|
||
`pose_at`, `settle_units`, `spin_period_units`, `exit_ramp_units` **and the
|
||
plate**. I checked `pose_at` and deleted `exit_ramp_units`, reported that, and
|
||
**did not work the rest of the list.**
|
||
|
||
`spin_period_units` required *"the first timed and the second untimed"*. Under
|
||
the corrected layout the ring reads `t=0 rot=0` and `t=120 rot=360` — **both
|
||
timed** — so the rule returned 0 and **the focus ring stopped spinning**. Nothing
|
||
reported it: a period of 0 is a legal *"this element does not spin"*.
|
||
|
||
Rewritten to take the **span** between the two poses. On the ring that is
|
||
120 − 0 = **120 units, the same number the old rule produced**, which is a small
|
||
piece of evidence that the corrected layout is self-consistent rather than merely
|
||
different.
|
||
|
||
**Verified the way P5 verified it — bit-identity one period apart**, on the ring's
|
||
own 60×60 box so the `ptloop` sweeps cannot confound it:
|
||
|
||
| separation | mean difference |
|
||
|---|---|
|
||
| **+120 units (one period)** | **0** |
|
||
| +120 units again | **0** |
|
||
| +30 units (quarter) | 8.61 |
|
||
| +60 units (half) | 8.88 |
|
||
|
||
⚠️ Getting there took three wrong instruments, and the sequence is the lesson.
|
||
A whole-frame `max` saturates on one rotating edge — adjacent frames scored 131
|
||
while their mean was 0.022. A live `--menu` filmstrip jitters by up to a frame,
|
||
which is ~3° of ring, and its cadence cannot be pinned. And a whole-frame
|
||
comparison is dominated by the sweeps, which move 480 px over one ring period.
|
||
**`--focus=<id>` was added so a `--screen` run can draw a focus record
|
||
deterministically**, which is what made the check reproducible at all.
|
||
|
||
## The plate's period is now the disc's 105, and it disagrees with the measurement
|
||
|
||
Under the corrected layout `ptbtn00f` runs **t=0 (alpha 0) → t=105 (alpha 0)** — a
|
||
closed cycle with every pose timed.
|
||
|
||
✅ **The ambiguity this entry carried is gone.** It used to say the cycle might
|
||
restart at t=6 rather than 0, giving 123 units, and that nothing separated the
|
||
two. There is now one reading.
|
||
|
||
🔴 **And the number is worse against the oracle, which is stated rather than
|
||
avoided.** 105 units is **1.750 s**; scaled by the factor the ring shows between
|
||
its declared 120 units and its measured 2.177 s (×1.089), **1.906 s** — about
|
||
**17 % below** every one of the corpus's four timings (2.12 / 2.19 / 2.34 /
|
||
2.31 s). The old 129 gave 2.34 s, at the top of that range, which is exactly why
|
||
it looked right.
|
||
|
||
**129 was the last timed keyframe plus `exit_ramp_units`, and that constant is
|
||
deleted.** A period built from a constant that no longer exists cannot stay, even
|
||
though it fitted better. So the port ships the disc's number **and says it is
|
||
wrong**, rather than keeping a number that agreed with the measurement for a
|
||
reason that has evaporated.
|
||
|
||
Verified: the plate is bit-identical 105 units apart (mean diff **0**) and differs
|
||
at 30 units (0.83). ❔ What would resolve the disagreement: whether the group
|
||
loops from its start at all, or holds at alpha 0 between cycles. Asked.
|
||
|
||
## The plate's period is 120, decoded — and it was falsified with my own ring number
|
||
|
||
The Decoder found it in the format: **a nested record is itself a RATC bundle,
|
||
and its header's `+0x08` is the loop length** — the same field
|
||
`ui_header_time_disc` already tests at the top level. A record's keyframes need
|
||
not fill it, and the slack is a hold at the final pose. **`ptbtn00f` is 105 units
|
||
of ramp inside a 120-unit cycle**, so the glow rests dark for 15 units. The five
|
||
`ptbtn0Nf` records fill their 120 exactly, which is what shows the slack belongs
|
||
to *that record* rather than to the format.
|
||
|
||
Disc-wide over 1 781 timed nested records: 92.3 % declare exactly their last
|
||
keyframe time, 7.7 % declare more, **0 declare less**. That last row is the
|
||
falsifier — a cycle cannot restart before its own last pose — and it never fires.
|
||
|
||
### The decisive test used this port's number, not theirs
|
||
|
||
Both candidates need the same emulator pacing factor, and **the ring measures it
|
||
independently**: declared 120 units → 2.177 s → **1.0885**.
|
||
|
||
| plate period | nominal | factor needed for the measured 2.12–2.34 s | |
|
||
|---|---|---|---|
|
||
| 105 units | 1.750 s | 1.211 … 1.337 | 🔴 excludes 1.0885 |
|
||
| **120 units** | 2.000 s | 1.060 … 1.170 | ✅ **contains it** |
|
||
|
||
**105 cannot reach the measured range under any pacing the ring also satisfies.**
|
||
Two different elements in different bundles, measured in separate runs, tied only
|
||
by both declaring 120.
|
||
|
||
⚠️ **My three readings of this number, in order, are the useful record:** 129
|
||
(`105 + exit_ramp_units`) fitted the measurement for a reason that later
|
||
evaporated; 105 (the group length) I shipped *knowing* it was 17 % short; 120 is
|
||
decoded. And the 123-vs-129 pair I once called unseparable **straddled the right
|
||
answer without containing it** — which is the sharpest argument I have seen
|
||
against treating "two candidates, nothing separates them" as if the truth must be
|
||
one of the two.
|
||
|
||
Verified: bit-identical **120 units** apart (mean diff 0), 0.061 at a quarter and
|
||
0.888 at half. Still authored, because the pinned tag does not expose `+0x08`
|
||
yet — **delete the entry the day a tag does.**
|
||
|
||
### Their `rest()` flag, checked rather than assumed
|
||
|
||
They warned that a focus record is exactly the kind of element `rest()`
|
||
mishandles, since a pulse's last hold is not its resting state. Censused: **34
|
||
focus-record elements in the export, and only 2 have a varying alpha** — both
|
||
`ptbtn00f`, EN and JP. Their `rest.alpha` is **80, identical to their peak**,
|
||
which is precisely the pathology described. The port does not hit it because the
|
||
plate is drawn through the loop path, and the other **32 are constant-alpha, so
|
||
`rest()` is safe for them**. Bounded, not hoped.
|
||
|
||
## ✅ A settled screen is ONE instant, and it collapsed three residuals at once
|
||
|
||
The Decoder's finding, applied: **`rest()` returns each element's last hold
|
||
keyframe chosen independently of every other element.** That is right for
|
||
anything that ends the screen settled and **exactly wrong for a transient** —
|
||
the title's `ptlogo_back2eff1` is a two-frame flash (0 until t52, 255 at t54–56,
|
||
0 by t58), so its last hold *is* the flash peak and `rest()` left it burning.
|
||
There are five of them, and `rest()` drew all five at once.
|
||
|
||
The settled instant is **the longest interval containing no keyframe time**, over
|
||
a bundle's **top-level** elements. Reproduced independently here before adopting:
|
||
title `[160, 236]`, midpoint **198** — the Decoder's number to the unit. ⚠️ The
|
||
top-level restriction is what makes it match: including the `ptloop` leaves gives
|
||
`[269, 540]` instead.
|
||
|
||
### Against the oracle
|
||
|
||
| screen | before | after |
|
||
|---|---|---|
|
||
| **`title`** | 20.92 RMSE, **1.81 %** | **14.61 RMSE, 0.26 %** |
|
||
| **`publisher_logo`** | 9.05, **0.75 %** | **2.17, 0.01 %** |
|
||
| **`developer_logos`** | 8.86, **0.33 %** | **3.05, 0.01 %** |
|
||
| `main_menu` | 0.08 % | 0.08 % — unchanged, window too narrow |
|
||
| `extras` | 0.19 % | 0.19 % — unchanged, window too narrow |
|
||
|
||
**Seven times fewer differing pixels on the title, seventy-five times fewer on
|
||
the publisher splash**, whose differing region is now a **13×18 box**. This is
|
||
the largest correctness gain the port has had, and none of it is mine: it is a
|
||
decode, computed from the keyframe table with no reference to any capture.
|
||
|
||
### ⚠️ It is applied only where the window is wide, and that bar is not invented
|
||
|
||
The widths in this export split with **nothing in between**: `press_start` 214,
|
||
`publisher_logo` 190, `developer_logos` 145, `title` 76 — then `main_menu` 12,
|
||
`extras` 12, the loading screens 8 and 4. A 12-unit "settle" on a menu that
|
||
builds in until t=70 is a gap between staggered ramps, not a settled pose.
|
||
|
||
The bar is **30 units**: the Decoder's disc-wide census puts the knee there (30 %
|
||
of bundles ≥ 30, 42 % under 10, the latter mostly `loop*` fragments meant to be
|
||
in motion), and this export's own screens sit **4× either side of it with nothing
|
||
between 12 and 46**. Two independent populations agreeing on where to cut is what
|
||
makes it a bar rather than a preference.
|
||
|
||
Checked unbroken: the boot pacing is unmoved (`developer_logos@4.26`,
|
||
`title@7.91`, developer agrees) and the scripted walk still runs end to end with
|
||
focus restored.
|
||
|
||
## Their census, and a framing of mine they sharpened
|
||
|
||
I reported *"34 focus-record elements in the export, only 2 with a varying
|
||
alpha"*. Disc-wide it is **210 varying, 202 with `rest()` at the peak**, across
|
||
1 130 focus records — 116 in `GP_DEBRIEFING_PILOTLOG`, 54 in `GP_MOVIE_THEATER`,
|
||
30 in `GP_HANGAR_ARSENAL`, 8 in `GP_LEADERBOARD`, and **2 in `GP_TITLE`**.
|
||
|
||
**My 2 is right because `GP_TITLE` has 2.** ⚠️ But *"only 2 have a varying alpha"*
|
||
reads as a fact about the format and is a fact about one pak — and the pathology
|
||
sits in exactly the screens a wider port reaches next. The sentence was true as
|
||
measured and false as remembered, which is the failure this corpus keeps
|
||
finding, and it was mine this time.
|
||
|
||
⚠️ **And they corrected a framing I had:** I called `rest.alpha == peak` "the
|
||
pathology". It is worse than that — **a pulsing element has no resting pose at
|
||
all.** The question `rest()` answers is *malformed* rather than mis-answered,
|
||
because the element's state is a phase, not a value. `pose_at(t)` with `t` inside
|
||
the record's own declared cycle is the only well-formed query on one.
|
||
|
||
🔴 Worth carrying for whenever this port grows: `GP_LEADERBOARD`'s
|
||
`py_ranking_btn01f` swings 255 → 127 → 255 with no two adjacent keyframes equal,
|
||
so `rest()` falls through to its longest-dwell rule and returns **244** — neither
|
||
peak nor trough. **A glow stuck at its peak is visibly wrong; one stuck at 244 of
|
||
a 127–255 range looks entirely plausible, and nothing reports it.**
|
||
|
||
✅ And a free second instance of the loop-length decode from a pak neither of us
|
||
was looking at: `py_ranking_btn01f`'s ramp ends at **t=90 inside a declared 120**
|
||
— 30 units of hold, the same shape as the plate's 105-in-120.
|
||
|
||
## Their "do not draw all five flashes" flag — checked, and it does not apply here
|
||
|
||
The Decoder armed a draw capture before the title exists and caught the build-in
|
||
on the console: `ptlogo_back2eff1` in frames 130–131, `eff2` at 133, `eff4` at
|
||
133–135 — and **`eff3` never drawn at all.** Not a miss: a flash's peak is 2
|
||
keyframe units, which at that run's pacing is **0.85 of a presented frame**, so
|
||
which flashes get sampled is a matter of phase. The console shows a *subset* on
|
||
any given play, and the warning was that drawing all five would read heavier than
|
||
the real thing.
|
||
|
||
**Checked rather than reasoned about.** Sweeping the port's build-in with
|
||
`--no-hold`:
|
||
|
||
| t | flashes drawn |
|
||
|---|---|
|
||
| 54, 56 | `eff1` |
|
||
| 58 | `eff2` |
|
||
| 60 | `eff2`, `eff3` |
|
||
| 62 | `eff3`, `eff4` |
|
||
| 64 | `eff4`, `eff5` |
|
||
| 66, 68 | `eff5` |
|
||
|
||
**The port draws them sequentially, never more than two at once**, and the two
|
||
only overlap where their declared windows abut. That is the stagger the disc
|
||
declares, not a pile-up — the pile-up was the `rest()` bug, and it is fixed.
|
||
|
||
⚠️ **So the difference from the console is presentation rate, not content.** At
|
||
60 fps each 2-unit flash gets ~2 frames; at the console's pacing it gets 0.85, so
|
||
some are skipped. **A frame-by-frame comparison of the build-in against a console
|
||
capture will therefore show flashes the console missed, and that is a fact to
|
||
know rather than a bug to fix.** It is also why the settled-frame comparison —
|
||
the one `verify-capture` makes — is unaffected: at t=198 none of the five is
|
||
drawn.
|
||
|
||
### Three things of theirs worth recording
|
||
|
||
✅ **My top-level restriction was verified, not merely accepted**: top-level
|
||
`[160, 236]` width 76, versus `[269, 540]` width 271 with the `ptloop` leaves
|
||
included — an instant *after every top-level element has exited*. Worth having on
|
||
their page, because the rule as described permits the wrong reading.
|
||
|
||
✅ **The 120-unit loop is confirmed from the guest's own vertex data**, not
|
||
inferred from pixels: the glow quad's per-vertex colour alpha *is* the element's
|
||
fade alpha, giving an observed range of **0…80 against a decoded peak of 80**,
|
||
exact and unfitted, over 20 cycle starts. Fitting the decoded ramp gives RMS
|
||
13.16 alpha levels against **38.18 for the same ramp reversed** — if the shape
|
||
carried no information those would be equal.
|
||
|
||
📌 **A trap noted for whenever this port grows draw-stream tooling:** a 2D draw's
|
||
identity is its **vertex geometry, not its bound texture**, because these sprites
|
||
sample large shared pages. Matching on texture dimensions told them first that no
|
||
flash is ever drawn, then that `ptbase2` and `pteff04` are drawn in frames 75–105
|
||
— which are the intro movie, whose YUV planes are 640×360 targeting 1280×720.
|
||
Two errors, opposite directions, one pass, neither loud.
|
||
|
||
## ✅ The `publisher_logo` residual was a missing black hold, and we had both dismissed it
|
||
|
||
I had carried this as *"0.03 s outside a composite bound, probably a property of
|
||
the bound rather than the game"*, and the Decoder agreed. **We were both wrong,
|
||
and the way it was settled is the point: I stopped reasoning about the bound and
|
||
filmed the transition.**
|
||
|
||
At 0.05 s the port fell straight out of the publisher's fade into the developer
|
||
logos — mean 5.06 → 0.32 at t=4.20, then **5.65 at t=4.25**. There was **no black
|
||
frame at all**, where the oracle measures a 0.17–0.23 s pure-black plateau
|
||
(HANDOFF Q7). The bound was fine. The port was missing a fifth of a second of
|
||
black, and had been since P3.
|
||
|
||
**Authored at 12 units**, because on the boot path there is nothing to read it
|
||
from: `publisher_logo` and `developer_logos` each carry a single `palogo_eff0` —
|
||
a 1280×720 primitive with **one keyframe at t=0**, static, not a transition ramp.
|
||
The menus' quad declares black for 12 units and 12/60 = **0.200 s** sits in the
|
||
middle of the measured range, so the number is the disc's where a screen has one.
|
||
|
||
Filmed after: **t=4.25, 4.30, 4.35, 4.40 all at mean 0** — four black frames,
|
||
0.20 s — then the developer logos at 4.45.
|
||
|
||
| | before | after |
|
||
|---|---|---|
|
||
| publisher interval | 4.26 s, **DIFFERS** | **4.47 s, agrees** |
|
||
| developer interval | 3.62 s, agrees | 3.73 s, agrees |
|
||
|
||
The settled-frame comparisons are untouched, as they should be — this is pacing,
|
||
not pixels.
|
||
|
||
⚠️ **The lesson is about the shape of the dismissal, not the number.** *"A 0.03 s
|
||
miss against a bound composed from two measured ranges plus jitter slack is more
|
||
likely a property of the bound"* is a **plausible** explanation, it was offered
|
||
and accepted by both of us, and it was wrong. The composite bound was the reason
|
||
the miss looked small — the underlying gap was 0.2 s — and a plausible
|
||
explanation for a small number is exactly how a real defect stays hidden. **The
|
||
film cost one command.**
|
||
|
||
## `ptlogo_back2eff3` — recorded, deliberately not acted on
|
||
|
||
The Decoder has reproduced across two independent build-ins that the console
|
||
**never draws `eff3`**: 0 draws against ~5 expected, while `eff1` gets 4, `eff2`
|
||
3 and `eff4` 6. Three explanations are ruled out — sampling phase (`eff3` is
|
||
non-zero over six units against a 2.23-unit step, and frames at t=60.1 and 62.3
|
||
sit inside it drawing `eff2` and `eff4` instead), a draw the log cannot see, and
|
||
a bad position guess (no quad anywhere is within ±30 of the expected 408 width;
|
||
the spectrum jumps 262 → 748).
|
||
|
||
❔ **But *why* is not established** — nothing in `eff3`'s record differs from its
|
||
neighbours: same kind `0x0`, same keyframe shape, same `u4`/`u8`, same scale.
|
||
|
||
🔴 **So the port keeps drawing it, and that is a decision rather than an
|
||
oversight.** Dropping an element the disc declares, on a measurement with no
|
||
mechanism behind it, is authoring a behaviour neither agent can derive — and
|
||
**nothing this port gates on would notice either way**: the flashes live only in
|
||
the build-in, and `verify-capture` compares the settled frame at t=198 where none
|
||
of the five is drawn. Acting would buy no measurable fidelity and cost an
|
||
unexplained exception in `authored/`.
|
||
|
||
**What would change it:** a mechanism in the record, or a gate that measures the
|
||
build-in against a capture. Until then the port is visibly wrong for two frames
|
||
during a build-in nobody compares, which is the cheaper of the two wrongs.
|
||
|
||
## The narrow settle windows are harmless, and I can now say why
|
||
|
||
Adopting the settle instant left `main_menu` and `extras` on per-element `rest()`
|
||
— their windows are 12 units, below the 30-unit bar — and I recorded that as a
|
||
gap. **It is not one.** `rest()` is malformed only for a **transient**, and the
|
||
transients are precisely on the screens whose windows are wide:
|
||
|
||
| screen | window | transients |
|
||
|---|---|---|
|
||
| `title` | 76 | `ptlogo1`/`ptlogo2` ×4, `pteff01`, `ptlogo_back2eff1…5`, `ptlogoall_eff` |
|
||
| `publisher_logo` | 190 | `palogo_sqex_eff` |
|
||
| `developer_logos` | 145 | `palogo_*_eff` ×3 |
|
||
| **`main_menu`** | **12** | **none** |
|
||
| **`extras`** | **12** | **none** |
|
||
|
||
The discriminator is *returning to dark far before the screen's own end* —
|
||
`ptlogo_back2eff1` is dark again at t=58 while the title runs to t=269 — as
|
||
opposed to the ordinary fade-in-hold-fade-out that every menu element has, where
|
||
`rest` at 255 **is** the settled pose and the final 0 is the exit.
|
||
|
||
⚠️ **I am not claiming that as a rule.** Two screens having narrow windows *and*
|
||
no transients could be coincidence; it is n = 2, and n = 2 is where I have gone
|
||
wrong before. What it does mean is that **nothing measurable is being left on the
|
||
table by the 30-unit bar today.**
|
||
|
||
## Refuted, mine — "the menu residual is localised on the `ptloop` sweeps"
|
||
|
||
I have written that twice. It came from reading the **bounding box** of the
|
||
differing pixels, which sat at (441, 230) — the sweep position. Tested by sweeping
|
||
the leaf's phase against the live-menu captures:
|
||
|
||
| `main_menu`, sweep phase | differing |
|
||
|---|---|
|
||
| t=60 (barely on screen) | 0.063 % |
|
||
| t=350 (mid-screen) | **0.183 %** |
|
||
| t≥600 (parked off-screen) | **0.061 %** |
|
||
|
||
✅ Two things fall out. **The capture shows no sweeps**: the port matches best
|
||
when they are off-screen and three times worse when they cross the middle. And
|
||
with them parked the residual's box is **834×358 at (445, 167)** — the *button
|
||
column*, which is where the focus signature lives, not the sweep position.
|
||
|
||
On `extras` the same test moves the box (398×295 at the sweep position at t=70,
|
||
736×525 spread at t=700) while the **magnitude barely changes: 0.192 % against
|
||
0.200 %**.
|
||
|
||
🔴 **So the box moves with the sweeps and the residual does not.** A bounding box
|
||
over scattered pixels tells you where the outermost differing pixels are, **not
|
||
where the difference is** — and I had been quoting it as if it localised a cause.
|
||
`verify-capture` prints that box, so this is a caution about reading my own tool.
|
||
|
||
### And a first piece of evidence on whether the sweeps loop
|
||
|
||
The disc gives one pass, t=0…600, ending parked off-screen at x=1521.
|
||
`ORACLE-CAPTURES.md` says the title's sweeps *"move continuously"*, which I had
|
||
taken as implying a loop. **The idle main-menu capture matches best with them
|
||
off-screen**, which is evidence they run once and park.
|
||
|
||
⚠️ One capture, one screen, and "best match" is a weak instrument for an absence
|
||
— but it is the first evidence either way, and it points against looping. The
|
||
loop-length field the Decoder decoded (`+0x08` of a nested record's header) would
|
||
settle it outright; it is not in the pinned tag.
|
||
|
||
## Refuted — "the developer splash is one composited quad, the bounding box of the three logos"
|
||
|
||
The Decoder observed the game submitting **one 525×259 quad at (378, 155)** on
|
||
the developer splash and read it as the bounding box of the three logos, warning
|
||
that drawing three sprites there draws something the console does not. **The
|
||
arithmetic does not support it**, and the port keeps drawing three.
|
||
|
||
| | bounding box |
|
||
|---|---|
|
||
| the **three logos** | **500×421 at (390, 164)** |
|
||
| `gamearts_eff` + `seta_eff` | **521×261 at (379, 154)** |
|
||
| the observed quad | **525×259 at (378, 155)** |
|
||
|
||
**A 259-tall quad cannot contain the three logos**, which span y 164…585:
|
||
`palogo_anima` alone starts at y = 449, thirty-five pixels below that quad's
|
||
bottom edge. The observed quad matches the union of the two `_eff` **glows** to
|
||
about four pixels in every dimension.
|
||
|
||
⚠️ And those two are **transients** — my own census flagged them, dark again by
|
||
t=45 — so a frame containing that quad is a **build-in** frame, not the settled
|
||
screen. Consistent with a draw capture that starts early, which is exactly what
|
||
theirs does.
|
||
|
||
I cannot see their draw stream, so I have sent the arithmetic rather than a
|
||
verdict. What I will not do is stop drawing an element on a claim whose stated
|
||
identification excludes that element from its own bounding box.
|
||
|
||
## The black hold is 9 units, not 12 — measured in draws rather than luminance
|
||
|
||
I authored 12 from HANDOFF Q7's luminance plateau of 0.17–0.23 s, supported by
|
||
the menus' transition quad declaring black for 12. The Decoder counted **submitted
|
||
quads** instead, which is the better instrument: luminance cannot separate the
|
||
outgoing fade's tail from true black.
|
||
|
||
Frames 21–125 submit `palogo_sqex` fading to alpha 7; **frames 126–129 submit no
|
||
sprite quad at all**; 130–153 fade the developer splash in from alpha 34. Four
|
||
presented frames at 2.284 units/frame — a rate derived from the **disc as its own
|
||
clock**, because that run ran at 13.1 fps against 28 elsewhere — gives **9.1
|
||
units = 0.152 s**, ±1 frame 6.9–11.4.
|
||
|
||
⚠️ **It disagrees with the luminance figure and the disagreement is the point.**
|
||
0.114–0.190 s against 0.17–0.23 s overlaps only at the top, and the true black is
|
||
**shorter** than 9 even so: both boundary frames still carry picture. My 12 was
|
||
also supported *by analogy* — a different screen's quad on a different path,
|
||
where the boot splashes carry no quad at all. **A number that fits by analogy
|
||
loses to one measured in place.**
|
||
|
||
`verify-dwell`'s bound moved with it. Both screens still agree: publisher 4.42 s,
|
||
developer 3.78 s.
|
||
|
||
## The title's sweeps loop — measured, and the field could not have told us
|
||
|
||
The disc gives one pass (`ptloop01` t=0…600, `ptloop02` t=0…720), each ending
|
||
parked off-screen, and the port ran them once. **The oracle says they loop**:
|
||
across two title dwells the sweep quad oscillates over its whole x range and
|
||
resets hard to the same start — one reset in the first dwell, two in the second.
|
||
|
||
🔴 **And the loop-length field could not have settled it, which corrects a hope I
|
||
had stated.** Both records declare exactly their last keyframe time — **slack
|
||
zero** — and *"loops at 600"* and *"runs once for 600 and stops"* write the
|
||
identical header. 92.3 % of records on the disc are in that state, so the field
|
||
discriminates only where there **is** slack, as the plate's 105-in-120 had.
|
||
|
||
Implemented and verified on the two sweeps' **least common multiple**, since they
|
||
have different periods: 600 and 720 realign at **3600 units = 60 s**.
|
||
|
||
| separation | mean difference |
|
||
|---|---|
|
||
| **+3600 units (LCM)** | **0** |
|
||
| +1800 units | 0.438 |
|
||
| +600 units (`ptloop01` only) | 0.100 |
|
||
|
||
⚠️ **Scoped to the title**, because that is where it is measured. The menus
|
||
declare the same 600/720 and nothing on the disc distinguishes them, but my own
|
||
weak evidence points the other way there — sweeping the phase against
|
||
`live-main-menu.png`, the port matches best with the sweeps **off-screen**
|
||
(0.061 %) and three times worse mid-screen (0.183 %), and if they looped the
|
||
sweep is on screen for roughly 73 % of the cycle. **Two weak signals in opposite
|
||
directions is a reason to scope, not to pick.**
|
||
|
||
## The menus' residual is the tone floor, not structure — and `extras` is not really 3× worse
|
||
|
||
`extras` sits at 0.19 % differing against `main_menu`'s 0.06 %, on two screens of
|
||
the same family, and that gap wanted explaining.
|
||
|
||
**Signed difference (port − capture), by cell:**
|
||
|
||
| | x=0 | x=320 | x=640 | x=960 |
|
||
|---|---|---|---|---|
|
||
| `extras` y=169 | **+12.13** | −3.64 | +2.43 | **+10.60** |
|
||
| `extras` y=338 | **+12.29** | +1.36 | +1.42 | **+9.05** |
|
||
| `main_menu` y=169 | **+11.63** | −0.68 | +3.01 | **+10.24** |
|
||
| `main_menu` y=338 | **+11.03** | +3.93 | +2.92 | **+8.84** |
|
||
|
||
**The two screens are nearly identical**, and the port is uniformly **+9 to +12
|
||
brighter in the dark outer columns** — which is exactly the transfer curve I
|
||
measured earlier: γ > 1 in the darks, capture darker than render. There is no
|
||
dipole, no displacement, no missing element.
|
||
|
||
So the 0.06 % / 0.19 % gap is **not a difference in fidelity**. The thresholded
|
||
count only sees pixels differing by more than 64 levels, which are text and
|
||
sprite **edges**; the two screens simply have different amounts of high-contrast
|
||
edge. The *level* disagreement, which is what a tone term produces, is the same
|
||
on both.
|
||
|
||
⚠️ **This is the bounding-box lesson again in a different costume.** I had two
|
||
numbers, 0.06 and 0.19, and took the ratio as meaningful. It is a count of
|
||
threshold crossings, and a count of threshold crossings is not a measure of how
|
||
wrong a screen is.
|
||
|
||
### A diagnostic trap of my own, worth writing down
|
||
|
||
My first pass at this reported **10 of 18 elements "transparent at rest"** on
|
||
`extras` — the buttons, the title, the frames — and looked exactly like a
|
||
missing-element bug. It was not. **`--screen=NAME` without `--time` renders at
|
||
t = 0**, and `pose_at` clamps `t` to `minf(t, settle_units)`, so t=0 stays t=0 and
|
||
every element is still at its first keyframe. Passing `--time=2.0` draws 18 of 18.
|
||
|
||
The tool was right and my invocation was wrong, and the failure looked like a
|
||
serious defect rather than an empty argument. Same family as the instrument traps
|
||
this session has collected — and mine was the one that reported a *worse* problem
|
||
than existed, which is the direction that wastes an iteration rather than hiding
|
||
one.
|
||
|
||
## Refutation attempt — their 239.8-unit figure, checked from my export
|
||
|
||
The Decoder converted the boot's black gap using the disc as its own clock:
|
||
*"`palogo_sqex` declares alpha ≥ 1 for **239.8 units** and is drawn in 105 frames
|
||
→ 2.284 units/frame."* That 239.8 comes from their reading of the record; I have
|
||
the same element in my export and can compute it independently.
|
||
|
||
`palogo_sqex` ramps 0 → 255 over t=15…30 and 32 → 0 over t=251…255. Under the
|
||
linear ramp the port already uses, alpha first reaches 1 at **t = 15.0588** and
|
||
last exceeds it at **t = 254.8750**:
|
||
|
||
**239.816 units.**
|
||
|
||
✅ **Survives, to four significant figures.** It matters more than a spot-check:
|
||
that number is the *denominator* of the units-per-frame conversion behind the
|
||
9-unit black hold I just authored, so an error in it would have propagated
|
||
straight into a constant I ship. Two derivations from different sides of the same
|
||
record agreeing to 0.02 % is what makes that constant safe to hold.
|
||
|
||
## 🔴 The loading screens are black at *every* instant — which proves the layer rule wrong for a layerless element
|
||
|
||
`build_12` and `build_15` have rendered blank since P3, and I had filed it as a
|
||
`rest()` casualty: `pgloading_eff00` is a 1280×720 quad whose `rest` is opaque
|
||
black. **It is not that**, and the corrected keyframe association makes it
|
||
provable rather than suspected.
|
||
|
||
Every element's declared alpha window on `build_12`:
|
||
|
||
| element | opaque / visible | gone by |
|
||
|---|---|---|
|
||
| **`pgloading_eff00`** (black quad) | **t=0 … 38 at alpha 255** | clears at t=48 |
|
||
| `pgloading_loop4` | t=8…32 | 38 |
|
||
| `pgloading_loop1` | t=16…32 | 40 |
|
||
| `pgloading_line` | t=18…26 | 32 |
|
||
| `pgloading_str` | t=22…28 | 34 |
|
||
| `pgloading_loop3` | t=24…26 | 34 |
|
||
| `baseeff`, `eff01`, `eff02`, `loop5` | t≈16…32 | 32–40 |
|
||
|
||
**The quad is fully opaque across the entire span in which any content is up, and
|
||
it only clears at t=48 — by which time every other element has faded to zero.**
|
||
Rendered at t = 20, 30, 36, 40, 42, 44, 46, 50 units with the timeline *playing*,
|
||
the frame is **mean 0 at every one**.
|
||
|
||
So this is not a bad choice of pose. **Under the port's current layer rule there
|
||
is no instant at which this screen shows anything**, and a loading screen that is
|
||
black for its whole life is not what the game does. That is a proof by
|
||
contradiction, not a preference.
|
||
|
||
### The rule under suspicion is mine, and it is narrow
|
||
|
||
`pgloading_eff00` carries `layer_source: "none"` — no layer key at all — and the
|
||
exporter sorts a layerless element **last**, i.e. on top. Its `paint_order` is
|
||
`[6, 7, 8, 9, 1, 5, 4, 2, 3, 0]` with element 0, the quad, drawn last.
|
||
|
||
⚠️ **Every other full-frame primitive in the export has a layer key** —
|
||
`main_menu`, `extras` and `title` all give their `pteff00` `0x00008030` — so this
|
||
rule only ever bites here, which is exactly why it survived eight milestones
|
||
behind two screens nobody draws.
|
||
|
||
❔ **Where a layerless element sorts is a decoding question and I am not
|
||
answering it.** If the game sorts it *first* — behind everything — the screen
|
||
renders and the quad becomes a backdrop rather than a cover, which is what a
|
||
1280×720 black rectangle at the bottom of a loading screen would sensibly be.
|
||
That reading is *consistent* with the contradiction above, which is not the same
|
||
as being established, and I have asked rather than flipped the sort.
|
||
|
||
✅ **What this does settle:** `verify-screen`'s `BLANK` verdict on those two rows
|
||
was the right call. It reports that both renderers drew nothing and that the row
|
||
proves nothing — and `sylpheed-cli` agrees with the port here precisely because
|
||
it shares the assumption under suspicion. Two renderers agreeing, again.
|
||
|
||
## Their `eff3` retraction — my refusal was right, and my refutation found the same bug
|
||
|
||
The Decoder has withdrawn *"the game never draws `eff3`"*. It draws all five, in
|
||
the declared stagger, in both title entries. **And the mechanism was the one my
|
||
developer-splash refutation had already caught one layer down**: a draw batches
|
||
several quads and the log dumps only the first 8 vertices, so min/max over a
|
||
line's vertex list *merges* them. `eff3` (788…1196) lies entirely inside `eff4`
|
||
(447…1196), so the union is exactly `eff4`'s extent and `eff3` vanished with
|
||
nothing anomalous to see. My `525×259` was `gamearts_eff` merged with `seta_eff`
|
||
by the identical mechanism.
|
||
|
||
⚠️ **The part worth carrying is theirs**: three explanations were reported "ruled
|
||
out", and all three were aimed at the wrong failure — the *"a draw the log cannot
|
||
see"* check counted draws with **no** geometry when the hiding place was draws
|
||
with **partial** geometry. **Refuting three wrong hypotheses is not evidence for
|
||
a fourth**, and a list of failure modes written by whoever built the instrument
|
||
is the least likely to contain that instrument's blind spot.
|
||
|
||
Nothing in the port changes: `eff3` was never dropped, and the developer splash
|
||
still draws three sprites.
|
||
|
||
## The forced backdrop: two of sixteen screens were black for their whole life
|
||
|
||
`build_12` and `build_15` — the two dressed loading screens — rendered as **pure
|
||
black at every instant of their declared timeline**. Not at rest, where a wrong
|
||
`rest.t` could explain it: at t = 20, 30, 36, 40, 42, 44, 46 and 50 units with
|
||
the timeline *playing*, mean 0 in every frame.
|
||
|
||
That is not a defect you can attribute to a pose. A screen that is black for its
|
||
entire life is impossible on its face, and it is the kind of impossibility that
|
||
survives a render-vs-render diff: `verify-screen` scored those two rows
|
||
`max 0 mean 0 over3 0 OK` — **the strongest verdict that script has, awarded for
|
||
comparing nothing against nothing.** Both renderers were black because both
|
||
share `implied_layer_key`. The blank guard now in `verify-screen` was written
|
||
after that, and it is what turned the pass into a row that says it proves
|
||
nothing.
|
||
|
||
### The rule, and whose it is
|
||
|
||
It is the **Decoder's**, decoded from the file rather than inferred from the
|
||
render:
|
||
|
||
> An element that covers the screen and is **fully opaque** at some instant
|
||
> cannot paint above anything visible at that instant. Where the elements
|
||
> visible during its opaque span are **all** of them, its position is forced to
|
||
> first.
|
||
|
||
`pgloading_eff00` is a full-screen quad at alpha 255 from t=0 to t=38, clearing
|
||
at t=48; every other element on those screens peaks around t=8–32 and is gone by
|
||
t=32–40 — entirely inside the opaque span. Under a layer-key sort it painted
|
||
over all nine of them, at every instant they existed. Hence black.
|
||
|
||
### What is implemented, and the two limits that are not negotiable
|
||
|
||
`forced_backdrop_first` in `crates/sylpheed-export/src/screen.rs`, as a post-pass
|
||
over `ui_layout::derived_paint_order`. Two restrictions are copied from the
|
||
Decoder verbatim because each one was found by a test that failed:
|
||
|
||
* 🔴 **Elements with no sprite only.** Applied to sprites, the rule claimed 22
|
||
`.t32` textures must sort first *against their own layer keys*. An element's
|
||
alpha says nothing about whether its **texture** covers the screen — most of a
|
||
sprite may be transparent. The assertion that caught this was one the Decoder
|
||
had nearly deleted as over-strict.
|
||
* 🔴 **Not a name heuristic.** `*base*` first / `*eff*` last matches 77 of 80 and
|
||
fails on exactly the three families that cross it: `palogo_eff0`,
|
||
`pgloading_eff00`, `pzeff00`. `palogo_eff0.prm` is named like an overlay and is
|
||
*measured* painting first. The name is not the rule; occlusion is.
|
||
|
||
⚠️ Reach: it assumes straight alpha-over. Blend mode is undecoded, and an
|
||
additive quad at alpha 255 would not occlude. It is a **lower bound on one
|
||
element's position**, not an ordering — 80 elements are forced, 50 are
|
||
constrained but not forced, and this says nothing about those 50.
|
||
|
||
### The controls
|
||
|
||
Both are the Decoder's prior measurements off the running game. No new oracle run
|
||
was made for this change, by either agent.
|
||
|
||
| primitive | measured | our opaque instants | outcome |
|
||
|---|---|---|---|
|
||
| `palogo_eff0.prm` | **first** | 256 (they measured 211) | ✅ forced first |
|
||
| `pteff00.prm` | **last** | **2** (they measured 2) | ✅ still last |
|
||
|
||
`pteff00` is the one that would break if this were implemented as "push every
|
||
layerless element down". It is the fade cover: opaque at t=0 and again at t=269,
|
||
its screen's entry and exit, and transparent for the 253 instants between. The
|
||
constraint never binds it, and it remains last on all four title-family screens.
|
||
|
||
The `palogo_eff0` count differs — 256 against 211 — because we take the opaque
|
||
span to the **screen's** last keyframe (255) and they stop at 210. It changes no
|
||
verdict here, since the element is opaque across the whole span either way, but
|
||
the two spans are not the same span and only one of them can be the screen's.
|
||
Filed in BLOCKED.
|
||
|
||
An element **holds its final pose to the end of the screen**; it does not vanish
|
||
at its own last keyframe. Reading `palogo_eff0`'s span as `0..=0` — it declares a
|
||
single keyframe — would make the splash's backdrop a one-instant event rather
|
||
than the thing on screen for the whole splash. Rendering `build_12` confirms the
|
||
hold directly: the frame is constant from t=30 to t=60 with the timeline running.
|
||
|
||
### What changed, measured
|
||
|
||
* `build_12`/`build_15`: mean 0 at every instant → ramps in over t=0…30 and
|
||
holds (mean 1.95, max 214.5). The two BLANK rows are gone from `verify-screen`.
|
||
* The splashes are unmoved against the **oracle**: `publisher_logo` 0.01 %,
|
||
`developer_logos` 0.01 % differing region, unchanged before and after.
|
||
⚠️ That is **non-regression, not confirmation** — `verify-capture` poses at the
|
||
settle instant, and the ordering does not necessarily bind there. The evidence
|
||
for the rule is the Decoder's two controls and the impossibility of a
|
||
permanently black screen, not this row.
|
||
* Six `verify-screen` rows now DIFFER: the six screens the rule touches. The
|
||
reference `sylpheed-cli` builds from the workspace `sylpheed-formats`, which
|
||
does not have the rule. **That disagreement is expected and must not be tuned
|
||
away** — it ends when a pinned tag carries the Decoder's change, at which point
|
||
this post-pass is deleted rather than kept in two places.
|
||
|
||
It also explains 36 builds the Decoder had filed as "coming out one colour":
|
||
`pzeff00.prm` is forced first in 32 of 32 of them. Those were wiped by our own
|
||
sort.
|
||
|
||
## Refutation attempt — the forced-backdrop rule's quantifier, and whether it misses a case
|
||
|
||
The rule fires only when **all** other elements are visible during the opaque
|
||
span. That is a strict quantifier, and a strict quantifier fails quietly: an
|
||
element that is a full-screen opaque backdrop but misses the bar by one dark
|
||
element would keep its layer-key position and go on hiding the screen, exactly
|
||
the defect the rule was written to fix. So the question worth asking of somebody
|
||
else's rule is not "is it right" but **"is it enough"** — and that one I can test
|
||
without an oracle, over every layerless full-screen element in `GP_TITLE`.
|
||
|
||
| screen | element | opaque instants | others visible | forced |
|
||
|---|---|---|---|---|
|
||
| `build_12`, `build_15` | `pgloading_eff00` | 39 | **9 / 9** | ✅ |
|
||
| `developer_logos` ×2 | `palogo_eff0` | 211 | **6 / 6** | ✅ |
|
||
| `publisher_logo` ×2 | `palogo_eff0` | 256 | **2 / 2** | ✅ |
|
||
| `title`, `title_jp` | `pteff00` | 2 | 3 / 23 | — |
|
||
| `main_menu` ×2 | `pteff00` | 2 | 7 / 15 | — |
|
||
| `extras` ×2 | `pteff00` | 2 | 5 / 17 | — |
|
||
| every screen | `pteff02` | **0** | — | — |
|
||
|
||
**The rule survives, and the margin is the reason.** Nothing sits near the
|
||
boundary. Every element that fires does so at **100 %** of the others; every
|
||
element that does not is at 13–47 %, and `pteff02` never reaches alpha 255 at all,
|
||
so no quantifier could fire on it. There is no borderline case in this archive
|
||
for a stricter or looser reading to disagree about — which is the answer I could
|
||
not have gotten by re-checking the six screens where it already fired.
|
||
|
||
It also reproduces the Decoder's **second** control number exactly: they report
|
||
`pteff00.prm` forced below **3 of 23**, and `title` measures 3 of 23 here. With
|
||
their opaque-instant count of 2 already matched, two of their three published
|
||
figures for that control now come out of an independent implementation
|
||
unchanged. The third — `palogo_eff0`'s 211 against our 256 — remains the span
|
||
disagreement filed in `BLOCKED.md`, and note that the 211 is the number our
|
||
`developer_logos` row *does* reproduce. That is worth saying plainly: **the
|
||
disagreement is not a constant offset**, so "they stop 45 instants early" is not
|
||
the explanation, and whatever it is differs per screen.
|
||
|
||
⚠️ What this does **not** test: whether the rule is right about the 50 elements it
|
||
calls constrained-but-not-forced, and whether alpha-over is the blend mode. Both
|
||
are the Decoder's to settle. It tests completeness within one archive, which is
|
||
the half I can reach.
|
||
|
||
## The 256/211 was never a disagreement — and my own census had already said so
|
||
|
||
The Decoder answered the span question, and the answer is that **both numbers are
|
||
right**. `palogo_eff0.prm` appears on *both* splashes: the publisher pair
|
||
(entries 10, 13) runs to t=255 and gives **256** instants, the developer pair
|
||
(11, 14) runs to t=210 and gives **211**. I computed the publisher; their page
|
||
quoted the developer.
|
||
|
||
Worth noting how that came out, because it is the one part I can claim: the
|
||
refutation census in the section above reported **256 on `publisher_logo` and 211
|
||
on `developer_logos`, in the same table**, and concluded *"the disagreement is not
|
||
a constant offset, so 'they stop 45 instants early' is not the explanation"*. The
|
||
resolution was already sitting in my own output before their reply arrived. What
|
||
I got wrong was **filing it as a disagreement at all** — I compared one of my two
|
||
numbers against one of theirs and did not check the other row of my own table
|
||
against it. A per-screen quantity needs the screen named next to it, and my
|
||
`BLOCKED.md` row named neither.
|
||
|
||
### The span convention, confirmed rather than assumed
|
||
|
||
> The span is `0 ..= max keyframe time over EVERY element in the build`, and an
|
||
> element **holds its final pose** past its own last keyframe.
|
||
|
||
That is exactly what `forced_backdrop_first` implements, so nothing changes in
|
||
the port. Two things they add that are worth having in writing:
|
||
|
||
* the hold **is not a convenience**: a group holds at its last keyframe rather
|
||
than looping, and the header's `+0x08` never falls short of the last keyframe —
|
||
the slack *is* that hold;
|
||
* `+0x08` and the elements' maximum are **interchangeable**, zero disagreements
|
||
disc-wide. We use the elements' maximum. It stays, and this paragraph is the
|
||
note that the two were *checked* equivalent rather than assumed so.
|
||
|
||
### The hold decides 55 % of verdicts, and the oracle picks it
|
||
|
||
Reported by them over 130 keyless full-screen primitives (their measurement, not
|
||
reproduced here — their page is `docs/re/structures/ui-forced-backdrop.md`, ⚠️ not
|
||
yet on `main` as of this commit):
|
||
|
||
| alternative convention | verdicts changed |
|
||
|---|---|
|
||
| span = the header's declared `+0x08` | 0 |
|
||
| span = the primitive's own last keyframe | **72** |
|
||
| elements **gone** after their last keyframe | **72** |
|
||
|
||
So the reading I very nearly shipped — span = the element's own last keyframe —
|
||
would have changed **55 %** of the verdicts disc-wide. My first implementation
|
||
did exactly that, and `palogo_eff0` is the case that catches it: a *single*
|
||
keyframe at t=0, opaque for one instant, nothing else up yet, so the rule calls
|
||
it **free** — against a game measured painting it first. The convention is not a
|
||
matter of taste; the oracle rules one out.
|
||
|
||
✅ None of our six verdicts rests on it. `pgloading_eff00` is first under all four
|
||
conventions and `pteff00` is free under all four; only `palogo_eff0` moves, and
|
||
only under the convention its own measured order excludes.
|
||
|
||
### The sharper form of the `verify-screen` failure
|
||
|
||
Theirs, and it is better than how I wrote it: those two solid-black frames
|
||
**were not two witnesses**. Both renderers read `implied_layer_key`, so their
|
||
agreement carried *no information* — a correlated failure is indistinguishable
|
||
from a confirmation. What caught it was not a second opinion but that the agreed
|
||
answer was **impossible on its face**. *"Is this result even possible?"* beats
|
||
*"do two implementations agree?"* whenever the two share an ancestor, and in this
|
||
project they nearly always do.
|
||
|
||
### The boot gate still holds
|
||
|
||
Re-run after the reorder, since the rule changed the paint order of the first two
|
||
screens in the boot path: `publisher_logo` → `developer_logos` → `ADV` (skipped at
|
||
8.12 s) → `title` + `press_start` overlay, plate at full alpha at t=236, complete
|
||
at 10.83 s holding on the title. No script errors.
|
||
|
||
## The clock freezes at settle — the port's settle window, seen from the other side
|
||
|
||
The Decoder measured `GP_TITLE` build 4 in the draw stream and found the
|
||
top-level clock **advances through the build-in, stops inside the settle window,
|
||
and holds**. The exit ramp is not on a timer; it plays when something makes the
|
||
screen leave.
|
||
|
||
**Their interval is `[160, 236]`. The exporter computes `title`'s settle window as
|
||
`[160, 236, 198]`.** Those are the same two numbers, and they were not obtained
|
||
the same way: mine is the longest keyframe-free interval over top-level elements,
|
||
read out of the file with no game running; theirs is where a captured clock stops
|
||
advancing. A heuristic I adopted because it collapsed three pixel residuals at
|
||
once turns out to name the exact interval the game holds in. That is the first
|
||
evidence for the settle instant that does not come from the port's own renderer.
|
||
|
||
The file agrees from a third direction: `ptcopyright` sits at alpha 255 from
|
||
t=160 to t=238 — it reaches full opacity precisely as the window opens.
|
||
|
||
### Refutation attempt — their declared spans, checked against the file
|
||
|
||
Their draw-stream argument cites what the file *declares*, which I can check
|
||
without a capture:
|
||
|
||
| their claim | the file | |
|
||
|---|---|---|
|
||
| `ptlogo1` declares an exit at **t=264** | last keyframe t=264, alpha 0 | ✅ exact |
|
||
| `ptcopyright` alpha ≥ 1 for **106 units**, t=138…244 | keyframes at t=138 and t=244, alpha 0 at both | ✅ endpoints exact |
|
||
|
||
Both survive. One quibble, and it is only that: the instants with alpha ≥ 1 number
|
||
**105**, t=139…243, not 106 — the endpoints they bracket with are themselves
|
||
alpha 0, so 106 is the keyframe span rather than the visible one. It changes
|
||
nothing in their argument, whose force comes from 1 050 frames against either
|
||
number.
|
||
|
||
### What this costs the port, and it is not nothing
|
||
|
||
`authored/timing.json` and `authored/flow.json` both said *"a screen's dwell is
|
||
its OWN keyframe group"* and *"the pacing is the disc's own"*. 🔴 **That is
|
||
refuted.** Build 4 declares about 120 presented frames and dwelled ~1 100 — nine
|
||
times its own timeline. The group is not the dwell.
|
||
|
||
The **decision** is unchanged and still right: hold zero extra rather than invent
|
||
a number. What was wrong is what the port claimed for it. Leaving when the group
|
||
ends is not reproducing the disc's pacing — it is leaving at the moment the game
|
||
starts waiting. Both files now say so, and so does `boot.gd`, where the comment
|
||
had inherited the same claim.
|
||
|
||
✅ The *structure* was already right, and this is the part the measurement
|
||
confirms: `_advance` is caused by the next screen arriving, never scheduled off a
|
||
timer, and the port's own comment already read *"a screen plays itself out because
|
||
something is taking its place"*. `exit_ramp_units` was deleted for an unrelated
|
||
reason and its absence is now doubly justified.
|
||
|
||
⚠️ **Only build 4 is measured, and build 4 is the one screen where the port is
|
||
unaffected** — it is the boot's end state and holds indefinitely. The two screens
|
||
this actually governs, `publisher_logo` and `developer_logos`, have no measured
|
||
dwell at all. The port's boot is **known too fast [refuted] on both, by an unmeasured
|
||
amount**. Nothing here is a number for them.
|
||
|
||
> 🔴 **WITHDRAWN, and this paragraph stood for days after the withdrawal was
|
||
> written.** "Known too fast on both" is false: the splash dwells are **declared
|
||
> on the disc** — publisher t=0…255, developer t=0…210 — corroborated over three
|
||
> cold boots to 1.1 %, and the port was already emitting each declared value plus
|
||
> the 9-unit black hold, *exactly*. See
|
||
> [Withdrawn — "the boot is known too fast [refuted]"](#-withdrawn--the-boot-is-known-too-fast-the-splash-dwells-are-declared-and-the-port-was-already-playing-them).
|
||
>
|
||
> Found 2026-08-30 by the Decoder's rule: **grep the corpus for the claim, not
|
||
> for the file you were working in.** I wrote the withdrawal as a new section and
|
||
> left the original assertion untouched, so a reader arriving here first got the
|
||
> dead answer with nothing to warn them — the same failure as a correction that
|
||
> never reaches the manifest, one layer up.
|
||
|
||
## 🔴 Withdrawn — "the boot is known too fast [refuted]". 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 [refuted] on both splashes, by an unmeasured amount"**. That is
|
||
**withdrawn**. It was wrong, and the way it was wrong is the interesting part.
|
||
|
||
They then measured the splashes directly, over 3 cold boots:
|
||
|
||
| splash | declared | at 60 units/s | corpus wall clock |
|
||
|---|---|---|---|
|
||
| publisher (entries 10, 13) | t = 0…**255** | 4.250 s | 4.30 / 4.60 / 4.37 |
|
||
| developer (entries 11, 14) | t = 0…**210** | 3.500 s | 3.51 / 3.50 / 3.37 |
|
||
|
||
The developer agrees to **1.1 %**, two of its three runs to 0.3 %. And the port
|
||
emits **4.400 s and 3.650 s** — each declared value plus the 9-unit black hold,
|
||
exactly. ✅ **The pacing was right the whole time and no code changes.**
|
||
|
||
### What I actually did wrong
|
||
|
||
Not the arithmetic — the generalisation. Build 4 is the **title**, whose exit is
|
||
caused by something outside its timeline, so it holds. A splash's exit is caused
|
||
by nothing, so it plays its declared timeline and leaves. **The title is the
|
||
exception, not the rule.** I had one screen, it was the one screen in the boot the
|
||
port is structurally unaffected by, and I used it to overturn the two it governs.
|
||
|
||
I wrote at the time that a ratio from one screen is not a unit of pacing, and
|
||
declined to scale the splashes by nine. That refusal was right and is the only
|
||
reason this cost nothing but documentation. But refusing to apply the number
|
||
while adopting the *conclusion* it implied was half a caution: I still let one
|
||
screen's behaviour rewrite what the port claimed about two others. **The correct
|
||
move was to file build 4 as measured and leave the splashes alone**, which is
|
||
what the file now says.
|
||
|
||
### And the unit stays units
|
||
|
||
🔴 The Decoder's own container timed those same two dwells **15–20 % longer** than
|
||
both the declared values and the corpus — same disc, same declared timeline — and
|
||
three independent readings of that container's frame rate disagree with each
|
||
other. A seconds figure is one emulator's pacing on one run. The declared units
|
||
are on the disc. `authored/flow.json` `dwell` therefore takes **units**, and only
|
||
for a screen measured to wait beyond its group.
|
||
|
||
This also retires the "two timestamps would settle it" ask I filed: timestamps
|
||
were the wrong thing to author, and the measurement's own result says so.
|
||
|
||
## Refutation attempt — their two splash boundaries are not anchored the same way
|
||
|
||
Their draw stream reports *"publisher wordmark frames 6–119"* and *"developer
|
||
glows 123, wordmarks 140–209"*. Taking those spans against the declared groups:
|
||
|
||
| splash | declared units | their frames | units / frame |
|
||
|---|---|---|---|
|
||
| publisher | 255 | 6…119 = 114 | **2.237** |
|
||
| developer | 210 | 123…209 = 87 | **2.414** |
|
||
|
||
**Within one continuous boot, on one guest, those should be the same number.**
|
||
They differ by **7.9 %**, and the discrepancy runs the same direction as the
|
||
error split they have open — publisher +4.1 % where the developer is 1.1 %.
|
||
|
||
The file suggests why, and it is not the guest's clock: **the two boundaries are
|
||
anchored on different elements.** The developer span starts at its *glows*; the
|
||
publisher span is reported as starting at its *wordmark*. Those are 15 units
|
||
apart — every wordmark on both splashes is `alpha > 0` from t=16, every glow from
|
||
t=1 — and the publisher **has a glow**, `palogo_sqex_eff`, visible t=1…44,
|
||
structurally identical to the developer's three. So either frame 6 is the
|
||
publisher's glow and is mislabelled, or the publisher span genuinely starts 15
|
||
units later than the developer's and the two are not comparable quantities.
|
||
|
||
⚠️ I cannot tell which from here — it is their log, and the answer is one grep for
|
||
`palogo_sqex_eff` in it. Recording it because a 7.9 % internal inconsistency and
|
||
an unexplained 4.1 % error on the same screen are more likely one problem than
|
||
two. ⚠️ It also does **not** touch the corpus comparison, which is a separate
|
||
instrument (3 cold boots, not this draw log); their 4.1 % may still be real.
|
||
|
||
## Their corrected boundaries check out against the file — all six, exactly
|
||
|
||
The Decoder found the cause of the 7.9 % I reported, and it was worse than the
|
||
anchor mismatch I proposed: **the developer splash batches six quads into one
|
||
draw and their log dumps only the first two.** While the three glows are alive
|
||
they occupy that prefix, so the three wordmarks are invisible to the log until
|
||
the glows stop at t=45. *"Developer wordmarks first drawn at frame 140"* was the
|
||
logging prefix shifting, not the game. The anchor difference I found was a
|
||
symptom; the truncation was the cause. It is also what hid `palogo_anima`.
|
||
|
||
Their fix is to count `indices / 4`, which the 8-vertex dump cap cannot touch.
|
||
Every one of the six resulting calibration points matches this export:
|
||
|
||
| splash | their transition | their t | export |
|
||
|---|---|---|---|
|
||
| publisher | 1→2 quads | 15 | `palogo_sqex` joins at **t=16** |
|
||
| publisher | 2→1 | 45 | `palogo_sqex_eff` ends **t=44** |
|
||
| publisher | last drawn | 255 | group ends **t=255** ✅ |
|
||
| developer | 3→6 quads | 15 | three wordmarks join at **t=16** |
|
||
| developer | 6→3 | 45 | three glows end **t=44** |
|
||
| developer | last drawn | 210 | group ends **t=210** ✅ |
|
||
|
||
The two 15-vs-16 rows are a half-open boundary, not a disagreement: they name the
|
||
last frame at the old count, the export names the first instant at the new one.
|
||
|
||
✅ A second thing falls out that neither of us was looking for: their quad counts
|
||
are **1 and 2** on the publisher against **3 and 6** on the developer, and a
|
||
count restricted to *sprite-bearing* elements reproduces exactly that. So
|
||
`palogo_eff0` — the layerless forced backdrop — is **not in the batch they log**,
|
||
confirmed from the file. Their instrument and this export agree on which element
|
||
is the odd one out, having disagreed about it in every earlier iteration.
|
||
|
||
### Refutation attempt — does the drift actually explain the corpus's 4.1 %?
|
||
|
||
Their four segment rates recompute exactly (1.765 / 2.165 / 2.308 / 2.357; the
|
||
developer's two agreeing to 2.1 %, the run rising 33.5 %). The explanation is that
|
||
the publisher runs in the first seconds where the rate is furthest from its later
|
||
value. Testing what that predicts for the *corpus*:
|
||
|
||
| publisher ÷ developer | ratio |
|
||
|---|---|
|
||
| declared (255 ÷ 210) | 1.214 |
|
||
| **their drift predicts** | **1.369** |
|
||
| corpus, 3 cold boots | **1.278** |
|
||
|
||
**Sign confirmed, magnitude not.** The corpus ratio does sit above declared, which
|
||
is what the drift predicts and is real evidence. But their container's drift
|
||
would inflate it by 12.8 % where the corpus shows 5.3 % — roughly 2.4× too
|
||
strong. So drift of *some* size is doing the work; drift of *their* size is not.
|
||
|
||
⚠️ And the reason to be careful here is that the move is the one I just got wrong:
|
||
the 4.1 % is a property of the **corpus**, a different instrument (3 cold boots,
|
||
elsewhere), and the drift was measured in **their container**. Transferring it is
|
||
exactly what I did carrying build 4 onto the splashes. A general warm-up is
|
||
plausible for any emulator, so this is not baseless — but it is unconfirmed for
|
||
the corpus, and the magnitude gap is the evidence that the corpus's drift is not
|
||
theirs. It cannot be closed without the corpus's own frame log.
|
||
|
||
✅ Untouched by any of it: the declared **255** and **210**, and the port's
|
||
4.400 s / 3.650 s. Neither uses their draw log.
|
||
|
||
### The guard this puts on `keyframe_units_per_second`
|
||
|
||
⚠️ *"No single units-per-frame figure describes a run"* is a statement about
|
||
**emulator presentation pacing**, and a later reader could easily take it as
|
||
grounds to revisit the port's `60`. It is not. 60 is the **game's logical unit
|
||
rate** (HANDOFF Q1, measured), the port renders at its own frame rate and
|
||
converts through it, and guest pacing cannot reach it. `authored/timing.json` now
|
||
says so at the constant itself, where someone about to change it will read it.
|
||
|
||
## The n=1 disclosure, and the one port constant that rests on a single run
|
||
|
||
The Decoder disclosed that their `ARM=early` capture silently loses its trigger
|
||
**~40 % of the time** — two of five runs logged `ARMED EARLY` and produced no
|
||
draw log at all, indistinguishable in the session log from a run that armed. So
|
||
every draw-stream figure of theirs is **n = 1**.
|
||
|
||
That is worth more than the number it was attached to, because the port authors
|
||
constants from those runs. Auditing which: exactly one,
|
||
`black_hold_units = 9`. Everything else comes from the disc (the declared dwells
|
||
255/210, the settle window, the plate period), from the exporter, or from
|
||
multi-sample measurements (Q1's unit rate over seven frames, Q5's navigation).
|
||
|
||
**9 is not wrong, and three of its supports have moved.** Its conversion used a
|
||
105-frame count their own truncation fix has revised to 114; its second
|
||
corroboration (2.231) is the figure behind their retracted plate period; and a
|
||
run-average units-per-frame is the wrong shape for a 3–4 frame event now that the
|
||
rate is known to rise 33 % across a boot. Redone on their corrected local
|
||
segments, their two runs give **8.95** and **6.71** units.
|
||
|
||
⚠️ Those two were reconciled as replicating "within the ±1 both are quantised
|
||
to". **Overlapping error bars are not agreeing central values** — one frame is a
|
||
third of this quantity. The range is ~6.5–9.2 and the port sits at the top of it.
|
||
|
||
The value stays. Changing it would be my arithmetic on their instrument, and this
|
||
port does not author a number the corpus has not given; it is filed as a proposal
|
||
in `BLOCKED.md` with the one run that would settle it. ✅ And what is not in doubt
|
||
is that the hold is **real**: until this was implemented the port had no black
|
||
frame at all where the oracle measures a plateau.
|
||
|
||
### Their statistics, checked
|
||
|
||
Their per-boot excesses reproduce exactly: **+0.89 %, +8.24 %, +6.79 %**, spread
|
||
7.35 pp, wider than the 5.30 pp gap under test — so boot 1 alone essentially *is*
|
||
the declared ratio. Their concession is right and my 2.4× cannot carry the weight
|
||
I gave it.
|
||
|
||
One refinement, which cuts **against** their concession rather than for it: their
|
||
2.3 σ uses the *population* SD (3.178). At n = 3 the sample SD (3.892) is the
|
||
right estimator, giving **1.89 σ** — their run is *less* of an outlier than they
|
||
credited themselves with. Testing the other question, whether the corpus mean
|
||
differs from their prediction, gives t = 3.27 on 2 df, p ≈ 0.08. **Neither
|
||
framing reaches significance**, which is where both of us landed anyway.
|
||
|
||
📌 Their sharpening of the instrument point is the keeper: a truncated log and a
|
||
`--screen=NAME` render at t=0 both return a **complete, well-formed answer to a
|
||
different question**. That is why neither looks like an error — nothing inside
|
||
either view can tell you it is not the view you asked for.
|
||
|
||
## P6 gate — sound on the P5 walk, verified, and the tool I nearly shipped instead
|
||
|
||
`tools/port/verify-menu-audio`. Until now the evidence that P6's gate was met was
|
||
that `audio.play("move")` appears in `boot.gd` — evidence that a *call is
|
||
written*, not that a sound reaches the bus. Those differ, and this project has
|
||
the case to prove it: the black hold was implemented, called, and emitted nothing
|
||
for five milestones.
|
||
|
||
It needs no sound card. Godot records the Master bus to a WAV under the Dummy
|
||
driver. Three runs on `main_menu`: the walk (`down,down,accept,cancel,up`) and
|
||
two controls — `wait`×5 for the bed alone, and `left`×5 for **presses that reach
|
||
`_unhandled_input` and are bound to nothing** (Q5: left/right do nothing).
|
||
|
||
| check | result |
|
||
|---|---|
|
||
| a dead press is silent | **bit-identical** to the bed alone, 114 688 samples |
|
||
| `move` on the bus | r=0.201 at 1.85 s, bed-only 0.016, margin **+0.185** |
|
||
| `confirm` | r=0.945 at 2.14 s, bed-only 0.371, margin **+0.574** |
|
||
| `back` | r=0.660 at 2.42 s, bed-only 0.195, margin **+0.465** |
|
||
| cue order vs script order | **consistent** |
|
||
|
||
The order is the strongest line and it is free: the correlator is never told
|
||
where to look, so three different templates landing in script order is three
|
||
independent searches agreeing with the log. `move`'s absolute r is low because it
|
||
is the quietest cue under the loudest part of the bed; the margin over the same
|
||
template against the bed is what carries it.
|
||
|
||
🔴 **What it cannot conclude, and must never be read as:** that these are the cues
|
||
the *game* plays. That binding is HANDOFF Q8, the Decoder's, and nothing here
|
||
re-measures it. This tool cannot tell a correct cue from a confidently wrong one.
|
||
|
||
### The instrument I nearly shipped
|
||
|
||
The first version counted envelope bursts above a multiple of the bed level. It
|
||
reported **4 cues on one run and 0 on the next, from the same script** — its
|
||
answer was set by two hand-picked constants, the multiple and a minimum run
|
||
length, and the bed level is not constant across a run. I caught it only because
|
||
I ran it twice.
|
||
|
||
The replacement has no such constant: **the exported cue file is its own
|
||
template**, the search covers the whole recording, and the verdict is a margin
|
||
over that same template matched against the bed-only control.
|
||
|
||
⚠️ Cue *length* is deliberately not asserted. The audible part of a cue is far
|
||
shorter than its wave — 0.12–0.38 s against authored 0.344–1.016 s — because the
|
||
bed masks the tail. "Elevated for 0.13 s" is a fact about the bed, not the cue,
|
||
and I came close to filing that gap as a defect.
|
||
|
||
### A check that could not be made independent
|
||
|
||
I tried to verify Q8's cue durations against the exported waves. They agree
|
||
exactly — 0.533 / 1.016 / 0.344 — **and the agreement is worthless**: the
|
||
exporter decodes from Q8's own offsets and packet counts, so the duration is
|
||
determined by the claim under test. It confirms the export is faithful to the
|
||
authored table, nothing more. Recording it because "I checked and it matched" is
|
||
exactly what a correlated instrument feels like from the inside.
|
||
|
||
## Their `.tbm` self-refutation does not reach this archive — and it fixes my guard anyway
|
||
|
||
The Decoder downgraded 38 of the forced-backdrop rule's 80 verdicts from decoded
|
||
to inferred: those elements are `.tbm`, declaring fade `ffffffff`, and a solid
|
||
white quad painted first at alpha 255 would make the screen white. No screen is
|
||
white, so a `.tbm`'s white is a modulation **on a texture** and its element alpha
|
||
says nothing about coverage — the `.t32` mistake one file extension further out.
|
||
|
||
✅ **No verdict the port ships is affected.** All six forced elements here are
|
||
`palogo_eff0.prm` and `pgloading_eff00.prm`, `role: primitive`, `kind_raw 0x10`,
|
||
fade `0xff000000`. They fall in the 42 that stay decoded. And **no layerless
|
||
full-screen element anywhere in `GP_TITLE` has a non-black fade** — checked
|
||
across all 16 screens, so the downgrade cannot touch this archive.
|
||
|
||
But their diagnosis applies to my code regardless: *an element's alpha is not its
|
||
texture's opacity, and only an untextured primitive makes the two the same fact*.
|
||
My guard was `sprite.is_none()` — a **symptom** test, the same shape as the one
|
||
they say fixed their symptom and not their cause. It would keep admitting a
|
||
`.tbm` that this exporter happens not to emit a sprite for.
|
||
|
||
The guard is now the positive test, `role == "primitive"`. It changes no verdict
|
||
today — the six are identical before and after, 16 screens still validate — and
|
||
it is correct by construction if the corpus grows.
|
||
|
||
⚠️ Not adopted from their message: their reading that the blend question now
|
||
narrows to `pbafc.prm`. That is theirs to settle and the port draws no additive
|
||
quad either way.
|
||
|
||
## Coverage is now tested per instant, because scale animates
|
||
|
||
The Decoder found that `forced_backdrop` judged screen coverage from the declared
|
||
size alone, ignoring scale — and the disc carries its own counterexample.
|
||
`pbafc.prm` declares **844×600 at alpha `ff`**, which reads as a screen-filling
|
||
cyan wash; it is scaled **2 % × 3 %** and draws about **17×18 px**, strobing and
|
||
travelling x=178→291. A moving glint. A rule reading its declared size would call
|
||
it screen-covering.
|
||
|
||
The port had the same gap and it is closed. `scale_at` interpolates scale on the
|
||
same linear ramp as the fade, and coverage is folded **into the opaque-instant
|
||
test** rather than checked once: an instant counts only where the element is both
|
||
alpha 255 *and* covering. That is the rule's own wording — "covers the screen
|
||
**and** is fully opaque **at some instant**" — where the previous code tested the
|
||
two halves at different times.
|
||
|
||
The static size prefilter is now deliberately *not* a rejection: an element scaled
|
||
**above** 100 could cover the screen from a smaller declared size, so rejecting on
|
||
declared size would have replaced one version of the bug with its mirror.
|
||
|
||
✅ **No verdict moves.** Six forced elements before and after; 16 screens validate;
|
||
the oracle figures are identical to the digit (`publisher_logo` 0.01 %,
|
||
`developer_logos` 0.01 %, `main_menu` 0.07 %, `extras` 0.19 %, `title` 0.26 %).
|
||
Their claim that all 80 forced instances sit at scale 100 reproduces on the
|
||
GP_TITLE subset, and more strongly: **no layerless full-screen element anywhere in
|
||
this archive has a non-100 scale on any keyframe.**
|
||
|
||
It is in for the reason they gave, which is the right one: the data that would
|
||
break it demonstrably exists on this disc. That is a better argument than a
|
||
failure would have been, because it does not require the bug to happen first.
|
||
|
||
### Their blend-robustness argument, checked
|
||
|
||
They classify the blend mode **undecodable with reach** and argue the rule does
|
||
not depend on it, for a black quad:
|
||
|
||
| | drawn **first** | drawn **last** |
|
||
|---|---|---|
|
||
| alpha-over, α=255 | correct | blanks the screen |
|
||
| additive, α=255 | correct — adds nothing | correct |
|
||
|
||
The table holds. An additive black quad contributes nothing at any position, so
|
||
both orders are correct under it; only alpha-over distinguishes them, and it
|
||
picks *first*. **"First" is right under both hypotheses, "last" under one** — so
|
||
`forced_backdrop`'s verdict is robust to a question neither of us can close.
|
||
|
||
It also explains a detail of the original bug that I had not accounted for:
|
||
"layerless sorts last" was *wrong* under alpha-over and merely *pointless* under
|
||
additive, which is why those screens came out **solid black** rather than
|
||
**empty**. The symptom was diagnostic of the blend mode all along.
|
||
|
||
⚠️ Not evidence that the blend is alpha-over, and I am not recording it as such.
|
||
It is the reason the port can stop waiting on it. `pbafc.prm` remains the sole
|
||
additive candidate and is outside the rule at 17×18 px; the port draws no
|
||
additive quad either way.
|
||
|
||
## P7 gate — the new-game intro plays and returns, and a defect I nearly invented
|
||
|
||
`--menu=main_menu --script=accept` walks the P7 path: Ⓐ on NEW GAME announces the
|
||
two measured screens this export skips, opens `S00A`, plays it to its natural end
|
||
at **93.33 s** against the manifest's 93.9, and returns to the title. Nine film
|
||
frames across the movie are distinct and non-black (mean 10 → 140). The gate asks
|
||
for "plays, then returns to a defined state"; that is both halves.
|
||
|
||
### The near-miss, which is the part worth keeping
|
||
|
||
Checking that the movie's audio actually reached the Master bus, I correlated the
|
||
recording against the exported `S00A.ogv` audio and got **r = −0.0068 at the
|
||
movie's known start**, with the correlator passing its own positive control at
|
||
**r = 1.0000**. A working instrument returning zero at the right place. I was one
|
||
step from filing *"the movie's own audio never reaches the bus"* — which would
|
||
have been a serious P4/P7 defect, and the inverse of the human's original report
|
||
that the intro "plays music but no voice".
|
||
|
||
It is false. Re-run on the **boot** path, where no menu bed is playing:
|
||
|
||
| against the bed-free recording | r | at |
|
||
|---|---|---|
|
||
| `ADV` voice | **0.8855** | movie start 7.6 s |
|
||
| `ADV` movie audio | **0.4178** | movie start 7.6 s |
|
||
|
||
Both present, at the same start. The movie's audio is simply quieter than the
|
||
voice mixed on top of it, and in the P7 run the **menu bed masked it below
|
||
detection** while the louder voice survived. The bed carries across into the
|
||
movie — `play_bed` is documented as carrying across submenus — so the P7 run was
|
||
never a clean measurement.
|
||
|
||
🔴 **The lesson is about the control, not the bed.** I validated the correlator on
|
||
clean data and then ran it on masked data. A positive control proves the
|
||
instrument works *on the material it was given*; it says nothing about whether
|
||
the instrument can see through an interferer that was not in the control. **A
|
||
negative result needs its own positive control under the same masking**, and this
|
||
is the third time in this corpus that an unvalidated negative nearly became a
|
||
finding — after "10 of 18 elements transparent at rest" and the burst counter that
|
||
read 4 cues on one run and 0 on the next.
|
||
|
||
`tools/port/verify-video-audio`'s header already warned that a fidelity
|
||
comparison needs cross-correlation alignment *and* an agreed downmix. It was
|
||
right, and I would add the third condition it did not know to state.
|
||
|
||
⚠️ What is **not** settled: whether the bed *should* carry into the movie and on
|
||
to the title after it. It does, it is authored that way, and nobody has watched
|
||
the game do either. That is a separate question from this one and is not filed as
|
||
a defect — only as unmeasured.
|
||
|
||
✅ Also checked, from their `compose` finding: `GP_TITLE` declares **115 `.t32`,
|
||
45 `.rat`, 18 `.prm` and no `.tbm` at all**, and every non-primitive element has a
|
||
resolvable sprite. The "draws no pixels for an unresolvable element" hazard —
|
||
the shape that hid `pteff05` from both renderers — does not reach this archive.
|
||
|
||
## `ScreenView.skipped` was correct and unread since P1 — now it says so itself
|
||
|
||
The draw loop has always tracked what it could not draw, with the comment *"a
|
||
silently missing element looks like art"*. **Nothing ever printed it.** For eight
|
||
milestones the port could drop an element on every frame and report it to nobody.
|
||
|
||
That is the same shape as the black hold — implemented, called, emitting nothing
|
||
until somebody filmed it — and as `verify-screen` scoring two blank frames `OK`.
|
||
A fact that needs someone to remember to look at it is a fact that goes
|
||
unnoticed, so `_note_structural` **prints from inside `ScreenView`** rather than
|
||
returning a value for a caller to surface. Routing it through a caller is exactly
|
||
what did not happen.
|
||
|
||
Only **structural** skips are reported — `(no sprite in the export)` and
|
||
`(sprite failed to load)`. `(transparent at rest)` is ordinary animation, true of
|
||
every element at some instant, and reporting it would bury the two that mean
|
||
something under the one that never does.
|
||
|
||
✅ **Nothing is being skipped today**: 0 across every screen, on the boot path and
|
||
per-screen. This is a guard, not a fix. The export corroborates it — no missing
|
||
sprite PNG, no element at alpha 0 on every keyframe, none at scale 0.
|
||
|
||
### 🔴 And the first version of that scan was a false pass
|
||
|
||
My first run reported *"0 structural skips"* on ten screens. **`screen_view.gd`
|
||
did not parse.** I had inserted a line at three tabs inside a four-tab block —
|
||
the Python `assert old in s` passed because a three-tab string is a *substring*
|
||
of a four-tab line — which orphaned a `continue`. Godot loaded nothing, printed
|
||
nothing, and `grep -c` faithfully counted zero.
|
||
|
||
A count of zero from a dead script is indistinguishable from a count of zero from
|
||
a clean one, and I had already written the sentence claiming the clean reading.
|
||
The scan now counts the screen summary line as a **positive control**: if the
|
||
script did not run, `summary-lines=0` says so, and the zero cannot be read as a
|
||
pass. That is the third time this session that a well-formed answer to a
|
||
different question nearly became a finding.
|
||
|
||
⚠️ Note the mechanism, because it will recur: matching indented code by substring
|
||
is unanchored, and it silently matched a *shallower* indent than the one in the
|
||
file.
|
||
|
||
## Refutation attempt — "the element declared first paints first"
|
||
|
||
All six of the port's forced-backdrop elements sit at **element index 0**. So on
|
||
those six screens the rule's verdict is indistinguishable from a far simpler
|
||
hypothesis I had not tested: *the first-declared element is painted first.* If
|
||
that held, `forced_backdrop` would be an elaborate way to reproduce the file's own
|
||
ordering, and my six verdicts would be no evidence for it at all.
|
||
|
||
**It is refuted, on 8 of 16 screens.** Index 0 is *not* painted first on
|
||
`build_00`/`build_01` (position 2), `extras` (7), `title` (13), `title_jp` (18)
|
||
and — decisively — `main_menu`, where index 0 is **`pteff00`, painted last**,
|
||
position 15 of 16. `pteff00` is the Decoder's own *measured* control: the game
|
||
puts the first-declared element on top of that screen.
|
||
|
||
So declaration order is not paint order, the six coincidences are coincidences,
|
||
and the rule is not redundant.
|
||
|
||
⚠️ What survives as a real limitation: **those six screens, taken alone, cannot
|
||
distinguish the two hypotheses.** The evidence separating them comes from
|
||
elsewhere in the archive. Worth stating because it is the exact weakness in the
|
||
Decoder's `pfbase.tbm` upgrade — *"element 0 of the save/load frame, and the
|
||
measured order starts [0, 1, 2, …]"*. An order that equals the trivial order is
|
||
weak evidence for **any** rule, since every rule preserving declaration order
|
||
agrees with it. ✅ Their claim survives, but on evidence they did not cite: it is
|
||
`main_menu`'s `pteff00` that rules out the trivial reading, not the save/load
|
||
frame itself.
|
||
|
||
## The menu bed plays under the cutscene, nobody decided that, and it stays
|
||
|
||
`MenuAudio.stop_bed()` exists and is **called from nowhere**. So the music that
|
||
starts when the main menu goes live runs through the cutscene and on past it —
|
||
and since `authored/audio.json` sets `loop: "restart"`, it then loops. Both
|
||
follow from the source and the authored data alone; no measurement is needed to
|
||
establish them.
|
||
|
||
The port therefore emits **two unrelated music tracks at once** during `S00A`:
|
||
the movie carries its own music and effects, and the menu bed is underneath it.
|
||
|
||
### It is not being fixed, and that is the decision
|
||
|
||
`PORT-MISSION`'s rule is to leave an unmeasured detail **plainly wrong rather
|
||
than plausibly invented**, and this is the textbook case for it. Music over a
|
||
cutscene is wrong in a way any listener catches in one second. Ducking or
|
||
stopping the bed would sound entirely right — and would be a guess about a game
|
||
nobody has watched. **The audible version gets fixed; the plausible version ships
|
||
forever.**
|
||
|
||
So `_play_video` now *announces* it, the way `skipped_chain` already announces
|
||
the two screens NEW GAME jumps over:
|
||
|
||
```
|
||
-> video S00A at 1.13 s (…)
|
||
🔴 the menu bed is STILL PLAYING under this movie -- unmeasured,
|
||
left audible on purpose (BLOCKED.md: does menu music duck?)
|
||
```
|
||
|
||
It fires on the menu path and correctly stays silent on the boot path, where no
|
||
bed has started. `stop_bed` is **kept**, not deleted: the day a capture says
|
||
whether the game's menu music ducks under a movie, it is the one line to change.
|
||
|
||
⚠️ This is the mirror of `ScreenView.skipped` from the previous iteration — a
|
||
fact recorded and never surfaced, against a capability provided and never used.
|
||
Both were invisible for the same reason: **nothing fails when they are missed.**
|
||
|
||
### 🔴 And my correlator is not trustworthy on music under music
|
||
|
||
Chasing this I ran the envelope correlator over the P7 recording repeatedly and
|
||
got answers that moved with the window and the template: the bed at r=0.42 with
|
||
one template and no peak at all above 0.4 with another; a post-movie window
|
||
search whose range **excluded the correct answer** and duly reported the bed's
|
||
own loop as unidentifiable. I was drafting *"46 s of unexplained audio after the
|
||
movie"* when the explanation was the authored `loop: "restart"` sitting in a file
|
||
I had not re-read.
|
||
|
||
The instrument is sound where it was validated — discrete SFX against a bed, with
|
||
margins of +0.5 over a negative control. It is **not** sound for music under
|
||
music at comparable level, where every candidate scores 0.15–0.42 and nothing
|
||
separates. ⚠️ A margin is only meaningful against a control **at the same SNR**,
|
||
and I did not have one here. That is the fourth near-miss of this kind, and the
|
||
first where I would have invented an *anomaly* rather than a defect.
|
||
|
||
Nothing in this section rests on that correlator. The finding is `stop_bed` having
|
||
no caller, which is a fact about the source.
|
||
|
||
## `wait:<seconds>`, and the bed's loop seam is 3.4 seconds of silence
|
||
|
||
The port could not be asked to **run for a stated duration**. A bare `wait`
|
||
script step is `pass` — it returns as soon as the screen settles — so nothing
|
||
happening after the settle point was observable from a harness at all. The music
|
||
bed made that concrete: an 87.7 s track whose restart nobody had watched, on a
|
||
harness whose longest menu run was under seven seconds.
|
||
|
||
`--script=wait:105` fixes that, and the first thing it found was the answer.
|
||
|
||
### The bed loops, exactly where it should
|
||
|
||
Recording the Master bus over 132 s with nothing but the menu playing — no movie,
|
||
no voice, a clean signal — the bed's `t=2…17 s` template matches twice:
|
||
|
||
| | r | pass begins |
|
||
|---|---|---|
|
||
| first pass | **0.947** | 0.0 s |
|
||
| second pass | **0.885** | **87.8 s** |
|
||
|
||
The track is **87.7 s**. So `loop: "restart"` does what `authored/audio.json`
|
||
says: replays from sample 0 at the track's end, no trimming, no loop point.
|
||
✅ First end-to-end observation of P6's looping behaviour.
|
||
|
||
### And the seam is measurably as bad as it was authored to be
|
||
|
||
`loop_why` predicted *"the listener hears the track's own fade-out and the silence
|
||
after it before the music comes back"*. Measured off the bus:
|
||
|
||
| window around the seam | RMS |
|
||
|---|---|
|
||
| −8 … −4 s | 2057 |
|
||
| −4 … −2 s | 714 |
|
||
| −2 … −0.3 s | 431 |
|
||
| +0.3 … +2 s | 2164 |
|
||
|
||
and **36 consecutive 50 ms windows below peak 300, from 84.40 s to 87.80 s** —
|
||
about **3.4 seconds of near-silence** before the music returns. That is long
|
||
enough to read as *the music stopped*, not as a loop.
|
||
|
||
The claim was right and is now a number. ⚠️ It does **not** license trimming to
|
||
the fade: that would still invent a loop point, and an invented one is
|
||
indistinguishable from a decoded one a month later. The measurement is recorded
|
||
to make the cost of the missing loop point concrete, in `authored/audio.json`.
|
||
|
||
### 🔴 My first `wait:` was wrong by 39 %, and the way it was wrong matters
|
||
|
||
It used `create_timer`, which counts down on the frame delta. In an **idle**
|
||
scene this container throttles and the delta it reports is not the time that
|
||
passed: a requested 30 s took **41.7 s** of wall clock while the port reported
|
||
30. Measured against `date` either side of the process, with a no-wait control
|
||
to subtract 1.21 s of startup.
|
||
|
||
Now polled on `Time.get_ticks_msec()`: 30 s requested, **31.38 s** wall, +4.6 %.
|
||
|
||
⚠️ **This is idle-specific and is not a general clock fault**, which I checked
|
||
before writing any of it down. Over a whole boot, where things are animating, the
|
||
port's clock tracks wall clock to **within 4 %** — 10.43 s wall against 10.82 s
|
||
reported. So the port's *animation* timing is sound and the earlier splash-dwell
|
||
agreement (4.400 s and 3.650 s against declared 255 and 210 units) **stands**; I
|
||
had briefly believed it did not.
|
||
|
||
What is genuinely unsound is `_elapsed` **while idle**: it reported 23.21 s across
|
||
30 real seconds of waiting. Every timing the port prints during animation is
|
||
fine; a timing that spans a wait is not.
|
||
|
||
📌 The reason to care: the only reason to hold a screen is to observe something on
|
||
a **real** clock — an audio loop, a timeout, an idle return. A timer that
|
||
silently ran 39 % long would have put every such observation at the wrong instant,
|
||
and the bed-loop result above would have been the first casualty. It survived only
|
||
because the seam was read off the **recording's** clock, which the bed's own known
|
||
length calibrates.
|
||
|
||
## Two harness bugs, and the defect the second one was hiding
|
||
|
||
### 1. `--capture` with `--script` photographed the frame *before* the script
|
||
|
||
`--capture` fired in `_ready` and quit. With `--script` that is **before the
|
||
first press**: at t=0.133 s, with 10 of 16 elements still transparent. Two runs
|
||
differing by two `down` presses came out **bit-identical**, and I read that as
|
||
*"runtime focus never changes"* — a confident wrong finding that `--shots`
|
||
contradicted within a minute.
|
||
|
||
Fixed: with a script, the capture defers to the end of the run, through the same
|
||
`_capture_to` member the boot path already used. Verified — the two runs now
|
||
differ at max 235, and the capture lands at t=82 units instead of t=8.
|
||
|
||
### 2. `--boot --capture=` wrote no file at all — ✅ FIXED, see below
|
||
|
||
`_finish_boot()` is reachable only from the overlay-quit branch, but the boot
|
||
quit first: line 412 exits when `_film == "" and _overlay_spec.is_empty()`, and
|
||
`_overlay_spec` is **cleared when the overlay is raised**. So a plain `--boot`
|
||
ended at 10.99 s, 1.2 s before its own scheduled 12.21 s, and the capture never
|
||
happened.
|
||
|
||
Confirmed pre-existing by stashing my changes and re-running. Fixed by also
|
||
requiring `_overlay_quit_at < 0.0`. The boot now runs to 12.19 s, prints *"boot
|
||
ends on title + press_start"*, and writes the file.
|
||
|
||
⚠️ The flag has a doc comment explaining that it exists so the boot has an
|
||
artifact of its own instead of a 600-PNG filmstrip. **It has been producing
|
||
nothing.** A flag that silently writes no file is the same failure as
|
||
`ScreenView.skipped` and `stop_bed`: provided, plausible, and never exercised.
|
||
|
||
### 3. 🔴 And the artifact it now produces shows the plate is missing
|
||
|
||
The boot's end frame is **bit-identical** to `--screen=title` at the same instant
|
||
— max difference 0. The `PRESS Ⓐ` plate is not in the port's end state.
|
||
|
||
`ptbtn00`'s own fade explains it exactly:
|
||
|
||
| t | 0 | 214 | **236** | **238** | 244 |
|
||
|---|---|---|---|---|---|
|
||
| alpha | 0 | 0 | **255** | **255** | 0 |
|
||
|
||
The plate is visible for **8 units — 0.133 s** — and the boot captures at
|
||
**t=246.54**, two and a half units after it has gone.
|
||
|
||
That is not an accident of frame timing; the code chose it. `_overlay_quit_at`
|
||
takes `max(view.settle_time(), overlay.settle_time())`, and its comment says why:
|
||
*"the plate arrives at t=238 and build 4 is still fading up from black until
|
||
t=261 … quitting when the plate lands photographs a title that has not finished
|
||
presenting."* Both halves are true, and together they mean **the two states
|
||
cannot both be in one frame.** The port picked the title, and the consequence —
|
||
that the plate is in no artifact at all — was never written down.
|
||
|
||
⚠️ I am **not** moving the trigger. The earlier reasoning is sound and the
|
||
measurement it protects is real; picking the other instant would trade a missing
|
||
plate for a visibly dark title, which is the swap that was already made once and
|
||
regretted. What settles it is what the *game* does with the plate after t=244 —
|
||
filed.
|
||
|
||
📌 Worth naming: defect 3 was **invisible while defect 2 existed**. A capture flag
|
||
that writes nothing cannot show you a missing element. The broken tool was hiding
|
||
the thing the tool was built to find, and neither was noticed because the absence
|
||
of a file looks exactly like not having run the command.
|
||
|
||
### What was *not* wrong, and how I nearly recorded that it was
|
||
|
||
Runtime focus works. Per-step `--shots` across `down,down` differ at max 232–233,
|
||
with the differing boxes tracking down the button column. My contrary reading came
|
||
from analysing 410 `f_NNN` files after asking for `--shots=…/s` — **the filenames
|
||
did not match the flag I passed, and that was visible in my first `ls`.** A
|
||
verbatim re-run produces six correctly-named per-step shots. I do not know where
|
||
those 410 files came from, which is itself the point: I drew a conclusion from a
|
||
file set whose provenance I never checked.
|
||
|
||
## The `PRESS Ⓐ` plate: four bugs in a row, and a number I have been misquoting
|
||
|
||
Last iteration I filed that the plate was visible for 8 units and simply missed by
|
||
the boot's capture instant. That was wrong in the direction that matters: **the
|
||
plate could not be drawn at any instant at all**, and three separate faults had to
|
||
be removed before it appeared. The fourth is a correction to figures I have quoted
|
||
to the Decoder repeatedly.
|
||
|
||
### 1. `--time=` was silently ignored on half the screens
|
||
|
||
`pose_at` did `if holding: t = settle_instant`. The requested instant was
|
||
**discarded** on every screen with a settle window ≥ 30 units — `title`,
|
||
`press_start`. The flag parsed, the log printed the time asked for, and the pose
|
||
came from somewhere else entirely.
|
||
|
||
`ScreenView.frozen` now marks an explicitly pinned instant and skips both clamps.
|
||
|
||
### 2. The settle window picked an interval where nothing is visible
|
||
|
||
`press_start`'s keyframes are 0, 214, 236, 238, 244. The widest keyframe-free gap
|
||
is **0…214** — the dead stretch *before* the plate exists, where `ptbtn00` is
|
||
alpha 0 throughout — so its settle instant was **t=107**, and every question about
|
||
that screen was answered there.
|
||
|
||
🔴 **A gap in which nothing is visible is not a settled state.** The exporter now
|
||
rejects those intervals. `press_start` becomes [214, 236] — 22 units, under the
|
||
runtime's 30-unit bar — so it falls back to each element's own hold, which is the
|
||
plate, opaque, as the disc declares it.
|
||
|
||
⚠️ It disturbs no window the settle instant was measured on: `title` keeps
|
||
[160, 236], the interval the Decoder's draw stream independently found the game's
|
||
clock freezing in.
|
||
|
||
### 3. An authored entry of mine was suppressing the decode
|
||
|
||
Even then the plate stayed dark, because `authored/timing.json`'s
|
||
`looping_focus_records` entry for `press_start/ptbtn00` made `_draw` take the
|
||
focus path — which draws the focus record **instead of** the base sprite:
|
||
|
||
| | with the entry | without |
|
||
|---|---|---|
|
||
| `press_start` t=236 | max **0** | max **252.5** |
|
||
| t=240 | max 0 | max 252.5 |
|
||
| t=250 | max 15.3 | max 252.5 |
|
||
|
||
I authored that entry to give the plate a glow. It substituted a dim glow at the
|
||
wrong phase for the element's own bright sprite, on the screen whose entire
|
||
content is that sprite. **Deleted** — an authored guess that overrides decoded
|
||
data with a worse answer is removed, not tuned. The glow is not claimed either
|
||
way; drawing both would be a rendering rule nobody has measured.
|
||
|
||
✅ The boot's end artifact now contains the plate: mean **95.7** in its region
|
||
against **33.6** for the title art alone, and the overlay reports `drew 1:
|
||
ptbtn00` where it reported `drew 0`.
|
||
|
||
### 4. 🔴 `verify-capture` has been measuring a different pose than it reported
|
||
|
||
It passed `--time=5.9617` for the title — t=357.7 units, the Decoder's refined
|
||
sweep fit — and **that value was never applied**, because of fault 1. Every title
|
||
figure this tool has printed, including the **0.26 %** I have quoted to the
|
||
Decoder more than once, was measured at the **settle instant, t=198**, under a
|
||
note claiming t=357.7.
|
||
|
||
Honouring the flag made it visible: t=357.7 is past the title's own group, which
|
||
ends at t=269, so the whole screen posed at its faded-out final keyframes and the
|
||
disagreement went to **30.97 %**. The instant was only ever meant for the `ptloop`
|
||
leaf, which runs to t=600 and is looped separately by `loop_leaf`. Applying it to
|
||
the whole screen was always wrong and was harmless only while it was ignored.
|
||
|
||
The splashes had the same shape: `--time=99` was an idiom for "settled" that
|
||
worked only because it was discarded. Both rows now pose by omission, and the
|
||
tool's note says what it does.
|
||
|
||
| | before | now |
|
||
|---|---|---|
|
||
| `title` | 0.26 % *(labelled t=357.7, actually t=198)* | **0.21 %** at t=198, labelled t=198 |
|
||
| `publisher_logo` | 0.01 % | 0.01 % |
|
||
| `developer_logos` | 0.01 % | 0.01 % |
|
||
| `main_menu` | 0.07 % | 0.08 % |
|
||
| `extras` | 0.19 % | 0.19 % |
|
||
|
||
The agreements were real; the **stated pose was not**. Corrected with the
|
||
Decoder, since they have those numbers.
|
||
|
||
### The flag audit that started it
|
||
|
||
All 16 flags `boot.gd` parses were exercised for an observable effect after last
|
||
iteration's two silent ones. `--pose=rest` (max 111 against the timeline),
|
||
`--play` (enters the menu), `--no-hold` (max 255 on two screens) all pass. `--time`
|
||
was the one that did not, and it took a screen whose content is a single late
|
||
spike to make the failure visible.
|
||
|
||
## The title's residual is the sweep phase, and the sweeps fit at ~400 units, not 357.7
|
||
|
||
Last iteration I found `verify-capture` was passing the Decoder's refined sweep
|
||
fit as `--time=5.9617` and having it silently discarded, so the value had **never
|
||
been tested**. Asking for it also destroyed the frame — t=357.7 is past the
|
||
title's own group end at t=269 — which is why nobody noticed.
|
||
|
||
`--leaf-time=<seconds>` separates the two clocks: the screen sits at its settled
|
||
pose, the `ptloop` leaf is placed at whatever phase is under test. That makes the
|
||
fit testable for the first time.
|
||
|
||
### Controls first
|
||
|
||
* The renderer is **deterministic** — three runs at one leaf phase are
|
||
bit-identical, max difference 0. So variation across phases is signal, not noise.
|
||
* The sweeps are **detectable** — two phases differ over **0.3953 %** of the frame
|
||
at a 10 % threshold. A comparison at this scale can see them.
|
||
|
||
### The fit
|
||
|
||
Sweeping the leaf across its full 600-unit span against
|
||
`live-title-build4-no-plate.png`, structural disagreement at a 25 % threshold:
|
||
|
||
| leaf phase | differing |
|
||
|---|---|
|
||
| 240 units | 0.1410 % |
|
||
| 320 units | 0.3395 % |
|
||
| **357.7 — the Decoder's fit** | **0.2532 %** |
|
||
| 390 | 0.0129 % |
|
||
| **395–402** | **0.0124 %** |
|
||
| 405 | 0.0127 % |
|
||
| 440 | 0.2033 % |
|
||
|
||
A sharp basin at **390–415 units**, an order of magnitude below everything
|
||
around it, and **20× better than t=357.7**.
|
||
|
||
### What that means, and what it does not
|
||
|
||
✅ **The title's 0.21 % residual is the sweep phase, not structure.** At the
|
||
fitted phase the disagreement falls to **0.0124 %** — the same order as the
|
||
splashes' 0.01 %. The port's title rendering is structurally right; the sweeps
|
||
were simply somewhere else in their loop.
|
||
|
||
🔴 **The port does not adopt 400 units, and `verify-capture` is not re-posed to
|
||
it.** That would be tuning until they match, which this repository's own tooling
|
||
header warns against. The port loops the leaf freely — there is no phase
|
||
parameter to set — and 400 units is a property of *that capture's instant*, not
|
||
of the game.
|
||
|
||
⚠️ Reach: this assumes the port's leaf geometry and sprite are otherwise correct.
|
||
A systematic error in how the sweeps are drawn could be absorbed by shifting the
|
||
phase, and one capture cannot separate those. What makes the result worth having
|
||
anyway is the **sharpness** — a 20× drop over 40 units is not something a
|
||
geometry error would produce at a wrong phase.
|
||
|
||
⚠️ And it does not tell the Decoder their 357.7 is wrong *as a measurement of
|
||
whatever they measured it on*. It says the phase that matches this capture is
|
||
~400. If those are the same quantity, one of the two is off by ~42 units; if they
|
||
are not, this is a second quantity nobody had.
|
||
|
||
## A second capture closes the sweep-geometry question, and the plate matches at 0.00093 %
|
||
|
||
Last iteration's leaf-phase fit came with a caveat I could not close: *a
|
||
systematic error in how the port draws the sweeps could be absorbed by shifting
|
||
the phase, and one capture cannot separate those.* A second capture can, and
|
||
`live-title-press-a.png` — the title **with** the plate — was sitting in the
|
||
corpus unused.
|
||
|
||
### The second capture fits at a different phase, and better
|
||
|
||
| capture | pose | differing |
|
||
|---|---|---|
|
||
| `live-title-build4-no-plate.png` | settled, leaf at ~400 units | **0.0124 %** |
|
||
| `live-title-press-a.png` | t=237, everything | **0.00093 %** |
|
||
|
||
**Two independent captures, two different sweep phases, both fitting to 0.01 % or
|
||
better.** A geometry error in how the sweeps are drawn would leave a floor in
|
||
*both*, and at a phase-independent level. Neither has one. ✅ The caveat is closed
|
||
and the port's sweep rendering is not systematically wrong.
|
||
|
||
The two phases are also consistent with each other rather than merely different:
|
||
the plate is opaque only at t=236–238, so a frame containing it is early
|
||
(t≈237) and a frame without it is either earlier or later. The no-plate capture
|
||
fits at leaf ~400 — 6.7 s in, well past the plate's window — and its filename
|
||
says it is build 4 alone. Both readings agree on where each frame sits.
|
||
|
||
⚠️ I nearly drew a further inference — that the no-plate capture *dates* the
|
||
plate's disappearance and therefore answers the BLOCKED question about whether
|
||
the plate stays up. It does not: the filename says `build4-no-plate`, so the
|
||
capture was taken **of build 4 alone, deliberately without the overlay**. It
|
||
carries no information about how long a plate lasts. That row stays open.
|
||
|
||
### Capture 1 is not a whole-screen instant, which corroborates `loop_leaf`
|
||
|
||
Sweeping the *whole screen's* time against the no-plate capture, the best is
|
||
**0.1483 % at t=230**, degrading sharply past 240 as the group fades out — an
|
||
order of magnitude worse than the leaf-only fit's 0.0124 % at phase ~400, which
|
||
the main timeline cannot reach without fading everything.
|
||
|
||
So that capture is **not** "the screen at instant t". It is the screen **settled**
|
||
with the sweeps **still looping** — which is exactly what `authored/rendering.json`'s
|
||
`loop_leaf_on_screens: ["title"]` models. That decision was authored from the
|
||
leaf's zero slack; this is the first independent evidence for it.
|
||
|
||
### 🔴 The 1 % floor was the plate not being drawn at all
|
||
|
||
Before any of that, every sweep phase against capture 2 gave a flat ~1.0 %, with
|
||
the residual a row of glyph-sized blobs on the plate's own position.
|
||
|
||
`--screen=X --overlay=Y` pushed the **raw elapsed clock** into the overlay — 9
|
||
units at the moment `--capture` fires. `press_start` is transparent until t=214.
|
||
So the one flag whose entire purpose is *put the plate on the title* drew nothing
|
||
and reported `drew 0`, and the frame read as a title with no plate.
|
||
|
||
A static overlay now poses at **its own arrival**. The `--boot` path is untouched:
|
||
there the shared clock is the finding — the 120 units between build 4's last ramp
|
||
and the plate's `a=255` is a fixed interval on one timeline.
|
||
|
||
⚠️ My first patch for this was wrong and I nearly committed the comment for it:
|
||
I wrote that *"nothing outside a boot sequence drives the overlay's clock"*. It is
|
||
driven — from `view.time_units`, every frame. The symptom was identical either
|
||
way, and only re-reading the log after the fix failed showed the cause was the
|
||
opposite of what I had written down.
|
||
|
||
### The new row
|
||
|
||
`title_plate` joins `verify-capture` at **0.00%** — two orders below every other
|
||
row, which makes it the most sensitive regression detector in the harness.
|
||
|
||
⚠️ Its instant is **fitted, not measured**. t=237 is where this capture's content
|
||
places it, found by sweeping. Choosing which frame to compare against is what
|
||
every row here does, but the 0.00093 % is a floor for *that pose*, not a general
|
||
statement of accuracy.
|
||
|
||
## `--focus=` did nothing on the menu path, and the corpus had an untested focus capture
|
||
|
||
Two unused live captures were sitting in `docs/re/captures/title-builds/`.
|
||
`live-main-menu-options-focused.png` is the menu with **OPTIONS** focused — the
|
||
only capture of a *known* focus state — and it was untestable, because
|
||
`--focus=` **silently did nothing on the `--menu` path**.
|
||
|
||
The flag parsed, was stored in `_force_focus`, and was applied to
|
||
`view.focused_id` at startup — and then `_menu_enter` overwrote it with the
|
||
authored initial focus on every entry. Every run logged `focus ptbtn01` whatever
|
||
was asked for, and all five buttons produced the same frame. It is now pushed
|
||
into the **menu model**, not just the view, so navigation continues from where it
|
||
was forced rather than jumping back on the first press.
|
||
|
||
### The port's focus rendering is right, measured against the oracle
|
||
|
||
Rendering each of the five buttons focused, against each capture:
|
||
|
||
| focused | vs `live-main-menu-options-focused` | vs `live-main-menu` |
|
||
|---|---|---|
|
||
| `ptbtn01` NEW GAME | 0.7352 % | **0.0705 %** |
|
||
| `ptbtn02` LOAD GAME | 0.8204 % | 0.8378 % |
|
||
| `ptbtn03` TUTORIAL | 0.7220 % | 0.7365 % |
|
||
| **`ptbtn04` OPTIONS** | **0.1355 %** | 0.7449 % |
|
||
| `ptbtn05` EXTRAS | 0.7029 % | 0.7236 % |
|
||
|
||
Each capture picks out exactly one button, by **5×** and **10×**. This is the
|
||
first time the port's focus rendering has been checked against the game at all —
|
||
the harness's own `main_menu` row uses an *authored* focus, so it could never
|
||
have caught a focus error.
|
||
|
||
### What that settles, and what it does not
|
||
|
||
✅ The port draws focus on the right button, distinguishably.
|
||
✅ `live-main-menu.png` shows **NEW GAME** focused, so the authored
|
||
`initial_focus: ptbtn01` matches the one frame it can be checked against.
|
||
|
||
⚠️ **It does not overturn HANDOFF Q5**, which measured initial focus as *unstable
|
||
boot to boot* across four runs. One capture showing `ptbtn01` is consistent with
|
||
instability, not evidence against it. The value stays **authored**, with the
|
||
agreement recorded beside it. Reading this as "initial focus is settled" would be
|
||
exactly the over-generalisation from a single observation that this corpus keeps
|
||
having to withdraw.
|
||
|
||
`main_menu_options` joins `verify-capture` at 0.13 %.
|
||
|
||
⚠️ Still unused: `live-attract-title-press-a-band.png`, a 1279×**120** strip
|
||
rather than a full frame. It needs a banded comparison the harness does not do,
|
||
so it is named here rather than quietly left out.
|
||
|
||
## The last unused capture, placed — and its residual is the oracle's, not the port's
|
||
|
||
`live-attract-title-press-a-band.png` was the one live capture nothing consumed.
|
||
Following last iteration's rule — *an unused capture is a signal about the
|
||
harness* — it turned out the same way as the previous two: nothing about the
|
||
capture was unusable, the harness simply could not compare a **band**. It is
|
||
1279×**120**, not a full frame.
|
||
|
||
### Placing it
|
||
|
||
Sliding it down the render, structural difference against the port:
|
||
|
||
| y | differing |
|
||
|---|---|
|
||
| 300 | 51.99 % |
|
||
| 500 | 19.61 % |
|
||
| 515 | 9.14 % |
|
||
| **520** | **0.354 %** |
|
||
| 525 | 8.87 % |
|
||
| 555 | 24.02 % |
|
||
|
||
**y = 520**, a 25× drop over five pixels. Measured, not guessed. Sweeping the
|
||
instant at that offset puts it at **t = 236–238** — the plate's own opaque
|
||
window, the same instant the full-frame `title_plate` row fits.
|
||
|
||
### 🔴 The 0.354 % is not the port's error
|
||
|
||
Three comparisons separate it:
|
||
|
||
| | differing |
|
||
|---|---|
|
||
| port's band vs `live-title-press-a`'s same band | **0.000 %** |
|
||
| `live-title-press-a`'s band vs the attract band *(oracle vs oracle)* | **0.301 %** |
|
||
| port's band vs the attract band | 0.354 % |
|
||
|
||
The port reproduces one capture's band **exactly**. The two captures differ from
|
||
*each other* by 0.301 %, which is nearly the whole residual.
|
||
|
||
And that oracle-to-oracle difference is two thin horizontal strips — **248×5 px**
|
||
and **206×1 px** — which is the shape of a sub-pixel edge difference or capture
|
||
noise, not of a state difference.
|
||
|
||
⚠️ I had started writing that the *attract-returned* title differs from the boot
|
||
title, which would have been a finding about the game inferred from 0.3 % of a
|
||
band. It is two hairlines. The connected-component breakdown is what stopped it,
|
||
and I would not have run it if the number alone had looked small enough to
|
||
dismiss or large enough to report — it was in the range where you have to look.
|
||
|
||
So `title_band` joins the harness at 0.35 %, and its job is to **stay near the
|
||
oracle-to-oracle gap**, not to reach zero. A row whose target is not zero has to
|
||
say so, or the next person tunes toward it.
|
||
|
||
✅ **All eight live captures in the corpus are now used.** Three were sitting idle
|
||
and all three were blocked by the harness, not by the capture: an overlay posed at
|
||
t=9 that drew nothing, a `--focus=` overwritten on every menu entry, and a banded
|
||
comparison that did not exist.
|
||
|
||
## `MODDING.md` had five rules and no check. Now it has one, and all five pass
|
||
|
||
`MODDING.md` opens by calling modding *a constraint on the exporter **today**,
|
||
not a later feature*. Nothing verified it. That is the shape this port keeps
|
||
finding — a rule stated, believed and unexercised: the black hold implemented and
|
||
never called, `ScreenView.skipped` written and never read, `stop_bed` provided and
|
||
never used, `--focus` parsed and overwritten on every menu entry.
|
||
|
||
`tools/port/check-modding` covers all five. Every one passes today, so it is a
|
||
**guard, not a fix**: its value is that the next thing to break one says so.
|
||
|
||
| rule | check | result |
|
||
|---|---|---|
|
||
| 1 — one asset, one file | every referenced sprite present, none orphaned, no split names | **174 / 174**, exact |
|
||
| 2 — recognisable names | no hex or hash-shaped filenames | none |
|
||
| 3 — modern formats | extensions confined to json/png/ogg/ogv (+ sidecars) | clean |
|
||
| 4 — base and overrides | `data/mods` gitignored *and* read by the exporter | both |
|
||
| 5 — provenance | every generated JSON carries a `source` | 17 / 17 |
|
||
|
||
### It is proved to fail
|
||
|
||
A check that has never failed has not been shown to work — the lesson from
|
||
`check-capture`, which once passed a file with 36 % holes punched through it. Three
|
||
controls, each failing correctly with a non-zero exit:
|
||
|
||
* a `.cmd` sidecar with its header stripped → rule 3;
|
||
* a `bogus.bmp` in the sprite tree → rule 3;
|
||
* one orphaned PNG → rule 1, *"174 referenced, 175 present"*.
|
||
|
||
### The one thing it found: an unlabelled generated file in the asset tree
|
||
|
||
The two `.cmd` encode-cache sidecars sat beside the `.ogv`s in the modder-facing
|
||
tree with no line saying what they were — a bare ffmpeg command next to a video
|
||
reads as something to edit or delete. They now carry a header stating that they
|
||
are generated, are not assets, and that the way to change a video is an override
|
||
under `data/mods/`.
|
||
|
||
Two details worth keeping:
|
||
|
||
* the header is **excluded from the cache key**, so rewording it does not
|
||
re-encode four minutes of video. A cache that punishes documentation gets
|
||
documented once and never again.
|
||
* the sidecar is now refreshed whenever its **text** differs, not only when a
|
||
re-encode happens. It used to be written inside the `!fresh` branch — which
|
||
meant a header change could never reach an existing export, because nothing
|
||
that reads the header triggers the write that updates it. The explanation would
|
||
have been correct in the source and absent on disc. Confirmed: two consecutive
|
||
exports, 20 s and 19 s, header present, no re-encode.
|
||
|
||
### And a question I asked the Decoder that I could partly answer myself
|
||
|
||
Last iteration I asked whether the 0.301 % between two of their captures implies a
|
||
**capture-path floor on every comparison in the corpus**. It does not, and I had
|
||
the evidence already: the port matches `live-title-press-a.png` at **0.00093 %**
|
||
full-frame and **0.000 %** across the band. A general floor could not coexist with
|
||
either number. So the 0.301 % is specific to the attract band capture, and my
|
||
0.01–0.2 % rows are not sitting on a hidden floor. ⚠️ What that does *not* settle
|
||
is why those two frames differ — still theirs, and still worth an answer.
|
||
|
||
## Five authored values had no reader — including the one I asked for measurements into
|
||
|
||
Applying the prior from the last six findings — *a rule or capability nothing
|
||
exercises turns out broken or inert when someone looks* — to `authored/` itself.
|
||
Grepping every authored key for a reader in the exporter or the runtime:
|
||
|
||
| key | file | status |
|
||
|---|---|---|
|
||
| `dwell` | `flow.json` | 🔴 **no reader** — now wired |
|
||
| `ramp` | `timing.json` | no reader — now asserted |
|
||
| `left_right` | `flow.json` | no reader — now asserted |
|
||
| `input_during_transition` | `flow.json` | no reader — now asserted |
|
||
| `stems` | `audio.json` | no reader (`stems_why` is carried; the sum is hardcoded) |
|
||
|
||
Everything else — `se`, `bgm`, `voice`, `boot`, `screens`, `navigation.wrap`,
|
||
`draw_leaf_for`, `loop_leaf_on_screens`, `keyframe_units_per_second`,
|
||
`black_hold_units`, `archives`, `also_export`, `presentation`, `loop_mode`,
|
||
`initial_focus`, `skippable`, `then_video`, `after_video` — is read.
|
||
|
||
### 🔴 `dwell` is the one that mattered
|
||
|
||
Its own text says *"when a capture times the real boot, the extra hold per screen
|
||
goes here."* **A number placed there did nothing.** Two iterations ago I asked
|
||
the Decoder for measurements destined for exactly that slot; had they arrived,
|
||
they would have been filed into a value with no reader and the boot would have
|
||
been unchanged, silently — and I would have reported the boot as matching.
|
||
|
||
It is wired now, and **stays empty**. Nothing is authored into it: the splash
|
||
dwells are declared on the disc and measured to agree. Wiring the slot so that a
|
||
future number has an effect is the opposite of adopting one.
|
||
|
||
⚠️ **I wired it to the wrong branch first, and it did nothing — silently.** Holding
|
||
longer after settle changes nothing, because the screen still leaves when
|
||
`exit_time() + black_hold` arrives and the extra hold is absorbed. A dwell has to
|
||
delay the **departure**. I found it only because I tested the control:
|
||
+120 units moved the transition 4.46 s → **6.43 s**, +1.97 s, with the video
|
||
following by the same amount. Reproducing the exact defect I was removing, inside
|
||
the fix for it, is the strongest argument I have for testing that a wire carries
|
||
current rather than that it exists.
|
||
|
||
### The other three are asserted, not implemented
|
||
|
||
`ramp`, `left_right` and `input_during_transition` describe behaviour the port
|
||
**hardcodes**. That is fine for a record and dangerous for a switch, and they are
|
||
written like switches — setting `left_right` to `"move"` would change nothing and
|
||
warn nobody.
|
||
|
||
Rather than invent the missing implementations, `_check_authored_invariants`
|
||
**asserts the value the port was built against**, naming the file. Changing one
|
||
now produces an error instead of silence.
|
||
|
||
That is precisely the distinction `left_right`'s own `why` claims to be making —
|
||
*"written out rather than left unhandled so that 'the game ignores it' and 'we
|
||
never wired it' are different lines of code"* — and which was not in fact being
|
||
made, because nothing read the value that was supposed to make it.
|
||
|
||
✅ The validator is **called**, not merely defined. A validator nobody invokes is
|
||
the same defect it exists to catch, and this file now documents six other
|
||
instances of exactly that.
|
||
|
||
Verified: clean boot with no invariant errors and unchanged timings; setting
|
||
`left_right: "move"` produces the error; all five MODDING rules still pass; the
|
||
oracle rows are unmoved.
|
||
|
||
## `FORMAT.md` declared the port's own export invalid, and a failed export is not atomic
|
||
|
||
Continuing the audit that has now found seven unexercised rules: `FORMAT.md` is
|
||
the **open format spec** — written for a stranger reading the tree with no access
|
||
to the disc or this exporter. So the question is whether what it promises is what
|
||
`sylpheed-export check` enforces.
|
||
|
||
Five documented requirements, each broken in a copy of the tree:
|
||
|
||
| broken | caught |
|
||
|---|---|
|
||
| `unresolved` removed from a screen | ✅ |
|
||
| `peak_dbfs` removed | ✅ |
|
||
| `peak_dbfs` = −120 (silence) | ✅ |
|
||
| `duration_s` removed | ✅ |
|
||
| `peak_dbfs` = 0.0 on an `se` | **passes** |
|
||
|
||
### The last row is the doc's error, not the code's
|
||
|
||
`FORMAT.md` said flatly that check *"refuses a tree whose peak is ≤ −90 dBFS or
|
||
**≥ 0 dBFS**"*. The implementation is kind-dependent and deliberate: a `bgm` is a
|
||
sum **we** produced, so a peak at or above full scale is our arithmetic and is
|
||
refused outright; an `se` or `voice` is a single wave off the disc, mastered near
|
||
full scale, whose lossy decode overshoots by a fraction of a dB, and those are
|
||
allowed to +1.0.
|
||
|
||
🔴 **And the doc was wrong about the port's own export.** It ships `confirm` at
|
||
**+0.18 dBFS** and the `ADV` voice at **+0.31** — both above 0. A consumer
|
||
implementing a validator from `FORMAT.md` would have rejected a valid tree, and
|
||
the file that exists to let someone check our work without trusting us would have
|
||
been the thing that misled them. Corrected, with the +1.0 marked as the judgement
|
||
it is.
|
||
|
||
✅ Verified both directions: a `bgm` forced to 0.0 is refused with *"a SUM we
|
||
produced clips"*; an `se` at 0.0 passes.
|
||
|
||
### 🔴 A failed export leaves a tree that is not an export tree
|
||
|
||
Found by accident, and worth more than the way it was found. Testing the new
|
||
`stems` assertion, the exporter `bail!`ed part-way — and left `export/` **with no
|
||
`manifest.json` at all**. Every subsequent tool then reported *"has no
|
||
manifest.json — is that an export tree?"*, which reads as a broken harness rather
|
||
than as the aftermath of a deliberate failure.
|
||
|
||
⚠️ It cost me a wrong reading immediately: the first run of the requirement audit
|
||
above reported every case as "no manifest", and I nearly recorded that the
|
||
validator was checking nothing. It was checking a tree that had been half-written.
|
||
|
||
The exporter writes the manifest last, which is the right order — a manifest is a
|
||
claim about a tree, and a manifest for a tree that was never finished would be
|
||
worse. So this is **filed rather than fixed**: the behaviour is defensible and the
|
||
message is not, since "is that an export tree?" describes the symptom and hides
|
||
the cause. What a stranger needs to be told is *the last export failed; re-run it*.
|
||
|
||
## `check-all`, a verdict that ignored its own statistic, and a claim of mine that was wrong
|
||
|
||
Eleven tools under `tools/port/` and **nothing ran them together**, so each had to
|
||
be remembered individually. That is the ninth instance of this port's recurring
|
||
shape — correct, documented, unexercised — one level up: the checks were the thing
|
||
nobody was running.
|
||
|
||
`tools/port/check-all` runs the four that assert (`check`, `check-modding`,
|
||
`check-capture-controls`, `verify-menu-audio`), prints the oracle table, and
|
||
handles `verify-screen` specially. All eleven were exercised first and **none had
|
||
rotted**; `which-focus` independently picks NEW_GAME at a **93.8× margin**, which
|
||
is a second instrument agreeing with the capture fit's 10×.
|
||
|
||
Two things it is careful about:
|
||
|
||
* the six exploratory tools are **not** listed as passes. They produce artifacts
|
||
for a person to look at and have no verdict; counting them would invent six.
|
||
* `verify-capture` is **reported, not asserted** — it always exits 0. Its header
|
||
is right that the numbers are not a target, but *not a target* is not *not a
|
||
regression detector*, and nothing would notice `title_plate` moving off 0.00 %.
|
||
Named as a gap rather than papered over; a real fix needs stored baselines, and
|
||
what a baseline means when the pose is fitted is a decision, not a chore.
|
||
* the `verify-screen` allowance **expires on its own condition**. It is allowed to
|
||
fail only while `formats-pin-2026-08-29d` is not an ancestor of `origin/main`;
|
||
the day it lands, `check-all` fails instead. A suppression with no expiry is
|
||
just a hidden failure.
|
||
|
||
### 🔴 The verdict ignored the statistic added to inform it
|
||
|
||
`verify-screen` computes `over3` — how many pixels exceed the bar — because *"a
|
||
single `max` cannot tell 2 pixels from 25 444"*, its own words. **The verdict was
|
||
then decided on `max` alone.** So `main_menu` (max 4, `over3` **0**) read DIFFERS
|
||
while `extras` (max 3, `over3` 0) read OK: one unit on one pixel separating two
|
||
frames that are equivalent at the bar.
|
||
|
||
⚠️ Not fixed by raising the bar, which this file rightly forbids. The bar is still
|
||
3. A frame with **no** pixel over it now gets its own verdict, `ROUNDING`, instead
|
||
of being lumped in with a real disagreement. Tenth instance: the fix was
|
||
implemented, documented, and never wired to the thing it was for.
|
||
|
||
### 🔴 And "six expected DIFFERS [refuted]" — 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.
|
||
|
||
## A static overlay now advances, and a refutation attempt on the pulse floor
|
||
|
||
### The static overlay was frozen at its arrival, which was my fix overshooting
|
||
|
||
`--screen=X` animates X. `--screen=X --overlay=Y` **froze** Y, because the fix for
|
||
the original defect — the raw elapsed clock pushed in, 9 units at capture, plate
|
||
drawing nothing — replaced a frozen-too-early overlay with a frozen-at-arrival
|
||
one. One command animating one build and freezing the other is an inconsistency,
|
||
and the plate pulse is what made it visible: the plate oscillated on the boot path
|
||
and sat flat here, which reads as a regression and is not one.
|
||
|
||
Now **offset, not pinned**: the overlay begins at its own settle and takes the same
|
||
delta the main view takes. Verified — the static path pulses over the same range as
|
||
the boot path, 95.85 → 115.41 against 95.68 → 115.52. Still frames are unaffected
|
||
(`--time` freezes both) and `title_plate` holds at **0.00 %**.
|
||
|
||
📌 Both halves of this were mine, a week apart: the original bug, then the
|
||
over-correction for it. The over-correction was invisible until a *third* change —
|
||
the pulse — gave it something to be wrong about. A fix that overshoots leaves no
|
||
symptom until something else needs the part it disabled.
|
||
|
||
### 🟡 Refutation attempt — their pulse floor of 159 green pixels
|
||
|
||
Their measurement gives a plate-absent floor of **159** thresholded green pixels,
|
||
a pulse minimum of **714** and a peak of **1520**, the floor measured on
|
||
`live-title-build4-no-plate.png` — a capture I hold. So it is checkable.
|
||
|
||
**It is not reproducible from the published description.** Across the plate region
|
||
(513×48 at +383+551) my counts are 3–5× theirs at every threshold:
|
||
|
||
| green > | no-plate | press-a | ratio |
|
||
|---|---|---|---|
|
||
| 100 | 676 | 9 369 | 1 : 13.9 |
|
||
| 150 | 528 | 5 732 | 1 : 10.9 |
|
||
| 180 | 462 | 4 908 | 1 : 10.6 |
|
||
| 200 | 411 | 4 272 | 1 : 10.4 |
|
||
| **theirs** | **159** | **1 520** | **1 : 9.6** |
|
||
|
||
No threshold produces both numbers, so their region must be a tighter crop than
|
||
mine. Neither the region nor the threshold is stated on the page.
|
||
|
||
✅ **The finding survives in the part that matters.** The *ratio* is stable at
|
||
1 : 10.4–10.9 across a wide threshold band and brackets their 1 : 9.6, so "the
|
||
plate-present frame carries about ten times the green of the plate-absent one" is
|
||
robust to how it is counted. What cannot be checked is the absolute figures — and
|
||
those are what the "never goes off" conclusion is stated in.
|
||
|
||
⚠️ This is the convention we agreed two iterations ago, applied to their own new
|
||
page: **a cross-renderer pixel figure needs its region and its threshold named.**
|
||
I raised it there about *my* numbers drifting between two files; the same rule
|
||
makes a published measurement unverifiable when a reader has the capture and not
|
||
the crop. Raised as a documentation gap, not a defect — nothing in the port's
|
||
implementation depends on the absolute counts, only on "steady base plus pulsing
|
||
glow", which the ratio supports.
|
||
|
||
## Their pulse floor reproduces exactly once the predicate is named — 159, to the pixel
|
||
|
||
I challenged their 159 / 714 / 1520 as unreproducible: my counts over the plate
|
||
region were 3–5× theirs at every threshold. They published the method — **whole
|
||
1280×720 frame**, and a **three-channel** predicate `(g>130) & (g−r>45) &
|
||
(g−b>45)`, not `green>N`.
|
||
|
||
Applied to the capture I hold:
|
||
|
||
| | theirs | mine |
|
||
|---|---|---|
|
||
| floor, `live-title-build4-no-plate.png` | **159** | **159** — exact |
|
||
| pulse minimum | 714 | `live-title-press-a.png` gives **753**, 5.5 % apart |
|
||
| "never goes off" ratio | 714 / 154 = **4.6×** | 753 / 159 = **4.7×** |
|
||
|
||
✅ **The floor reproduces to the pixel**, and the load-bearing claim now has an
|
||
independent witness: a capture from a different session, counted by me, lands at
|
||
753 against their pulse minimum of 714. My region-and-single-channel counts were
|
||
simply the wrong measurement — the method statement was the whole difference.
|
||
|
||
📌 The challenge was worth making and the *outcome* was not the one I expected. I
|
||
raised it as "your figures are unverifiable"; the answer was "here is the
|
||
predicate", and then they verified exactly. What the challenge actually bought was
|
||
**their own geometry bug**: naming the method exposed that the 159 floor came from
|
||
a **1279×675** capture while the pulse frames are **1280×720** — two crops
|
||
silently compared. They have replaced it with a same-run, same-geometry floor of
|
||
**154**.
|
||
|
||
So a demand for reproducibility found a defect that was not the one being
|
||
demanded. ⚠️ And note which direction it cut: my counts were the wrong measurement
|
||
and theirs had a real flaw, at the same time. "One of us must be right" was never
|
||
the shape of it.
|
||
|
||
### Their retraction, and whether the port banked it
|
||
|
||
They retract citing a faulting run's dumped `logged_profile_slot_0_xuid = ""` as
|
||
evidence no profile was signed in: **Xenia dumps the config *file*, before
|
||
command-line overrides** — a run launched with `--mute=true` dumps `mute=false`,
|
||
four for four. So that dump cannot say what any run did.
|
||
|
||
✅ Checked: the port's docs never cite it. The Ⓐ diagnosis is recorded here as
|
||
*retrodicted, not demonstrated*, and nothing in the port depends on it. Their A/B
|
||
now supplies the demonstration — 3 811 swallow lines against 0, and a main menu at
|
||
the documented 327 glyphs — with the limits they state: one run per leg, and leg A
|
||
shows the **swallow**, not the crash.
|
||
|
||
⚠️ Also worth carrying: their first A/B pair was **void**. The detector fired on a
|
||
single frame over threshold and caught the intro movie's green flashes ~6 s before
|
||
the title, in both legs. The presses were real — each skipped the rest of the
|
||
movie, which is Q9's behaviour — but the pair tested nothing. A detector that can
|
||
fire on one frame will fire on the wrong one.
|
||
|
||
## My rendered pulse, counted in their units — and #4 refutes the voice value without fixing it
|
||
|
||
### The pulse lands in the right range, and I am not tuning the depth
|
||
|
||
Their predicate makes my render comparable to their measurement for the first
|
||
time. Counting the boot's held title with `(g>130) & (g−r>45) & (g−b>45)` over the
|
||
whole frame:
|
||
|
||
| | theirs | mine |
|
||
|---|---|---|
|
||
| plate-absent floor | 154 | — |
|
||
| pulse minimum | 714 | **805** |
|
||
| pulse peak | 1 520 | **1 420** |
|
||
|
||
✅ Same range, and both ends far above the floor — so **"never goes off" holds in
|
||
the port's own render**, not just in their capture. The trace oscillates cleanly:
|
||
`1413, 1359, 930, 857, 805, 830, 1182, 1413, …`
|
||
|
||
⚠️ My pulse is **24 % shallower** (615 against 806). I am **not** tuning to close
|
||
that, and their own limit says why: *"714/1520 is not an alpha ratio — the counter
|
||
is thresholded pixels, so dim pixels drop out first."* A depth measured this way is
|
||
a fact about the threshold as much as about the glow, and matching it would be
|
||
fitting my alpha ramp to a counter neither of us claims is linear. The port draws
|
||
the record's declared ramp.
|
||
|
||
### #4 refutes the voice value from the output side
|
||
|
||
They recorded 148 s of the game's own output over the boot intro — ALSA tee,
|
||
`--gpu=null`, **0.15 % silence**, with provenance from the XMA probe rather than a
|
||
screenshot. **Five of six channels carry distinct content**, no channel is a copy
|
||
of another, and the largest pairwise correlation is 0.70 between FL and FR.
|
||
|
||
🔴 So `presentation: "loudest"` — keeping **one** stream — cannot be right. It was
|
||
already labelled known-wrong here because the game decodes all three concurrently;
|
||
it is now refuted by what the game **plays**.
|
||
|
||
✅ **And it stays unfixed, on their instruction.** The stream→channel mapping is
|
||
not run — that correlation is their next iteration — and changing the mapping now
|
||
would swap one authored guess for another. **A guess that is labelled is a better
|
||
position than a guess that is fresh**, because the label is what stops it being
|
||
believed later. `authored/audio.json` records the refutation and keeps the value.
|
||
|
||
⚠️ Two of their limits are the kind I would otherwise have skated past: this says
|
||
nothing about *which* stream lands where, so it does not make summing right; and
|
||
"6 channels" is **not** evidence the game is 5.1 — that count is Xenia's hardcoded
|
||
`kFrameChannelsDefault`. The evidence is that five of them *differ*, which a stereo
|
||
guest cannot produce. The number of channels in a capture is a property of the
|
||
capture.
|
||
|
||
## The voice export now carries every qualifying stream — and a unity sum was refused by our own check
|
||
|
||
#4 is answered and it reframes the question the port had been asking. **`ADV.wmv`
|
||
carries one audio stream and it is WMA Pro 5.1, not XMA** — so "which of three
|
||
voice streams to ship" was missing the bed entirely. The movie's own track is the
|
||
bed; the three streams are *additional*.
|
||
|
||
Solving `capture = 0.600 × movie + residual` per channel, the gain is 0.600
|
||
uniformly (−4.44 dB), and the residual is **three signals at three positions** —
|
||
front pair (r 0.918), rear pair (r 0.929), and a centre whose partner LFE is empty
|
||
to −115 dB. 🔴 The load-bearing number is **LFE reproducing to −115.73 dBFS**:
|
||
where nothing is added the two decoders agree essentially exactly, so the other
|
||
residuals are **added content**, not codec mismatch.
|
||
|
||
`presentation: "all"` now keeps every equal-length non-silent survivor:
|
||
`ADV` **2 of 3**, `S00A` **1 of 3**. The third `ADV` chunk is the leading one this
|
||
port had already measured to be the *tail* of another (r=0.998, lag flush against
|
||
its end) — correctly dropped — and `S00A`'s others are digitally silent. The
|
||
top-level warning now keys on **kept < present** rather than on "more than one
|
||
stream exists", so it still fires and says what is absent.
|
||
|
||
### 🔴 A unity sum was tried and `check` refused it
|
||
|
||
First attempt summed at unity, on the precedent of `stems: "sum"` for a BGM bank.
|
||
`ADV` came out at **+2.62 dBFS**, over the +1.0 bound, and the validator rejected
|
||
the tree.
|
||
|
||
It was right, and the precedent did not transfer. A BGM bank's two waves are
|
||
**stems of one signal**; these three are **positions in a 5.1 field**. A stereo
|
||
downmix weights them 0.4142, 0.2929 and 0.2929 — which **sum to one whatever the
|
||
assignment**. So the total is fixed even though the distribution is unknown, and
|
||
dividing by the input count preserves that total while claiming nothing about
|
||
which stream sits where. `ADV` now lands at **−3.1 dBFS**.
|
||
|
||
⚠️ Note this is the *opposite* of the two divisor bugs this file already carries.
|
||
Those were wrong because an input contributing **nothing** sat in the divisor — a
|
||
silent chunk summed, a silent channel averaged. Here every input carries signal
|
||
and the weights genuinely sum to one. "Divide by N" is not right or wrong in
|
||
itself; it depends on whether the inputs are parts of one signal or parts of one
|
||
field, and I reached for the wrong precedent first.
|
||
|
||
### What is still not claimed
|
||
|
||
⚠️ **Which stream sits at which position is not determined** — their assignment is
|
||
by position, not content — so the port builds no 5.1 and applies no positional
|
||
downmix. ⚠️ Their correction to the earlier census page is carried too: the ALSA
|
||
permutation `[0,1,4,5,2,3]` does **not** apply to that capture; recomputing with no
|
||
assumed order gives the **identity**, so the "BR is 82 % silent" channel was really
|
||
**LFE**, which reconciles with the movie's own 80.64 % silent LFE. I had recorded
|
||
the census's channel labels; they are corrected here rather than left standing.
|
||
|
||
Every asserting check passes.
|
||
|
||
## Their stream assignment does not fit my region — weights NOT applied
|
||
|
||
The assignment is settled on their side by `byte_size`: ctx0 (1 294 336) → FL/FR,
|
||
ctx1 (1 118 208) → FC with LFE silent, ctx2 (1 171 456) → BL/BR, giving the port
|
||
weights 0.4142 / 0.2929 / 0.2929. Applying them needs their contexts to be my
|
||
chunks. **They are not.**
|
||
|
||
| | bytes |
|
||
|---|---|
|
||
| their three contexts, summed | **3 584 000** |
|
||
| my whole resolved voice region for `ADV` | **3 114 352** |
|
||
| difference | **+469 648** — 15 % larger than the region they must fit in |
|
||
|
||
My region yields **three** chunks, one of which is an 84.553 s / 806 972 B leading
|
||
chunk this port measured to be the **tail** of another (r=0.998, lag flush against
|
||
its end) — and 806 972 is none of their three sizes. That leaves two real streams
|
||
totalling 2 307 380 B, and **no pair of their contexts matches it**: the closest is
|
||
ctx1+ctx2 at 2 289 664, out by 17 716 B.
|
||
|
||
🔴 **So the weights are not applied.** Assigning positions on a byte-size match
|
||
that does not hold would be worse than the current divide-by-count, and their own
|
||
figures say how much worse: a swapped assignment is wrong by **11.76 dB**. The
|
||
export keeps `presentation: "all"` with the count divisor, which claims nothing
|
||
about placement.
|
||
|
||
⚠️ What I am *not* claiming: that their assignment is wrong. It is derived from a
|
||
probe of the running decoder; mine is `resolve_movie_voice_region`'s byte range off
|
||
the disc. **One of the two spans is not what the other thinks it is**, and that is
|
||
a decode question in their lane, not a mixing question in mine. The numbers above
|
||
are the whole of what I can say.
|
||
|
||
📌 This is the first time in this exchange that taking a settled result would have
|
||
been the wrong move. Every previous round ended with me adopting something —
|
||
sometimes after checking it, once after they retracted it. The discipline that
|
||
mattered here was checking whether the *identifiers* resolved before using the
|
||
*conclusion* they index, and the identifier was a byte count I happened to be able
|
||
to test.
|
||
|
||
### What I did take
|
||
|
||
✅ **One mixer gain, not two** — the same 0.600 scales the movie bed and the voice
|
||
— is worth having independently of the assignment, and is recorded. ⚠️ Not baked
|
||
in as a constant: whether 0.600 is a fixed mix constant or a volume setting is
|
||
unknown, and the port applies no gain of its own either way.
|
||
|
||
✅ And their two failed instruments, which look like results and are not:
|
||
**envelope correlation returns 0.86–0.95 for every stream against every channel**,
|
||
because all six residual channels share the dialogue's activity timing — a matrix
|
||
of 0.9s reads as a strong finding and is the opposite. **Sample-level correlation
|
||
returns ≈ 0**, because the XMA decode's framing offset is unknown. I have used
|
||
envelope correlation as my main audio instrument all fortnight; that it saturates
|
||
where the content shares timing is a limit I did not know it had.
|
||
|
||
## The resolver starts late, and my "duplicate tail" was a real stream all along
|
||
|
||
My refusal to apply their weights found a defect in their decoder:
|
||
**`resolve_movie_voice_region` starts inside the first stream.** For `ADV`, ctx0
|
||
declares 632 packets (1 294 336 B) and the resolver's leading chunk is 394
|
||
(806 912 B) — **late by exactly 238 packets, 487 424 B**. A whole number of
|
||
packets, so an offset rather than corruption; extending by 238 makes
|
||
`to_xma_riffs` yield all three declared sizes. Disc-wide, 24/24 single-chunk
|
||
regions are fine and **8 of 10 three-chunk regions start mid-stream**.
|
||
|
||
### 🔴 Which re-reads a measurement of mine, without touching the measurement
|
||
|
||
This port measured the leading chunk as *"the TAIL of the kept stream [refuted]"* —
|
||
envelope correlation r=0.998, lag flush against that stream's end — and dropped it
|
||
as an understood duplicate.
|
||
|
||
**The correlation was right and the conclusion was wrong.** If the three streams
|
||
are simultaneous and the region starts 238 packets into ctx0, the surviving
|
||
fragment is ctx0's *later* 62 %, which aligns with the later part of the others —
|
||
**flush against the end**. That is precisely what I measured. Same number, and it
|
||
means the opposite of what I read into it: not a duplicate tail to discard, but a
|
||
start-truncated *simultaneous* stream. **The port has been dropping a real stream.**
|
||
|
||
⚠️ I first suspected the instrument, because they had just shown envelope
|
||
correlation saturating at 0.86–0.95 across every stream/channel pair. Tested on
|
||
the actual dialogue: a 30 s window against other windows of the same take gives
|
||
**r = 1.0000** at zero offset and **−0.08 … +0.08** everywhere else. It localises
|
||
sharply. Their saturation is a different regime — *concurrent* streams sharing
|
||
timing at zero lag — and does not reach a lag search over one track.
|
||
|
||
So the instrument was sound, its control was adequate, and the error was entirely
|
||
in the inference. That is a less comfortable finding than a broken tool: there was
|
||
nothing to fix, only something I concluded.
|
||
|
||
### Not fixed here, deliberately
|
||
|
||
The port still drops chunk 0. Including it correctly needs one of two things I do
|
||
not have:
|
||
|
||
* **the corrected span** — theirs, and they explicitly say not to extend blind:
|
||
`ADV` is start − 238×2048, but for the other seven affected regions the exact
|
||
clip is **unknown**, their audit's figure being an upper bound (243 for `ADV`
|
||
where the truth is 238);
|
||
* **or an alignment decision** — chunk 0 is missing its *head*, so summing it from
|
||
sample 0 would be wrong by 84.5 s against 137.3 s. Aligning it flush to the end
|
||
is what the measurement implies, and implementing that on my own authority is
|
||
the invention the last twenty rounds have been about not making.
|
||
|
||
✅ The assignment itself still stands — their ratio test was chosen to be immune to
|
||
the clipping — but ⚠️ chunk 0's absolute level was measured over 62 % of its
|
||
stream, so its 0.05 dB agreement is luckier than it reads.
|
||
|
||
## The export knew the voice was incomplete; the runtime did not say so
|
||
|
||
The manifest has carried a full account of the voice export's gap for weeks, and
|
||
the runtime printed `+ voice ADV` and nothing else. That asymmetry is the
|
||
dangerous one for audio specifically: a reader of `manifest.json` gets a
|
||
paragraph, and a person **listening** gets clean dialogue with no way to learn
|
||
that a stream is absent from it.
|
||
|
||
This port already governs the same situation elsewhere — NEW GAME announces the
|
||
two measured screens it jumps over rather than skipping them silently. Audio had
|
||
no equivalent, so:
|
||
|
||
* `ManifestAudio` gains **`incomplete`**, one line naming what an asset is *known*
|
||
to be missing. Absent means nothing is known to be missing — **never** that the
|
||
asset was checked and found complete.
|
||
* `MenuAudio` carries it alongside the stream, and `_play_video` prints it at the
|
||
moment the voice starts.
|
||
|
||
```
|
||
+ voice ADV
|
||
🔴 KNOWN INCOMPLETE: 2 of 3 streams. The running game decodes all 3 …
|
||
```
|
||
|
||
Verified on both paths — the boot's `ADV` and P7's `S00A`.
|
||
|
||
### 🔴 And the first version of the message was false for one of the two assets
|
||
|
||
It read *"one is a start-truncated stream this export drops"*. That is `ADV`'s
|
||
story: its dropped chunk is the start-clipped remainder of ctx0. **`S00A`'s
|
||
dropped chunks are digitally silent** — a different reason entirely — and the
|
||
message would have told anyone running the new-game intro something untrue about
|
||
their own asset.
|
||
|
||
Caught by reading the output for both, which took one command and which I nearly
|
||
skipped because the `ADV` line was obviously correct. The wording now states the
|
||
counts and points at the entry's `why`, because **which streams are dropped and
|
||
why is not the same story twice** and a single sentence cannot carry both.
|
||
|
||
⚠️ Worth naming as its own shape: a message generated *once* from a template but
|
||
*true* only for the case it was written against. It is the failure mode of every
|
||
generic warning, and it is harder to see than a wrong number because the sentence
|
||
is well-formed and confident in both places.
|
||
|
||
## The voice export is complete — new pin, and the cause was a "within one bank" cap
|
||
|
||
`formats-pin-2026-08-30`. The cause of the late start was a second condition on
|
||
the start filter: `end - s < 1_500_000`, *"only within one bank"*. `ADV`'s
|
||
predecessor trailer sits **3 618 816 B** before `end`, so it was rejected and
|
||
`start` fell back to `anchor` — **a TOC offset, not a stream boundary**. That is
|
||
exactly why it hit regions over 1.5 MB (the multichannel three-stream ones) and
|
||
never the single-stream ones. 17 of 95 movies took the fallback.
|
||
|
||
| | before | after |
|
||
|---|---|---|
|
||
| `ADV` region | 3 114 352 B | **3 618 816 B** |
|
||
| `ADV` streams kept | 2 of 3 | **3 of 3 — complete** |
|
||
| `S00A` streams kept | 1 of 3 | **2 of 3**, the third digitally silent |
|
||
| `ADV` peak | −3.1 dBFS | −2.84 dBFS |
|
||
|
||
✅ **The voice export is now complete for both movies**, closing a defect that was
|
||
open for this entire session — and my re-reading of the "duplicate tail" as a
|
||
start-truncated simultaneous stream is what the fix confirms. `ADV` drops **zero**
|
||
chunks; the runtime no longer prints its incomplete line.
|
||
|
||
### 🔴 And the incompleteness warning was crying wolf on `S00A`
|
||
|
||
With `ADV` fixed, `S00A` still read **KNOWN INCOMPLETE** at 2 of 3 — because its
|
||
third chunk is **93.694 s of exact zeroes**. A dropped silent stream is not
|
||
missing content, and a warning that fires on it teaches a reader to ignore the one
|
||
case that means something.
|
||
|
||
`Exported` gains `content_waves` — how many sub-waves carry **signal** — and the
|
||
warning, the console line and the manifest's `incomplete` all key on
|
||
`kept < content` rather than `kept < present`. Both movies now report no gap,
|
||
correctly.
|
||
|
||
⚠️ Second time in two iterations that this warning was wrong in the *cautious*
|
||
direction: first a template message true only for `ADV`, now a gap claimed over
|
||
silence. Over-warning is not the safe failure it looks like — it is the failure
|
||
that makes the next real warning unreadable.
|
||
|
||
### A second movie, in the predicted direction
|
||
|
||
They note only `ADV` has external ground truth; the other 16 rest on their own
|
||
sweep. **`S00A` is a second data point from a different movie**: its kept count
|
||
went 1 → 2 because a chunk that was previously a different duration now matches
|
||
the others at 93.694 s — exactly what restoring a truncated first stream
|
||
predicts.
|
||
|
||
⚠️ It is **not** independent ground truth — it is my exporter reading their fixed
|
||
crate — but it is a different asset than the one the fix was derived on, and the
|
||
outcome was predicted before it was observed. Recorded as that and nothing more.
|
||
|
||
Oracle rows unmoved; MODDING rules pass.
|
||
|
||
## The positional weights are applied — keyed by byte size, so the key is a check
|
||
|
||
With the span fixed, `ADV`'s three chunks map onto the Decoder's contexts
|
||
**exactly** — each is a declared `byte_size` plus the 60-byte RIFF header
|
||
`to_xma_riffs` prepends:
|
||
|
||
| chunk | bytes | − 60 | context | position | weight |
|
||
|---|---|---|---|---|---|
|
||
| 0 | 1 294 396 | 1 294 336 | ctx0 | FL/FR | **0.4142** |
|
||
| 1 | 1 118 268 | 1 118 208 | ctx1 | FC, LFE silent | **0.2929** |
|
||
| 2 | 1 171 516 | 1 171 456 | ctx2 | BL/BR | **0.2929** |
|
||
|
||
`authored/audio.json` gains `voice.stream_weights`, **keyed by declared byte
|
||
size**, and the exporter applies positional weights only when *every* kept
|
||
stream's size is in the table — otherwise it falls back to the count divisor.
|
||
|
||
🔴 **The key is the check.** Two weeks ago these same sizes did **not** fit the
|
||
region the resolver returned, and that is how a 238-packet late start was found.
|
||
Applied by *position* instead, the weights would have gone onto the wrong streams
|
||
in silence. `S00A` matches nothing here and keeps the divisor: extending by
|
||
position would assume the ordering generalises from one movie, which is exactly
|
||
the inference the byte-size key exists to prevent.
|
||
|
||
`ADV` now mixes at 0.4142 / 0.2929 / 0.2929 and lands at **−2.87 dBFS**.
|
||
|
||
### ✅ An unlooked-for structural confirmation
|
||
|
||
The generated filter folds chunks 0 and 2 from **two** live channels
|
||
(`0.5*c0+0.5*c1`) and chunk 1 from **one** (`c0`). `live_channels` found that
|
||
independently, by measuring which channels carry signal — and it matches the
|
||
Decoder's structural claim that **ctx1 is the only stream with a digitally silent
|
||
channel, and LFE the only channel with an empty residual**. Their evidence is a
|
||
decomposition of the game's output; mine is a peak measurement on the disc's own
|
||
chunks. Different sides, same structure.
|
||
|
||
## 🔴 Unexplained: `verify-menu-audio`'s dead-press check has started failing
|
||
|
||
Its first assertion — five presses bound to nothing produce a Master bus
|
||
**bit-identical** to five waits — now reports DIFFER, reproducibly across three
|
||
runs. The two recordings diverge at **0.085 s**, differ on 92 % of samples, and
|
||
have different durations (1.300 s against 1.207 s) where they were previously
|
||
identical.
|
||
|
||
⚠️ **I have not identified the cause and am not guessing at one.** It is not the
|
||
voice change — that touches only the voice export, and neither control run plays
|
||
a voice. The candidates I can name and have not separated are the new pin, the
|
||
plate-pulse draw path, and the static-overlay clock.
|
||
|
||
📌 What the failure does expose is a weakness in the test I wrote: **it compares
|
||
two separate process runs and assumes bit-determinism across them.** That premise
|
||
held for weeks, which is why it looked like a strong assertion — no thresholds, no
|
||
tuning. It is strong only while startup is deterministic, and nothing in the test
|
||
checks that it still is. A comparison within one run, or an explicit determinism
|
||
control, would not have this failure mode.
|
||
|
||
Filed rather than patched: silencing it would remove the only check that a dead
|
||
press stays silent, and I would rather have a failing check than a passing one
|
||
whose premise I have stopped believing.
|
||
|
||
## External ground truth for every three-chunk region — the movies' own durations
|
||
|
||
They have no external check on the 15 regions beyond `ADV`, and no route to one
|
||
that does not go through the port or the emulator. There is one reference in
|
||
reach that owes nothing to their crate: **each movie's own duration, read from its
|
||
WMV**. A start-truncated stream must decode *shorter than its movie* — `ADV`'s was
|
||
84.55 s against 137.71, a 38 % deficit.
|
||
|
||
Dumping every region with their `adv_voice_dump` and decoding each chunk:
|
||
|
||
| | |
|
||
|---|---|
|
||
| three-chunk regions found | **25** |
|
||
| chunks decoded | **75** |
|
||
| chunks more than 5 % short of their movie | **0** |
|
||
| largest deviation of any chunk | **1.78 %** |
|
||
|
||
✅ **No region is still truncated.** The reference is external — the durations come
|
||
from `/disc/dat/movie/*.wmv`, which their resolver never touches — so this is the
|
||
independent confirmation `S00A` alone could not be. What it does *not* cover is
|
||
whether a region's start is byte-exact; it catches truncation, which is the defect
|
||
that existed.
|
||
|
||
🟡 **A population discrepancy worth their attention.** Their page says *"8 of 10
|
||
three-chunk regions start mid-stream"*. I find **25** three-chunk regions on this
|
||
disc, not 10. Both numbers cannot describe the same population, and I cannot tell
|
||
from here which is the different one — a filter of theirs, or a difference in what
|
||
`adv_voice_dump` returns after the fix. Reported, not resolved.
|
||
|
||
### 🔴 My first run silently covered half of them and said it was clean
|
||
|
||
It reported *"0 of 13 three-chunk regions have a short chunk"*. There were 25.
|
||
`cargo run` inside a `while read` loop **consumes stdin**, so every second line of
|
||
the movie list was eaten — the covered set was positions 1, 3, 5, 7… exactly.
|
||
|
||
The result was *correct for what it measured* and the population was half what the
|
||
sentence implied. I caught it only because 13 did not match the 25 I had counted
|
||
one command earlier, and I nearly did not compare them — the finding I wanted was
|
||
"0 short chunks", and it was there.
|
||
|
||
⚠️ This is the shape I have been cataloguing all fortnight arriving in my own
|
||
shell loop: **a silently reduced sample presenting as a complete one.** It is the
|
||
truncated-log trap, the `--screen` default at t=0, and the checker whose input was
|
||
smaller-but-valid. Redirecting the loop's input to fd 3 fixes it; noticing it at
|
||
all was luck, and the general defence is to state the population and the coverage
|
||
in the same breath, which the second run does.
|
||
|
||
## The menu bed loops at 61.93 s — and my 3.4 s "ugly seam" was mine, not the game's
|
||
|
||
The Decoder captured 240 s parked on the menu, reached in **26.8 s** via the
|
||
XMA-log oracle they wrote down rather than a screenshot. Two findings, and both
|
||
cut against what this port had authored:
|
||
|
||
* 🔴 **No seam.** Zero runs ≥0.3 s below median−18 dB in 232 s of the real menu.
|
||
* 🔴 **Not the wave's length.** r = **−0.009** at 87.750 s; top lag **61.909 s**.
|
||
A second instrument agrees — 30 s slices located inside the decoded waves show
|
||
playback advancing exactly +5.00 s per 5 s and wrapping at **61.93 s**, three
|
||
times, against a control that finds slices cut at 10/45/70 s at 10.00/45.00/70.00.
|
||
|
||
The loop is **[≈0, 61.93)** of an 87.744 s wave, so the final ~25.8 s — the
|
||
fade-out and trailing silence — is **never played**. The game loops before the
|
||
fade.
|
||
|
||
### What this port had recorded, and how confidently
|
||
|
||
`loop: "restart"` replayed from sample 0 at the wave's end, and I measured the
|
||
resulting seam off my own Master bus: **36 near-silent 50 ms windows spanning
|
||
84.40–87.80 s, about 3.4 seconds**. I wrote that up as *the price of a missing
|
||
loop point*, put it in `BLOCKED.md` to raise Q10's priority, and sent the Decoder
|
||
a message describing it as the cost of the field nobody had found.
|
||
|
||
**It was our seam.** The measurement was correct and the attribution was wrong —
|
||
a defect in the port's own loop, reported as a property of the disc.
|
||
|
||
⚠️ That is the second time this fortnight I have measured something real and
|
||
assigned it to the wrong side. The first was reading a start-truncated stream as
|
||
a duplicate tail. Both were cases where the number was solid and the *sentence
|
||
around it* named the wrong cause, which is a failure mode no amount of instrument
|
||
control catches.
|
||
|
||
### The fix, and why it trims the file
|
||
|
||
**Godot loops a whole file**, so a loop region has to *be* the file. `BgmSpec`
|
||
gains `loop_end_s` and the exporter trims: the bed is now **61.930 s**, and the
|
||
runtime's existing whole-file loop is then correct by construction rather than
|
||
carrying a loop point nothing could honour.
|
||
|
||
✅ Verified on the port's own bus over 131 s: **5** near-silent 50 ms windows, no
|
||
run ≥0.2 s — against 36 windows and 3.4 s before. The seam is gone.
|
||
|
||
⚠️ Recorded limits: the loop **start** is inferred, since [0.0, 61.93) and
|
||
[0.25, 62.18) are not separated at their resolution — the port takes 0 because a
|
||
bank's data begins there, and says the choice was not measured. And a modder
|
||
replacing `main_menu.ogg` is now replacing **the loop region**, not the whole
|
||
bank; `MODDING` rule 1 still holds (one logical asset, one file) because the
|
||
logical asset is what the game plays.
|
||
|
||
### 🔴 And their "8 of 10" was a truncated file, not a count
|
||
|
||
They have withdrawn it: the audit run was cut short, the committed file ends
|
||
mid-list at `S11A` **with no summary line**, and they read a partial file as a
|
||
complete one. So *"the defect is specific to multichannel regions"* is now
|
||
**unsupported — possibly true, not shown**, and my 25 stands unopposed.
|
||
|
||
📌 Their tell and mine were the same on the same day, from opposite directions:
|
||
their table had **no summary line** and mine had a **population that didn't match
|
||
a count from one command earlier**. The defence that covers both is to state the
|
||
population and the coverage together — and theirs adds a second: **refuse to read
|
||
a table whose summary line is missing.**
|
||
|
||
## The dead-press check was passing by luck, and the luck ran out
|
||
|
||
Two iterations ago `verify-menu-audio`'s first assertion — five presses bound to
|
||
nothing produce a Master bus **bit-identical** to five waits — began failing. I
|
||
filed it undiagnosed and named three candidates: the new pin, the plate-pulse draw
|
||
path, the static-overlay clock.
|
||
|
||
**It is none of them.** Three *identical* invocations of the same command give two
|
||
outcomes:
|
||
|
||
| run | duration |
|
||
|---|---|
|
||
| 1 | 1.207438 s |
|
||
| 2 | **1.300317 s** |
|
||
| 3 | 1.207438 s |
|
||
|
||
The difference is **0.092879 s = exactly 4096 samples**, one mixing buffer. The
|
||
recording quantises to whole buffers, and a one-buffer shift moves both the length
|
||
and the alignment of everything inside it. So a byte-for-byte comparison of two
|
||
separate runs cannot hold.
|
||
|
||
🔴 **The premise was never guaranteed — it was luck.** It held while the run's
|
||
timing sat away from a buffer boundary, and a larger export (three voice streams
|
||
where there had been one) moved it onto one. **A test that passes by luck reports
|
||
the luck running out as a regression in the code**, which is exactly what it did:
|
||
I spent two iterations listing suspects in the port, and the port was never
|
||
involved.
|
||
|
||
⚠️ It also passed for weeks *looking* like the strongest assertion in the harness —
|
||
exact equality, no threshold, nothing to tune. That was true and it was resting on
|
||
an assumption nothing checked. Strength of the assertion said nothing about
|
||
soundness of the premise.
|
||
|
||
### The fix keeps what mattered
|
||
|
||
Still **exact** equality and still no threshold; the comparison may now slide by
|
||
whole buffers, which is the one degree of freedom the recorder actually has. In
|
||
practice it finds `+0` or `+1`.
|
||
|
||
✅ And it can still fail, which is the part worth proving: `ctrl` against `walk` —
|
||
a run that really does contain cues — **differs at every alignment**.
|
||
|
||
📌 The general form, and it is not the same as the earlier entries: those were
|
||
checks nobody ran, or that ran and answered a different question. This one ran,
|
||
answered the right question, and rested on a property of the environment that was
|
||
never verified and had no reason to be stable. **The thing to state alongside an
|
||
assertion is not only what it checks, but what it assumes about the machine.**
|
||
|
||
## Independent confirmation of the 1.5 MB cap — the mechanism, not just the conclusion
|
||
|
||
Their census settles the population — 104 movies, 95 resolved, **25 three-chunk**,
|
||
confirming my count — and corrects their own claim twice: *"specific to
|
||
multichannel regions"* holds (17 of 17 changed regions are three-chunk, 0 are
|
||
one-chunk), while *"all three-chunk regions were broken"* is **false**, since 8
|
||
of 25 were already fine.
|
||
|
||
The 8 they name are the checkable part, because the cause predicts them. A region
|
||
trips the `end - s < 1_500_000` filter only if its span **exceeds** the cap.
|
||
Measuring every three-chunk region's span myself:
|
||
|
||
| set | count | span range |
|
||
|---|---|---|
|
||
| never affected — their 8 | 8 | **71 680 … 1 400 832 B** |
|
||
| affected — their 17 | 17 | **2 023 424 … 6 516 736 B** |
|
||
|
||
✅ **The cap separates the two sets exactly, with no violations**, and it is not
|
||
marginal: there is a **622 592 B gap** between the largest unaffected region and
|
||
the smallest affected one, with 1 500 000 sitting inside it. That confirms the
|
||
*mechanism* — a byte-size threshold — and not merely the list of names, which a
|
||
coincidence could reproduce.
|
||
|
||
### 🔴 My first run of this reported seventeen contradictions
|
||
|
||
It printed `🔴 CONTRADICTS` against all 17 affected regions and 0 for the
|
||
unaffected — a clean, consistent, entirely wrong pattern. Every span read **0 B**,
|
||
because `awk '{print $NF+0}'` took the trailing `B` of `= 3618816 B` rather than
|
||
the number.
|
||
|
||
Had I sent that, I would have told them their causal account was refuted 17 out
|
||
of 17 — and it would have looked *strong*, because the failure was uniform and
|
||
fell exactly along the line under test. **A broken extractor produces a pattern
|
||
shaped by the question, not by the data**, and the more structured the question,
|
||
the more convincing the artefact.
|
||
|
||
⚠️ What saved it was the 8 unaffected regions reading 0 B too. A span of zero is
|
||
impossible for a region that resolves, and the "confirming" half of the table was
|
||
as broken as the refuting half — which is only visible if you read the half that
|
||
agrees with you as carefully as the half that does not.
|
||
|
||
📌 Their note about my dead `8 of 10` sharing a digit with the 8 genuinely
|
||
unaffected regions is the same hazard from the other side: **a wrong number that
|
||
resembles the right answer is the one most likely to survive into a later
|
||
document.** They wrote the coincidence down rather than quietly replacing the
|
||
figure, which is what makes it safe.
|
||
|
||
## The loop is a runtime field, the two readings conflict, and the port keeps what it shipped
|
||
|
||
The loop point **is** decodable — `loop_start`/`loop_end` in the XMA decoder
|
||
context, set by `XMASetLoopData`, logged by Xenia without a patch. But the values
|
||
imply a cycle of roughly **[10 s, 72 s]** against the **[0.25, 57.18 s]** their
|
||
audio tracking gave, and neither reading is withdrawn.
|
||
|
||
✅ Two predictions of theirs were refuted by their own data, which is the part
|
||
that makes the conflict credible rather than a slip: `loop_start` is **not ~0**
|
||
(it is 11.6 % into the stream), and a linear bits→seconds conversion yields
|
||
**62.34 s and 63.29 s for two stems that must stay sample-synchronous** — 0.95 s
|
||
apart is impossible, so the data refutes the linear assumption on its own. XMA
|
||
frames are variable-length in bits.
|
||
|
||
**The port keeps `loop_end_s: 61.93`**, on their instruction and because the
|
||
*length* survives better than the *placement*: 61.93 rests on an autocorrelation
|
||
that used no wave at all.
|
||
|
||
### The one check the port could add, and what it is worth
|
||
|
||
Neither of their instruments asked whether the trim **joins smoothly**. Over
|
||
126.5 s of the port's own bus, the wrap at 61.93 s and again at 123.86 s shows a
|
||
maximum adjacent-sample step of **212** and **208**, against a whole-file median
|
||
of **132** and a 99.9th percentile of **3 737**. The join is not a click.
|
||
|
||
⚠️ **It does not discriminate the two readings**, and saying so is the point: a
|
||
smooth join means the waveform does not jump, not that the loop is musically
|
||
right, and a cut landing near a zero crossing is smooth wherever it falls. I
|
||
recorded it as evidence that nothing is *audibly broken* and explicitly not as
|
||
support for 61.93 over [10, 72].
|
||
|
||
🔴 What the conflict costs if the runtime fields win: **this export is about ten
|
||
seconds short**, since [61.93, 72] would be content the game plays and we omit.
|
||
Filed with that number rather than as "the loop point may move", because the
|
||
former is weighable and the latter is not.
|
||
|
||
### Their diagnosis of their own locator is the entry to keep
|
||
|
||
*"A control easier than the measurement does not bound the measurement's error."*
|
||
Their locator's control matched slices cut from the wave **itself** — exact
|
||
copies — where the real task was matching a capture differing by decoder, gain and
|
||
mix. The clean +5.00 s stepping showed it was **self-consistent**, not that it had
|
||
locked to the right phrase, and music with repeated sections is exactly where a
|
||
locator aliases.
|
||
|
||
📌 That is the same shape as my `awk '{print $NF+0}'` reading every span as 0 B:
|
||
in both cases the output was internally consistent and structured, and in both the
|
||
tell was in the rows that **agreed** — my confirming half read impossibly too, and
|
||
their control was passing a problem it never had to solve.
|
||
|
||
## The duration is confirmed and the window is wrong — and the start is now a visible field
|
||
|
||
They stopped *converting* the runtime fields and **timed** them instead: a probe
|
||
tailing the Apu debug log, stamping `read_offset` on arrival, watching **three**
|
||
wraps — each from its own `loop_end` to its own `loop_start`, with **both
|
||
contexts wrapping at the same instant every time**.
|
||
|
||
| | |
|
||
|---|---|
|
||
| observed cycle | 61.56 s, 62.06 s → **61.81 s** |
|
||
| authored here | **61.93 s** |
|
||
| difference | **0.2 %** |
|
||
|
||
✅ **The length is settled**, and by instruments sharing nothing: a wall clock
|
||
between decoder events against an autocorrelation that never touched the wave.
|
||
Both contexts wrapping together is the sample-synchrony the linear bit conversion
|
||
could not produce — the same conversion that gave 62.34 and 63.29 s for two stems
|
||
that must be synchronous, and so refuted itself.
|
||
|
||
🔴 **The window is wrong.** `loop_start` is at 3.6 M bits — **11.6 % of the
|
||
stream, about ten seconds** — not the 0.25 s their earlier tracking gave. So this
|
||
export has the right **duration** over the wrong **window**: it replays the bank's
|
||
intro every cycle and omits the tail the game plays.
|
||
|
||
📌 **My smooth-join check has a second use I could not have anticipated.** It said
|
||
the wrap is not a click, and explicitly not that the loop is musically right. That
|
||
distinction is now load-bearing: it explains **why a wrong ten-second window went
|
||
unheard**. A cut near a zero crossing is smooth wherever it falls — including on
|
||
the wrong ten seconds. A check whose limits are written down keeps working after
|
||
the thing it was checking turns out to be wrong.
|
||
|
||
### Not re-cut — and the assumption is now a field
|
||
|
||
Their instruction is to wait: the exact start is **not measured**. Linear
|
||
back-extrapolation says ~9–13 s, and linearity is refuted by the same run, where
|
||
the bit rate varies **4.4 %** within one stream.
|
||
|
||
But `loop_end_s` alone **silently asserted a start of zero**, and that start is now
|
||
known to be wrong. So the entry gains `loop_start_s`, authored as **0.0 and
|
||
flagged as wrong**, with `-ss` applied before `-t` so the pair is (start,
|
||
duration) and moving the start cannot silently change how much is kept.
|
||
|
||
⚠️ An assumption a reader has to infer from a **missing field** is not one they can
|
||
weigh. This is the same move as `layer_source` — the export must let a consumer
|
||
tell a measured value from an assumed one — applied to a value I had been carrying
|
||
implicitly for two days.
|
||
|
||
✅ The new path is **proved before it is needed**: with `loop_start_s = 10.0` the
|
||
command carries `-ss 10 -t 61.93` and the output stays 61.930 s — a window, not a
|
||
truncation. Restored to 0.0; the export is byte-unchanged. When the start is
|
||
measured this is a one-value edit, not a code change.
|
||
|
||
## The loop window is measured — `-ss 9.44 -t 61.87` — and the near-silence count tracked the error
|
||
|
||
The region is **[9.44 s, 71.31 s]** of an 87.744 s wave: the first 9.44 s is an
|
||
intro played **once**, the last 16.4 s a fade-out **never played**. Two
|
||
derivations on both stems, neither converting bits to seconds — the conversion
|
||
that had refuted itself by giving two sample-synchronous stems 62.34 and 63.29 s.
|
||
|
||
✅ **61.87 replaces 61.93**, 0.1 % apart. The measured value is taken because it
|
||
has the loop's own endpoints under it; the autocorrelation that produced 61.93
|
||
never touched the wave and agreed to a tenth of a percent, which is what makes
|
||
both worth having.
|
||
|
||
### The port's own near-silence count tracked the window's correctness
|
||
|
||
| window | near-silent 50 ms frames in ~127 s |
|
||
|---|---|
|
||
| no trim, `restart` at the wave's end | **36**, spanning 3.4 s |
|
||
| `[0, 61.93]` — right length, wrong window | **5**, no run ≥0.2 s |
|
||
| **`[9.44, 71.31]` — measured** | **0** |
|
||
|
||
That is a real corroboration from this side and it was not designed as one. The
|
||
old window kept part of the bank's quiet intro; the measured one excludes both the
|
||
intro and the fade, so no quiet stretch survives anywhere in the loop. The count
|
||
fell monotonically as the window got closer to right.
|
||
|
||
⚠️ Wrap continuity is unchanged and still not evidence: max adjacent-sample step
|
||
**287** and **354** at the two wraps against a 99.9th percentile of **3 812**. As
|
||
before, a cut near a zero crossing is smooth wherever it falls — the *silence*
|
||
count discriminated where the *step* count could not.
|
||
|
||
### 🔴 A stale `why` reached the manifest for two days
|
||
|
||
Correcting `loop_end_why` and `loop_start_why` left `loop_why` — **the field the
|
||
exporter concatenates into `manifest.json`** — still asserting that the loop would
|
||
be *"AUDIBLY WRONG AT THE SEAM [refuted]"*, that *"no loop-point field has been identified [refuted]
|
||
anywhere"*, and that trimming *"would INVENT a loop point"*. All three refuted;
|
||
all three shipped to any consumer reading the export.
|
||
|
||
**A correction that does not reach the artifact a consumer reads has not been
|
||
made.** The corrections existed, were accurate, and were in the wrong fields.
|
||
|
||
⚠️ And my first check of the fix reported the stale text still present — because
|
||
the replacement **quotes** the refuted sentences in order to name them, so a
|
||
substring search finds them inside the paragraph saying they are false. I had to
|
||
read the context to see it. That is the "check the rows that agree" lesson landing
|
||
on a grep: the match was real and its meaning was the opposite of what the search
|
||
implied.
|
||
|
||
### Why the wait for 9.44 was cheap
|
||
|
||
Their note is worth keeping: it was not that the field predicted the value, but
|
||
that `loop_end_s` alone was **asserting** a start of zero in a form no reader
|
||
could weigh or find — and that proving `10.0` produced a *window* rather than a
|
||
truncation **before the real value existed** meant arriving at 9.44 was a
|
||
one-value edit with a clean baseline behind it.
|
||
|
||
## Applying "grep the corpus for the claim" to my own corpus
|
||
|
||
The Decoder found that a claim they refuted in a *new page* was still standing in
|
||
`bgm-two-stems.md` and in `HANDOFF.md` — the page a reader is told to consult
|
||
instead of the rest. Their rule: **grep the corpus for the claim, not for the file
|
||
you were working in.** Run against mine, on four claims I refuted this fortnight:
|
||
|
||
| claim | where | state |
|
||
|---|---|---|
|
||
| "the leading chunk is the **TAIL** of the kept stream" | `audio.rs` → **`manifest.json`** | 🔴 **still shipping** |
|
||
| "six expected DIFFERS [refuted]" | `BLOCKED`, `DECISIONS` | marked |
|
||
| "the boot is **known too fast [refuted]** on both" | `DECISIONS` | 🔴 **standing, unmarked** |
|
||
| "the **only thing** making the plate reappear" | `BLOCKED` | marked |
|
||
|
||
### The one that shipped
|
||
|
||
The dropped-chunk explanation in the exported `why` still told readers the leading
|
||
chunk *"IS understood: the TAIL of the kept stream [refuted]"*. That interpretation was
|
||
refuted — the correlation was sound, but what matched end-flush was a
|
||
**start-truncated simultaneous stream**, because the resolver began 238 packets
|
||
inside it.
|
||
|
||
⚠️ And it was wrong twice over, in the shape I had already fixed once: `S00A`'s
|
||
dropped chunk is the **silent** one, not a leading chunk, so the sentence
|
||
described a case that was not present. **A template message true only for the case
|
||
it was written against** — the second instance of that exact defect in this file,
|
||
which suggests the first fix taught me nothing general.
|
||
|
||
Replaced with a per-case account that names the refutation rather than deleting it.
|
||
|
||
### The one that was standing
|
||
|
||
*"The port's boot is known too fast [refuted] on both, by an unmeasured amount"* — withdrawn
|
||
days later, when the splash dwells turned out to be **declared on the disc** and
|
||
the port already exact. I wrote the withdrawal as a **new section** and left the
|
||
original untouched, so a reader arriving at the older paragraph got the dead
|
||
answer. Annotated in place, pointing at the withdrawal.
|
||
|
||
### And a false positive that is its own lesson
|
||
|
||
`BLOCKED.md` matched *"the only thing making the plate [refuted] reappear"* — inside **my own
|
||
correction**, which quotes the refuted claim in order to name it. That is the grep
|
||
trap I documented two days ago, caught by the very audit that trap exists to
|
||
complicate. **Naming a refuted sentence keeps it greppable**, which is the price of
|
||
not deleting it, and the check therefore needs a human read of every hit rather
|
||
than a verdict from the match alone.
|
||
|
||
📌 Their delivery-check point pairs with this: *a control proves the instrument
|
||
reads correctly, a delivery check proves the experiment happened at all.* Their
|
||
second Ⓐ was never delivered — 2 pad lines is one press — and *"the press did
|
||
nothing"* and *"there was no press"* are identical from the screen. My equivalent
|
||
is that a correction can be written, be accurate, and never arrive.
|
||
|
||
## A refuted-claim register, because the audit found what the audit found
|
||
|
||
The Decoder ran my corpus audit against theirs and found **four** refuted claims
|
||
still standing — including one they had corrected in a message to me, agreed with,
|
||
and written a METHOD entry about, **without landing the correction for a full
|
||
iteration**. Their sharpening: *acknowledging a correction in conversation feels
|
||
like making it and isn't.*
|
||
|
||
A hand audit finds the instances present on the day it runs. It does not stop the
|
||
next one. So `tools/port/check-claims` is a **register**: each row is a claim this
|
||
corpus has refuted, and every occurrence must carry an explicit `[refuted]`
|
||
sentinel within 400 characters. `check-all` runs it.
|
||
|
||
### 🔴 It found four more than my hand audit did
|
||
|
||
My manual pass checked four claims and found two problems. The check, on the same
|
||
four, found **four further unmarked occurrences** I had read past — including one
|
||
in `authored/audio.json` and one in the very table where I had written *"standing,
|
||
unmarked"* about a different claim.
|
||
|
||
### The marker is a sentinel, not a keyword, and that mattered
|
||
|
||
The first version matched a per-claim keyword near the hit — "refuted",
|
||
"WITHDRAWN". **Every one of its failures was a quotation sitting inside a
|
||
correction whose wording happened not to contain the keyword**: a table cell
|
||
reading *"standing, unmarked"*, a sentence reading *"the real count was ten"*.
|
||
|
||
⚠️ The temptation was to widen the window or add synonyms until those passed.
|
||
**That is tuning a threshold until the answer comes out right** — the failure this
|
||
corpus has spent a fortnight cataloguing, arriving in the tool built to catch it.
|
||
So the marker became a token the author must place. It cannot be satisfied by
|
||
phrasing, and its absence means exactly one thing.
|
||
|
||
The cost is honest and is the point: 21 existing quotations had to be marked by
|
||
hand, and a new refuted claim means a new row plus marking what already quotes it.
|
||
|
||
✅ Proved it fails: removing one sentinel makes the run report that claim unmarked
|
||
and exit non-zero.
|
||
|
||
### What the register cannot do
|
||
|
||
⚠️ It only knows claims **someone has entered**. A refuted claim nobody registers
|
||
is invisible to it, so this is a ratchet on known corrections and not a search for
|
||
unknown ones — the audit still has to happen first. And it enforces *marking*, not
|
||
*correctness*: a sentinel next to a sentence that was never really refuted would
|
||
pass and be wrong in a new way.
|
||
|
||
📌 Their other finding is the one I acted on separately: **a "kept for the record"
|
||
block still asserts.** `BLOCKED.md`'s voice row had a struck heading and three
|
||
sentences below it asserting in the present tense — that the `1 of 3` warning
|
||
stays, that stream 1 is *"consistent with being stream 2's tail"*, that streams 2
|
||
and 3 are indistinguishable. All three resolved days earlier. Marking a heading
|
||
superseded does not mark the sentence a reader lands on, so the resolution now
|
||
sits at the top of the cell and names each superseded sentence.
|
||
|
||
## State of the port, and a claim I built on for a week without checking
|
||
|
||
Every asserting check passes: the format validator (16 screens against
|
||
`sylpheed.screen/3`), all five MODDING rules, the capture-control sweep, the
|
||
refuted-claim register, the decisions index. The oracle rows sit at the tone
|
||
floor — `title_plate` **0.00 %**, both splashes **0.01 %**, `main_menu` 0.06 %,
|
||
`main_menu_options` 0.15 %, `extras` 0.19 %, `title` 0.21 %, `title_band` 0.35 %
|
||
against its own oracle-to-oracle gap. The P5 walk runs and ends on the title.
|
||
|
||
### 🔴 The refutation attempt this iteration was of something I had already used
|
||
|
||
The claim that reframed the entire voice question — *`ADV.wmv` carries **one**
|
||
audio stream and it is **WMA Pro 5.1**, not XMA, so the movie's own track is the
|
||
bed and the three streams are additional* — is checkable in one command against
|
||
the disc:
|
||
|
||
```
|
||
index=0 codec_name=wmapro channels=6 channel_layout=5.1 sample_rate=48000
|
||
1 audio stream
|
||
```
|
||
|
||
✅ Exactly confirmed.
|
||
|
||
⚠️ **And I had built on it for a week without running it.** The positional
|
||
weights, the `presentation: "all"` change, the refusal to apply the assignment
|
||
when the byte sizes did not fit — all of it rests on that reframing, and the
|
||
verification cost one `ffprobe` against a file I have had all along. I checked the
|
||
*byte sizes* scrupulously because they were the identifier I could test, and never
|
||
checked the sentence the identifier was serving.
|
||
|
||
📌 That is a different failure from the ones this file catalogues. Not an
|
||
unexercised rule, not a correction that never landed, not a control easier than
|
||
the measurement: **a premise so foundational that everything downstream got
|
||
audited and the premise itself did not.** The scrutiny went to the parts that
|
||
moved.
|
||
|
||
### What is still authored rather than measured
|
||
|
||
Four values, and the file says so at each:
|
||
|
||
| value | state |
|
||
|---|---|
|
||
| `flow.screens.main_menu.on_cancel` | **authored — likely but UNPROVEN**; Ⓑ returning to the title is stated in HANDOFF with no capture behind it |
|
||
| `ptbtn01.after_video` | **authored**; the game goes into Mission 1, which MISSION §7 scopes out, so "return to the title" is a chosen end state |
|
||
| `flow.navigation.input_during_transition` | **authored, not measured** — nobody has watched a press mid-fade; ignoring invents least |
|
||
| `authored/rendering.json`'s withheld leaves | two leaf records deliberately not drawn, each with its reason |
|
||
|
||
Everything else in `authored/` now carries `kind: measured` — the BGM bank and its
|
||
loop window, the plate's pulse period, the keyframe unit, the black hold, the
|
||
navigation wrap, the cue bindings, the voice streams and their positional weights.
|
||
|
||
⚠️ The black hold is measured but sits at **the top of its range** (~6.5–9.2 units,
|
||
authored 9), and that is recorded at the value rather than in a footnote.
|
||
|
||
## Identifying their submenu capture: edges where intensity could not
|
||
|
||
They reached and captured a submenu but could not identify it. Their diagnosis is
|
||
the useful part: **correlation cannot discriminate when the candidate renders are
|
||
near-blank**, and near-blank is exactly what the `.tbm` hypothesis predicts — all
|
||
19 `GP_SAVE_LOAD` builds scored −0.004…−0.010, a ranking with no information in
|
||
it. *The instrument is disabled by the thing it was brought in to detect.*
|
||
|
||
That diagnosis implies its own fix. Their capture is **99.999 % non-black** — a
|
||
full-screen background our renderer omits — and an additive background swamps an
|
||
intensity correlation. **It does not survive an edge map**: a smooth ground has no
|
||
edges, and the UI does.
|
||
|
||
### The control first, because a ranking is worthless without one
|
||
|
||
Edge correlation against my own `title` capture, over seven `GP_TITLE` builds
|
||
whose answer I know:
|
||
|
||
| build | r |
|
||
|---|---|
|
||
| **4 — the right answer** | **+0.2792** |
|
||
| 6 (`extras`, its nearest sibling) | +0.1936 |
|
||
| everything else | ≤ +0.037 |
|
||
|
||
✅ Right answer on top, 1.4× over second and 7.6× over third. Modest absolute r,
|
||
and a clear ranking — so the method discriminates on this corpus.
|
||
|
||
### The result
|
||
|
||
Their capture against all 22 candidate builds:
|
||
|
||
| build | r |
|
||
|---|---|
|
||
| **`GP_TUTORIAL` build 0** | **+0.4962** |
|
||
| `GP_TUTORIAL` build 1 | +0.3137 |
|
||
| best `GP_SAVE_LOAD` (17) | +0.0713 |
|
||
| worst | −0.0331 |
|
||
|
||
🟢 **The submenu is a `GP_TUTORIAL` build.** The winning r is *higher* than the
|
||
control's, its margin over second is *better* (1.58× against 1.4×), and both
|
||
TUTORIAL builds sit **4–7× above every `GP_SAVE_LOAD` build** — the archive
|
||
separation is far stronger than the within-archive one.
|
||
|
||
✅ It is independently plausible: `authored/flow.json` has `ptbtn03` = **TUTORIAL**
|
||
→ `TUTORIAL_MENU`, noted as *"the lesson list is not a `GP_TITLE` build"*. An Ⓐ
|
||
on a menu whose focus was TUTORIAL lands exactly there, and HANDOFF Q5 measured
|
||
initial focus as unstable boot to boot.
|
||
|
||
⚠️ **What this does and does not settle.** The **archive** is identified with a
|
||
large margin. **Which build within it** is not: 1.58× is the same order as my
|
||
control's 1.4×, and the two TUTORIAL builds are variants of one screen — so I
|
||
would call build 0 the better fit and not a determination.
|
||
|
||
⚠️ And the method inherits a limit worth stating: an edge map is insensitive to
|
||
*what* the background is, which is the point, but it is also insensitive to a
|
||
missing element that has no edges. It answers "which screen", not "is our render
|
||
complete".
|
||
|
||
### Refutation attempt: "`screen render` omits every `.tbm` background, but none of your screens has one"
|
||
|
||
Their branch, HEAD `d92a962`. The first half is theirs to prove and they proved
|
||
it against a capture. **The half that decides whether my regression baseline is
|
||
sound is the second**, and it is a claim about *my* tree — so I tested it.
|
||
|
||
`screen info --all`, grepped for `.tbm`, across all **16** builds in my manifest:
|
||
zero references. ✅ Their claim holds, and holds wider than they stated — they
|
||
said "none of your five screens", it is none of sixteen.
|
||
|
||
**Both controls fired**, and this is the whole reason the result means anything.
|
||
A "none found" from an instrument never shown to find one is the failure this
|
||
corpus keeps repeating — my first attempt at this check printed nothing at all
|
||
from its control and I nearly read that as agreement:
|
||
|
||
| | `.tbm` mentions |
|
||
|---|---|
|
||
| positive — `GP_TUTORIAL` build 0 | **1** (`pubase.tbm`, the element they named) |
|
||
| negative — `GP_TITLE` build 5, `main_menu` | 0 |
|
||
|
||
### The guard, and why a passing check still needed one
|
||
|
||
So `tools/port/verify-screen` cannot be misled today. ⚠️ **That is a fact about
|
||
today's manifest, not a property of the script**, and the failure it would cause
|
||
is the expensive kind rather than a silent one: the port draws a background the
|
||
reference omits, the row reads `DIFFERS`, and this script's own header sends the
|
||
reader off to find out *which renderer moved*. Neither did. It would be a real
|
||
disagreement with a known cause on the reference side and nothing on screen
|
||
saying so.
|
||
|
||
The row now says so. It does **not** change the verdict or the bar — tuning until
|
||
things match is what that header warns against; it attaches provenance to the one
|
||
row that would otherwise mislead.
|
||
|
||
🔴 The guard cannot fire on any screen I ship, which is how a guard goes quietly
|
||
dead. Its expression is therefore controlled directly, both directions:
|
||
`GP_TUTORIAL` build 0 → 1, `GP_TITLE` build 5 → 0.
|
||
|
||
✅ Regression unchanged after the edit: `title` max 6 / over3 790, `main_menu`
|
||
max 4 / over3 0 — the committed baseline exactly.
|
||
|
||
### Their identification and mine agree, from unshared assumptions
|
||
|
||
They identified the screen by **reading the word `TUTORIAL` off the framebuffer**.
|
||
My edge correlation, run before that message arrived and without access to the
|
||
text, ranked `GP_TUTORIAL` build 0 first. Two methods with no assumption in
|
||
common, one answer.
|
||
|
||
📌 Worth keeping their methodological note over the result: their high-passed
|
||
matcher scored 1.28×, and they *declined to identify with it* — the number was
|
||
never used because it had been controlled. My 1.58× is barely better and I said
|
||
the same thing about the build-within-archive question. **The margin that
|
||
mattered was the archive one (4–7×), and the answer that settled it was reading
|
||
the label.** Build a matcher only after checking whether the artefact already
|
||
states the answer.
|
||
|
||
## `on_cancel`: one half measured, and a MEASURED stamp removed from the other
|
||
|
||
The Decoder measured **Ⓑ on the main menu → the title** (their `86a8ce7`,
|
||
`docs/re/data/b-on-main-menu.txt`): delivery-confirmed, 73.5 % of pixels changed,
|
||
both captures naming themselves, **≤ 0.4 s**, and **no loading screen** on the
|
||
path despite the disc carrying four.
|
||
|
||
✅ `authored/flow.json` `main_menu/on_cancel` moves from *"likely but UNPROVEN"*
|
||
to **MEASURED**. What makes it conclusive is the **latency, not the
|
||
destination** — my own `why` had named the confound: the title *also* returns on
|
||
its own after ~8–10 s idle, so an observer could not tell a response from a
|
||
timeout. ≤ 0.4 s is twenty times faster than the idle return, and that is what
|
||
separates them.
|
||
|
||
### 🔴 The other half: my tree stamped MEASURED on a claim with no evidence
|
||
|
||
`title/on_cancel_why` read **`"MEASURED, HANDOFF Q5: Ⓑ on the title does
|
||
nothing."`** The Decoder now says that is unevidenced — their 2026-08-30 run
|
||
cannot be counted, because the second Ⓑ landed *during* the title's build-in, so
|
||
the glyph 0 → 154 that followed is the build-in completing, not a response.
|
||
|
||
I did not invent the stamp, and that is the point worth recording. **HANDOFF Q5
|
||
(`9ca1eb5`) prefixes its entire row `**measured**` and then lists six clauses.**
|
||
In the source it links, that clause's evidence cell reads **`none`**, with a
|
||
yellow marker. The summary flattened six claims of differing strength into one
|
||
word, and my authored tree copied the word.
|
||
|
||
⚠️ **The value does not change — `null` either way.** Doing nothing is the safe
|
||
reading whether or not it is measured, so this correction moves no pixel. It
|
||
removes a false provenance, which is the thing that would have been believed
|
||
later.
|
||
|
||
### The same row has a second empty cell, which nobody flagged
|
||
|
||
Auditing the rest of Q5 rather than only the clause I was handed: the **`up /
|
||
down`** row — *"one item per press, no auto-repeat at the durations tried"* —
|
||
also has an **empty evidence cell**, and my `navigation` block cites that same
|
||
row.
|
||
|
||
✅ It splits cleanly, and only one half is exposed:
|
||
|
||
* **one item per press** is evidenced *indirectly and well* — the wrap montage's
|
||
count only comes out if each press moves one (4 presses from `EXTRAS` landing
|
||
on `OPTIONS`). Keep it.
|
||
* **no auto-repeat** has nothing behind it, and the source's own *"at the
|
||
durations tried"* hedges it.
|
||
|
||
🔴 Worse, the port already behaved this way **without stating it**: `boot.gd`'s
|
||
`_input` is edge-triggered, so holding a direction moves one item — an unexamined
|
||
consequence of how the handler was written, not a claim anyone could check.
|
||
`navigation.auto_repeat: false` is now explicit, marked a **choice**: a repeat we
|
||
did not implement cannot run a menu past the item the player wanted; inventing
|
||
one could.
|
||
|
||
### Audit of every MEASURED stamp in `authored/`
|
||
|
||
34 stamps. **Six cite a HANDOFF row and nothing else** — the laundering path
|
||
above. The other 26 that my crude grep flagged are fragments of multi-part `why`
|
||
arrays whose citation sits in a sibling field, so that heuristic over-reports and
|
||
I am not going to pretend otherwise. Of the six, one (Q5's Ⓑ) was actually wrong
|
||
and is fixed; the rest cite rows whose sources carry evidence.
|
||
|
||
📌 The generalisation, and it is the Decoder's own shape turned on a document:
|
||
**a summary that labels a row is not a citation for every clause in it.** A
|
||
bundled `**measured**` is exactly as strong as its weakest cell.
|
||
|
||
## BLOCKED.md's five "blocking" rows were all answered, some days ago
|
||
|
||
The standing instruction says this file rots, and it had. Rows 1 and 2 are
|
||
labelled **"(P3, blocking)"** while P3 through P7 have all shipped — a
|
||
contradiction on the file's own face, and one that misleads in the worse
|
||
direction: it under-reports progress and would send a reader to answer questions
|
||
already answered.
|
||
|
||
Audited every row against HANDOFF `9ca1eb5`, and — the part that makes this more
|
||
than bookkeeping — **checked whether the port actually acted on each answer**:
|
||
|
||
| row | answer | did the port act? |
|
||
|---|---|---|
|
||
| 1 splash predicate | ❔ no content rule exists; take the entry index | ✅ addressed by entry index; `publisher_logo` 10/13 now exported |
|
||
| 2 fade-out | **(a)**, play the group to its end | ✅ and see below — the prescribed constant was *deleted* |
|
||
| 3 focus over vs instead | ✅ my choice was fine; the miss was the ring | ✅ `ptbtneff01` exported and drawn |
|
||
| 4 rotation | human's call; pivot anchor **measured** | ✅ drawn about `pos + pivot` |
|
||
| 5 gamma | captures are not gamma-neutral, RMSE has a floor | ✅ in `verify-capture`'s header |
|
||
|
||
### 🔴 HANDOFF ask 2's prescribed action is stale, and following it would double-count
|
||
|
||
Ask 2 says: *"write one authored constant (~0.4 s / ~24 units) and play the group
|
||
to its end."* Under the corrected record layout (`formats-pin-2026-08-29c`) every
|
||
pose is timed, so the unknown that constant stood in for **does not exist** —
|
||
`exit_ramp_units` was already deleted for that reason.
|
||
|
||
Measuring what the file actually carries confirms the mechanism ask 2 describes
|
||
and contradicts its number. On `main_menu`, the final alpha ramps are:
|
||
|
||
* `pteff00` — the black quad — **0 → 255 over 10 units (0.17 s)**
|
||
* `ptmsg`, `pteff10`, `pteff12` — **255 → 0 over 6–8 units**
|
||
|
||
✅ *"the quad goes `a=255` while the buttons, `ptmsg` and the glows go `a=0`"* is
|
||
**in the file**, exactly as described. ⚠️ But the ramp is **10 units, not 24**.
|
||
Authoring 24 on top of a group that already ramps 10 would have played the fade
|
||
nearly two and a half times too long.
|
||
|
||
### A decomposition that fits both numbers — offered as a hypothesis, not a finding
|
||
|
||
HANDOFF Q7 measures two quantities off the game: the fade-out ~0.4 s and the
|
||
black-hold plateau **0.17–0.23 s**. The file gives the ramp as 10 units (0.17 s).
|
||
|
||
in-file ramp 10 units + measured hold 10–14 units = 20–24 units = 0.33–0.40 s
|
||
|
||
🟡 The measured ~0.4 s sits at the **top** of that range. So the ~0.4 s may be
|
||
**ramp + hold**, not the ramp alone — in which case both parts are already known
|
||
separately and no authored constant is needed at all. **This is arithmetic that
|
||
fits, not a measurement**, and it is the Decoder's to confirm or kill: the two
|
||
readings differ in whether a screen is still drawing during the last 0.2 s.
|
||
|
||
### 🔴 And it exposes a disagreement in my own tree
|
||
|
||
`authored/timing.json` holds `black_hold_units: 9` = **0.15 s**, measured in the
|
||
draw stream. HANDOFF's plateau is **0.17–0.23 s**, measured off the game. **Mine
|
||
sits below their floor**, by 1–5 units.
|
||
|
||
I am **not** changing it. Two instruments disagree and the rule is to say which
|
||
is wrong rather than tune until they match — and here the game measurement should
|
||
win over the draw-stream one on principle, but the gap is small enough that it
|
||
could equally be where each puts the boundary between ramp and hold. It goes to
|
||
`BLOCKED.md` as an ask, at the value it was measured at.
|
||
|
||
## The plate came back in the game and not in the port
|
||
|
||
The Decoder's Ⓑ run answered both my asks and threw in a third finding: **after Ⓑ
|
||
from the menu the `PRESS Ⓐ` plate is re-drawn** — pressed 351.2 s, pulse back
|
||
358.5 s (`daf8f47`).
|
||
|
||
🔴 **The port did not do that.** Ⓑ landed on a *bare* title. `_menu_arrive()`
|
||
calls `_drop_overlay()` — correct, the plate goes with the screen it was measured
|
||
on — but nothing ever put it back: `_overlay_spec` is cleared the instant the
|
||
overlay is raised, and only the boot sequence ever set it. Confirmed by running
|
||
it, not by reading: the drawn list was the ten title elements with no `ptbtn00`.
|
||
|
||
✅ Fixed. `_rearm_overlay_for(name)` looks the declaration up in
|
||
`authored/flow.json`'s **boot step for that screen** rather than naming
|
||
`press_start`, so the plate returns by the same code path and the same shared
|
||
clock as on boot, and a screen that gains an overlay later gets it on both paths
|
||
with no edit here. **No new constant** — the delay is not authored, it is
|
||
whatever the boot already does.
|
||
|
||
Controlled both ways: Ⓑ → `overlay press_start raised`, drawing `ptbtn00`,
|
||
`ptbtn00f`; entering `EXTRAS`, which declares no overlay, raises **nothing**.
|
||
|
||
### An independent agreement I did not tune for
|
||
|
||
The script log had no press timestamp, so the port's own latency could only be
|
||
guessed from surrounding lines. Added one. With it:
|
||
|
||
| | |
|
||
|---|---|
|
||
| Ⓑ pressed | 1.01 s |
|
||
| title arrives, overlay armed | 1.37 s |
|
||
| **port's press → title** | **0.36 s** |
|
||
| **their measured Ⓑ latency** | **≤ 0.4 s** |
|
||
|
||
✅ That agreement is worth something because **nothing here was fitted to it** —
|
||
the port's transition timing comes from the screens' own fade keyframes, and this
|
||
is the first time the two numbers have been put beside each other.
|
||
|
||
### 🟡 The plate's return time does not agree, and I am not adjusting it
|
||
|
||
The plate is raised on arrival and its own group takes it opaque at t=238
|
||
(3.97 s), so the port's **press → plate visible ≈ 4.33 s**. Theirs is **7.3 s to
|
||
the pulse**. The pulse has a 120-unit (2 s) period, so pulse *detection* can lag
|
||
first paint by up to 2 s — which closes it to ~6.3 s at most and leaves roughly
|
||
**a second unexplained**.
|
||
|
||
⚠️ It would be easy to author a delay that makes 4.33 into 7.3. That is exactly
|
||
the tuning this corpus keeps warning about, and the previous authored delay in
|
||
this very block (`after_settle_seconds: 2.13`) was already refuted once by
|
||
arithmetic. Left alone; recorded as an ask.
|
||
|
||
### Two stamps upgraded, both now measured for real
|
||
|
||
* `navigation.auto_repeat` — a 2.0 s held ⬇ moves the cursor **once**, their
|
||
counter passing its control first. Was a consequence of edge-triggered
|
||
`_input`; now a measurement.
|
||
* `title/on_cancel` — Ⓑ on a **settled** title does nothing, twenty seconds
|
||
confirmed. This cell has now been `MEASURED` (wrongly), `AUTHORED` (honestly),
|
||
and `MEASURED` (truly), with the value `null` the whole way through.
|
||
|
||
## 🔴 `verify-screen` was nondeterministic, and it looked fine most of the time
|
||
|
||
Running the full set after the plate fix, two rows had moved off the committed
|
||
baseline. One of them was not a regression at all — it was the harness.
|
||
|
||
`press_start` returned `over3` **5021, 8919, 5021** on three identical runs. The
|
||
plate's looping focus record takes its phase from `time_units`, which free-runs,
|
||
so the captured frame lands wherever the grab happened to fall — while the
|
||
reference renderer cannot pulse at all. **A detector that answers differently
|
||
each run is worse than one that fails**: it teaches its reader to ignore it.
|
||
|
||
⚠️ **The port is not the thing that was wrong.** A thing that pulses does not stop
|
||
because the screen has arrived, and the pulse is measured. What was wrong was
|
||
comparing a moving frame against a static one and calling the difference a
|
||
regression. So `ScreenView.loop_phase_units` pins the phase, negative means
|
||
free-running, that stays the default everywhere, and only the harness passes
|
||
`--loop-phase=0`.
|
||
|
||
✅ Controlled, and the control is what makes the fix trustworthy:
|
||
|
||
| | |
|
||
|---|---|
|
||
| pinned, 3 runs | **identical md5** |
|
||
| free-running, 4 runs | 3 identical, **1 different** |
|
||
|
||
🟡 That 3-of-4 is the finding worth keeping. **It is usually stable**, which is
|
||
exactly why it survived — a flake that fires one run in four reads as a real
|
||
regression that "went away", and a `--loop-phase` that changed nothing would have
|
||
been indistinguishable from a fix without that negative control.
|
||
|
||
✅ With the phase pinned, `press_start` reads **max 1 / over3 0 OK** — *the
|
||
recorded baseline exactly*, not some new number. Fifteen of sixteen rows now
|
||
match the committed baseline.
|
||
|
||
### The sixteenth: `title_jp` has genuinely drifted, and I cannot say which side
|
||
|
||
| | max | over3 |
|
||
|---|---|---|
|
||
| committed baseline | 155 | 20 498 |
|
||
| now | **233** | **61 208** |
|
||
|
||
What is established:
|
||
|
||
* ✅ **deterministic** — 233 / 61 208 twice, so not the phase.
|
||
* ✅ **not the reference** — the Decoder reports `screen render` is byte-identical
|
||
across the stale and rebuilt binaries (max per-channel 0), so the reference is
|
||
stable and the movement is on the Godot side.
|
||
* ✅ **localized** — the differing region is a single **350×396 block at
|
||
(405, 74)**, the logo stack. `title` is untouched at max 6 / over3 790, which
|
||
rules out anything shared by both title screens (the forced-backdrop rule
|
||
among them).
|
||
* The port draws `ptlogo_jp`, `ptlogo3a/b/c` and the five `ptlogo_back2eff*`
|
||
layers here that are transparent at rest on the English title.
|
||
|
||
🔴 **What I cannot do is say which renderer is right.** There is no capture of the
|
||
Japanese title in the corpus, and this script's own header is explicit that
|
||
agreement with the reference is not correctness and a `DIFFERS` is not
|
||
automatically the port's fault. Guessing a direction here is precisely the move
|
||
the mission forbids. Asked, not resolved.
|
||
|
||
## The JP title capture adjudicates `title_jp` — and it goes against the port
|
||
|
||
The Decoder captured the Japanese title at rest (`310bf86`) and deliberately did
|
||
**not** compare it to either renderer, so that my diff and theirs stay
|
||
independent. This is the oracle for the block I could not adjudicate.
|
||
|
||
### Aligning it, because the last capture's geometry did not transfer
|
||
|
||
Their submenu capture had the game surface at y=45 in a 1280×720 frame. I did not
|
||
assume that here — I recovered the alignment by row/column profile correlation,
|
||
with the English pair as a control:
|
||
|
||
| | dy | dx |
|
||
|---|---|---|
|
||
| **control** — English capture (1279×675) vs port | **0** (r 0.994) | **0** (r 0.977) |
|
||
| JP capture (1280×720) vs port | **−45** (r 0.927) | −1 |
|
||
|
||
✅ The control lands on (0,0) as it must, and the JP offset comes out at their
|
||
stated 45 **as a measurement rather than an inheritance**. My first look at the
|
||
frame said "no letterbox, content spans all 720 rows" — true, and irrelevant: the
|
||
surface is offset inside content that extends past it.
|
||
|
||
### The instrument is fair, and then the verdict
|
||
|
||
Comparing the capture against **both** renderers in the disputed 350×396 block at
|
||
(405,74), and against a control strip where the two renderers agree:
|
||
|
||
| region | vs port | vs reference | closer |
|
||
|---|---|---|---|
|
||
| **control strip** | r +0.9751 | r +0.9756 | tie — ✅ instrument is fair |
|
||
| **disputed block** | r +0.7462 | **r +0.8727** | **REFERENCE** |
|
||
|
||
🔴 **The port moved, and it moved away from the game.** The verdict is stable
|
||
under gamma compensation at both measured title gammas (raw / 1.34 / 1.49 →
|
||
reference every time), so it is not an artefact of the known capture gamma floor.
|
||
The port puts light on **25.6 %** of the block that the capture does not have,
|
||
against the reference's 15.9 % — it is drawing too much, not too little.
|
||
|
||
⚠️ **This is the opposite of what I expected.** The Decoder's description — a
|
||
crystalline burst behind the wordmark, the `ptlogo3a/b/c` + `ptlogo_back2eff*`
|
||
stack that English holds transparent at rest — reads as confirmation that the
|
||
port's extra layers are right. They are not: the burst is there, and the port
|
||
draws *more* of it than the game does. **A qualitative match on "is the effect
|
||
present" was about to stand in for a measurement of how much.**
|
||
|
||
### What is not settled: which change did it
|
||
|
||
Both renderers draw this screen at `rest`, t=10 units, so the settle-window logic
|
||
is not in play — the two decoders disagree about the **rest pose of the JP effect
|
||
stack itself**. Four commits this session touched that path (the forced-backdrop
|
||
rule, per-instant coverage, the looping record, the sweep/hold work) and I have
|
||
not bisected them. Naming one now would be a guess dressed as a cause.
|
||
|
||
📌 What this does settle: `title_jp`'s `DIFFERS` is **the port's**, not the
|
||
reference's. That reverses this script's usual presumption, and it is the first
|
||
row in the baseline whose direction has ever been established against a capture.
|
||
|
||
## 🔴 CORRECTION: the port did not move away from the game — I scored the wrong frame
|
||
|
||
The previous entry concluded, from the JP title capture, that *"the port moved,
|
||
and it moved away from the game"*. **That conclusion is withdrawn.** It is wrong,
|
||
and the way it was wrong is worth more than the answer.
|
||
|
||
I scored `verify-screen`'s `title_jp` frame against the oracle. That frame is
|
||
posed `--pose=rest`, which this port **does not ship**. Posed as it actually
|
||
runs:
|
||
|
||
| | disputed block | whole surface |
|
||
|---|---|---|
|
||
| port, `--pose=rest` (the frame I scored) | +0.7462 | — |
|
||
| **port, as shipped** | **+0.9994** | **+0.9652** |
|
||
| reference | +0.8727 | +0.9200 |
|
||
|
||
✅ Holds under gamma compensation (+0.9928 at γ=1.34) and ✅ on the **English
|
||
control**, same method: port +0.9946 against the reference's +0.9560. The port is
|
||
closer to the game than the reference on **both** title screens.
|
||
|
||
### Why `rest` produces a frame the game never shows
|
||
|
||
`ptlogo_back2eff1` on the JP title is `(t, alpha) = (0,0) (98,0) (100,255)
|
||
(102,255) (104,0)` — a **4-unit sparkle**, and its `rest.t` is **100: the peak of
|
||
its own flash**. Six of these stagger across the logo. Posing at `rest` fires
|
||
every sparkle simultaneously at full brightness, which is exactly the "port puts
|
||
light on 25.6 % of the block the capture does not have" I reported as a defect.
|
||
The excess light was real; it was in a frame nobody sees.
|
||
|
||
⚠️ `verify-screen` is not at fault — it poses `rest` **deliberately**, because
|
||
both renderers read `rest` through one decoder and that is what makes it a
|
||
consistency check. Its header said so. **I used a consistency-check frame to
|
||
answer a correctness question**, and the tool now says in its own header that its
|
||
frames must never be scored against a capture.
|
||
|
||
### A second, smaller thing in that entry was also wrong
|
||
|
||
It said the port draws layers "that are transparent at rest on the English
|
||
title". Both screens draw them under `--pose=rest`. I had compared a `--menu`
|
||
run's log (timeline pose) against a `verify-screen` log (rest pose) and read the
|
||
difference as a property of the screens rather than of the two modes.
|
||
|
||
### What actually stands from that entry
|
||
|
||
The alignment work survives intact — the measured dy=−45 with the English control
|
||
at (0,0), and the observation that the instrument is fair on a control strip. So
|
||
does the arithmetic. **What failed was choosing which frame to feed it**, and no
|
||
amount of control on the comparison could have caught that: every control I ran
|
||
was a control on the *metric*, and the error was upstream in the *input*.
|
||
|
||
📌 The generalisation: **a control proves the instrument, not the sample.** Both
|
||
of my last two iterations' errors were of that shape — a live reader pointed at
|
||
the wrong field name, and a fair metric pointed at the wrong frame.
|
||
|
||
### Wired so it cannot recur
|
||
|
||
`tools/verify-capture` takes a fifth per-row field, a capture crop, because this
|
||
capture is a full 1280×720 display frame with the surface at +0+45 while every
|
||
other capture in that directory is pre-cropped to 1279×675 — comparing it whole
|
||
would score the port against a 45 px shift. With it, `title_jp` reads **RMSE
|
||
20.91, differing region 1.04 %**, beside `title`'s 14.16 / 0.21 %.
|
||
|
||
⚠️ The row prints `no capture` until the Decoder's branch merges. Their capture is
|
||
theirs to commit; it was staged locally to test the row and removed.
|
||
|
||
## The `rest()` flash defect reaches four screens I ship — and the port already survives it
|
||
|
||
The Decoder censused it from the file side while I was looking at one instance:
|
||
of 13 991 elements with ≥2 keyframes, **2 305** have no plateau so the dwell
|
||
fallback decides, and **1 697 (74 %)** of those get a *visible* pose. In
|
||
`GP_TITLE`, 5 fires and 4 are visible — **all four on the splash screens this
|
||
port ships**.
|
||
|
||
✅ Confirmed in my own export, and it is exactly the JP-title shape on different
|
||
screens:
|
||
|
||
| element | keyframes | `rest` |
|
||
|---|---|---|
|
||
| `palogo_sqex_eff` | `0:a0 15:a255 30:a212 45:a0` | t=30, **a=212** |
|
||
| `palogo_anima_eff` | `0:a0 15:a255 30:a212 45:a0` | t=30, **a=212** |
|
||
| `palogo_gamearts_eff` | `0:a0 15:a255 **30:a255** 45:a0` | **t=15, a=255** |
|
||
| `palogo_seta_eff` | `0:a0 15:a255 **30:a255** 45:a0` | **t=15, a=255** |
|
||
|
||
📌 **A refinement to their description**, which named the `212` shape: two of the
|
||
four hold **255 through t=30**, so their fallback lands on the flash's *peak*
|
||
rather than its decay. Same defect, worse pose — full brightness, not
|
||
four-fifths. The logos themselves (`palogo_sqex` holds 255 from t=30 to t=235)
|
||
have a real plateau and are unaffected.
|
||
|
||
### The port ships the right frame, and now there is a number for it
|
||
|
||
Both poses of the publisher splash against the **committed oracle capture**:
|
||
|
||
| pose | RMSE | differing |
|
||
|---|---|---|
|
||
| **timeline — what the port ships** | **2.17** | **0.01 %** |
|
||
| `--pose=rest` — the harness frame | 9.05 | 0.75 % |
|
||
|
||
🔴 **75× the differing area on a screen this port ships.** So the rule I wrote
|
||
into `verify-screen`'s header after getting it wrong on `title_jp` is not a
|
||
special case — it generalises, and here it is demonstrated against an oracle
|
||
rather than argued.
|
||
|
||
✅ The port's settled pose evaluates `pose_at(hold)`, not `rest`, so it skips the
|
||
flashes and agrees with the capture at 0.01 %. The defect is confined to the
|
||
harness pose. **Nothing shipped is wrong; nothing needed fixing in the render.**
|
||
|
||
### What did need fixing: the port said "at rest" about a pose it never looked at
|
||
|
||
`ScreenView` logged `"%s (transparent at rest)"` for every skipped element,
|
||
whatever instant it had posed. On the timeline path the pose is
|
||
`pose_at(time_units)` — so it reported `palogo_sqex_eff (transparent at rest)`
|
||
about an element whose **resting alpha is 212**.
|
||
|
||
⚠️ That is not cosmetic. The rest-versus-posed-instant confusion is precisely what
|
||
made me score a `--pose=rest` frame against a capture and write up a drift that
|
||
did not exist. A log line that erases the distinction is that error pre-printed,
|
||
waiting to be believed. It now names the instant: `transparent at t=6`.
|
||
|
||
Controlled both ways on one screen: timeline → `transparent at t=6` and the flash
|
||
skipped; `--pose=rest` → still `at rest`, and the flash **drawn**.
|
||
|
||
## Correction: those two are the *sound* path, which makes the rule stronger
|
||
|
||
The Decoder refuted my refinement, and it is a correction I would rather have than
|
||
the credit. I wrote that `palogo_gamearts_eff` / `palogo_seta_eff` show "the same
|
||
defect, worse pose — their fallback lands on the flash's peak". **Wrong on the
|
||
mechanism.** They hold `a=255` at identical x, y *and scale* from t=15 to t=30 —
|
||
a genuine plateau at pair index 1, which `rest_plateau()` handles, and t=15 is
|
||
the **correct** answer for that path. They are not among their census's four.
|
||
|
||
🔴 **And the consequence runs the other way from a retraction.** My rest pose for
|
||
them really is the flash's peak, reached by the **sound** path. So *"a rest render
|
||
is not a frame to score against a capture"* does **not** depend on the fallback
|
||
being unsound: **a plateau can itself be the held peak of a transient.** The
|
||
2 305 / 1 697 census *understates* the exposure rather than bounding it.
|
||
|
||
### Censusing my own tree — and the first answer was wrong
|
||
|
||
I asked how many elements I ship whose `rest` is visible but whose visibility is
|
||
transient. First pass keyed "transient" on the element's own visible span, and
|
||
returned **28 across 12 of 16 screens** — a plausible-looking number.
|
||
|
||
🔴 It was wrong, and what caught it was the check the Decoder and I just agreed
|
||
on: **say what the number means physically.** The list included `ptmsg` — the main
|
||
menu's own `⊙ Select Ⓐ OK` footer — as "visible 2 of 64 units", and `ptbtn00`,
|
||
the `PRESS Ⓐ` plate. Those are on screen the whole time the game sits there. The
|
||
story collapses on contact.
|
||
|
||
The cause: `ptmsg` is `[0:a0 44:a0 56:a255 58:a255 64:a0]`, and that final zero is
|
||
the **screen's exit ramp**, which *every* element has. I had counted the exit as
|
||
the end of visibility, so every normal element looked like a flash. No control
|
||
would have caught this — the arithmetic was right.
|
||
|
||
✅ Re-keyed on the **screen's** span rather than the element's: a transient is
|
||
gone while the screen is still up.
|
||
|
||
| | |
|
||
|---|---|
|
||
| elements whose `rest` shows what the settled screen does not | **31** |
|
||
| screens affected | **8 of 16** |
|
||
|
||
Every entry now has a coherent story — `*eff*` and `*loop*` sparkles, plus the two
|
||
loading screens — and `ptmsg`/`ptbtn00` fall out on their own, which is the check
|
||
passing rather than being applied by hand.
|
||
|
||
📌 **My exposure is twice what the splash finding suggested.** Not four screens,
|
||
eight — both titles, both splashes and their region twins, and both loading
|
||
screens. ✅ None of it reaches shipped output: the port poses `pose_at(hold)` and
|
||
agrees with every capture it has. The number bounds what would break the day
|
||
anything scored a `rest` frame against an oracle.
|
||
|
||
## The two loading screens are no longer black, and it was the paint order
|
||
|
||
`verify-screen`'s header has carried, since P1, that `build_12` and `build_15`
|
||
*"render as pure black in BOTH renderers, mean 0 and max 0"*, with an open
|
||
question: *"whether that is the port's bug or the decoders' reading of `rest`"*.
|
||
|
||
✅ **Both halves are now settled, and the answer is neither.** Measured today:
|
||
|
||
| | max | mean |
|
||
|---|---|---|
|
||
| port | 214.5 | 1.949 |
|
||
| reference | 214.5 | 1.918 |
|
||
|
||
Not blank, on either side, and the two agree — the rows read `OK` on a real
|
||
comparison rather than on nothing-against-nothing.
|
||
|
||
🔴 **It was the paint order, not `rest`.** My own earlier measurement had already
|
||
answered it and I had not connected the two: removing the forced-backdrop pass
|
||
makes these screens' first element `pgloading_loop5`, *"and the black screen
|
||
returns"*. `pgloading_eff00` is the full-frame opaque untextured quad, and it
|
||
carries `layer: null`, `layer_source: none` — the only elements in the export with
|
||
neither a read nor an implied key. Its position rests entirely on the occlusion
|
||
constraint. The rule that fixed it is the one the Decoder supplied and I
|
||
implemented this session.
|
||
|
||
⚠️ **The guard stays and the stale paragraph stays with it**, marked as history.
|
||
It was correct when written — two of sixteen rows were reporting this script's
|
||
strongest verdict for comparing nothing against nothing — and a guard that stops
|
||
firing is exactly the kind that rots out of a tool. A reader who hits a blank pair
|
||
tomorrow needs the reasoning, not just the verdict.
|
||
|
||
### Refutation attempt: does the Decoder's census miss my title screens?
|
||
|
||
Their `GP_TITLE` census is *"5 fires, 4 visible, all four on the splash screens"*.
|
||
My own census found six transient `ptlogo_back2eff*` elements on `title` and seven
|
||
on `title_jp` — also `GP_TITLE` builds. If those were fallback fires, their count
|
||
of four would be wrong.
|
||
|
||
✅ **Their claim survives.** All six reach `rest` by the **plateau** path — alpha
|
||
255→255 with identical `pos` *and* `scale` across the pair — so `rest_plateau()`
|
||
handles them and the dwell fallback never runs. They are not fires.
|
||
|
||
📌 Which is their own point back at them, now with my screens as evidence: my
|
||
census counts a **superset spanning both paths**, and the difference between the
|
||
two numbers is not disagreement but scope. A plateau that happens to sit on a
|
||
transient's peak is invisible to a fallback census and still produces a rest frame
|
||
the game never shows.
|
||
|
||
### A proposed sharpening of the census, tested and rejected
|
||
|
||
The Decoder's surviving number rests on a structural fact: the dwell fallback runs
|
||
only when no two adjacent poses are equal, so **every pose it returns is un-held
|
||
by construction**, and no threshold is needed. That is clean, and the obvious move
|
||
was to borrow it — replace my "gone before 60 % of the screen" cutoff with *how
|
||
long the rest pose is held*, which would drop the arbitrary threshold.
|
||
|
||
🔴 **It fails my own control.**
|
||
|
||
| element | held | of screen | flagged? |
|
||
|---|---|---|---|
|
||
| `ptmsg` — the main menu's footer | 2 units | 80 (2.5 %) | **yes** ❌ |
|
||
| `ptbtn00` — the `PRESS Ⓐ` plate | 2 units | 244 (0.8 %) | **yes** ❌ |
|
||
| `ptlogo_back2eff1` — a real sparkle | 2 units | 269 (0.7 %) | yes ✅ |
|
||
|
||
All three sit on a **2-unit plateau**. Hold duration cannot separate them, and the
|
||
two it gets wrong are the exact pair whose absurdity caught my first census.
|
||
|
||
✅ **Why the criterion does not transfer.** On the fallback path nothing is held,
|
||
so "un-held" *is* the defect. On the plateau path the plateau is real — what
|
||
distinguishes a footer from a sparkle is **where it sits relative to the screen's
|
||
end**: `ptmsg`'s 2-unit plateau is the last pose before the exit ramp, so the port
|
||
holds it past the end and the game shows it throughout; the sparkle's identical
|
||
2-unit plateau is followed by a return to zero *while the screen is still up*.
|
||
|
||
📌 So the screen-span criterion stays, threshold and all. **A cleaner definition
|
||
that fails a control is worse than an ugly one that passes** — and I would have
|
||
adopted this on its elegance if the control pair had not already been sitting
|
||
there from the earlier mistake.
|
||
|
||
## Adjudicating the Decoder's `rest()` replacement against the game
|
||
|
||
They proposed posing every element at the **screen's** settle instant instead of
|
||
asking each element for its own resting pose, found their own control could not
|
||
validate it — *"a candidate cannot be adjudicated against the incumbent it is
|
||
meant to replace"* — and said the oracle number is what decides. It is, and I had
|
||
only ever run it on one screen. Running it on every capture-backed screen:
|
||
|
||
| screen | candidate (settled) | incumbent (`rest`) | |
|
||
|---|---|---|---|
|
||
| `title` | **0.21 %** | 1.82 % | candidate |
|
||
| `publisher_logo` | **0.01 %** | 0.75 % | candidate |
|
||
| `developer_logos` | **0.01 %** | 0.33 % | candidate |
|
||
| `main_menu` | 0.07 % | 0.25 % | ⚠️ **confounded** |
|
||
| `extras` | 0.19 % | 0.46 % | ⚠️ **confounded** |
|
||
|
||
✅ Three screens adjudicate cleanly and all three favour the candidate, by 9× to
|
||
75×. The settled figures are corroborated: they match `verify-capture`'s
|
||
independently recorded numbers to the digit.
|
||
|
||
### 🔴 Two of the five rows are not evidence, and my first table said they were
|
||
|
||
My first run had `main_menu` at **3.29 %** for the candidate — losing to the
|
||
incumbent by 13×, the opposite direction from everything else. That had no
|
||
plausible story, which is what made me look.
|
||
|
||
`--screen=` shoots the frame immediately: the "settled" main_menu drew **6 of 16**
|
||
elements and skipped `ptframe1`, `ptframe2` and `ptmsg` as *"transparent at t=9"*.
|
||
It was a mid-build-in frame. **The same wrong-frame error as `title_jp`, caught
|
||
before publishing this time and only because the number's direction made no
|
||
sense.** Properly posed via `--menu --script=wait`, it is 0.07 %.
|
||
|
||
⚠️ **But that fix introduces a confound, and it is fatal to those two rows.** The
|
||
only way to pose these screens settled is `--menu`, which also draws the **focus
|
||
record**; the `rest` column is rendered by `--screen`, which draws none. This
|
||
tool's own header records that difference: main_menu without focus is 2 159
|
||
differing pixels — **0.234 %** — against 531 with it. My incumbent figure is
|
||
0.25 %. *The entire gap on those two rows is the focus record, not the pose.*
|
||
|
||
So they stay in the table marked confounded rather than counted. **A 5–0 result
|
||
was available by not looking.**
|
||
|
||
### What this does and does not settle
|
||
|
||
✅ The candidate is better on every screen where the question can be asked
|
||
cleanly, against the game rather than against the incumbent — which is the
|
||
adjudication their failed control could not provide.
|
||
|
||
⚠️ It does **not** validate their implementation. I tested the port's settled pose,
|
||
not `UiBuild::settle_time()`; the two agree in *direction*, and whether they agree
|
||
in value is unmeasured. And three screens are three screens.
|
||
|
||
📌 They are right not to change `rest()` on this. I pin their crate, nothing I
|
||
ship reads `rest`, and a proposal whose evidence comes entirely from the consumer
|
||
has no business landing in the dependency on that basis alone.
|
||
|
||
## The boot's own end frame, scored against the game for the first time
|
||
|
||
`--boot --capture=` used to write **no file**: `_finish_boot()` was reachable only
|
||
from the overlay-quit branch, and the boot quit first because that branch fires
|
||
when `_overlay_spec.is_empty()` — which it is the instant the overlay is raised.
|
||
✅ Fixed by the `_overlay_quit_at < 0.0` guard added earlier this session. The
|
||
defect entry above is left standing with a pointer here, because the reasoning is
|
||
what makes the guard legible.
|
||
|
||
⚠️ I fixed it and never went back to check what it made possible. **The whole
|
||
P3/P7 artifact — the boot running unattended and photographing its own end
|
||
state — has been available for hours and unused.**
|
||
|
||
### What it shows
|
||
|
||
| | RMSE | differing |
|
||
|---|---|---|
|
||
| **boot's own end frame**, real sequence, unattended | **12.80** | **0.00 %** |
|
||
| `title_plate`, synthetically posed at `--time=3.95` | 12.83 | 0.00 % |
|
||
|
||
✅ **Zero pixels over the threshold against the game.** The residual RMSE is the
|
||
known capture gamma floor, which every row on this corpus carries and which is not
|
||
a target.
|
||
|
||
📌 **And the two agree to 0.03 RMSE.** That is the more useful half. `verify-capture`
|
||
reaches this frame by a *shortcut* — `--screen=title --overlay=press_start
|
||
--time=3.95` — rather than by booting. The shortcut has been trusted since it was
|
||
written and never tested against the thing it stands in for. It is faithful:
|
||
posing the composite directly and arriving there through publisher logo →
|
||
developer logos → `ADV.wmv` → title → plate land on the same frame.
|
||
|
||
⚠️ What this does **not** show is that the intervening sequence is right. It is one
|
||
frame, the last one; the boot could take a wrong path and still end correctly.
|
||
`--shots` walks the sequence, and comparing those against captures needs captures
|
||
of the intermediate states, which the corpus does not have for the video handover.
|
||
|
||
## Refutation attempt: the settle-instant candidate is **not** uniformly better
|
||
|
||
Their symmetry-breaking property attributes every `rest()` disagreement to the
|
||
chosen plateau not covering the settle instant. That is testable from my side on
|
||
the two screens my earlier table had to mark **confounded** — and it turns up a
|
||
screen class where their candidate is *worse*.
|
||
|
||
🔴 **At `main_menu`'s settle instant the footer is half-drawn.**
|
||
|
||
| screen | settle window | settle instant | `ptmsg` alpha there |
|
||
|---|---|---|---|
|
||
| `main_menu` | [44, 56] | **50** | **127.5** of 255 |
|
||
| `extras` | [38, 50] | **44** | **127.5** of 255 |
|
||
|
||
`ptmsg` is `[0:a0 44:a0 56:a255 58:a255 64:a0]` — it finishes arriving at t=56,
|
||
*after* the settle window has closed at 56 and well after its midpoint at 50. The
|
||
settle instant is the midpoint of the longest keyframe-free interval, and on both
|
||
menus **that interval ends exactly as the footer starts to arrive.**
|
||
|
||
Measured against the game, with focus absent from both renders so the confound
|
||
that voided my earlier rows cancels:
|
||
|
||
| pose | RMSE | differing |
|
||
|---|---|---|
|
||
| t=50 — the screen's settle instant | 14.98 | **0.30 %** |
|
||
| t=58 — the footer's own hold | 14.83 | **0.25 %** |
|
||
|
||
⚠️ **Small, and real.** This does not overturn their proposal — it wins by 9× to
|
||
75× on `title` and both splashes, and by 4.6× on their own `title` measurement.
|
||
What it shows is that "pose everything at the screen's settle instant" has a
|
||
**failure mode of its own**: an element that arrives after the settle window
|
||
closes is caught mid-fade. The port's hold-based pose does not have it, because it
|
||
parks each element at its own hold.
|
||
|
||
📌 So the honest summary of this whole thread is narrower than either of us has
|
||
been writing: **the screen-settle pose beats per-element `rest()` decisively where
|
||
`rest()` returns a transient's peak, and loses slightly where an element arrives
|
||
late.** Both are approximations to a settled screen; neither is the settled screen.
|
||
|
||
### And a unit bug, caught by two identical numbers
|
||
|
||
My first run of this asked for `--time=50` meaning 50 *units* and got **50
|
||
seconds** — 3000 units, past the end of everything. Both poses returned RMSE 56.02
|
||
/ 9.15 %, **identical to two decimal places**, which is what made me look. Two
|
||
different inputs producing exactly the same output is not a result; it is the
|
||
instrument saying it ignored the input. Same tell as the two `--script` runs that
|
||
came out bit-identical because the capture fired before the first press.
|
||
|
||
## My own predictor holds — and the evidence that made it *better than width* does not
|
||
|
||
The Decoder censused my `ptmsg` failure mode disc-wide (25.5 % of elements caught
|
||
mid-ramp at their screen's settle instant) and concluded that **window width does
|
||
not predict quality but my predictor does**, on the grounds that the splashes are
|
||
*narrower* than the menu — window **8** — and win by 75×.
|
||
|
||
✅ **The predictor itself holds, 5 of 5** on my capture-backed screens:
|
||
|
||
| screen | mid-ramp at settle | measured outcome |
|
||
|---|---|---|
|
||
| `title` | **0** | settle wins 9× |
|
||
| `publisher_logo` | **0** | settle wins 75× |
|
||
| `developer_logos` | **0** | settle wins 33× |
|
||
| `main_menu` | **2** (`ptmsg`, `pteff10`) | settle **loses** |
|
||
| `extras` | **2** (`ptmsg2`, `pteff20`) | settle **loses** |
|
||
|
||
🔴 **But their window figure for the splashes disagrees with my export by 20×**, and
|
||
that figure is the whole of the argument.
|
||
|
||
| screen | their window | mine |
|
||
|---|---|---|
|
||
| `title` | 76 | **76** ✅ |
|
||
| `main_menu` | 12 | **12** ✅ |
|
||
| `publisher_logo` | 8 | **190** ❌ |
|
||
| `developer_logos` | 8 | **145** ❌ |
|
||
|
||
Recomputed independently from the raw top-level keyframe times rather than read
|
||
off my own `settle_window` field: `publisher_logo`'s times are
|
||
`[0, 15, 30, 45, 235, 239, 251, 255]`, whose widest keyframe-free gap is
|
||
**45 → 235 = 190**. `developer_logos` gives `45 → 190 = 145`. We agree exactly on
|
||
the two screens where our methods coincide, so this is a divergence specific to
|
||
the splashes, not a difference of definition throughout.
|
||
|
||
### Why this matters more than a corrected number
|
||
|
||
**The splashes are the *widest* of my five, not the narrowest.** With that, the
|
||
data reads:
|
||
|
||
| screen | window | mid-ramp | outcome |
|
||
|---|---|---|---|
|
||
| `publisher_logo` | 190 | 0 | wins 75× |
|
||
| `developer_logos` | 145 | 0 | wins 33× |
|
||
| `title` | 76 | 0 | wins 9× |
|
||
| `main_menu` | 12 | 2 | loses |
|
||
| `extras` | 12 | 2 | loses |
|
||
|
||
🔴 **Width and mid-ramp now predict identically and are perfectly confounded.** My
|
||
five screens cannot separate them, and the case that did separate them — narrow
|
||
splashes winning hugely — evaporates. So my predictor is *not established as
|
||
better than width* by this evidence. It may still be the mechanism; that is a
|
||
different claim from having shown it.
|
||
|
||
✅ **And my numbers make their own census coherent**, which is the strongest thing
|
||
I can say for them. Their buckets run 40.9 % mid-ramp on windows under 10 and
|
||
11.7 % on wide ones. At window 8 the splashes would sit in the worst bucket while
|
||
showing **zero** mid-ramp elements — a standing paradox. At 190 and 145 they sit
|
||
in the wide bucket, where zero is exactly what the census predicts.
|
||
|
||
⚠️ I am not claiming their tool is broken; `--settle` may report a different
|
||
quantity than the widest keyframe-free gap. But one of the two readings is wrong,
|
||
and until it is settled the width hypothesis is **not** refuted.
|
||
|
||
## Checking my own tree for the ordinal foot-gun that just voided three of theirs
|
||
|
||
The Decoder retracted three claims: `screen render --build N` takes a **build
|
||
ordinal**, `screen list` maps `[10] → entry 12` and `[11] → entry 15`, and the
|
||
splashes are entries 10 and 11 — so their splash rows had rendered the **loading
|
||
screens** against splash captures. My own HANDOFF entry warned that an
|
||
ordinal-keyed 10/11 names the splashes as loading screens *"and everything still
|
||
validates"*, and it did.
|
||
|
||
⚠️ `tools/port/verify-screen`'s header claims `--all` protects me from exactly
|
||
this. **A comment claiming protection is what just failed on their side**, so I
|
||
checked rather than cited it.
|
||
|
||
| | RMSE |
|
||
|---|---|
|
||
| my CLI reference for build 10 vs the **publisher** splash capture | **8.97** ✅ |
|
||
| my CLI reference for build 11 vs the **developer** splash capture | **8.77** ✅ |
|
||
| cross-control — publisher reference vs **developer** capture | **48.17** |
|
||
|
||
✅ Both references are the screens they claim to be, and the cross-control is 5.4×
|
||
worse, so the discriminator has teeth rather than passing everything. My `--all`
|
||
addressing is correct, and now measured rather than asserted.
|
||
|
||
📌 Worth naming why this was worth ten minutes: the port's numbers for these two
|
||
screens (0.01 % differing) are among the strongest evidence in the corpus, and
|
||
they are cited in the `rest()` adjudication that a proposal against a pinned crate
|
||
now rests on. **Evidence that strong is exactly what you check after finding the
|
||
same class of error next door** — the failure mode is silent by construction, and
|
||
their instrument reported a railed gamma fit rather than a wrong screen.
|
||
|
||
### What survives of the settle-window disagreement
|
||
|
||
Their retraction confirms my reading: 190 and 145, matching my recomputation from
|
||
raw keyframe times exactly. Their library was never wrong, only the invocation.
|
||
|
||
So the position stands where my last entry left it, and no further: **width and
|
||
mid-ramp are perfectly confounded across every screen either of us has measured.**
|
||
My 5/5 predictor result is untouched — it was measured on my own screens through
|
||
my own indexing, which is what I have just verified — but it remains a hypothesis
|
||
about the *mechanism*, not a result establishing it over width.
|
||
|
||
## Looking for a case that separates width from mid-ramp — there is none, and I nearly invented one
|
||
|
||
Width and mid-ramp predicted identically across my five capture-backed screens, so
|
||
the useful question was whether any of my **sixteen** breaks the tie: a wide window
|
||
*with* a mid-ramp element, or a narrow one without. Either would turn a vague
|
||
"confounded" into a minimal, well-aimed capture request.
|
||
|
||
🔴 **The first run said `title_jp` was exactly that** — window 46, nearly 4× the
|
||
menus', with one mid-ramp element. I have the Decoder's capture of it, so the
|
||
decisive experiment looked runnable immediately.
|
||
|
||
It was wrong. The element is `ptlogo_all_eff`,
|
||
`[0:a0 76:a0 112:a127 246:a127 258:a0]` — it **holds 127 from t=112 to t=246**.
|
||
That is its plateau, not a transition. **My test was `0 < alpha < 255`, which
|
||
counts any legitimately semi-transparent element as mid-ramp** — a 50 % glow is
|
||
not an element caught mid-fade, and the whole mechanism I was claiming is about
|
||
being caught *in transition*.
|
||
|
||
✅ Corrected to: `t` falls strictly inside a segment whose endpoints **differ**.
|
||
|
||
| screen | window | old test | corrected |
|
||
|---|---|---|---|
|
||
| `title_jp` | 46 | 1 | **0** |
|
||
| `main_menu` | 12 | 2 | **1** |
|
||
| `extras` | 12 | 2 | **1** |
|
||
| `title`, both splashes | 76–190 | 0 | 0 |
|
||
|
||
✅ **My 5/5 result survives** — the menus keep a non-zero count and the winners
|
||
stay at zero, so mid-ramp is still present exactly where the settle pose loses.
|
||
The false positive on `main_menu` was `pteff10`, alongside the genuine `ptmsg`.
|
||
|
||
🔴 **And there is no separating case anywhere in the export.** Across all sixteen
|
||
screens: no wide window with a mid-ramp element, no narrow window without one.
|
||
|
||
### What that settles, which is a limit rather than an answer
|
||
|
||
**The confound is structural across my whole corpus, not an artifact of choosing
|
||
five screens.** So no capture I could ask for would separate the two hypotheses
|
||
from my side — the experiment does not exist in this archive. Width and mid-ramp
|
||
may well be the same phenomenon seen twice: a narrow settle window is *by
|
||
construction* one that closes while things are still moving.
|
||
|
||
📌 The near-miss is the part worth keeping. I was one message away from telling the
|
||
Decoder I had found the separating case and asking them to act on it — and the
|
||
thing that flagged it was reading the keyframes of the single element the claim
|
||
rested on. **The screen most useful to me was the one I checked least.**
|
||
|
||
## Auditing my tree for the disc-wide ordinal foot-gun
|
||
|
||
The Decoder found the ordinal/entry divergence is disc-wide — 21 of 24
|
||
build-bearing archives, 18 diverging at ordinal 0 — and that **`GP_TITLE` is the
|
||
mildest case on the disc**, the only archive whose first ten ordinals are the
|
||
identity. That is the whole reason this corpus survived, and it is luck, not
|
||
design.
|
||
|
||
✅ **No exposure in my tree, checked rather than assumed.**
|
||
|
||
* The four archives they flag as exposed — `GP_READY_ROOM`, `GP_HANGAR_ARSENAL`,
|
||
`GP_MISSION_SELECT`, `GP_OPTIONS` — appear in `authored/flow.json` with **no
|
||
numbers at all**, only as *"not in this export"*. Nothing to misread.
|
||
* `authored/screen_names.json` already says **"LOCATED BY ENTRY INDEX, not by a
|
||
rule"** — the exact disambiguation their second warning asks for.
|
||
* Their second point (`--all` swaps the predicate, so `--build N` and
|
||
`--build N --all` are different objects) is what `verify-screen`'s header
|
||
already relies on, and every tool of mine passes `--all`.
|
||
|
||
### Verifying the high ordinals, where GP_TITLE's luck would run out first
|
||
|
||
The identity holds for the first ten ordinals. My export addresses **13** and
|
||
**14**, past that point, so the interesting test is up there:
|
||
|
||
| | RMSE | |
|
||
|---|---|---|
|
||
| `publisher_logo` (10) vs `publisher_logo_r` (13) | **3.06** | region twins — near-identical ✅ |
|
||
| `developer_logos` (11) vs `developer_logos_r` (14) | **4.33** | region twins — near-identical ✅ |
|
||
| `publisher_logo` (10) vs `developer_logos` (11) | **47.91** | different screens — control, 11–16× worse |
|
||
|
||
✅ `--build N --all` lands exactly where HANDOFF says entries 10/13 (publisher) and
|
||
11/14 (developer) are, across the full range where divergence could begin, and the
|
||
control shows the test would have caught a mismatch.
|
||
|
||
⚠️ **The constraint is recorded for whoever exports those four archives**, which is
|
||
not this port today: `--build 0` is not entry 0 in any of them. The current
|
||
absence of exposure is a fact about what I have exported, not a property of the
|
||
tooling.
|
||
|
||
### A precision correction to my own wording
|
||
|
||
They tried to refute my `ptlogo_all_eff` correction and could not — the quote is
|
||
exact and `a=127` holds flat across 134 units with position and scale constant.
|
||
|
||
⚠️ But they flag something I should not have said. I called it a *"50 % glow"*.
|
||
**What is measured is the plateau**; that it *is* a glow rests on kind `0x3000`
|
||
and a 200 % scale, and nobody has put that in front of the running game. The
|
||
correction to my mid-ramp test stands on the numbers alone and needs no reading of
|
||
what the element depicts — which is how it should have been written.
|
||
|
||
## Their withdrawn "~14 units of black hold" — my authored 9 survives it
|
||
|
||
Two warnings arrived. The first does not touch me: ✅ nothing of mine is authored
|
||
from `screen-transitions.md`'s 0.87 / 0.97 / 4.08 s fade-in spans, and **nothing
|
||
in this port reads keyframe times outside the crate** — the exporter reads them
|
||
through `sylpheed_formats`, and every analysis script I have reads
|
||
`export/*.json` downstream of it. Their `fade_quads.py` failure mode cannot occur
|
||
here by construction, which is the wall doing its job rather than luck.
|
||
|
||
The second is about a value I ship: `authored/timing.json` `black_hold_units: 9`.
|
||
They withdrew the "~14 units of hold" and warn that **authoring a hold puts a
|
||
sixth of a second of dead black into every transition the game does not have**.
|
||
|
||
### Testing their structural claim on my own export
|
||
|
||
*"Content elements start fading about six frames before the black quad's ramp
|
||
begins."* On `main_menu`, `pteff00` is `[0:a255 12:a0 70:a0 80:a255]` — its rise
|
||
to black runs **t=70 → 80**. The content fade-outs start at:
|
||
|
||
| element | starts | ends |
|
||
|---|---|---|
|
||
| `ptmsg` | **58** | 64 |
|
||
| `pteff10`, `pteff12` | 60 | 68 |
|
||
| `ptbtn05` | 60 | 64 |
|
||
|
||
✅ **12 units of lead — exactly six frames at 30 Hz**, matching their measurement
|
||
off the running game. Two independent routes, disc and capture, same number.
|
||
|
||
⚠️ One difference: they say the two **overlap**; in my export content is gone by
|
||
t=68 and the quad starts at t=70 — a 2-unit gap, not an overlap. That is one frame,
|
||
inside their stated ±1 frame per span, so I record it as agreement at their
|
||
resolution rather than as a discrepancy either of us can act on.
|
||
|
||
### The arithmetic, which is the part that matters
|
||
|
||
Their new figure: **total blackout 9 frames ≈ 0.30 s = 18 units**, gap between
|
||
screens one frame.
|
||
|
||
| | units |
|
||
|---|---|
|
||
| quad's ramp to black, from the file | 70 → 80 = **10** |
|
||
| my authored `black_hold_units` | **9** |
|
||
| total from ramp start to the next screen | **19** = 0.317 s |
|
||
| their measured blackout | **18** = 0.30 s |
|
||
|
||
✅ **One unit apart — inside their own resolution.** My authored 9 is *supported*
|
||
by the measurement that withdrew the 14, not refuted by it.
|
||
|
||
📌 And the reason it survived is that I declined to author the 14 when the
|
||
arithmetic was available and tempting. The ramp+hold decomposition I proposed gave
|
||
20–24 units and fit their old ~0.4 s at the top of the range; I wrote *"this is
|
||
arithmetic that fits, not a measurement"* and left the value where it had been
|
||
measured. **Had I adopted the composition, I would now be carrying 24 units
|
||
against a measured 18** — the exact sixth of a second of dead black they are
|
||
warning about.
|
||
|
||
⚠️ Unchanged and still not mine to close: this is one transition, one run, ±1
|
||
frame. I am not adjusting 9, and there is nothing here that would justify it.
|
||
|
||
## 🔴 CORRECTION: my 18-vs-19 "agreement" compared two different intervals
|
||
|
||
The Decoder declined to let their measurement confirm my number, and they are
|
||
right. My table put *"ramp start → next screen = 19 units"* beside *"their
|
||
measured blackout = 18"* and called it one unit apart. **Those are not the same
|
||
interval.** Theirs runs content-start → fully-black; mine runs ramp-start → next
|
||
screen. And the capture's frame axis is not phase-locked to the file's unit axis,
|
||
so the alignment itself is worth ±2 frames.
|
||
|
||
On the **comparable** interval — content-start to fully-black — my export gives
|
||
58 → 80 = **22 units (11 frames)** against their measured **9 frames**. Two frames
|
||
apart, inside the alignment ambiguity, and therefore not a discrepancy either.
|
||
|
||
🔴 **And `black_hold_units` is not in that interval at all.** Their measurement
|
||
ends where the hold begins. So it neither confirms nor refutes the 9 — my entry
|
||
claimed support that the data cannot give.
|
||
|
||
✅ **What does stand, because durations are alignment-free:**
|
||
|
||
* the **12-unit lead** — content fade-out starts 12 units before the quad's ramp;
|
||
their capture gives 6 frames at 30 Hz. Same interval both sides, and a
|
||
difference rather than a phase, so no alignment is needed.
|
||
* the **2-unit gap** — content gone at 68, quad at 70; their frames 39 → 40.
|
||
They have withdrawn "overlap" in favour of this.
|
||
|
||
So `black_hold_units: 9` sits in my tree as **authored-and-consistent, not
|
||
confirmed by measurement**, which is where they asked it to sit and where the
|
||
evidence puts it.
|
||
|
||
### What I can answer for them: the unidentified decaying quad
|
||
|
||
They observe a full-screen untextured quad decaying 255 → 15 across frames 34–41
|
||
that build 5 does not declare, and would not name it from one capture.
|
||
|
||
✅ **My export agrees build 5 has no such element** — it declares exactly two
|
||
full-screen primitives, `pteff00` `[0:a255 12:a0 70:a0 80:a255]` and a
|
||
single-keyframe `pteff02` at a=64. Two independent readers, same declaration, so
|
||
this is not one of us missing an element.
|
||
|
||
📌 **Hypothesis, offered as one: it is the *incoming* screen's `pteff00`.** Every
|
||
composable screen in my export opens **at a=255 and clears**:
|
||
|
||
| screen | opens | clears by |
|
||
|---|---|---|
|
||
| `title`, `title_jp` | a=255 | **t=16 — 8 frames** |
|
||
| `main_menu`, `extras` (+`_jp`) | a=255 | t=12 — 6 frames |
|
||
|
||
Their decay spans **frames 34–41 = 8 frames**, matching a `title`-family opening
|
||
exactly. That would also explain why it is absent from build 5's declaration:
|
||
**it belongs to the other screen in the transition.** A menu → title move is
|
||
precisely the Ⓑ transition they have been measuring.
|
||
|
||
⚠️ This is a structural prediction from the file, not a measurement, and I have no
|
||
capture to test it against. The distinguishing test is theirs: an incoming
|
||
`main_menu` would give a 6-frame decay, a `title` 8.
|
||
|
||
## `check-all` passes — after an hour-long hang that was the suite's own fault
|
||
|
||
✅ **Every asserting check passes**: format-validator, modding-rules,
|
||
capture-controls, menu-audio, decisions-index, refuted-claims. Oracle captures
|
||
report `main_menu` 0.06 %, `extras` 0.19 %, `main_menu_options` 0.15 %, `title`
|
||
0.21 %, `title_plate` **0.00 %**, `title_band` 0.35 %, both splashes 0.01 %.
|
||
`title_jp` reads `no capture` — the row is wired and waits for their branch.
|
||
`verify-screen` reports 2 DIFFERS, allowed for its stated reason.
|
||
|
||
⚠️ This is the first end-to-end pass I have actually seen. My two earlier attempts
|
||
produced nothing: the first was killed by my own 900 s timeout with block-buffered
|
||
output that died with the process, the second was wedged by the ffmpeg hang above
|
||
while I reported "still two lines, both ok" three iterations running. **I was
|
||
treating an absence of output as patience.**
|
||
|
||
## Ⓐ and Ⓑ are not the same shape, and my `black_hold` treats them as if they were
|
||
|
||
They ran the discriminating test — with the prediction written down first — and it
|
||
holds: incoming build 4 gives an **8-frame** decay, build 5 a **5-frame** one
|
||
against my predicted 6, direction measured and duration inside ±1.
|
||
|
||
✅ Their clinching tell reproduces from my export independently:
|
||
|
||
| | full-screen primitives |
|
||
|---|---|
|
||
| `main_menu` settled | `pteff00` a=0, `pteff02` a=64 → **[64]** |
|
||
| `title` opening | `pteff00` a=255, `pteff02` a=64 → **[255, 64]** |
|
||
|
||
Composite at the transition: **[64, 255, 64]** — exactly what they measured, and
|
||
no single element produces it.
|
||
|
||
### 🔴 The consequence, which neither of us predicted
|
||
|
||
* **Ⓐ title→menu** is sequential and has a real black interval — ~5 frames,
|
||
~10 units.
|
||
* **Ⓑ menu→title has no black interval at all.** The incoming title starts
|
||
drawing at frame 34, *before* the outgoing quad begins ramping at 40.
|
||
|
||
`boot.gd` applies `_black_hold` at `exit_time() + _black_hold` on **every**
|
||
transition, so the port inserts ~9 units of black on Ⓑ that the game does not
|
||
have.
|
||
|
||
⚠️ **I am not changing it, and the reason is their own warning.** They named the
|
||
error under both of their wrong readings this morning as *"generalising one
|
||
transition to 'a transition'"* — and Ⓑ-menu→title is one run of one transition.
|
||
Suppressing the hold on every cancel path would repeat exactly that. Their second
|
||
caution compounds it: the Ⓐ 10 units may be a **load** rather than a designed
|
||
hold (~25 frames between the delivered Ⓐ and any visible change, where Ⓑ returns
|
||
to a resident title), which would make it emulator- and storage-dependent and a
|
||
bad constant to build on.
|
||
|
||
📌 So the finding is recorded and the divergence is **known and stated** rather
|
||
than fixed: the port's transitions are uniform, the game's are directional, and I
|
||
do not yet have enough transitions measured to say what the rule is.
|
||
|
||
## 🔴 `check-all` excused two failing rows with a reason that is measurably false
|
||
|
||
The suite reported *"2 DIFFERS, allowed: the pin is not on main, so this compares
|
||
two decoder eras."* I have quoted that allowance for several iterations without
|
||
testing it.
|
||
|
||
**Tested.** Built `sylpheed-cli` at `formats-pin-2026-08-30` — the tag the
|
||
exporter is pinned to — and at the workspace HEAD, and rendered the same builds
|
||
through both:
|
||
|
||
| screen | pixels differing between the two eras |
|
||
|---|---|
|
||
| `title` | **0** |
|
||
| `title_jp` | **0** |
|
||
| `main_menu` | **0** |
|
||
|
||
✅ **Byte-identical**, despite **508 lines** of difference in `ui_layout.rs`
|
||
between the two revisions. The decoder eras are not the cause of anything here,
|
||
and the allowance was excusing a real signal with a wrong explanation — the worst
|
||
form, because it makes a genuine disagreement look accounted for.
|
||
|
||
🔴 **A second, independent defect in the same eight lines.** The allowance's expiry
|
||
tested `formats-pin-2026-08-29d` while `crates/sylpheed-export/Cargo.toml` pins
|
||
`formats-pin-2026-08-30`. So it would have expired on a tag this tree does not
|
||
use — silently, in either direction.
|
||
|
||
### What the rows actually are, both already documented elsewhere
|
||
|
||
* **`title`** — the `ptloop` **sweep phase** residual: max 6 / over3 790,
|
||
unchanged across every renderer change since P1.
|
||
* **`title_jp`** — the **`--pose=rest` sparkle handling**. Adjudicated against the
|
||
oracle: the port's *shipped* pose scores r **+0.9994** against the game where
|
||
the reference scores +0.8727, and `--pose=rest` is what this script compares.
|
||
**The port is closer to the game on the row the script calls a disagreement.**
|
||
|
||
### The replacement is a named set, not a count with an excuse
|
||
|
||
A count can only say *how many*; it cannot notice that a **different** screen
|
||
started drifting while the total stayed at two. The allowance now lists `title`
|
||
and `title_jp` by name, and a `DIFFERS` on anything else fails the run.
|
||
|
||
✅ Controlled in both directions, because a guard that cannot fail is not a guard:
|
||
|
||
| log | verdict |
|
||
|---|---|
|
||
| `title`, `title_jp` | passes |
|
||
| `title`, **`main_menu`** | **fails on `main_menu`** |
|
||
| **`extras`** alone | **fails on `extras`** |
|
||
| clean | passes |
|
||
|
||
The pin reminder survives as its own line, and now reads the tag **out of
|
||
`Cargo.toml`** so it cannot drift out of step with the dependency again.
|
||
|
||
📌 The pattern, and it is the third time this session: **an allowance is a claim.**
|
||
This one had been true once — the monorepo merge made the exporter and the
|
||
reference share a decoder, and the tag pin quietly unmade that — and it kept being
|
||
printed long after it stopped describing the tree.
|
||
|
||
## `black_hold_units` 9 → 0, and why not the value that fits best
|
||
|
||
The Decoder measured the black gap on **three** transitions off the running game.
|
||
It is not a constant:
|
||
|
||
| transition | measured gap | my hold of 9 |
|
||
|---|---|---|
|
||
| menu → title | **0 units** | +9 |
|
||
| title → menu | 4 | +5 |
|
||
| EXTRAS → menu | 6 | +3 |
|
||
|
||
🔴 **9 was outside the measured range entirely** — wrong for all three, by up to a
|
||
sixth of a second of black the game does not show. Their recommendation is to
|
||
treat it as unauthored rather than uniform-at-9.
|
||
|
||
⚠️ **I did not take the value that fits best.** A uniform **4** minimises total
|
||
error (6 units against 0's 10). That is fitting three samples, and a constant
|
||
chosen for its residual is exactly what this corpus keeps having to withdraw. **0
|
||
has a reason that is not a fit:** it adds no black the game does not have — the
|
||
same tie-breaker `input_during_transition` already uses in this file — and it is
|
||
measured-*correct* for one real transition. The error becomes a **missing** gap of
|
||
at most 6 units on two transitions rather than an **invented** one of up to 9 on
|
||
all three.
|
||
|
||
✅ The verified boot artifact survives: the end frame is still **0.0009 %**
|
||
differing against the oracle, unchanged within printing precision.
|
||
|
||
📌 And their EXTRAS run vindicates the refusal. "Ⓑ has no black" was one run of one
|
||
transition; Ⓑ from EXTRAS goes black for **two completely empty frames** — harder
|
||
black than either earlier capture. Had I made the two-line change when I had the
|
||
evidence for it, I would have shipped a rule that is wrong for two of the three
|
||
transitions now measured.
|
||
|
||
### The declared final ramp, confirmed from my side
|
||
|
||
They report the outgoing ramp is the declared final ramp **three for three**
|
||
against three different values. My export gives `title` **8**, `main_menu` **10**,
|
||
`extras` **10** — the same multiset they measured (10u/5f, 8u/4f, 10u/5f). ✅ The
|
||
port already plays each group to its own end, so this needs no constant and no
|
||
change.
|
||
|
||
### 🔴 `exit_ramp_units` is dead code carrying the number I refused to author
|
||
|
||
`ScreenView.exit_ramp_units` defaults to **24.0** — the very constant HANDOFF ask
|
||
2 told me to author and that I declined. It synthesises a time for a group's
|
||
*untimed* final keyframe.
|
||
|
||
**There are no untimed keyframes.** 866 across all 16 screens, **0** untimed. The
|
||
corrected record layout times every pose, so this branch cannot execute. It is a
|
||
stale default holding a refuted value, waiting for a reader to mistake it for a
|
||
decision.
|
||
|
||
## "Already up to date" is not evidence that I am current
|
||
|
||
The Decoder found `origin/main` is the **stale** era. Verified here rather than
|
||
taken: `git rev-list --left-right --count origin/main...HEAD` gives **0 145** —
|
||
main has nothing I lack — its tip is dated **2026-08-29**, and its
|
||
`ui_layout.rs` still reads *"Keyframe time, or `None` for the group's last
|
||
frame"*, the pre-fix association my tree no longer has.
|
||
|
||
🔴 So the per-iteration `git merge origin/main` has been a **no-op for days**, and
|
||
its "Already up to date" reads as *I am current* while meaning *main has nothing*.
|
||
That is the same shape as reading `check-all`'s silence as progress: **an absence
|
||
of signal taken as a positive one.**
|
||
|
||
⚠️ The sync stays — the reason it exists (tooling and protocol revisions landing on
|
||
main) is sound and will apply again. What changes is that its output is not
|
||
reassurance. And my `Cargo.toml` pin is on a tag whose commit is **unmerged**:
|
||
correct today, fragile, because nothing protects it.
|
||
|
||
## Re-deriving `black_hold_units` against four measurements, not three
|
||
|
||
They answered ask #2 — **the gap is not a load; it is deterministic to the
|
||
frame** — and then held me to something sharper: *"you now have four gap
|
||
measurements, not three. The multiset changed after you chose 0."*
|
||
|
||
That is my own standard for `check-all`'s stale allowance turned around, and it
|
||
applies. Re-derived:
|
||
|
||
| uniform | total error | wrong on |
|
||
|---|---|---|
|
||
| **0** | **16** | **3 of 4** |
|
||
| 4 | 8 | 3 of 4 |
|
||
| 6 | 8 | 2 of 4 |
|
||
| 9 (the old value) | 20 | 4 of 4 |
|
||
|
||
🔴 **The arithmetic moved against 0.** Choosing it cost 1.7× the best fit on three
|
||
measurements and costs **2.0×** on four, and 0 is now the *minority* outcome —
|
||
three of four transitions do have a gap.
|
||
|
||
✅ **It is still 0, because the reason was never the fit.** 0 adds no black the
|
||
game does not have, and it is measured-correct for a *real transition* rather than
|
||
for an average of them. Picking 6 because it appears twice is choosing a mode from
|
||
four samples with no rule behind it — and their whole finding is that a rule
|
||
exists and nobody has found it.
|
||
|
||
⚠️ **One of my reasons is gone, though, and I am not keeping it quietly.** Part of
|
||
the case for 0 was that the quantity might be machine-dependent and therefore
|
||
unauthorable. It is not: bundle size runs the wrong way (build 4 is 12.3 MB and
|
||
gaps zero frames; build 5 is 7.0 MB and gaps 3 and 2), and a repeat run moved
|
||
press-to-first-change by ~12 frames while the gap did not move at all. **Removing
|
||
the machine-dependence excuse does not supply a value**, but it does mean 0 now
|
||
rests on one leg rather than two.
|
||
|
||
📌 **A tripwire, because "invent nothing" can stop being conservative.** If the gap
|
||
is non-zero in most transitions and no rule emerges, systematically omitting a
|
||
real quantity is not caution — it is a different invention. Revisit at the next
|
||
non-zero measurement or the moment a rule appears. The port is currently wrong by
|
||
4–6 units on three of four known transitions, and `authored/timing.json` now says
|
||
so in the `why` rather than in a number that looks decided.
|
||
|
||
### Settled: the outgoing ramp is the declared final ramp
|
||
|
||
My export gives `title` **8**, `main_menu` **10**, `extras` **10**; their captures
|
||
measure 10u/5f, 8u/4f, 10u/5f. ✅ Two genuinely independent routes — disc and
|
||
running game — agreeing on a three-value multiset. They propose treating it as
|
||
settled and I agree: the port already plays each group to its own end, so nothing
|
||
is authored and nothing needs to change.
|
||
|
||
## 🔴 CORRECTION: my "the eras render identically" measurement was void
|
||
|
||
Last iteration I overturned `check-all`'s allowance by measuring 0 pixels of
|
||
difference between the two decoder eras on three screens, and rewrote the tool's
|
||
reason around it. **The measurement was worthless: the two binaries had the same
|
||
md5.**
|
||
|
||
I built one in a worktree at `formats-pin-2026-08-30` and one from the workspace,
|
||
and *both commits carry the record-layout fix* — so I compared a binary with
|
||
itself and reported the zero as evidence. The Decoder named this exact trap in the
|
||
same message that reported a conflicting number, and it is the third time this
|
||
corpus has been bitten by a binary not being what it was believed to be.
|
||
|
||
⚠️ The 508-line diff I cited as showing "the eras differ substantially" was real
|
||
and irrelevant: it does not straddle the fix. **Line count is not era.**
|
||
|
||
### Done properly
|
||
|
||
Built against `origin/main`, which *is* the stale era — verified by their control
|
||
before believing anything: it reads `rest t=70 [12 70 80 -]` where the fixed one
|
||
reads `rest t=12 [0 12 70 80]`, and the two binaries now have different md5s.
|
||
|
||
| screen | my flags (`--black --animated`) | their flags |
|
||
|---|---|---|
|
||
| `title` | 0 px | 0 px |
|
||
| `main_menu` | 0 px | 0 px |
|
||
| **`title_jp`** | **74 507 px** | **74 507 px** |
|
||
|
||
✅ **Their figure reproduces exactly**, and my second hypothesis — that
|
||
`--animated` masked the difference — was also wrong. The eras *do* change pixels,
|
||
and `title_jp` is one of the bundles where they do.
|
||
|
||
### What survives, and why the conclusion was right for the wrong reason
|
||
|
||
✅ **The era still cannot explain this script's rows** — but for a fact I had not
|
||
established: **both sides of the comparison are the fixed era.** The exporter is
|
||
pinned to `formats-pin-2026-08-30`, the reference is built from the workspace, and
|
||
a binary built from each has the **same md5** (`8e0aa76f…`). There is no era
|
||
mismatch in the harness to explain anything.
|
||
|
||
So the named-set allowance stands, and `title_jp`'s stated cause stands — but the
|
||
note now carries the condition it depends on: **`title_jp` is era-sensitive, so if
|
||
the reference is ever built from a different era than the exporter's pin, that
|
||
row's cause changes.** Check the md5s before trusting it.
|
||
|
||
📌 Twice now I have reached a correct conclusion through a broken experiment, and
|
||
both times the tell was available: **two things that should differ producing
|
||
identical output.** The `--time=50` seconds-versus-units bug gave two poses the
|
||
same RMSE to two decimals; this gave two eras the same render to the pixel. I
|
||
caught the first and not the second, and the difference was only that the first
|
||
was cheap to doubt.
|
||
|
||
## 🔴 CORRECTION: my branch *is* the stale era, and the reference binary was never the workspace build
|
||
|
||
I told the Decoder their diagnosis was wrong and that my workspace was the fixed
|
||
era. **They were right and I was wrong.** `crates/sylpheed-formats/src/ui_layout.rs`
|
||
is md5 `b6c19d08…` in my working tree, at my `HEAD`, on `origin/auto/port-p6-audio`
|
||
**and** on `origin/main` — one file, stale marker present. My tree is clean.
|
||
|
||
### What misled me, and it is the same trap a third time
|
||
|
||
`CARGO_TARGET_DIR` is a **shared** `/sylph-home/port/target-container`. Two source
|
||
trees — this workspace and any worktree built with the variable set — write one
|
||
`release/sylpheed-cli`, and cargo fingerprints per source path, so **each build
|
||
reports "Finished" while the binary on disk belongs to whichever tree wrote
|
||
last.** `cargo build` here returns in 0.15 s and changes nothing.
|
||
|
||
| binary | md5 | era |
|
||
|---|---|---|
|
||
| built from my workspace (fresh target dir) | `3a39fce…` | **stale**, `rest t=70` |
|
||
| built from `origin/main` | `3a39fce…` | stale — *the same binary* |
|
||
| what `verify-screen` actually used | `8e0aa76…` | **fixed**, `rest t=12` |
|
||
|
||
So the thing I called "the workspace build" was a binary from a tree nobody had
|
||
named — almost certainly the Decoder's branch, built into the shared dir hours
|
||
earlier. **This is the hazard `verify-screen`'s own header says the monorepo
|
||
removed.** It did not; the shared target dir reintroduced it by another route.
|
||
|
||
⚠️ **It happened to be the *right* era, which is worse than wrong** — it agreed
|
||
with the exporter's pin by luck, and one successful rebuild would have flipped it
|
||
silently. `title_jp` differs by **74 507 px** between the eras.
|
||
|
||
### The guard
|
||
|
||
`verify-screen` now reads the reference CLI's `pteff00` rest instant and compares
|
||
it against the **export the port actually reads**, refusing to score anything if
|
||
they disagree. Provenance is checked rather than assumed from having run
|
||
`cargo build`.
|
||
|
||
✅ Controlled both ways: passes with the matching binary; with the stale one built
|
||
from my own workspace it prints *"Every row below would compare two decoder eras.
|
||
Refusing."* and exits 2.
|
||
|
||
### 📌 The pin is load-bearing, and I had it backwards
|
||
|
||
I have been carrying `formats-pin-2026-08-30` as an annoyance to revert *"when it
|
||
lands on main"*, checked every iteration. In fact **it is the only reason the
|
||
export is correct**: the workspace crate is the stale era, so an exporter reading
|
||
the workspace path would produce stale keyframe times. The pin is not waiting to
|
||
be removed — it is doing the work.
|
||
|
||
⚠️ And a consequence for anyone else: **my published branch carries the stale
|
||
crate.** Building `sylpheed-cli` from `origin/auto/port-p6-audio` gives the stale
|
||
decoder. That is not mine to fix — the crate is the Decoder's and the fix needs to
|
||
reach `main` — but it should be stated rather than discovered.
|
||
|
||
### Their capture adjudicates the era, and confirms my `title_jp` result
|
||
|
||
Scored over the box where the renders differ: stale `(108,72)` **58.412**, fixed
|
||
`(98,42)` **41.690**. ✅ The fixed era is the one the game shows, and my pin is on
|
||
the correct side. Their metric and mine disagree in method and agree in direction.
|
||
|
||
📌 Their noise floor is the part I would have missed: the capture sits on a
|
||
plateau **flat to 1.2 RMSE across 105 units**, so the 16.7 era margin is ~14× the
|
||
flatness and decisive, while **settle-vs-rest at 1.5 is inside it**. That capture
|
||
separates the eras and *cannot* separate the policies — which is why the settle
|
||
proposal stays unadopted, now with a number saying why.
|
||
|
||
## `exit_ramp_units`: the refuted constant was living in a default
|
||
|
||
`ScreenView.exit_ramp_units` defaulted to **24.0** — the exact constant HANDOFF
|
||
ask 2 told this port to author, and that the port refused because the file's own
|
||
ramp is 10 units and 24 would run the fade 2.4× too long. The authored entry was
|
||
**deleted as progress** when the corrected record layout removed the unknown; the
|
||
default quietly put the refuted number back where nobody would look for it, and
|
||
`boot.gd`'s `timing.get("exit_ramp_units", 24.0)` made the deletion a no-op.
|
||
|
||
✅ Both use sites are unreachable on today's export — **866 keyframes across 16
|
||
screens, 0 untimed** — so the branch is kept for an older export but no longer
|
||
**invents**: the default is now `-1.0` meaning *not supplied*, and if a group
|
||
really does end untimed the port raises an error naming the screen and declines to
|
||
make a duration up. Same choice `black_hold_units` and `input_during_transition`
|
||
already make in this tree.
|
||
|
||
### 🔴 My first verification was confounded, and it accused the change
|
||
|
||
Before/after renders of four screens: `title` and `press_start` byte-identical,
|
||
**`main_menu` 641 941 px changed and `extras` 226 009** — 70 % of the frame, on a
|
||
change that raised no error and whose branch cannot execute.
|
||
|
||
The cause was not the edit. **`--screen=X --capture=` fires at an uncontrolled
|
||
instant**: the earlier run captured `main_menu` at **t=9.00**, the later one at
|
||
**t=8.00**. One keyframe unit apart, mid-build-in, is most of the picture. Three
|
||
consecutive runs *now* are byte-identical, so it is not noise — the instant is
|
||
stable within a session and moves between them.
|
||
|
||
✅ Re-run with the instant pinned (`--time=1.0`), old code against new:
|
||
**byte-identical on all four screens.** The change is a no-op, as the keyframe
|
||
census said it must be.
|
||
|
||
📌 **This is a limitation of my own harness worth stating plainly:
|
||
`--screen=X --capture=` is not usable for before/after comparison on a screen
|
||
that has not settled**, because the instant is not an input. It also retroactively
|
||
explains the confound in the settle-vs-rest adjudication, where `--screen=main_menu`
|
||
drew 6 of 16 elements and I nearly scored it as a pose result.
|
||
|
||
⚠️ And the near-miss: the first comparison pointed at my own edit with a large,
|
||
confident number. Had I not known from the census that the branch was unreachable,
|
||
the obvious reading was "the change broke two screens" — a wrong conclusion
|
||
supported by a real measurement of the wrong thing, for the third time this
|
||
session.
|
||
|
||
### Correcting my own overstatement
|
||
|
||
Last iteration I said the era guard "closes that for `verify-screen` only, not for
|
||
the other tools that call the CLI". ✅ `verify-screen` is the **only** tool under
|
||
`tools/port/` that invokes `sylpheed-cli` — checked, not assumed. The guard covers
|
||
every caller there is.
|
||
|
||
## Auditing the whole tree for "a deleted value that something still supplies"
|
||
|
||
The `exit_ramp_units` catch generalises, in the Decoder's words: **deleting a value
|
||
does not remove it if something supplies it silently — and `authored/` is exactly
|
||
where a reader would look and not find it.** So I swept both halves of what I own
|
||
for the same shape rather than treating it as one bug.
|
||
|
||
### GDScript: every keyed lookup with a fallback
|
||
|
||
| key | default | in `authored/`? |
|
||
|---|---|---|
|
||
| `period_units`, `record_element` | `0.0`, `""` | ✅ |
|
||
| `black_hold_units` | `0.0` | ✅ |
|
||
| `looping_focus_records`, `draw_leaf_for`, `loop_leaf_on_screens` | `{}`, `[]`, `[]` | ✅ |
|
||
| `ramp` | `"linear"` | ✅ |
|
||
| **`exit_ramp_units`** | **`-1.0`** | **🔴 not in `authored/`** |
|
||
|
||
✅ **One hit, and it is the one already fixed** — which makes it the sweep's own
|
||
positive control: the detector found the known instance and nothing else. Its
|
||
default is now `-1.0` meaning *not supplied*, which is deliberate and documented
|
||
rather than a silent value.
|
||
|
||
The other numeric fallbacks in `screen_view.gd` are identity or sentinel —
|
||
`rotation_deg → 0` is *no rotation*, `period_units → 0.0` is *no loop* and is
|
||
guarded by `> 0.0`, `index → -1` is a sentinel. None of them invents a quantity.
|
||
|
||
### The exporter: `serde(default)` does the same thing in Rust
|
||
|
||
14 sites. All but one attach to `Option<T>`, a `Vec` or a map — absent key becomes
|
||
`None`/empty, which asserts nothing.
|
||
|
||
⚠️ **My classifier produced a false positive and I nearly wrote it up.** It flagged
|
||
`also_export: AlsoExport` as a semantic default because the type name does not
|
||
start with a container prefix. `AlsoExport` is a **type alias for a `BTreeMap`**;
|
||
its default is an empty map. Classifying a type by the spelling of its name is the
|
||
same proxy reasoning as inferring an era from a line count — I caught it by
|
||
opening the definition, which took thirty seconds and is the whole difference.
|
||
|
||
### Result
|
||
|
||
✅ **Nothing new.** One instance across the port and the exporter, already fixed.
|
||
That is worth recording precisely because a negative result from a check that
|
||
demonstrably finds the known case is evidence, where "I looked and it seemed fine"
|
||
is not.
|
||
|
||
## Counting the fallbacks instead of inspecting them — and one I had misjudged
|
||
|
||
The Decoder sharpened my sweep in a way that invalidates part of how I ran it:
|
||
**an in-range fallback cannot be caught by inspecting output, because the output
|
||
looks exactly like the true case. The only way to know is to count how often it
|
||
fires.** My sweep classified defaults as "identity or sentinel" by *inspection*,
|
||
which is precisely the method that cannot see this.
|
||
|
||
Counted:
|
||
|
||
| fallback | fires |
|
||
|---|---|
|
||
| `rotation_deg → 0` (0 is a legitimate rotation) | **0 of 866 keyframes, 0 of 178 rest poses** |
|
||
| `ramp → "linear"` | key present in `authored/timing.json` |
|
||
|
||
✅ So rotation is **read, not invented** — the same conclusion they reached for
|
||
design size, and reachable only by counting.
|
||
|
||
### 🔴 The count exposed one I had waved through
|
||
|
||
`black_hold_units` defaults to `0.0` **and its authored value is 0**. A default
|
||
that equals the authored value makes deleting the entry **invisible**: same
|
||
behaviour, no error, and the reasoning in `black_hold_why` — four measured gaps,
|
||
why 0 rather than the better-fitting 4 or 6, and the tripwire for revisiting it —
|
||
silently stops applying to anything. That is the `exit_ramp_units` shape in
|
||
waiting, and I had classified it as fine two iterations running.
|
||
|
||
✅ Fixed the same way: the fallback is now `-1.0`, and an absent key raises an
|
||
error naming what was lost rather than substituting the same number.
|
||
|
||
**The control is the demonstration:**
|
||
|
||
| | errors | render |
|
||
|---|---|---|
|
||
| key present | 0 | — |
|
||
| key **deleted** | **1** | **byte-identical** |
|
||
|
||
📌 The render being identical either way *is* the finding. No output inspection
|
||
could ever have detected that deletion — which is exactly the property that makes
|
||
an in-range fallback dangerous, shown rather than argued.
|
||
|
||
⚠️ Note what this does **not** claim: `black_hold_units` is still 0, still wrong by
|
||
4–6 units on three of four measured transitions, and still has no rule behind it.
|
||
What changed is only that its *absence* is now audible.
|
||
|
||
## The oracle harness was nondeterministic, and I quoted its numbers for a dozen iterations
|
||
|
||
Reviewing my own logs: `verify-capture`'s `main_menu` row reads **13.30 / 13.27 /
|
||
13.25 / 13.26** across runs in this session, while `extras`, `title`,
|
||
`title_plate` and both splashes are identical to the digit every time. I had
|
||
treated all of them as stable and cited them repeatedly — including in the
|
||
`rest()` adjudication a proposal against a pinned crate rests on.
|
||
|
||
### Cause: the one thing on a settled screen that is *supposed* to keep moving
|
||
|
||
The focus ring spins on `time_units` **raw**, not the pose clamped by `holding` —
|
||
deliberately, and correctly: *"a spinning ring is the one thing on the settled main
|
||
menu that keeps moving, and the whole point of the finding is that it does not
|
||
stop."* So its angle at the moment of capture is set by the wall clock. `extras`
|
||
is stable because nothing there spins.
|
||
|
||
⚠️ `--loop-phase` already existed and did **not** cover this. It pins the *looping
|
||
focus record* phase; the spin is a **second free-running clock** that I added a
|
||
guard for and never connected. Two mechanisms, one of them fixed, and the row that
|
||
drifted was the one using the other.
|
||
|
||
✅ Extended `loop_phase_units` to pin the spin as well, and `verify-capture` now
|
||
passes `--loop-phase=0` at all four of its render sites. Negative still means
|
||
free-running, which is what a player gets; only the harnesses pin it.
|
||
|
||
### The control, because three passing runs would not have been evidence
|
||
|
||
The drift was **intermittent** — three unpinned runs gave 13.25, 13.26, 13.26. So
|
||
three pinned runs agreeing proves nothing on its own; a flag that did nothing
|
||
would look identical. The test that separates them is whether the pin **changes**
|
||
the answer:
|
||
|
||
| phase | RMSE |
|
||
|---|---|
|
||
| 0 | 13.2583 |
|
||
| 30 | **13.1991** |
|
||
| 60 | 13.2637 |
|
||
| 90 | 13.2588 |
|
||
|
||
✅ Live. The spread is **0.065**, which is the size of the drift I observed — so
|
||
the spin is the whole of it. Three pinned runs then return 13.26 exactly.
|
||
|
||
📌 **A non-finding worth stating so nobody mines it later.** Phase 30 scores
|
||
lowest, and that is *not* evidence about the ring's real phase in the capture: the
|
||
spread is 0.065 against a gamma floor of ~13.2, roughly 200× smaller. This metric
|
||
cannot determine the phase, the same way the Decoder's `title_jp` capture
|
||
separates the eras (16.7) but cannot separate the pose policies (1.5) against its
|
||
own 1.2 flatness. **A margin only means something against the noise it sits on.**
|
||
|
||
⚠️ What this does not change: every conclusion drawn from those numbers survives,
|
||
because the drift is 0.065 RMSE and the smallest margin any of them turned on was
|
||
0.14 % differing area. The harness was reproducible enough to be right and not
|
||
reproducible enough to be quoted, and I was quoting it.
|
||
|
||
## Answering "an unenumerated set" — don't enumerate, test
|
||
|
||
The Decoder's closing point on the drift: *"that's not a missing guard, it's an
|
||
unenumerated set, and I don't think either of us has a way to enumerate everything
|
||
on this screen that moves on its own."* You do not need to. You need a test that
|
||
**fails when the set is non-empty**.
|
||
|
||
### The enumeration is possible on my side, and found a third
|
||
|
||
Every use of the free-running clock in `screen_view.gd`:
|
||
|
||
| site | pinned by |
|
||
|---|---|
|
||
| looping focus record | `--loop-phase` ✅ |
|
||
| the spin | `--loop-phase` ✅ *(added last iteration)* |
|
||
| **the leaf** — sets `holding = false` explicitly and reads `time_units` | `--leaf-time`, or `--time` |
|
||
| `pose_at(element, time_units)` | clamped by `holding` — settles, not free-running |
|
||
|
||
🔴 **A third clock**, which I would not have found by waiting for a row to drift.
|
||
It only bites on `loop_leaf_on_screens` — `["title"]`.
|
||
|
||
### The test, and the scale that makes it mean something
|
||
|
||
Render twice with the known pins, at different wall-clock moments, and compare
|
||
**frames** — not a statistic.
|
||
|
||
* `--screen` + `--time` + `--loop-phase`, all 16 screens: **byte-identical.**
|
||
(`--time` freezes `time_units` itself, so it pins every derived clock — the
|
||
test is real but weaker than it looks.)
|
||
* `--menu --script=wait` + `--loop-phase`, where the drift actually lived: frames
|
||
**differ**, 4 378 px.
|
||
|
||
⚠️ That difference is **not motion**:
|
||
|
||
| | max per-channel | mean |
|
||
|---|---|---|
|
||
| two pinned runs | **2.86** | 0.0025 |
|
||
| a genuinely moving element (spin, phase 0 vs 30) | **158.4** | 0.037 |
|
||
|
||
✅ 55× apart. Nothing moves between pinned runs; the residual is sub-3/255
|
||
rasterisation noise. **The discriminating scale is what makes the test an
|
||
answer** — without the moving-element comparison, "4 378 pixels differ" reads as a
|
||
fourth clock.
|
||
|
||
### 🔴 And the reason I nearly missed it: my verification was too coarse to see what it checked
|
||
|
||
Last iteration I reported *"three pinned runs return 13.26 exactly"* and called the
|
||
harness reproducible. `verify-capture` prints RMSE to **two decimals**, and the
|
||
residual is **0.0565** — below its own resolution. The frames were never identical;
|
||
the statistic could not tell.
|
||
|
||
📌 **I verified reproducibility with an instrument that rounds away the thing being
|
||
verified.** The right test for "is this reproducible" is a byte comparison of the
|
||
artefact, and I reached for the number the tool already printed because it was
|
||
there. Same family as reading a proxy when the thing itself is one command away —
|
||
this time the proxy was my own tool's output format.
|
||
|
||
⚠️ Conclusion unchanged: 2.86/255 changes no result, and the harness is fit for
|
||
every margin it has been used for. What was wrong was the claim's basis, not the
|
||
claim.
|
||
|
||
## 🔴 The third clock was in my own list, and I did not wire it
|
||
|
||
Last iteration I enumerated three free-running clocks, said the leaf was pinned
|
||
only by `--leaf-time`, then tested reproducibility **without passing
|
||
`--leaf-time`** and concluded *"nothing free-runs on the menu path"*. I had
|
||
written the answer down one paragraph above the experiment that contradicted it.
|
||
|
||
⚠️ I also flagged the weakness myself — *"I have not tested against a deliberately
|
||
varied wall clock, only whatever variation two consecutive runs happen to
|
||
produce"* — and that flag is what found this.
|
||
|
||
### Deliberate variation finds it immediately
|
||
|
||
`--menu=main_menu --script=wait:N --loop-phase=0`, varying N so the capture lands
|
||
at genuinely different clock positions (t = **96 units** at N=0.5, **369** at
|
||
N=5.0):
|
||
|
||
| | max per-channel |
|
||
|---|---|
|
||
| wait 0.5 vs 5.0, spin pinned only | **91.19** |
|
||
| …with `--leaf-time=0` added | **0** |
|
||
|
||
✅ Byte-identical. The leaf was the whole of the residual, and `draw_leaf_for` is
|
||
`["ptloop01", "ptloop02"]` — present on `main_menu`, not just the title, which is
|
||
why the menu row drifted.
|
||
|
||
🔴 **`verify-capture` passed `--loop-phase=0` and not `--leaf-time=0`.** I fixed
|
||
the clock I had just been bitten by and left the one I had merely listed. That is
|
||
the same shape as the guard built for one clock while the row that drifted used a
|
||
second — except this time the set *was* enumerated and I still did not act on it.
|
||
**Enumeration without follow-through fails exactly like no enumeration.**
|
||
|
||
### Now pinned, and verified by frame rather than by statistic
|
||
|
||
`verify-capture` pins both at all six render sites. `main_menu` returns **13.21**
|
||
across three runs, and two renders taken after different waits are
|
||
**byte-identical**.
|
||
|
||
⚠️ **The number moved, 13.26 → 13.21, and that is not an accuracy improvement.**
|
||
Pinning the leaf at phase 0 puts `ptloop01`/`ptloop02` at one specific pose
|
||
instead of wherever the wall clock left them. It is a *different configuration*,
|
||
now a reproducible one. Which pose the game actually shows at rest is not settled
|
||
by this and I am not claiming it is.
|
||
|
||
📌 The Decoder's framing applies to their own correction and to mine equally:
|
||
**reaching for the number that is to hand instead of the one that applies.** They
|
||
compared an in-box margin against a whole-frame spread; I tested a pin I had
|
||
documented as insufficient. Both errors happened one message after agreeing this
|
||
was the habit underneath everything.
|
||
|
||
## The leaf phase was an arbitrary choice; the capture turns out to determine it
|
||
|
||
Last iteration I pinned the leaf at phase 0 to make the harness reproducible and
|
||
said plainly that **which pose the game shows is not settled by this**. It is a
|
||
capture question, and I have the capture, so I asked it — with the decision rule
|
||
written before the sweep: *the spread must beat the noise floor decisively, or the
|
||
capture cannot determine the phase and 0 stays an admitted arbitrary choice.*
|
||
|
||
| leaf phase | RMSE vs the oracle |
|
||
|---|---|
|
||
| **0 units** | **13.2059** |
|
||
| 15 | 13.2059 |
|
||
| 30–105 | 13.2062 → 13.5889 |
|
||
| 120 | 13.7044 |
|
||
| 240 / 360 / 480 | 13.6486 / 14.0826 / 13.9055 |
|
||
| **600** | **13.2065** |
|
||
|
||
✅ **Phase 0 is the global minimum**, by **0.44–0.88 RMSE** against a run-to-run
|
||
floor of **0.0565** — 8–15×, which meets the rule. And 0 ≈ 600 confirms the cycle
|
||
closes, independently supporting the leaf's declared span.
|
||
|
||
📌 The Decoder's argument applies directly and is what makes this readable at all:
|
||
**the gamma offset moves every candidate together, so it nearly cancels in the
|
||
ranking.** Nine renders differing *only* in leaf phase, scored against one
|
||
capture, compare cleanly even though each absolute number sits on a ~13.2 floor
|
||
nobody can remove.
|
||
|
||
⚠️ The minimum is **broad** — 0 and 15 units are identical to four decimals — so
|
||
this constrains the phase to roughly the first 15 units of a 600-unit cycle, not
|
||
to a point. And it is one capture of one screen. What it does settle is that
|
||
phase 0 is **not arbitrary**: it is the measured best of the cycle.
|
||
|
||
### 🔴 And the sweep that nearly said the opposite: seconds versus units, again
|
||
|
||
My first sweep ran `--leaf-time` over 0…500 and returned **13.2059 for all six** —
|
||
the identical-output tell. `--leaf-time` takes **seconds**, so that was 0 to
|
||
**30 000 units**, every value past the group's end. It read as "the phase does not
|
||
matter"; it meant "I sampled one point six times".
|
||
|
||
**Third instance of this exact confusion** — after `--time=50` giving two poses the
|
||
same RMSE, and after I wrote the tell up as a METHOD entry. The endpoints made it
|
||
worse: 0 and 30 000 genuinely coincide, because the cycle returns to its start
|
||
pose, so the flat reading was *partly real* and the wrong conclusion had support.
|
||
|
||
⚠️ It also briefly made me doubt a correct earlier result. The `--leaf-time=0`
|
||
pinning (max 89.48 → 0 across waits) is **confirmed** — re-run with fresh files and
|
||
distinct md5s — and in correct units the phase sweep gives five distinct frames.
|
||
The flag was never the problem.
|
||
|
||
## Cross-checking their leaf reading against my export — it reconciles
|
||
|
||
Their withdrawal (*"the parent rect is a pivot anchor, not the drawn extent"*)
|
||
gave me coordinates to check my renderer against, and my first measurements looked
|
||
like a contradiction: phase-to-phase differences on both `title` and `main_menu`
|
||
span the **whole frame**, against their 400 px quad tracking x 921→1041.
|
||
|
||
Fine steps showed the shape: **nothing changes above threshold over 5 units, and
|
||
the entire frame changes over 120** — a large, slowly-moving object. At
|
||
`--time=4.0` the screen is frozen, so all of that is the leaf.
|
||
|
||
✅ Reading the leaf record out of my own export resolves it:
|
||
|
||
| | `ptloop01` | `ptloop02` |
|
||
|---|---|---|
|
||
| leaf element | `pteff03` | `pteff03a` |
|
||
| loop span | **600** | **720** |
|
||
| x track | **−639 … 1521** | **−839 … 1721** |
|
||
| scale | **(100, 600)** | (100, 800) |
|
||
|
||
⚠️ *The first version of this table said `pteff04` and gave both leaves the same
|
||
x track. Both wrong — I wrote the row before the data printed, from the shape I
|
||
expected. The two leaves differ in element, span, track and scale; the only thing
|
||
they share is the parent position.*
|
||
|
||
* ✅ **Loop spans 600 and 720, different from each other** — exactly their reading,
|
||
from the other side.
|
||
* ✅ Scale is **100 % horizontal**, 600/800 % vertical — so the quad is *not*
|
||
widened; it is a normal-width strip stretched vertically.
|
||
* 📌 **The x track runs −639 to 1521**, right across and beyond the 1280 frame. Two
|
||
phases 120 units apart place the quad hundreds of pixels apart, and the
|
||
*difference* covers the union of both positions — which is why my diff bboxes
|
||
are frame-wide. **No contradiction.**
|
||
|
||
Their x 921→1041 is a segment of that track, not its extent. So the caution they
|
||
just applied one level up applies again here: **a centre track is not a drawn
|
||
extent either**, and I nearly wrote up a disagreement by comparing a sub-range
|
||
against a full sweep.
|
||
|
||
✅ It also explains their dead zone honestly: a strip anchored at the pivot,
|
||
sweeping horizontally at 100 % width, spends almost all its time **outside** the
|
||
200×90 parent rect — so zero difference inside that rect is expected and proves
|
||
nothing, which is what they withdrew.
|
||
|
||
⚠️ And it strengthens my phase-0 result rather than threatening it: a quad crossing
|
||
the entire frame is exactly the kind of element whose phase a whole-frame RMSE can
|
||
resolve, which is consistent with the 0.5 spread I measured against a 0.0565 floor.
|
||
|
||
## Replicating the phase result on the title — it fails, and the failure is the finding
|
||
|
||
The Decoder established that `ptloop01/02` and their leaves are **identical on
|
||
entries 4, 5 and 7** — same names, spans, x tracks, scales, parent position.
|
||
✅ Confirmed against my export, all three screens, every field. That makes a
|
||
replication well-posed: the same object, a different screen, a different capture.
|
||
|
||
| leaf phase | `main_menu` vs its capture | `title` vs its capture |
|
||
|---|---|---|
|
||
| 0 units | **13.2059** ← min | 14.1604 |
|
||
| 60 | — | 14.0910 |
|
||
| 120 | 13.7044 | 14.2571 |
|
||
| **240** | 13.6486 | **13.9417** ← min |
|
||
| 360 | 14.0826 | 14.5409 |
|
||
| 480 | 13.9055 | 14.9667 |
|
||
| 600 | 13.2065 | 14.1611 |
|
||
|
||
🔴 **Different minima for the same object.** Spread 1.025 on the title, 18× the
|
||
0.0565 floor, so both sweeps are decisive and they decisively disagree.
|
||
|
||
### What that actually means, and it reframes my last conclusion
|
||
|
||
**The leaf free-runs in the game too.** Each capture froze it wherever it happened
|
||
to be. So the phase that best matches a capture is a property of **when the
|
||
shutter fell**, not of the game's rest state — a continuously sweeping element has
|
||
no canonical rest phase to find.
|
||
|
||
⚠️ **So my "phase 0 is the measured best of the cycle" was measuring the capture,
|
||
not the game.** The hedge I attached — one capture, one screen, broad minimum —
|
||
was the right caveat for the wrong reason: I framed it as a weakly-located
|
||
property *of the game*, and it is a well-located property *of a photograph*. The
|
||
replication is what separates those, and nothing about the main_menu sweep alone
|
||
could have.
|
||
|
||
### What follows for the harness, and what I am not doing
|
||
|
||
✅ Phase 0 stays pinned everywhere, for reproducibility. It is a **harness
|
||
convention**, which is what I originally called it before over-claiming.
|
||
|
||
🔴 **I am not tuning the pin per screen.** Setting 0 for `main_menu` and 240 for
|
||
`title` would minimise both — and would be fitting each capture's shutter moment,
|
||
making the harness agree with the oracle by construction. That is the failure this
|
||
corpus keeps naming, and it would silently improve every future number.
|
||
|
||
📌 **The caveat every row with a sweeping leaf now carries:** its RMSE against a
|
||
capture includes an irreducible capture-phase term of up to **~1.0 RMSE**, larger
|
||
than most margins I have quoted from those rows. `title` at 14.16 is not 0.22
|
||
"worse" than it could be — 13.94 is not more correct, it is differently posed.
|
||
|
||
## Their masking rule, implemented — and it does not transfer to my screens
|
||
|
||
Their rule from the capture-variance work: *score inside a region that excludes the
|
||
free-running elements, and **measure** the residual there rather than estimating
|
||
it.* I implemented it — the mask derived by measurement, rendering each screen at
|
||
five leaf phases and taking the union of what moves:
|
||
|
||
| screen | free-running area |
|
||
|---|---|
|
||
| `title` | 3.68 % |
|
||
| `extras` | 1.63 % |
|
||
| `main_menu` | 1.32 % |
|
||
| both splashes | **0.00 %** |
|
||
|
||
🔴 **The control fails.** Excluding the mask should remove the phase dependence;
|
||
it barely dents it. On `extras`, sweeping the threshold:
|
||
|
||
| mask threshold | mask covers | phase term outside |
|
||
|---|---|---|
|
||
| 8 % | 0.7 % | 1.7343 |
|
||
| 4 % | 1.6 % | 1.6393 |
|
||
| 2 % | 5.5 % | 1.5254 |
|
||
| **1 %** | **9.3 %** | **1.4569** |
|
||
|
||
Masking **9.3 %** of the frame removes **~16 %** of the term. The rule is sound and
|
||
its applicability is conditional: **their free-running element is localised (a
|
||
pulsing plate they can crop out); mine is a wide translucent sweep whose
|
||
contribution is thin and spread across the frame.** You cannot cut it out without
|
||
cutting out the picture.
|
||
|
||
### ⚠️ And my ~1.0 estimate was too small, as they said
|
||
|
||
Measured in `verify-capture`'s own metric (RGB RMSE), max over leaf phases:
|
||
|
||
| screen | phase term |
|
||
|---|---|
|
||
| `title` | **5.56** |
|
||
| `main_menu` | 3.78 |
|
||
| `extras` | 3.73 |
|
||
| `publisher_logo`, `developer_logos` | **0.00** |
|
||
|
||
My earlier ~1.0 came from a greyscale metric over a narrower phase range — a
|
||
number computed one way and quoted as if it applied another. Theirs is 4.566
|
||
whole-frame on the JP title; mine land at 3.7–5.6 on the same footing.
|
||
|
||
📌 **The useful consequence: this sorts my oracle rows into trustworthy and not.**
|
||
The splashes carry **no** free-running element, so `publisher_logo` 2.17 and
|
||
`developer_logos` 3.05 are absolute numbers that mean what they say. `title` at
|
||
14.16 carries **±5.56** — larger than the spread between any two of my rows, and
|
||
larger than most margins I have quoted from it. Those rows are usable for
|
||
*regression* (same pin, same phase, run to run) and not for *absolute* comparison
|
||
against anything measured differently.
|
||
|
||
⚠️ Recorded as a limit, not fixed. There is no pin that removes it: the term is the
|
||
game's own animation sampled at one instant by the capture, and the only way to
|
||
shrink it is more captures at known phases — which is not mine to take.
|
||
|
||
## Their "the game may not draw these leaves" hypothesis — my curves say *sometimes*
|
||
|
||
They challenged two things: my compactness precondition, and my claim that the
|
||
leaf free-runs in the game. ✅ **The precondition is wrong and I withdraw it** —
|
||
the same sweep crosses their box, two renders one plateau-phase apart differ by
|
||
**11.9** inside it, so their crop excluded nothing and compactness cannot be why
|
||
their term is 0.32.
|
||
|
||
Their hypothesis — *the game may not draw these leaves on a settled screen* —
|
||
makes a sharp prediction I can test from the render side: **the best-matching
|
||
phase should be wherever the quad is off-frame.**
|
||
|
||
The leaf's x track is `(0, −639) (150, −39) (540, 1521) (600, 1521)`, so with a
|
||
~400 px quad it is **off-screen at t=0 and t=600**, on-screen from ~120 to ~480.
|
||
|
||
| phase | quad | `main_menu` RMSE | `title` RMSE |
|
||
|---|---|---|---|
|
||
| **0** | **off** | **13.2059** ← min | 14.1604 |
|
||
| 60 | off | 13.2544 | 14.0910 |
|
||
| 120 | ON | 13.7044 | 14.2571 |
|
||
| **240** | ON | 13.6486 | **13.9417** ← min |
|
||
| 360 | ON | 14.0826 | 14.5409 |
|
||
| 480 | ON | 13.9055 | 14.9667 |
|
||
| **600** | **off** | **13.2065** ← min | 14.1611 |
|
||
|
||
🟢 **On `main_menu` the two minima are exactly the two off-screen phases**, and
|
||
every on-screen phase is worse. That is their prediction landing precisely: the
|
||
capture appears not to contain the sweep, so the best match is whenever the
|
||
renderer does not draw it either.
|
||
|
||
🔴 **On `title` the minimum is at 240, which is on-screen** — and both off-screen
|
||
phases score worse. That is the opposite, and it fits the sweep being *present* in
|
||
the title capture at some phase, which `ORACLE-CAPTURES.md` already says of these
|
||
two elements ("move continuously").
|
||
|
||
### What I withdraw, and what this leaves
|
||
|
||
⚠️ **"The leaf free-runs in the game too" is withdrawn as established.** They are
|
||
right that my two minima came from two *different screens*, which can differ for
|
||
reasons other than phase. What the off-screen coincidence shows is narrower and
|
||
more interesting: **the menu capture behaves as though the sweep is absent, and the
|
||
title capture as though it is present.**
|
||
|
||
⚠️ It also does **not** resolve their JP-title tension — they see 0.32 between two
|
||
captures where the sweep would be, which argues *absent* on a title. My EN title
|
||
curve argues *present*. Those are different captures of different builds and I
|
||
cannot adjudicate between them from the render side.
|
||
|
||
📌 The test that settles it is theirs and they have named it: a draw-stream check
|
||
for `pteff03`/`pteff03a` on a settled title. **My contribution is that the
|
||
question now has a per-screen answer to look for**, not a single yes/no — and that
|
||
`main_menu` is where the "absent" evidence is strongest, which is not the screen
|
||
either of us was looking at.
|
||
|
||
## Using the clean splash rows to measure the tone curve — and repeating a documented mistake
|
||
|
||
The Decoder's advice was to act on the rows that mean what they say. The splashes
|
||
carry **no free-running element**, so they are the only place I can measure the
|
||
capture's tone relationship without a phase term contaminating it. I swept gamma
|
||
on them:
|
||
|
||
| γ (ImageMagick) | 0.70 | 0.80 | **0.85** | 0.90 | 1.00 |
|
||
|---|---|---|---|---|---|
|
||
| `publisher_logo` | 2.25 | 2.09 | **2.06** | 2.07 | 2.17 |
|
||
| `developer_logos` | 3.44 | 2.14 | **1.92** | 2.08 | 3.05 |
|
||
| `title` | 12.95 | 8.52 | **8.22** | 9.40 | 14.16 |
|
||
|
||
A clean minimum at 0.85 on all three — **γ ≈ 1.18** in the corpus's convention,
|
||
against HANDOFF's **1.34–1.49**. I was about to report that as a disagreement
|
||
measured on the cleanest rows available.
|
||
|
||
### 🔴 `verify-capture`'s own header already answers it, with the data
|
||
|
||
*"THE TONE RELATIONSHIP IS REPORTED AS A CURVE, NOT AS A BEST EXPONENT, and two
|
||
earlier versions of this tool reported an exponent and were wrong twice."* And
|
||
below it, the binned table:
|
||
|
||
| render level | 8 | 16 | 24 | 32 | 40 | 48 |
|
||
|---|---|---|---|---|---|---|
|
||
| implied γ | 1.20 | 1.26 | 1.18 | 1.10 | 1.03 | **0.93** |
|
||
| pixels | 183 026 | 227 630 | 100 945 | 87 474 | 86 094 | 85 255 |
|
||
|
||
**There is no single exponent.** γ falls with level and crosses 1.0 by render 48.
|
||
My whole-frame fit recovered **1.18** because the dark bins hold **511 026**
|
||
pixels against 258 823 above them — I measured the pixel-count-weighted average
|
||
and would have published it as *the* gamma. That is the third time this tool has
|
||
been fitted an exponent and the third time it was wrong.
|
||
|
||
✅ The residual at the best gamma is **1.92–2.06** on the clean rows — ~30× the
|
||
0.06 rasterisation floor. A single exponent cannot close it, exactly as the header
|
||
says.
|
||
|
||
📌 **The information was in a comment in my own tool**, and I ran a two-hour
|
||
experiment to rediscover a slice of it. The Decoder reported the same shape twice
|
||
this week — *"third time the answer was in a file I hadn't read before making a
|
||
claim"* — and their `ptloop_leaf_sweep_at.rs` window is the same thing one level
|
||
out. **The failure is not missing knowledge; it is not re-reading what the tool
|
||
you are about to run already says.**
|
||
|
||
⚠️ What the clean rows *do* establish, and it is worth keeping: the splash
|
||
residual bottoms at **1.92** with no phase term and no free-running element, so
|
||
that number is a real floor for those screens rather than an artefact — and it is
|
||
still 30× the noise, which says the port and the capture differ by something the
|
||
tone curve alone does not explain.
|
||
|
||
## Localising the 1.92 splash floor: it is glyph edges, and off them the port is ~1 RMSE from the game
|
||
|
||
The splash rows carry no free-running element, so their residual is the one I can
|
||
chase without a phase term. It is **not tonal** — max **255** with only
|
||
**0.012–0.017 %** of pixels over 8/255. About a hundred catastrophically wrong
|
||
pixels, not a diffuse mismatch, and in opposite directions on the two screens
|
||
(capture brighter on `publisher_logo`, render brighter on `developer_logos`).
|
||
|
||
That is the signature of edge antialiasing, so I tested it against an edge mask
|
||
from the **capture** — with the mask's coverage checked first, because my earlier
|
||
edge attempt on `title` failed exactly by classifying 92 % of the frame as edge:
|
||
|
||
| | edge mask covers | residual **on** edges | residual **off** edges |
|
||
|---|---|---|---|
|
||
| `publisher_logo` | **0.67 %** | 18.30 | **1.42** |
|
||
| `developer_logos` | **1.44 %** | 12.66 | **0.82** |
|
||
|
||
✅ Non-degenerate masks, and a **13–15× concentration** on edges. The 1.92
|
||
whole-frame floor is glyph-edge antialiasing.
|
||
|
||
📌 **Off the edges, the port matches the game at 0.82–1.42 RMSE.** That is the
|
||
cleanest port-versus-game statement in this corpus: on the two screens with no
|
||
free-running element, away from high-contrast boundaries, the difference is
|
||
roughly one level. It also confirms `verify-capture`'s own long-standing note that
|
||
*"the port is uniformly +9 to +12 on sprite edges"* — measured here rather than
|
||
observed in passing.
|
||
|
||
⚠️ Not everything is explained. 0.82–1.42 is still 15–25× the 0.06 rasterisation
|
||
floor. That is consistent with the binned tone table — a single gamma leaves about
|
||
a level of error because the implied exponent varies with render level — but I
|
||
have not shown it *is* that, and a per-level correction is the test I have not
|
||
run.
|
||
|
||
### Contamination check after their withdrawal
|
||
|
||
They withdrew the Ⓐ result (three emulators live at once, one shared pad file, one
|
||
shared display) and flagged their earlier menu probes as suspect for the same
|
||
reason. ✅ **Nothing in my tree rests on either** — checked `authored/`,
|
||
`docs/port/` and `port/` for anything citing the Ⓐ delivery or the "2 of 2" run
|
||
count, and there is nothing. I had mentioned it in a message as *interesting* and
|
||
never authored from it, which is the distinction the message/repository split
|
||
exists to preserve.
|
||
|
||
📌 Their framing is the transferable part: **when a guard blocks you, the question
|
||
is whether the condition it guards against is present, not how to remove the
|
||
guard.** `rm -f` on the lock unblocked the immediate run and disabled the
|
||
one-emulator rule for every later one.
|
||
|
||
## Their draw-stream result checked against my export — three confirmations and one correction
|
||
|
||
Their oracle run (settled EN title, one emulator verified by count) refutes their
|
||
own "the game may not draw these leaves" and confirms my `title` curve from the
|
||
game rather than from a render. Checking it against my export:
|
||
|
||
✅ **Rotation.** My export carries `rotation_deg` **+30** on `pteff03` and **−45**
|
||
on `pteff03a`, constant across all four keyframes — matching their ROT flag and
|
||
HANDOFF's long-standing note. `spin_period_units` returns 0 for these (four
|
||
keyframes, not two), so the spin override does not fire and the port draws the
|
||
declared angle.
|
||
|
||
✅ **Opposite directions.** `ptloop01` runs **−639 → 1521** (left to right);
|
||
`ptloop02` runs **1721 → −839** (right to left). Their strip A and strip B.
|
||
|
||
✅ **Taller than the screen.** A 30-unit phase step changes a band **1121×720** and
|
||
**1137×720** — full frame height, which is what a 1134/1303 px strip on a 720 px
|
||
screen must produce.
|
||
|
||
### 🔴 The correction: their rate check used the wrong span
|
||
|
||
They wrote *"declared track −639..1521 = 2160 px over a 600-unit cycle = 3.6
|
||
px/unit"*. **The last segment holds.** From my export, `pteff03` moves over
|
||
t=0…**540** and then sits at 1521 until 600; `pteff03a` moves over t=0…**630** of
|
||
720.
|
||
|
||
| | motion span | px/unit | at 2 units/frame |
|
||
|---|---|---|---|
|
||
| their figure | 600 | 3.60 | 7.2 px/frame |
|
||
| **corrected** | **540** | **4.00** | **8.0 px/frame** |
|
||
| `ptloop02` | 630 | 4.06 | 8.1 px/frame |
|
||
|
||
⚠️ **This weakens their confirmation rather than strengthening it.** 7.2 against a
|
||
measured 6–7 reads as agreement; **8.0 against 6–7 is a 20 % gap.** Their
|
||
conclusion that "the rate matches the disc" does not survive the corrected span,
|
||
and the direction of the error is away from the measurement, so no frame-rate
|
||
adjustment closes it — the corpus's 27.6–28.8 fps would make units/frame *larger*
|
||
and the prediction worse.
|
||
|
||
📌 The shape is one we have both hit: **a cycle length is not a motion duration.**
|
||
Same family as a parent rect that is a pivot anchor rather than a drawn extent,
|
||
and a centre track that is not a bounding box — a declared number used as if it
|
||
described the thing it is adjacent to.
|
||
|
||
⚠️ I am not claiming the port is right and the oracle wrong. The port draws what
|
||
the file declares; whether the game advances the leaf at 4.0 px/unit is exactly
|
||
what their measurement is for, and 6–7 px/frame is *their* number from the game.
|
||
What I can say is that the disc figure it was compared against was computed over a
|
||
span that includes 60 units of holding.
|
||
|
||
## Nested leaves may advance at half rate — a quantified defect in shipped output
|
||
|
||
Their corrected fit (least squares over 132/112 points, replacing an eyeballed
|
||
figure that was 50 % high) gives **4.287** and **−4.348** px/frame against my
|
||
declared **4.000** and **4.063** px/unit — i.e. **1.072** and **1.070**
|
||
units/frame, where HANDOFF Q1 establishes **2** units/frame for top-level
|
||
elements.
|
||
|
||
🔴 **My port drives everything from one clock.** `boot.gd:375` is
|
||
`view.time_units += delta * view.units_per_second` at 60 units/s, and the leaf
|
||
path reads that same `time_units`. So:
|
||
|
||
| | port cycle | game cycle (at 1.07) | |
|
||
|---|---|---|---|
|
||
| `pteff03` | 10.0 s | **18.7 s** | port **1.87×** too fast |
|
||
| `pteff03a` | 12.0 s | **22.4 s** | port **1.87×** too fast |
|
||
|
||
That is a visible defect in what the boot sequence ships — the title's sweeps
|
||
cross in half the time the game takes.
|
||
|
||
⚠️ **Not changed, and not only out of caution.** `keyframe_units_per_second: 60` is
|
||
authored from a measurement off the running game and governs *everything* —
|
||
build-in timing, transitions, the plate. Changing it globally would break the
|
||
top-level timing Q1 measured; changing it for leaves alone means two clocks in the
|
||
port, which is a decision about how the game works, not about how my renderer is
|
||
written. **That is a Q1 sub-question and it is theirs.**
|
||
|
||
### Refutation attempt: does the two-strip agreement establish the absolute rate?
|
||
|
||
Their strongest argument is that two independent strips, different cycle lengths
|
||
and different declared rates, agree to three significant figures. 🟡 **It is
|
||
weaker evidence than it looks for the *absolute* value.**
|
||
|
||
Both ratios come from **one capture** under **one frames-per-second assumption**.
|
||
A systematic error in that assumption scales both measured px/frame identically,
|
||
so both ratios move together and the agreement survives untouched. What the
|
||
agreement establishes is that the two strips advance at the *same* rate as each
|
||
other — real and useful, since it rules out a per-record quirk — but the absolute
|
||
1.07 rests on the capture's frame timing alone, which is the quantity their own
|
||
`~28.5 fps` note says is not exactly 30.
|
||
|
||
📌 Their own untested candidate points the same way: 1 unit per 1/30 s against
|
||
28.5 fps gives **1.053**, and the gap between that and 1.070 is about the size of
|
||
the frame-rate uncertainty. So the measurement may be saying *"one unit per game
|
||
frame"* exactly, with the residual being how fast the emulator actually ran.
|