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.
357 lines
16 KiB
Markdown
357 lines
16 KiB
Markdown
# 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.
|