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.
13 KiB
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 §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_screenand 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_rects 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_fpsor 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.