Last iteration I enumerated three free-running clocks, wrote that the leaf is
pinned only by --leaf-time, then tested reproducibility without passing
--leaf-time and concluded nothing free-runs on the menu path. The answer was one
paragraph above the experiment that contradicted it.
My own flagged weakness found it: deliberate wall-clock variation via
--script=wait:N, putting the capture at t=96 units against t=369. Spin pinned
only, wait 0.5 vs 5.0 differs by max 91.19 per channel; with --leaf-time=0 added
it is byte-identical. draw_leaf_for is ptloop01/ptloop02, present on main_menu and
not just the title, which is why that row drifted.
verify-capture passed --loop-phase=0 and not --leaf-time=0 -- I fixed the clock I
had been bitten by and left the one I had merely listed. Enumeration without
follow-through fails exactly like no enumeration.
Both are now pinned at all six render sites. main_menu returns 13.21 across three
runs and two renders after different waits are byte-identical.
The number moved 13.26 -> 13.21 and that is NOT an accuracy improvement: pinning
the leaf at phase 0 puts ptloop01/02 at one specific pose rather than wherever the
wall clock left them. A different configuration, now reproducible. Which pose the
game shows at rest is not settled by this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
verify-capture's main_menu row reads 13.30 / 13.27 / 13.25 / 13.26 across runs
this session while every other row is identical to the digit. I cited those
numbers repeatedly, including in the rest() adjudication.
Cause: the focus ring spins on time_units raw rather than the pose clamped by
holding -- deliberate and correct, since the ring is the one thing on a settled
screen that keeps moving -- so its angle at capture is set by the wall clock.
extras is stable because nothing there spins.
--loop-phase already existed and did not cover it: it pins the looping focus
record phase, while the spin is a second free-running clock I guarded once and
never connected. Extended loop_phase_units to pin the spin too, and verify-capture
now passes --loop-phase=0 at all four render sites.
The control matters because the drift was intermittent -- three unpinned runs gave
13.25, 13.26, 13.26, so three pinned runs agreeing would prove nothing. Phases
0/30/60/90 give 13.2583 / 13.1991 / 13.2637 / 13.2588: the pin is live and the
0.065 spread is the whole of the observed drift.
Non-finding recorded so nobody mines it: phase 30 scoring lowest is not evidence
about the ring's real phase -- 0.065 against a ~13.2 gamma floor is 200x too
small. A margin only means something against the noise it sits on.
No conclusion changes: the smallest margin any of them turned on was 0.14%
differing area.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
The previous entry scored verify-screen's title_jp frame against the oracle and
concluded the port had moved away from the game. That frame is posed --pose=rest,
which the port does not ship. Posed as it runs, the disputed block scores +0.9994
against the reference's +0.8727, and the whole surface +0.9652 against +0.9200 --
holding under gamma compensation and on the English control (+0.9946 vs +0.9560).
The port is closer to the game than the reference on both title screens.
Mechanism: ptlogo_back2eff1 is (0,0)(98,0)(100,255)(102,255)(104,0) -- a 4-unit
sparkle whose rest.t is the peak of its own flash. Six of them stagger across the
logo, so --pose=rest fires every sparkle at once. The 25.6% excess light was real
and was in a frame nobody sees.
verify-screen is not at fault: it poses rest deliberately, so that both renderers
read one decoder and the run is a consistency check. I used a consistency-check
frame for a correctness question. Its header now says its frames must never be
scored against a capture.
A second claim in that entry was also wrong -- both screens draw those layers
under pose=rest; I had compared a --menu timeline log against a verify-screen
rest log and read a mode difference as a screen difference.
verify-capture takes a fifth per-row field, a capture crop, because this capture
is a full display frame with the surface at +0+45 while the others are
pre-cropped. With it title_jp reads RMSE 20.91 / 1.04%, beside title's 14.16 /
0.21%. The row prints 'no capture' until their branch merges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
live-attract-title-press-a-band.png is 1279x120 and the harness could not compare
a band. Placed by sliding: y=520, a 25x drop over five pixels, and it fits at
t=236-238, the plate's own window.
Its 0.354% is not the port's error. The port reproduces the same band of
live-title-press-a EXACTLY (0.000%), and the two captures differ from each other
by 0.301% -- two thin strips, 248x5 and 206x1, the shape of a sub-pixel edge
difference. The row's job is to stay near the oracle-to-oracle gap, not reach
zero, and it says so.
I had begun writing that the attract-returned title differs from the boot title.
It is two hairlines. The connected-component breakdown stopped it.
All eight live captures are now used. The three that were idle were each blocked
by the harness, not the capture.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
live-main-menu-options-focused.png -- the only capture of a known focus state --
was untestable because --focus= parsed, was stored, and was overwritten by the
authored initial focus on every _menu_enter. Every run logged focus ptbtn01
whatever was asked for. Now pushed into the menu model so navigation continues
from where it was forced.
With it working, each capture picks out exactly one button: ptbtn04 at 0.1355%
against 0.70-0.82% for the others on the OPTIONS capture, and ptbtn01 at 0.0705%
against 0.72-0.84% on the plain one. 5x and 10x discrimination. First time the
port's focus rendering has been checked against the game at all -- the existing
main_menu row uses an authored focus and could never have caught a focus error.
Records in flow.json that live-main-menu.png shows NEW GAME focused, so the
authored initial_focus matches the one frame it can be checked against -- and
that this does NOT overturn Q5's measured instability. It stays authored.
Adds main_menu_options to verify-capture at 0.13%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
live-title-press-a.png was unused in the corpus. Posed at t=237 -- inside the
plate's 8-unit window -- the port matches it at 0.00093%, against 0.0124% for
the no-plate capture at leaf phase ~400. Two captures, two different phases,
both under 0.013%: a systematic sweep-geometry error would leave a floor in
both, so last iteration's caveat is closed.
Sweeping the whole screen's instant against capture 1 gives at best 0.148% at
t=230 -- 10x worse than the leaf-only fit. So that capture is the screen SETTLED
with the sweeps still looping, which is the first independent evidence for the
authored loop_leaf decision.
Fixes the cause of a flat 1% floor: --screen=X --overlay=Y pushed the raw
elapsed clock into the overlay (9 units at capture), so press_start drew nothing
-- the flag whose purpose is 'put the plate on the title'. A static overlay now
poses at its own arrival; the --boot shared clock is untouched.
Adds title_plate to verify-capture at 0.00%, the most sensitive row in it. Its
instant is FITTED and labelled as such.
Also records that I nearly committed a wrong cause for the overlay bug: I wrote
that nothing drives the overlay's clock outside a sequence. It is driven, every
frame, from view.time_units.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
The Decoder's refined sweep fit had never been testable: verify-capture passed
it as a whole-screen --time that pose_at discarded, and asking for it honestly
poses past the title's group end. --leaf-time separates the leaf's clock from
the screen's.
Controls: the renderer is deterministic (3 runs bit-identical) and the sweeps
move 0.40% of the frame between phases, so the comparison can see them.
Sweeping the full 600-unit span gives a sharp basin at 390-415 units (0.0124%)
against 0.2532% at t=357.7 -- 20x. So the title's 0.21% residual is the sweep
phase, not structure: at the fitted phase it matches the capture as well as the
splashes do.
NOT adopted: the port loops the leaf freely and re-posing the harness to the
fitted value would be tuning until they match. Filed instead, with the question
of whether 357.7 and this are even the same quantity.
Also verified last iteration's settle-window change was surgical: only
press_start and its twin moved, 14 screens unchanged including title's
Decoder-confirmed [160,236]. Settle-window ties exist on 4 screens but all sit
under the 30-unit bar, so the arbitrary tie-break never reaches the runtime.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
1. --time= was silently ignored on any screen with a settle window >= 30 units:
pose_at overwrote the requested instant with settle_instant. ScreenView.frozen
now marks an explicit instant and skips both clamps.
2. press_start's settle window was [0,214] -- the dead stretch BEFORE the plate
exists -- so its settle instant was t=107, where the element is alpha 0. The
exporter now rejects intervals in which nothing is visible. title keeps
[160,236], the interval the Decoder's draw stream confirmed.
3. My authored looping_focus_records entry for press_start/ptbtn00 drew a dim
focus record INSTEAD of the plate's own sprite: max 0 vs max 252.5. Deleted --
an authored guess that overrides a decode with a worse answer is removed.
4. verify-capture passed --time=5.9617 for the title and it was never applied.
Every title figure it has printed, including the 0.26% quoted to the Decoder,
was measured at the settle instant under a note claiming t=357.7. Both rows now
pose by omission and the note matches. title is 0.21% honestly; splashes
unchanged at 0.01%.
The boot's end artifact now contains the plate (region mean 95.7 vs 33.6).
Corrects last iteration's BLOCKED row, which had the entry's effect backwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
The Decoder closed it by ADDING OBSERVABLES, not by tuning. The vertex buffer
carries positions and colours at the same instant, so all four quantities must
agree on one t: quad A x solves to 357.88 and quad B x to 357.58, both +/-0.12
units, against 355.75 +/-1.54 and 354.09 +/-1.89 from the alphas. Alpha moves
only 0.27-0.33 levels per unit, so one byte of quantisation is worth 1.5-1.9
units -- 6-8 px of sweep at 4 px/unit. That is the whole of the 11.5 px. At
t=357.7 the centres land within 0.70 px and both alphas inside one level.
THE LESSON IS THE EARLIER ONE INVERTED AND IT IS THE HALF WORTH KEEPING.
Checking a wrong rule against alpha made it look confirmed; here the same
insensitivity MANUFACTURED a residual that did not exist. An insensitive quantity
does not merely fail to falsify -- it invents error. Solve on the fastest-moving
field, check the slow one, never the reverse. I was already looking for a pivot
rule to explain 11.5 px when they wrote; there was nothing to find.
REFUTATION ATTEMPT, survived with a nuance: they state the leaf pivot is (200,90)
on a 399x180 sprite, "the pivot is the centre, so rotation displaces it by
nothing". Checked against my export -- pivot [200,90], sprite 399x180, true
centre 199.5,90. It survives, but the sprite is ODD-WIDTH so the pivot is the
centre to within half a pixel rather than exactly. No consequence against their
0.70 px agreement; worth stating because "displaces it by nothing" is the kind of
sentence that later gets leaned on for a sub-pixel claim.
verify-capture now poses the title at t=357.7 rather than 355: RMSE 21.07 ->
20.92, differing 1.82% -> 1.81%. Marginal, and it is the right pose for a stated
reason rather than a better number.
AND ptlogo_eff2 IS WITHHELD FOR A BETTER REASON THAN MINE. I had it on caution
about untested generalisation; the Decoder points out it is on title_jp and
MISSION section 7 scopes out "localisation beyond English", so it is not a
question this port has to answer and the parked Japanese capture does not need
reviving for it. authored/rendering.json now gives scope first and undecidability
second. Widening scope to close a residual would have been the wrong trade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
I tested the wrong switch. SYLPHEED_KF_TIME_SHIFT is a superseded partial fix: it
got the association right but LEFT POSE 0 UNTIMED, which is exactly why the
untimed keyframe appeared to move from last to first. The real correction is the
DEFAULT in the tagged crate, with the old reading behind SYLPHEED_KF_TIME_LEGACY.
So last iteration's five rows measured a mismatch against a knob nobody should
use -- I suspected they were not decisive, I did not suspect the knob was retired.
THE CONSEQUENCE IS MUCH SMALLER THAN I BUDGETED. A placement group is an 8-byte
header then frames x {u32 time; 36-byte pose}, so pose 0's time is the group's
lead-in word and every pose is timed. Measured on the re-export: 866 keyframes,
0 untimed. `pose_at`'s "the final keyframe carries no t, so give it a synthetic
time" premise does not invert, it DISAPPEARS -- dead code rather than wrong code,
which is why nothing needed re-deriving. And the leaf now reads t=0 x=-639,
t=150 x=-39, t=540 x=1521, giving x=781 at t=355: the Decoder's predicted
top-left, and the 1300 px discrepancy is gone.
Pinned by tag, which is what MISSION section 2's tagging rule is for. BLOCKED was
wrong in both directions -- "cannot be taken yet" AND "only when that branch lands
on main". It arrives when the tag is pinned.
COST STATED: sylpheed-cli builds from the workspace crate, so until this reaches
main the exporter and the reference renderer read different decoders and
verify-screen compares two eras. verify-capture is unaffected -- it compares
against oracle captures and never touches the CLI. Revert to the path dependency
when the tag is an ancestor of main.
Oracle: publisher_logo 1.00% -> 0.75%, developer_logos 0.39% -> 0.33%, and
extras' differing region COLLAPSING from 736x525 to 398x295 at the sweep position
-- the residual localised onto the one element still in question. title unchanged
at 1.82%, now posed at t=355, the Decoder's FITTED sweep time. t=390 measures
1.65% and picking it would be fitting the pose to the score.
REFUTED, MINE: "ptlogo_eff2 is the single drawn element at a scale that is not a
whole multiple of 100%". That census was parents-only; the 45 leaves hold
thirteen distinct non-whole-multiple scales and 125% is among the rarest at two.
The claim's real content was "the only one the port draws" -- about my element
set, not the disc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
The Decoder attached an honest caveat to its reproduction of the tone-curve
refutation -- its bins included the focus-state mismatch, so it was "not a clean
second opinion". Removing that caveat found a defect of mine.
IDENTIFYING THE FOCUSED BUTTON, with a known-answer control. Walking focus
through all five buttons and comparing each against a capture:
vs -options-focused vs live-main-menu
ptbtn01 NEW GAME 6351 531
ptbtn02 LOAD GAME 7087 7094
ptbtn03 TUTORIAL 6230 6237
ptbtn04 OPTIONS 1292 6364
ptbtn05 EXTRAS 6073 6080
The control picks OPTIONS by 4.7x -- the answer its own filename gives -- so the
test is trustworthy, and live-main-menu.png has NEW GAME focused by 11.5x. That
is what the port already focuses. Corroboration, NOT a decode: Q5's instability
stands (four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME) and this
identifies one frame rather than a rule. Recorded in the authored entry.
THE DEFECT: `verify-capture` rendered menus with `--screen=`, which draws no
focus record, so it had been comparing main_menu to the oracle in a state the
oracle was never in.
main_menu 0.25% differing, RMSE 14.79 -> 0.06%, RMSE 13.21
extras 0.46%, RMSE 15.29 -> 0.20%, RMSE 13.38
A 4x improvement that was entirely my harness posing the port wrong rather than
the port drawing wrong. The first run of a new correctness harness reported a
discrepancy and three quarters of it was the harness.
AND THE DECODER'S CAVEAT RESOLVES TO NOTHING, measured rather than assumed:
re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 /
1.18 / 1.10 / 1.03 / 0.94 at render 8..48, unchanged from the mismatched run. Its
reproduction stands as a second opinion after all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
`tools/port/verify-screen` line 20 has said "use `tools/verify-capture` for the
correctness question" since P1, and there was no such file. The port has had a
harness comparing itself to sylpheed-cli -- two renderers sharing its assumptions
-- and none comparing it to the game, while its own docs said otherwise. That gap
is what ORACLE-CAPTURES.md warns about: this corpus has been bitten three times
by two renderers agreeing, and every one was obvious against a capture.
Five screens against framebuffer captures of the real game:
main_menu RMSE 14.79 0.25% differing focus state may differ
extras RMSE 15.29 0.46% focus state may differ
title RMSE 21.07 1.82% ptloop sweeps never stop
publisher_logo RMSE 10.77 1.00%
developer_logos RMSE 9.37 0.39%
NO SCREEN SHOWS A LARGE CONNECTED BLOB -- the shape a missing element makes, and
the shape all three historical failures made.
And 74.1% of main_menu's differing pixels fall inside the ORACLE'S OWN focus
signature (live-main-menu vs live-main-menu-options-focused, the same screen with
a different button lit). So the bulk of that disagreement is a state mismatch,
not a defect.
REFUTATION ATTEMPT, on ui-render-tone-curve.md's `capture = 255*(render/255)^g`.
It survives where it was measured and not past it. Binning every structurally
matched pixel by render level gives the relationship directly, and the implied
exponent is NOT constant: 1.26 at render 16, 1.10 at 32, crossing 1.0 near 44,
down to 0.69 at 96. Above ~44 the capture is BRIGHTER than the render, which one
exponent cannot express -- and that is exactly why my whole-frame fits kept
returning 1.00, the two halves cancelling. The page's own stated reach ("nothing
constrains midtones or highlights") was not a hedge, it was the finding. Its 1.49
for this screen measures 1.18-1.26 in my darks; recorded as a disagreement rather
than resolved, since they fit selected flat patches and I binned everything.
Two earlier versions of this tool reported a best-fit gamma and were wrong both
times -- once fitting across a 74% structural mismatch, once extrapolating past
the measurement's stated reach. The fix was not a better fit but a different
instrument: it prints the curve, which somebody can argue with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF