port: land the play-tested work, and only that
Takes the port branch up to77320d5e-- the state the human play-tested on 2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio` is 366 commits and 938 files, and most of that must not land. WHAT COMES IN (76 files, all human-confirmed working): * the logo splash animation.08ed3dd1found it: `pose_at` ASSIGNED the settle instant instead of clamping to it, so the splash never animated at all -- and the same bug manufactured a passing harness result, because the harness photographed t past the settle. Confirmed by play-test: "cannot notice any obvious difference from the actual game." * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad binding), stick latched with hysteresis at the game's own 61% digitise threshold. This is what made (A), video-skip and Extras work at all. * menu navigation and flow, menu audio, the exporter, the authored declarations, and 23 verification tools under tools/port/. WHAT IS DELIBERATELY LEFT ON THE BRANCH: * everything afterc0ae460a-- the F5/F6 title-timing investigation, whose own tip commit calls itself a "hand-off for one-minute human checks". Unchecked by definition; it goes through the new review gate like anything else. * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested. * the F1 repeat mechanism, which its own commit calls "deliberately inert". WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED: 545 MB of extracted game content was committed on that branch -- 850 sprite, audio and transcoded video files under `export-probe/` and `export-probe2/`, plus 246 MB of loose .wav and .tsv at the repo root. This repository's own rule, in this file, is "never game content". The rule was not missing. It was written, and it was tightened on that very branch, with a careful comment explaining why BOTH `export/` and `data/base/` had to be listed -- while the exporter was writing to a third name that nobody had thought to list. Enumerating names is the thing that failed. So the ignore rules now describe the SHAPE: any top-level `export*/`, game media by extension, and loose capture output at the root. Verified both ways -- it catches all four offenders and ignores nothing currently tracked. Verified: `cargo check --workspace` clean; all nine GDScript files parse in project context, with a positive control (an injected syntax error is detected, 3 lines) so the clean result means something. `tools/port/check-all` was NOT run -- it needs the container, the export tree and a display.
This commit is contained in:
129
docs/agents/RETRO-2026-08-31-agreed.md
Normal file
129
docs/agents/RETRO-2026-08-31-agreed.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Agreed retro — Port and Decoder, 2026-08-31
|
||||
|
||||
Two self-reviews, one round of mutual attack, and the result both agents accept.
|
||||
|
||||
* Decoder's own review: `docs/agents/RETRO-2026-08-31.md` on `auto/frame-blend-draw-path`.
|
||||
* Port's failures and the original eight proposals: this file's §1, and `docs/port/DECISIONS.md`.
|
||||
|
||||
⚠️ **Nothing here is applied to `PROTOCOL.md`.** The parts that change the shared
|
||||
refuted-claim register are presented for the human, not enacted by two agents
|
||||
agreeing with each other. Both agents remain paused.
|
||||
|
||||
## 1. The single most expensive thing we did
|
||||
|
||||
**We let claims that rest on our own renderer sit in the register as settled
|
||||
refutations.**
|
||||
|
||||
`REFUTED.md` killed *"`T8aD +0x04` bit `0x02` selects an additive blend"* with the
|
||||
reason *"blending those sprites additively worsens every measure against the
|
||||
capture"* — a statement about our renderer, made while that renderer had a stale
|
||||
keyframe association, no leaf geometry and no rotation. The field is real. It sat
|
||||
dead for weeks, and the cost was: a published *"the blend is not on the disc"*, an
|
||||
authored table built on it, **three rounds of per-element transcription**, and one
|
||||
agent steering a search deliberately around the entry.
|
||||
|
||||
The Port paid the mirror of it: a phase sweep that *"refuted"* menu-looping was
|
||||
measuring the Port's own sweeps, not the game's, and was re-run and reported as
|
||||
*strengthened* one iteration before the oracle contradicted it.
|
||||
|
||||
📌 **Neither of us was careless. The rule was in `PROTOCOL.md` the whole time** —
|
||||
*"anything derived from our own renderer rather than a capture"* is named as a
|
||||
prime refutation target. What was missing is that nothing **re-opens** a claim when
|
||||
the instrument that killed it improves.
|
||||
|
||||
## 2. The gap underneath our controls
|
||||
|
||||
`PROTOCOL.md` already requires running an instrument through a control. **We both
|
||||
did, and it did not help**, because:
|
||||
|
||||
> **Our controls verified capability, not configuration.**
|
||||
|
||||
* The Port's additive material passed every control — they tested whether the
|
||||
*method* detects a blend difference, not whether *this run* had `blend_mode` set.
|
||||
It was left at Godot's default, `MIX`. The change predicted a large move and
|
||||
delivered **0.03**, and would have been publishable as a careful negative.
|
||||
* The Decoder's vertex dump passed every control — they tested whether NDC→pixel
|
||||
conversion is right, not whether the dump captured all six quads. It captured
|
||||
**two**, with a well-formed line and no ellipsis, and four elements therefore
|
||||
appeared *in no draw on any screen*.
|
||||
|
||||
## 3. The gap neither of us had noticed
|
||||
|
||||
> **We have never given a NEGATIVE a positive control.**
|
||||
|
||||
Every *"undecodable, with reach"* page lists **where we looked**. Not one shows
|
||||
that the search method **can find a property that is there**. *"Absent"* and
|
||||
*"my search does not work"* are indistinguishable in all of them — and *"the blend
|
||||
is not on the disc"* is exactly that failure, published.
|
||||
|
||||
## 4. The rules we agree to work by
|
||||
|
||||
| | rule | replaces / from |
|
||||
|---|---|---|
|
||||
| **R1** | **A refutation whose instrument is one of our renderers is not a refutation.** It is *"our renderer disagrees"* — 🟡, not ❌. Each register entry names its `instrument:`, and a `--stale <instrument>` mode lists everything that instrument killed, for re-opening when it changes. | Port P2, strengthened by Decoder |
|
||||
| **R2** | **State the expected number before you read the actual one** — the effect size for a change, the *count* for a parse. *"This draw declares 24 indices, so I expect 6 quads."* | Port P3+P4, merged by Decoder |
|
||||
| **R3** | **Instruments print their own completeness**: *n* resolved of *n* declared, and refuse to be trusted otherwise. | Port P4 |
|
||||
| **R4** | **A negative carries a positive control.** Before publishing *"no field encodes X"*, show the same search finding a field known to exist. | Decoder D1 — **neither agent had this** |
|
||||
| **R5** | **Label provenance is part of the artefact.** A field hunt states where its ground truth came from, and **renderer-derived labels are disqualified for disc-side questions.** | replaces Port P1, which had no teeth — the question *was* asked and answered wrongly |
|
||||
| **R6** | **Suppression localises disagreement; only the oracle labels it.** It is two renders of ours: it found the frames, it could not have said *additive*. | Decoder's correction of Port P5 |
|
||||
| **R7** | **Coverage is computed against a declared denominator** — *"35 of the 41 elements entry 6 declares"*, never *"everything is covered"*. | Port P6 + Decoder |
|
||||
| **R8** | **Hold the role line even when the answer looks obvious.** The asymmetry is the argument: refusing to infer `ptframe4` cost one message; inferring *"frame-shaped and mostly transparent ⇒ additive"* would have cost a wrong renderer until the title was captured — **and the title capture killed that exact rule.** | Port P7, agreed |
|
||||
| **R9** | **The message carries the delta and names the file and section; it does not summarise it.** Short messages are safe only when the pointer is precise. | Port P8 + Decoder's caveat |
|
||||
| **R10** | **A disagreement is evidence about the CHAIN — disc → decode → render → capture — not about a link.** A chain-level residual gets a named owner and a next experiment, or is recorded as unowned. | Decoder F |
|
||||
| **R11** | **A cross-agent pointer must fail loudly when it goes stale.** Every staleness incident here was silent. | Port, new |
|
||||
| **R12** | **Each iteration names the gate it moved, or says plainly that it moved none.** | Port, new — see §5 |
|
||||
|
||||
## 5. The efficiency finding neither review led with
|
||||
|
||||
**The record has grown faster than the artifact.** `DECISIONS.md` is past 13 000
|
||||
lines. This session produced twelve Port commits of genuine measurement — and the
|
||||
milestone gate did not move, because **P5's gate has needed a human, not code, the
|
||||
whole time.** Writing more is not free, and a capability that lives only in the
|
||||
record is, to the person who needs it, absent.
|
||||
|
||||
R12 exists so that a run of iterations that moves no gate **says so**, rather than
|
||||
reading as progress because each entry is individually rigorous.
|
||||
|
||||
## 6. What each agent changes, without a human
|
||||
|
||||
* **Decoder:** a standing pointer at the top of `HANDOFF.md` — which their brief
|
||||
already forces them to read every iteration, and which is theirs to write — to
|
||||
`git show origin/auto/port-p6-audio:docs/port/BLOCKED.md`. **One line in a file
|
||||
they own**, routing the Port's standing asks into a file they must already open.
|
||||
This closes a gap `BLOCKED.md` records as having cost three sessions.
|
||||
* **Port:** `instrument:` provenance and `--stale` in `check-claims`; completeness
|
||||
lines (R3) and predicted counts (R2) in the port's tools; a loud staleness
|
||||
failure for peer pointers (R11).
|
||||
|
||||
## 7. What needs the human
|
||||
|
||||
1. ✅ **The register re-classification (R1) — DONE 2026-09-01, by the human**, on
|
||||
`docs/re/REFUTED.md` at the Decoder's tip. All **222** entries now carry an
|
||||
`⟨instrument⟩`; the file opens with a reading guide naming which instruments
|
||||
are ours; R1 is now standing text in `PROTOCOL.md`; and
|
||||
`tools/stale-instrument` is the `--stale` query — run it whenever you improve
|
||||
a renderer, a reader or the harness, and it lists what that instrument killed.
|
||||
|
||||
**Ten entries moved ❌ → 🟡**, each naming what would settle it: eight
|
||||
`render-vs-capture`, one `our-reader`, one `harness`.
|
||||
|
||||
Three things the pass turned up that neither self-review had:
|
||||
|
||||
* **The `rest()` question is open, and had been reading as settled in both
|
||||
directions.** *"rest = last keyframe"* was refuted by the sibling argument;
|
||||
that refutation was then refuted by correlating our render against
|
||||
captures. Both legs run through our renderer, so under R1 neither survives
|
||||
— and which one you believed depended on which entry you found first.
|
||||
🔴 **This one is load-bearing for the port**: `rest()` decides the pose
|
||||
every plateau-less element is drawn at.
|
||||
* **A withdrawal never reached its sibling.** *"2 391 frames, max glyph 0"*
|
||||
was withdrawn because a long-lived `x11grab` stream degrades and then
|
||||
repeats a stale frame. The 1 674-sample negative three lines above it —
|
||||
same probe, same instrument, comparable duration — was left standing as a
|
||||
*reinstated measurement*. §1's lesson, inside the register itself.
|
||||
* **83 of 222 entries — 37 % — record no instrument at all.** Not disputed,
|
||||
not safe: **unauditable**. `stale-instrument unrecorded` is the backfill
|
||||
queue, and it is larger than every other group combined.
|
||||
|
||||
2. **P5's gate** — a person clicking through the port. Unchanged, and it is the
|
||||
only thing standing between the milestone and done.
|
||||
@@ -78,6 +78,13 @@ rec.set_recording_active(false)
|
||||
rec.get_recording().save_to_wav("user://master.wav")
|
||||
```
|
||||
|
||||
**This is implemented.** `godot --path port -- --menu … --audio=/tmp/p6.wav`
|
||||
installs the effect, records for the whole run, and saves on exit — in
|
||||
`_exit_tree` rather than beside each `quit()`, because there are eight of those
|
||||
and the one that would get missed is an error path, i.e. exactly the run whose
|
||||
audio somebody wants to look at. The run prints the driver name beside the file
|
||||
it wrote.
|
||||
|
||||
Then feed that WAV through §1 against the source. That closes the loop: it
|
||||
proves the asset is right **and** that the engine reached it, which no amount of
|
||||
file comparison can show on its own.
|
||||
@@ -109,9 +116,287 @@ silent**, because silence is the failure that looks like success: a WAV of
|
||||
exactly the right duration, full of zeroes, because the application opened a
|
||||
different sink. A duration check alone would pass it.
|
||||
|
||||
## 5. A multichannel capture must pass a provenance check BEFORE it is analysed
|
||||
|
||||
`tools/port/check-capture FILE.wav` — run it first, every time.
|
||||
|
||||
⚠️ **This section exists because a capture of the game's own 6-channel output was
|
||||
analysed at length and the file was corrupt.** It got three controls, a
|
||||
drift test and a written-up negative, and every one of those was sound; none of
|
||||
them could see that channels were missing, because the corruption was upstream of
|
||||
everything they tested.
|
||||
|
||||
**PulseAudio was remapping between two mismatched channel maps, and a 6-channel
|
||||
remap silently drops and duplicates.** The Decoder proved it with a control that
|
||||
needs no emulator and no disc — six channels each carrying a different tone,
|
||||
through the same sink and the same `parec` invocation
|
||||
(`docs/re/audio-capture-channel-map-trap.md`):
|
||||
|
||||
| ch | played | recorded |
|
||||
|---|---|---|
|
||||
| 0 | 400 | 400 |
|
||||
| 1 | 800 | **3200** |
|
||||
| 2 | 200 | 200 |
|
||||
| 3 | 1600 | **800** |
|
||||
| 4 | 3200 | **800** |
|
||||
| 5 | 6400 | **200** |
|
||||
|
||||
**Two source channels were gone entirely** and two were duplicates. Setting the
|
||||
sink's `channel_map` to the guest's own (`FL,FR,FC,LFE,RL,RR`) and passing the
|
||||
same map to `parec` returns all six.
|
||||
|
||||
### The signature is an exact duplicate pair, and only a hash finds it
|
||||
|
||||
Duration is right. Channel count is right. `Corked: no`. There is no error
|
||||
anywhere, and the **per-channel levels look entirely reasonable** — which is the
|
||||
whole difficulty. In the tool's own known-bad control, all six channels report a
|
||||
peak of **−18.063656 dB, identical to six decimals, while containing three
|
||||
duplicate pairs.** A level check cannot see this. Hashing each channel can.
|
||||
|
||||
Two channels of a real surround mix are never byte-identical over tens of
|
||||
seconds. On the corrupt game capture the tool reports:
|
||||
|
||||
```
|
||||
ch2 peak -4.466272 ba497de78217c438a3e430c5ef6b951b
|
||||
ch5 peak -4.466272 ba497de78217c438a3e430c5ef6b951b
|
||||
🔴 ch2 and ch5 are BYTE-IDENTICAL
|
||||
```
|
||||
|
||||
⚠️ **It is a necessary check, not a sufficient one.** Passing says the file has no
|
||||
duplicated channels. It says nothing about whether the right thing was recorded —
|
||||
that is what §1's correlation against a known source is for, and a capture should
|
||||
survive **both** before anything is concluded from it.
|
||||
|
||||
### Two more conditions, learned the same way
|
||||
|
||||
* **Start the recorder before the process you are capturing**, so `t = 0`
|
||||
precedes it and the window certainly contains the moment of interest.
|
||||
* **Log what was on screen, with timestamps keyed to the recording's own clock.**
|
||||
A capture that matches nothing is then diagnosable rather than ambiguous; the
|
||||
corrupt one could not be told apart from "recorded the wrong phase of the boot"
|
||||
by any amount of analysis at this end.
|
||||
|
||||
And the failure this page already warns about, in a second costume:
|
||||
`run-canary` is silent **twice over** — `SDL_AUDIODRIVER=dummy` *and*
|
||||
`--mute=true`. Fix only the first and Canary attaches a healthy 6-channel stream
|
||||
at 100 % volume, reports `Corked: no`, and emits a 19 MB WAV of zeroes.
|
||||
|
||||
## 7. A capture can be starved — right duration, holes punched through it
|
||||
|
||||
`check-capture` tests this too, and it is the second way a recording looks
|
||||
perfect and carries nothing.
|
||||
|
||||
**A monitor sink advances at wall-clock rate and substitutes silence whenever the
|
||||
producer is late.** An emulator running below real time therefore yields a file
|
||||
of exactly the right duration, the right channel count, no duplicated channels —
|
||||
chopped into fragments with holes between them, thousands of times over.
|
||||
|
||||
Measured independently on the capture that prompted this (the Decoder's numbers
|
||||
on the untruncated original in brackets):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| frames silent on **all six** channels | **35.6 %** [39.3 %] |
|
||||
| alternating runs | **10 482** [10 595] |
|
||||
| median burst / gap | **13.5 ms / 3.9 ms** [13.6 / 3.9] |
|
||||
| period | **17.4 ms → 57 Hz** [≈17.5 ms → 57 Hz] |
|
||||
|
||||
⚠️ **This destroys envelope correlation by construction.** What dominates the
|
||||
envelope of such a file is the dropout schedule, not the content — so §6's method
|
||||
was working correctly on a file that could not carry the signal, and the negative
|
||||
it produced said nothing about the game.
|
||||
|
||||
### Two thresholds I invented were wrong, and the controls caught both
|
||||
|
||||
1. **Counting exact-zero frames.** Real audio crosses zero constantly, so 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 of ≥ 1 ms count.
|
||||
2. **Gap count and median length.** A genuine music-and-effects bed has **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.
|
||||
|
||||
3. 🔴 **The gap RATE alone.** This one shipped, and the Decoder found it: raising
|
||||
the client buffer keeps cutting the rate while total silence **bottoms out and
|
||||
then doubles**, because an over-large buffer starves in a few enormous holes
|
||||
instead of many small ones. Its `PULSE_LATENCY_MSEC=500` capture scores
|
||||
**1.3 gaps/s — better than a genuine music bed at 3.3 — while being 50 %
|
||||
silence**, and a 20/s bar passed it.
|
||||
|
||||
**It takes two numbers, because either one alone is blind to the failure next
|
||||
door** — the same shape as a level table that cannot see a duplicated channel.
|
||||
Reproduced on a file held here (`bigholes`: a real bed with 350 ms holes punched
|
||||
into it) so the regime is controlled rather than quoted:
|
||||
|
||||
| control | all-channel silence | gaps/s | verdict |
|
||||
|---|---|---|---|
|
||||
| real music+SFX bed | 1.1 % | 3.3 | **PASS** |
|
||||
| voice track, mono, real pauses | 53.2 % | 0.3 | **PASS** |
|
||||
| bed with 350 ms holes | **46.3 %** | 3.2 | **FAIL** |
|
||||
| the starved capture | **35.6 %** | 30.9 | **FAIL** |
|
||||
|
||||
Rate alone cannot separate rows 2 and 3; silence alone cannot separate rows 1 and
|
||||
3. **The pair does:** fail when ≥ 10 % of the file is silent on every channel
|
||||
*and* there is at least 1 gap per second. Real audio is either mostly not silent,
|
||||
or silent in a few long stretches — not both at once.
|
||||
|
||||
### A format it cannot read is refused, not guessed at
|
||||
|
||||
Everything in the starvation check assumes 16-bit signed. An ALSA `type file` tee
|
||||
writes **float32** (`SND_PCM_FORMAT_FLOAT_LE`), and read as s16 that produces a
|
||||
*plausible-looking* file — the Decoder measured one, and its only tell was
|
||||
per-channel peaks alternating **exactly**, which is the two halves of each float
|
||||
landing in alternate channels.
|
||||
|
||||
So an unreadable format ends the run at **`PARTIAL`** (exit 2), not `PASS`:
|
||||
channels were checked, starvation was not, and the tool says which. A checker
|
||||
that claims a check it skipped is the shape of every failure this file documents.
|
||||
|
||||
⚠️ **`WAVE_FORMAT_EXTENSIBLE` (tag `0xFFFE`) is accepted at 16 bits**, and the
|
||||
first version of the guard was not — it rejected one of this tool's own controls,
|
||||
a file `ffprobe` correctly calls `pcm_s16le`. **A format guard that refuses a
|
||||
legitimate capture is the same defect as one that mis-reads an illegitimate one**,
|
||||
pointing the other way. The check turns on `wBitsPerSample`, which is what
|
||||
actually decides the sample layout; a float tee is 32-bit and is still caught.
|
||||
|
||||
### The control sweep, which is the tool's real specification
|
||||
|
||||
**Run it: `tools/port/check-capture-controls`.** 🔴 Until 2026-08-30 this table was prose — the specification existed and nothing executed it, so a regression in `check-capture` or a drifting threshold would have gone unremarked in a tool whose own history is *two invented thresholds that were both wrong and were caught only by controls*. This document states the principle it was breaking: **"a control that does not execute is not a control."**
|
||||
|
||||
⚠️ The verdicts below are **compressed**. `check-capture` emits two — one for channel provenance, one for starvation — and the sweep asserts the pair, because the voice control is `PASS` on channels and `UNJUDGED` on starvation *by design* and a single word cannot say that. A starved file **short-circuits** before the channel check, which the sweep records as `n/a` rather than as a failure: *the check did not run* and *the check failed* are different facts.
|
||||
|
||||
⚠️ The **starved capture cannot be rebuilt** — that artifact was transient and is gone. The sweep reports it `MISSING` rather than omitting it, and deliberately does not synthesise one from the statistics published above: a control fitted to the answer it must give is not a control either.
|
||||
|
||||
| file | verdict |
|
||||
|---|---|
|
||||
| real music+SFX bed | `PASS` |
|
||||
| voice track, mono, 53 % real pauses | `PASS` |
|
||||
| six distinct tones (PCM and extensible) | `PASS` |
|
||||
| bed with 350 ms holes punched in | **`FAIL`** |
|
||||
| the starved capture | **`FAIL`** |
|
||||
| the same tones as float32 | **`PARTIAL`** |
|
||||
|
||||
### ⚠️ The regime this tool cannot judge, and says so
|
||||
|
||||
**High silence with very few gaps is what a real voice track looks like (53.2 %
|
||||
in 0.3 gaps/s) and also what an over-buffered capture looks like.** No statistic
|
||||
here separates them. The tool prints `UNJUDGED` and tells you to check the file
|
||||
against a known source rather than passing it silently — because inventing a bar
|
||||
for a regime with no control in it is how the two bars above came to be wrong.
|
||||
|
||||
⚠️ **A control that does not execute is not a control.** An earlier version
|
||||
returned immediately for a single-channel file, so the mono voice track — one of
|
||||
the four controls — was never actually run through the check it was meant to
|
||||
control. Mono now skips only the duplicate test.
|
||||
|
||||
### 🟡 The monitor-sink route may be fixable after all — retry before rebuilding
|
||||
|
||||
An earlier version of this section said the route *"cannot be fixed by
|
||||
configuration"*. **Withdrawn.** That inferred from the holes that the guest runs
|
||||
below real time, without testing the alternative: **the client buffer is simply
|
||||
tiny.** Xenia asks SDL for 256 samples — **5.33 ms** at 6 ch — against a stock
|
||||
`daemon.conf` with no fragment tuning.
|
||||
|
||||
| client buffer | silence | gaps/s |
|
||||
|---|---|---|
|
||||
| Xenia default (~5.3 ms) | 39.3 % | 30.5 |
|
||||
| `PULSE_LATENCY_MSEC=200` | **15.6 %** | 3.5 |
|
||||
| `PULSE_LATENCY_MSEC=500` | 50.1 % | 1.3 |
|
||||
|
||||
⚠️ Not clean, and not like-for-like — 88 s against 347 s, and the short run covers
|
||||
the splash logos where silence is real. But **the capture route deserves a retry
|
||||
at ~200 ms before anyone spends a session on a Canary rebuild.**
|
||||
|
||||
### The tap, if configuration is not enough
|
||||
|
||||
`parec` reads a monitor that advances at wall-clock rate and substitutes silence,
|
||||
so **every moment the emulator runs below real time is a hole**, and the timebase
|
||||
is warped non-uniformly — deleting the silences compresses time unevenly rather
|
||||
than repairing it. The route that would work is an **internal tap at
|
||||
`SDLAudioDriver::SubmitFrame`**, which sees every frame the guest produces in
|
||||
guest order with no wall clock in the loop.
|
||||
|
||||
⚠️ That needs a Canary rebuild, and the Decoder has costed it: `build-canary`
|
||||
targets a source root that does not exist in that container, the warm build tree
|
||||
is configured against the same missing path, so any change is a full reconfigure
|
||||
plus a full compile on a box with ~700 MB free and a history of parallel builds
|
||||
OOM-killing the host. **A whole session for one probe** — the human's call, not
|
||||
an agent's.
|
||||
|
||||
### And a header that never got patched
|
||||
|
||||
A streaming writer leaves `data` declaring **0 bytes**. `check-capture` says so
|
||||
and tells you the duration is unverified — which is not pedantry: the file shared
|
||||
here was **copied while it was still being written**, and the provenance claim
|
||||
that came with it was wrong about both its length and what it contained.
|
||||
|
||||
## 6. Finding one component inside a mix — and why §1's method cannot
|
||||
|
||||
🔴 **This section begins with a retraction.** Two captures of the game's own
|
||||
output were analysed with sliding envelope cross-correlation and declared not to
|
||||
contain the intro's audio. **The instrument was never controlled for the actual
|
||||
task**, and when it finally was, it failed:
|
||||
|
||||
> Can it find the movie's bed inside a synthetic mix of that bed plus the three
|
||||
> voice streams? **r = 0.415** — below the `r > 0.8` bar those negatives were
|
||||
> judged against.
|
||||
|
||||
The first negative happened to be right (the file was independently proved
|
||||
corrupt by a tone control). **It was right by luck, and the reasoning behind it
|
||||
was not supported.** A filter that fails its own known-positive is dead, not
|
||||
tuneable.
|
||||
|
||||
### What was wrong: the threshold, not the idea
|
||||
|
||||
`r > 0.8` was calibrated on **clean-against-clean** comparisons, where it is
|
||||
correct — a transcode against its source scores 1.000. A *component inside a
|
||||
mix* can never score that, because everything else in the mix is uncorrelated
|
||||
noise from the component's point of view. Judging one task by the other's bar
|
||||
guarantees a false negative.
|
||||
|
||||
**Judge on the LAG and the MARGIN instead.** A real match lands at the *right*
|
||||
lag with a clear gap to the runner-up; a false one is a plateau. And **band-limit
|
||||
first**, so the component you are hunting dominates what you measure.
|
||||
|
||||
### The calibration, on a known-present and a known-absent pair
|
||||
|
||||
Both bands, both directions, envelope at 0.1 s, minimum 60 s overlap:
|
||||
|
||||
| hunting | band | against | *r* | lag | **margin** |
|
||||
|---|---|---|---|---|---|
|
||||
| the movie bed | 40–180 Hz | mix containing it | 0.663 | **0.0 s** ✓ | **+0.111** |
|
||||
| the movie bed | 40–180 Hz | voice-only mix | 0.262 | −31.9 s ✗ | +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 | −58.4 s ✗ | +0.005 |
|
||||
|
||||
**A 20–50× separation in the margin, and the lag is right or absurd.** That is a
|
||||
decision rule set by controls rather than by tuning until the data agreed —
|
||||
which is the distinction that matters, and the one the first version of this
|
||||
method skipped.
|
||||
|
||||
⚠️ **Reach.** The known-positive is a *synthetic* mix at equal gains. A real game
|
||||
mix weights its components differently, so this bounds the method rather than
|
||||
modelling the real case exactly. It is enough to separate present from absent; it
|
||||
is not a level measurement.
|
||||
|
||||
## What none of this establishes
|
||||
|
||||
That it *sounds right*. Every method here shows correspondence to a source, not
|
||||
that the source is the audio the game plays at that moment, and not that levels
|
||||
are sane in a mix. A ten-second human listen still answers something no
|
||||
measurement above does — so when a result rests on one of these, say which one.
|
||||
|
||||
## 4. What the exporter checks, so nobody has to remember to
|
||||
|
||||
`sylpheed-export` measures **peak level and duration** of every audio file it
|
||||
writes and records both in `manifest.json`; `sylpheed-export check` refuses a
|
||||
tree whose peak is ≤ −90 dBFS (silent) or ≥ 0 dBFS (clipping).
|
||||
|
||||
Those are content checks in a format validator on purpose. Silence is the failure
|
||||
this page opens by naming — right duration, right channel count, right size, full
|
||||
of zeroes — and every structural check passes it. Clipping is the other one, and
|
||||
the BGM can produce it, because a music bank is two stems summed at unity gain
|
||||
(HANDOFF Q10).
|
||||
|
||||
⚠️ Neither says the audio is the **right** audio. `docs/port/BLOCKED.md` says
|
||||
which bindings are measured and which are still authored, and no measurement on
|
||||
this page can move a row there.
|
||||
|
||||
1034
docs/port/BLOCKED.md
1034
docs/port/BLOCKED.md
File diff suppressed because it is too large
Load Diff
15797
docs/port/DECISIONS.md
15797
docs/port/DECISIONS.md
File diff suppressed because it is too large
Load Diff
@@ -147,7 +147,7 @@ for a long time.
|
||||
> grossly (`ptlogo_back2`, 1118×262, pivot 500,117 where half is 559,131). It is
|
||||
> not a problem for this port — the exporter emits the declared pivot and never
|
||||
> derives one — but it is a claim a consumer should not lean on. Raised in
|
||||
> `docs/BLOCKED.md`.
|
||||
> `docs/port/BLOCKED.md`.
|
||||
|
||||
**`sprite`** / **`focus_sprite`** are paths relative to `export/`. The highlight
|
||||
pairs **by name** on the sprite — `ptbtn01.t32` ↔ `ptbtn01f.t32` — which is 🟡 a
|
||||
@@ -212,10 +212,25 @@ from: the quad is `pivot × 2`, and its colour is the keyframe's `fade_argb`.
|
||||
|
||||
**`keyframes`** carry the on-disc time verbatim in `t`. A keyframe is the
|
||||
**start of a ramp toward the next**, not a pose that is held, and the ramp is
|
||||
linear. The **last keyframe of a group has no `t`** — the disc has no time slot
|
||||
there — and a file that puts one on it is wrong, not merely odd. The unit of `t`
|
||||
is measured, not on the disc, and so lives in `authored/` and is applied in
|
||||
exactly one place.
|
||||
linear.
|
||||
|
||||
🔴 **Every keyframe has a `t`, including the last**, and this paragraph said the
|
||||
exact opposite until 2026-08-29. A placement group is an 8-byte header followed
|
||||
by `frames` × `{u32 time; 36-byte pose}`, so **pose 0's time is the group's
|
||||
lead-in word** and no pose is untimed. The old reading — that a group's data
|
||||
stopped four bytes short of its final block's time slot — paired every pose with
|
||||
the *next* pose's time, and `sylpheed-export check` enforced it as a rule. A file
|
||||
with an untimed keyframe is now the wrong one.
|
||||
|
||||
⚠️ Two things went with that correction. The **exit ramp is gone**: there is no
|
||||
untimed final keyframe to give a synthetic time to, so `authored/timing.json`'s
|
||||
`exit_ramp_units` — an authored *measured* constant since P3 — is **deleted**,
|
||||
which is what MISSION §3 means by a deletion being the measure of progress. And
|
||||
`rest.t` moved on several screens: `publisher_logo` settles at t=30 rather than
|
||||
t=235.
|
||||
|
||||
The unit of `t` is still measured rather than on the disc, so it stays in
|
||||
`authored/` and is applied in exactly one place.
|
||||
|
||||
**`rotation_deg`** is screen-plane rotation in degrees, clockwise-positive,
|
||||
decoded from the keyframe's `+12`. **The game renders it**, confirmed twice by
|
||||
@@ -257,7 +272,7 @@ keyframe *k* means the screen spends that time *arriving at* `k+1`.
|
||||
> A consumer that wants the pose after arrival should therefore take **the last
|
||||
> timed keyframe**, not `rest`. `rest` is kept in the format because it is what
|
||||
> the pinned decoders say and removing it would hide the disagreement — see
|
||||
> `docs/DECISIONS.md`. The format is unchanged at **v2**: no field changed
|
||||
> `docs/port/DECISIONS.md`. The format is unchanged at **v2**: no field changed
|
||||
> meaning, this is a warning about one of them.
|
||||
|
||||
**`paint_order`** is back-to-front, as declaration indices, and is a permutation
|
||||
@@ -309,13 +324,56 @@ reaches which entry is Q4 and is not).
|
||||
"disc": "/disc",
|
||||
"screens": [{ "name": "main_menu", "file": "screens/title/main_menu.json",
|
||||
"sprites": 18, "missing_sprites": [] }],
|
||||
"video_transcode": "ffmpeg -i ADV.wmv -c:v libtheora -q:v 8 -c:a libvorbis -q:a 5 ADV.ogv",
|
||||
"videos": [{ "name": "ADV", "file": "video/ADV.ogv",
|
||||
"command": "ffmpeg -i …", "why": "HANDOFF Q9: …" }],
|
||||
"audio": [{ "kind": "se", "name": "move", "file": "audio/se/move.ogg",
|
||||
"command": "ffmpeg -i …", "why": "HANDOFF Q8, measured: …",
|
||||
"peak_dbfs": -3.2, "duration_s": 0.533,
|
||||
"name_match": "SE_UI_CURSOR" },
|
||||
{ "kind": "bgm", "name": "main_menu", "file": "audio/bgm/main_menu.ogg",
|
||||
"command": "ffmpeg -i …", "why": "AUTHORED, an arbitrary choice: …",
|
||||
"peak_dbfs": -1.1, "duration_s": 173.8, "loop_mode": "restart" }],
|
||||
"warnings": ["GP_READY_ROOM not exported -- out of scope"]
|
||||
}
|
||||
```
|
||||
|
||||
`video_transcode` will record the exact command so a modder can re-run it rather
|
||||
than reverse-engineer what was done. It is absent until P4 writes a video.
|
||||
`videos` and `audio` are **absent** until a milestone writes one, rather than
|
||||
present and empty: an empty array reads as "we looked and there is none", and
|
||||
that is not what an export taken before P4 or P6 means.
|
||||
|
||||
### `command` and `why`, on every media entry
|
||||
|
||||
`command` is the exact ffmpeg invocation that produced the file. MISSION §6: a
|
||||
modder who dislikes the quality re-runs one line rather than reverse-engineering
|
||||
what was done to their asset. `why` is where the value came from, in the
|
||||
project's three-way vocabulary — **decoded** off the disc, **measured** off the
|
||||
running game, or **chosen**. A `why` that does not say which of those it is has
|
||||
not done its job.
|
||||
|
||||
### `audio`, field by field
|
||||
|
||||
| field | |
|
||||
|---|---|
|
||||
| `kind` | `se`, `bgm` or `voice`. The runtime dispatches on it, so it is a field rather than a prefix on `name` that a consumer would have to parse |
|
||||
| `name` | the **role**, not the disc asset: `move`, `confirm`, `back`, `main_menu`. Which bank plays a role is authored and expected to change; a rename on the disc side must not be a change to the Godot project. ⚠️ **`voice` is the exception and keys by MOVIE NAME** (`ADV`, `S00A`), because there is no role to name: the binding of recording to picture came off the disc's own movie manifest, so unlike a music bed nothing about it was chosen |
|
||||
| `peak_dbfs` | measured off the finished file. **Required.** Silence is the audio failure that looks like success — right duration, right channel count, right size, full of zeroes — and clipping is the other one, which the BGM can produce because it is a sum of two stems at unity gain. `sylpheed-export check` refuses a tree whose peak is ≤ −90 dBFS, and applies a **kind-dependent** upper bound. 🔴 This paragraph used to state a flat *≥ 0 dBFS* and was wrong about the port's own export: `confirm` ships at **+0.18** and the `ADV` voice at **+0.31**, so a consumer implementing a validator from this file would have rejected a valid tree. The rule is: 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, and a lossy decode of it overshoots by a fraction of a dB — those are allowed to **+1.0 dB**. ⚠️ The +1.0 is a judgement, not a measurement: a few tenths is reconstruction overshoot and a whole dB is not, and if a cue ever trips it the right response is to measure the overshoot distribution, not to loosen the bound |
|
||||
| `duration_s` | measured off the finished file, so that a claim about a cue's length can be checked against the finding that produced it |
|
||||
| `name_match` | the game's own cue identifier **guessed by name**. Absent means nobody claimed one — never that the binding is unknown. The binding is the measured part; the name is not |
|
||||
| `loop_mode` | what the runtime does at the end of the file, where that was authored. Absent on a cue: a cue ends |
|
||||
|
||||
**A `voice` entry is a cutscene's dialogue, and it is a separate file on
|
||||
purpose.** On this disc a movie's `.wmv` carries music and effects only; the
|
||||
voice is a byte region of one continuous XMA stream in `sound.pak`, bound by the
|
||||
movie manifest. A consumer plays the two together, **from the same instant** —
|
||||
there is no offset and none is authored. A movie with no `voice` entry is
|
||||
genuinely unvoiced, which is the honest answer for most `hokyu_*` cutscenes;
|
||||
nothing is substituted, and the manifest carries a warning naming the movie.
|
||||
|
||||
⚠️ The `why` on a `voice` entry names every region chunk the exporter **dropped**
|
||||
and its measured length. That is not commentary: which chunks of a region are the
|
||||
track is an open decoding question (see `docs/port/BLOCKED.md`), and a consumer
|
||||
reading a shorter file than it expected should be able to see what was left out
|
||||
rather than infer it.
|
||||
|
||||
## Changes from v2
|
||||
|
||||
|
||||
@@ -97,15 +97,15 @@ A milestone is done when its **artifact** exists, not when the code compiles.
|
||||
|---|---|---|
|
||||
| **P0** | Exporter skeleton; one screen and its sprites to `export/` | `export/screens/title/main_menu.json` validates against FORMAT.md and the PNGs open |
|
||||
| **P1** | Godot renders that screen statically at 1280×720 | A Godot screenshot beside `sylpheed-cli screen render` of the same build — they should agree, and where they do not, say which is wrong |
|
||||
| **P2** | Keyframe animation | Buttons slide in. **Blocked on HANDOFF Q1** (the time unit). Do not invent it |
|
||||
| **P2** | Keyframe animation | Buttons slide in. ~~Blocked on HANDOFF Q1 (the time unit). Do not invent it~~ — **Q1 is answered**: ramp linear, 2 units per rendered frame, 1 unit = 1/60 s. Gate met |
|
||||
| **P3** | Splash → title, with the transition | Both screens back to back, unattended |
|
||||
| **P4** | Intro video | `ADV.wmv` plays with audio (§6) |
|
||||
| **P5** | Main menu: navigation, focus states, Ⓐ into a submenu, B back | ~~A human clicks through it~~ — ✅ **GATE MET 2026-09-02.** A human walked it: *"Menu walk and navigation is fine. Video skips too. Extras open."* [`../agents/PLAYTEST-2026-09-02-menus.md`](../agents/PLAYTEST-2026-09-02-menus.md) |
|
||||
| **P6** | Audio — menu BGM and move/confirm SFX | Sound on the P5 gate. **Looping is blocked on HANDOFF Q10** |
|
||||
| **P6** | Audio — menu BGM and move/confirm SFX | Sound on the P5 gate. ~~Looping is blocked on HANDOFF Q10~~ — **Q10 is answered**: two stems of one performance, played together. 🔴 **Gate NOT claimed**: the same play-test found the SFX mix wrong (F2), and "sound on the P5 gate" means the right sound. |
|
||||
| **P7** | New-game intro video after NEW GAME | Plays, then returns to a defined state |
|
||||
|
||||
Work the lowest unfinished milestone. When one is blocked on an RE answer, say so
|
||||
in `docs/BLOCKED.md`, and take the next milestone that is not.
|
||||
in `docs/port/BLOCKED.md`, and take the next milestone that is not.
|
||||
|
||||
## 6. The video problem
|
||||
|
||||
|
||||
158
docs/port/RUNNING.md
Normal file
158
docs/port/RUNNING.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Running the port
|
||||
|
||||
**P5's gate is *"a human clicks through it"*, and until now there was no page
|
||||
telling a human how.** The commands existed — in `boot.gd`'s header comment and
|
||||
scattered through a twelve-thousand-line `DECISIONS.md`. A capability that lives
|
||||
only in the record is, to the person who needs it, absent.
|
||||
|
||||
Everything below has been run. Where a number is quoted it was measured in this
|
||||
container, and where the container distorts it that is said rather than left for
|
||||
the reader to discover.
|
||||
|
||||
## 1. Build the asset tree
|
||||
|
||||
The Godot project reads `export/`, never the disc.
|
||||
|
||||
```bash
|
||||
cargo run --release -p sylpheed-export -- export --disc /disc --out export
|
||||
```
|
||||
|
||||
Roughly four minutes, most of it transcoding two movies. It **rewrites `export/`
|
||||
wholesale** — never hand-edit anything in there; hand-written decisions live in
|
||||
`authored/` beside it, and survive a re-export.
|
||||
|
||||
## 2. The P5 walk, from a cold start
|
||||
|
||||
```bash
|
||||
godot --path port -- --boot --play
|
||||
```
|
||||
|
||||
This is the one a human should judge. It boots the way the game does — two
|
||||
splashes, the `ADV` intro, the title — hands over to the menu on Ⓐ, and then
|
||||
**stays live and waits for input**.
|
||||
|
||||
| you press | what should happen |
|
||||
|---|---|
|
||||
| Ⓐ on the title | the main menu opens on **NEW GAME** |
|
||||
| ⬆ / ⬇ | one item, wrapping at both ends |
|
||||
| ⬅ / ➡ | **nothing** — measured, and implemented as an explicit no-op |
|
||||
| Ⓐ on **EXTRAS** | the EXTRAS submenu, opening on **MISSION SELECT** |
|
||||
| Ⓑ in EXTRAS | back to the main menu, **on the item you left** |
|
||||
| Ⓑ on the main menu | back to the title |
|
||||
| Ⓐ on the title again | the menu, **still on the item you left** |
|
||||
|
||||
That last row is the one worth checking deliberately: the main menu **remembers
|
||||
its cursor**, and every submenu **resets** to its own opening item. Both are
|
||||
measured, and they disagree on purpose.
|
||||
|
||||
⏱ **The intro is ~157 s.** To skip straight to the menu:
|
||||
|
||||
```bash
|
||||
godot --path port -- --menu=main_menu
|
||||
```
|
||||
|
||||
and to drive it unattended:
|
||||
|
||||
```bash
|
||||
godot --path port -- --menu=main_menu --script=down,down,down,down,accept,cancel
|
||||
```
|
||||
|
||||
🔴 **This example used to say `down,down,accept,cancel`, and it walked the wrong
|
||||
path.** Two ⬇ from the opening item lands on **`TUTORIAL`**, whose destination this
|
||||
export does not carry — so the example exercised a *not-carried* message and
|
||||
returned, never opening a submenu at all. **`EXTRAS` is the fifth item**, so it
|
||||
takes four. The one submenu P5's gate rests on was the one the runbook's own
|
||||
command did not reach. Verified 2026-08-31 by running both.
|
||||
|
||||
🔴 `--script` **without** `--play` or `--menu` refuses and says so. It used to
|
||||
parse, be stored, and do nothing.
|
||||
|
||||
## 3. What is knowingly missing — not bugs
|
||||
|
||||
Four of the five main-menu destinations are **measured but not in this export**:
|
||||
they live in other archives (`GP_SAVE_LOAD`, `GP_OPTIONS`, …). Pressing Ⓐ on them
|
||||
prints what it would have opened and why it cannot:
|
||||
|
||||
```
|
||||
(LOAD GAME) opens a screen this export does not carry:
|
||||
The save-slot list is GP_SAVE_LOAD, not in this export. Destination MEASURED.
|
||||
```
|
||||
|
||||
**EXTRAS is the only Ⓐ-into-a-submenu this milestone can walk**, which is why the
|
||||
P5 gate rests on it.
|
||||
|
||||
`NEW GAME` is a deliberate gap of a different kind: the real chain is
|
||||
NEW GAME → DIFFICULTY → SELECT DATA → the `S00A` movie, and the port **jumps to
|
||||
the movie**, printing the two screens it skipped. That is a gap, stated out loud;
|
||||
nobody should read the port's behaviour there as the game's.
|
||||
|
||||
## 4. What this container distorts
|
||||
|
||||
* **No GPU.** 720p Theora decodes **+6.7 % … +6.9 % slower than real time** here
|
||||
(5 runs, both movies, on a quiet box). The boot's printed seconds carry that
|
||||
deficit. It is a property of the machine, not of the port.
|
||||
* **No sound card.** Godot falls back to a dummy driver, so **you will hear
|
||||
nothing**. The audio is present and measurable —
|
||||
`docs/port/AUDIO-VERIFICATION.md` answers every audio question without a
|
||||
device, and `tools/port/verify-menu-audio` asserts it — but *"I heard it"* is
|
||||
not available in here.
|
||||
* **A leaked-object warning at exit** is engine-side, not the port's. Measured:
|
||||
releasing every reference the port owns moves the count from 8 to 8.
|
||||
|
||||
## 5. Modding
|
||||
|
||||
`data/mods/` shadows `export/` by path. Each override is announced as it is read,
|
||||
and at the end of a run any file that **can never apply** is listed:
|
||||
|
||||
```
|
||||
mod: sprites/title/main_menu/ptbase.png <- data/mods/...
|
||||
mods: 1 file(s) in data/mods can shadow NOTHING -- no such path in the export:
|
||||
inert: sprites/title/TYPO_menu/pteff05.png
|
||||
```
|
||||
|
||||
A file whose path exists in the export but was simply not read this run is **not**
|
||||
listed. See `docs/port/MODDING.md` for the five rules the asset tree keeps.
|
||||
|
||||
## 6. Where the work is, and what P5's gate is waiting on
|
||||
|
||||
**P5's gate is the only one that needs a person, and it is not waiting on code.**
|
||||
|
||||
Everything above runs from `auto/port-p6-audio`.
|
||||
|
||||
🔴 **This section used to quote counts — "256 commits ahead, 58 files" — and they
|
||||
were stale the moment they were committed, because committing them incremented
|
||||
the count.** By the time anyone read it, it said 256 and the answer was 258. A
|
||||
number written into a document meant to inform a decision **decays with every
|
||||
commit either agent makes**, and the Decoder hit the same thing in their own
|
||||
merge-state page one message after recording the class.
|
||||
|
||||
**So what follows are the invariants, which do not move, and the commands to
|
||||
re-derive anything that does.**
|
||||
|
||||
| invariant | check | **passes when** |
|
||||
|---|---|---|
|
||||
| `main` is an **ancestor** of this branch — a fast-forward, nothing to resolve | `git merge-base --is-ancestor origin/main HEAD; echo $?` | prints **`0`**. ⚠️ The command itself prints **nothing** on success — without the `echo` a reader cannot tell success from failure |
|
||||
| `main` is an ancestor of the Decoder's branch too | `git merge-base --is-ancestor origin/main origin/auto/build-ordinal-audit; echo $?` | prints **`0`**, same caveat |
|
||||
| the two change sets touch **zero files in common** | `comm -12 <(git diff --name-only origin/main...HEAD \| sort) <(git diff --name-only origin/main...origin/auto/build-ordinal-audit \| sort) \| wc -l` | prints **`0`** |
|
||||
| merging both produces **no conflicts** | `git merge-tree --write-tree HEAD origin/auto/build-ordinal-audit \| wc -l` | prints **`1`** — one line is the tree id; conflicts would follow it. **Read-only: this merges nothing** |
|
||||
|
||||
**Last run here: `0`, `0`, `0`, `1`.** A command published without a pass
|
||||
condition is half a check — the reader gets a number and no way to know whether it
|
||||
is the right one — so each row states what the right one is.
|
||||
|
||||
📌 **So the sentence is not "N commits behind", which sounds like something to
|
||||
schedule. It is: two fast-forwards over disjoint file sets, mergeable in either
|
||||
order with zero conflicts.** Counts if you want them:
|
||||
`git rev-list --count origin/main..HEAD`.
|
||||
|
||||
### What a person is actually being asked to do
|
||||
|
||||
1. `godot --path port -- --boot --play`, then walk §2's table.
|
||||
2. Say whether it behaves as described. **Not whether it matches the game** —
|
||||
that comparison is the oracle's job and is already asserted by
|
||||
`tools/port/check-all`.
|
||||
3. If it does, P5's gate is met and nothing else is blocking P6, which asserts its
|
||||
own audio and has no human step.
|
||||
|
||||
⚠️ **You will hear nothing** (§4), and the intro takes ~157 s. `--menu=main_menu`
|
||||
skips straight to the part being judged.
|
||||
228
docs/port/blend-decoded-adoption.md
Normal file
228
docs/port/blend-decoded-adoption.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# The blend map is deleted — and adopting the decoded field found a counter-example
|
||||
|
||||
**Status:** ✅ **adopted.** ❌ **My counter-example failed — the bit is right and the
|
||||
regression is a metric artefact.** See the last two sections.
|
||||
Port at `7dd754f` + this commit; formats pinned at `formats-pin-2026-09-01`;
|
||||
HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
`PORT-MISSION` §3: *"When the RE agent later decodes something you had authored,
|
||||
delete the authored entry and let the exporter emit it. That deletion is the
|
||||
measure of progress."* This is that deletion.
|
||||
|
||||
## What changed
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| source | `authored/rendering.json` → `additive_elements`, keyed by **screen name** | `blend_additive` per element, emitted by the exporter |
|
||||
| origin | transcribed from the Decoder's per-draw `RB_BLENDCONTROL0` log | **decoded** — `T8aD +0x04` bit `0x02` |
|
||||
| reach | three screens somebody drove the game to | every screen on the disc |
|
||||
|
||||
The pin bump is its own commit (`7dd754f`). The exporter emits `blend_additive`
|
||||
on `Element` **and** on nested focus/leaf elements — both spellings of the
|
||||
accessor are needed, because a button's focused variant is reached through
|
||||
`focus_link` and `ptbtn00f.t32` is in `build.sprites` while no element carries it
|
||||
as `sprite`. `ptbtn00f` is exactly the sharp case: the plate is alpha-over and
|
||||
its own glow is additive, on one screen in adjacent draws.
|
||||
|
||||
## The check before the swap — the map was a subset, not the answer
|
||||
|
||||
Over `main_menu`, `extras`, `press_start` and `title`:
|
||||
|
||||
| | count |
|
||||
|---|---|
|
||||
| map says additive **and** the disc agrees | **15** |
|
||||
| map says additive and the disc does **not** | **0** — no contradictions |
|
||||
| disc says additive and the map did not | **17** |
|
||||
|
||||
Nothing transcribed was wrong. It was **incomplete and was being read as
|
||||
complete**. The 17 include:
|
||||
|
||||
* `pteff03` / `pteff03a` — the sweep **leaves**. `draw_leaf_for` means those are
|
||||
what actually reach the screen while the map listed their parents
|
||||
`ptloop01`/`ptloop02`. (Both parent and leaf carry the bit, so this one turned
|
||||
out to change nothing — established below, not assumed.)
|
||||
* **twelve on `title`**, where the map was deliberately empty. The port has been
|
||||
drawing every title effect alpha-over.
|
||||
|
||||
**And it answers `BLOCKED.md` H6 with no capture at all.** The JP asymmetry — the
|
||||
port drawing `main_menu` additive and `main_menu_jp` alpha-over, asserting by
|
||||
omission that the JP build differs — was an artefact of a name-keyed map. The bit
|
||||
is on the disc for every screen at once.
|
||||
|
||||
## 🔴 The regression, which is one element
|
||||
|
||||
Scored against the oracle captures, on the GPU, before and after:
|
||||
|
||||
| screen | before | after | Δ |
|
||||
|---|---|---|---|
|
||||
| **`main_menu`** | 10.88 | **13.02** | **+2.14** |
|
||||
| **`main_menu_options`** | 11.56 | **13.57** | **+2.01** |
|
||||
| `extras` | 13.10 | 13.10 | — |
|
||||
| `title` | 14.11 | 14.11 | — |
|
||||
| `title_plate`, `title_band`, both splashes | unchanged | unchanged | — |
|
||||
|
||||
**The scores are deterministic** — two further runs gave 13.02 / 13.10 / 13.57
|
||||
to the digit — so this is a real change, not sampling noise.
|
||||
|
||||
### It is `pteff10`, isolated
|
||||
|
||||
* `main_menu`'s only newly-additive **top-level** element is `pteff10`.
|
||||
* `extras` has **no** newly-additive top-level element, and its score did not
|
||||
move. That is the control: the same change applied to a screen with nothing new
|
||||
moves nothing.
|
||||
* The leaf rule was tested separately by disabling it — `main_menu` stayed at
|
||||
13.02, so `pteff03`/`pteff03a` are **not** the cause. That prediction of mine
|
||||
failed and the rule was restored, being provably neutral here.
|
||||
|
||||
`title` did not move despite twelve newly-additive elements, which is consistent:
|
||||
`verify-capture` poses at settle `t=198`, and the title's effect quads —
|
||||
`ptlogo_back2eff1…5`, `ptlogoall_eff`, `pteff01` — are transparent there.
|
||||
|
||||
### 🔴 WHY I THOUGHT THIS WAS A COUNTER-EXAMPLE — and it was not. Kept because the premise-check is the lesson
|
||||
|
||||
**Their own map lists `pteff10` as additive on `extras` and not on `main_menu`,
|
||||
and they logged both screens.** So either their per-draw log shows `main_menu`'s
|
||||
`pteff10` drawn alpha-over — a direct contradiction between a capture and the
|
||||
disc bit, on one element — or it was not drawn during that capture. The oracle
|
||||
comparison independently prefers alpha-over there.
|
||||
|
||||
❌ **Wrong, and the premise was the failure.** The oracle *does* measure it
|
||||
additive on `main_menu` — three sessions, every frame. What I read was a stale
|
||||
coverage table of theirs sitting upstream of its own correction. **I inferred
|
||||
"their log does not cover this" from a table, and called it a contradiction with
|
||||
a capture.** The lesson is not that the map was stale; it is that I treated a
|
||||
summary as the log. See the resolution at the foot of this page.
|
||||
|
||||
## Why the change ships anyway, stated rather than assumed
|
||||
|
||||
1. `main_menu` carries a **±3.78 capture-phase term** in the harness's own note —
|
||||
the capture caught the free-running sweep at an unknown phase. **+2.14 is
|
||||
inside that stated uncertainty** and cannot adjudicate a disc fact.
|
||||
`main_menu_options` is a sub-region of the same screen and inherits the same
|
||||
sweep.
|
||||
2. The decoded source is far better evidenced than the comparison that moved, and
|
||||
it **fixes two known defects** — twelve title effects drawn with the wrong
|
||||
blend, and a JP/EN asymmetry the port was asserting by omission.
|
||||
3. Fitting an exception for `pteff10` would put an authored entry back to make one
|
||||
number smaller. That is the move this project keeps having to undo.
|
||||
|
||||
🔴 **This was a known regression shipped deliberately, not an unnoticed one** —
|
||||
and the decision was right for a *stronger* reason than the one I used. Not only
|
||||
is +2.14 inside the ±3.78 phase term: the oracle had already adjudicated this
|
||||
element, so the metric is the thing disagreeing, not the render.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the bit is wrong. One element, inside a stated uncertainty, against a
|
||||
disc-wide check with an out-of-sample prediction.
|
||||
* That `pteff10` on `main_menu` and on `extras` are the same sprite. Not checked.
|
||||
* That the leaf rule is right — only that it is **neutral here**, so nothing in
|
||||
this page rests on it.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# ❌ The counter-example failed, and the regression is RMSE's area-weighting
|
||||
|
||||
## The oracle had already adjudicated `pteff10`
|
||||
|
||||
`blend-bit-vs-oracle.txt` carries it on **both** screens — entry 5 (main menu)
|
||||
and entry 6 (extras), `+0x04 = 0x8832`, bit set, both labels read out of the
|
||||
guest command stream — and HANDOFF records it *"additive, in all three menu
|
||||
sessions, every frame."*
|
||||
|
||||
**My premise was a stale coverage table**, not a reading of the log. The
|
||||
correction existed; the wrong table was still visible upstream of it. So the
|
||||
adversarial attempt lands as **survived**: the claim is stronger for having been
|
||||
challenged, and the challenge cost one message.
|
||||
|
||||
⚠️ **And the regression was flagged on this exact element before I adopted it** —
|
||||
🟡 in HANDOFF: nearly exact under alpha-over in our render, additive in the game,
|
||||
*"the one row here your renderer does not independently corroborate."*
|
||||
|
||||
## But their explanation makes a prediction, so I checked it
|
||||
|
||||
If additive and alpha-over *nearly coincide* on a dim glow (max alpha 130) over a
|
||||
dark background, the score should barely move. **Mine moved 20 %.** That is a
|
||||
real tension and it was worth one measurement.
|
||||
|
||||
Diffing the two port renders — the only change between them is `pteff10`'s blend:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| pixels identical | 373 588 (**40.5 %**) |
|
||||
| pixels differing by > 3 | 330 794 (**35.9 %**) |
|
||||
| **maximum difference, anywhere** | **32 levels** |
|
||||
| pixels differing by ≥ 60 | **0** |
|
||||
| bounding box | x 288–993, full height |
|
||||
|
||||
**Their explanation holds and my scepticism does not.** No pixel moves by more
|
||||
than 32/255 — 12.5 % — which is "nearly coincide" measured rather than asserted.
|
||||
What moved the RMSE is **area**: a third of the frame shifting slightly.
|
||||
|
||||
## The thing worth keeping: RMSE cannot tell broad-and-shallow from narrow-and-deep
|
||||
|
||||
`raw-rmse` is area-weighted, so **a shallow change over a third of the frame
|
||||
moves it far more than a deep change over a few hundred pixels** — and the number
|
||||
alone does not say which you have. 10.88 → 13.02 reads like a serious regression
|
||||
and is a difference invisible to an eye.
|
||||
|
||||
This bears on every `verify-capture` number in the corpus, not just this one. A
|
||||
row that moves should be asked *"broad or deep?"* before it is called a
|
||||
regression, and that costs one histogram.
|
||||
|
||||
## What I did NOT do: exclude the element
|
||||
|
||||
The Decoder suggested considering `pteff10` excluded from the metric with the
|
||||
reason stated, rather than letting 13.02 sit as debt. **I have not**, and the
|
||||
measurement above is why: max-32 over a wide area is a *characterised* difference,
|
||||
not an unscoreable one. Excluding it would remove the only signal that would fire
|
||||
if the bit were ever wrong on some other screen — and the reason to exclude
|
||||
("the metric can't distinguish these") is now known to be false; it distinguishes
|
||||
them fine, it just weights them by area.
|
||||
|
||||
**The 13.02 stands, with its explanation attached.** That is cheaper than an
|
||||
exclusion nobody will revisit.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 🔴 Provenance: the oracle behind this adoption had a container-only instrument
|
||||
|
||||
Recorded 2026-09-01 at shutdown, from the Decoder's own audit of their `/canary`
|
||||
checkout, not from anything visible on this side.
|
||||
|
||||
This page deleted an authored map on the strength of `ui-blend-mode-decoded.md` —
|
||||
35 elements against `RB_BLENDCONTROL0` read out of the guest command stream, zero
|
||||
errors both ways, plus an out-of-sample prediction. **That evidence was not
|
||||
reproducible by anyone who cloned this repository.** The Canary logger commit
|
||||
that emits `blend=` per draw lived only in their container; without it a draw log
|
||||
records **no blend state at all**, so the oracle could not be re-derived at all —
|
||||
not approximately, not at reduced confidence. Not at all.
|
||||
|
||||
It is reproducible now: four container-only logger commits are exported as
|
||||
patches under `tools/canary-patches/` **on `auto/frame-blend-draw-path`**, with a
|
||||
rebuild recipe. ⚠️ Named without a resolvable path on purpose — that directory is
|
||||
not in this checkout, and citing it as one would be the exact defect
|
||||
`check-citations` exists to catch.
|
||||
|
||||
## What this does and does not change
|
||||
|
||||
* **It does not weaken the adoption.** The measurement was real when made and is
|
||||
now reproducible. Nothing here is retracted.
|
||||
* **It does change what "decoded" was resting on.** For the window between the
|
||||
adoption and the export, this port had deleted an authored entry in favour of a
|
||||
field whose supporting oracle no one else could regenerate. The map was the
|
||||
*worse* of the two — a screen-name table that could not answer for a screen
|
||||
nobody drove to — so the trade was still right. But it was a trade made against
|
||||
an instrument, and the instrument's reach was smaller than the finding's.
|
||||
|
||||
📌 **The generalisation, and it is theirs:** a finding is only as portable as the
|
||||
tool that produced it, and a reproduce recipe that reads as complete is the
|
||||
dangerous kind. Theirs *looked* complete — it named shas — which is why four
|
||||
commits sat unexported while one was noticed.
|
||||
|
||||
⚠️ And the port cannot check this class from here. `check-citations` scans repo
|
||||
paths; an instrument living in another container is not a path at all. **The only
|
||||
defence available on this side is asking what produced a number before adopting
|
||||
it**, which is not a check and does not run.
|
||||
85
docs/port/captures-are-crops-not-resamples.md
Normal file
85
docs/port/captures-are-crops-not-resamples.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# The committed captures are CROPS, not resamples — so pixel comparisons are like-for-like
|
||||
|
||||
**Status:** ✅ **measured, and it refutes a consequence rather than a finding.**
|
||||
Written 2026-09-02 by the Port; HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
## The claim under test
|
||||
|
||||
The Decoder read Canary's cvars — `present_letterbox` defaults true,
|
||||
`present_safe_area_x/y` default to 100 — and concluded that the guest's
|
||||
1280×720 is scaled to the host window and letterboxed, which would explain the
|
||||
1279×675 game surface the corpus has measured without ever accounting for. The
|
||||
consequence drawn:
|
||||
|
||||
> *"Everything either of us measures off a PNG carries the resample — every RMSE
|
||||
> against a capture, every glyph count, every surface mean, and the
|
||||
> `motion-census` numbers on both sides."*
|
||||
|
||||
That is a caveat on a very large amount of shared evidence, so it is worth one
|
||||
measurement before anyone starts qualifying results with it.
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
> If the captures carry a scale, then **scaling** this port's 1280×720 render
|
||||
> down to 1279×675 should match a capture better than **cropping** it. If they
|
||||
> are crops, the reverse.
|
||||
|
||||
## Measured
|
||||
|
||||
`live-splash-publisher.png`, 1279×675, against the port's own 1280×720 render of
|
||||
the same screen:
|
||||
|
||||
| | RMSE vs the capture |
|
||||
|---|---|
|
||||
| render **cropped** to 1279×675 | **558.1** (0.85 %) |
|
||||
| render **scaled** to 1279×675 | **10 118.8** (15.4 %) |
|
||||
|
||||
**Cropping is 18× better.** A 0.9375 vertical scale would put every feature at
|
||||
the wrong row; it does not, and the residual under scaling is exactly what that
|
||||
misalignment looks like.
|
||||
|
||||
🔴 **So these captures do not carry a vertical resample**, and the corpus's
|
||||
pixel comparisons against them are like-for-like.
|
||||
|
||||
## What is refuted and what is not
|
||||
|
||||
**Refuted: the consequence.** *"Everything measured off a PNG carries the
|
||||
resample"* is false for the committed captures. Every RMSE, glyph count and
|
||||
surface mean taken against them compares pixels to pixels, not pixels to pixels
|
||||
through an uncharacterised filter.
|
||||
|
||||
**Not refuted: the cvar reading.** Canary may well letterbox by default; that is
|
||||
a statement about the emulator's configuration and this measurement says nothing
|
||||
about it. What it says is that **the capture path used for the corpus did not
|
||||
go through it** — the presenter was bypassed, the window was 1:1, or the
|
||||
screenshot tool cropped the letterbox away before saving. Which of those, nobody
|
||||
here has established.
|
||||
|
||||
📌 **A second, independent line already agreed and nobody connected it.**
|
||||
`ui-render-tone-curve.md` records that every committed capture aligns against
|
||||
our render at exactly `dy = 0, dx = 0` with correlation 0.9466. A vertical scale
|
||||
of 0.9375 cannot produce a zero-offset alignment. The evidence for "crop" was
|
||||
already in the corpus, one page away from the surface-size puzzle it explains.
|
||||
|
||||
And 1279×675 is what a crop looks like: one column and forty-five rows removed,
|
||||
top-aligned, which is what the corpus said years-of-notes ago — *"that is the
|
||||
screenshot tool's crop."*
|
||||
|
||||
## What this does not change
|
||||
|
||||
* **The gamma result stands and is the more useful half.** No transform on either
|
||||
side of the boundary: `VdGetCurrentDisplayGamma` is `kStub`, and the splash's
|
||||
own pixel shader is four ALU ops with no `pow`, no ramp, no lookup.
|
||||
* **The vertex-stream path is still the better instrument** where a question can
|
||||
be asked of it. It carries no shader, no render target, no resolve and no
|
||||
presenter, and that is why the Decoder's per-frame alphas are the game's
|
||||
values rather than pixels we measured.
|
||||
* **`motion-census` was never at risk.** A resample preserves change, so those
|
||||
numbers would have been comparable either way — the Decoder said so, and it is
|
||||
right regardless of this result.
|
||||
|
||||
## Reach
|
||||
|
||||
One capture, one screen, one comparison. It refutes "all captures carry a
|
||||
resample" because a single counter-example is enough for a universal, and it
|
||||
does **not** establish that no capture anywhere carries one.
|
||||
99
docs/port/held-direction-repeat.md
Normal file
99
docs/port/held-direction-repeat.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# F1 — the menu repeats on a held direction: mechanism shipped, **rate deliberately not**
|
||||
|
||||
**Status:** ✅ mechanism implemented and wired. 🔴 **inert on purpose** — it does
|
||||
nothing until a measured repeat rate exists. Written 2026-09-02 by the Port.
|
||||
|
||||
## What was reported
|
||||
|
||||
Two statements from the human, both about the **real game**, a play-test apart:
|
||||
|
||||
> *"Moving stick up/down and holding only moves one item. In game it actually
|
||||
> continues to move when holding up/down, just at a medium pace so player does
|
||||
> not need to move pad middle↔up/down, but also slow enough to see which item is
|
||||
> selected and move to target."*
|
||||
|
||||
> *"Confirmed D-Pad does repeat when holding too."*
|
||||
|
||||
## The file predicted its own refutation
|
||||
|
||||
`gamepad.gd` carried this, written when the latch was added:
|
||||
|
||||
> *"Whether the real game repeats while a direction is held, and how fast, is
|
||||
> unknown… If the game does repeat, this is a difference a human will notice as
|
||||
> 'I have to flick it again', and the fix is a measured repeat interval — not a
|
||||
> guessed one."*
|
||||
|
||||
That is exactly what happened, in the words it predicted. **So
|
||||
one-step-per-deflection is no longer the conservative reading — it is a known
|
||||
defect**, and keeping it is choosing a wrong behaviour over an approximate one.
|
||||
|
||||
## What was built
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `Gamepad.held_direction()` | −1 / 0 / +1, polled from the **devices** |
|
||||
| `Gamepad.repeat_due(delta)` | one step or 0, per frame |
|
||||
| `Boot._menu_repeat(delta)` | calls it under the same guards a real press gets |
|
||||
|
||||
### Why it polls devices and not `Input.is_action_pressed`
|
||||
|
||||
`ui_up`/`ui_down` are bound to the stick axis at **Godot's 0.50 action
|
||||
deadzone**, while this port steps at the game's measured **0.61** (`ENTER`).
|
||||
Polling the action would repeat throughout the 0.50–0.61 band — the exact band
|
||||
`ENTER` exists to exclude — so the repeat would contradict the threshold on the
|
||||
same stick, on the same frame.
|
||||
|
||||
That is the input-map lesson from 2026-09-01 arriving in a new place: **assert
|
||||
the device, not the layer above it.** The stick reads from the latch
|
||||
`accepts()` already maintains, so the first step and the repeat cannot disagree
|
||||
about hysteresis; the d-pad reads `JOY_BUTTON_DPAD_UP/DOWN` directly, which the
|
||||
human's second report makes load-bearing rather than defensive.
|
||||
|
||||
### Why the guards are duplicated rather than shared
|
||||
|
||||
`_menu_repeat` re-applies the same four conditions `_unhandled_input` applies —
|
||||
no movie playing, a menu exists, its stack is non-empty, no transition pending.
|
||||
A repeat that could fire during a movie or mid-transition would be a **second,
|
||||
subtly different input path**, and the first thing this port learned about input
|
||||
is that a second path is where the defect hides.
|
||||
|
||||
## 🔴 And the rate is not shipped
|
||||
|
||||
An earlier draft of this change had `REPEAT_DELAY = 0.40` and
|
||||
`REPEAT_INTERVAL = 0.20`, with a paragraph explaining that they were authored.
|
||||
**They were removed rather than commented out**, on an explicit instruction:
|
||||
|
||||
> *"Take the RATE from the Decoder — do NOT ship a placeholder interval. An
|
||||
> invented rate here is indistinguishable from a measured one later, and this is
|
||||
> the exact field where that already cost us."*
|
||||
|
||||
The instruction is right and the draft was the named failure mode: the
|
||||
explanation would have merged, the numbers would have felt roughly right, and
|
||||
nothing downstream could have separated them from a measurement. `REPEAT_DELAY`
|
||||
is `-1.0`; `repeat_due()` returns 0 while `repeat_rate_known()` is false.
|
||||
|
||||
**One thing about the rate IS measured, and it narrows the question.** The game
|
||||
digitises the left stick to four direction bits at 61 % deflection, so it cannot
|
||||
see deflection magnitude at all — the repeat it drives *cannot* be
|
||||
faster-the-harder-you-push. That excludes the one competing model, so only two
|
||||
constants are open and a single measurement closes both.
|
||||
|
||||
## ⚠️ Adopting the rate breaks a green check, for the right reason
|
||||
|
||||
`tools/port/verify-input` asserts *"a held stick is ONE step, not six"*. That row
|
||||
passes today **because the feature is inert**, i.e. it asserts the absence of the
|
||||
repeat. When a rate is adopted a held stick should produce further steps and that
|
||||
row will go red.
|
||||
|
||||
It is not wrong and it should not be deleted in a hurry: it was written for the
|
||||
2026-09-01 jitter defect, so it will *look* like that bug returning. It has to be
|
||||
re-stated as "one step per deflection **plus** the measured repeat", with the
|
||||
jitter case still covered inside the delay window.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the repeat feels right. It cannot — it does not run.
|
||||
* Any rate, or any bound on one. "Medium pace" is a direction, not a number, and
|
||||
it is not recorded anywhere as data.
|
||||
* That the d-pad and the stick repeat at the *same* rate. Both repeat; nobody
|
||||
has said they match, and the code currently assumes one rate for both.
|
||||
95
docs/port/p7-gate.md
Normal file
95
docs/port/p7-gate.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# P7 — the new-game intro plays and returns to a defined state ✅
|
||||
|
||||
**Status:** ✅ **gate met, with an artifact.** Run 2026-09-01 by the Port at
|
||||
`4be90c2` + this commit; HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
`PORT-MISSION.md` P7: *"New-game intro video after NEW GAME | Plays, then returns
|
||||
to a defined state."*
|
||||
|
||||
The path had been **wired** for some time — `authored/flow.json` gives `ptbtn01`
|
||||
a `then_video: "S00A"`, a `skipped_chain`, and `after_video: {goto: "title"}`,
|
||||
and `S00A.ogv` is in the export. **Nobody had run it.** A milestone is done when
|
||||
its artifact exists, not when the wiring reads correctly, and this file is the
|
||||
difference.
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
> `--menu --script=accept` with focus on `NEW GAME` announces the skipped
|
||||
> `DIFFICULTY, SELECT DATA` chain, plays `S00A`, and returns to `title`.
|
||||
> Unskipped, `S00A` is 93.78 s of media, so it should end **on its own** at
|
||||
> ≈94 s and hand off.
|
||||
|
||||
## What happened
|
||||
|
||||
```
|
||||
menu on main_menu, focus ptbtn01
|
||||
script[1] accept at 1.00 s
|
||||
(NEW GAME) -> the real chain is DIFFICULTY -> SELECT DATA, then the movie.
|
||||
Neither screen is in this export.
|
||||
-> video S00A at 0.97 s (/work/export/video/S00A.ogv)
|
||||
+ voice S00A
|
||||
video ended at 94.13 s
|
||||
-> title (authored: authored)
|
||||
(after the movie) -> title
|
||||
overlay press_start raised, settles at t=236
|
||||
script complete after 96.83 s on title
|
||||
```
|
||||
|
||||
**94.13 s against 93.78 s of declared media — +0.35 s, 0.4 %.** It ends on its
|
||||
own, at the right time, and hands off. Both predictions held.
|
||||
|
||||
Artifacts: `s_00_start.png` (main menu, `NEW GAME` focused) and `s_01_accept.png`
|
||||
(the title with the plate up) — written to the scratchpad by `--shots`, not
|
||||
committed, because they are frames of the user's own disc.
|
||||
|
||||
## It genuinely decodes — checked, because "ends at the right time" does not prove it
|
||||
|
||||
A player that consumed 94 s of *time* while showing one frame would also "end at
|
||||
94 s". So the frame counts, at two window lengths:
|
||||
|
||||
| window | frames shown | of 2813 |
|
||||
|---|---|---|
|
||||
| 2.03 s | 45 | 2 % |
|
||||
| 18.91 s | **244** | 9 % |
|
||||
|
||||
Frames scale with the window — it is decoding, not stalled. **Sub-linearly
|
||||
though**, 22.2 fps early against 12.9 fps over the longer window, and that is
|
||||
the same software fill ceiling documented in
|
||||
[`port-frame-rate.md`](port-frame-rate.md); these two runs predate the GPU.
|
||||
|
||||
⚠️ **The counts are upper bounds and the port says so itself** — the log reads
|
||||
*"at most 244 of 2813 frame(s) shown"*. It cannot see inside `VideoStreamPlayer`,
|
||||
so it reports what it can bound rather than a number it cannot support.
|
||||
|
||||
## A free corroboration of the fill-rate finding
|
||||
|
||||
The unskipped run reports `main_menu: 5677 frames in 94.13 s — **60.3 fps**`.
|
||||
The screen never changes during a movie, so the rate line attributes the whole
|
||||
playback to it.
|
||||
|
||||
**The same process, in the same container, on the same screen: 60.3 fps while a
|
||||
full-screen video texture is on top, and 9.7 fps while drawing the menu's five
|
||||
additive full-screen quads.** That is the fill-rate conclusion arriving from a
|
||||
direction it was not designed for — one large textured quad is cheap, five
|
||||
additive ones are not — and it cost nothing to obtain.
|
||||
|
||||
## Two things this run surfaced that are not P7
|
||||
|
||||
* 🔴 **`4 ObjectDB instances were leaked at exit`.** Godot's own warning, on
|
||||
every run of this path. Small and at shutdown, so it costs a player nothing,
|
||||
but it is a real signal that something in the video/overlay teardown is not
|
||||
freed. **Not chased, recorded.**
|
||||
* 🟡 **The menu bed keeps playing under the movie.** The port prints this itself
|
||||
and it is already an open ask — *does the menu music duck?* — in
|
||||
`BLOCKED.md`. Left audible on purpose rather than guessed at.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the skipped chain is right. `DIFFICULTY` and `SELECT DATA` live in
|
||||
archives this milestone does not export; the port **announces the skip** rather
|
||||
than pretending the sequence is complete, which is the honest half of a gate it
|
||||
cannot fully reach.
|
||||
* That `after_video: title` is what the game does. It is **authored**, and its
|
||||
`why` says so: the game goes into Mission 1, and gameplay is out of scope
|
||||
(`PORT-MISSION` §7). "Returns to a defined state" is the gate; `title` is the
|
||||
state we defined.
|
||||
458
docs/port/plate-arrival-halves.md
Normal file
458
docs/port/plate-arrival-halves.md
Normal file
@@ -0,0 +1,458 @@
|
||||
# H3 — the `PRESS Ⓐ` plate: which half the lateness is in
|
||||
|
||||
**Status:** ✅ **both halves answered, and TWO of my own conclusions on this page
|
||||
were refuted within the hour** — the `5 units/frame` reading and the claim that
|
||||
`clock: "shared"` collapses. Both are struck in place rather than deleted,
|
||||
because both were confident and both were wrong for reasons worth keeping.
|
||||
The answers are the Decoder's `h3-units-per-frame-measured.md`,
|
||||
`origin/auto/frame-blend-draw-path` at `a482d9a`.
|
||||
Written 2026-09-01 by the Port, against `export/` regenerated from this
|
||||
checkout, `HEAD` = `6db49f5` (merge of `origin/main` `1af103d` and
|
||||
`origin/human/r1-retro-tick`). HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
The [play-test](../agents/PLAYTEST-2026-09-01.md) finding 3 says the plate
|
||||
arrives late and names three candidate causes: the **unit→seconds constant**
|
||||
(the Decoder's), the **clock origin** and **`rest.t`** (both ours). The brief
|
||||
asks which half it is, and how that was established. This page is the answer.
|
||||
|
||||
> **Short version, after the answer landed.** `rest.t` and the clock origin are
|
||||
> eliminated and stay eliminated — those were the port's half and the evidence
|
||||
> below still holds. **Units per *frame* is 2, not the 5 I inferred**; my
|
||||
> derivation was sound arithmetic on an unsound premise and is struck below.
|
||||
> **The anchor is t=160**, as I suspected. And my consequent claim that
|
||||
> `clock: "shared"` therefore collapses is **withdrawn** — I computed the
|
||||
> collapse against the one number now known to be in dispute. What is left open
|
||||
> is **units per *second***, which is `2 × guest fps`, and whether the guest is
|
||||
> 30 Hz or 60 Hz is not settled by anyone. At 60 Hz the plate lands at 1.97 s
|
||||
> instead of 3.93 s, which is the size of what the human reported.
|
||||
|
||||
## What the export actually declares
|
||||
|
||||
`export/screens/title/press_start.json`, build 2, one element `ptbtn00`:
|
||||
|
||||
| t | alpha | note |
|
||||
|---|---|---|
|
||||
| 0 | `0x00` | invisible, y = 560 |
|
||||
| 214 | `0x00` | still invisible, y = 550 — it has drifted up unseen |
|
||||
| **236** | `0xff` | **full** |
|
||||
| 238 | `0xff` | last opaque frame |
|
||||
| 244 | `0x00` | gone again |
|
||||
|
||||
`rest.t = 236`. `settle_window = [214, 236, 225]`.
|
||||
|
||||
🔴 **The corpus says the plate reaches `a=255` at `t=238`. It reaches it at
|
||||
`t=236`.** 238 is the last frame at full alpha, not the arrival. Every
|
||||
derivation quoting `238 − 118 = 120 units = 2.000 s` is really `236 − 118 =
|
||||
118 units = 1.967 s`. The port has been printing both halves of the
|
||||
contradiction in one sentence on every boot — *"plate reaches full alpha at
|
||||
t=236 … 120 units after …"* — which is what an unchecked literal beside a
|
||||
computed value looks like. Corrected in `boot.gd` and in `authored/flow.json`.
|
||||
It moves the reconciliation by 0.033 s and overturns nothing.
|
||||
|
||||
## Candidate 1 — `rest.t`. **Eliminated.**
|
||||
|
||||
`rest.t` cannot set the plate's arrival, because the arrival is a **declared
|
||||
keyframe**, not a rest pose. `ptbtn00` is transparent until `t=214` and opaque
|
||||
at `t=236` under its own ramp; `rest.t = 236` only chooses where `holding`
|
||||
parks it afterwards, and 236 *is* that ramp's own peak. Setting `rest.t` to any
|
||||
other value moves where the plate stops, never when it starts.
|
||||
|
||||
Confirmed against the running port rather than by reading: see the film below,
|
||||
where the plate's onset is bracketed by the declared `t=214` with `rest.t`
|
||||
untouched.
|
||||
|
||||
⚠️ This does **not** rehabilitate `rest.t` generally. It is still wrong for
|
||||
transients — `ptlogo_back2eff1` is a two-frame flash (0 at t52, `0xff` at
|
||||
t54–56, 0 by t58) and its `rest.t = 54` is the flash *peak*, so `rest()` leaves
|
||||
five of these burning at once. It is simply not in this defect's causal path.
|
||||
|
||||
## Candidate 2 — the clock origin. **Eliminated, and measured.**
|
||||
|
||||
Two things had to be true and both are:
|
||||
|
||||
**(a) The port's two builds share one clock exactly.** `_advance` sets
|
||||
`view.time_units = 0.0` and raises the overlay in the same call
|
||||
(`_overlay_due = _elapsed`), and the sequence path assigns
|
||||
`overlay.time_units = view.time_units` outright rather than integrating a second
|
||||
delta. Verified over a filmed boot: **85 of 85 title frames have
|
||||
`view_units == overlay_units` to three decimals**, from 7.812 to 679.182. There
|
||||
is no drift and no offset to find.
|
||||
|
||||
**(b) The title step begins when the previous step ends.** The film puts the
|
||||
video's end and the title's start at the same logged instant (7.86 s / 7.92 s
|
||||
across two runs), with no black hold between them.
|
||||
|
||||
## Candidate 3 — the unit→seconds constant. **NOT eliminated. It is the live one.**
|
||||
|
||||
🔴 **This section said "eliminated by sign" and that was wrong.** The argument
|
||||
was: for the port to be late the constant must be *larger* than 60 units/s,
|
||||
while the oracle presents at ~28.1 fps and the corpus measured the idle title at
|
||||
28.5 fps — both slower than nominal, so the game's units run slower, not faster.
|
||||
|
||||
**That conflates two different quantities.** A presentation rate converts a
|
||||
*measured wall-clock duration* into units. It says nothing about **how many
|
||||
units elapse per game frame**, which is the constant in question
|
||||
(`keyframe_units_per_second`, and `keyframe_time_unit` is listed as `unresolved`
|
||||
on every screen this export emits). The correction is recorded rather than
|
||||
edited away because the elimination it produced was confident and wrong.
|
||||
|
||||
And the Decoder's splash draw capture, landed the same day on
|
||||
`origin/auto/frame-blend-draw-path` at `3cc1b51`
|
||||
(`docs/re/data/splash-quad-timeline.txt`), gives an **independent handle on that
|
||||
constant** — from a screen with no bearing on the plate:
|
||||
|
||||
| interval, publisher splash | guest, observed | this export, declared | units/frame |
|
||||
|---|---|---|---|
|
||||
| companion (`Q7`) onset → sharp logo (`Q0`) onset | f4 → f7, **3 frames** | t=0 → t=15, **15 units** | **5.0** |
|
||||
| companion onset → its own peak | f4 → f7 (a=240), **3 frames** | t=0 → t=15 (a=255), **15 units** | **5.0** |
|
||||
| sharp logo onset → full alpha | f7 → f10, **3 frames** | t=15 → t=30, **15 units** | **5.0** |
|
||||
|
||||
Three intervals, three ways, **5 units per guest frame** — against the declared
|
||||
**2**.
|
||||
|
||||
## 🔴 STRUCK. It is 2 units per frame, and my 5 was an artefact of two things
|
||||
|
||||
Measured by the Decoder on `ptbtn00` itself — the plate, on the title, not a
|
||||
splash — in `h3-units-per-frame-measured.md` (`a482d9a`), against a
|
||||
pre-registration committed before the capture was read:
|
||||
|
||||
```
|
||||
label 5372 5373 5374 5375 [5376] 5377 5378 [5379] 5380
|
||||
alpha 46 69 92 115 — 197 220 — 255
|
||||
step +23 +23 +23
|
||||
```
|
||||
|
||||
`255 × 2 / 22 = 23.18`, and the plate's declared ramp is `t=214 → 236`, i.e.
|
||||
`T = 22`. **Three consecutive gap-free steps of exactly 23 is 2.0 units per
|
||||
frame on the nose.** Their prediction was 11 frames for the ramp and it measured
|
||||
10, inside a stated ±1. My 4.4-frame prediction is excluded by more than 2×.
|
||||
|
||||
**Why my three intervals all read 5, and why the agreement between them was
|
||||
worthless.** Two independent errors, both mine to have made:
|
||||
|
||||
1. **An alpha step is not a clock rate.** For a linear segment,
|
||||
`Δα per frame = 255 × (units per frame) / T`. Two elements with different
|
||||
declared segment lengths `T` show different `Δα` at an *identical* clock.
|
||||
Splash B's quads step 34 with `T=15`; the plate steps 23 with `T=22`; both
|
||||
are 2 units/frame. Reading a step as a rate is what produced the 2.7×.
|
||||
2. **My "onsets" were not onsets.** I took each quad's *first submission* as its
|
||||
`α = 0` time. On splash A, `Q7` and `Q0` are both already at **α = 85** when
|
||||
first submitted. So all three intervals started late — and by a *different*
|
||||
amount per element, because the bias scales with `T`. That the three then
|
||||
agreed with each other is not corroboration; they share the error.
|
||||
|
||||
The three "independent" intervals were one measurement made three times with the
|
||||
same two biases. That is the shape I should have checked for and did not.
|
||||
|
||||
⚠️ The Decoder records the published `splash-quad-timeline.txt` having no `T`
|
||||
column as their own defect, now fixed. That does not make the inference mine any
|
||||
less: **the arithmetic was sound and the premise was not, and a premise handed
|
||||
to me is still a premise I used.**
|
||||
|
||||
⚠️ **Two caveats, and they are why this is asked rather than acted on.** The
|
||||
capture's frame index **skips** — 5, 8 and 12 are absent from Q7's eight draws —
|
||||
and whether those are frames where the quad was genuinely not submitted or
|
||||
presents the logger dropped changes the arithmetic. And the present rate during
|
||||
a splash is not established.
|
||||
|
||||
### 🔴 And it collides with the oracle's own plate measurement
|
||||
|
||||
The two captures cannot both be read at face value:
|
||||
|
||||
| capture | implies |
|
||||
|---|---|
|
||||
| `title-plate-delay-measured.md` — 118 units in 2.135 s | **~55 units/s** |
|
||||
| `splash-quad-timeline.txt` — 15 units in 3 frames | **~150 units/s** at 30 fps |
|
||||
|
||||
A factor of **2.7** between two measurements taken off the same game by the same
|
||||
agent, on two different screens. Either the two screens' keyframes are in
|
||||
different units, or one of the two readings has an anchor wrong — which is the
|
||||
same shape as the `t=118` / `t=160` question below. **Both are oracle
|
||||
measurements and neither is the port's to resolve.** Asked in
|
||||
[`BLOCKED.md`](BLOCKED.md) H3.
|
||||
|
||||
## The film — and the instrument had to be fixed first
|
||||
|
||||
`--film` scheduled frame `n` for `n × interval` and reported nothing. One
|
||||
1280×720 `save_png` under llvmpipe costs ~0.24 s, so **a request for 0.05 s
|
||||
delivered 247 frames in 60 s where 1 200 were asked for** — an achieved 4.1 fps
|
||||
against a requested 20 — and the deficit accumulated silently into the frame
|
||||
index. `f_071.png` still looked exactly like the frame that was meant to be
|
||||
3.55 s in.
|
||||
|
||||
[`TEMPORAL-VERIFICATION.md`](../agents/TEMPORAL-VERIFICATION.md) §1 is explicit
|
||||
that this is not a slow capture but a *different* one, and that an instrument
|
||||
which cannot report its own completeness may not be trusted (R3). So `--film`
|
||||
now writes `<film>_frames.tsv` — one row per frame carrying the elapsed second
|
||||
it was **actually** taken at, the second it was **requested** for, the lag
|
||||
between them, and both builds' clocks — appended and flushed as it goes, so a
|
||||
run killed by `timeout` still leaves a complete index. It prints the achieved
|
||||
rate against the requested one every 40 frames. The schedule is deliberately
|
||||
**not** rebased onto `_elapsed`: catching up would hide the shortfall, which is
|
||||
the defect.
|
||||
|
||||
The run this page rests on: requested 20 fps, **achieved 15.9 fps for the first
|
||||
120 frames and 11.3 fps by frame 200**, lag reaching 8.7 s. Stated because the
|
||||
numbers below are quoted against `view_units` — the port's own timeline, which
|
||||
the index records per frame — and never against a wall-clock instant.
|
||||
|
||||
### Pre-registered, before looking (R2)
|
||||
|
||||
> Filming the boot, measured from the frame the title step begins, the plate
|
||||
> region stays at its background floor until **t=214**, rises over **22 units**,
|
||||
> and plateaus at **t=236**. Accept ±2 frames at each end.
|
||||
|
||||
### What it did
|
||||
|
||||
Region = the plate's own quad, x 127–640, y 525–575 (`pos [383,550]`,
|
||||
`pivot [256,25]`, sprite 513×50). Mean of the crop, one row per filmed frame:
|
||||
|
||||
| `view_units` | region mean | |
|
||||
|---|---|---|
|
||||
| 7.8 → 143.8 | 0.13765, **flat to 5 decimals** | nothing there |
|
||||
| 151.9 → 207.8 | 0.13773 → 0.14341 | a slow 4 % drift, build 4's own |
|
||||
| **207.8 → 215.3** | 0.14341 → **0.15348** | ⬅ the step; brackets the declared **214** |
|
||||
| 215.3 → 239.8 | → 0.21254 | the ramp |
|
||||
| 239.8 → 279.3 | → 0.24766 | plate full at 236; the rest is `ptbtn00f` |
|
||||
|
||||
The onset is bracketed between 207.8 and 215.3 at a frame spacing of ~7.5 units,
|
||||
i.e. **within one frame of the declared 214**, and `rest.t` was never touched.
|
||||
Prediction held.
|
||||
|
||||
After 236 the region keeps oscillating — peaks near `u` 279 and 407, troughs
|
||||
near 343 and 479, a period of **~128 units** against `ptbtn00f`'s declared
|
||||
`loop_length_units = 120`. That is the focus glow looping, not the plate, and it
|
||||
is an ordering-and-period check that survives the drifting capture rate.
|
||||
|
||||
## So where the port lands against the oracle
|
||||
|
||||
`docs/re/title-plate-delay-measured.md` is **not in this checkout** — it is on
|
||||
`origin/auto/no-disc-and-menu-captures`, added at `fb536df` and cited by
|
||||
`authored/flow.json` at `5b0a6e6`. Read there:
|
||||
|
||||
| | run 1 | run 2 | **the port** |
|
||||
|---|---|---|---|
|
||||
| settled → plate | 2.138 s | 2.132 s | **1.967 s** (118 units at 60/s) |
|
||||
| first drawn → plate | 3.781 s | 4.263 s | **3.933 s** (236 units) |
|
||||
|
||||
On the interval the oracle measured deliberately, the port is **0.17 s early**.
|
||||
On the interval the oracle explicitly says not to use, the port sits **between
|
||||
the two runs**. No number anyone has taken makes this port's plate late.
|
||||
|
||||
**The human watched both and says late, and that observation stands.** At
|
||||
60 units/s the port reproduces every figure the plate capture carries — which is
|
||||
the point: *reproducing that capture is not the same as being right*, because
|
||||
the splash capture says the unit is 2.5× off and would put the port 2.4 s late.
|
||||
The two cannot both hold. The human's eye agrees with the splash capture.
|
||||
|
||||
## 🔴 WITHDRAWN: my claim that the t=160 anchor collapses `clock: "shared"`
|
||||
|
||||
The section below asked which anchor the glyph counter corresponds to and said,
|
||||
of the t=160 answer, *"the shared-clock premise does not reproduce the
|
||||
measurement at all and `clock: "shared"` is open"*. **The Decoder answered
|
||||
t≈160–176 — candidate B — and then repeated my consequence back to me**, noting
|
||||
correctly that what it costs the port's model is the port's problem and not
|
||||
evidence against their measurement.
|
||||
|
||||
**They were right to report it and I was wrong to have claimed it.** I computed
|
||||
the collapse by comparing the declared 76 units against **2.135 s**, and 2.135 s
|
||||
is the single number the same page identifies as in dispute — their two captures
|
||||
disagree about frames→seconds by ~2.9×. *Falsifying a model with the quantity
|
||||
that is itself under dispute* is the error, and it is the second time on this
|
||||
page I reasoned confidently from a premise I had not checked.
|
||||
|
||||
Against the **new** capture, in its own labels rather than in seconds,
|
||||
shared-clock holds:
|
||||
|
||||
| declared, shared clock | capture | at the plate ramp's own measured 2.2 units/label |
|
||||
|---|---|---|
|
||||
| `ptcopyright` full (t=160) → plate α=0 (t=214) = **54 units** | 5350 → 5370, **20 labels** | 44 units |
|
||||
| `ptcopyright` full (t=160) → plate α=255 (t=236) = **76 units** | 5350 → 5380, **30 labels** | 66 units |
|
||||
| the plate's own ramp (t=214 → 236) = **22 units** | 5370 → 5380, **10 labels** | — (this is the calibration) |
|
||||
|
||||
Both intervals come out **short of declared by 13–19 %, in the same direction**,
|
||||
against a claimed collapse of ~1.7×. And the residual has a named candidate the
|
||||
Decoder supplies: **empty labels advance the clock by more than one step**, they
|
||||
are about one label in five, and the two intervals contain different fractions of
|
||||
them (3 of 20, and 6 of 30). Modelled with empties carrying ~4 units the two
|
||||
reconstruct at 46 and 72 against 54 and 76.
|
||||
|
||||
**So `clock: "shared"` is not falsified and the port keeps it.** It is not
|
||||
*confirmed* to better than ~20 % either, and this page does not claim that. What
|
||||
it is no longer is "collapsed on the port's own account".
|
||||
|
||||
## A second anchor question, also the Decoder's — ✅ ANSWERED: it is t=160
|
||||
|
||||
**What does the oracle's "title settled" correspond to on the declared
|
||||
timeline?** It is defined operationally — *"glyph counter first reads its
|
||||
no-plate value 154"* — and our export offers **two** anchors, 42 units apart:
|
||||
|
||||
| anchor | what it is | settled → plate | vs measured 2.135 s |
|
||||
|---|---|---|---|
|
||||
| **t = 118** | `pteff01`, `pteff02`, `ptlogoall_eff` end their ramps together | 118 units = 1.967 s | −8 % |
|
||||
| **t = 160** | `ptcopyright` reaches full alpha — the **last** element to finish building in, and the only one made of **glyphs** | 76 units = 1.267 s | −41 % |
|
||||
|
||||
The port's reconciliation picked 118. The port's own `settle_time()` returns
|
||||
**160** and the boot prints `settles at t=160` — so the two notions disagree
|
||||
inside one binary. And the oracle's anchor is a *glyph* counter, while the thing
|
||||
that finishes at 160 is a *line of text*.
|
||||
|
||||
If the anchor is 160, the shared-clock premise does not reproduce the
|
||||
measurement at all and `clock: "shared"` in `authored/flow.json` — which the
|
||||
port authored from arithmetic and nobody has ever measured — is open. If it is
|
||||
118, the reconciliation stands and H3 is not a timing defect.
|
||||
|
||||
**Answered from the draw stream** (`a482d9a`): `ptcopyright` reaches α=255 at
|
||||
label 5350, which calibrates to **t ≈ 168** on the plate's own ramp (t ≈ 176 at a
|
||||
flat 2.0/label). Candidate B is 8–16 units away; candidate A is 50–58. It is B.
|
||||
|
||||
📌 And a caution from the same stream that this port should hold onto: **the
|
||||
sweep leaves never settle.** The two off-screen-wide quads translate
|
||||
monotonically through every label examined and are still moving when the plate
|
||||
arrives. *"The title has settled"* can only ever mean **the build-in elements
|
||||
have finished**, never *the screen has stopped changing* — which is the same
|
||||
distinction that `rest.t` keeps getting wrong.
|
||||
|
||||
### 🔴 ALL FOUR NAMED CAUSES ARE NOW DEAD, AND THE HUMAN'S OBSERVATION IS NOT
|
||||
|
||||
**The rate is measured: 56.8 units per guest second** (`units-per-second-measured.md`,
|
||||
control passing at 1.15 %, two elements agreeing at one clock — `ptbtn00` at
|
||||
657.9 α/s and `ptcopyright` at 650.4 α/s, which puts `ptcopyright`'s segment at
|
||||
`T = 22.25`, a round declared length nobody fitted). **30 and 120 are both
|
||||
excluded.**
|
||||
|
||||
At 56.8 units/s the plate's `t = 236` lands at **4.15 s** after clock zero,
|
||||
against the port's **3.93 s**. The port is fractionally **early**. So:
|
||||
|
||||
| candidate | verdict |
|
||||
|---|---|
|
||||
| `rest.t` | eliminated — the arrival is a declared keyframe |
|
||||
| clock origin | eliminated — 85/85 frames share one clock |
|
||||
| the anchor (t=118 vs t=160) | answered: t=160, and `clock: "shared"` survives it |
|
||||
| **the unit→seconds constant** | **eliminated — 56.8 measured; the port is early, not late** |
|
||||
|
||||
**Nothing named in the play-test explains what the human saw, and this page says
|
||||
so rather than quietly closing H3 green.** The observation stands and is now
|
||||
unattributed. Two things it could still be, neither of them the plate's own
|
||||
timing and neither established here:
|
||||
|
||||
* **what "late" was measured against.** Every number above is relative to the
|
||||
title's clock zero. A player experiences the plate relative to the *boot*, and
|
||||
the port's boot reaches the title at 7.86 s only because `--skip-at` presses Ⓐ.
|
||||
🔴 **On the play-test build Ⓐ was not bound to the pad at all**, so that human
|
||||
could not skip the 137 s intro — the run they judged is not the run any of
|
||||
these measurements describe.
|
||||
* **the splash dwells.** The rate's reach is the **title**. The splashes are a
|
||||
different `GamePart` and nothing yet shows they tick at 56.8; the Decoder has
|
||||
reading their `T` off the disc as a next item.
|
||||
|
||||
### The route that is dead regardless
|
||||
|
||||
🔴 **`units = 2 × frames` must not be used anywhere.** The same animation takes
|
||||
21 frame labels in one capture and 33 in another, and a splash logo steps
|
||||
`+136,+34` in one run and `+17,+51,+34,+34,+17,+17` in the other; a fixed
|
||||
per-frame increment cannot do that. The 2 was one run's frame pacing.
|
||||
|
||||
✅ **Audited, and the port never did this.** `boot.gd` advances
|
||||
`time_units += delta * units_per_second` off delta time. The retirement cost this
|
||||
port a *justification* in `authored/timing.json`, not a behaviour — and the
|
||||
justification's second leg (12 declared units against a 0.14–0.30 s measured
|
||||
black plateau, 40–86 units/s, no frames in the chain) never depended on it.
|
||||
|
||||
### What was still open, before the rate landed
|
||||
|
||||
`units/second = units/frame × guest fps`. The first factor is now **2**. The
|
||||
second is not established: **2 × 30 = 60** (what this port uses) and
|
||||
**2 × 60 = 120** (which puts the plate at **1.97 s** instead of 3.93 s — the size
|
||||
of what the human reported). The Decoder's capture ran at 27.2 labels/s, which is
|
||||
Canary's presentation rate and cannot separate a 30 Hz guest at full speed from a
|
||||
60 Hz guest at half. **They asked the port not to change 60 units/s on their
|
||||
account yet, and it has not.** Asked in [`BLOCKED.md`](BLOCKED.md) H3.
|
||||
|
||||
## Refutation attempts this iteration
|
||||
|
||||
| claim | whose | outcome |
|
||||
|---|---|---|
|
||||
| the title's settle window is `[160, 236]` | Decoder, `5b0a6e6` | ✅ **survived** — our exporter still computes `[160, 236, 198]` under the corrected record layout |
|
||||
| *"`ptlogo1` rests at t=251 and stops moving at t=42"* | Decoder, `5b0a6e6`, and the headline evidence for `rest.t ≠ settle` | ❌ **refuted on its evidence** — in the current export `ptlogo1.rest.t` is **42**, equal to when it stops moving. The record-layout fix repaired exactly this element. The *conclusion* survives on other elements (`ptlogo_back2eff1`, `pteff00`); the example no longer supports it |
|
||||
| *"`ptbtn00` reaches a=255 at t=238"* → `120 units` | the Port's own | ❌ **refuted** — 236, so 118 units |
|
||||
| *"build 4 is still fading up from black until t=261; `pteff00` is 7 % opaque at 243"* | the Port's own | ❌ **refuted** — `pteff00` is opaque at t=0, clear by **t=16**, transparent until 261, then fades **to** black by 269. At t=243 it is 0 % opaque. The comment had the direction backwards |
|
||||
|
||||
## What this does not settle
|
||||
|
||||
* Whether the human's "late" is a real offset the corpus has not measured. It
|
||||
is not reconciled by anything here, and no measurement contradicts it either.
|
||||
* The `t=118` / `t=160` anchor. Asked, not guessed.
|
||||
* Whether the plate **pulses** after arrival. The port holds it; the corpus
|
||||
measures a ~2.24 s pulse. Filed already in `flow.json`'s `no_pulse_why`.
|
||||
* Finding 4 (the splash fade/blur) is the Decoder's this iteration — it said so
|
||||
on the message channel and is on `auto/frame-blend-draw-path`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# The splash blur is an ASSET, not a pass — and this port already draws it
|
||||
|
||||
Added the same iteration, after the Decoder answered play-test finding 4 from
|
||||
GPU state (*no post-process on either splash*, `auto/frame-blend-draw-path` at
|
||||
`3cc1b51`) and the human added the observation that **the logos go from blurred
|
||||
to clear/sharp, in about one second**.
|
||||
|
||||
Those two are not in tension. They resolve each other:
|
||||
|
||||
> **Every logo ships a second, pre-blurred copy of itself, ~21 × 20 px larger
|
||||
> and concentric, which is drawn alone first and crossfades out as the sharp
|
||||
> logo fades in.** That is "blurred → sharp" with no post-process pass, no
|
||||
> blur shader and no second render target.
|
||||
|
||||
Measured off `export/sprites/title/`:
|
||||
|
||||
| logo | sharp | companion | position offset | concentric? |
|
||||
|---|---|---|---|---|
|
||||
| `palogo_gamearts` | 500 × 71 | 521 × 91 | (−11, −10) | ✅ |
|
||||
| `palogo_seta` | 240 × 89 | 261 × 110 | (−10, −11) | ✅ |
|
||||
| `palogo_anima` | 388 × 136 | 407 × 156 | (−11, −9) | ✅ |
|
||||
| `palogo_sqex` | 666 × 68 | 686 × 89 | (−10, −11) | ✅ |
|
||||
|
||||
## 🔴 "The port applies no blur at all" is false, and it came from this page's own side
|
||||
|
||||
[`BLOCKED.md`](BLOCKED.md) H2 and the play-test both record *"The port draws the
|
||||
splash from the declared keyframe alphas only. It applies **no blur at all**."*
|
||||
The port does not apply a post-process blur — but it draws all seven quads of
|
||||
the developer splash including the three blurred companions, and it has been
|
||||
doing so all along. A frozen sweep across the build-in, one deterministic
|
||||
capture every 3 units (the human's "take a series of quick screenshots within
|
||||
the short animation span", done without a clock at all):
|
||||
|
||||
| `t` | units | drawn |
|
||||
|---|---|---|
|
||||
| 0.00 s | 0 | background only |
|
||||
| 0.05 – 0.25 s | 3 – 15 | background + **the three blurred companions alone** |
|
||||
| 0.30 – 0.70 s | 18 – 42 | **all seven** — the crossfade |
|
||||
| 0.75 – 0.80 s | 45 – 48 | background + the three sharp logos |
|
||||
|
||||
Blurred first, then both, then sharp. The mechanism is reproduced. The claim
|
||||
that it was not came from describing the renderer instead of running it.
|
||||
|
||||
## Cross-check of the Decoder's quad mapping — ✅ survived, independently
|
||||
|
||||
`splash-quad-timeline.txt` names its quads by NDC rectangle off the guest's
|
||||
vertex stream. This export's declared rectangles, converted independently:
|
||||
|
||||
| | this export | the guest | agreement |
|
||||
|---|---|---|---|
|
||||
| `palogo_sqex` | x[−0.517, +0.523] y[−0.106, +0.083] | `Q0` x[−0.520, +0.520] y[−0.100, +0.080] | **~2 px** |
|
||||
| `palogo_sqex_eff` | x[−0.533, +0.539] y[−0.133, +0.114] | `Q7` x[−0.530, +0.540] y[−0.130, +0.120] | **~4 px** |
|
||||
|
||||
Two decoders, two paths — a `.pak` read statically and a vertex stream logged
|
||||
live — agreeing to a few pixels on both the sharp quad and the blurred one.
|
||||
That is corroboration of the mapping and of `Q7` being the companion.
|
||||
|
||||
## What is still open on the splash
|
||||
|
||||
* **The extent.** The rectangles agree; the *alphas* are where the 5-units-per-
|
||||
frame discrepancy above came from, and until that is settled it is not
|
||||
possible to say whether the game holds the blurred copy longer than we do.
|
||||
That is the same question as H3 and is asked once, there.
|
||||
* Whether `palogo_eff0` (kind `0x10`, a full-screen quad, like the title's
|
||||
`pteff00`) is the splash's fade veil. Not examined this iteration.
|
||||
120
docs/port/plate-arrives-on-time-but-never-blinks.md
Normal file
120
docs/port/plate-arrives-on-time-but-never-blinks.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# H3 re-asked after the animation fix: the plate is **on time**, and it **never blinks**
|
||||
|
||||
**Status:** ✅ **the "arrives late" observation does not reproduce as a timing
|
||||
error** — measured on the current build, and the plate's onset sits on its
|
||||
declared keyframe. 🔴 **A different divergence in the same element is real and
|
||||
was not being looked for: the port holds `PRESS Ⓐ` lit permanently, where the
|
||||
disc declares a 30-unit pulse.** Written 2026-09-02 by the Port at `6263686`.
|
||||
|
||||
`PLAYTEST-2026-09-02.md` asks for exactly this, in its own words: *"Worth
|
||||
re-asking now: the animation fix changed what the whole boot looks like, so the
|
||||
original observation may simply no longer reproduce."* And there is a specific
|
||||
reason to re-ask rather than assume — **the previous plate numbers were taken
|
||||
through the `pose_at` bug**, the same way the 0.01 % that manufactured H2's false
|
||||
green was.
|
||||
|
||||
## What the disc declares
|
||||
|
||||
`export/screens/title/press_start.json`, one element `ptbtn00`:
|
||||
|
||||
```
|
||||
0:0 214:0 236:255 238:255 244:0 rest [383,550], sprite 513x50
|
||||
```
|
||||
|
||||
Invisible until **214**, full at **236–238**, **gone by 244**. A 30-unit pulse.
|
||||
|
||||
## The instrument, and the two controls it went through first
|
||||
|
||||
Filmed a real boot (`--skip-at=1 --linger=8 --film-interval=0.05`), no `--time`,
|
||||
no pinning. Then **two attempts at isolating the plate failed before one worked**,
|
||||
which is the part worth keeping:
|
||||
|
||||
1. 🔴 **A rect around the plate.** Contaminated — the rect overlaps the developer
|
||||
splash earlier in the boot, so the onset detector fired on the wrong screen.
|
||||
2. 🔴 **A control band 55 px above the plate.** Still wrong. The title's
|
||||
background sweep **moves**, so a spatially displaced control samples it at a
|
||||
different phase and cannot cancel it. It showed a "rise" that was the sweep.
|
||||
3. ✅ **The plate sprite's own transparent holes.** Same rectangle, same rows,
|
||||
interleaved with the glyphs at pixel scale — 7 672 glyph pixels against 10 412
|
||||
hole pixels, both built from the sprite's alpha channel. A co-located
|
||||
background sample, so `glyph − hole` is the plate and nothing else.
|
||||
|
||||
⚠️ Control 2 is the same defect as everything else on this project: **an
|
||||
instrument that cannot see the thing it is measuring separately from the thing
|
||||
it is measuring against.** It produced a plausible curve. The tell was that the
|
||||
"plate" and the background rose together in phase.
|
||||
|
||||
## Result 1 — the arrival is correct
|
||||
|
||||
`glyph − hole`, dark baseline **0.003**:
|
||||
|
||||
| overlay units | 205 | **214** | **222** | 230 | **238** | 246 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| plate | 0.005 | **0.005** | **0.222** | 0.434 | **0.586** | 0.601 |
|
||||
|
||||
**Flat through 213.9 and risen by 222.1.** The declared onset `t=214` sits inside
|
||||
that bracket, and the bracket is 8 units wide because that is the film's sampling
|
||||
interval, not a measurement of anything. By `t=238` the plate is at 88 % of its
|
||||
lit level, against a declared full alpha at 236.
|
||||
|
||||
**The plate is not late.** Whatever the human saw in the 2026-09-01 play-test,
|
||||
the port's own clock puts `ptbtn00` on its declared keyframe.
|
||||
|
||||
## Result 2 — 🔴 and it then stays lit forever
|
||||
|
||||
The declared ramp returns to **0 at t=244**. It does not:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| dark baseline, `t < 214` | **0.003** |
|
||||
| minimum at any point after `t = 250` | **0.587** (at `ou` 342) |
|
||||
| maximum after `t = 250` | **0.664** |
|
||||
| span measured | `ou` 6 → **1974** — **8.1 declared cycles** |
|
||||
|
||||
Over eight cycles of its own declared timeline the plate **never returns within
|
||||
88 % of dark**. The 11.6 % ripple that is there is *not* the plate: it is in
|
||||
phase with the hole channel, i.e. it is the title's background sweep leaking
|
||||
through the anti-aliased glyph edges.
|
||||
|
||||
### Why, and it is a failure mode this port already wrote down
|
||||
|
||||
`ptbtn00`'s `rest.t = 236` and its settle instant is `t=236` — **the peak of the
|
||||
pulse**. `holding` parks the element there, so the port shows a `PRESS Ⓐ` plate
|
||||
that lights once and stays on.
|
||||
|
||||
`plate-arrival-halves.md` names this exact class already, about a different
|
||||
element:
|
||||
|
||||
> *"It is still wrong for transients — `ptlogo_back2eff1` is a two-frame flash
|
||||
> (0 at t52, `0xff` at t54–56, 0 by t58) and its `rest.t = 54` is the flash
|
||||
> **peak**, so `rest()` leaves five of these burning at once."*
|
||||
|
||||
**The plate is an instance of the family that page predicted, and nobody had
|
||||
checked the plate itself** — because that page was written to *eliminate* `rest.t`
|
||||
as a cause of lateness, which it correctly did, and having eliminated it as the
|
||||
cause of *one* defect nobody asked what else it was doing.
|
||||
|
||||
## What I have NOT changed, and why
|
||||
|
||||
Nothing. The port still holds the plate.
|
||||
|
||||
Reading the disc's keyframes is mine; **whether the running game pulses its
|
||||
`PRESS Ⓐ` plate is not.** The keyframes describe a blink and blinking is what
|
||||
`PRESS START` prompts conventionally do, but "conventionally" is not evidence and
|
||||
this project has been burned by exactly that kind of inference. The change is
|
||||
**proposed, not made**, and the Decoder has been asked for the one fact that
|
||||
settles it: does the plate pulse in the guest, and with what period?
|
||||
|
||||
⚠️ **This is not a regression from the animation fix.** Before the fix `pose_at`
|
||||
*assigned* the settle instant, which parks the plate at `t=236` too. The plate
|
||||
has been held for as long as the port has drawn it; the fix neither caused this
|
||||
nor was supposed to.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That finding 3 was wrong when it was made. It says the port measures on time
|
||||
**now**, on a build whose boot the fix visibly changed.
|
||||
* That holding is wrong. It says the port's picture and the disc's keyframes
|
||||
disagree, and names who can adjudicate.
|
||||
* Anything about the plate's **absolute** alpha or position. Onset timing and
|
||||
whether it extinguishes, only.
|
||||
290
docs/port/port-frame-rate.md
Normal file
290
docs/port/port-frame-rate.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# The port never reported its own frame rate — it does now, and it is 13–25 fps here
|
||||
|
||||
**Status:** ✅ **instrument added and measured.** ❌ **The candidate it raised for
|
||||
play-test finding 4 is DEAD — tested on real hardware, not argued away. See the
|
||||
final section.** The port's draw path never had a case to answer. Written 2026-09-01 by the Port at
|
||||
`977965e`; HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
## The gap this closes
|
||||
|
||||
[`TEMPORAL-VERIFICATION.md`](../agents/TEMPORAL-VERIFICATION.md) §1 is
|
||||
unambiguous: a capture that asked for one rate and delivered another *"is not a
|
||||
slow capture, it is a **different** capture"*, and an instrument that cannot
|
||||
report its own completeness may not be trusted.
|
||||
|
||||
That rule has been applied to `--film` (which I fixed for exactly this), to the
|
||||
Decoder's harnesses, and to the oracle. **It had never once been applied to the
|
||||
thing being shipped.** The port had no idea what rate it drew at and no way to
|
||||
say.
|
||||
|
||||
It matters here specifically, because the splashes are the current focus and the
|
||||
open complaint about them is that ours is *less pronounced* than the game's.
|
||||
**A fade drawn in 45 frames and the same fade drawn in 12 are different
|
||||
animations**, and nothing in this port could have told them apart.
|
||||
|
||||
## The instrument
|
||||
|
||||
`boot.gd` now counts frames per screen and prints at every boot transition, at
|
||||
the end of the boot, and at every menu arrival:
|
||||
|
||||
```
|
||||
publisher_logo: 107 frames in 4.29 s -- 25.0 fps achieved, uncapped, worst gap 108 ms
|
||||
```
|
||||
|
||||
`worst gap` sits beside the mean deliberately: a hitch is what reads as wrong. A
|
||||
screen averaging 55 fps with one 400 ms stall looks broken, and a mean hides that
|
||||
by construction.
|
||||
|
||||
🔴 **Its first version printed `-9223372036854775808 requested`.**
|
||||
`DisplayServer.screen_get_refresh_rate()` returns a *float* and is `-1.0` when
|
||||
the display cannot say — which Xvfb cannot — and `%d` on that underflows to
|
||||
`INT64_MIN`. A rate line whose own denominator is nonsense is worse than no rate
|
||||
line. It now names the cap or says `uncapped`.
|
||||
|
||||
## What it measures, in this container
|
||||
|
||||
Three boots, same command, nothing else running:
|
||||
|
||||
| screen | run 1 | run 2 | run 3 | worst gap |
|
||||
|---|---|---|---|---|
|
||||
| `publisher_logo` | 17.3 fps | 19.6 | 25.0 | 100–115 ms |
|
||||
| `developer_logos` | 16.7 | 21.9 | 22.8 | 103–138 ms |
|
||||
| `title` | 17.2 | 14.2 | 12.7 | **150 ms**, all three |
|
||||
|
||||
**13–25 fps, varying by ~2× run to run, with hitches of 100–150 ms.** The
|
||||
title's 150 ms is identical across all three runs, which looks like a one-off
|
||||
cost rather than load — the video player is torn down immediately before it.
|
||||
|
||||
### 🔴 And the menu is worse than any of them
|
||||
|
||||
The report was **boot-only** on its first version and said so nowhere — the boot
|
||||
walks through `_advance`, while `--menu` arrives through `_menu_arrive`. So the
|
||||
mode a human actually spends time in, and the one where a slow frame is *felt as
|
||||
input lag* rather than seen as a coarse fade, reported nothing. An instrument
|
||||
covering half the application while its own page claims "every boot" is exactly
|
||||
the shape this port keeps finding in other people's work. Fixed in the same
|
||||
commit:
|
||||
|
||||
```
|
||||
main_menu: 20 frames in 2.05 s -- 9.7 fps achieved, uncapped, worst gap 150 ms
|
||||
```
|
||||
|
||||
**9.7 fps.** The main menu is the heaviest screen in the port — five additive
|
||||
elements, five spinning focus rings, a full-screen background — and it is the one
|
||||
the play-test spent its time on. At 9.7 fps a press takes up to 103 ms to appear
|
||||
and a spinning ring advances in ~10 visible steps per revolution.
|
||||
|
||||
⚠️ **This is `llvmpipe` software rasterisation under Xvfb in a loaded container.
|
||||
It is not a measurement of the human's hardware and must not be quoted as one.**
|
||||
What it establishes is that the port *can* run this slowly and never said so.
|
||||
|
||||
## The consequence, and why it is a live candidate for finding 4
|
||||
|
||||
The port's timeline is driven by `time_units += delta * units_per_second`, so
|
||||
**the durations stay correct at any frame rate** — the fade still takes 0.75 s.
|
||||
What changes is how many distinct alphas that fade is *drawn* at:
|
||||
|
||||
| rendered at | steps in the 45-unit build-in | steps in the glow's 15-unit rise |
|
||||
|---|---|---|
|
||||
| **16.7 fps (measured)** | **12.5** | **4.2** |
|
||||
| **22.8 fps (measured)** | **17.1** | **5.7** |
|
||||
| 30 Hz | 22.5 | 7.5 |
|
||||
| 60 Hz | 45.0 | 15.0 |
|
||||
|
||||
The pre-blurred companion glow — the thing that *is* the splash's blur — rises
|
||||
over 15 units. **In this container it is drawn at four to six distinct alphas.**
|
||||
At 60 Hz it would be fifteen.
|
||||
|
||||
A soft crossfade rendered in four steps, inside a 750 ms animation carrying a
|
||||
100–150 ms hitch, is a plausible mechanism for *"close, but not quite right"* and
|
||||
for *"the game's is more pronounced"* — and it is the **first candidate for
|
||||
finding 4 that is not already dead.** Every other one is: the keyframes are
|
||||
vindicated against the vertex stream, the companion quads are drawn, the blend
|
||||
space matches, the settled pose scores 0.01 % against the capture, and there is
|
||||
no post-process pass to add.
|
||||
|
||||
🔴 **It is a candidate, not a cause.** It depends entirely on what the machine
|
||||
running the port manages, and I cannot measure the human's. **The line now
|
||||
prints on every boot**, so the next play-test answers it for free: if it says 60
|
||||
fps and the splash still looks wrong, this is dead too.
|
||||
|
||||
## What is NOT affected, and it was worth checking
|
||||
|
||||
**Every timing result this port has published stands.** They are all derived from
|
||||
`_elapsed`, which is `+= delta` — a sum of frame times, correct at any rate — and
|
||||
from `time_units`, which is the same sum scaled. So:
|
||||
|
||||
* the splash dwells (4.270 s and 3.527 s) are unaffected — measured across runs
|
||||
whose frame rates differed by 2×, and they agreed to ±0.03 s;
|
||||
* the plate's arrival, the shared-clock check and the film's own index are all
|
||||
`_elapsed`-based.
|
||||
|
||||
That is the reassuring half of the same design: the port is *correct* at 13 fps
|
||||
and merely *coarse*. Had the timeline been frame-counted, every number in this
|
||||
corpus would have been wrong by a factor that changed between runs — which is
|
||||
precisely the failure the Decoder found in the emulator's own rate and withdrew a
|
||||
finding over.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That 13–25 fps is what a player sees. It is what this container manages.
|
||||
* That the port has a performance defect. Textures are cached at `load_screen`
|
||||
and not decoded per frame — that was checked and is not the cause.
|
||||
* That capping or vsyncing would help. It would not raise the rate here, and
|
||||
changing presentation behaviour on my own authority is not mine to do.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# The control: it is the software rasteriser, and the port has no case to answer
|
||||
|
||||
Written the iteration after the section above, because *"is 9.7 fps llvmpipe or
|
||||
something in our draw path"* was left open and it is not a question to leave open
|
||||
after publishing a candidate cause.
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
> If the rasteriser is the limit, a near-empty Godot scene in this same container
|
||||
> will also run at roughly 10–25 fps. If my draw path is the limit, it will run
|
||||
> far faster — take **>100 fps** as the discriminator.
|
||||
|
||||
Same container, same Xvfb, same 1280×720 viewport, same `[rendering]` settings,
|
||||
almost nothing drawn:
|
||||
|
||||
```
|
||||
FPSPROBE mode=empty: 651 frames in 4.03 s -- 161.6 fps, worst gap 54 ms
|
||||
```
|
||||
|
||||
**161.6 fps.** The engine loop, the viewport and the present path are not the
|
||||
limit, by a factor of sixteen over the menu.
|
||||
|
||||
## 🔴 And the first control was not a control
|
||||
|
||||
Its `fill` modes drew **untextured** `draw_rect`s while every element the port
|
||||
draws is a **texture**. A control that does not do what its subject does bounds
|
||||
nothing — and it showed: the port's splashes were achieving ~21 Mpx/s against
|
||||
that control's ~50, which read as the port being mysteriously slow and was really
|
||||
the control being mysteriously fast. Adding a matched textured mode:
|
||||
|
||||
| mode | full-screen quads | achieved |
|
||||
|---|---|---|
|
||||
| `empty` | 0 | **161.6 fps** |
|
||||
| `fill3` | 3, untextured | 31.5 |
|
||||
| **`tex3`** | **3, textured** | **23.9** |
|
||||
| `fill7` | 7, untextured | 12.7 |
|
||||
| **`tex7`** | **7, textured** | **11.2** |
|
||||
|
||||
## The port sits inside the bracket, ordered by large-quad count
|
||||
|
||||
| screen | full-screen-ish quads | measured | bracket |
|
||||
|---|---|---|---|
|
||||
| `publisher_logo` | 1 | 17.3–25.0 | ≈ `tex3` |
|
||||
| `developer_logos` | 1 | 16.7–22.8 | ≈ `tex3` |
|
||||
| `title` | 6 | 12.7–17.2 | between `tex3` and `tex7` |
|
||||
| **`main_menu`** | **5** | **9.7** | ≈ `tex7` (11.2) |
|
||||
|
||||
**Every screen lands between the two matched controls, in the order the quad
|
||||
count predicts.** The port is drawing large textured alpha quads on a software
|
||||
rasteriser at exactly the rate a software rasteriser draws large textured alpha
|
||||
quads.
|
||||
|
||||
⚠️ Note the control's own spread: `fill3` measured **18.0** on one run and
|
||||
**31.5** on another. The container's load swings by ~1.75×, so the absolute
|
||||
numbers here are noise-dominated and only the **ordering and the bracket** are
|
||||
load-invariant — which is what `TEMPORAL-VERIFICATION.md` §3 says to prefer, and
|
||||
the reason this conclusion rests on those rather than on any single figure.
|
||||
|
||||
## 🔴 So the finding-4 candidate is downgraded, and I am saying so plainly
|
||||
|
||||
The section above called the frame rate *"the first candidate for finding 4 that
|
||||
is not already dead"*. **That now looks wrong, and it was mine.**
|
||||
|
||||
The quantisation argument still holds — a fade drawn in four steps is not a fade
|
||||
drawn in fifteen — but it only bites at these frame rates, and these frame rates
|
||||
are a property of **software rasterisation in this container**. Five to seven
|
||||
full-screen quads at 720p is nothing to any GPU; on real hardware the port would
|
||||
hit vsync and the fade would get its full 45 steps.
|
||||
|
||||
So unless the human ran the port software-rendered, this is not what they saw,
|
||||
and **every candidate for play-test finding 4 is now dead or near-dead.** That is
|
||||
an honest dead end rather than a lead, and it is written as one: leaving a
|
||||
plausible-sounding cause standing when its own control has undercut it is exactly
|
||||
how *"close but not right"* got explained four different wrong ways.
|
||||
|
||||
**The one thing that would revive it costs the human nothing**: the rate line now
|
||||
prints on every boot. If their next run says 60 fps, this is finished.
|
||||
|
||||
## What the port does NOT need
|
||||
|
||||
* No draw-path optimisation. 161.6 fps empty says the loop is fine, and the
|
||||
screens sit on the fill curve exactly where their quad counts put them.
|
||||
* No texture caching work — already done at `load_screen`, checked last
|
||||
iteration.
|
||||
* No `max_fps` or vsync change. It would not raise the rate here, and it is a
|
||||
presentation decision rather than a defect.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# ❌ Settled on hardware: 60–69 fps, and the candidate is dead
|
||||
|
||||
The human activated a **hardware GPU** in both containers on 2026-09-01, which
|
||||
made the one open half of this page directly testable. Godot picks it up with no
|
||||
change on our side:
|
||||
|
||||
```
|
||||
Vulkan 1.4.312 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce GTX 1070 Ti
|
||||
```
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
> Five to seven full-screen textured quads at 720p is trivial for that card. I
|
||||
> expect the port well above 60 fps on every screen, and the splash fade to get
|
||||
> its full 45 steps.
|
||||
|
||||
| screen | llvmpipe (before) | **GPU (after)** | worst gap |
|
||||
|---|---|---|---|
|
||||
| `publisher_logo` | 17.3–25.0 | **69.4** | 83 ms |
|
||||
| `developer_logos` | 16.7–22.8 | **69.1** | 15 ms |
|
||||
| `title` | 12.7–17.2 | **61.1** | 67 ms |
|
||||
| **`main_menu`** | **9.7** | **59.6** | 117 ms |
|
||||
|
||||
**A 3–6× jump, and every screen is now at or above 60 fps.** The prediction held.
|
||||
|
||||
## What that does to the fade
|
||||
|
||||
| animation | declared | steps drawn at 69 fps |
|
||||
|---|---|---|
|
||||
| splash build-in, 45 units = 0.750 s | 45 alphas | **52** |
|
||||
| companion glow's rise, 15 units = 0.250 s | 15 alphas | **17** |
|
||||
|
||||
**More frames than declared units, so every declared alpha is drawn.** The
|
||||
quantisation this page raised does not exist on this hardware — not reduced,
|
||||
*absent*.
|
||||
|
||||
## ❌ So the candidate is dead, and it died the right way
|
||||
|
||||
It was raised as a mechanism, downgraded by a matched control, and is now closed
|
||||
by a direct measurement on the hardware in question. **Every candidate for
|
||||
play-test finding 4 is now dead**: the keyframes are vindicated against the
|
||||
vertex stream, the pre-blurred companion quads are drawn, the blend space
|
||||
matches, the settled pose scores 0.01 % against the capture, there is no
|
||||
post-process pass, and the frame rate draws every declared step.
|
||||
|
||||
🔴 **The port has nothing left that is known to be wrong about the splashes, and
|
||||
that is a statement about our knowledge rather than about the port.** The human
|
||||
saw something. Nothing we can measure reproduces it.
|
||||
|
||||
**The next play-test is now the highest-value thing available on this focus**,
|
||||
and it is cheap: the rate line prints on every boot, so it will say 60-something
|
||||
rather than 10-something, and whatever remains will be visible against a port
|
||||
that is no longer coarse.
|
||||
|
||||
## The one figure that did not improve
|
||||
|
||||
`main_menu`'s **worst gap is 117 ms** on the GPU against 150 ms on llvmpipe —
|
||||
essentially unchanged while the mean improved 6×. A hitch that survives a 6× fill
|
||||
speed-up is not fill. It is most likely first-frame cost (texture upload, shader
|
||||
compilation) and it sits at the start of the screen, but that is **stated as
|
||||
untested**: nobody has separated it from load. It is small, it is once per
|
||||
screen, and it is recorded rather than chased.
|
||||
87
docs/port/rest-fallback-reaches-nothing.md
Normal file
87
docs/port/rest-fallback-reaches-nothing.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# The re-opened `rest()` pair cannot change a single pixel this port draws
|
||||
|
||||
**Status:** ✅ **measured, and it bounds a question rather than answering it.**
|
||||
Written 2026-09-01 by the Port at `94e44b5`; HANDOFF on this branch answers
|
||||
`9ca1eb5`.
|
||||
|
||||
The loop brief carries a standing warning: the **`rest()` pair is open in both
|
||||
directions**, and *"the two splashes are the only screens reaching that
|
||||
fallback."* R1 re-opened both legs, and 8 further claims died to
|
||||
`⟨render-vs-capture⟩` — an instrument that no longer exists in that form.
|
||||
|
||||
`ScreenView.settle_time()` uses `rest.t`, so this port has been sitting on a
|
||||
question with no instrument behind either leg. **It turns out not to matter, and
|
||||
that is worth establishing rather than waiting on.**
|
||||
|
||||
## What reaches the fallback
|
||||
|
||||
`Element::rest()` takes the longest **plateau** — a run of consecutive identical
|
||||
keyframes — and falls back to the longest dwell when there is none. So an element
|
||||
reaches the fallback exactly when no two consecutive keyframes are identical.
|
||||
|
||||
Census over all sixteen exported screens, at every nesting depth:
|
||||
|
||||
| screen | elements reaching the fallback |
|
||||
|---|---|
|
||||
| `publisher_logo` (and `_r`) | `palogo_sqex_eff` |
|
||||
| `developer_logos` (and `_r`) | `palogo_anima_eff` |
|
||||
| **`title_jp`** | **`ptlogo_eff3`** |
|
||||
|
||||
**Five elements in the whole export**, and only three distinct ones.
|
||||
|
||||
## 🔴 The brief's claim is narrowly refuted
|
||||
|
||||
*"The two splashes are the **only** screens reaching that fallback"* — `title_jp`
|
||||
reaches it too, through `ptlogo_eff3`.
|
||||
|
||||
⚠️ Stated as a measurement of the **current** export and nothing more. That
|
||||
sentence may have been true when written: the record-layout fix re-timed
|
||||
keyframes across the corpus, and a plateau is exactly the kind of thing it could
|
||||
create or destroy. This does not say the claim was wrong when made; it says it is
|
||||
not true now.
|
||||
|
||||
## And every one of them is invisible where it is read
|
||||
|
||||
The fallback only matters if the element it picks a pose for is actually drawn.
|
||||
Asked of the port directly, at each screen's own settled instant:
|
||||
|
||||
```
|
||||
publisher_logo t=140 drew 2 not drawn: palogo_sqex_eff (transparent at t=140)
|
||||
developer_logos t=117 drew 4 not drawn: palogo_anima_eff (transparent at t=117)
|
||||
title_jp --pose=rest drew 23 not drawn: ptlogo_eff3 (transparent at rest)
|
||||
```
|
||||
|
||||
**Every element that reaches the `rest()` fallback is fully transparent at the
|
||||
instant anything reads its rest pose.**
|
||||
|
||||
> So no `rest()` rule — the current plateau-plus-dwell, `last`, `maxalpha` or
|
||||
> `lastall` — can change any pixel this port draws. The pair is open, and for the
|
||||
> port it is **moot**.
|
||||
|
||||
That is why the splash rows score 0.01 % against their captures while resting on
|
||||
a heuristic nobody can currently defend: the heuristic is not load-bearing there.
|
||||
|
||||
## Why this was worth an iteration rather than a wait
|
||||
|
||||
The three elements are the **pre-blurred companion glows** and one title sparkle
|
||||
— transients that exist to be seen briefly and then leave. An element with no
|
||||
plateau is, almost by definition, one that never holds still, and a screen's
|
||||
settled instant is chosen to be where things are holding still. The two
|
||||
conditions are close to mutually exclusive, which is why the intersection is
|
||||
empty and why it was worth checking rather than assuming either way.
|
||||
|
||||
⚠️ **This does not rehabilitate `rest.t`.** It is still the wrong answer for
|
||||
transients — `ptlogo_back2eff1` is a two-frame flash whose `rest.t = 54` is the
|
||||
flash *peak*, and `rest()` would leave five of those burning at once on the
|
||||
title. That is a **plateau** case, not a fallback case, and it is untouched by
|
||||
anything here.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the `rest()` pair is settled. It is open, and this page does not touch it.
|
||||
* That `rest.t` is right. `settle_time()` still takes the maximum over elements,
|
||||
and what that means for *when the boot advances* is a separate question — a
|
||||
screen still plays to `exit_time()` afterwards, so the dwell is governed by
|
||||
that, not by `rest.t`. **Measured for the drawn picture; argued for the dwell.**
|
||||
* Anything about the other 7 claims R1 re-opened against
|
||||
`⟨render-vs-capture⟩`. Not mine to re-derive.
|
||||
160
docs/port/splash-animation-fixed.md
Normal file
160
docs/port/splash-animation-fixed.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# The splash: frozen, then fixed, and now checked for SHAPE as well as motion
|
||||
|
||||
**Status:** ✅ **animates, and the ramp matches the declared curve.** Written
|
||||
2026-09-02 by the Port; HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
The 2026-09-02 play-test found the splash frozen. The cause and the fix are in
|
||||
the commit history; this page is the part that comes after — **is the animation
|
||||
now the right animation?** `motion-census` says explicitly that it cannot answer
|
||||
that: *"a wrong ramp that moves every frame passes here."*
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
`palogo_sqex_eff` declares `0:a=0 → 15:a=255 → 30:a=212 → 45:a=0` — three
|
||||
segments, three gradients:
|
||||
|
||||
| segment | declared |
|
||||
|---|---|
|
||||
| 0 → 15 | **+17.0** per unit |
|
||||
| 15 → 30 | **−2.87** per unit |
|
||||
| 30 → 45 | **−14.13** per unit |
|
||||
|
||||
> So a film should show three straight runs with breakpoints at **15** and
|
||||
> **30**, the middle slope about **1/5** the magnitude of the last, and the rise
|
||||
> about **1.2×** the last.
|
||||
|
||||
## Measured, from a film of a real boot
|
||||
|
||||
Region: `686x11+299+319` — the companion's top strip, which **no other element
|
||||
overlaps**, so the number is that element's own alpha and not a composite.
|
||||
|
||||
| view units | strip mean | slope/unit |
|
||||
|---|---|---|
|
||||
| 5.58 | 0.02809 | |
|
||||
| 10.00 | 0.05086 | **+0.00515** |
|
||||
| 13.04 | 0.06611 | **+0.00503** |
|
||||
| 16.12 | 0.07530 | +0.00298 ← crossing the breakpoint |
|
||||
| 19.21 | 0.07260 | **−0.00087** |
|
||||
| 25.38 | 0.06695 | **−0.00092** |
|
||||
| 28.39 | 0.06482 | −0.00071 |
|
||||
| 31.39 | 0.05719 | −0.00254 ← crossing the breakpoint |
|
||||
| 34.39 | 0.04471 | **−0.00416** |
|
||||
| 40.39 | 0.01910 | **−0.00428** |
|
||||
| 43.39 | 0.00655 | −0.00418 |
|
||||
| 46.39 | 0.00000 | gone |
|
||||
|
||||
**Breakpoints land where declared**: the rise stops between 13.0 and 16.1, the
|
||||
gentle fall steepens between 28.4 and 31.4.
|
||||
|
||||
| ratio | declared | measured |
|
||||
|---|---|---|
|
||||
| middle : last | 0.203 | **0.213** |
|
||||
| rise : last | 1.20 | **1.20** |
|
||||
|
||||
Within 5 % and exact respectively. **The port interpolates piecewise-linearly
|
||||
across the declared segments** — which is what the Decoder independently measured
|
||||
the game doing (28 distinct alphas over 28 consecutive presents, modal steps −3
|
||||
and −14 against predicted −2.87 and −14.13).
|
||||
|
||||
⚠️ **Ratios, not absolutes, and deliberately.** The strip mean is an alpha
|
||||
scaled by whatever the sprite's own pixels are; its absolute value carries the
|
||||
texture. A ratio between segments divides that out, which is why the shape is
|
||||
checkable from a composite at all. Anyone quoting 0.00515 as an alpha is
|
||||
misreading it.
|
||||
|
||||
## What this still does not establish
|
||||
|
||||
* That it **looks** right. Three instruments have now agreed with a picture a
|
||||
human called wrong, and the fourth agreeing does not change the standing of
|
||||
the fifth. A play-test is the check.
|
||||
* ~~Anything about the **developer** splash's three logo/companion pairs. One
|
||||
element, one screen.~~ ✅ **Closed below, and the developer splash turned out to
|
||||
be the far better test.**
|
||||
* The **absolute** alpha. The shape matches; whether the port's alpha equals the
|
||||
game's at a given unit is `verify-capture`'s question, and it answers 0.01 %
|
||||
at the settled pose only.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# ✅ The developer splash: three elements, TWO declared shapes, on one screen
|
||||
|
||||
The publisher check above had a structural weakness I named at the time: one
|
||||
element, one screen, one shape. If the port applied *some* single ramp to
|
||||
everything, that check would pass.
|
||||
|
||||
**The developer splash cannot be fooled that way, because it declares two
|
||||
different shapes at once:**
|
||||
|
||||
| element | declared alpha | middle segment |
|
||||
|---|---|---|
|
||||
| `palogo_gamearts_eff` | `0:0 15:255 30:255 45:0` | **flat** |
|
||||
| `palogo_seta_eff` | `0:0 15:255 30:255 45:0` | **flat** |
|
||||
| `palogo_anima_eff` | `0:0 15:255 30:212 45:0` | **decays 17 %** |
|
||||
|
||||
Same screen, same frames, same clock, same code path. The two flat elements are
|
||||
the control for the one that is not — no second run, no second renderer, and
|
||||
nothing for a phase or rate error to hide behind, because any such error hits all
|
||||
three identically.
|
||||
|
||||
## Pre-registered, before the film was read
|
||||
|
||||
> In units 15→30, `gamearts` and `seta` hold flat while `anima` falls gently.
|
||||
> All three fall steeply 30→45.
|
||||
|
||||
## Measured, off a real filmed boot
|
||||
|
||||
Companion-only strips (each companion's rectangle minus the logo sitting inside
|
||||
it), mean luma, `--film-interval=0.03`:
|
||||
|
||||
```
|
||||
segment units 16-29 units 31-44
|
||||
gamearts +0.00000 -0.00101
|
||||
seta +0.00000 -0.00151
|
||||
anima -0.00001 -0.00003
|
||||
```
|
||||
|
||||
**The two flat ones are flat to five decimals. The decaying one decays.** And all
|
||||
three fall over 31–44, so the flatness is not a dead element.
|
||||
|
||||
## The quantitative version, and the falsification arm
|
||||
|
||||
Each trace normalised by a **single solved gain** — one scalar per element, not a
|
||||
per-point fit — against the declared curve, over the whole `t=0..45` ramp:
|
||||
|
||||
| element | fitted against | max err | rms err |
|
||||
|---|---|---|---|
|
||||
| `gamearts` | **its own (flat)** | **0.49 %** | 0.23 % |
|
||||
| `seta` | **its own (flat)** | **0.50 %** | 0.27 % |
|
||||
| `anima` | **its own (decay)** | **2.18 %** | 1.19 % |
|
||||
| `gamearts` | anima's decay | 8.32 % | 4.80 % |
|
||||
| `seta` | anima's decay | 8.08 % | 4.86 % |
|
||||
| `anima` | flat | 8.82 % | 4.94 % |
|
||||
|
||||
**Both directions.** Every element fits its own declared shape 4–17× better than
|
||||
it fits the other one available on the same screen. A port that drew one ramp for
|
||||
all three would sit at ~8 % on at least one row; none does.
|
||||
|
||||
`anima`'s 2.18 % is the loosest row and the reason is its signal: its
|
||||
companion-only strip means ~0.0005 against gamearts's ~0.015, thirty times
|
||||
dimmer, so its noise floor is thirty times higher in these units. It still
|
||||
separates from the wrong curve by 4×.
|
||||
|
||||
## What this adds over the publisher check
|
||||
|
||||
The publisher check established that the ramp has the declared *breakpoints and
|
||||
slope ratios*. This establishes that the ramp is **per-element** — that the port
|
||||
reads each element's own keyframes rather than applying a screen-wide curve. That
|
||||
is a different failure mode, and it is the one that would have survived the
|
||||
publisher check unnoticed.
|
||||
|
||||
## What it still does not establish
|
||||
|
||||
* That it **looks** right. This is instrument five. The standing of a human's
|
||||
eyes is unchanged by it.
|
||||
* The **absolute** alpha, for the same reason as above — a solved gain is
|
||||
deliberately scale-free, so this is a shape result and says nothing about
|
||||
whether the port's alpha equals the game's at a given unit.
|
||||
* Anything about the **logo** elements. Both checks measure the pre-blurred
|
||||
*companions*, because those are the strips that can be isolated from the sharp
|
||||
logo underneath them.
|
||||
194
docs/port/splash-rate-contradiction.md
Normal file
194
docs/port/splash-rate-contradiction.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# The proposed splash rate contradicts the splash dwells — NOT adopted
|
||||
|
||||
**Status:** ✅ **RESOLVED — the rate was withdrawn.** The Decoder withdrew it the
|
||||
same day (`splash-rate-withdrawn.md`, `1e7343e` *"WITHDRAW 'the unit rate is
|
||||
per-GamePart' — it was the emulator's frame rate"*), and struck the section that
|
||||
carried it. §1 of `splash-declared-vs-captured.md` — the keyframe vindication —
|
||||
stands, because it never divides by a duration.
|
||||
|
||||
**The port never moved, so nothing has to be undone.** What follows is the
|
||||
refutation as it was made, kept because the shape of the error is reusable: a
|
||||
duration measured in emulator frames is the emulator's rate, not the game's, and
|
||||
the tell was that it made a part outlast its whole.
|
||||
|
||||
---
|
||||
|
||||
**Original status:** 🔴 **refutation attempt, and it lands.** Two of the Decoder's own
|
||||
measurements, of **the same two screens**, disagree by 1.7×. The port has **not**
|
||||
changed `keyframe_units_per_second` and is still at 60. Written 2026-09-01 by the
|
||||
Port at `0a9bf4e`; HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
## What was proposed
|
||||
|
||||
`docs/re/splash-declared-vs-captured.md` (`origin/auto/frame-blend-draw-path`)
|
||||
reports that one rate cannot cover every screen:
|
||||
|
||||
| screen | evidence | units/guest-second |
|
||||
|---|---|---|
|
||||
| title | `ptbtn00` ramp, `T=22` | 56.8 |
|
||||
| splash | `palogo_gamearts` ramp, `T=15` | 39.1 |
|
||||
| splash | **160-unit hold in 4.514 guest s** | **35.4** |
|
||||
|
||||
with the conclusion that *"a splash played at 60 units/s runs 1.5–1.7× too
|
||||
fast"*, and a recommendation to use ~35–40 for the splashes. The hold leg is
|
||||
offered as the safe one, and the argument for it is good: **a hold carries no
|
||||
`T`** — it is a declared duration measured directly, with no alpha slope and no
|
||||
interpolation in the chain.
|
||||
|
||||
## Which hold it is — identified, not assumed
|
||||
|
||||
Exactly one interval in either splash is 160 units. From `export/`:
|
||||
|
||||
| screen | element | `a=255` from → to | hold |
|
||||
|---|---|---|---|
|
||||
| `developer_logos` | `palogo_gamearts` (and `_seta`, `_anima`) | t=30 → t=190 | **160 units** |
|
||||
| `publisher_logo` | `palogo_sqex` | t=30 → t=235 | 205 units |
|
||||
|
||||
So the 160-unit hold is the **developer** splash's full-alpha plateau, and it
|
||||
sits **inside** that screen's declared group of `t = 0…210`.
|
||||
|
||||
## The contradiction
|
||||
|
||||
`authored/timing.json` already carries a measurement of that same screen —
|
||||
`docs/re/structures/boot-splash-dwells-are-declared.md`, the Decoder's, over
|
||||
**three cold boots**:
|
||||
|
||||
```
|
||||
publisher declared t=0..255 corpus 4.30 / 4.60 / 4.37 s
|
||||
developer declared t=0..210 corpus 3.51 / 3.50 / 3.37 s
|
||||
```
|
||||
|
||||
🔴 **The 160-unit hold is measured at 4.514 s. The 210-unit group that contains
|
||||
it is measured at 3.37–3.51 s. A sub-interval cannot outlast the interval
|
||||
containing it.**
|
||||
|
||||
That is not two methods disagreeing about a rate. It is an arithmetic
|
||||
impossibility, and one of the two measurements is wrong.
|
||||
|
||||
| | implied units/s |
|
||||
|---|---|
|
||||
| developer whole group, 210 units in 3.46 s (3 boots) | **60.7** |
|
||||
| publisher whole group, 255 units in 4.42 s (3 boots) | **57.7** |
|
||||
| developer 160-unit hold in 4.514 s (1 run) | 35.4 |
|
||||
|
||||
**The two dwell measurements corroborate ~60 on exactly the two screens the new
|
||||
figure puts at 35–39**, they agree with each other to 5 %, and the developer
|
||||
figure agrees with its declared value to 1.1 % — two of its three runs to 0.3 %.
|
||||
|
||||
At 35.4 units/s the declared groups would run **5.93 s** and **7.20 s**, against
|
||||
corpus dwells of 3.37–3.51 and 4.30–4.60. The port would show each splash for
|
||||
about 70 % longer than three cold boots measured them lasting.
|
||||
|
||||
## The one escape route, named rather than dismissed
|
||||
|
||||
The new figure is quoted in **guest seconds** and the dwell corpus in wall-clock
|
||||
seconds. If those clocks differ by 1.7× the two are not comparable and there is
|
||||
no contradiction.
|
||||
|
||||
**It does not look like the answer.** The known gap between the two is Canary
|
||||
presenting at ~28.1 fps against a nominal 30 — about **6 %**, not 71 %, and in
|
||||
the wrong direction to close a factor of 1.7. But this is the Decoder's
|
||||
instrument and the Decoder's clock, so it is asked rather than ruled out here.
|
||||
|
||||
## What the port did
|
||||
|
||||
**Nothing.** `keyframe_units_per_second` stays at **60**, one value, for every
|
||||
screen.
|
||||
|
||||
This is deliberate and it is the conservative half in both directions: 60 is what
|
||||
three cold boots of both splashes support, and it is what the port has been
|
||||
shipping, so not moving costs nothing that was not already being paid. Adopting
|
||||
35.4 would slow both splashes by 70 % on the strength of a number that the same
|
||||
agent's earlier measurement of the same screens says is impossible.
|
||||
|
||||
⚠️ **And the proposal's structural claim may well be right even if this figure is
|
||||
not.** *"One rate cannot cover every screen"* is a claim about the format, and it
|
||||
is supported independently by the title's 56.8 sitting 5 % off the splashes'
|
||||
~58–61. If a per-screen rate is real, the port will need the mechanism — a field
|
||||
or a `GamePart` constant — and not two authored numbers. The Decoder has *"where
|
||||
the per-GamePart rate actually comes from"* as its next item, which is the right
|
||||
question.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the title's 56.8 is wrong. It rests on a different screen and a
|
||||
gap-free ramp, and nothing here touches it.
|
||||
* That the splash **keyframes** are wrong. They are now vindicated — see below.
|
||||
* That the dwell corpus is right and the new capture wrong. Only that they cannot
|
||||
both be, and that the port must not move on the strength of the one that
|
||||
contradicts the other.
|
||||
|
||||
## ✅ Separately, and it is good news: the splash keyframes are vindicated
|
||||
|
||||
`docs/re/splash-declared-vs-captured.md` also settles the R1-re-opened *"the
|
||||
declared keyframe timeline reproduces the captured splash"*, **in favour of the
|
||||
timeline** — disc table against vertex stream, no renderer in the chain: 50
|
||||
captured alphas, 39 exact under truncation, worst error **one alpha level in
|
||||
255**.
|
||||
|
||||
That entry was 🟡 `⟨our-reader⟩` and is the one the play-test's finding 4 leaned
|
||||
on. **The port's splash keyframes were never the defect**, which means the
|
||||
remaining candidate for what the human saw on the splashes is the *rate* — and
|
||||
the rate is precisely what this page declines to change.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# ✅ And what the splashes actually do, measured on the shipping boot
|
||||
|
||||
With the rate settled at 60, the keyframes vindicated against the vertex stream,
|
||||
and `verify-capture` scoring both splashes at **0.01 %** against the oracle
|
||||
frames, the one thing nobody had checked was the **real-time boot path** — the
|
||||
port's own timing, end to end, rather than a frozen pose or a model.
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
> At 60 units/s, `publisher_logo` holds for 255 + 9 = **264 units = 4.400 s** and
|
||||
> `developer_logos` for 210 + 9 = **219 units = 3.650 s** — the figures
|
||||
> `authored/timing.json` states the port emits. Accept ±0.10 s.
|
||||
|
||||
Three boots, `--skip-at=1`, no film (so nothing competes with the renderer):
|
||||
|
||||
| | run 1 | run 2 | run 3 | mean | declared | residual |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `publisher_logo` | 4.28 | 4.26 | 4.27 | **4.270 s** | 255 u = 4.250 s | **+1.2 units** |
|
||||
| `developer_logos` | 3.50 | 3.57 | 3.51 | **3.527 s** | 210 u = 3.500 s | **+1.6 units** |
|
||||
|
||||
**The prediction failed, by 0.130 s and 0.123 s — and the port was right.**
|
||||
|
||||
## 🔴 The failure was in the claim, not the code
|
||||
|
||||
`authored/timing.json` sets **`black_hold_units = 0`**, deliberately, with its
|
||||
own argument attached: a uniform black hold is *positively excluded* — the
|
||||
Decoder's five replicates show the same origin giving different values to
|
||||
different destinations — so only an ordered-pair key survives and nothing may be
|
||||
authored until one is measured.
|
||||
|
||||
So there is no 9-unit hold to add, and the port has never added one. Yet two
|
||||
places asserted it did:
|
||||
|
||||
* `authored/timing.json`, `dwell_why`: *"The port emits 4.400 s and 3.650 s —
|
||||
each declared value plus the 9-unit black hold, exactly. So the pacing was
|
||||
right all along and nothing changes in the code."*
|
||||
* `port/scripts/boot.gd`, in the `_advance` block: the same sentence.
|
||||
|
||||
**The `why` asserted a behaviour that the same file refused three keys below,
|
||||
and the code comment repeated it.** Both are corrected in place. Nothing in the
|
||||
port changed — this commit fixes a false statement about our own behaviour, and
|
||||
the port has been shipping 4.270 / 3.527 since P3.
|
||||
|
||||
## What it does not settle
|
||||
|
||||
Against the corpus dwells (means **4.42** and **3.46** s) neither figure
|
||||
dominates: the port is 3.4 % short on the publisher and 2.0 % long on the
|
||||
developer, where the claimed values would be 0.5 % short and 5.5 % long. **So
|
||||
this does not show a hold does not belong there** — it shows nobody had checked
|
||||
whether the port did what it said. `black_hold_why`'s ordered-pair ask stands
|
||||
unchanged.
|
||||
|
||||
⚠️ Worth naming as a pattern rather than an incident: this is the third time in
|
||||
this corpus that a `why` described behaviour the code did not have. The previous
|
||||
two were `exit_ramp_units` and the `dwell` slot that *"was read NOWHERE for eight
|
||||
milestones"*. The common shape is a value **authored, documented, and never
|
||||
exercised end-to-end** — and the only thing that catches it is running the
|
||||
shipping path and timing it.
|
||||
356
docs/port/units-per-second-switch-readiness.md
Normal file
356
docs/port/units-per-second-switch-readiness.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# If 120 units/s is right, the switch is one constant — audited, and pre-registered
|
||||
|
||||
**Status:** ✅ **SETTLED by a designed experiment.** The game's clock is
|
||||
**frame-based, 1 unit per present**; this port's is time-based; **they agree at
|
||||
60 Hz and the port keeps its own design.** 120 is withdrawn by its author. The
|
||||
port never moved, so nothing has to be undone.
|
||||
🔴 **Two things on this page are mine and wrong: the falsifier (conceded
|
||||
mid-page) and the "no hold / two errors that cancel" finding (withdrawn at the
|
||||
foot).** Written
|
||||
2026-09-01 by the Port at `b42ff38`; HANDOFF on this branch answers `9ca1eb5`.
|
||||
|
||||
The Decoder now measures **120 units/s** with a content-hash experiment whose
|
||||
controls are the ones the withdrawn version lacked. **It is their third position
|
||||
on this number in one day**, and they said plainly that a second independent boot
|
||||
before a timeline is rewritten is the defensible call, and that they would rather
|
||||
this port held for another iteration than swung twice on their say-so.
|
||||
|
||||
**Agreed, and the port has not moved.** What follows is the work that is worth
|
||||
doing *now* regardless of which value wins.
|
||||
|
||||
## Why hold
|
||||
|
||||
1. **Three positions in a day**, two of them already withdrawn by their author.
|
||||
2. **Reach is one boot.** They say so.
|
||||
3. They do **not** offer the 2.13 s reconciliation as support — it needs a ~47 %
|
||||
emulator speed fitted post-hoc, and they label it as the thing this corpus
|
||||
keeps losing claims to.
|
||||
4. Doubling a shipped timeline is the change a play-test would notice most, and
|
||||
the current value is the one a human has already seen.
|
||||
|
||||
None of that is an argument that 60 is right. **60 has no surviving derivation
|
||||
either** — its bracket was withdrawn this morning. Both numbers are now
|
||||
undefended; the port keeps the one it ships because switching on a single capture
|
||||
is a worse failure than holding on none.
|
||||
|
||||
## The audit they asked for, and it comes out clean
|
||||
|
||||
> *"Every duration in SECONDS I have ever handed you is half what it should be. If
|
||||
> your timeline is authored in units and converted once, this is a single
|
||||
> constant. If seconds are baked in anywhere, they all move."*
|
||||
|
||||
Every numeric constant in `authored/`, and every float in the port's code:
|
||||
|
||||
| where | value | seconds? | moves with the constant? |
|
||||
|---|---|---|---|
|
||||
| `timing.json` `keyframe_units_per_second` | 60 | — | **it IS the constant** |
|
||||
| `timing.json` `black_hold_units` | 0 | no — **units** | ✅ derived |
|
||||
| `timing.json` `dwell_seconds` | `null` | n/a | — |
|
||||
| `flow.json` `dwell` | prose only, "NOT SET" | n/a | — |
|
||||
| `audio.json` `loop_start_s` / `loop_end_s` | 9.44 / 61.87 | **yes** | ✅ **correctly not** — these are positions in an audio file, real-time by nature, with no keyframe unit in them |
|
||||
| `gamepad.gd` `ENTER` / `RELEASE` | 0.61 / 0.4 | no — deflection | — |
|
||||
|
||||
**No seconds are baked into the timeline anywhere.** Every second the port prints
|
||||
or acts on is computed as `units / units_per_second` at the point of use —
|
||||
`settle_time()`, `exit_time()`, `_overlay_quit_at`, the boot's own log lines. The
|
||||
switch is one number in one file.
|
||||
|
||||
## 🔴 Except one, and it was hiding behind a comment about not drifting
|
||||
|
||||
`tools/port/verify-dwell` had:
|
||||
|
||||
```python
|
||||
# Read from the authored file so it cannot drift again, and REPORT the shortfall
|
||||
PORT_HOLD = float(...get("black_hold_units", 0)) / 60.0
|
||||
```
|
||||
|
||||
**The value was read from the file. The rate was a literal.** The value could not
|
||||
drift; the conversion could, and would have gone silently wrong the moment
|
||||
`keyframe_units_per_second` moved — which is under active dispute right now, so
|
||||
it is a live hazard rather than a tidy-up. Harmless only because the hold is
|
||||
currently 0.
|
||||
|
||||
Fixed to read `keyframe_units_per_second` from the same file it already opens.
|
||||
|
||||
⚠️ The shape is worth more than the line: **a comment asserting that something
|
||||
cannot drift, one expression above a hardcoded copy of the thing that drifts.**
|
||||
That is the third time in this corpus a `why` has described a property the code
|
||||
did not have.
|
||||
|
||||
## Pre-registered: what a switch to 120 would do (R2)
|
||||
|
||||
Written **before** any second boot, so the switch is checkable rather than a
|
||||
leap. At 120 units/s every declared interval halves in seconds; unit counts and
|
||||
`2 units/present` are untouched.
|
||||
|
||||
| | declared | at 60 (shipping) | **at 120** |
|
||||
|---|---|---|---|
|
||||
| `PRESS Ⓐ` plate, full | t=236 | 3.933 s | **1.967 s** |
|
||||
| plate ramp onset | t=214 | 3.567 s | **1.783 s** |
|
||||
| publisher splash group | 255 units | 4.250 s | **2.125 s** |
|
||||
| developer splash group | 210 units | 3.500 s | **1.750 s** |
|
||||
| title build-in end | t=118 | 1.967 s | **0.983 s** |
|
||||
| `ptcopyright` full | t=160 | 2.667 s | **1.333 s** |
|
||||
|
||||
**The falsifier is the splash dwells.** Three cold boots measured the publisher
|
||||
at 4.30 / 4.60 / 4.37 s and the developer at 3.51 / 3.50 / 3.37 s. At 120 the
|
||||
port would show them for **2.13 s** and **1.75 s** — roughly half what those
|
||||
boots recorded.
|
||||
|
||||
🔴 So 120 and the dwell corpus cannot both be right **in wall-clock seconds**, and
|
||||
that is the same collision that killed the earlier 35 units/s proposal from the
|
||||
other direction. Either those dwells carry the emulator's speed factor — which
|
||||
would make them worth exactly as little as the 2.13 s route the Decoder has
|
||||
already declined to lean on — or 120 is wrong. **Naming the falsifier now is the
|
||||
point of writing this before the boot rather than after.**
|
||||
|
||||
## What would move the port
|
||||
|
||||
A second independent boot agreeing with the content-hash result, **and** a
|
||||
statement about whether the cold-boot dwell corpus survives the same speed-factor
|
||||
objection that the 2.13 s route does not. The first without the second leaves a
|
||||
2× contradiction standing between two numbers this port would then be holding
|
||||
simultaneously.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 🔴 My falsifier was malformed — and following it through found something worse
|
||||
|
||||
## Conceded: it compared two different quantities
|
||||
|
||||
I set the falsifier as *"at 120 the publisher splash runs 2.125 s, against three
|
||||
cold boots measuring 4.30/4.60/4.37"*. **2.125 s is the declared *animation*
|
||||
length. 4.3 s is how long the *screen* is up.** The screen holds after the
|
||||
timeline ends, so those are not the same quantity and **the comparison would have
|
||||
found a contradiction at any units-per-second at all.**
|
||||
|
||||
The Decoder also checked the defence I offered them — that the dwell corpus might
|
||||
be emulator-contaminated — and declined it: their capture reproduces those boots
|
||||
(publisher 4.263 s, developer 3.457 s, four runs agreeing). **They could have
|
||||
waved my numbers away and did not.**
|
||||
|
||||
## But the port has NO hold, and that is the part nobody had said
|
||||
|
||||
| | declared | port measured | animation at 60 | **hold** |
|
||||
|---|---|---|---|---|
|
||||
| `publisher_logo` | 255 units | 4.270 s | 4.250 s | **+0.020 s** |
|
||||
| `developer_logos` | 210 units | 3.527 s | 3.500 s | **+0.027 s** |
|
||||
|
||||
**The port's screen time *is* its animation time.** The game, by the Decoder's
|
||||
counts, is on screen for 219 presents and animates for ~128 of them — about
|
||||
**42 % hold**.
|
||||
|
||||
So if 120 is right, this port is making **two errors that cancel**:
|
||||
|
||||
* it animates every splash **2× too slow**, and
|
||||
* it omits the hold **entirely**,
|
||||
|
||||
and the two sum to almost exactly the right total screen time. That is why the
|
||||
dwell check has been passing, and **`authored/timing.json` cites that agreement as
|
||||
proof the pacing is right** — *"So the pacing was right all along and nothing
|
||||
changes in the code."* Conditional on 120, that sentence is a coincidence of
|
||||
compensating errors. Corrected in place.
|
||||
|
||||
⚠️ A passing check on a *sum* cannot see two errors of opposite sign inside it.
|
||||
That is the fourth member of today's family — after the non-inverting latch check,
|
||||
the unguarded buffer assumption, and the segmentation that nearly produced a
|
||||
convenient answer.
|
||||
|
||||
## 🔴 And 120 explains both open play-test findings. 60 explains neither.
|
||||
|
||||
This is not a measurement and it is not mine to call decisive. It is the one
|
||||
thing the port can contribute that no emulator capture can: **what a human
|
||||
watching both actually reported.**
|
||||
|
||||
| | at 60 (shipping) | at 120 |
|
||||
|---|---|---|
|
||||
| **finding 3** — *"the plate arrives late"* | full at **3.933 s** | **1.967 s** |
|
||||
| **finding 4** — *"the game's fade is more pronounced"* | build-in **0.750 s**, then the screen leaves at once | build-in **0.375 s**, then the screen **holds ~1.7 s** |
|
||||
|
||||
At 60 the port's logo drifts in slowly and leaves immediately. At 120 it snaps in
|
||||
and sits — which is what *"more pronounced"* describes, and the hold is what makes
|
||||
a splash read as a splash rather than a transition.
|
||||
|
||||
**Finding 4 is explained twice over by the same constant**: the fade is 2× slow
|
||||
*and* the hold is missing, and both follow from one number.
|
||||
|
||||
Every named cause for both findings has died over the past several iterations.
|
||||
**120 is the first hypothesis that accounts for either, and it accounts for
|
||||
both.**
|
||||
|
||||
## The hold and the constant are coupled — do not add one without the other
|
||||
|
||||
⚠️ **At 60 the port must NOT gain a hold.** The animation already fills the screen
|
||||
time; adding a hold would overshoot the measured dwells by ~40 %. The missing
|
||||
hold is only a defect *if* 120 is right. They stand or fall together, and that is
|
||||
precisely why this port is still not moving on one capture.
|
||||
|
||||
## What would complete it
|
||||
|
||||
Unchanged, and now sharper: **a second independent boot of the content-hash
|
||||
ratio.** The dwell objection is withdrawn — they answered it with a count against
|
||||
a hard limit (51.4 presents per host-second on the publisher, against a ceiling of
|
||||
30 for a 30 fps guest) rather than a duration against a fitted factor.
|
||||
|
||||
When that lands, the switch is **two** changes, not one: the constant, and a hold
|
||||
whose length is `screen_presents − animation_presents` and which must be
|
||||
**measured, not inferred from the total** — because the total is exactly the
|
||||
quantity that cannot distinguish the two errors.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 🔴 WITHDRAWN: "the port has no hold" and "two errors that cancel"
|
||||
|
||||
Both were mine, both were last iteration's headline, and both are wrong.
|
||||
|
||||
## What I misread
|
||||
|
||||
The Decoder gave a split — *"219 presents on screen, ~128 animating"* — and I
|
||||
read it as a hold **outside** the declared timeline, which the port would then be
|
||||
missing. **It is a split *within* the timeline.** The declared group is ramp +
|
||||
hold + fade, and the hold is the largest part of it:
|
||||
|
||||
```
|
||||
publisher palogo_sqex: 0:a=0 15:a=0 30:a=255 235:a=255 239:232 251:32 255:0
|
||||
ramp 0→30 = 30 units
|
||||
HOLD 30→235 = 205 units ← 80.4 % of the screen
|
||||
fade 235→255 = 20 units
|
||||
```
|
||||
|
||||
## The port plays it. Measured, not read
|
||||
|
||||
Frozen samples across the publisher splash, logo region:
|
||||
|
||||
| t | units | region mean |
|
||||
|---|---|---|
|
||||
| 0.25 s | 15 | 0.390957 |
|
||||
| 1.00 s | 60 | **0.405488** |
|
||||
| 2.00 s | 120 | **0.405488** |
|
||||
| 3.00 s | 180 | **0.405488** |
|
||||
| 3.80 s | 228 | **0.405488** |
|
||||
| 4.20 s | 252 | 0.038142 |
|
||||
|
||||
**Identical to six decimals across 168 units.** The port holds, for 80 % of the
|
||||
screen, exactly as declared.
|
||||
|
||||
So there was never a missing hold, and therefore never a pair of cancelling
|
||||
errors. **`authored/timing.json`'s "the pacing was right all along" was right all
|
||||
along**, and my paragraph casting it as a possible coincidence is withdrawn there
|
||||
too.
|
||||
|
||||
⚠️ The failure is worth naming because it is not the usual one: I did not
|
||||
mis-measure anything. **I took a two-part split from someone else's instrument and
|
||||
assumed the boundary was where my own model put it.** Presents are not units, and
|
||||
"animating vs holding" in presents does not decompose the same way as "ramp vs
|
||||
hold" in declared units.
|
||||
|
||||
# ✅ And the port's own data is an independent leg for the time-based clock
|
||||
|
||||
The Decoder's mechanism: `units/present` halved when the present rate doubled
|
||||
while `units/second` did not move, so the UI clock advances by elapsed **time**,
|
||||
not by frame count — and *"2 units per frame"* was a property of a 27 fps capture
|
||||
rather than of the game.
|
||||
|
||||
**The frame-rate work of two iterations ago tests exactly that, and I did not
|
||||
notice at the time.** The same splash, measured across a 4× change in the port's
|
||||
own rendering rate:
|
||||
|
||||
| | frame rate | dwell | implied units/s |
|
||||
|---|---|---|---|
|
||||
| llvmpipe | 17.3 fps | 4.28 s | 59.6 |
|
||||
| llvmpipe | 19.6 fps | 4.26 s | 59.9 |
|
||||
| llvmpipe | 25.0 fps | 4.27 s | 59.7 |
|
||||
| **GPU** | **69.4 fps** | **4.26 s** | **59.9** |
|
||||
|
||||
**Frame rate varies 4.0×; the dwell varies by 0.5 %.** That is the signature of a
|
||||
time-based clock, and it puts the rate at 59.6–59.9 every time.
|
||||
|
||||
🔴 **This is a weaker leg than it looks and I am labelling it rather than
|
||||
counting it.** It shows *the port's* clock is time-based — which it is by
|
||||
construction, `time_units += delta * units_per_second` — so it cannot be evidence
|
||||
about the game's. What it does show is that the **dwell figures I supplied are
|
||||
frame-rate-independent measurements**, not artefacts of whatever rate a run
|
||||
happened to hit. That is the property their argument needs of them, and it is now
|
||||
established from this side rather than assumed.
|
||||
|
||||
# Where finding 3 stands
|
||||
|
||||
**Open, with no surviving named cause.** Units-per-second is eliminated in favour
|
||||
of the value the port already ships; every other candidate died earlier. The
|
||||
clock origin remains untouched, and every quantity in the resolved account is a
|
||||
ratio or a count, so a common offset survives all of it.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# ✅ Settled: the game is frame-based, the port is time-based, and that is correct
|
||||
|
||||
`--framerate_limit=30` — the run this page asked for — refuted the time-based
|
||||
reading on every discriminating row, **against its author's own expectation**:
|
||||
|
||||
| | predicted if time-based | **measured at 30 fps** |
|
||||
|---|---|---|
|
||||
| modal alpha step | 34 | **17** (unchanged) |
|
||||
| units/second | 60 | **30.2** (halved) |
|
||||
| publisher dwell | 4.25 s | **8.450 s** (doubled) |
|
||||
|
||||
Both controls passed *first*: the limiter demonstrably took effect (28.4
|
||||
presents/host-s against 51–55, interval mass moving to two vblanks, 422 of 468),
|
||||
and all 8 splash quad rects were identical, so nothing but the frame rate
|
||||
differed. **`255 × 1 / 15 = 17`** at 28.4, 51.4 and 54.8 presents/s alike.
|
||||
|
||||
## What that changes for the port: nothing — but for a reason worth writing down
|
||||
|
||||
The game advances **1 unit per presented frame**. This port advances
|
||||
`time_units += delta * units_per_second`. **They are different mechanisms that
|
||||
agree at exactly one frame rate: 60 Hz** — which is the only rate the console
|
||||
ever asked the game to be right at.
|
||||
|
||||
🔴 **Do not make the port frame-based to match the game.** A time-based port
|
||||
reproduces a 60 Hz console on hardware that is not 60 Hz; a frame-based one would
|
||||
drift on every machine that is not — and this port has measured *itself* between
|
||||
**9.7 and 69.4 fps** depending on the renderer. Matching the game's mechanism
|
||||
would import a fragility the game never had to survive, because the game only
|
||||
ever ran on one box.
|
||||
|
||||
## 🔴 But it sharpens what `60` is claiming, and makes it falsifiable
|
||||
|
||||
If units/second **is** the present rate, then `keyframe_units_per_second = 60` is
|
||||
no longer "the unit is 1/60 s". It is:
|
||||
|
||||
> **the game presented these screens at 60 Hz on the console.**
|
||||
|
||||
That is a harder claim and a checkable one. **It is also supported for the first
|
||||
time:** Canary unlimited presents at 51–55 Hz and the splash dwell is
|
||||
4.30 / 4.60 / 4.37 s over three cold boots. A natively 30 Hz game would present
|
||||
at ~30 in Canary too — the `--framerate_limit` run proves it, since forcing 30
|
||||
made that same splash take 8.45 s. **It does not take 8.45 s unforced.**
|
||||
|
||||
⚠️ Still `authored`, not promoted to `measured`: this is inference over three
|
||||
measurements rather than a measurement of units per second. It becomes `measured`
|
||||
when someone reads the console's present rate for these screens directly.
|
||||
|
||||
## And it closes the constant as a cause of finding 3 — in the direction that matters
|
||||
|
||||
| console rate | units/s | plate `t=236` |
|
||||
|---|---|---|
|
||||
| 30 Hz | 30 | **7.87 s** |
|
||||
| **60 Hz (shipping)** | **60** | **3.93 s** |
|
||||
|
||||
Under the frame-based model the only alternative is 30 Hz, and it puts the plate
|
||||
**later**, not earlier. **There is no console present rate that makes the plate
|
||||
arrive sooner than this port already shows it.** The human reported it arriving
|
||||
late; no value of this constant can produce that.
|
||||
|
||||
## The method note, and it is theirs
|
||||
|
||||
> *Four of my positions on this number were inference over a measured quantity;
|
||||
> this one changed an input and watched what moved.*
|
||||
|
||||
The opportunistic comparison — two captures that happened to differ — pointed
|
||||
**exactly the wrong way**, because nothing controlled what else differed between
|
||||
them. One designed capture settled it against its author's expectation. That is
|
||||
the difference between an observation and an experiment, and it cost this pair
|
||||
five positions in a day to relearn.
|
||||
401
docs/port/verify-screen-blend-divergence.md
Normal file
401
docs/port/verify-screen-blend-divergence.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# `verify-screen` DIFFERS on six more screens — it is ADDITIVE, and the port is ahead of the reference
|
||||
|
||||
**Status:** ✅ **RESOLVED.** The reference gained an additive path
|
||||
(`formats-pin-2026-09-01b`) and the divergence this page is about collapses **6×**.
|
||||
Everything below stands; the last section is the measurement that closes it. Port `HEAD` `da7864e` + this
|
||||
commit; HANDOFF at `9ca1eb5`.
|
||||
|
||||
> 🔴 **This page said, in its first version, that the cause was a blend-SPACE
|
||||
> divergence — one renderer linearising and the other not. That was wrong, and
|
||||
> the way it was wrong is the useful part.** Both renderers demonstrably blend in
|
||||
> the encoded space. The transfer curve I built to support it averaged Godot's
|
||||
> value per *reference value* bucket, which collapsed a **bimodal** population —
|
||||
> a large majority differing by ≤1 level and a minority differing by 40+ — into a
|
||||
> smooth-looking curve that resembled gamma and was an artefact of the binning.
|
||||
> A mean over a mixed population is not a transfer function.
|
||||
|
||||
## The measurement that settles it
|
||||
|
||||
53 % of pixels agree to within **1 level** and 69 % to within 3. The rest are not
|
||||
spread over a curve; they are **concentrated in the middle of the frame**:
|
||||
|
||||
```
|
||||
|delta| 0 : 134 969 (cum 14.6 %) > 3 : 282 166 px = 30.6 %
|
||||
|delta| 1 : 355 261 (cum 53.2 %) bbox: the whole frame, but
|
||||
|delta| 2 : 103 348 (cum 64.4 %) occupancy is ~zero in columns
|
||||
|delta| 3 : 45 856 (cum 69.4 %) 0,1,6,7 of 8 and heavy in 2..5
|
||||
|delta| 40+: 16 844 (cum 100 %)
|
||||
```
|
||||
|
||||
The ≤1 tier is integer truncation against float rounding — the reference computes
|
||||
`(sc*sa + dc*(255-sa)) / 255` in `u32` and truncates; Godot rounds. The 30.6 %
|
||||
is something else, and it is in the columns where the menu's content sits.
|
||||
|
||||
## What it is
|
||||
|
||||
**The port draws some elements ADDITIVE. The reference has no additive path at
|
||||
all.**
|
||||
|
||||
`crates/sylpheed-formats/src/ui_layout.rs` has exactly two blend sites, lines
|
||||
1072 and 1174, and both are alpha-over:
|
||||
|
||||
```rust
|
||||
canvas[di + k] = ((sc * sa + dc * (255 - sa)) / 255) as u8;
|
||||
```
|
||||
|
||||
and line 1169 carries the reason — an *"ADDITIVE selector and REFUTED — it moved
|
||||
every metric against the …"*. The reference tried additive, refuted it against
|
||||
its own composite metrics, and does not do it.
|
||||
|
||||
`authored/rendering.json` gives the port an additive set per screen, and its
|
||||
`why` records that this is **transcribed, not authored**: the port proposed
|
||||
additive from a two-background composite solve, kept it a proposal because
|
||||
nothing on the disc selects a blend mode, and adopted it only when the Decoder
|
||||
logged **`RB_BLENDCONTROL0` per draw in Canary** and drove the game to both
|
||||
screens.
|
||||
|
||||
So the two renderers disagree **on purpose**: one implements a measurement of the
|
||||
game's blend register, the other implements a refutation made from composite
|
||||
metrics before that measurement existed.
|
||||
|
||||
## The prediction this makes, and it holds
|
||||
|
||||
If the divergence is the additive set, its size should scale with how many
|
||||
elements are in that set. It does:
|
||||
|
||||
| screen | additive elements in `authored/rendering.json` | mean diff |
|
||||
|---|---|---|
|
||||
| `extras` | **9** — `pteff10 pteff20 ptframe3 ptframe4 pteff21 pteff22 pteff23 ptloop01 ptloop02` | **6.7422** |
|
||||
| `main_menu` | **5** — `pteff12 ptframe1 ptframe2 ptloop01 ptloop02` | **3.9363** |
|
||||
| `main_menu_jp` | **0** — not in the map | 0.7885 |
|
||||
| `extras_jp` | **0** — not in the map | 0.6592 |
|
||||
| `title` | **0** — present, deliberately empty | 0.4431 (the known sweep residual) |
|
||||
|
||||
Nine beats five beats zero, in order, with the two zero-rows an order of
|
||||
magnitude below the two non-zero ones. That ordering was not fitted; the additive
|
||||
map was written before this comparison existed.
|
||||
|
||||
## And which side is right: the port, on the evidence there is
|
||||
|
||||
* the additive set is a **measurement off the running game** — the blend control
|
||||
register, per draw — and the reference's alpha-over is a refutation from
|
||||
*renderer metrics*, which the protocol ranks below a capture;
|
||||
* scored against `docs/re/captures/title-builds/live-main-menu.png`, Godot is
|
||||
**RMSE 3151.96** and the reference **3769.61** — the port is 16 % closer.
|
||||
|
||||
⚠️ **That second line is an ordering and nothing more.** `verify-screen` poses
|
||||
`--pose=rest` and its own header is emphatic that such a frame must never be
|
||||
scored against a capture — that mistake produced a published finding once
|
||||
already. Both sides carry the same pose contamination, so *which* is nearer is
|
||||
still meaningful; *how near* is not. Nobody should quote 0.0481 as the port's
|
||||
fidelity.
|
||||
|
||||
## The blend SPACE, separately: both are encoded, and so is the game
|
||||
|
||||
Worth keeping even though it turned out not to be the cause, because it closes
|
||||
`BLOCKED.md` H4 and it was measured rather than recalled.
|
||||
|
||||
**Godot, measured with a control** — a white quad over black at three known
|
||||
modulate alphas, through the port's own texture path
|
||||
(`load_png_from_buffer` → `ImageTexture`), with the port's `[rendering]` settings:
|
||||
|
||||
| declared alpha | encoded-space prediction | linear-then-re-encode prediction | **measured** |
|
||||
|---|---|---|---|
|
||||
| 64 | 64 | 138 | **64** |
|
||||
| 128 | 128 | 188 | **128** |
|
||||
| 192 | 192 | 225 | **192** |
|
||||
|
||||
Pre-registered before running; exact on all three, with the alternative excluded
|
||||
by 33–74 levels.
|
||||
|
||||
**The reference:** integer arithmetic straight on 8-bit values, no linearisation
|
||||
anywhere in the file.
|
||||
|
||||
**The game:** the Decoder's `blend-space-rt-format.txt` — `RB_COLOR_INFO`
|
||||
`color_format` is `k_8_8_8_8` on 2402/2402 splash draws and 33779/33791 of the
|
||||
boot-to-title capture, `k_8_8_8_8_GAMMA` appears **zero** times, and
|
||||
`color_exp_bias` is 0 throughout. `k_8_8_8_8_GAMMA` is the only format around
|
||||
which Canary applies a gamma↔linear conversion.
|
||||
|
||||
**All three agree. Blend space is not a difference between anybody here**, and
|
||||
the port needs no change for it.
|
||||
|
||||
## The other four rows — localised, and two hypotheses died getting there
|
||||
|
||||
`check-all` still fails on these four, and the allowance was not widened to
|
||||
cover them. What follows is where they are, not yet why.
|
||||
|
||||
### 🔴 Hypothesis 1, refuted: a blend-space divergence
|
||||
|
||||
Covered above. Killed by a control on my own renderer.
|
||||
|
||||
### 🔴 Hypothesis 2, refuted: the port renders `rotation_deg` and the reference does not
|
||||
|
||||
This one looked strong. `ui_layout.rs:86` says so in its own words —
|
||||
*"`rotation_deg` is decoded but NOT rendered"* — and the port does render it. A
|
||||
census over all sixteen exported screens gave:
|
||||
|
||||
> **Every screen with a non-zero rotation at rest DIFFERS, and every screen with
|
||||
> none agrees** — 6 of 6 either way, including both legacy rows.
|
||||
|
||||
**It is still wrong.** Widening the rule to *any* rotating element — a non-zero
|
||||
rest rotation **or** a two-keyframe 360° spinner at any depth — breaks it:
|
||||
`build_00` and `build_01` carry two spinners each and **agree** with the
|
||||
reference. Asked directly, the port's own draw log says it draws both spinners
|
||||
on those screens. So rotation is present, rendered, and produces no difference.
|
||||
|
||||
A rule that holds on 14 of 16 and fails on the two cases nobody had looked at is
|
||||
the shape of a rule fitted to the rows it was built from. Recorded because the
|
||||
next person will find the `ui_layout.rs:86` comment and reach for it too.
|
||||
|
||||
### ✅ What is actually established: `build_12` and `build_15` are ONE element
|
||||
|
||||
All **951** differing pixels lie inside `pgloading_loop5`
|
||||
(`pgloading_ring.png`, 333×276 at `[1,444]`), and the diff's own bounding box —
|
||||
`x 69..301, y 478..710` — sits entirely within it. No other element's rectangle
|
||||
contains a differing pixel that this one does not.
|
||||
|
||||
And that closes the `build_00` / `build_01` question without any rule about
|
||||
rotation: the port's draw log shows those two screens draw **7** elements and
|
||||
`build_12` draws **10**. `pgloading_loop5` is one of the three extra. The
|
||||
loading screens that agree are the ones that never draw the element the
|
||||
disagreement is in.
|
||||
|
||||
Why the two renderers disagree *on that element* is not settled. It is small —
|
||||
max 17, mean 0.0368 — and the element is unusual: its top-level keyframes hold
|
||||
`a=0x7f` for eight units around `rest.t = 24`, while its **leaf** record
|
||||
expands `pgloading_ring` from `scale 0` to `1000` over t=30…130. The port's
|
||||
`draw_leaf_for` lists only `ptloop01` and `ptloop02`, so the port draws the
|
||||
element and not that leaf. Whether the reference does the same is the open half.
|
||||
|
||||
### 🟡 `main_menu_jp` and `extras_jp` — consistent with the sweep leaves, not established
|
||||
|
||||
Both carry `ptloop01/pteff03` (rot 30, at x=1521) and `ptloop02/pteff03a`
|
||||
(rot −45, at x=−839) — the **same sweep leaves** whose phase residual is the
|
||||
already-named reason for `title`. Their means sit with `title`'s and nowhere
|
||||
near their own EN twins':
|
||||
|
||||
| | mean |
|
||||
|---|---|
|
||||
| `title` (sweep residual, named since P1) | 0.4431 |
|
||||
| `extras_jp` | 0.6592 |
|
||||
| `main_menu_jp` | 0.7885 |
|
||||
| `main_menu` (5 additive) | 3.9363 |
|
||||
| `extras` (9 additive) | 6.7422 |
|
||||
|
||||
That is consistent with the JP rows being the sweep residual alone, with no
|
||||
additive contribution — which is what the port's own map implies, since it lists
|
||||
no additive set for them. **Consistent with, not established:** nobody has
|
||||
isolated the sweep leaves on those two screens.
|
||||
|
||||
## ✅ H6 update — the asymmetry has a better answer than a measurement
|
||||
|
||||
The Decoder's reply: the blend is a **decoded disc field**, `T8aD +0x04` bit
|
||||
`0x02` (set ⇒ additive, clear ⇒ premultiplied alpha-over), with a disc-wide check
|
||||
and a surviving out-of-sample prediction. So the port can *derive* the blend per
|
||||
element on every screen instead of transcribing a table, and the JP question
|
||||
answers itself statically — no boot needed.
|
||||
|
||||
**Blocked on one thing:** `sylpheed-formats` does not expose `+0x04`.
|
||||
`ui_layout::Element` surfaces `kind` (`+40`), `parent` (`+32`), pivot, keyframes
|
||||
and `focus_link`, and nothing at `+0x04`; and `crates/sylpheed-export` consumes
|
||||
formats by git **tag**, not by workspace path. Asked as `BLOCKED.md` H6.
|
||||
|
||||
🔴 **And a negative worth having, because it is the obvious thing to try:
|
||||
`kind_raw` in this export is NOT that field.** Its bit `0x2` against the additive
|
||||
map over four screens is **anti-correlated** — 0 of 14 mapped elements have it
|
||||
set, and 9 unmapped ones do (`0x3002` on every button, `0x0` on every element the
|
||||
map lists). Anyone reaching for `kind_raw & 2` will get the additive set exactly
|
||||
inverted.
|
||||
|
||||
## 🔴 The asymmetry, until that lands
|
||||
|
||||
`main_menu_jp` contains **exactly the elements** `main_menu` marks additive —
|
||||
`ptloop01 ptloop02 ptframe1 ptframe2 pteff10 pteff12` — and `extras_jp` contains
|
||||
all nine of `extras`'. The port draws them **alpha-over** on the JP screens and
|
||||
**additive** on the EN ones, purely because `authored/rendering.json` is keyed by
|
||||
screen name and the Decoder's `RB_BLENDCONTROL0` log was taken on the EN screens.
|
||||
|
||||
**The port is therefore asserting, by omission, that the JP build blends the same
|
||||
elements differently — and that is the less likely of the two possibilities.**
|
||||
Extending the map would extrapolate a measurement onto a build nobody drove to,
|
||||
which is not the port's to do; leaving it silent asserts the surprising thing by
|
||||
default. So it is made explicit in `authored/rendering.json` and asked in
|
||||
`BLOCKED.md`, and the map is **not** extended.
|
||||
|
||||
⚠️ It does not affect the deliverable: MISSION §7 puts localisation beyond
|
||||
English out of scope, and the JP screens are not in the boot path.
|
||||
|
||||
## Why `check-all` stays red
|
||||
|
||||
Four rows are **not** explained by the additive set:
|
||||
|
||||
| screen | mean | over3 | note |
|
||||
|---|---|---|---|
|
||||
| `main_menu_jp` | 0.7885 | 3 248 | not in the additive map, yet differs |
|
||||
| `extras_jp` | 0.6592 | 3 163 | same |
|
||||
| `build_12` | 0.0368 | 462 | tiny, and localised — diff bbox `464x266+60+454` |
|
||||
| `build_15` | 0.0368 | 462 | identical figures to `build_12` |
|
||||
|
||||
`build_12` and `build_15` producing byte-identical statistics suggests one shared
|
||||
element rather than two coincidences. None of the four is diagnosed and none is
|
||||
excused.
|
||||
|
||||
## What changed in `check-all`
|
||||
|
||||
The allowance is now **derived** rather than listed: a screen may differ if it
|
||||
has a non-empty additive set in `authored/rendering.json`, plus the two named
|
||||
legacy rows. That is strictly stronger than the hard-coded list — a screen that
|
||||
differs *without* additive elements now fails, which a literal list could not
|
||||
express, and the allowance cannot go stale against the map it is computed from.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the reference is wrong to have refuted additive from its own metrics. It
|
||||
is superseded by a capture, which is a different thing.
|
||||
* That the port's additive set is complete. It covers three screens; nobody has
|
||||
logged the register on the rest.
|
||||
* Anything about the four undiagnosed rows.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 🔴 Re-validated on the GPU, and my prediction failed: these numbers are rasteriser-specific
|
||||
|
||||
The human activated a hardware GPU on 2026-09-01. **Every number on this page
|
||||
above was measured under `llvmpipe`**, so they needed re-deriving before anyone
|
||||
builds on them.
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
> Both renderers blend in encoded 8-bit space, so if the port's drawing is
|
||||
> rasteriser-independent the diffs should be **identical, or within 1 level**.
|
||||
> Anything materially different means a published conclusion here was
|
||||
> GPU-specific.
|
||||
|
||||
## It failed
|
||||
|
||||
| screen | llvmpipe mean | **GPU mean** | change | max (llvmpipe → GPU) |
|
||||
|---|---|---|---|---|
|
||||
| `title` | 0.4431 | **0.5936** | **+34 %** | 41 → 41 |
|
||||
| `main_menu` | 3.9363 | **4.1449** | +5.3 % | 97 → 97 |
|
||||
| `extras` | 6.7422 | **6.9757** | +3.5 % | 113 → 113 |
|
||||
| `title_jp` | 2.7715 | **2.9448** | +6.3 % | 233 → 233 |
|
||||
| `main_menu_jp` | 0.7885 | **1.0157** | **+29 %** | 26 → 27 |
|
||||
| `extras_jp` | 0.6592 | **0.8906** | **+35 %** | 26 → 26 |
|
||||
| `build_12` / `build_15` | 0.0368 | **0.0454** | +23 % | 17 → 17 |
|
||||
|
||||
**Every mean rose, by 3–35 %.** So the diffs are *not* rasteriser-independent
|
||||
and the prediction was wrong.
|
||||
|
||||
## What survives, and what does not
|
||||
|
||||
🔴 **The maxima are unchanged** — 41, 97, 113, 233, 17 identical, and 26 → 27 on
|
||||
one row. The large differences are exactly where they were.
|
||||
|
||||
That is the shape of a **rounding population growing, not content moving**:
|
||||
llvmpipe and the NVIDIA rasteriser round the last bit of a blend differently, so
|
||||
the ≤1-level tier grows while the elements that genuinely differ do not move at
|
||||
all. Consistent with both still blending in encoded space — which the control on
|
||||
this page established for Godot generally, not for one rasteriser.
|
||||
|
||||
**What survives:**
|
||||
|
||||
* the **additive diagnosis**, because it rests on an *ordering*, and the ordering
|
||||
holds on the GPU: `extras` 6.98 > `main_menu` 4.14 > `extras_jp` 0.89 and
|
||||
`main_menu_jp` 1.02 > `title` 0.59 — nine additive elements, then five, then
|
||||
none;
|
||||
* the **`pgloading_loop5` localisation**, which is a bounding box;
|
||||
* the **`build_00`/`build_01` agree** result — still 0 pixels over the bar;
|
||||
* the **derived allowance**: the failing set is the same four rows.
|
||||
|
||||
**What does not, and is now labelled:**
|
||||
|
||||
* the histogram (*53 % within 1 level, 16 844 over 40*) was llvmpipe-specific and
|
||||
the ≤1 tier is larger on the GPU;
|
||||
* every absolute **mean** quoted above this section;
|
||||
* the **RMSE-vs-capture** pair (3151.96 / 3769.61) was llvmpipe. The *ordering*
|
||||
claim — the port is nearer than the reference — has not been re-derived on the
|
||||
GPU and is not claimed here until it is.
|
||||
|
||||
## The rule this earns
|
||||
|
||||
**A renderer comparison carries its rasteriser as a hidden parameter.** Nothing
|
||||
in this corpus recorded which one produced a diff, and for eight months there was
|
||||
only one so it never mattered. Any diff quoted from here on should say what drew
|
||||
it — the same discipline `TEMPORAL-VERIFICATION.md` already demands for capture
|
||||
rate, applied to the thing that rasterises rather than the thing that clocks.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# ✅ Closed: the reference can draw additive now, and the divergence collapses 6×
|
||||
|
||||
The Decoder taught `ui_layout::blit` the additive blend at
|
||||
`formats-pin-2026-09-01b`, on the argument this page made — that the comparison
|
||||
was **structurally incapable**, and that the refutation which had kept additive
|
||||
out was `⟨render-vs-capture⟩`, i.e. that renderer disagreeing with itself while
|
||||
it had a stale keyframe association, no leaf geometry and no rotation.
|
||||
|
||||
**Measured without pulling their branch into mine**: a detached worktree at the
|
||||
tag, `sylpheed-cli` built there, and `verify-screen` pointed at it through
|
||||
`SYLPHEED_CLI`. My branch is untouched and the reference on `main` is unchanged.
|
||||
|
||||
## Pre-registered (R2)
|
||||
|
||||
> With the reference finally able to draw additive, the diffs caused by that gap
|
||||
> should collapse. `main_menu`/`extras` and their JP twins should fall sharply
|
||||
> from ~7, and whatever remains is a *different* cause.
|
||||
|
||||
| screen | alpha-over reference | **additive reference** | factor |
|
||||
|---|---|---|---|
|
||||
| `main_menu` | 7.2580, max 105 | **1.2068, max 28** | **6.0×** |
|
||||
| `main_menu_jp` | 7.3440, max 108 | **1.2111, max 31** | **6.1×** |
|
||||
| `extras` | 6.9757, max 113 | **1.0229, max 28** | **6.8×** |
|
||||
| `extras_jp` | 7.0734, max 115 | **1.0255, max 30** | **6.9×** |
|
||||
| `title` | 1.0335, max 88 | **0.5685, max 41** | 1.8× |
|
||||
| `title_jp` | 4.4944, max 233 | **2.8225, max 233** | 1.6× |
|
||||
| `build_12` / `build_15` | 0.0772, max 60 | **0.0463, max 17** | 1.7× |
|
||||
| `build_00` / `build_01` | 0.0676, max 60 | **0.0366, max 4** | **DIFFERS → OK** |
|
||||
|
||||
**`build_00` and `build_01` stop differing entirely** — `over3` 3 422 → **0**.
|
||||
|
||||
## And the twins agree to a third of a percent
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `main_menu` 1.2068 vs `main_menu_jp` 1.2111 | **0.36 % apart** |
|
||||
| `extras` 1.0229 vs `extras_jp` 1.0255 | **0.25 % apart** |
|
||||
|
||||
Under the name-keyed map these pairs were 4.1× and 7.8× apart. Nothing was fitted
|
||||
to make that happen — the locale twins converged first when the *port* took the
|
||||
decoded field, and again now that the *reference* draws the same blend.
|
||||
|
||||
## The residual is smaller and its causes are the documented ones
|
||||
|
||||
* `title_jp` 2.82 at max 233 remains the largest, and its named reason —
|
||||
`--pose=rest` sparkle handling — is untouched by any of this.
|
||||
* `title` 0.57 at max 41 is the `ptloop` sweep-phase residual, also untouched.
|
||||
* `main_menu`/`extras` and twins now sit at ~1.0–1.2, max ~28–31. **New, small,
|
||||
and undiagnosed** — additive was the dominant cause and not the only one.
|
||||
* `build_12`/`build_15` do **not** return to their pre-change 0.0368: they land at
|
||||
0.0463 with both renderers drawing `pgloading_loop5` additive. So that element
|
||||
carries a small residual of its own beyond the blend.
|
||||
|
||||
## 🔴 What must happen to `check-all`, and why it has NOT happened yet
|
||||
|
||||
The allowance widened when the reference could not draw additive. **That
|
||||
justification is gone**, so the allowance should be narrowed back and the check
|
||||
should regain its teeth.
|
||||
|
||||
**It is not narrowed in this commit, deliberately.** `check-all` builds the
|
||||
reference from the **workspace** `crates/sylpheed-formats`, and the additive path
|
||||
is at a tag that has not landed on `main`. Tightening now would turn `check-all`
|
||||
red against a reference that still cannot draw additive — a wall of failures
|
||||
meaning one thing, which is the exact defect the display guard was added for.
|
||||
|
||||
**The trigger is mechanical**: when the additive path is on `main`, drop the
|
||||
export-derived clause and leave the two named legacy rows. The set that should
|
||||
then differ is measured above and is `title`, `title_jp`, `main_menu`, `extras`,
|
||||
`main_menu_jp`, `extras_jp`, `build_12`, `build_15` — with `build_00`/`build_01`
|
||||
expected to pass.
|
||||
Reference in New Issue
Block a user