Commit Graph

177 Commits

Author SHA1 Message Date
sylph-decoder
7b3c83858d re: index F6 unit 9, which closes the sweeps' unmeasured period
ui-clock-freezes-at-settle.md left the sweep period  because a validated
estimator disagreed between two dwells (515 vs 452 frames). The cause is now
measured: captured frame counts are not comparable across runs at all --
1.947x between two captures of the same animation, with the baseline moving
1.953x alongside. As a ratio the period reproduces to 0.35%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-02 19:21:09 +00:00
sylph-decoder
27f81d6d91 re: the guest presents at 30 fps -- 60 units/s, and the port was right
Answered against the pre-registration committed before the capture.

Units per second was the last open number on the PRESS (A) plate, and both
prior measurements of it were wall-clock readings off an emulator that runs
the guest slow by an unknown factor. They disagreed by 2.9x, because a 30 Hz
guest at full speed and a 60 Hz guest at half speed look identical on a wall
clock.

The ruler here is not a clock. ADV.wmv declares 30.0000 fps in its own ASF
header, so a decoded movie frame is a tick the emulator's speed cannot
stretch. Presented frames per decoded movie frame is guest_fps/30 with no
wall clock in the chain.

  predicted  H_A 30 fps ->  60 units/s -> 1.0
             H_B 60 fps -> 120 units/s -> 2.0
  measured                                1.0000

Both pre-registered guards pass. Guard 2: a perfect repeating 3-buffer
cycle, 52 uses each (exactly 156/3), 2 chroma planes per luma on 156 of 156.
Guard 1: run lengths are 156 runs ALL of length 1 -- no smear, so the
dropped-movie-frame bias that would have pushed the answer toward 120 is
measurably absent rather than argued away.

So H_A. The port keeps its 60 and changes nothing.

REFUTES the live H3 hypothesis that 120 units/s explains the play-test's
late plate. That hypothesis was well-formed and attractive precisely because
it would have explained the complaint, which is why it needed a ruler that
is not a clock.

Which means finding 3 still has no cause. The strongest remaining candidate
is decoded rather than speculative: the plate's declared onset is t=214, not
t=236 -- a 22-unit fade, matching the T=22 the oracle confirmed by measuring
+23 alpha per presented frame on that element.

The pre-registered control could NOT be run: this logger build emits vb=
addresses, not vertex contents, so there was no alpha to check +34 against.
A weaker control is substituted and labelled -- the splash shader/blend
census, which validates the log's structure (what this measurement uses) and
not alpha extraction (which it does not).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 18:49:40 +00:00
sylph-decoder
4518b5c76a re: the pad word is REMAPPED -- my own bit table was mislabelled throughout
input-pad-read-path.md says of the word the C_PAD_DECODER reads: "There is
no shift and no remap on the way in -- the bit positions are XINPUT's own."
That is wrong. sub_8220D500 rebuilds the word out of XINPUT_GAMEPAD before
anything else sees it, into the game's own numbering.

  bits  0-3   A B X Y
  bits  4-7   left stick  UP DOWN LEFT RIGHT   (+/-20000 of 32767)
  bits  8-11  right stick UP DOWN LEFT RIGHT
  bits 12-15  D-pad       UP DOWN LEFT RIGHT
  bits 16-17  START, BACK
  bits 18-19  LB, RB
  bits 20-21  LT, RT  -- digital, threshold >220 of 255
  bits 22-23  L3, R3

Extracted mechanically from the image, no row typed by hand. The control is
the shape of the result: the 24 assignments land on bits 0..23, each used
exactly once, none repeated. A misdecode does not produce a bijection over a
contiguous range, and coincidence does not put the stick and D-pad
directions in the same order in two aligned nibbles.

So every mask in that page's tables names the wrong button. The 0xE000 x18
site, read there as "B | X | Y", is "D-pad DOWN | LEFT | RIGHT" -- eighteen
sites testing a menu cursor, which is what 18 sites should be.

And its headline negative is REFUTED: "LB and RB are not menu inputs" is
false. They are bound at config fields this+0x70 and this+0x84, LT/RT at
+0x74/+0x80. The negative was searched for 0x0100/0x0200 -- LB and RB in
XINPUT's numbering -- in a word where they live at 0x40000/0x80000. Right
function, right buttons, wrong bit positions, so it could only come back
empty. A negative is only as good as the numbering it was searched in.

Also decodes the ring record: +12 HELD, +16 PRESSED, +20 RELEASED, +28/+32
raw trigger bytes. Edge and level are one struct four bytes apart, which
displaces that page's guess that press-vs-hold was split between GetState
and the XamInputGetKeystrokeEx queue.

The superset claim in that page survives and is untouched: sub_82457038
really does compare every XINPUT_GAMEPAD field, and it really is
XINPUT-layout. This page depends on it.

Not decoded: which output bit means which ACTION, and per-screen sets. 5 of
18 output-bit sites did not resolve to a pad guard, so the output map is a
lower bound -- in particular "START is not tested" is NOT claimed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 18:33:42 +00:00
sylph-decoder
e263041337 re: the splash blur is a TEXTURE -- palogo_*_eff is a baked 10-px glow
Play-test finding 4, answered as a mechanism and from the disc, so it
generalises instead of describing one boot.

ui-splash-draw-pass.md excluded a post-process from GPU state and closed
with "that softness is in the texture or in which quads are drawn, not in
a pass", leaving the two unseparated. It is both, and they are one fact:
each logo ships a second texture that IS the blur -- the same artwork
outset by exactly 10 px per side, concentric to <=1.5 px, drawn as its
own alpha-over quad.

Three results, each with its control:

* The capture's eight anonymous quads are NAMED from the disc. Predicting
  each NDC rect from declared position + decoded sprite size matches all
  eight bijectively; every match <=0.0061, every runner-up >=0.0272, a
  4.5-8.9x margin. That margin is the control -- eight similar boxes
  would match anything.

* REFUTES splash-quad-timeline.txt's "the same three rects scaled
  slightly larger" (my own earlier wording). The x and y scale factors
  differ by up to 0.28; a uniform scale cannot do that, a fixed 10-px
  border can. The conclusion it supported (draw all six quads) stands;
  the model was wrong, and the wrong model tells a port to scale a
  sprite.

* The T8aD blend bit tested OUT of sample on entries 10/11, which were
  not in its 35-row fit and are the screens under complaint.
  Pre-registered additive=false for all eight against 0 additive draws in
  1048; held 8/8, with the control still reporting 9 additive on entry 6.

Also resolves a REFUTED.md 🟡 <our-reader> in the reader's favour: the
prediction is ours and the target is the oracle, so the agreement is
evidence about the reader rather than a claim resting on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
2026-09-01 18:24:14 +00:00
sylph-decoder
c1c8aa4288 re: WITHDRAW 'the unit rate is per-GamePart' -- it was the emulator's frame rate
The Port's arithmetic was right: a 160-unit sub-interval cannot outlast the
210-unit group containing it, and my hold (4.514 s) outlasted three cold-boot
measurements of the whole group (3.37/3.50/3.51 s).

My own capture says why, once asked the right question. The three numbers I
quoted came from three regions of ONE run, and I never asked how fast the
emulator was going in each:

  splash B (the hold)      3.39 labels/guest-second  -> I reported 35.4
  splash A (publisher)    15.33                      -> I reported 40.0
  the title (the plate)   23.20                      -> I reported 56.8

Monotonic. The 'per-GamePart rate' is the pacing of the region it was
measured in. Splash B was captured at 3.4 fps, an eighth of the title
region.

And the corpus already had this. boot-splash-dwells-are-declared.md says in
its own words that the wall-clock dwell is an emulator-pacing artefact that
varies run to run, with a no-input boot 15-20% long on the same declared
timeline. I re-derived a documented artefact as a discovery and drew a false
conclusion from it.

The instrument lesson, which is the part worth keeping: I believed the guest
timebase removed the artefact. It does not. The game's animation clock is not
the guest timebase -- it is frame-coupled -- so a slow run advances less
animation per guest second and no clock can see that from inside. My control
asked 'does this timebase track real time', which is capability. The question
that mattered was 'is the quantity I divide by coupled to the frame rate',
and nothing I ran asked it. That is PROTOCOL's own warning, which I quoted at
the other agent two iterations ago and then walked into.

Restated: the clock is neither purely frame-counted (21 vs 33 labels for one
animation) nor purely time-integrated (rate scales with frame rate).
Consistent with a clamped per-frame delta, untested, and now the real open
question.

Consequence: no rate measured on this emulator is the console's; all are
biased low. Best estimate stays the declared timeline against the fastest
runs -- 60 units/s, 1.1% on the developer splash. The port keeps 60 for every
screen and nothing needs averaging.

Untouched: section 1 of splash-declared-vs-captured.md. It compares a disc
table against vertex alphas at integer t and never divides by a duration, so
the pacing artefact cannot reach it.
2026-09-01 17:29:52 +00:00
sylph-decoder
4b5ac88f0a re: the declared timeline DOES reproduce the splash -- and the unit rate is per-GamePart
Two results, one of which corrects me.

1. The R1-re-opened 'declared keyframe timeline reproduces the captured
splash' resolves in FAVOUR of the declared timeline. Instrument is a disc
keyframe table against the guest's vertex stream -- no renderer in the chain,
which is what the 'our-reader' tag demanded. Calibration-free test: is each
captured alpha an exact member of the declared piecewise-linear value set at
integer t? 39/50 exact under truncation, 30/50 under rounding, and the worst
error under either rule is ONE alpha level in 255. All 11 non-exact samples
are low by exactly 1 and all are on falling segments. The old refutation
rested on 'still at a=255 nine frames after its declared a=32' -- under the
fixed layout a=32 is at t=206, four units from the end of a 210-unit
timeline. That was the off-by-one association.

2. The unit->seconds rate is PER-GAMEPART, which corrects what I told the
port an hour ago. Title ~57 units/guest-second; splash ~35-40. The splash
figure is confirmed two ways that share no algebra: a 15-unit ramp (T read
off the disc, not borrowed) and a 160-unit HOLD, which is a declared
duration with no T, no alpha slope and no interpolation in it.

And my 'the borrowed T=15 does not apply' was wrong in the other direction:
the disc says T=15 plainly. What failed was the premise of a single global
rate, not the T. Noted at the foot of that page rather than by editing it.

The trap worth recording: all four elements give 650-679 alpha/s, agreeing
to 2%, which reads exactly like one clock. It is a coincidence -- T differs
22 vs 15 and the rates differ 57 vs 37, and the ratios nearly cancel. A
quantity that looks constant across screens is not evidence of one clock
when the thing that would vary is inside it. The hold breaks the tie.

For the port: a single keyframe_units_per_second cannot be right, and a
splash played at 60 runs 1.5-1.7x too fast -- shorter, sharper fades than
the game's, which is the direction the play-test reported.
2026-09-01 17:19:59 +00:00
sylph-decoder
d67aaaf30b re: the rate is 56.8 units per guest second -- both predictions hold, control passes
The capture reached the title AFTER the harness stopped classifying, so the
plate's ramp was in the log with tick stamps after all.

ptbtn00: alpha 11->231 over 334.4 guest ms = 657.9 alpha/s. With its
independently attested T=22 that is 56.8 units per guest second, inside the
pre-registered 55-65 band. The final step of every ramp is excluded because
it clamps at 255 and reports more elapsed time than it consumed -- including
it drags the plate to 633.0 alpha/s, a 4% error entirely inside the clamp.

Control passes at 1.15%: ptcopyright gives 650.4 alpha/s, and at one shared
clock that makes its own segment T=22.25. Two elements agreeing on a rate
AND independently landing on a round declared length is stronger than either.

Why my earlier 29.9 was wrong, and it is the failure the page predicted: it
used T=15 borrowed from a row that is about A element with a 15-unit fade,
generalised to splash B's quads, which is not what that row says. At 56.8 the
splash elements' implied T is 23-34, none of them 15. And the step quantum
does not rescue 15: splash steps are multiples of 17 = 255/15, which is what
made 15 look confirmed, but at T=28.5 a step of 17 is simply TWO units of
8.9. A quantum fixes T only if you already know the step is one unit.

60 is NOT refuted -- 5.6% away against ~5% quantisation resolution -- so the
port keeps it. But this DOES eliminate the unit constant as a cause of a late
plate: at 56.8 units/s t=236 lands at 4.15 s against the port's 3.93 s, so
the port is fractionally early.
2026-09-01 17:11:19 +00:00
sylph-decoder
781a70af9a re: the animation clock is TIME-INTEGRATED -- one prediction held, one failed
Reported against the pre-registration, failure first.

HELD, and by a cleaner argument than the regression: the clock is NOT
frame-counted. The same animation takes 21 frame labels in one capture and
33 in another, and splash A's logo steps +136,+34 in one and
+17,+51,+34,+34,+17,+17 in the other. A fixed per-frame increment cannot do
that. Steps are always integer multiples of 17 (255/15 = one unit), so the
clock advances in whole units at a rate set by how long the frame took.

That retires '2 units per submitted frame' as a MECHANISM -- including my own
page from two hours ago. The measurement stands; 2 was that run's frame
pacing. units = 2 x frames computes an emulator artefact. Recorded as a
proposal against the register, not enacted, and noted at the head of the H3
page rather than by editing it.

FAILED: predicted 60 units per guest second, accept 55-65. Measured median
29.9 over six elapsed-ratio estimates (25.2-36.6). Prediction 3 said 30 was
excluded and 30 is what came out.

Why the rate is not a number yet: units/s = (d_alpha/dt) * T/255, and T is
the load-bearing term. d_alpha/dt is measured cleanly six ways. T=15 for
these elements comes from a corpus row whose usual derivation is circular
with the thing just retired -- a step of 34/frame implies T=15 only GIVEN 2
units/frame. T=30 would give ~60. No amount of re-measuring alpha settles it.

Settles with ptbtn00, whose T=22 is attested by two independent readers with
no clock anywhere in the chain. That capture did not reach the title in 531 s
of attract loop against 243 s in the previous run -- the variable attract
loop capture-harness-status.md already documents at up to 604 s. Instrument
built and control-passed; one run is missing.

Instrument: guest timebase, 50 MHz per emulator.cc:225, scalar 1.0 per
clock.cc:37, so no host wall clock enters any number. Control: 123.24 guest
seconds across a capture running ~118 wall seconds.

Told the port to change NOTHING today: ~30 rests on an unverified T, and
retiring 2 x frames does not by itself supply a replacement.
2026-09-01 17:04:23 +00:00
sylph-decoder
0565098fa2 handoff+index: H3 answered, the settle anchor, and the pad's full field set
Three rows the port is blocked on:

H3 -- 2 units per guest frame, measured on the plate's own ramp against a
pre-registration. Their 5 excluded by >2x, and the reason named: an alpha
step is not a clock rate without the element's declared T, and my own
splash-quad-timeline.txt published alpha against frame with no T column.
That file now warns at its head.

The title settle anchor is t~160, not t=118 -- it is ptcopyright, the last
build-in element and the only glyph one. Said plainly that this is the
reading under which their clock:'shared' collapses, and that a consequence
is not a counter-argument.

Told them NOT to change their 60 units/s yet: units/second is untouched by
this and two of my own captures disagree ~2.9x on frames->seconds.

The pad: sub_82457038 reads every field of XINPUT_GAMEPAD, 14/14 loads
verified against the image, plus a second keystroke-queue path. Flagged as
the superset the game can SEE, not the per-screen set, so it is used to
check a binding table and not to write one.
2026-09-01 16:50:52 +00:00
sylph-decoder
4bb67fa166 re: the splashes have NO post-process -- one pass, source-over, alpha in the vertex stream
Play-test finding 4 asked, in order: is there a pass, what is it, where do
its parameters come from, only then what curve. All four, from GPU state.

1. NO post-process pass. Over all 1 048 draws of frames 4..226 (both boot
   splashes): rt0=[tile=0 fmt=0 exp=0] on 1048/1048, pitch=1280 msaa=0 on
   1048/1048, edram_mode only ever kColorDepth or kCopy, resolve dests only
   the two alternating front buffers, and NO texture base anywhere in the
   capture equals a resolve destination. The only texture bound in the whole
   splash region is the sprite page 0x11A50000. No blur, no bloom, no fade
   quad over a resolved image, no tone curve, no resolve-and-resample.

2. What it is: per frame, a full-screen replace triangle (the clear), a
   full-screen black quad through the ordinary blend, ONE batched sprite
   draw carrying every visible element (indices 4/8/12/24), and the two
   presentation resolves.

3. Where the parameters come from: NOT the constant banks -- the splash
   pixel shaders read zero float constants, ps_c[n=0] on 1048/1048, taken
   off each shader's own float_bitmap. NOT immediates. The fade is the
   per-vertex k_8_8_8_8 colour in a vertex buffer the guest rewrites every
   frame.

4. The curve falls out of 3 and is the corpus's existing 34/frame law,
   reproduced on an independent capture. Not claimed as new.

And the composite is ORDINARY SOURCE-OVER, confirmed from the shader ucode
rather than inferred from the blend register: the register reads
ONE/ONE_MINUS_SRC_ALPHA, which looks premultiplied, and is -- because the
shader premultiplies. The two together are src*A + dst*(1-A). So the 'more
pronounced' fade is neither a blend difference nor a pass.

The likely mechanism instead: the developer splash submits SIX quads, three
logos plus three slightly-larger companions that lead them by 8 frames and
are gone 79 frames early. Two over-blended copies of the same art at
different scale is a halo, on screen only during the entry.

Refutation attempt, recorded: REFUTED.md's re-opened rest() pair states its
settling condition as 'a draw capture of the developer splash naming which
of the three glows is submitted at rest'. This is that capture. All three
companions ARE submitted in all 21 frames -- so no rule that hides one
describes this stream -- but they are NOT interchangeable: Q6 leaves the
plateau on its own -5.6/frame decay while Q4 and Q5 hold 255. The one-byte
sibling difference is drawn.

Trap recorded with the evidence: censusing the whole 600-frame log finds six
640x360 textures sampled ~330 times each, which reads exactly like a
half-resolution blur chain. They are the attract movie's chroma planes and
first appear at frame 234, after both splashes. Restricting the window is
what separates them.

Evidence: docs/re/data/splash-draw-pass-census.txt,
docs/re/data/splash-quad-timeline.txt, docs/re/data/shaders/*.ucode.frag.
Logger: canary sylpheed-re d90d14e02.
2026-09-01 16:21:17 +00:00
sylph-decoder
bac8e17a7f re: the blend mode IS on the disc -- T8aD +0x04 bit 0x02, DECODED
Reverses two of my own pages. t32-blend-mode-not-on-disc.md said the mode is not
on the disc; ui-blend-mode-measured.md classified it measured and told the port
that which field selects it was unknown. Both were honest and neither is current.

Bit 0x02 of +0x04 set means the game draws that sprite ADDITIVE
(RB_BLENDCONTROL0 = 0x01010101); clear means premultiplied alpha-over.

Fit: 35 elements over three screens, 16 set/additive and 19 clear/alpha-over,
zero errors, every label read out of the guest command stream rather than off a
render.

Control: of every bit of the first twelve header words, exactly one separates
those 35 without error. Nothing ties with it -- which is precisely what the
+0x08 = 0x8050 candidate failed.

Within-pair: ptbtn00 0x0110 alpha-over against ptbtn00f 0x0112 additive, same
screen, same bundle, adjacent draws, one bit apart. And other f variants are
bit-clear and alpha-over, so it is not 'focused variants are additive'.

Out of sample: a prediction committed at 088df91, before its capture, on a
different archive -- GP_OPTIONS entry 19, 3 additive of 16, falsified if any
other element drew additive. The game drew exactly po_menu_eff01/02/03 additive
and nothing else.

This revives a claim REFUTED.md killed. The refutation said blending those
sprites additively worsens every measure against the capture -- a claim about
our renderer, made while that renderer had a stale keyframe association, no leaf
geometry and no rotation. Recorded as a revival with its evidence;
check_refuted.py passes with 0 unmarked assertions.

Two other readings of the same bit stay refuted and are explicitly not revived:
'the name contains eff' and 'premultiplied storage'. Those were the bit's
meaning; this is its effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 07:20:12 +00:00
sylph-decoder
020f456974 re: retract 'the game batches elements that share a mode' -- its own log refutes it
I wrote that yesterday in HANDOFF, the finding page and INDEX. It is false: in
one main-menu frame, draws 5, 6 and 7 are three separate additive draws --
consecutive, identical blend state, not merged.

Only the one-way implication holds: elements inside one draw share a blend
state; sharing a state does not put elements in one draw. The wrong version
would have licensed the port inferring a mode for an element nobody observed,
which is the one thing that page must not do. The ptframe4 conclusion is
unaffected -- it is in the same DRAW as ptframe3, not merely the same mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:16:37 +00:00
sylph-decoder
0af81d7549 handoff: deliver ADDITIVE, and correct the 'any blend you choose is authored' instruction
HANDOFF gets the measured table, the two controls, the three caveats that travel
with it (src=ONE is not evidence of premultiplied textures; the frames share a
draw call so ptframe4 comes free; read it as per-element facts because the
selecting field is still unknown), and the correction to the port's own
sharpener -- 'neither frame has a fully-opaque pixel' is true and is not the
discriminator, because pteff10 has none either and renders accurately.

t32-blend-mode-not-on-disc.md keeps its negative and its reach and loses its
conclusion. INDEX gets the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 06:07:36 +00:00
sylph-decoder
0ea2b8e00d re: first audit of INDEX against the pages it links -- 3 of 8 rows were stale
INDEX is read every iteration and is the first thing a new reader meets, and I had
admitted three times to never auditing it. Found by accident: I was about to spend
a boot measuring whether A skips a movie because INDEX said 🟡, when movie-binding.md
had it settled since 2026-08-28 with a three-boot baseline and a delivery counter,
and HANDOFF carried it correctly. The staleness was in the index alone.

Three stale rows fixed: movie skippability, SE audio extractability, and B leaving
the main menu. Five other hits read and left alone -- index and page were talking
about different clauses.

One of the three should have been caught by check_refuted.py: REFUTED holds the
same dead claim with a different second clause, and the register matches exact
wording. Its docstring documents that weakness; this is the first live instance.

Also flags a within-page contradiction not fixed here: menu-audio-cues.md line 81
still heads a section saying SE audio is not extractable, which its own line 189
refutes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 23:44:01 +00:00
sylph-decoder
c5d685ba15 re: the sweep IS drawn on the JP title -- and my gate was phase-locking the shutter
The occlusion hypothesis is refuted: build 7 draws the same three ROT strips at
higher alpha than English, so there was never an absence to explain.

The 0.32-vs-11.9 tension that motivated it was an artefact of my own instrument.
Both JP captures were shuttered on the plate pulse, and the plate's pulse is part
of the animation -- so the gate synchronises the shutter to the animation's phase.
Measured at the shutter instant, the sweep sits 25-26 px apart across two runs in
different locales and different sessions: 1.6 % of a ~1600 px traverse.

So the 0.32 I recorded as between-session capture noise measures my trigger's
repeatability, and I read it as evidence the title is still when it is evidence
the gate works. The era adjudication is unaffected -- margin 16.72 clears even the
un-locked 11.9 -- and unaffected for the reason that file already gave: correlated
noise cancels in a margin.

Refutation attempt on sylpheed-port's positional-mechanism rejection: FAILED, the
claim stands. Its residual sits inside lit logos, and the logo ROI is
byte-identical across five differently-phased frames in two sessions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 19:52:15 +00:00
sylph-decoder
c364cde476 re: sweep the disc for the ordinal foot-gun -- GP_TITLE was the mildest case
Last iteration I retracted three claims because `--build 10/11` on GP_TITLE are
entries 12/15, and named the untested remainder in my own report: how much else
in the corpus used a build ordinal as an entry index. This is that sweep.

`screen --build N` indexes a predicate-filtered list, so every rejected entry
shifts every later ordinal. Disc-wide: 21 of 24 build-bearing archives diverge,
18 of them at ordinal 0 -- `--build 0` is entry 108 in each GP_MAIN_GAME_*2D,
24/26 in GP_HANGAR_ARSENAL/GP_READY_ROOM. GP_TITLE is the ONLY archive whose
first ten ordinals are the identity, which is the sole reason 207 of the
corpus's 226 build citations are safe. Second foot-gun: `--all` swaps the
predicate and renumbers 18 archives, so `--build N` and `--build N --all` are
not the same object.

The instrument failed its control first. A version using parse_build as the
predicate reported GP_TITLE as 16 builds, ordinal == entry throughout -- it
would have certified the exact bug it was built to find. The shipped version
uses the same predicates screen_builds() uses and reproduces `screen list` on
GP_TITLE exactly.

Audited all 226 citations. One real defect: a five-row table in
ui-keyframe-time-unit.md headed "declared element (build 11)" spans builds 10
and 11 -- palogo_sqex is in 10. All five placements re-verified and correct, so
the linear-ramp measurement is untouched; only the label was wrong. Fixed with a
per-row bundle column. GP_DIALOG --build 0 and GP_DEBRIEFING_PILOTLOG --build 10
re-run and reproduce.

Refutation attempted: sylpheed-port's corrected mid-ramp test rests on
ptlogo_all_eff holding a=127 from t=112 to t=246. Their quote is exact and it is
a plateau. The refutation fails; their correction stands.

METHOD already carried the rule I broke, and ui-splash-addressing already said
the splashes need --all. The failure was not missing knowledge -- it was
addressing a bundle by index without grepping for the index first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 13:20:41 +00:00
sylph-decoder
dc0b84c87f handoff+index: a .tbm draws, and screen render omits it silently
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 10:53:57 +00:00
sylph-decoder
3c004845fa re: the PRESS A plate PULSES -- measured, and it keeps an authored entry
Answers the port's ask #1, which it had flagged as the only one of its four
that could delete an authored entry rather than confirm one. It confirms one.

Held at the title with no input, the plate oscillates continuously: two windows
in one boot, 58 s and 57 s, ~23 cycles each, no decay. Periods 2.530 and 2.540 s
by upward mid-crossings -- 0.4 % apart.

It never goes off. The plate-absent floor is 159 green pixels, measured on the
committed live-title-build4-no-plate.png; the pulse bottoms at 714, 4.5x that.
So the port's 'flash and nothing after', reasoned from ptbtn00 expiring at
t=244, is wrong on the boot's end state -- ptbtn00f's 120-unit cycle is what
runs.

Instrument controls were run before it was pointed at anything unknown: the
glyph counter reproduces the documented 753 on live-title-press-a.png and 327 on
live-main-menu.png exactly.

Two estimators, and only one replicates. Mid-crossings agree across the two
windows to 0.4 %; a single-sinusoid least-squares fit does not (2.553 vs 2.413),
because the waveform is fast-rise/slow-decay rather than sinusoidal -- its own r2
of 0.468 and 0.228 is the tell. Both were controlled on synthetic sinusoids at
2.24/2.55/3.10 s laid on the ACTUAL timestamps and recovered every one exactly,
so neither is broken; one is misspecified. Recorded as such.

The wall-clock is 13 % longer than the corpus's earlier 2.24 s mean. Same
declared 120 units, different emulator pacing (x1.27 here against x1.12), so
this corroborates 'author the units' rather than disturbing it.

Reach stated: one boot; does not distinguish the boot title from an attract-loop
title; and the glyph count is a thresholded pixel count, so 714/1520 is not an
alpha ratio and no duty cycle can be read off it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 07:37:49 +00:00
sylph-decoder
e94e203a71 re: the A-press fault is SOLVED -- Xenia swallows input, the guest pump is unbounded
The 326 MB log from the failing run was still on disk, so this needed no
emulator time at all.

Mechanism: Xenia's XamInputGetKeystrokeEx returns X_ERROR_SUCCESS with a zeroed
keystroke on every call while a XAM dialog is up (xam_input.cc:197, upstream
Canary). The game's keystroke pump -- sub_82457038, read out of the image -- is
an unbounded 'while (GetKeystrokeEx() == SUCCESS) queue.push_back()'. It queued
8 388 608 empty keystrokes, grew its vector to 64 MB, asked for 128 MB, got a
failed allocation back unchecked, and copied off the top of the guest stack.

Two independent instruments agree to within 7: the Canary counter's last report
before the crash says 8 388 601 swallowed calls; the crash dump's r29 says the
vector held 8 388 608. The reporting granularity is 600.

Retracts this page's own 'r9 is a wild pointer above 4 GB'. Xenia prints
si_addr, a host address; the guest is mapped at 0x100000000, so the fault
address is guest 0x701D0000 -- which is exactly r9 in the register dump.

Also refutes nothing of the port's, but answers its ask #3: the two press-a
captures are different frames (40.84 % of the band's pixels differ at the
best alignment, which has a sharp minimum), so its 0.301 % is not an
instrument floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 06:44:49 +00:00
sylph-decoder
6d7bc87b0f re: diagnose the A-press fault -- a wild pointer, and a third failure mode
The blocker on all menu-side dynamic RE in this container, measured over four
runs and traced to an instruction.

A single A press on the title produces a Xenia CRASH DUMP with PC 0x824578A0 and
"Access Violation: write at 0x00000001701D0000", repeating 32 356 times and
writing 326 MB of register dump in about ten seconds. Four A runs faulted; four
no-input runs in the same sessions completed.

REFUTED, my own hypothesis: it is not an unimplemented instruction. The config
carries break_on_unimplemented_instructions = true and Xenia's own message reads
"to skip, disable break_on_unimplemented_instructions", so the flag looked like
the fix. Booting with it false faults identically, and no "Unimplemented instr"
line is ever logged on stdout or stderr in any run -- and since that path emits
its XELOGE BEFORE the guarded DebugBreak, the absence rules the mechanism out
rather than leaving it open. The dump comes from Emulator::ExceptionCallback, a
genuine guest exception.

The instruction, read from the image rather than the database: b0c90000 is
sth r6, 0(r9), the first of four halfword stores at offsets 0/2/4/6 through r9
inside a bne- loop -- code filling an array of 8-byte records with four u16
fields each. So r9 is a wild pointer, and 0x1701D0000 is above 4 GB, outside the
guest's 32-bit address space entirely: not a null dereference and not a small
overrun, but a base that was never a guest address. The database agrees on the
containing function, sub_82457780 at +0x120.

It is a THIRD failure mode. Not the cache-flush crash at 0x82307128, and not the
loader stall documented in canary-scripted-input-traps.md, which logs ZERO crash
dumps. Unlike the stall it reproduced 4 of 4, so that page's "retry whole boots"
does not obviously apply. It does not explain how Q4 and Q5 pressed A
successfully; what differs between those runs and these has not been found.

frame_clock.sh's 300 MB guard killed the run as designed -- the session log's
"EMULATOR GONE at 56s" is the guard, not the crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 22:17:45 +00:00
sylph-decoder
29c99e1b3e re: primitive blend mode -- undecodable with reach, and no longer a risk
Looked in four places. The bundle has no field: a primitive has no RATC child at
all and the declaration words are constant across every element of three measured
screens -- the two grounds ui-prm-primitives.md already used to refute a
bundle-side LAYER key, and they apply identically to blend.

The colour census: every full-screen *eff00* primitive on the disc carries pure
black at its various alphas, and the only non-black primitive anywhere is
pbafc.prm, RGB 00e8e0 cyan.

The occlusion constraint cannot reach that one. pbafc.prm looked alarming at a
declared 844x600 and alpha ff; it is a small moving glint. It strobes between
alpha 255 and 124 every 2 units, travels from x=178 to x=291, and is scaled
2%x3%, so it draws about 17x18 pixels. At that size it occludes essentially
nothing.

The oracle is unavailable: GP_READY_ROOM is a recorded no-go and gameplay needs
the A press that faults the guest in this container.

But the consequence closes even though the question does not. For a BLACK quad
-- which is every primitive forced_backdrop touches -- the hypotheses differ only
in whether it hides what is beneath. Drawn first it is correct under both; drawn
last it is correct only under additive. So the rule's verdict is robust to the
open question, and the port's original "layerless sorts last" was wrong under
alpha-over and merely pointless under additive. This is explicitly NOT evidence
for alpha-over.

The investigation also found forced_backdrop judging coverage from the pivot
alone, ignoring scale -- pbafc.prm is the disc's own proof that a nominally
844x600 element can draw at 2%. Checked before changing anything: all 80 forced
instances are at scale 100% on every opaque instant, so no verdict moved. The
guard now requires scale >= 100 at the instants it counts as opaque. Defensive,
not a fix. 4 + 13 disc tests green either side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 21:58:25 +00:00
sylph-decoder
1f6e07598e re: the primitive colour census -- and it refutes 38 of my own 80 forced verdicts
A disc-wide census of the ARGB that keyless elements carry.

Every full-screen *eff00* PRIMITIVE is pure black at its various alphas
(ff000000, 7f000000, 40000000, b2000000, cc000000, d4000000, 00000000). Black at
alpha a over content is exactly an alpha-over dim or fade, and an additive black
quad would be a no-op nobody would author -- so this narrows the open blend
question a long way. The only non-black primitive on the disc is pbafc.prm, RGB
00e8e0 cyan at alphas up to ff, and it is 844x600, NOT full-screen, so it sits
outside forced_backdrop's geometry guard. It is now the sole additive candidate.

The census also refutes my own argument for nearly half its verdicts. Of the 80
forced-first instances only 42 are .prm; 38 are .tbm carrying fade ffffffff. A
SOLID white quad at alpha 255 painted first would make the screen white, and no
screen is white -- so a .tbm is a white modulation on a texture, and element
alpha does not establish its coverage.

That is the .t32 error one file extension further out. I guarded that with
el.sprite.is_some(), which fixed the symptom and not the cause: an element's
alpha is not its texture's opacity, and only an untextured primitive makes the
two the same fact.

So 42 verdicts stay decoded and 38 drop to inferred -- still almost certainly
right, since all are named *base*, all are full-screen, and pfbase.tbm's first
position is measured in the running game, but that is a name-and-role argument
which this page elsewhere calls the weaker kind.

The code is deliberately unchanged. Restricting forced_backdrop to .prm would
send eleven screens' backgrounds back to u32::MAX -- last -- which is the
blank-screen bug the rule was written to fix. Downgrading the status is honest;
reverting the position would be wrong. The 42/38 split is pinned by a test so
anyone tightening the rule sees what it costs.

Separately, on the port's black_hold_units ask: four more no-input boots yielded
one usable log, which armed late and missed the publisher splash, so the sample
is still two runs spanning 3 and 4 frames. Their 6.5-9.2 range stands. And a
reason it may not be resolvable this way: the draw log DROPS frame numbers -- in
the 3-frame run, frames 121 and 124 are absent entirely, so "frames with no
sprite" and "span of frame numbers" are different quantities.

Their statistical correction is taken: at n=3 the sample SD (3.893) is the
estimator, not the population SD (3.179), making my run 1.88 sigma from the
corpus mean rather than 2.31.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 21:51:06 +00:00
sylph-decoder
acaacc36e0 re: the boot splash dwells are declared -- and wall clock is the wrong unit
The port asked for two wall-clock timestamps across the boot splashes. Measured,
and the measurement's own result is that timestamps are not the invariant.

The dwells are the bundles' own declared timelines: publisher t=0..255 = 4.250 s
at 60 units/s, developer t=0..210 = 3.500 s. The corpus's independent screenshot
timing over three cold boots gives 4.30/4.60/4.37 and 3.51/3.50/3.37 -- the
developer agreeing to 1.1%, two of its three runs to 0.3%.

A fresh no-input boot with a frame->wall-clock map puts the same two dwells at
5.10-5.61 s and 3.83-4.30 s, 15-20% longer than both the declared values and the
corpus's runs, on the same disc and the same declared timeline. So the
wall-clock dwell is an emulator-pacing artefact that varies run to run, and a
port authoring seconds is authoring one run's pacing.

Boundaries from the draw stream, read per quad: publisher glow frame 1, wordmark
6-119, three frames with NO sprite drawn, developer glows 123, wordmarks
140-209, intro video 216. The 3-frame gap replicates the earlier 4-frame
measurement within the +-1 both are quantised to.

New tool frame_clock.sh, and its limitation found by its own control: it
resolves to one BUFFER FLUSH, not one frame. The capture writes through a C++
ofstream, so tail sees the log in bursts -- 69 of 125 samples showed no advance
and the rest jumped 7-15 frames. Naive interpolation inside a burst made the
apparent rate swing between 0.0164 and 0.0316 s/frame, which is the flush and
not the guest. Frames 119 and 123 fall in one burst, so the inter-splash gap is
not separable by this clock at all. Everything is quoted as brackets and the
point estimates were withdrawn before being reported.

palogo_anima never appears in the log and is NOT reported as undrawn: the
developer bundle batches 7 elements into one draw and only the first two quads
are logged. That is the trap that produced the eff3 false negative, so it is
named rather than claimed.

Also records the port's correction: ptcopyright has 105 instants with alpha >= 1
(t=139..243) against 105.89 units of span; I had quoted the rounded span.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 21:29:06 +00:00
sylph-decoder
78328c5022 re: the top-level clock freezes at the settle point -- closing the 114-vs-120 gap
Measured in the title draw capture, re-read with the per-quad parser.

GP_TITLE build 4 declares t = 0..269, about 120 presented frames at this run's
pacing. The title dwell lasted ~1100. ptcopyright declares alpha >= 1 for 106
units (t=138..244) and is drawn for 1050 frames; ptlogo1 declares an exit at
t=264 and is drawn for 1095. Both vanish within three frames of the dwell
ending.

So the top-level clock advances through the build-in, stops inside the settle
window [160,236], and holds. The exit ramp is not played on a timer -- it plays
when something makes the screen leave. That is ui-settle-time.md's decode seen
from the other side and observed in the running game rather than inferred from
the file. A nested record keeps looping on its own clock throughout.

This closes the 114-vs-120 gap, and it was my arithmetic rather than a
discrepancy in the decode. The 2.231 units/frame was regressed over BUILD-IN
events -- the only stretch in which the top-level clock advances -- and applied
to a period measured over the settled dwell, where that clock is frozen and
only the plate's own record is running. Two different clocks. The declared 120
was never in doubt from the calibration-free dark-fraction test.

The 51.158-frame period is now confirmed by a second independent estimator:
autocorrelation returns lag 51 with clean harmonics at 102 and 154. Its FIRST
version failed its control, returning 48 for a period known to be 51.158,
because it indexed by sample position where the log's frame numbers have gaps.
Recorded, because the failure is the reason the second version can be trusted.

Not settled: the sweeps' period. The same validated estimator disagrees between
two dwells of one screen -- 515 vs 452 frames for the same family -- and a 14%
disagreement within one screen is not a period. The +0x08 field cannot settle
it either, since ptloop01/ptloop02 have zero slack.

Blocker recorded in CONTAINER-NOTES: a single A press on the title faults the
guest. Three menu-capture attempts, two ending in register dumps of 223 MB and
519 MB, against three no-input runs in the same session that all completed. And
a guest fault writes an UNBOUNDED register dump to stdout on a filesystem at
91%, so any scripted button press needs a size guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 21:21:27 +00:00
sylph-decoder
cf91ad1dcc re: the forced-backdrop span -- 256 vs 211 is a bundle mismatch, and the hold
decides 55% of verdicts

The port implemented the forced-backdrop rule and reported a discrepancy:
palogo_eff0.prm at 256 opaque instants against this corpus's 211.

There is no discrepancy. palogo_eff0.prm appears on BOTH splashes -- the
publisher (entries 10, 13) runs to t=255, giving 256 instants; the developer
(11, 14) runs to t=210, giving 211. Same definition, different bundle. The page
now names the entries so it cannot recur.

The definition, stated: the span is 0..=max keyframe time over every element in
the build, and an element HOLDS its final pose past its own last keyframe --
which is what pose_at does, and which is decoded rather than assumed (a group
holds at its last keyframe rather than looping; the declared +0x08 never falls
short of the last keyframe, the slack being that hold).

The port's instinct that the hold was load-bearing was right. Over the 130
keyless full-screen primitives with an opaque interval:

  * span = the header's declared +0x08          ->  0 verdicts change
  * span = the primitive's own last keyframe    -> 72 change
  * elements GONE after their last keyframe     -> 72 change

So the hold decides 55% of verdicts -- and dropping it is REFUTED by a measured
order. palogo_eff0.prm is a single keyframe at t=0: without the hold it is
opaque for one instant, no other element is up yet, and the rule calls it free,
against a game measured painting it first. Pinned by a new test that spells out
the counterfactual rather than importing it.

The verdicts that matter are convention-independent: pgloading_eff00.prm is
FIRST under all four conventions and pteff00.prm FREE under all four. And the
header length is interchangeable with the elements' maximum -- zero
disagreements disc-wide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 21:14:58 +00:00
sylph-decoder
cfcda5501c formats: a keyless primitive that would hide the screen is forced to paint first
Partly closes ui-prm-primitives.md's standing blocker, "where an UNMEASURED
primitive paints". Raised by the port: build_12/build_15 composite to solid
black at every instant of their declared life, because pgloading_eff00.prm -- a
full-screen opaque quad -- sorts last.

The rule is a constraint read off the file, not a preference: an element that
covers the screen and is fully opaque at some instant cannot paint above
anything visible at that instant. Where the elements visible during its opaque
span are ALL of them, its position is forced to first.

pgloading_eff00.prm is opaque for 39 instants and all 9 other elements are
visible inside that span -> forced first, 4/4 instances.

Two controls, both measured orders from the running game, and the rule has to
survive both:

  * palogo_eff0.prm is measured painting FIRST -- opaque 211 instants, forced
    below 6 of 6. It is NAMED like an overlay, so a name-based rule sorts it
    wrong against a measured order. Occlusion gets it right.
  * pteff00.prm is measured painting LAST -- opaque for 2 instants at its
    screen's entry and exit, forced below only 3 of 23, so the constraint
    permits it on top where it belongs.

Disc-wide: 80 instances forced first, 50 constrained but not forced, 0
unconstrained. The split runs almost exactly along the names -- every *base* is
forced, every *eff00* is not -- with three families crossing it, which is
exactly why the name is not the rule.

It also explains 36 builds the corpus had recorded as "coming out one colour"
with no cause: pzeff00.prm is forced first in 32 of 32 instances, so they were
wiped by our own sort rather than by the game.

The rule's real limit was found by its own disc-wide test failing. Applied to
any element it claimed 22 .t32 SPRITES must sort first against their own layer
keys -- pneff01.t32 (key 0xd850, #8 of 13) and pbfriendly.t32 (0x9230, #17 of
49). A sprite's ELEMENT alpha says nothing about whether its TEXTURE covers the
screen, so forced_backdrop is now restricted to untextured primitives, which is
also the only case derived_paint_order consults it for.

Reach stated: assumes straight alpha-over (blend mode is still open, and an
additive quad at alpha 255 would not occlude); it is a lower bound, not an
ordering; and there is no new oracle measurement -- both controls are prior
ones, and a loading screen is not reachable from the title path.

3 new disc tests; the 13 paint-order tests are green, including
the_derived_order_matches_the_measured_ones_up_to_ties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 21:02:17 +00:00
sylph-decoder
47faeaa7a9 re: RETRACT "the game never draws eff3" -- a batched draw merged two quads
The console draws all five title flashes. My claim that ptlogo_back2eff3 is
never drawn was an instrument artefact, and I had reported it to the port with
three alternative explanations "ruled out".

A GPU draw can batch several quads -- indices=4 is one, indices=8 two,
indices=24 six -- and the UI draw log dumps only the first 8 vertices. Taking
min/max over a line's whole vertex list merges quads into one box.

eff3 is batched with eff4, and because the wipe family is right-aligned, eff3
(788..1196) lies ENTIRELY INSIDE eff4 (447..1196). The union is exactly eff4's
own extent, so the merged box matched eff4 to 1 px, eff3 vanished, and nothing
looked wrong.

Parsed per quad, all five fire in both title entries in the declared stagger:
eff1 130-131, eff2 133, eff3 133-134, eff4 133-135, eff/eff5 134+, back2 136+;
and 5953-5955 / 5955-5957 / 5957-5958 / 5957-5959 / 5958+ / 5962+ in entry 2.
Frames 133 and 134 are t=60.1 and 62.3, inside eff3's declared t in (58,64).

Also retracts "the developer splash is one composited quad" -- the same bug,
which the port refuted by arithmetic first (a 259-tall box cannot contain three
logos spanning y 164..585). It draws three logos and three glows as separate
quads in one indices=24 call; the 525x259 was gamearts_eff merged with
seta_eff. The 9-unit black hold is unaffected: those glows are the developer
splash's first draw.

The three "ruled out" explanations were all aimed at the wrong failure. In
particular the invisible-draw check counted draws with NO geometry line, when
the hiding place was draws with PARTIAL geometry. Refuting three wrong
hypotheses is not evidence for a fourth, and a list of failure modes written by
whoever built the instrument is the least likely to contain its blind spot.
Recorded in METHOD.md, along with the tell that was present and explained away:
a merged box carries the first quad's colour, which made one element's alpha
read 255/127/254 on consecutive frames.

New tool: tools/re-capture/quads_per_frame.py parses vertices in groups of four
and warns when the logged quad count falls short of indices/4.

Also guards a double-A-tap in ui_draw_capture.sh: the movie branch ignored that
TARGET=menu had already tapped, so a run tapped A on the title at t=23s and
again at t=27s on the transition; the guest faulted and Xenia dumped registers
to stdout until the file reached 519 MB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 20:49:27 +00:00
sylph-decoder
1a2f4fdc2d re: the boot splash black gap is ~9 units, measured in draws not luminance
The port found its boot had no black frame between the publisher and developer
splashes and authored 12 units by analogy with the menus' transition quad. On
the boot path that analogy has nothing behind it -- palogo_eff0.prm is a single
static keyframe, so the splash bundles declare no fade quad.

I had agreed with the dismissal that hid the defect: told the residual was
0.03 s against a bound built from two measured ranges plus jitter slack, I said
it said more about the bound than the game. The real gap was 0.2 s.

Measured in the draw stream, which separates true black from a fade tail where
luminance cannot: palogo_sqex is drawn to frame 125 at alpha 7, frames 126-129
submit NO sprite quad at all, and the developer fades in at frame 130 from
alpha 34. Four presented frames, the only such run in the sequence.

Converted with the disc as its own clock rather than a frame rate -- this run
presented at 13.1 fps against 28 elsewhere -- palogo_sqex declares alpha >= 1
for 239.8 units and is drawn in 105 frames, giving 2.284 units per presented
frame, which the title capture independently corroborates at 2.231. So the gap
is ~9.1 units (0.152 s), against the 12 authored; +-1 frame is 6.9-11.4. And
the true black is SHORTER, since both boundary frames still carry picture.

Second finding: the developer splash is ONE composited 525x259 quad at the
bounding box of its three declared logos, none of whose individual sizes is
ever submitted. That is why an earlier pass reported "developer splash: 0
frames".

Declaration sites ruled out: the splash bundles (no fade quad) and the
top-level +0x08 (a family constant, 300/60, slack 12-226 units). The
executable is NOT looked at and is named as the next place rather than
claimed.

Also answers the port's sweep question: +0x08 canNOT settle it, because
ptloop01/ptloop02 have zero slack and a zero-slack record cannot distinguish
"loops" from "runs once and stops". The oracle settles it for the TITLE -- the
sweep oscillates over its whole range and resets hard to the same start, once
in dwell 1 and twice in dwell 2, so it does not park. The MENU is unmeasured
and stays open.

Tooling: GRACE and NOTAP knobs for ui_draw_capture.sh. The script taps A on
"the screen changed a lot", which is also true of a fading splash -- a first
run tapped through the publisher and the developer never appeared. The
instrument was perturbing what it measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 20:36:15 +00:00
sylph-decoder
69bc4cd1cd re: the game never draws ptlogo_back2eff3 -- reproduced, with three explanations
ruled out

Last iteration I recorded eff3's absence as unexplained after withdrawing a bad
explanation for it. The previous capture survived on disk with 6907 frames, and
the attract loop returns to the title, so it contains a SECOND build-in at
frames 5942..7025. eff3 is absent there too.

Three alternative explanations tested and failed:

  * sampling phase -- eff3 is non-zero for t in (58,64), SIX units, against a
    2.23-unit step. A window wider than the step cannot be missed; frames 133
    (t=60.1) and 134 (t=62.3) sit inside it and draw eff2 and eff4 instead.
  * a draw the log cannot see -- exactly 2 draws per frame carry no geometry,
    on all 932 settled title frames, always the same full-screen-triangle
    shader, and present on frames where no wipe element is active.
  * a bad position guess -- dropping position entirely, ZERO quads anywhere on
    screen in either build-in window have a width within +-30 of 408. The width
    spectrum jumps straight from 262 to 748.

Draw counts across both entries: eff1 4, eff2 3, eff3 0, eff4 6, against ~5
expected each. The four are a right-aligned wipe (938+258, 788+408, 447+749,
64+1133, all ending at x~1196) -- a left-growing reveal in four widths, of
which the game draws three.

Why is NOT established: nothing in eff3's element record differs from its
neighbours. Classified measured, not decoded, and the port is told that
dropping eff3 means authoring a behaviour I cannot derive from the file.

Two further corrections, both to my own earlier claims:

  * "frame 107 is the title composited once" was an over-read. It binds NO
    texture and only 4 of its 27 draws log geometry. The second title entry
    has no such frame.
  * the two build-ins are NOT frame-identical. I had that from a coincidentally
    aligned pair of rows; aligned properly only 4 of 46 frames match. They are
    the same animation at different sampling phases -- which is precisely what
    makes the eff3 result robust.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 20:19:04 +00:00
sylph-decoder
311bd16ad8 re: withdraw the eff3 explanation and the flash advice; add a calibration-free
test that refutes 105

Two claims shipped this morning are withdrawn, and the port had already acted
on one of them.

WITHDRAWN 1: "eff3 was never drawn because a 2-unit flash peak is sub-frame."
eff3's alpha is non-zero for t in (58,64), and the capture's frames 133 and 134
sit at t = 60.0 and 62.2 -- squarely inside that window, with eff2 and eff4
both drawn in the same frames. It should have been submitted and was not. The
absence is real and UNEXPLAINED; it is not sampling phase.

WITHDRAWN 2: "a port drawing all five flashes shows more sweep than the
console". No evidence behind it. The port checked against its own renderer and
found it draws them sequentially at their declared times, never more than two
at once -- which is exactly what frames 131-135 show the game doing. The
pile-up worth warning about was the rest() bug, now fixed.

Kept, at the port's request: a frame-by-frame comparison of the build-in WILL
disagree about which flash lands in which frame -- 2 units per submitted frame
against this run's 2.231 units per presented frame -- and neither side is
wrong. Without that stated, the discrepancy reads as a port defect.

Added, and stronger than the argument it replaces: a calibration-free test of
105 vs 120. The glow's draw is omitted when its alpha reaches zero, and the
smallest alpha actually submitted across 807 drawn frames is 1, so the culling
threshold is read off the data rather than assumed. Measured dark fraction
17.7% (173 of 980 settled frames); a 120-unit cycle with its declared 15-unit
hold predicts 14.4%; a 105-unit cycle predicts 2.2%. 105 is out by 8x and
would need a threshold of alpha 11 out of a peak of 80, while the capture
contains submitted draws at alpha 1..12. No frame rate, no pacing factor, no
wall clock.

Also recorded: a regression of five build-in events against their declared
times (residuals <=0.9 frames) recovers t=0 at frame 106.1 when the composite
spike, not in the fit, is frame 107 -- and that same slope makes the glow's
period imply a 114-unit cycle against a declared 120, which is unexplained.
And the vertex-alpha identity holds for the glow but does NOT generalise:
eff4 reads 255/127/254 on consecutive frames.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 20:03:48 +00:00
sylph-decoder
d98c8214cc re: the title's build-in measured in the guest's draw stream -- the flashes are real
The settle-time decode was confirmed only against a SETTLED frame, which shows
the end state is right and says nothing about whether the five flashes ever
happen. This runs the oracle: a draw capture armed before the title exists,
so the window contains the frames in which the screen is built.

The flashes fire in a six-frame window and are absent from all 155 other
sampled frames. `ptlogo_back2eff1` is drawn in exactly two frames at t = 54.0
against a decoded peak of t54-56; `ptlogo1` first appears at t = 42.2 against
a decoded t42. Units-per-frame was taken from the GLOW's period alone, a
different element, so the timings are not circular. The two holders are
continuous from frame 134.

The plate glow's quad carries a per-vertex colour whose alpha IS the element's
fade alpha, so the ramp is read straight out of the guest: observed range
0..80 against a decoded peak of 80, exact and unfitted; period 51.158
presented frames over 20 cycle starts. Fitting the decoded ramp gives RMS
13.16 alpha levels against 38.18 for the same ramp REVERSED -- if the shape
carried no information those would be equal, so the asymmetry is real and
correctly directed. Further controls: symmetric triangle 15.73, flat 31.13.

`ptlogo_back2eff3` was never drawn, and that is expected rather than a miss: a
2-unit flash peak is 0.85 of a presented frame, so catching one is a matter of
phase. A port drawing all five every time shows more sweep than the console.

METHOD.md gains the trap this cost: a 2D draw's identity is its vertex
geometry, not its bound texture. These sprites sample shared pages, and
matching texture dimensions produced a false negative (no flash is ever drawn)
and a false positive (the intro movie's 640x360 YUV planes read as `ptbase2`)
in the same pass.

Also records the top-level restriction on the settle window, which the port
raised and which is verified here: top-level [160,236] width 76, including the
`ptloop` leaves [269,540] width 271 -- an instant past the end of every
top-level element's timeline.

Evidence committed as a derived per-frame series, not the 7 MB raw log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 19:56:34 +00:00
sylph-decoder
4c74e579a0 re: 210 focus records pulse, not 2 -- and 8 of them fail in a way that looks right
The port censused focus-record alpha over its own export -- 34 elements, 2
varying, both `ptbtn00f` -- and concluded there is nothing to fix. That is
correct and correctly scoped. This asks the same question of the whole disc.

1 130 focus records, 2 664 timed elements, 210 with a varying alpha. 202 have
`rest()` returning the PEAK, the `ui-settle-time` pathology. By pak:
PILOTLOG 116, MOVIE_THEATER 54, HANGAR_ARSENAL 30, LEADERBOARD 8, GP_TITLE 2.

So the port's 2 is right because GP_TITLE has 2. The scope was load-bearing
and was not stated as a limit -- "only 2 have a varying alpha" reads as a fact
about the format and is a fact about one pak. The pathology is concentrated in
exactly the screens a wider port reaches next.

The 8 LEADERBOARD ones are the worse mode. `py_ranking_btn01f` swings
255->127->255 with no two adjacent keyframes equal, so `rest()` falls through
to its longest-dwell rule and returns 244 -- neither the peak nor the trough.
A glow stuck at its peak is visibly wrong; one stuck at 244 of a 127..255
range looks entirely plausible and nothing reports it.

Verified rather than asserted: two hits dumped keyframe by keyframe, and a
control on `ptbtn01f`, which is genuinely constant across its cycle and is
correctly NOT flagged. `py_ranking_btn01f` also confirms the loop-length
decode independently -- its ramp ends at t=90 inside a declared 120-unit
cycle, holding bright for 30 units.

Reach stated: 210 is a floor. Focus records are matched by the `Xf.rat` name
rule, and elements with constant alpha but varying scale, rotation or
position have the same problem and are not counted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 19:45:17 +00:00
sylph-decoder
d4a08ad194 re: a nested record's +0x08 is its loop length -- the plate's period is 120, not 105
Answers the question the port agent asked: does the `PRESS A` plate's pulse
group loop from its start, or hold at alpha 0 between cycles? It holds.

A nested record is itself a RATC bundle with its own header, and that header's
`+0x08` is the loop length -- the same field ui_header_time_disc already tests
as an animation length at the top level. Its keyframes need not fill it, and
the slack is a hold at the final pose. `ptbtn00f` is 105 units of ramp inside
a 120-unit cycle, so the glow rests dark for 15 units between pulses. The five
main-menu focus records fill their 120 exactly, which is what shows the slack
belongs to this record rather than to the format.

Disc-wide over 1 781 timed nested records: 92.3% declare exactly their last
keyframe time, 7.7% declare more, and 0 declare less. That last row is the
falsifier -- a cycle cannot restart before its own last pose -- and it never
fires; the 7.7% is what keeps the reading from being an unfalsifiable
relabelling of the keyframes.

Falsification against the running game, using a pacing factor measured
INDEPENDENTLY on the main menu's focus ring (declared 120 units, measured
2.177 s, factor 1.0885): to reach the corpus's four measurements of the plate
pulse (2.12/2.19/2.34/2.31 s), a 105-unit period needs a factor of 1.211-1.337,
which EXCLUDES the ring's; a 120-unit period needs 1.060-1.170, which CONTAINS
it. Predicted 2.177 s against a measured 2.12-2.34. The two elements are in
different bundles and were measured in separate runs; the only thing tying
them together is that both declare 120.

So the port should stop shipping 105. Its 123-vs-129 ambiguity straddled the
right answer without containing it, and 129 only fitted because it was
105 + the exit_ramp_units constant it has since correctly deleted.

Reach is stated: this says where a cycle ends, not which records cycle, and
the TOP-level +0x08 is a different field left untouched -- every GP_TITLE
entry declares 300 while its elements end at 244-269.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 19:40:23 +00:00
sylph-decoder
7b5a4aa041 re: the paint-order tie-break costs one pixel, on one screen we do not ship
Closes the open half of Q3. `ui-paint-order-derived-check.md` bounded WHERE a
wrong tie-break could show -- overlapping same-key pairs -- and said outright
that nobody had measured how many change a pixel.

At the instant the player sees, the answer is: at most 1 px at max channel
difference 1, on the JAPANESE title only (`ptlogo2` x `ptlogo_tm`, 5 px of
shared ink). Exactly 0 px on all five port screens.

The earlier 24-pair bound was counted at `rest()`, and 10 of the title's 11
overlapping tied pairs are between `ptlogo_back2eff1`..`eff5` -- the five
transient flashes from the settle-time finding, transparent on the settled
screen. A tie between two invisible elements cannot cost a pixel.

Not a knife-edge. Sweeping every keyframe time and every midpoint between
keyframe times, the live-pair count is flat across the ENTIRE settle window:
1 on the EN title, 2 on the JP title, 0 on all four loading bundles -- whose
tie is live only at t17..t33, during the build-in, which matters because
their settle windows are narrow enough to deserve little trust otherwise.

Controls: every entry reporting zero also swaps an overlapping DIFFERENT-key
pair, which must and does move pixels (25 310 / 268 698 / ~765 000 px). Zeros
are explained by shared-ink counts rather than asserted -- the `ptframe` pairs
overlap by bounding box and share 0 px of ink. Entries 0/1/12/15 have NO live
control and their zeros rest on keyframe data rather than a render; recorded
as the weaker claim it is.

Refutation attempt on the corpus's "24 overlapping pairs": it SURVIVES as a
rest-pose count -- an independent recount reproduces entry 7's 16 exactly.
What is overturned is its interpretation as the risk surface.

`tie_break_pixel_cost` gains a settle-time case and an alpha/scale filter on
its rect test; `tie_cost_over_time` is new. Also strips 611 bytes of captured
cargo warnings from the head of the committed tie census.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 19:35:29 +00:00
sylph-decoder
38b80302b7 re: the title's light arc is five transient flashes, not a tone or rotation error
Records the settle-time decode, and corrects two claims it overturns.

docs/re/structures/ui-settle-time.md is the finding: a settled screen is one
instant every element is posed at, the disc names it (the midpoint of the
longest keyframe-free interval), and the title's arc closes from 33.22 to
11.79 with the clipped-pixel count landing on the console's 1459 within 0.5%.
Includes the disc-wide reach -- only 30% of bundles have a window wide enough
to trust -- and a three-way figure.

Withdrawn in ui-rotation-implemented.md:

  * "Flat. No minimum." was not a property of rotation. `at` posed LEAVES ONLY,
    so the scan moved the light sweeps and never touched the top-level flashes.
  * "our renderer does not draw ptlogo1/ptlogo2 at all" is wrong. Both are
    drawn; the four elements the diagnostic named are kind-0x4 ghosts sharing
    their template's name. Hiding the real ones makes the error WORSE by
    +5.20 and +7.47.
  * Its 10.92 baseline is not reproducible -- 14.07 at its own pre-change tag
    and 14.07 today -- so the "1.7% better" verdict rests on an unrecorded
    recipe. Flagged in title-residual-tone-vs-geometry.md too.

METHOD.md gains two traps: a shared CARGO_TARGET_DIR makes a `git worktree`
build silently replace the binary you run next (it cost three renders here,
caught only because a missing flag was a hard error rather than a wrong
number); and an aggregate computed per-element is not a state of the system.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 19:29:12 +00:00
sylph-decoder
d71a74f938 re: index and cross-references for the record layout and the loading screens 2026-08-29 14:06:10 +00:00
sylph-decoder
f24304248c re: the plate delay was on the disc all along -- my instruction is refuted
The port caught this with arithmetic off the disc, and it was right: build 2 has
a keyframe group of its own, so "when build 4 has settled, wait 2.13 s, then
composite build 2" puts the plate at settle + 2.13 + 3.97 s. Confirmed build 2's
group here independently of their message: `ptbtn00.t32` reaches a=255 at t=238.

The reconciliation needs no free parameter. Both builds run on ONE clock, started
together, and the premise that fails is `rest.t`:

  rest.t is NOT when a screen settles. It is the last HOLD keyframe before the
  exit. ptlogo1 rests at t=251 and stops moving at t=42.

The title's visible build-in ends at t=118, where pteff01, pteff02.prm and
ptlogoall_eff end their ramps together. 238 - 118 = 120 units = 2.000 s, against
a measured 2.138 and 2.132. So the interval the two runs agreed on to 6 ms was a
DECLARED one and I handed over a wall-clock reading of it.

That reading is 6.7% long, and the corpus already knew why: 120 units in 2.135 s
is the game presenting at 28.06 / 28.14 fps against a nominal 30, and the idle
title was independently measured at 28.5 fps before these runs. Corroborated from
inside the same two runs -- first pixels -> settle is 1.643 s and 2.131 s, a 30%
spread, while settle -> plate is 2.138 and 2.132. Frames are dropped during the
build-in, not during the hold, which a change in the game's own timing could not
do.

So the port authors nothing here. What is unchanged: ScreenView still has to draw
two builds at once and the boot's end state is still not plate-free.

Not settled, and said so on the page: which reading of the keyframe times is
right (it moves the plate by 2 units and I cannot separate them from these
traces), and my settle landmark to better than +/-5 units.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 12:43:41 +00:00
sylph-decoder
f34d24941d formats: a music bank's third sub-wave was its own header
The port hit `sound_bank_riffs("BGM_103.slb")` returning three against a census
that says two, and refused to guess which to drop. It was our reader.

`to_xma_riffs`'s hybrid branch derives a leading packet stream's start as
`first_riff % XMA1_PACKET`. That is right only when the bank header is smaller
than one 2048-byte packet -- true of the voice banks the branch was written for
(1392/1468/1600/1728), false of a music bank, whose header is exactly five
packets. The modulus returned 0 and the whole 10 240-byte header was emitted as
sub-wave 0.

The header states its own length, so the guard needs no threshold: BE u32 0x800
at +0x18 with the bank id repeated at +0x00 and +0x20, header length in blocks at
+0x24. Disc-wide over sound.pak's 9 519 entries, 28 match at offset 0 -- every
music bank, ids 1001-1023 and 1101-1105 -- and on 28/28 the declared header ends
EXACTLY at the first RIFF. Zero have a gap, so a header and a leading packet
stream never coexist here; zero false positives among the other 9 491.

Controlled rather than argued: decoding the emitted region through the same
chain, on the same bank, in the same run gives 0.009 s of PCM where the bank's
real wave 0 gives 87.744 s against a declared 87.75. The region is also 99.1%
zero bytes. And the oracle had already said two -- the XMA probe at the main menu
saw exactly two streams, at BGM_103's two declared wave sizes.

BGM_106-109 are deliberately NOT in the 28: their entries start mid-bank, so they
have no header at offset 0 and their leading region is real audio. The
VOICE_D_453 recovery is untouched and its tests still pass, 10/10 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 12:30:16 +00:00
sylph-decoder
5fcc89be55 re: the PRESS-A plate arrives 2.13 s after the title settles, measured twice
The one number the port said decides a structural question on its side: whether
`ScreenView` has to draw two builds at once. It does. The boot title shows
build 4 alone, and 2.13 s after build 4 stops animating it composites build 2
over it. Two independent boots agree to 6 ms (2.138 / 2.132), which is under one
sample interval.

Measure from SETTLED, not from first pixels. "First drawn -> plate" is 3.78 s in
one run and 4.26 s in the other, because the build-in animation itself ran 1.64 s
and 2.13 s -- that spread is the emulator's frame pacing, and it is exactly the
kind of number that looks like a measurement.

Ruled out before believing it: that the plate was pulsing all along, too dim for
a thresholded glyph counter. The counter reads EXACTLY 154 -- the committed
no-plate title's own value -- for every frame of a plateau nearly one full pulse
period long, with zero variation, and the surface mean is flat to +/-0.03 across
it. A cycling overlay moves both.

Also settled, and also not:

  * the black hold between two screens is 0.14-0.30 s, which brackets the port's
    authored 0.17-0.23 s and the file's declared 12 units. Their constant stands.
  * the (A)->menu latency is STILL not available, and now the reason is known.
    Both runs freeze one frame for ~1.4 s at surface mean 26.626 -- agreeing
    between runs to six decimals, and reproduced in run 2 with stream restarts
    disabled, so it is not the capture path. It is a guest load stall: the (B)
    path, which loads nothing, has no freeze at all. Any figure from it would be
    an emulator load time.

Refutation attempt, recorded whether or not it survived: navigation.md's "the
title is not input-ready for about ten seconds, and even then (A) registers
roughly half the time". At 7.29 s and 7.28 s after the title settled, (A) was
accepted first press in both runs, as was (B) on the menu. n=2 only makes "half
the time" unlikely (p ~ 0.25); it contradicts the ten seconds outright.

And the standing red banner is withdrawn: the interactive title IS reachable in
this container, twice, with no pad input, in ~3.5 minutes. Why it changed is NOT
established -- this container came up with no Xenia storage root at all, so run 1
created a profile -- and that is written as a correlation for the next session to
test rather than as a cause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 12:15:22 +00:00
sylph-decoder
7a4a74f8d7 re: the main menu has no idle self-return -- and four of my own durations were a backlog
Two results and one retraction, all from the same session.

REFUTED: 'an ~8-10 s idle returns to the title' does not apply to the main menu.
Held untouched it stayed put for >= 60 s, correlation never leaving
0.9245-0.9249. That timer is real but belongs to the TITLE. It was the only
reason 'B leaves the main menu' was classed as authored, so Q5's B rule is
upgraded to measured-ordering: B is delivered (canary logs vk=5801) and is the
only input in >= 100 s before the return.

The PRESS (A) plate: the boot title presents build 4 WITHOUT the plate first --
green-glyph 154, against 159 on the committed no-plate capture and 753/977/1493
on plate titles -- and the plate arrives after. That is the port's third option.

RETRACTED: four durations taken the same day. classify_array costs 1503 ms per
frame; running it per frame against an 8 fps x11grab drained the pipe at
0.64 fps, so every classified frame was stale and increasingly so. It
manufactured 'plate 24.66 s after the title art', 'B->title 15.58 s', 'B->title
25.60 s' and 'A->menu 20.26 s'. The tell: a transition, a press and a fade do
not share a duration, and the two B figures GREW across a longer run.

A backlog preserves ordering and destroys durations, which is why the sequence
results above stand and every timing does not. The ring's period is unaffected
and that was checked, not assumed -- ring_period ran at 15.03 fps against a
requested 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNR5Y79D1T4bBr6gJQaWFP
2026-08-29 11:50:46 +00:00
sylph-decoder
3db09a3806 re: the decoder container has no disc -- root-caused to the volume migration
find / -xdev turns up no ISO, no default.xex and no GP_TITLE.pak; /exchange is
empty; SYLPHEED_DISC is unset; sylph-doctor agrees. Everything else in the
container is healthy -- xenia_canary is built, :98 is up, screenshot works,
Vulkan enumerates. There is simply no game to boot.

The cause is in the launcher. Before c58196b, sylph-agent bind-mounted the
human's working tree at /work, and the ISO and sylph_extract/ live in that tree,
so the disc arrived incidentally with the repository mount. c58196b replaced
that with `-v sylpheed-decoder-repo:/work` -- correct for the collision class it
was written for -- and nothing was added to replace the disc. sylph-decoder
still forwards SYLPH_ISO, but as a bare environment variable naming a host path
that does not exist inside the container. sylph-port mounts the disc explicitly,
so the one container that owns the disc and the oracle is the one without them.

This shuts the oracle, every sylpheed-cli call that names a pak, the disc-gated
tests, and -- because the XEX is on the disc -- the static PPC route too. It
does not touch the committed corpus, which is what this iteration worked from.

A second, smaller casualty of the same migration: no git identity is configured
anywhere, so the first commit in a fresh container fails outright. Both are
recorded with their fixes; neither is worked around, since the launcher runs on
the host and this container cannot restart itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UxPvE5cz7zekXBKi7Xw2r
2026-08-29 10:42:47 +00:00
Sylpheed RE agent
1a82ade187 re(ui): the 60 nameless RATC children are frames, not children -- .tan decoded
Closes the reach caveat the `opt ` name fix left behind: 60 of 18 002 RATC
children carry no `opt ` block, and it was not established whether they lack one
or sit past our 128-byte window.

Neither. They are not children. `examples/ratc_optless_children.rs` re-runs
`ratc::parse`'s own guards over the disc and reports which one fired: all 60 are
"tag beyond the window", none is rejected by length, gap or charset, none is
child #0, and all 60 live in six bundles of one archive. Within a bundle the
distances back to the nearest tag are an exact arithmetic progression, step
60 600 -- ten different records finding the SAME tag, because there is only one.

Reading a bundle directly: children 1..10 are equal-size T8aD blocks under a
single `opt ` name, `pb_f15_eg_anm.tan`. `.tan` is a FRAME SEQUENCE. One block
declares the resource; its payload is a run of T8aD frames.

Disc-wide, over all 18 718 `opt ` names in all 33 paks: a RATC bundle names
exactly six kinds of resource -- `.t32` 14 756, `.rat` 3 311, `.prm` 367,
`.tbm` 224, `.sbo` 54, `.tan` 6. Six `.tan`, ten frames each = 60, the entire
population with nothing left over. The negative is closed, not narrowed.

Consequence recorded but deliberately not fixed: `ratc::parse` over-reports
there, listing a `.tan`'s frames as anonymous children. Nothing in the menu
milestone reads a `.tan` -- it occurs only in GP_READY_ROOM, which S1 ruled
out -- so no screen the port draws changes.

Also a METHOD entry for this container OOM-killing `slb_leading_segment_disc`
under default test parallelism (SIGKILL, no assertion; 8/8 pass with
--test-threads=1).
2026-08-29 07:39:09 +00:00
Sylpheed RE agent
0ed33bcd38 re(ui): a RATC child's name is stated, not inferred -- and it was hiding every menu background
`ratc::parse` named each child by scanning backwards for the last printable run
of bytes before its magic. The format states the name explicitly instead, in an
`opt ` block: `"opt " | BE32 len | name | NUL | 3 bytes | magic` -- the same
block `ui_layout::opt_link` already read for a button's focus link.

The scan agrees with it 17 918 times out of 17 942 and is wrong 24 times, every
one the same failure: the 3 trailing payload bytes are themselves printable and
beat the real name. For `pteff05.t32` those bytes are `38 41 58` = `8AX`, so the
full-resolution background of all five menu screens registered under a name no
element declares, resolved to no sprite, and `compose` dropped it through an
early `continue` that -- unlike the two arms above it -- records nothing. The
screen lost its background and `screen render` still reported "all resolved".

`8AX` was never a name. Docs that treated it as one are corrected here.

Disc-wide, and the control is the 17 918 the scan already got right: the `opt `
reading reproduces every one of them. Effect on the five screens is the
signature of the same art at twice the resolution -- mean brightness unmoved,
high-frequency detail x1.15..x1.30 -- which is what the separately-measured
`ui-8ax-fullres-background` result said the game draws.

Also closes a long-standing dangling reference: `pmbase.t32`, recorded as "on
the disc nowhere", is the `GP_STAGE_CLEAR` child the scan called `8AX`. RATC
sibling references now resolve 10 148 of 10 148.

Verified: 114/114 sylpheed-formats unit tests (including two new ones pinning
the `8AX` case byte for byte and the no-block fallback), and every disc-gated
integration suite in sylpheed-formats/sylpheed-cli.
2026-08-29 07:26:56 +00:00
Sylpheed RE agent
969947e0e6 re(ui): put a number on the render-vs-capture tone difference
Closes an observation I left dangling last iteration ("the capture is ~4x
darker than the render") and puts a figure on the  INDEX's texture row
already carried: exact gamma/sRGB fidelity untested because a hue
comparison cannot see it.

Geometry first: cross-correlating the main-menu capture against our
render over +/-6 px puts the best alignment at exactly dy=0 dx=0,
correlation 0.9466. Only the tone differs.

Two methods failed before one worked, and both failures are recorded.
Three dark patches gave "4x darker" -- the whole-frame best linear scale
is 0.914, so three patches from one region are not a transfer curve. A
pixel-wise fit over 854,685 pixels then produced a NON-MONOTONIC transfer
(render 96-127 mapping brighter than render 128-159) with mean abs error
10-14 for every candidate model. That is edge misalignment, not a tone
curve: at correlation 0.947 a bright pixel routinely lands on a dark one.

Flat patches fix it -- 16x16 blocks with std < 8 in BOTH images, a
threshold chosen from the counts (0/83/404/1055/1788 at std<3/5/8/12/20):

  main menu  404 patches  gamma 1.491  err 0.28   (best linear 0.276, 0.34)
  EXTRAS     382 patches  gamma 1.493  err 0.22   (best linear 0.273, 0.28)
  title      506 patches  gamma 1.338  err 1.08   (best linear 0.842, 9.02)

Reach, stated because it is narrow: those patches span only render values
~0-60, where gamma and a plain scale are nearly indistinguishable -- the
two menus decide nothing (0.28 vs 0.34, 0.22 vs 0.28) and only the title
separates them. Nothing constrains midtones or highlights.

The held-out control FAILED TO DISCRIMINATE and is reported as such: the
splash's 2918 flat patches are pure black (render 0-4), so every model
scores ~0.00. That is a test with no power, not corroboration.

Confound left open: this compares our composite to what canary DISPLAYS,
and canary applies kernel_display_gamma_type = 2 (BT.709). The exponent
may be its output stage. The discriminating run -- set it to 0, recapture,
refit -- needs one emulator session reaching the main menu and was not
done.

Classified measured, not decoded; HANDOFF says plainly that a port
applying it is authoring.
2026-08-29 01:05:25 +00:00
Sylpheed RE agent
ffd1a30ba7 re(ui): decode keyframe +12 as screen-plane rotation in degrees
The rotated quads on the title screen come from the keyframe block after
all. The earlier negative -- "every GP_TITLE build 4 element has all three
angle words at zero" -- read the right bytes over too small a region: it
walked the top-level declaration table, and the rotated elements are the
nested leaf records ptloop01.rat / ptloop02.rat.

Confirmed against the framebuffer rather than against our own renderer.
The two records declare +12 = 30 and -45; the GPU capture submits their
quads at +30.26 and -45.28 degrees -- magnitude and sign, two different
values. Corroborated by shape in GP_BUNK 117ca14f, where +12 ramps
0 -> 360 with position, scale and alpha constant: a spin in place.

Identifying which draw it was needed edge lengths, not bounding boxes:
400x1076 and 400x1444 against pteff03/pteff03a 399x180 at the elements'
two different declared scales, 600% (1080) and 800% (1440). The same test
names three known-positives in the capture (ptlogo1, ptcopyright,
ptbtn00), so it passes its own control.

Keyframe gains rotation_deg plus unknown_4/unknown_8, carried rather than
dropped. NOT rendered -- ui_layout::blit is axis-aligned only, so the
reference renderer and the port will both draw these upright until a
rotating blit exists.

The census tool ships with the trap that broke its first version: nested
RATC blobs are not 4-byte aligned, so an aligned scan found 0/3 of its
own control blocks and missed 16 341 blocks. Disc-wide +12 is non-zero in
14.50 % of 83 862 blocks.

sylpheed-formats tests, SYLPHEED_DISC set: 131 passed, 0 failed across the
6 suites finished at commit time; the run had not yet completed.
2026-08-28 22:42:00 +00:00
Sylpheed RE agent
a4fc129ba3 re: what the game actually reads at boot -- config.ini, and which
GameParts exist at all

Q6's second half, advanced but not closed, and the negative is the point.

config.ini is the disc's ONLY config file -- one find over the whole
extract -- and its own Shift-JIS header calls it the "Application /
GamePart initial settings table". Its [SYSTEM] section, which that same
comment says holds what the game and every game part share, is EMPTY. So
the boot order is not in disc-side configuration at all, and that search
space is now closed rather than merely unexplored.

What the file DOES carry is the language: XC_LANGUAGE_* -> eng/jpn/deu/
fra/esp/ita, defaulting to eng. That is the mechanism behind the EN/JP
build pairs in GP_TITLE and the <lang>.pak families -- a question the
corpus had described but never traced to its input.

Then the registry. Pulling every RegisterToFactory diagnostic string
binds 24 of the 29 GamePart ids to a C++ class, and five ids have no
registration site: 1, 2, 16, 18, 28. Id 1 is GP_ADVERTISE_DEMO, which
agrees with what I measured two iterations ago -- the attract loop is the
TITLE replaying ADV.wmv, not a transition into an advertise part. Marked
amber, not green: it is an argument from an error message, not from code.

Two things fall out for Q4: ids 3 and 4 are the same class
(GamePart_SaveLoad, one part with two ids), and the menu buttons' ids now
match the executable's own class names rather than a list of table names.
Still a name match, one level closer to the code.

What is still missing is the transitions themselves, and I say so: the
manifest gives the boot-side assets, config.ini the language, the
registry which parts exist. What decides to advance is in
GamePart_Title's code and that dig has not been started.
2026-08-28 18:49:19 +00:00
Sylpheed RE agent
c5fb6682c7 re: the menu's sound events are named on the disc; the binding to them is
not

Q8. The cheapest thing nobody had tried was to look at the SE cue names,
and they are semantic: SE_UI_CURSOR (2), SE_UI_DECIDE (3), SE_UI_CANSEL
(4), SE_UI_IMPOSI (5, the error buzz), SE_UI_SUB_WIN_OPN/CLS, and
SE_UI_SPLASH_IN/OUT. That is exactly the move/confirm/back/error
vocabulary the question asks for, named by the authors after the EVENT
rather than the sound. 322 SE cues committed as reference data.

They all live in one bank: BANK_SE is a single field reading Static.slb,
and the disc-wide check agrees -- 0 of the 322 has an entry in FILES, the
5135-path list that names every voice, briefing and BGM bank.

I am calling the binding a NAME MATCH, not a measurement, and the page
says so in the same breath as it gives the table. It is a stronger name
match than Q4's GamePart ids -- these are the authors' own event names --
but nobody has watched the game emit cue 2 on a d-pad press, and this
container is muted against a dummy device so there is no audio path to
watch either.

One new negative, with its reach. Static.slb's 8 353 472 readable bytes
contain zero RIFF, zero seek and zero WAVE, scanned over the whole
buffer. The boundary marker that works for all 7 620 other banks is
absent, so the cue is named, the bank is named, and the wave inside it is
not locatable. The UI sound effects cannot be exported yet, and that is a
gap in the assets rather than in the naming. The named next step is
Pj_Silph.xgs -- the XACT project SETTINGS points at, which IS in
sound.pak at TOC 9454, though its 533 bytes carry no XGSF magic and its
region's phase says they are probably the previous bank's tail.
2026-08-28 18:33:58 +00:00
Sylpheed RE agent
d69272e53f re: the boot intro and the attract loop are the same video, and the
new-game intro has a name

Q9, answered twice over -- once from the disc and once from frames I had
already captured and not used.

The movie manifest's first eight slots are the whole boot-side flow, and
the slot key is the role: LOGO1-4 -> logo1-4.wmv (not on the disc, which
is why the splash is a screen), ADVERTISE_MOVIE -> ADV.wmv, STAFF_ROLL ->
the credits reel, MS00A -> S00A.wmv, MS01A -> S01A.wmv. So the new-game
intro is S00A.wmv, decoded -- which is the half I could not test at
runtime, because A on NEW GAME hangs the emulator.

And there is no separate boot-intro slot: ADV.wmv IS the advertise movie,
the boot just plays it first. Confirmed independently by matching 19
attract frames against five candidates -- 15 hit ADV with a playhead that
advances monotonically at the sampling rate and ends at 137 s, its full
length, with the title back on the next sample. So the attract movie
plays to its end; nothing cuts it short.

That corrects me. Two iterations ago I recorded the attract movie as
"~85 s, so probably not ADV.wmv" -- arithmetic on a start I never
observed, since sampling began 39 s in. REFUTED and METHOD both take it,
along with the matcher's real failure mode: a near-black frame has no
signature, and its runner-up is not evidence.

Skippability I did NOT settle, and the corpus contradicts itself: one
page says A skips a movie every time, while the boot harness deliberately
never taps during one because it breaks the title. Named the one-boot
test rather than picking a side.
2026-08-28 18:29:39 +00:00
Sylpheed RE agent
4820865835 re: S1 -- the Ready Room probe is a no-go, and not for the reason the
question expected

One iteration, as the mission gates it, and no Ready Room work started.

The two criteria it named both pass. It is NOT 3D: the corpus already
holds a capture of the running screen and it is a flat six-item menu in
the same visual language as the title. And unrecoverable paths do not
mean unrenderable screens -- content enumeration finds 60 builds in the
1106 entries.

What kills it is something the question did not ask. GP_READY_ROOM.pak
does not appear to hold the Ready Room's own menu at all. Two signals
agree: the four largest builds composite to briefing panels -- a planet
readout, unit markers, direction arrows -- and, much harder, the 902
bundles declare 317 distinct element names of which NONE matches any of
the six labels on the screen (takeoff, brief, hangar, pilot, option,
mission, ready all return nothing). The pak is the briefing /
tactical-map content behind the BRIEFINGS item. If this is ever picked
up, the first job is finding where the menu lives -- GP_BUNK.pak is the
obvious place -- not naming this pak's keys.

The probe also reaches back into something already delivered: kind ==
0x3002 finds ZERO buttons across all 902 bundles here, while GP_TITLE has
16. 0x3002 is one member of a 0x3000 family with sub-bits, and this pak
uses 0x3000/0x3004/0x300c/0x3008. Nothing in the milestone changes --
every screen in scope is GP_TITLE -- but the handoff now says not to ship
that equality as a general button test.
2026-08-28 18:24:48 +00:00
Sylpheed RE agent
c848668b1f re: a music bank is two stems that play together -- not intro+loop, and
not something to concatenate

Q10, and it starts by withdrawing the question's own premise. BGM_001 is
not three sub-waves of 10 KB / 4.47 MB / 4.67 MB: the 10 KB is the bank
header. A bank is exactly TWO waves, and across all 32 BGM banks on the
disc the two always have the SAME duration -- equal to 0.01 s over
lengths from 37 s to 277 s. That alone kills intro+loop and kills two
halves of one piece, both of which require unequal lengths.

Four banks appear to break the rule and do not: BGM_106-109 are the known
leading-region straddle, and the giveaway is that the entry named
BGM_107.slb contains BANK id=1108. The seek packet counts pin each join
exactly, so they realign to the same two-equal-waves shape rather than
being dropped as noise. That trap goes in METHOD.

Then the roles, by decoding both waves to PCM. They are
sample-synchronous: transient-envelope correlation searched over +/-5 s
peaks at lag +0.00 s, and both waves stop at the same millisecond,
167.663 s. Two stems of one performance, meant to sound at once. Wave 1
is quieter, has almost no bass and is far more L/R-decorrelated, which
reads as a surround-rear pair or a second intensity layer -- I cannot
separate those two from the file, and say so: ChannelMask is 0x0002 on
both, and this game's channel metadata is already documented as
meaningless.

Two things the port needs that are NOT on the disc, both marked as
authored: the track is not a seamless loop (BGM_001 fades out and is
followed by 6.15 s of silence, no loop-point field found), and nothing
names which bank the menu plays -- all 32 BGM cues are numeric.
2026-08-28 18:13:10 +00:00