port: the port never reported its own frame rate, and the menu draws at 9.7 fps

TEMPORAL-VERIFICATION §1 requires every instrument to state its achieved rate
against its requested one. That 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 now because the splashes are the focus and the open complaint 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 here could tell them
apart.

boot.gd now counts frames per screen and reports at every boot transition, at the
end of the boot, and at every menu arrival. `worst gap` sits beside the mean
because a hitch is what reads as wrong and a mean hides one by construction.

Measured in this container, three boots:

  publisher_logo   17.3 / 19.6 / 25.0 fps    worst gap 100-115 ms
  developer_logos  16.7 / 21.9 / 22.8 fps    worst gap 103-138 ms
  title            17.2 / 14.2 / 12.7 fps    worst gap 150 ms, all three
  main_menu         9.7 fps                  worst gap 150 ms

🟡 A LIVE CANDIDATE FOR PLAY-TEST FINDING 4, and the first one that is not dead.
The timeline is delta-driven so durations stay correct at any rate; what changes
is how many alphas the fade is DRAWN at. At the measured rates the 45-unit
build-in gets 12-17 distinct alphas instead of 45, and the pre-blurred companion
glow -- the thing that IS the splash's blur -- rises over 15 units and is drawn
at FOUR TO SIX steps instead of fifteen.

⚠️ It is a candidate, not a cause: this is llvmpipe under Xvfb and not the
human's hardware. The point is that the line now prints on every boot, so the
next play-test answers it for free. Every other candidate for finding 4 is
already dead -- keyframes vindicated against the vertex stream, companion quads
drawn, blend space matching, settled pose at 0.01 % against the capture, no
post-process pass to add.

 And nothing published is invalidated, which was worth checking rather than
assuming: every timing result here comes from `_elapsed` (+= delta) or
`time_units` (the same sum scaled), so all are correct at any frame rate. The
splash dwells were measured across runs whose rates differed by 2x and agreed to
±0.03 s. 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 rate and withdrew a finding over.

Two defects in the instrument itself, both caught and fixed before it was
trusted:
  - 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 underflows to INT64_MIN. It
    now names the cap or says `uncapped`.
  - it was BOOT-ONLY and said so nowhere -- `--menu` arrives through
    _menu_arrive, not _advance, so the mode a human spends time in reported
    nothing. That is the shape this port keeps finding in other people's work,
    and it lasted one measurement here.

Refutation attempt: I checked whether the port re-decodes PNGs per frame, which
would have been a real defect. It does not -- _load_textures caches at
load_screen. Hypothesis dead, cheaply, and recorded.

Not settled: what rate the human's machine manages; whether 9.7 fps on the menu
is llvmpipe or something in our draw path; H6's +0x04 exposure; H1 (the Decoder
is taking it this iteration).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AHUQvXGyNcKonSEWsgWcX
This commit is contained in:
Sylpheed port agent
2026-09-01 18:15:38 +00:00
parent 977965e92d
commit 170d255e82
3 changed files with 232 additions and 0 deletions

View File

@@ -456,11 +456,65 @@ var _elapsed := 0.0
var _boot_done := false
## Frames rendered on the current screen, and the worst gap between two of them.
##
## 🔴 THIS PORT REQUIRED EVERY MEASUREMENT TO REPORT ITS ACHIEVED RATE AND NEVER
## REPORTED ITS OWN. `TEMPORAL-VERIFICATION.md` §1 -- a capture that asked for one
## rate and delivered another "is not a slow capture, it is a different capture"
## -- was applied to `--film`, to the Decoder's harnesses and to the oracle, and
## not once to the thing actually being shipped.
##
## It matters here specifically. The splashes are the current focus, the
## developer splash's whole build-in is 45 units = 0.75 s, and the human's
## complaint about it is that ours is *less pronounced* than the game's. A fade
## drawn in 45 frames and the same fade drawn in 11 are different animations, and
## nothing in this port could have told the difference. Every timing check so far
## has measured `_elapsed`, which is the sum of the deltas -- it is exactly as
## correct at 8 fps as at 60 and says nothing about what a person sees.
##
## `worst` is kept beside the mean because 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.
var _screen_frames := 0
var _screen_t0 := 0.0
var _worst_gap := 0.0
## What the port managed on the screen just finishing, in the form this project
## demands of everyone else: achieved against requested, plus the worst hitch.
func _rate_report(name: String) -> String:
var span := _elapsed - _screen_t0
if span <= 0.0 or _screen_frames == 0:
return ""
# 🔴 THE "REQUESTED" HALF PRINTED -9223372036854775808 ON ITS FIRST RUN.
# `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, so it now names the cap or says plainly that there is none.
var cap := "uncapped"
if Engine.max_fps > 0:
cap = "%d requested" % Engine.max_fps
else:
var hz := DisplayServer.screen_get_refresh_rate()
if hz > 0.0:
cap = "%.0f Hz display" % hz
return " %s: %d frames in %.2f s -- %.1f fps achieved, %s, worst gap %.0f ms" % [
name, _screen_frames, span, _screen_frames / span, cap, _worst_gap * 1000.0]
func _rate_reset() -> void:
_screen_frames = 0
_screen_t0 = _elapsed
_worst_gap = 0.0
func _process(delta: float) -> void:
if _frozen or view == null:
return
view.time_units += delta * view.units_per_second
_elapsed += delta
_screen_frames += 1
_worst_gap = maxf(_worst_gap, delta)
view.queue_redraw()
_overlay_process(delta)
@@ -554,6 +608,12 @@ func _process(delta: float) -> void:
elif not _boot_done:
_boot_done = true
print("boot sequence complete after %.2f s, holding on %s" % [_elapsed, _sequence[_step].get("screen", _sequence[_step])])
# The LAST screen never reaches `_advance`, so without this the one
# screen the boot ends on -- the title, where the plate lands -- was
# the only one with no rate reported.
var last := _rate_report(String(view.screen.get("name", "?")))
if last != "":
print(last)
# The plate is timed from HERE -- the moment the screen reaches its
# own hold -- and not from the frame it first appeared. That is the
# finding, not a detail: measured from first-draw the two oracle
@@ -588,6 +648,13 @@ func _process(delta: float) -> void:
func _advance() -> void:
# Before the step changes, say what the screen that is leaving managed.
if view != null:
var leaving := String(view.screen.get("name", "?"))
var line := _rate_report(leaving)
if line != "":
print(line)
_rate_reset()
_drop_overlay()
_step += 1
var next: Dictionary = _sequence[_step]
@@ -958,6 +1025,17 @@ func _menu_enter(name: String, fresh: bool) -> void:
## The moment a screen has finished fading out and the next one takes over.
func _menu_arrive() -> void:
# 🔴 THE RATE REPORT WAS BOOT-ONLY ON ITS FIRST VERSION, AND SAID SO NOWHERE.
# `_advance` walks the boot; the MENU arrives through here, so `--menu` --
# 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 claimed "every
# boot" is the shape this port keeps finding in other people's work.
if view != null:
var line := _rate_report(String(view.screen.get("name", "?")))
if line != "":
print(line)
_rate_reset()
# The plate goes with the screen it was measured on. See `_drop_overlay`.
_drop_overlay()
var action: Dictionary = _pending