Files
Sylpheed/docs/port/splash-rate-contradiction.md
MechaCat02 a23c321831 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00

9.0 KiB
Raw Blame History

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.51.7× too fast", and a recommendation to use ~3540 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.373.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 3539, 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.373.51 and 4.304.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' ~5861. 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.