Files
Sylpheed/docs/port/plate-arrival-halves.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

25 KiB
Raw Permalink Blame History

H3 — the PRESS Ⓐ plate: which half the lateness is in

Status: both halves answered, and TWO of my own conclusions on this page were refuted within the hour — the 5 units/frame reading and the claim that clock: "shared" collapses. Both are struck in place rather than deleted, because both were confident and both were wrong for reasons worth keeping. The answers are the Decoder's h3-units-per-frame-measured.md, origin/auto/frame-blend-draw-path at a482d9a. Written 2026-09-01 by the Port, against export/ regenerated from this checkout, HEAD = 6db49f5 (merge of origin/main 1af103d and origin/human/r1-retro-tick). HANDOFF on this branch answers 9ca1eb5.

The play-test finding 3 says the plate arrives late and names three candidate causes: the unit→seconds constant (the Decoder's), the clock origin and rest.t (both ours). The brief asks which half it is, and how that was established. This page is the answer.

Short version, after the answer landed. rest.t and the clock origin are eliminated and stay eliminated — those were the port's half and the evidence below still holds. Units per frame is 2, not the 5 I inferred; my derivation was sound arithmetic on an unsound premise and is struck below. The anchor is t=160, as I suspected. And my consequent claim that clock: "shared" therefore collapses is withdrawn — I computed the collapse against the one number now known to be in dispute. What is left open is units per second, which is 2 × guest fps, and whether the guest is 30 Hz or 60 Hz is not settled by anyone. At 60 Hz the plate lands at 1.97 s instead of 3.93 s, which is the size of what the human reported.

What the export actually declares

export/screens/title/press_start.json, build 2, one element ptbtn00:

t alpha note
0 0x00 invisible, y = 560
214 0x00 still invisible, y = 550 — it has drifted up unseen
236 0xff full
238 0xff last opaque frame
244 0x00 gone again

rest.t = 236. settle_window = [214, 236, 225].

🔴 The corpus says the plate reaches a=255 at t=238. It reaches it at t=236. 238 is the last frame at full alpha, not the arrival. Every derivation quoting 238 118 = 120 units = 2.000 s is really 236 118 = 118 units = 1.967 s. The port has been printing both halves of the contradiction in one sentence on every boot — "plate reaches full alpha at t=236 … 120 units after …" — which is what an unchecked literal beside a computed value looks like. Corrected in boot.gd and in authored/flow.json. It moves the reconciliation by 0.033 s and overturns nothing.

Candidate 1 — rest.t. Eliminated.

rest.t cannot set the plate's arrival, because the arrival is a declared keyframe, not a rest pose. ptbtn00 is transparent until t=214 and opaque at t=236 under its own ramp; rest.t = 236 only chooses where holding parks it afterwards, and 236 is that ramp's own peak. Setting rest.t to any other value moves where the plate stops, never when it starts.

Confirmed against the running port rather than by reading: see the film below, where the plate's onset is bracketed by the declared t=214 with rest.t untouched.

⚠️ This does not rehabilitate rest.t generally. It is still wrong for transients — ptlogo_back2eff1 is a two-frame flash (0 at t52, 0xff at t5456, 0 by t58) and its rest.t = 54 is the flash peak, so rest() leaves five of these burning at once. It is simply not in this defect's causal path.

Candidate 2 — the clock origin. Eliminated, and measured.

Two things had to be true and both are:

(a) The port's two builds share one clock exactly. _advance sets view.time_units = 0.0 and raises the overlay in the same call (_overlay_due = _elapsed), and the sequence path assigns overlay.time_units = view.time_units outright rather than integrating a second delta. Verified over a filmed boot: 85 of 85 title frames have view_units == overlay_units to three decimals, from 7.812 to 679.182. There is no drift and no offset to find.

(b) The title step begins when the previous step ends. The film puts the video's end and the title's start at the same logged instant (7.86 s / 7.92 s across two runs), with no black hold between them.

Candidate 3 — the unit→seconds constant. NOT eliminated. It is the live one.

🔴 This section said "eliminated by sign" and that was wrong. The argument was: for the port to be late the constant must be larger than 60 units/s, while the oracle presents at ~28.1 fps and the corpus measured the idle title at 28.5 fps — both slower than nominal, so the game's units run slower, not faster.

That conflates two different quantities. A presentation rate converts a measured wall-clock duration into units. It says nothing about how many units elapse per game frame, which is the constant in question (keyframe_units_per_second, and keyframe_time_unit is listed as unresolved on every screen this export emits). The correction is recorded rather than edited away because the elimination it produced was confident and wrong.

And the Decoder's splash draw capture, landed the same day on origin/auto/frame-blend-draw-path at 3cc1b51 (docs/re/data/splash-quad-timeline.txt), gives an independent handle on that constant — from a screen with no bearing on the plate:

interval, publisher splash guest, observed this export, declared units/frame
companion (Q7) onset → sharp logo (Q0) onset f4 → f7, 3 frames t=0 → t=15, 15 units 5.0
companion onset → its own peak f4 → f7 (a=240), 3 frames t=0 → t=15 (a=255), 15 units 5.0
sharp logo onset → full alpha f7 → f10, 3 frames t=15 → t=30, 15 units 5.0

Three intervals, three ways, 5 units per guest frame — against the declared 2.

🔴 STRUCK. It is 2 units per frame, and my 5 was an artefact of two things

Measured by the Decoder on ptbtn00 itself — the plate, on the title, not a splash — in h3-units-per-frame-measured.md (a482d9a), against a pre-registration committed before the capture was read:

label   5372 5373 5374 5375 [5376] 5377 5378 [5379] 5380
alpha     46   69   92  115    —    197  220    —    255
step          +23  +23  +23

255 × 2 / 22 = 23.18, and the plate's declared ramp is t=214 → 236, i.e. T = 22. Three consecutive gap-free steps of exactly 23 is 2.0 units per frame on the nose. Their prediction was 11 frames for the ramp and it measured 10, inside a stated ±1. My 4.4-frame prediction is excluded by more than 2×.

Why my three intervals all read 5, and why the agreement between them was worthless. Two independent errors, both mine to have made:

  1. An alpha step is not a clock rate. For a linear segment, Δα per frame = 255 × (units per frame) / T. Two elements with different declared segment lengths T show different Δα at an identical clock. Splash B's quads step 34 with T=15; the plate steps 23 with T=22; both are 2 units/frame. Reading a step as a rate is what produced the 2.7×.
  2. My "onsets" were not onsets. I took each quad's first submission as its α = 0 time. On splash A, Q7 and Q0 are both already at α = 85 when first submitted. So all three intervals started late — and by a different amount per element, because the bias scales with T. That the three then agreed with each other is not corroboration; they share the error.

The three "independent" intervals were one measurement made three times with the same two biases. That is the shape I should have checked for and did not.

⚠️ The Decoder records the published splash-quad-timeline.txt having no T column as their own defect, now fixed. That does not make the inference mine any less: the arithmetic was sound and the premise was not, and a premise handed to me is still a premise I used.

⚠️ Two caveats, and they are why this is asked rather than acted on. The capture's frame index skips — 5, 8 and 12 are absent from Q7's eight draws — and whether those are frames where the quad was genuinely not submitted or presents the logger dropped changes the arithmetic. And the present rate during a splash is not established.

🔴 And it collides with the oracle's own plate measurement

The two captures cannot both be read at face value:

capture implies
title-plate-delay-measured.md — 118 units in 2.135 s ~55 units/s
splash-quad-timeline.txt — 15 units in 3 frames ~150 units/s at 30 fps

A factor of 2.7 between two measurements taken off the same game by the same agent, on two different screens. Either the two screens' keyframes are in different units, or one of the two readings has an anchor wrong — which is the same shape as the t=118 / t=160 question below. Both are oracle measurements and neither is the port's to resolve. Asked in BLOCKED.md H3.

The film — and the instrument had to be fixed first

--film scheduled frame n for n × interval and reported nothing. One 1280×720 save_png under llvmpipe costs ~0.24 s, so a request for 0.05 s delivered 247 frames in 60 s where 1 200 were asked for — an achieved 4.1 fps against a requested 20 — and the deficit accumulated silently into the frame index. f_071.png still looked exactly like the frame that was meant to be 3.55 s in.

TEMPORAL-VERIFICATION.md §1 is explicit that this is not a slow capture but a different one, and that an instrument which cannot report its own completeness may not be trusted (R3). So --film now writes <film>_frames.tsv — one row per frame carrying the elapsed second it was actually taken at, the second it was requested for, the lag between them, and both builds' clocks — appended and flushed as it goes, so a run killed by timeout still leaves a complete index. It prints the achieved rate against the requested one every 40 frames. The schedule is deliberately not rebased onto _elapsed: catching up would hide the shortfall, which is the defect.

The run this page rests on: requested 20 fps, achieved 15.9 fps for the first 120 frames and 11.3 fps by frame 200, lag reaching 8.7 s. Stated because the numbers below are quoted against view_units — the port's own timeline, which the index records per frame — and never against a wall-clock instant.

Pre-registered, before looking (R2)

Filming the boot, measured from the frame the title step begins, the plate region stays at its background floor until t=214, rises over 22 units, and plateaus at t=236. Accept ±2 frames at each end.

What it did

Region = the plate's own quad, x 127640, y 525575 (pos [383,550], pivot [256,25], sprite 513×50). Mean of the crop, one row per filmed frame:

view_units region mean
7.8 → 143.8 0.13765, flat to 5 decimals nothing there
151.9 → 207.8 0.13773 → 0.14341 a slow 4 % drift, build 4's own
207.8 → 215.3 0.14341 → 0.15348 ⬅ the step; brackets the declared 214
215.3 → 239.8 → 0.21254 the ramp
239.8 → 279.3 → 0.24766 plate full at 236; the rest is ptbtn00f

The onset is bracketed between 207.8 and 215.3 at a frame spacing of ~7.5 units, i.e. within one frame of the declared 214, and rest.t was never touched. Prediction held.

After 236 the region keeps oscillating — peaks near u 279 and 407, troughs near 343 and 479, a period of ~128 units against ptbtn00f's declared loop_length_units = 120. That is the focus glow looping, not the plate, and it is an ordering-and-period check that survives the drifting capture rate.

So where the port lands against the oracle

docs/re/title-plate-delay-measured.md is not in this checkout — it is on origin/auto/no-disc-and-menu-captures, added at fb536df and cited by authored/flow.json at 5b0a6e6. Read there:

run 1 run 2 the port
settled → plate 2.138 s 2.132 s 1.967 s (118 units at 60/s)
first drawn → plate 3.781 s 4.263 s 3.933 s (236 units)

On the interval the oracle measured deliberately, the port is 0.17 s early. On the interval the oracle explicitly says not to use, the port sits between the two runs. No number anyone has taken makes this port's plate late.

The human watched both and says late, and that observation stands. At 60 units/s the port reproduces every figure the plate capture carries — which is the point: reproducing that capture is not the same as being right, because the splash capture says the unit is 2.5× off and would put the port 2.4 s late. The two cannot both hold. The human's eye agrees with the splash capture.

🔴 WITHDRAWN: my claim that the t=160 anchor collapses clock: "shared"

The section below asked which anchor the glyph counter corresponds to and said, of the t=160 answer, "the shared-clock premise does not reproduce the measurement at all and clock: "shared" is open". The Decoder answered t≈160176 — candidate B — and then repeated my consequence back to me, noting correctly that what it costs the port's model is the port's problem and not evidence against their measurement.

They were right to report it and I was wrong to have claimed it. I computed the collapse by comparing the declared 76 units against 2.135 s, and 2.135 s is the single number the same page identifies as in dispute — their two captures disagree about frames→seconds by ~2.9×. Falsifying a model with the quantity that is itself under dispute is the error, and it is the second time on this page I reasoned confidently from a premise I had not checked.

Against the new capture, in its own labels rather than in seconds, shared-clock holds:

declared, shared clock capture at the plate ramp's own measured 2.2 units/label
ptcopyright full (t=160) → plate α=0 (t=214) = 54 units 5350 → 5370, 20 labels 44 units
ptcopyright full (t=160) → plate α=255 (t=236) = 76 units 5350 → 5380, 30 labels 66 units
the plate's own ramp (t=214 → 236) = 22 units 5370 → 5380, 10 labels — (this is the calibration)

Both intervals come out short of declared by 1319 %, in the same direction, against a claimed collapse of ~1.7×. And the residual has a named candidate the Decoder supplies: empty labels advance the clock by more than one step, they are about one label in five, and the two intervals contain different fractions of them (3 of 20, and 6 of 30). Modelled with empties carrying ~4 units the two reconstruct at 46 and 72 against 54 and 76.

So clock: "shared" is not falsified and the port keeps it. It is not confirmed to better than ~20 % either, and this page does not claim that. What it is no longer is "collapsed on the port's own account".

A second anchor question, also the Decoder's — ANSWERED: it is t=160

What does the oracle's "title settled" correspond to on the declared timeline? It is defined operationally — "glyph counter first reads its no-plate value 154" — and our export offers two anchors, 42 units apart:

anchor what it is settled → plate vs measured 2.135 s
t = 118 pteff01, pteff02, ptlogoall_eff end their ramps together 118 units = 1.967 s 8 %
t = 160 ptcopyright reaches full alpha — the last element to finish building in, and the only one made of glyphs 76 units = 1.267 s 41 %

The port's reconciliation picked 118. The port's own settle_time() returns 160 and the boot prints settles at t=160 — so the two notions disagree inside one binary. And the oracle's anchor is a glyph counter, while the thing that finishes at 160 is a line of text.

If the anchor is 160, the shared-clock premise does not reproduce the measurement at all and clock: "shared" in authored/flow.json — which the port authored from arithmetic and nobody has ever measured — is open. If it is 118, the reconciliation stands and H3 is not a timing defect.

Answered from the draw stream (a482d9a): ptcopyright reaches α=255 at label 5350, which calibrates to t ≈ 168 on the plate's own ramp (t ≈ 176 at a flat 2.0/label). Candidate B is 816 units away; candidate A is 5058. It is B.

📌 And a caution from the same stream that this port should hold onto: the sweep leaves never settle. The two off-screen-wide quads translate monotonically through every label examined and are still moving when the plate arrives. "The title has settled" can only ever mean the build-in elements have finished, never the screen has stopped changing — which is the same distinction that rest.t keeps getting wrong.

🔴 ALL FOUR NAMED CAUSES ARE NOW DEAD, AND THE HUMAN'S OBSERVATION IS NOT

The rate is measured: 56.8 units per guest second (units-per-second-measured.md, control passing at 1.15 %, two elements agreeing at one clock — ptbtn00 at 657.9 α/s and ptcopyright at 650.4 α/s, which puts ptcopyright's segment at T = 22.25, a round declared length nobody fitted). 30 and 120 are both excluded.

At 56.8 units/s the plate's t = 236 lands at 4.15 s after clock zero, against the port's 3.93 s. The port is fractionally early. So:

candidate verdict
rest.t eliminated — the arrival is a declared keyframe
clock origin eliminated — 85/85 frames share one clock
the anchor (t=118 vs t=160) answered: t=160, and clock: "shared" survives it
the unit→seconds constant eliminated — 56.8 measured; the port is early, not late

Nothing named in the play-test explains what the human saw, and this page says so rather than quietly closing H3 green. The observation stands and is now unattributed. Two things it could still be, neither of them the plate's own timing and neither established here:

  • what "late" was measured against. Every number above is relative to the title's clock zero. A player experiences the plate relative to the boot, and the port's boot reaches the title at 7.86 s only because --skip-at presses Ⓐ. 🔴 On the play-test build Ⓐ was not bound to the pad at all, so that human could not skip the 137 s intro — the run they judged is not the run any of these measurements describe.
  • the splash dwells. The rate's reach is the title. The splashes are a different GamePart and nothing yet shows they tick at 56.8; the Decoder has reading their T off the disc as a next item.

The route that is dead regardless

🔴 units = 2 × frames must not be used anywhere. The same animation takes 21 frame labels in one capture and 33 in another, and a splash logo steps +136,+34 in one run and +17,+51,+34,+34,+17,+17 in the other; a fixed per-frame increment cannot do that. The 2 was one run's frame pacing.

Audited, and the port never did this. boot.gd advances time_units += delta * units_per_second off delta time. The retirement cost this port a justification in authored/timing.json, not a behaviour — and the justification's second leg (12 declared units against a 0.140.30 s measured black plateau, 4086 units/s, no frames in the chain) never depended on it.

What was still open, before the rate landed

units/second = units/frame × guest fps. The first factor is now 2. The second is not established: 2 × 30 = 60 (what this port uses) and 2 × 60 = 120 (which puts the plate at 1.97 s instead of 3.93 s — the size of what the human reported). The Decoder's capture ran at 27.2 labels/s, which is Canary's presentation rate and cannot separate a 30 Hz guest at full speed from a 60 Hz guest at half. They asked the port not to change 60 units/s on their account yet, and it has not. Asked in BLOCKED.md H3.

Refutation attempts this iteration

claim whose outcome
the title's settle window is [160, 236] Decoder, 5b0a6e6 survived — our exporter still computes [160, 236, 198] under the corrected record layout
"ptlogo1 rests at t=251 and stops moving at t=42" Decoder, 5b0a6e6, and the headline evidence for rest.t ≠ settle refuted on its evidence — in the current export ptlogo1.rest.t is 42, equal to when it stops moving. The record-layout fix repaired exactly this element. The conclusion survives on other elements (ptlogo_back2eff1, pteff00); the example no longer supports it
"ptbtn00 reaches a=255 at t=238"120 units the Port's own refuted — 236, so 118 units
"build 4 is still fading up from black until t=261; pteff00 is 7 % opaque at 243" the Port's own refutedpteff00 is opaque at t=0, clear by t=16, transparent until 261, then fades to black by 269. At t=243 it is 0 % opaque. The comment had the direction backwards

What this does not settle

  • Whether the human's "late" is a real offset the corpus has not measured. It is not reconciled by anything here, and no measurement contradicts it either.
  • The t=118 / t=160 anchor. Asked, not guessed.
  • Whether the plate pulses after arrival. The port holds it; the corpus measures a ~2.24 s pulse. Filed already in flow.json's no_pulse_why.
  • Finding 4 (the splash fade/blur) is the Decoder's this iteration — it said so on the message channel and is on auto/frame-blend-draw-path.

The splash blur is an ASSET, not a pass — and this port already draws it

Added the same iteration, after the Decoder answered play-test finding 4 from GPU state (no post-process on either splash, auto/frame-blend-draw-path at 3cc1b51) and the human added the observation that the logos go from blurred to clear/sharp, in about one second.

Those two are not in tension. They resolve each other:

Every logo ships a second, pre-blurred copy of itself, ~21 × 20 px larger and concentric, which is drawn alone first and crossfades out as the sharp logo fades in. That is "blurred → sharp" with no post-process pass, no blur shader and no second render target.

Measured off export/sprites/title/:

logo sharp companion position offset concentric?
palogo_gamearts 500 × 71 521 × 91 (11, 10)
palogo_seta 240 × 89 261 × 110 (10, 11)
palogo_anima 388 × 136 407 × 156 (11, 9)
palogo_sqex 666 × 68 686 × 89 (10, 11)

🔴 "The port applies no blur at all" is false, and it came from this page's own side

BLOCKED.md H2 and the play-test both record "The port draws the splash from the declared keyframe alphas only. It applies no blur at all." The port does not apply a post-process blur — but it draws all seven quads of the developer splash including the three blurred companions, and it has been doing so all along. A frozen sweep across the build-in, one deterministic capture every 3 units (the human's "take a series of quick screenshots within the short animation span", done without a clock at all):

t units drawn
0.00 s 0 background only
0.05 0.25 s 3 15 background + the three blurred companions alone
0.30 0.70 s 18 42 all seven — the crossfade
0.75 0.80 s 45 48 background + the three sharp logos

Blurred first, then both, then sharp. The mechanism is reproduced. The claim that it was not came from describing the renderer instead of running it.

Cross-check of the Decoder's quad mapping — survived, independently

splash-quad-timeline.txt names its quads by NDC rectangle off the guest's vertex stream. This export's declared rectangles, converted independently:

this export the guest agreement
palogo_sqex x[0.517, +0.523] y[0.106, +0.083] Q0 x[0.520, +0.520] y[0.100, +0.080] ~2 px
palogo_sqex_eff x[0.533, +0.539] y[0.133, +0.114] Q7 x[0.530, +0.540] y[0.130, +0.120] ~4 px

Two decoders, two paths — a .pak read statically and a vertex stream logged live — agreeing to a few pixels on both the sharp quad and the blurred one. That is corroboration of the mapping and of Q7 being the companion.

What is still open on the splash

  • The extent. The rectangles agree; the alphas are where the 5-units-per- frame discrepancy above came from, and until that is settled it is not possible to say whether the game holds the blurred copy longer than we do. That is the same question as H3 and is asked once, there.
  • Whether palogo_eff0 (kind 0x10, a full-screen quad, like the title's pteff00) is the splash's fade veil. Not examined this iteration.