Merge remote-tracking branch 'origin/main' into auto/frame-blend-draw-path
# Conflicts: # crates/sylpheed-cli/src/main.rs
This commit is contained in:
@@ -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