Takes the port branch up to77320d5e-- the state the human play-tested on 2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio` is 366 commits and 938 files, and most of that must not land. WHAT COMES IN (76 files, all human-confirmed working): * the logo splash animation.08ed3dd1found it: `pose_at` ASSIGNED the settle instant instead of clamping to it, so the splash never animated at all -- and the same bug manufactured a passing harness result, because the harness photographed t past the settle. Confirmed by play-test: "cannot notice any obvious difference from the actual game." * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad binding), stick latched with hysteresis at the game's own 61% digitise threshold. This is what made (A), video-skip and Extras work at all. * menu navigation and flow, menu audio, the exporter, the authored declarations, and 23 verification tools under tools/port/. WHAT IS DELIBERATELY LEFT ON THE BRANCH: * everything afterc0ae460a-- the F5/F6 title-timing investigation, whose own tip commit calls itself a "hand-off for one-minute human checks". Unchecked by definition; it goes through the new review gate like anything else. * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested. * the F1 repeat mechanism, which its own commit calls "deliberately inert". WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED: 545 MB of extracted game content was committed on that branch -- 850 sprite, audio and transcoded video files under `export-probe/` and `export-probe2/`, plus 246 MB of loose .wav and .tsv at the repo root. This repository's own rule, in this file, is "never game content". The rule was not missing. It was written, and it was tightened on that very branch, with a careful comment explaining why BOTH `export/` and `data/base/` had to be listed -- while the exporter was writing to a third name that nobody had thought to list. Enumerating names is the thing that failed. So the ignore rules now describe the SHAPE: any top-level `export*/`, game media by extension, and loose capture output at the root. Verified both ways -- it catches all four offenders and ignores nothing currently tracked. Verified: `cargo check --workspace` clean; all nine GDScript files parse in project context, with a positive control (an injected syntax error is detected, 3 lines) so the clean result means something. `tools/port/check-all` was NOT run -- it needs the container, the export tree and a display.
16 KiB
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
- Three positions in a day, two of them already withdrawn by their author.
- Reach is one boot. They say so.
- 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.
- 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:
# 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.