115 Commits

Author SHA1 Message Date
Sylpheed RE agent
26b5afeaa3 agent: tag the decoder states the port pins, and stop leaking credential config
Two fixes to push-work and the policy that goes with them.

**Tags.** The port's exporter depends on sylpheed-formats BY REVISION, so a
commit of ours is part of its build -- and the commit it pinned lived on one
auto/* branch and nowhere else. Deleting that branch orphans it; squash-merging
it is worse, because squash creates NEW commits, so main appears to contain the
work while the pinned sha becomes unreachable and the port stops building for a
fresh checkout. Silently, at their build, long after the breakage.

push-work now pushes with --follow-tags, which publishes annotated tags
reachable from the pushed commits, and MISSION.md says to tag whatever the port
needs. formats-pin-2026-08-29 at 7eeae30 is the first, created after the fact.

**Credentials.** This script set credential.helper as --local config, which
PERSISTS in the repository. The repo is a bind mount the host also uses, so the
host's git inherited a path that exists only inside the container and every host
push failed with "unable to get credential storage lock". Now applied with -c to
the single push. Same fix already landed on the port side; this copy still had
the bug and would have re-leaked on the next push.

Committed with a pathspec so the agent's in-flight work is untouched.
2026-08-29 10:59:14 +02:00
Sylpheed RE agent
3491d30066 re(media): the disc ships movies in TWO audio profiles, and 28 of them are 5.1
Probed all 97 movies. 28 are wmapro 48 kHz 6-channel 5.1 -- ADV.wmv and every
S*.wmv story cutscene; the other 69 are wmav2 48 kHz stereo, every RT*.wmv and
hokyu_*.wmv. The split is cinematics vs in-mission radio chatter.

Both movies the menu milestone needs, ADV.wmv (the boot/attract intro) and
S00A.wmv (the new-game intro), are in the SURROUND group.

Why it matters: one ffmpeg command over dat/movie/ produces two different kinds
of result and records neither. The 69 stereo files pass through unchanged; the 28
surround files get downmixed 5.1 -> stereo by ffmpeg's DEFAULT matrix, folding
centre-channel dialogue into L/R at a weighting nobody chose and which is not
stable across ffmpeg versions. That is a content decision inherited by accident,
so it should be stated explicitly and recorded beside the command.

Credit where due: found by the human while checking a transcode, verified
independently here and widened from one file to the whole disc.

Two METHOD entries from the same episode, both about measurement rather than
format: don't probe a file another process is still writing (a half-written
transcode reported 33 s against a 137 s source, no error, nearly a filed bug),
and a difference-signal RMS is meaningless before cross-correlation alignment
(-34.2 dB against a -25.3 dB source looks like failure and is inconclusive).
2026-08-29 08:57:56 +00:00
Sylpheed RE agent
7eeae3006a re(ui): the focus ring SPINS, the game draws it, and the leaf owns the f record
Three things, all from parsing ptbtn0Nf.rat as a build.

1. THE RING SPINS. Its two keyframes differ in exactly one field: rotation_deg
ramps 0 -> 360 with position, scale, alpha and tint all constant. A spin in
place, the same shape as the GP_BUNK example already recorded.

2. THE ORACLE CONFIRMS THE GAME RENDERS IT. In the OPTIONS-focused capture the
ring's bright head sits in a completely different angular position from the
sprite's own -- caught mid-spin. This is a SECOND independent confirmation that
rotation_deg is drawn, now on a different screen and a different element from the
ptloop sweeps, and it raises rotation's priority: it is not a title-only concern
that sits off-screen at rest, it is the main menu's focus marker.
NO ANGLE IS QUOTED. A brightest-region centroid says ~250 deg, but the control
refuses that precision -- rotating the sprite by a known 30/90/180/270 and
re-measuring gives errors up to 19.8 deg. What survives the error bar is that a
<=20 deg error cannot manufacture a ~250 deg displacement.

3. WHICH PLACEMENT WINS -- correcting this page's own earlier caveat, which said
to use the leaf only for elements the parent does not declare. Right for a BASE
record, wrong for an f record: the parent declares NO element for ptbtn0Nf.rat at
all (zero of build 5's 16), so the f record's placement comes from its leaf for
BOTH elements, label included. The label's (-7,-7) is load-bearing -- the f
sprite is 13px larger per axis and -7 keeps them concentric (535+96/2 = 583 vs
542+83/2 = 583.5). Corroborated against the oracle: the focused-minus-unfocused
region is x 505..703, and the leaf predicts a right edge near 707 where the
parent reading predicts 714.

Also exposes UiBuild::records (name -> (offset, size) of a nested .rat leaf).
Nested records were parsed into a PRIVATE map, so a consumer holding a UiBuild
could not locate a leaf's bytes at all -- which is exactly what blocked the port
from reaching the ring.
2026-08-29 08:46:53 +00:00
Sylpheed RE agent
d110cf38c7 media: expose se_wave_riff -- the menu's SE cues, assembled where the format lives
The port is forbidden from reimplementing media assembly and Static.slb is
exactly that case: no RIFF, no seek chunk, no XACT container, just a packed run
of whole 2048-byte XMA1 packets, so a wave is defined only by (offset, packet
count) and the header has to be synthesized. That step now happens once, in the
crate that owns the format, instead of in each consumer.

`slb::xma1_wave_riff` wraps raw packets; `media::se_wave_riff` looks the bank up
and reads just the packets asked for. Both reuse the existing synth_xma1_fmt /
build_riff, which are already byte-identical to what tools/re-capture/
slb_extract_wave.py writes -- so this is exposure, not a second implementation.

It reads a TARGETED range rather than the whole bank, and that is load-bearing:
Static.slb is the ONE entry of sound.pak's 9 519 whose declared extent runs past
the end of the extracted segments -- by exactly 616 768 B -- so reading it whole
fails outright on this extraction. Every cue we need is in the first few hundred
KB. Recorded rather than worked around silently.

Verified as an artifact, not a compile: all three cues decode through ffmpeg to
mono 48 kHz PCM at 0.533 / 0.344 / 1.016 s, non-silent (rms 2085 / 2985 / 4327,
peaks 29813 / 16973 / 32767). The refusal path is exercised in the same run --
an impossible packet count is rejected rather than returning a short stream,
because a truncated XMA decodes to plausible-sounding garbage.

Also adds docs/re/captures/ORACLE-CAPTURES.md: an index of the nine canary
framebuffer captures already in this repo, and a plain statement that THEY are
the reference and `screen render` is not.
2026-08-29 08:41:42 +00:00
Sylpheed RE agent
b21c8e4118 re(ui): the focus ring's position is decoded -- a .rat leaf parses as a build
The port needed ptbtneff01.t32's placement and was about to author it from an
eyeballed PNG measurement. It does not have to: a `.rat` leaf needs no new
reader. Its first 32 bytes have a bundle header's shape -- "RATC", 0x3c
declaration-entry size at +4, element count at +20, design 1280x720 at +24/+28 --
so ui_layout::parse_build reads it unchanged.

The control is the base record, whose position is known independently: the parent
screen reports ptbtn01.rat resting at (542,162), and parsing the leaf alone
returns ptbtn01.t32 at (542,162). It reproduces all five buttons.

Positions are absolute design-space top-left. The ring rests at (500, 156/236/
316/396/476) for buttons 1-5 -- a uniform (-42,-6) from each button's own rest,
identical in the Japanese bundle. The bright label is a uniform (-7,-7).

Two things recorded rather than smoothed over: a leaf's placement DUPLICATES the
parent's rather than being relative to it, and the two copies are not always
byte-equal (ptbtn04's parent says y=401, its leaf says 402) -- the parent is what
compose honours, so the leaf is the source only for elements the parent does not
declare, which is exactly the ring. And `screen render --focus` is blind to the
ring for the same reason the port's exporter was: el.focused is name-based on
top-level elements and neither walks into the leaf.
2026-08-29 08:36:53 +00:00
Sylpheed RE agent
9ca1eb50fd re(ui): answer four of the port's five asks -- splash, fade-out, focus, gamma
1 SPLASH ADDRESSING (was blocking P3). No content predicate exists: design size
fails (every extra composable bundle sampled is 1280x720, like every screen) and
element count fails (fragments run 2..15, the splash halves are 3 and 7). But
GP_TITLE needs none -- `--all` adds exactly four bundles there and all four are
real screens, with the --all index equal to the pak entry index 1:1. And there
are TWO splash screens: 11/14 are the developer logos, 10/13 are the SQUARE ENIX
publisher wordmark, which the port did not have and which the boot shows first.

2 FADE-OUT (was blocking P3). It is (a), and it is bigger than the fade quad.
Every element ends on exactly ONE untimed keyframe, which rules out (b); that
block is where the screen plays out -- quad to a=255, buttons/labels/glows to
a=0, frames hold. (c) is refuted by a null test that discriminates: a black quad
alone holds the button/background brightness ratio constant, and through the
fade it falls 6.50 -> 1.94, 3.4x monotonic.

3 FOCUS (saves P5 rework). Over-vs-instead is unobservable -- the focused sprite
covers the base at 100.0% of base-visible pixels on three pairs once aligned
(true offset (7,7); the centre alignment reads a misleading 78-84%), and
compositing both ways differs by RMSE 1.1 inside the button rect. The real defect
is the focus record's SECOND element: ptbtn0Nf.rat declares ptbtneff01.t32 (a
42x46 glowing ring, focus only) plus the bright label, where the base declares
one sprite. That ring is the marker the port draws nowhere.

5 GAMMA. The capture is not neutral: capture ~ 255*(render/255)^g, g ~ 1.34-1.49,
and the chain says it is a ramp the GAME installed, not a capture artefact. So
RMSE against captures has a floor. Reach stated: the flat patches are all dark
(render ~0-60), so midtones and highlights are unconstrained.

4 ROTATION is a human's call and is recorded in MISSION, not acted on -- the port
rotating while the reference renderer does not would make verify-screen report a
large diff meaning "the port is right". The RE half is answered: rotation is
about the declared pivot, measured against a GPU capture.

The focus record's +20 element-count word is marked 🟡 not  -- read on GP_TITLE's
ten button records only; the disc-wide check is written and still running.
2026-08-29 08:19:23 +00:00
Sylpheed RE agent
9a0ca0d71f docs(method): time the disc mesh suite -- 22 minutes of silence is not a hang
Two runs were killed this session for looking stuck. Measured: 1 318 s serial,
no output while it runs. Also records that `build-reborn test` forces
--workspace and silently ignores a `-p`, so scoping needs the cargo
passthrough (`build-reborn t -p <crate>`).
2026-08-29 07:49:18 +00:00
Sylpheed RE agent
f817dd5939 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
56cc7acfc3 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
49f109deb1 agent: the loop must never stop itself
A run ended with a clean exit 0 while the display title read "Loop interval
optimization", leaving four files uncommitted in the tree. Nothing crashed --
the loop was ended, and ending the loop ends the run: the container exits and
there is no next iteration.

The prompt said the agent did not NEED to arm a wakeup. It never said not to
stop one, and an agent that reads about a pacing control will reasonably try to
use it. Now explicit: do not call ScheduleWakeup at all, and if the cadence is
wrong, say so and leave it to a human -- the interval is set outside the prompt.

Recorded with the date and the symptom, because "the container exited cleanly"
looks like a finished job rather than a self-inflicted stop.
2026-08-29 08:12:42 +02:00
Sylpheed RE agent
7a4e4333f4 docs: withdraw yesterday's "paint order is a sequence" -- wrong source
Last iteration I claimed the splash's measured_paint_order [0,2,4,6,1,3,5]
records, between its glow and logo halves, the temporal order they were
seen in rather than depth -- because the halves never share a frame.

The no-overlap measurement is right (glows f94-115, logos f116-211). The
inference is wrong, on two independent grounds:

  * Wrong source. That vector is not a read of the draw capture. It is a
    read of the live screen object's CHILD ARRAY -- ui-screen-runtime.md
    records it literally as "paint order (child slots)". A child list has
    a definite order whether or not its children are ever drawn together,
    so co-occurrence does not bear on it. The capture was the CHECK.
  * The order is in the file anyway. paint_order_audit on GP_TITLE entry
    11: derived == measured, 0 inverted pairs, 0 same-layer-key ties. The
    glows and logos carry distinct T8aD keys (0xa100 < 0xa110), so the
    file orders the halves statically, no capture involved.

I asked the question that started this iteration -- do the title and menu
orders have the same problem -- and the answer is that none of the three
does, for the same reason.

What survives is narrower and now recorded with numbers: how much of each
order its capture actually cross-checks. The title capture is stable (8
draws / 12 quads / 5 textures, identical in all five captured frames
across two logs) and confirms 7 of 24 positions; the menu capture is not
(texture 0x11C30000 present in frame 0, gone by frame 3); the splash
capture cannot cross-check its middle at all.

A counting trap worth the tool: count QUADS, not draws. The menu's draw 9
is indices=24 -- six quads batched from one texture. Counting draws reads
9 where 16 are on screen, and an earlier pass of this analysis briefly
"found" three quads for six declarations that way and concluded elements
were missing. They were batched.

METHOD: check what a "measured" value was measured FROM before reasoning
about its limits. The co-occurrence rule is real, and it is specific to
orders read from draw captures.
2026-08-29 05:19:44 +00:00
Sylpheed RE agent
2532c056be docs: the splash .prm measured -- and its paint order is a sequence, not depth
ui-prm-primitives recorded that where a .prm paints on a screen without a
measured order is unsolved. For the developer splash it is now measured.

Every frame opens with two full-screen draws before any sprite. The
second is untextured in all 212 frames with a constant vertex colour of
FF000000 -- opaque black -- matching palogo_eff0.prm's declaration
exactly: kind 0x10, pivot (640,360) -> 1280x720, one keyframe, a = 255.
So the splash backdrop is an opaque black full-screen quad from the
bundle itself, painted behind every sprite, which is why a splash render
needs --black rather than the default backdrop.

Not a general rule, and said so: the measured main-menu order puts
pteff02.prm at position 4 and pteff00.prm LAST, the latter being the
screen-transition fade.

And a correction to an existing row. measured_paint_order returns
[0, 2, 4, 6, 1, 3, 5] for the splash, described as "the .prm, then all
three glows, then the three logos". But the glows and the logos never
appear in the same frame -- 0 overlapping frames in 235 -- and two
elements that never co-occur have no observable relative depth. Between
those halves the vector records the order they were SEEN IN, not a
front-to-back relationship.

That does not make the render wrong, and element 0 is a real depth
observation since the .prm co-occurs with everything. But the type of the
claim matters: reading the vector as depth invites compositing all seven
elements at once, which is exactly what does not reproduce the screen.

METHOD: two things that never co-occur have no observable relative order;
when recording an order, note which pairs actually appeared together.
2026-08-29 05:06:37 +00:00
Sylpheed RE agent
7988f52ce7 re(ui): deliver the measured splash sequence the port has to author
The activation decision is code, and MISSION already says the port
authors the sequence -- so the useful move is to hand over the sequence
measured rather than chase the code. From the 235-frame draw capture, at
1 frame = 1/30 s (2 units/frame, 1 unit = 1/60 s, both settled in Q1):

  publisher: SQUARE ENIX logo    f1-90     90 frames   3.00 s+   at 0.00
  (gap, nothing drawn)           f91-93     3 frames   0.10 s    at 3.00
  developer: both glows          f94-115   22 frames   0.73 s    at 3.10
  developer: both logos          f116-211  96 frames   3.20 s    at 3.83

Three limits, stated with the numbers rather than after them. The capture
opens with palogo_sqex already at a=255, so the publisher phase began
before the window and 3.00 s is a FLOOR -- every "starts at" is relative
to the capture, not to boot. palogo_anima and palogo_anima_eff get 0
draws in all 214 frames, so a third pair's phase is not in this
measurement. And it is one capture, one run: the glow->logo switch being
a single frame boundary with no overlap is a strong shape, but each
duration is one sample.

What is solid is the part that matters: the 0.73 s and 3.20 s phases are
each within 2% of their element's declared span, so the durations are the
bundle's own and only the ordering is authored. That is the difference
between a port transcribing timing and inventing it.
2026-08-29 05:00:54 +00:00
Sylpheed RE agent
7156591654 re(ui): re-establish selective activation by killing the alternative statically
Last iteration I withdrew "a bundle is a palette" because the evidence
did not choose between selective activation within one bundle and two
compositions shown in sequence. The alternative can be killed from the
disc, which I had not tried.

Hypothesis 2 needs a bundle declaring the GLOWS WITHOUT THE LOGOS. There
is none. Every GP_TITLE entry carrying palogo elements:

  10, 13 (publisher twins)  palogo_eff0.prm, palogo_sqex, palogo_sqex_eff
  11, 14 (developer twins)  palogo_eff0.prm + all three logos + all three glows

Four entries, and each developer entry declares the complete set of six.
So whichever bundle was active across frames 94-211 -- entry 11, entry
14, or both in turn -- it declared the logos and the glows, while the
game drew two sprites at a time in disjoint phases.

Therefore only a subset of a bundle's elements is drawn at any moment,
whatever the bundle-loading story is. The conclusion no longer depends on
resolving how many bundles are involved, which is why the texture-base
test's failure stopped mattering.

So the claim is reinstated -- this time by eliminating the alternative
rather than by assuming it away. What worked was not a better capture but
asking what the competing hypothesis would REQUIRE on the disc and
finding it absent.

METHOD: a hypothesis that predicts an artefact can be killed by looking
for the artefact, which is often far cheaper than measuring the behaviour.
2026-08-29 04:56:13 +00:00
Sylpheed RE agent
2a086051b3 re(ui): withdraw the mechanism -- "palette" was an explanation, not a finding
Last iteration I wrote that a bundle is a palette whose elements are
selectively activated. The disjoint glow/logo phases have two
explanations and I asserted one:

  1. one bundle, some elements run then others;
  2. two bundle-loads shown in sequence (entries 11 and 14 are twins
     declaring identical sprites).

The draw log's tex[base=...] looked like it would separate them. It does
not, and the control is in the same table:

  publisher splash  f1-90     0x11C30000, 0x10000000
  glows             f94-115   0x11C30000, 0x10000000
  logos             f116-211  0x11C30000, 0x10000000

The publisher splash is certainly a DIFFERENT bundle from the developer
splash, and it uses the same base. So 0x11C30000 is a reused upload slot,
not a bundle identity, and the test cannot choose between the two
hypotheses.

Survives: a bundle's declared elements are not what gets drawn.
palogo_anima and palogo_gamearts carry byte-identical keyframe times and
in the same run one is drawn 95 frames and the other none -- and
whichever twin was active declares both. The phases are strictly disjoint
(0 overlapping frames in 235).

Withdrawn: the mechanism. The practical consequence is unchanged --
compositing every element of a bundle does not reproduce what the game
shows over time -- but the why is not established and I stated it as
though it were.

What would separate them: a per-draw capture recording the bundle each
draw came from, or a file-IO log showing whether a second RATC entry is
read between frames 115 and 116.

METHOD: a shared resource address does not identify the resource's owner;
and state the mechanism as a separate claim from the observation, or the
weaker one inherits the stronger one's evidence.
2026-08-29 04:51:34 +00:00
Sylpheed RE agent
baed44a9ea re(ui): the sequencing survives refutation -- and a bundle is a palette
Two checks on last iteration's "sequential, not simultaneous" reading.

First, the phases really are disjoint. If glows and logos ever shared a
frame the claim would be wrong. Across all 235 captured frames the count
of frames containing both is ZERO, and the switch is a single clean
boundary -- f110-115 draw 1280x720 + 262x108 + 525x90, f116 onward
1280x720 + 243x86 + 499x72. Two sprites either side, no transition frame.

Second, and larger: a third of the bundle is never drawn. Entry 11
declares three logo/glow pairs and only two appear.

  palogo_gamearts / _eff    95 / 22 frames
  palogo_seta     / _eff    95 / 22 frames
  palogo_anima    / _eff    never

palogo_anima declares the SAME keyframe times as palogo_gamearts. Two
elements with byte-identical data, 95 frames and 0 frames in one run.
Reach: the capture covers frames 1-214, so this is "never in the window".

So a bundle is a palette, not a script. Its elements say what to draw and
for how long; which of them run, and when each starts, is decided outside
the placement data. That is the same conclusion the boot-order work
reached from the other end -- the driver is code, not data -- now with a
per-element measurement behind it.

For the port, concretely: compositing every element of a bundle does not
reproduce what the game shows over time. It is the right thing for a
static screen that settles, and it is not a timeline.

METHOD: two elements with identical data and different outcomes is the
strongest possible evidence that the decision is elsewhere.
2026-08-29 04:47:05 +00:00
Sylpheed RE agent
7fb6bdfad8 re(ui): a group's duration is in the data, its start time is not
Tested whether the splash timeline, played, reproduces the capture --
the last gap in the animation model. Half of it does.

Durations match. At 2 units/frame under the shifted reading, from the
235-frame draw capture of the developer splash:

  glows  drawn f94-115  (22 frames = 44 units)  declared ~0..45  = 45   97.8%
  logos  drawn f116-211 (96 frames = 192 units) declared 15..210 = 195  98.5%

Each element is on screen for its declared span to within 2%.

Starts do not. Every glow declares the same times 15,30,45 and every
logo the same 15,30,190,194,206,210, so on one clock they would overlap
almost entirely -- and they do not overlap at all. The glows run 94-115
and the logos 116-211, strictly sequential, the logos starting the frame
after the glows end. Fitting one origin needs f0 ~ 93.5 for gamearts_eff
and ~103 for gamearts, about 19 units apart, and aligning one throws the
other off by ~9 frames at both ends.

The obvious candidate is refuted. parse_placements reads each group
header as (element index, keyframe count) plus one undecoded LEAD-IN
WORD -- exactly where a per-group start offset would live. It is
0x00000000 for all seven elements, glows and logos alike.

Reach: not the keyframe times (identical within each family), not that
word (zero), not declaration order (which interleaves logos and glows
where the observed sequencing groups them), not the RATC child order.
What remains is that the sequencing is code-driven, which agrees with
what the boot-order work concluded independently.

For the port: a group says how long an element animates and what it does,
not when it starts relative to its neighbours. The observed order on the
developer splash -- both glows, then both logos -- is measured for one
screen, not a decoded rule, so the sequencing has to be authored.

METHOD: when a model reproduces durations but not positions, the missing
piece is an origin, not a rate.
2026-08-29 04:42:55 +00:00
Sylpheed RE agent
1f2b469f7e re(ui): a static composite is only meaningful for a screen that settles
The model's sharpest prediction, tested with its control. The draw log
says that on the developer splash the _eff glows are drawn on frames
94-115 and the logos on 116-211, so at the moment the reference capture
was taken EVERY glow is already finished -- including the two that have
plateaus and which rest_plateau therefore renders visible. Suppressing
them should help on the splashes and hurt where a screen genuinely
settles.

  publisher splash   +0.9604 -> +0.9982   +0.0377
  developer splash   +0.9659 -> +0.9980   +0.0321
  title    (control) +0.9500 -> +0.9480   -0.0020
  main menu(control) +0.9460 -> +0.8544   -0.0916
  EXTRAS   (control) +0.9440 -> +0.8370   -0.1070

Both splashes jump to about 0.998; all three persistent screens get
worse. The control is what makes this a finding rather than a
coincidence: the same edit helps exactly where the model says it should
and hurts exactly where it says it should not.

So rest_plateau is not over-drawing in general -- it over-draws on
TRANSIENT screens. A plateau mid-animation means the element is held at
that point in the timeline, not that it is on screen once the screen has
settled. Where a screen settles, the held pose IS the settled pose and
the rule is measurably right.

And that answers the question left open several iterations ago -- what
"rest" means for a transient element. It does not mean anything: the
splashes never rest. A static composite of them can match a chosen frame,
and about 0.998 is what these captures' frame is worth, but the format
does not answer a question the screen never poses.

For the port: play the timeline for the two splashes, which the settled
keyframe timing now supports, and composite statically for title, main
menu and EXTRAS.

METHOD: an edit that improves one set of cases is only interesting once
you have shown it damages the cases where it should.
2026-08-29 04:37:59 +00:00
Sylpheed RE agent
458585d136 re(ui): the structural case for last -- 2 293 of 2 305, checked disc-wide
The weakness in the rest-rule finding was that `last` had been SCORED on
only two elements. It cannot be scored on more -- only two ambiguous
elements sit on a screen with a live capture -- but the entry -> hold ->
exit model makes a prediction that can be checked on all 2 305: what does
each element's FINAL keyframe look like?

  final keyframe invisible (a = 0)          1 618   transient: gone at rest
  final keyframe visible, at max alpha        675   faded in and stopped
  final keyframe visible, BELOW max alpha      12   genuinely unclear

Of the 687 that end visible, 472 have monotonically non-decreasing alpha
-- a plain fade-in that stops, [0, 255] over two keyframes in the
commonest case (pjex_eff.rat, pghud_speed_cut.t32) -- and another 203 end
at their maximum after dipping.

So `last` is structurally defensible for 2 293 of 2 305 (99.5 %), against
a dwell rule that returns a mid-movement frame by construction. Observed
correct for 2, structural for 675, model-consistent for 1 618, unclear
for 12.

The assumption carrying the 1 618 is stated rather than buried: that a
plateau-less element's animation has finished by the time the screen is
settled. The draw log establishes exactly this for the two splash glows
(drawn frames 94-115, logos 116-211) and establishes nothing for the
rest.

Default still unchanged. The case is now observational, structural and
model-based rather than two data points, but it would move 1 896 elements
and the decision belongs with whoever owns the renderer.
2026-08-29 04:34:21 +00:00
Sylpheed RE agent
4012d5b555 re(ui): why rest_plateau is right -- and last is right only for a transient
The shifted keyframe-time reading looked like it implied something
simple: the final pose is reached at a definite time and nothing follows,
so rest should just be the last keyframe and the plateau heuristic could
go. Tested by applying it to EVERY element:

  title              +0.9500 -> +0.6819   -0.2681
  main menu          +0.9460 -> +0.6416   -0.3044
  EXTRAS             +0.9440 -> +0.5745   -0.3695
  publisher splash   +0.9600 -> blank (zero variance, corr undefined)
  developer splash   +0.9643 -> blank

Refuted, and the failure supplies the model. A group is entry -> hold ->
exit, and the exit is the screen's DISMISSAL. While a screen is displayed
it has not reached its last keyframe; it is sitting at the hold. So
rest_plateau is the correct primary rule, and the last keyframe is the
post-exit state -- correct only once the screen is gone, which is why
applying it everywhere blanks the splashes.

This does not contradict the shifted reading. That reading says when each
pose is reached; it says nothing about the group being played to
completion while the screen is still up. The step between them was mine.

And it explains why last wins for the two plateau-less elements: an
element with no hold is a transient, it flashes and is over, and at any
settled moment it is gone -- which is its last keyframe. The draw capture
says the same independently: on the developer splash the _eff glows draw
on frames 94-115 and the logos on 116-211, so the glows are already
finished when the logos are up.

Three independent observables -- animation timing, static composites, and
the per-frame draw log -- now agree on one rule: plateau where there is
one, last keyframe where there is not.

METHOD: a blank render is a NaN correlation, not a low score, and that
NaN was the strongest form of the result; and when a model predicts
something the measurement refuses, suspect the step you supplied between
them.
2026-08-29 04:28:19 +00:00
Sylpheed RE agent
515456c59a re(ui): quantify what changing the rest rule would do disc-wide
The open question was whether "last keyframe" holds beyond the two
elements I could score against a capture. It cannot be scored disc-wide --
only two ambiguous elements sit on a screen with a live capture -- but the
blast radius can be measured, and it argues the same way.

  genuinely ambiguous elements                       2 305
    the two rules AGREE on                             409  (17.7 %)
    they DIFFER on                                   1 896  (82.3 %)

  dwell (current): invisible pose 1 711 (74.2 %), zero-scale 195 (8.5 %)
  last           : invisible pose 1 618 (70.2 %), zero-scale  43 (1.9 %)

Two things follow. It is not a marginal choice: the rules disagree on 82%
of the affected elements, so "either is fine" is not available. And the
current rule produces 4.5x more degenerate poses -- a zero-scale pose is
collapsed to nothing, i.e. an element's PRE-ROLL before it has grown in,
which is definitionally not a rest. 195 elements currently rest at a frame
they are only passing through, against 43 under last.

That is an argument from the data's own structure rather than from the two
captures, and it points the same direction.

Kept honest: it is indirect. Fewer degenerate results is not the same as
more correct results, and last still returns an invisible pose 70% of the
time -- right for a transient element, wrong for a persistent one. The
default stays put; the numbers are in HANDOFF for whoever decides.
2026-08-29 04:24:40 +00:00
Sylpheed RE agent
93e9b185ea re(ui): the rest fallback fires on 2 elements, and "last keyframe" wins there
Scored candidate rest-pose rules by rendering and correlating instead of
arguing, and both results correct something I had published.

First, the exposure. The guessing fallback is reached only by an element
that is plateau-less AND multi-keyframe -- a single-keyframe element
short-circuits at `match len { 1 => first }`. Per screen:

  title (4)               24 elements   2 plateau-less   0 reach the fallback
  main menu (5)           16            5                0
  EXTRAS (6)              18            5                0
  publisher splash (10)    3            2                1
  developer splash (11)    7            2                1

So on the three screens the port cares most about, rest() never guesses.
That is why three different rules render builds 4/5/6 to identical
correlations -- the code is unreachable there, which I nearly read as
"the choice does not matter".

Second, where it does fire, the last keyframe is markedly better:

  publisher splash   dwell +0.9600   last +0.9982   maxalpha +0.9600
  developer splash   dwell +0.9643   last +0.9758   maxalpha +0.9643

That refutes my own earlier refutation. I had killed the last-keyframe
rule by arguing it makes palogo_anima_eff invisible while its two
siblings stay lit, which looked like an artefact. The capture says
otherwise: making it invisible is what improves the match. The sibling
symmetry was my expectation, not evidence.

Caveat kept in front: both captures are single frames of a transient
animation, so this fixes which pose matches THOSE frames, not which is
canonically at rest. Default unchanged -- better on both screens where it
fires and identical on the other three, but it would move 2 305 elements
disc-wide on two measurements. Reachable via SYLPHEED_REST_RULE=last.

Also confirmed: all 195 zero-scale rest poses are inside the corrected
2 305 ambiguous population; none is a single-keyframe element.

METHOD: score a rule where it can differ, or you measure nothing; and an
argument from symmetry is a prediction, not a refutation.
2026-08-29 04:20:27 +00:00
Sylpheed RE agent
0265da31a1 re(ui): refute my own fix for rest(), and correct the defect rate by 65%
Two corrections from one experiment.

A keyframe group is entry -> hold -> exit, and the exit ends invisible:
on the five port screens the final keyframe is invisible for 21/24
(title), 8/16 (main menu), 12/18 (EXTRAS), 2/3 and 6/7 (splashes). So the
screen as seen is the HOLD, which is why rest_plateau is the right
primary rule and why "rest = last keyframe" would empty every screen.

That suggested a fix: an element with no hold has no representative pose,
so draw nothing rather than guess an endpoint. Tested through compose's
visible mask and correlated against the live captures:

  title       +0.9500 -> +0.6839   -0.2661
  main menu   +0.9460 -> +0.9037   -0.0423
  EXTRAS      +0.9440 -> +0.9094   -0.0346

Refuted on all three, and the reason invalidates a number I published. An
element with a SINGLE keyframe has no adjacent pair, so the plateau test
marks it plateau-less -- but its one pose is unambiguously its rest.
Suppressing those removes backgrounds and full-screen layers, which is
the title's -0.27.

  no plateau (as published)      3 807  (24.57 %)
    ... single-keyframe          1 502  trivially at rest, not a guess
    genuinely ambiguous          2 305  (14.88 %)

So rest() guesses for 2 305 elements, not 3 807 -- the figure I gave the
port overstated the defect by 65%. Corrected in HANDOFF and the page.

METHOD: a predicate over adjacent PAIRS silently misclassifies a
one-element list; and acting on a claim is a better test of it than
re-reading it -- this flaw survived a census, a write-up and a handoff
row, and died the moment the rule was used to change a rendering.
2026-08-29 04:15:58 +00:00
Sylpheed RE agent
60285a6ead re(ui): replicate the keyframe-time shift -- three elements, two screens
The case for reading +36 as "the time the NEXT pose is reached" rested on
one element's fade-out shape, then on one element's hold duration. Both
splash halves supply more, and they agree.

  element            screen              observed hold  as decoded  shifted
  palogo_gamearts    developer splash        83 f          8 f       80 f
  palogo_seta        developer splash        83 f          6 f       80 f
  palogo_sqex        publisher splash       >=77 f *       6 f      102 f

  * the capture opens mid-hold at frame 1, so 77 is a floor.

The readings predict opposite structures. For palogo_gamearts, as decoded:
hold 8f, in 80f, hold 2f, out 6f, out 2f -- an eighty-frame FADE-IN and a
two-frame hold. Shifted: in 8f, hold 80f, out 2f, out 6f, out 2f. The
capture shows an 83-frame hold and no fade-in at all.

The elements that cannot discriminate are not contradicted:
palogo_gamearts_eff observed in 7f / hold 7f / out 8f, and both readings
give 8f phases -- with four blocks the shift only relabels which phase is
which. So the glows, which is where Q1's linear law was measured, say
nothing either way rather than arguing against.

The decoder's default is still unchanged, and the reason is now
articulated rather than assumed. The single thing opposing the shift is
rest() on ptlogo_eff3, where the shifted reading makes the longest-dwell
fallback return the bloom's 200% peak. That fallback is unsound whenever
it runs -- it returns an endpoint of a movement, neither of which is held
-- and checked: the shift does not fix it either. So the objection was
never evidence about the times. Timing had three discriminating
measurements; pose selection had a heuristic guessing.

For the port: animation timing should use the shift; static composites are
unaffected and the five screens' correlations stand. Classified measured,
not decoded -- three elements in one screen family, not a disc-wide check.
2026-08-29 04:09:36 +00:00
Sylpheed RE agent
fb6376a548 docs: one page saying how good the five screens actually are
The answers for the port's five screens were spread across a dozen
documents and none of them said how good the result IS. Measured:

  screen              build  drawn   corr vs capture  alignment
  title                 4    15/24      +0.9500       dy=0 dx=0
  main menu             5    11/16      +0.9460       dy=0 dx=0
  EXTRAS                6    13/18      +0.9440       dy=0 dx=0
  publisher splash     10     2/3       +0.9600       dy=0 dx=0
  developer splash     11     6/7       +0.9643       dy=0 dx=0

Every one aligns at exactly zero offset over a +/-2 px search in both
axes, so placement and scale are right and the residual is tone and
detail rather than geometry.

The drawn/total counts are not slack. Each undrawn element has a reason
already documented: kind & 0x4 ghost instances (4 on the title), .prm
primitives off by default (2 per menu, 1 per splash), loop* animations
off by default and off-screen at rest (2 per menu screen), and the 8AX
name mismatch (1 per menu screen) whose art reaches the screen anyway via
ptbase. 4+2+2+1 = 9, 2+2+1 = 5, 1. Nothing unexplained.

The residual is ranked for a consumer: tone first (gamma 1.34-1.49, the
game's own display ramp), then 8AX resolution, then one drawable
paint-order tie on EXTRAS alone, then rotation-decoded-but-not-rendered
which does not affect these five at rest.

Reach stated: these are static composites at the resting pose against
single frames, so nothing here speaks to animation, and a whole-frame
correlation is a sanity figure rather than a per-element check.
2026-08-29 04:04:15 +00:00
Sylpheed RE agent
0efd692b4c docs: the suite is heavy, not hung -- correcting my own "cannot terminate"
Last iteration I wrote that build-reborn test cannot finish in a working
session, from having watched it run 3h26m. That was the stronger claim
and I made it without measuring the work.

Timing `mesh info` on each of the 166 .xpr containers with a 25 s cap:

  files scanned                166
  exceeding 25 s                19   Hangar, 17 Stage_*, ptc_pack
  Stage_S02 to completion      144 s, rc = 0

Nothing hangs. Nineteen heavy containers at roughly two minutes each is
about 45-60 minutes for one pass, before the 147 fast ones. The 3h26m
observed was that hour of work running at a load average of 9-14 --
inflated by the two duplicate runs I had left going, which did not merely
coexist with the slowness but multiplied it.

The practical conclusion is unchanged and only the wording softens: an
hour-scale suite is not an iteration-scale gate, and every "green" I
reported from it this session was partial. But an hour-scale gate can be
run deliberately, whereas a hung one cannot be run at all, so the
distinction is worth having right.

File list committed as reference data so the cost is attributable without
re-scanning.

METHOD: a slow thing observed under contention looks like a stuck thing;
measure the work before choosing between "cannot finish" and "takes an
hour".
2026-08-29 04:02:34 +00:00
Sylpheed RE agent
145046f88f docs: the verification gate cannot terminate, and I left two runs going 4h
Two findings from checking whether last iteration's partial green had
finished. It had not, and the reason matters for anyone using the gate.

build-reborn test contains twin_pairs_do_not_share_a_buffer, which
decodes every .xpr in hidden/resource3d -- 166 files, 1.4 GB -- through
the full Xbg7Model anchoring path, and is NOT #[ignore]d. Its sibling in
the same file walks the same 166 files and IS ignored as known-failing,
which makes the binary's cost easy to underestimate. Measured: one
instance accumulated 3h26m of CPU at 89% without finishing.

So every "green run" reported in this corpus from a workspace or
sylpheed-formats test is necessarily PARTIAL unless it says the suite
terminated -- including the ones I reported this session. The honest form
is the suite count and elapsed state, not the word "green". Not proposing
to #[ignore] or subsample it: that changes what the suite asserts and is
the project's call, not an audit side-effect.

And the mess is mine. Two cargo test -p sylpheed-formats runs launched
detached in earlier iterations never exited, because they were sitting in
that test:

  pid 103375  4h12m elapsed  child mesh_consistency_disc  3h26m CPU  89.3%
  pid  99965  4h39m elapsed  child pak_idxd_disc          1h16m CPU  93.8%

Load average 14.18 on 12 cores. Killed, after checking the legitimately
running workspace suite and leaving it alone; load fell to 9.68.

What this does NOT explain, because it is tempting: the session's
emulator troubles. screenshot cost 0.49 s with both runaways live and the
emulator stopped, against 10.8 s measured earlier with the emulator
running. The 92x figure really was emulator contention; the runaways were
a background tax on top. The black surface and the unreachable title
stand as measured, with their own controls.

METHOD: a detached job you never check can outlive many iterations --
setsid was added so a timeout could not kill them, which also means
nothing does; and know whether your verification gate can terminate.
2026-08-29 03:45:14 +00:00
Sylpheed RE agent
6d9214fd07 docs: check that the docs' headline figures match their committed data
Nothing had ever verified that a number written in prose matches the
reference data file committed beside it. The figure is written once from
a run; the prose is edited around it afterwards and the data file is
regenerated independently, so drift is silent.

All 19 headline figures across four censuses -- the eff-bit census, the
plateau census, the top-level rotation census and the eff-bit alpha test
-- currently agree with their data files.

The checker had to be numeric, and the first attempt is the reason it is
a script rather than a grep: comparing strings reported almost every
figure as a mismatch, because the data files write 14709 where the docs
write "14 709" with a thin space, and the docs round 33.66 to 33.7. A
consistency check that fails on formatting trains you to ignore it, so
the tolerance is explicit: exact against the data, within 0.05 against
the doc to allow rounding.

Also ran the full disc-gated workspace suite (build-reborn test, which
wires SYLPHEED_DISC -- without it the disc tests self-skip and green
means almost nothing), covering this session's three decoder changes:
rotation_deg on Keyframe, the scale-0 fix in blit/fill_quad, and the
flags field on T8adImage. 122 passed / 0 failed across the four suites
that had completed; the long disc-gated integration tests
(records_roundtrip_disc, first_header_word_is_record0_hash) were still
running and are not counted here.
2026-08-29 03:42:13 +00:00
Sylpheed RE agent
714a26769d re(ui): premultiplied alpha refuted for bit 0x02; parking the field
A per-sprite premultiplied-vs-straight-alpha flag would matter a lot to a
port and has a sharp static signature: premultiplied means RGB <= A
everywhere. Over the 170 decoded GP_TITLE textures that pair to a flag
word:

  bit SET    n= 61   mean %(RGB>A) 55.52   median 52.52
  bit clear  n=109   mean %(RGB>A) 33.66   median 30.17

Premultiplied requires ~0% for the flagged group. Both groups are far
from it and the flagged group violates MORE -- the opposite of the
hypothesis. Refuted.

What remains is a weak association: flagged sprites carry more
bright-RGB/low-alpha pixels, which is what glow art looks like. But the
best single threshold classifies 76.5% against a 64.1% base rate -- a
12-point lift with badly overlapping distributions. A tendency, not a
rule, and reported with its base rate so it cannot read as more.

Noted for whoever returns: "0x02 selects an additive blend" was refuted
by blending those sprites additively and finding every measure worse
against the capture -- but that ran through a title render since fixed
twice (rest_plateau, and the 8AX background the composer drops). The
refutation may well stand; it was measured through a renderer with known
other errors, so it is worth one re-run if blit ever gains additive
blending.

Parking the field. Four candidate meanings are dead -- additive blend,
eff name in both directions, transient element, premultiplied alpha --
none produced a positive account, and the bit blocks nothing: the port's
screens composite at 0.947 correlation against a capture without it. The
negative space and the sound attribution method (child order, not size)
are written down so a later attempt starts here.

METHOD: report a classifier's lift over its base rate; and park a field
after N failed hypotheses, saying what was eliminated.
2026-08-29 03:23:40 +00:00
Sylpheed RE agent
489ea12759 re(ui): the eff-name implication for bit 0x02 is refuted disc-wide
Last iteration I killed the biconditional and reported that the one-way
reading survived: all 10 bit-set sprites on GP_TITLE build 4 are eff
names, so "bit set => eff name". Checked over the disc, that is false.

  sprites with a resolvable preceding name   14 709
    bit SET   & name has 'eff'                2 338
    bit SET   & name lacks 'eff'              2 657   <-- counterexamples
    bit clear & name has 'eff'                1 399
    bit clear & name lacks 'eff'              8 315

  P(eff | set)   = 0.468
  P(eff | clear) = 0.144

The implication fails more often than it holds. What survives is an
association -- 3.3x enrichment -- and build 4's 10/10 was a local naming
habit in an 18-element bundle, not a format rule.

The counterexamples are the useful part: pv_loading_ring0,
pv_loading_light0-3, pv_loading_line, px_bunk_line, px_top_extra. Rings,
glows, lights, thin lines -- effect-like artwork that does not carry the
eff naming convention. Consistent with the bit marking effect sprites by
authoring intent rather than by name, which is a description and not a
decode, and is labelled as such.

Names here come from the string immediately preceding each T8aD,
validated 17/18 on build 4 against the RATC child order; the single
mismatch is the known pteff04.t32 -> registered as 8AX case, so this is
the element (opt) name rather than the sprite's registered name. That
mismatch is itself an independent confirmation of the 8AX finding,
reached from the opposite direction.

METHOD: a pattern perfect on one screen can be near-chance on the disc;
and when an association survives a refuted implication, the
counterexamples are the finding.
2026-08-29 03:20:24 +00:00
Sylpheed RE agent
5f701db588 re(ui): kill two candidate meanings for the T8aD 0x02 bit, and fix attribution
The bit at +0x04 was recorded as a real field with its meaning "not
diagnosed", noting ptlogo_back2eff is 0x8830 "despite its name". That note
rested on a size match -- and its size is ambiguous, which is the trap
this corpus already records.

First, a sound attribution. T8aD headers appear in the bundle in RATC
CHILD ORDER, verified on GP_TITLE build 4 against an independent property
-- each header's decoded dimensions versus the dimensions the named child
should have: 18 of 18 match, 0 mismatches. Two of those eighteen share a
size (ptlogo_back2eff and ptlogo_back2eff5, both 1133x280), so a size-keyed
lookup cannot separate them; ordering can. Index 12 is back2eff5 (0x8832,
bit set), index 14 is back2eff (0x8830, bit clear). The documented
counterexample is real and correctly attributed -- now on evidence.

Two candidate meanings tested and refuted:

  bit <=> name contains "eff"        REFUTED: ptlogo_back2eff is an eff
                                     name with the bit clear. All 10
                                     bit-set sprites are eff names, so
                                     the implication holds one way only.
  bit <=> the element is transient   REFUTED: pteff03/pteff03a carry the
                                     bit and run to t=250, ramping to
                                     a=255 and holding.

Up close, the exception pair differs in two header words: +0x04
0x8832/0x8830 and +0x08 0x8083/0x8081 -- layer keys 32899 and 32897. They
are NOT duplicates: their alpha summaries agree to one decimal (4.5%
opaque, 86.7% clear, mean 19.3) but a pixel compare gives max abs diff 21.
Two renditions of one image at one size, which is why the summaries were
not trusted.

Still not diagnosed, and said so -- but the search space is two smaller
and the attribution beneath it is now sound.

METHOD: T8aD headers sit in child order, use that not the size; and
identical summary statistics are not identical data.
2026-08-29 03:14:40 +00:00
Sylpheed RE agent
c463f98164 re: the GPU trace is compiled out of the release build -- both my guesses wrong
Last iteration left two candidates for why trace_gpu_stream produced no
file: the CLI flag not reaching the cvar, or BeginTracing failing
silently. Neither. Following the code instead of guessing:

BeginTracing only sets trace_state_ = kStreaming ("Streaming starts on
the next primary buffer execute"). The file is opened later, in
ExecutePrimaryBuffer, inside

  #if XE_ENABLE_TRACE_WRITER_INSTRUMENTATION == 1

and trace_writer.h defines that as 0 under NDEBUG, 1 otherwise -- the
trace writer exists only in debug builds.

Confirmed against the binaries, with a control. The format string
"{:08X}_stream.xtr" lives only inside that guard:

  build/bin/Linux/Release/xenia_canary          0 occurrences
  build/bin/Linux/Debug/xenia_canary            1 occurrence
  /sylph-home/re/canary-build/.../Release/...   0   <- what run-canary uses

The debug binary is the control: it proves the test finds the string when
it is present, so the release zero means something.

So trace_gpu_stream is a no-op in this container's emulator -- the cvar
parses, BeginTracing runs, and nothing can open a file. The kill -9 was
not the cause either, though it would have destroyed a trace had one
existed.

The route exists but is not cheap: a debug build with the writer compiled
in sits at build/bin/Linux/Debug/xenia_canary, 253 MB against Release's
18 MB, so a much slower boot plus a trace of every GPU packet on a disk
at 95%. Recorded as available rather than attempted -- what it would
confirm, the DC_LUT write, is already a well-supported inference, and the
cost is out of proportion to the gain.

METHOD: a cvar existing does not mean the feature is compiled in; and
test a compile-time gate against the binary, with a control.
2026-08-29 03:08:57 +00:00
Sylpheed RE agent
921a3cfc44 re: GPU trace attempt produced nothing -- and the config dump is not the flags
Tried to turn the gamma-ramp inference into a direct observation.
canary's trace_gpu_stream records gamma ramps as their own command type
(kGammaRamp, index 11 in TraceCommandType), so a boot trace should show
the write. Two bounded runs produced NO trace file at all -- nothing under
the prefix, no .xtr anywhere, no scratch/gpu/.

Bounded deliberately: the disk is at 95% (50 GiB free) and a trace of all
GPU packets during boot includes video decode, so the runner carried its
own watchdog that killed the emulator the moment output passed a 2 GiB
cap. It never fired -- there was nothing to cap -- and disk stayed at 95%
throughout. Bounding from inside cost nothing and removed any need to
gamble on how coarsely I could poll.

What the attempt did establish. BeginTracing() runs at GPU init when the
cvar is set (graphics_system.cc:237), but EndTracing() runs only from
GraphicsSystem::Shutdown() -- so the kill -9 this session has used
routinely can never finalise a trace. The second run was stopped with
SIGTERM and exited cleanly; still no file, so that is not the whole
story. Two candidates remain unseparated: the CLI flag not reaching the
cvar, or BeginTracing failing silently. The next run removes the
ambiguity by setting trace_gpu_stream in the config FILE instead.

And a trap I nearly fell into. The startup config dump showed
trace_gpu_stream = false after I passed --trace_gpu_stream=true, which
reads as "flag ignored". It is not evidence either way: the gamma run
passed --log_mask=12 --log_level=3, its dump printed log_mask = 0 and
log_level = 2, and Kernel Debug logging was demonstrably ON -- that run
is where VdGetCurrentDisplayGamma was captured. The dump reflects the
config file and can neither confirm nor refute a command-line override.
(It does not undo the earlier user_language conclusion: absence of a NAME
from the dump still shows a cvar is unregistered.)

The gamma-ramp write therefore remains an inference, unchanged.
2026-08-29 03:05:03 +00:00
Sylpheed RE agent
421c61a517 re(ui): close the gamma chain from canary's defaults -- the game writes a ramp
Continues the previous iteration, where the game was measured calling
VdGetCurrentDisplayGamma at video init. The remaining link -- does it then
WRITE the ramp -- is a GPU register operation (XE_GPU_REG_DC_LUT_RW_INDEX
in CommandProcessor::WriteRegister), unlogged and invisible to kernel
logging. Two facts from the source close it without instrumenting.

1. The swap-path gamma stage is a PURE LUT. apply_gamma_table.xesli is
   the whole transform: index by input*255, fetch from a 256-entry ramp
   buffer, output. No sRGB encode, no second transfer function.

2. The table DEFAULTS TO IDENTITY. CommandProcessor::Initialize fills it
   with value = i * 0x3FF / 0xFF, and its own comment says the linear
   default is "what games set when starting with the sRGB (return value 1)
   VdGetCurrentDisplayGamma". An unwritten ramp is a no-op.

So the only transform is a LUT, the LUT is identity unless written, the
game queries the display gamma at init, and the capture differs from our
composite by gamma 1.34-1.49 -- which identity cannot produce. The guest
wrote a non-identity ramp.

Labelled an inference, with its weak joint named: it assumes our
composite reproduces the PRE-RAMP framebuffer, which it does not exactly.
What carries it is the shape -- a systematic ~1.4 fitted on flat patches
across three screens is not a compositor bug. The obvious alternative, a
fixed sRGB stage in the presenter, fits neither direction: an encode
(^0.45) brightens and we measured darkening; a decode (^2.2) darkens far
more than 1.4.

Direct observation remains available and cheap, and needs the emulator
only to boot: a GPU trace records gamma ramps as their own command type,
or one log line at the DC_LUT register write would settle it outright.
Not done.

METHOD: a default value is evidence; and name the weak joint of an
inference in the same breath as the conclusion.
2026-08-29 02:53:45 +00:00
Sylpheed RE agent
c6ce000002 re(ui): the game does query the display gamma -- measured, with its control
Last iteration's corrected experiment, run. Boot with --log_mask=12
--log_level=3 (Kernel logging on, Cpu/Gpu off), which changes nothing
about the output and so cannot perturb the capture harness the way the
gamma-cvar experiment would have.

VdGetCurrentDisplayGamma is called once, at video init:

  d> VdGetSystemCommandBuffer(701CF830, 701CF804)
  d> VdGetCurrentDisplayGamma(701CE1F8(00000000), 701CE1F0(0))
  d> VdSetDisplayMode(40000000)
  d> VdGetCurrentDisplayInformation(701CF110)

The control is in the same log: 359 VdRetrainEDRAM and 358
VdGetSystemCommandBuffer lines, so an absent call would have been
visible. Per the export's own comment the returned type is "used in D3D
SetGammaRamp/SetPWLGamma" -- the game asks the question a ramp-builder
asks, at the moment one would ask it.

Still open, and stated: whether it then WRITES the ramp, and whether the
measured gamma 1.34-1.49 is that ramp. The write is a GPU register
operation (DC_LUT), invisible to kernel logging; a GPU trace records
gamma ramps as a command type (TraceWriter::WriteGammaRamp), which is
where to look next.

Worth its own METHOD line: this had been parked behind "needs the
emulator to reach a menu" for several iterations, and it needed the
emulator only to BOOT -- video init happens in the first seconds. A
blocker that stops one experiment does not stop every experiment in the
same area.
2026-08-29 02:51:32 +00:00
Sylpheed RE agent
93b0a6b20f re(ui): the gamma confound is refuted from source -- canary applies none of its own
I had parked the tone-curve finding behind "this may be the emulator, not
the game: canary applies kernel_display_gamma_type = 2 (BT.709) on
output", with a planned run setting it to 0 and re-fitting. Reading the
source kills both the confound and the experiment.

VdGetCurrentDisplayGamma_entry is a kStub GETTER the guest calls
(xboxkrnl_video.cc). Its own comment: "Used in D3D SetGammaRamp/
SetPWLGamma to adjust the ramp for the display." The cvar is a value
REPORTED TO THE GAME, which then builds its own ramp. Canary's role is
downstream: the guest writes DC_LUT, command_processor.cc reads it into
gamma_ramp_256_entry_table_, and the swap path applies it via
swap_apply_gamma_pipeline_layout with apply_gamma_table.ps /
apply_gamma_pwl.ps compiled in.

So there is no emulator-side BT.709 post-process to subtract, and any
gamma in a captured frame is a ramp the game installed.

What is NOT established, and the reach is stated: that this game installs
a ramp at all, or that the measured 1.34-1.49 is it. The run logs cannot
say -- kernel exports log at Debug and this harness masks Kernel logging
(log_mask = 13, per boot_menu.sh's own comment), so their silence is
guaranteed regardless of what the game did.

The planned experiment was wrong in design: changing the cvar changes
what the GUEST is told and therefore which ramp the GAME builds, so it
could never isolate a stage that does not exist -- and it perturbs the
capture harness, since skip_intro classifies movie-vs-static on an
absolute rmse threshold that a brighter frame biases. The right run
changes nothing about the output: LOG_MASK=12 LOG_LEVEL=3 and look for
the call and the DC_LUT writes.

Also for the port: the ramp depends on the display type the game is told,
and canary hard-codes TV/BT.709 where hardware uses a console setting. So
this is a display profile, not a fixed property of the game.

METHOD: read what a cvar does before building an experiment around it;
and an absence in a log is only evidence if the log would have shown it.
2026-08-29 02:44:30 +00:00
Sylpheed RE agent
3490fba9e3 re(ui): settle 8AX vs ptbase statically -- the game draws the full-res one
I had parked this as "needs a per-draw capture recording texture base
addresses". It did not. 8AX (1280x720) and ptbase (640x360 at 200%) are
the SAME artwork at two resolutions, which is exactly why comparing
either against a capture is inconclusive -- and why comparing their
DIFFERENCE is not. Compute 8AX - upscale(ptbase), the detail only 8AX
has, and ask whether the capture contains it.

Both candidates are first mapped into the capture's tone domain with the
measured gamma; without that the residual is dominated by the tone
difference and the test is blind.

  main menu  corr +0.0475   controls +0.0032 shift, -0.0075 flip   68% of ceiling
  title      corr +0.0634   controls +0.0095 shift, +0.0086 flip   68% of ceiling

Two independent screens, both at 68% of the theoretical ceiling
(sd of the 8AX-only detail over sd of the capture residual), 7-15x their
matched controls. The controls preserve spatial correlation and destroy
only alignment, so they are what "no signal" looks like.

So the recommendation changes: resolve the name and draw 8AX at 1:1.
Upscaling ptbase 2x is wrong, not merely softer. Still do not draw both --
an opaque layer over an identical one costs fill and hides later changes,
and ptbase's element is the one carrying the keyframes, so a consumer
needs its timing with 8AX's pixels.

Also recorded and withdrawn: a cruder pixel-pair test gave 0.00-0.72 for
upscales, 0.98 native and 1.01 for the capture -- apparently decisive.
Additive noise raises both terms of that ratio equally and drives any
value toward 1; fitting a noise term, both "native + noise" and "bilinear
+ noise" reproduce the observed numbers. The conclusion is right, that
test does not establish it, and it is in REFUTED because the number looks
conclusive and is not.

Not shown: whether ptbase is also drawn underneath. 8AX is ~86% opaque
and carries the same art, so it would hide it either way.
2026-08-29 02:40:53 +00:00
Sylpheed RE agent
aaaa08b164 docs: the UI decode's own evidence images were unreachable -- 11 links repaired
The brief's rule is to commit reference data beside the finding so the
port can be built without a disc. Nothing had ever checked that the docs'
cited artifacts actually exist. doc_link_check.py walks every markdown
file under docs/, resolves each relative link, and reports targets that
are missing -- and separately targets that resolve to a ZERO-BYTE file,
which looks fine in any listing.

  links resolving   1038 -> 1049
  missing targets     16 -> 5
  empty targets        0 -> 0

+11 resolving and -11 missing against 11 edits: the counts pair, which is
the confirmation the pass did what it claimed and touched nothing else.

Two of the sixteen were the evidence for the UI layout decode itself.
structures/ui-rat-layout.md is what the port is built on, and its two
figures -- backing "the tutorial PAUSE menu rebuilds pixel-accurately
from its sprites" and "the same method reproduces the main menu" -- were
written as captures/ui-layout/... from a file in structures/, one
directory too shallow. The headline evidence for the decode could not be
opened from its own document.

Eleven links had the wrong relative depth with the target present. Each
was rewritten only where exactly one candidate path resolved, so nothing
was guessed; the first pass left three alone because equivalent spellings
(captures/../captures/x) failed to collapse, and a second pass normalised
them.

Five remain genuinely absent and are left rather than invented: two point
at MEMORY.md outside the repo, one at a header in the separate
xenia-canary-native tree, and two name documents that were never written
(weapon-datasheet-runtime.md, canary-build-verified-env-confound.md).
None is port-relevant. A missing document is a different problem from a
bad path and is not something a link fix should paper over.
2026-08-29 02:34:16 +00:00
Sylpheed RE agent
7373035868 re: the port was still being told SE audio is undecodable -- it is not
A resolve-check on HANDOFF's own rows. Q8 read "SE audio is undecodable
from the disc -- no XACT container exists anywhere". menu-audio-cues.md
retracted exactly that ("### Retracting 'cannot be extracted'") and
locates three cues in Static.slb that decode to PCM: d-pad move 0x1ec0
(4 packets), (B) back 0x0ec0 (2), (A) confirm 0x5d6c0 (6), all mono
48 kHz. The retraction landed in docs/re/ and the page the port reads
kept the superseded text -- the fourth time in this corpus.

Writing the rule down has not worked, so there is a tool now.
handoff_lint.py flags every HANDOFF line making a strong negative claim
that links a doc containing retraction language. First run: found the Q8
row, plus one benign false positive (Q3 links a doc whose retraction is
about a sprite count, not about the tie-break -- checked, and HANDOFF
repeats none of the retracted figures). The lint also caught its own bug
first: it reported existing docs as missing because it joined a guessed
repo root, so it now resolves links relative to the file as markdown does.

Separately, EXTRAS's paint-order risk narrows twice more. Of its 15 tied
pairs only 2 overlap, and of those, ptloop01 x ptloop02 are loop*
animations compose skips by default -- so exactly ONE tie can be drawn:
ptframe3 x ptframe4, overlapping 102x132 px. Against live-extras.png that
contested region correlates +0.9622, better than the whole frame (+0.9440)
and inside the range of regions where order cannot matter (+0.8502 /
+0.9903). Consistent with our order, not proof: correlation cannot see a
swap between locally similar art.

15 -> 2 -> 1 -> consistent is now the whole paint-order risk on the five
screens, and HANDOFF says so.
2026-08-29 02:31:37 +00:00
Sylpheed RE agent
f7f9b555f6 re(ui): the paint-order tie-break is undecodable from the bundle
Q3 was delivered as "decoded: a u16 layer key at +0x0A". The audit last
iteration showed the key does not fully order a screen -- elements
sharing a key are tied, and on the title that tie-break decides two total
occlusions. This searches for what breaks the tie, and closes it as a
negative with reach.

The game paints the five tied ptlogo_back2eff glows in the order
eff1, eff2, eff5, eff3, eff4. Three static structures were searched:

1. The declaration table. Entries 14-18 are byte-identical apart from the
   pivot, which is only half the sprite's own size.

2. The T8aD headers. All five carry identical +0x04 (0x8832) and
   identical +0x08/+0x0A (32899, the key itself), differing only in
   position and tile count. Searched exhaustively -- every offset
   0x00-0x7f, u8/u16/u32, ascending and descending:

     fields sorting to the MEASURED order:              0
     fields sorting to the DECLARATION order (control): 64

   The control is the point: 64 fields can be found that reproduce a
   known ordering, so the scan finds ordering fields when they exist. It
   finds none for the order the game uses.

3. The RATC child order -- a genuinely different permutation on other
   screens -- gives eff1..eff5 here, declaration order again.

All three static orderings give eff1..eff5; the game gives eff1,2,5,3,4.
That agrees with ui-screen-runtime's conclusion from the other direction:
the game builds a reordered child list at load time and paints that.

Q3 now reads honestly: the layer key is decoded and orders 4 of the 5
measured bundles exactly; the tie-break within a key is undecodable, and
a consumer must use a measured order or accept declaration index as an
arbitrary stand-in. The port's exposure remains 2 overlapping tied pairs
on EXTRAS.

METHOD: an exhaustive field search needs a positive control, or "found
nothing" is worthless.
2026-08-29 02:26:19 +00:00
Sylpheed RE agent
ba47bdebe8 re(ui): measure the paint-order hedge -- exact on 4 of 5, and bound the rest
`compose` claimed the derived paint order "reproduces both measured
orders up to ties". That sentence was never measured and was stale by
one: there are three measured orders, not two. examples/paint_order_audit.rs
checks it.

  main menu (entries 5, 8)      derived == measured   0 inverted pairs
  developer splash (11, 14)     derived == measured   0 inverted pairs
  title (entry 4)               DIFFERS               8, all same-key ties

So the claim holds and the exception is entirely ties -- but two of those
ties are total occlusions, not near-misses. The tied family is the five
ptlogo_back2eff glows (key 32899); back2eff5 is 1133x280 and FULLY
CONTAINS back2eff3 (82,824 px^2 = 100% of the smaller) and back2eff4
(152,047 px^2 = 100%). Derived paints it on top of two glows it entirely
covers; the game paints it underneath. A tie-break by declaration index
can therefore be wrong by a whole layer. The title itself is unaffected --
it has a measured order.

The port's actual exposure, per screen: title, main menu and developer
splash all use MEASURED orders; the publisher splash is derived but has
ZERO ties, so it is fully determined; EXTRAS is derived with 15 tied
pairs of which only 2 OVERLAP. Two element pairs on one screen is the
whole risk, and that is what HANDOFF now says -- not the raw 15, which
would have overstated it 7x.

Reach stated: this compares the derived order against orders measured
from the game, not an independent derivation, so where no measured order
exists only the tie exposure can be checked. Overlap uses pivot*2 as the
element size at its resting placement.

Stale comment in compose corrected. METHOD: a hedge in a code comment is
an unmeasured claim; and count the cases that can bite, not the ones that
match the pattern.
2026-08-29 02:22:01 +00:00
Sylpheed RE agent
c3cf3c2e81 re: a title negative that survives its own cross-check
Three earlier "the title never appears" claims came from instruments
later found broken -- a stale pixel oracle, a 41 s sampling interval, a
freezing stream. This one carries its own evidence.

title_probe_xchecked.py restarts its capture stream every 30 s AND prints
its reading beside an independent `import` grab every 60 s:

  1851 frames in 560.2 s = 3.30 fps
  cross-checks 9, disagreements 1
  max glyph 0

  t= 62s stream   6.05 | import   0.07  disagree (a fade, logos mid-transition)
  t=123s stream   7.40 | import   7.49  agree
  t=183s stream   8.18 | import   8.29  agree
  t=243s stream   0.23 | import   0.10  agree
  t=311s stream  89.68 | import  89.51  agree
  t=371s stream  80.97 | import  81.58  agree
  t=426s stream 117.43 | import 117.72  agree
  t=487s stream  77.71 | import  76.25  agree
  t=546s stream  70.43 | import  70.55  agree

Eight of nine agree within 2%, fps held at 3.30 with no collapse to 1.60,
and the surface moved through dark and bright phases. So the frames were
live: over 560 continuous seconds from launch, sampled 3.3 times a
second, the interactive title's green (A) plate never appears while the
game renders throughout. The final frame correlates 0.0145 / -0.0047 /
0.0102 with our title / main menu / EXTRAS renders -- attract-movie
content, not a UI screen.

Why remains unknown. live-title-press-a.png with its 753 glyph pixels
proves the title was reachable from this container on 2026-08-28, and
clearing the shader cache fixed the black surface but not this.

The two emulator-side questions (gamma control, 8AX vs ptbase) are
therefore blocked on a characterised failure rather than a suspicion.
Neither blocks the five menu screens, so I am returning to static work;
the probe is committed for whoever picks it up.

METHOD: a probe that cross-checks itself turns "no result" into a result.
2026-08-29 02:14:21 +00:00
Sylpheed RE agent
46006406a4 re: the fast probe stalls -- its own dense negatives are withdrawn
Cross-checked the instrument built last iteration against an independent
grabber while both watched the same screen, and it fails.

A single long-lived ffmpeg x11grab stream degrades and then freezes:

  862 frames in 540.1 s = 1.60 fps        (it starts at 3.98)
  t=450/480/510/540 s: surface mean 5.21, identical every time

At that same moment `import` read surface mean 125.65, and a freshly
started ffmpeg stream read 122.43 -- agreeing with import to 3%. So the
acquisition was broken, not the analysis: the stream replayed a stale
frame while the screen was 24x brighter.

That withdraws last iteration's headline. "2391 frames over 600 s from
t=0, max glyph 0" cannot distinguish "the title never appeared" from "the
stream froze early and repeated one frame 2391 times". Its 3.98 fps was
measured over the first 20 s, before the degradation. Sample count is not
coverage unless the samples are known independent.

Fixed: the stream is now torn down and restarted every 30 s. Startup is
~0.3 s, cheap against the title's window, and it guarantees live frames.

Separately, the cache hypothesis was tested and is SUPPORTED. cache,
cache0, cache1, cache_host moved aside (to /tmp/xenia-cache-aside, not
deleted) and the surface renders again: import reads mean 54.8 and 68.6
with 100% non-black warm content, against 0.07 and 0.08% non-black in the
black run; 773 of 862 probe frames had >2% non-black. One run each side
and many kill -9s before the black one, so it is supported, not proven --
the old caches are kept for reproduction.

Still no title, but that number now comes from a stalling probe and
establishes nothing either way.

METHOD: validating a probe on static images tests its analysis, not its
acquisition -- cross-check against an independent grabber during a run.
2026-08-29 02:01:47 +00:00
Sylpheed RE agent
a3946e5005 re: the game surface is rendering BLACK -- check that before explaining absences
The named experiment was to attach the fast probe at t=0 so the boot
title could not be missed. Done, default config, English:

  2391 frames in 600.4 s = 3.98 fps; max glyph 0; hits 0

Ten minutes sampled four times a second FROM LAUNCH, no green-(A) glyph.
So "the plate only shows in an early boot window I keep missing" is mine
and refuted -- the third explanation refuted in three iterations.

Then the check that should have come first. Splitting the raw root grab
into bands:

  y   0- 44 (GTK menu bar)   100.00% non-black   mean 210.50
  y  45-719 (game surface)     0.08% non-black   mean   0.07

The game is rendering black, reproducibly across back-to-back samples,
while the guest is alive and polling input (XamInputGetKeystrokeEx past
1201 calls) and MEM-WATCH keeps reporting. The crop and every pixel
oracle were correct; there was nothing on the surface to detect.

What this does NOT do is retroactively explain the earlier failures, and
claiming so would be the fourth over-reach in a row. Those runs had
content: run 2 sampled mean 33.1, run 3's classifier measured real
frame-to-frame rmse, the gamma_type=0 run measured mean 122.8. The
failure mode CHANGED over the session; black is the newest and worst.

Hypothesis for the regression, untested: canary's shader/pipeline cache
is 47 MB and was last written 23:49 on Aug 28, during the failed runs,
and this session has kill -9'd the emulator repeatedly. The test is to
move cache* aside and boot again -- one line and one run, not done.

Two METHOD lines: ask whether the screen is drawing anything before
explaining why a feature of it is missing; and a newly found fault does
not retroactively explain older failures.
2026-08-29 01:49:42 +00:00
Sylpheed RE agent
e9924ff9e8 re: build the fast probe -- and it refutes the diagnosis that motivated it
Last iteration I blamed four failed runs on the probe sampling every
~41 s, slower than the title screen lasts, and withdrew three earlier
conclusions on that basis. Building the fix tested the claim and killed
it.

The speedup is real and control-verified. One long-lived ffmpeg x11grab
stream, raw RGB, glyph counted in numpy -- no per-sample process startup,
no PNG encode, no convert -crop:

  wrapper `screenshot`            3.98 s per sample (emulator running)
  import -window root -> PPM      1.20 s
  long-lived x11grab stream       0.29 s          13.7x

The counter is byte-identical to is_title.py: 753 on the committed title
capture, 327 on the main menu.

Pointed at a running game it says the opposite of what I expected:

  332 frames in  85.3 s = 3.89 fps; max glyph 0
  1674 frames in 420.0 s = 3.99 fps; max glyph 0

1674 consecutive samples over seven unbroken minutes, four per second,
zero green-(A) pixels. Sampling rate was a real defect that happened not
to be the cause.

So "neither locale reaches the interactive title without a pad press" --
withdrawn last iteration for want of evidence -- is reinstated, now as a
dense measurement, with its reach stated: a MID-RUN window only, silent
about the boot title.

Leading hypothesis, unconfirmed: the PRESS (A) plate appears only in the
boot title window and the attract loop's title carries none, which is
exactly what title_states_capture.sh was written to test. The experiment
is to start the fast probe from t=0 rather than attach to a run already
in progress.

METHOD: fixing the instrument is how you test the explanation that blamed
it -- a plausible mechanism is a hypothesis, and the fix is its
experiment, not its proof.
2026-08-29 01:34:40 +00:00
Sylpheed RE agent
cc4e5e04a7 re: the boot harness was blinking slower than the title -- diagnosed
Four consecutive runs failed to reach the interactive title, across two
locales, two launch paths and two display-gamma settings. I attributed it
in turn to a stale oracle, to the locale, and to the attract loop. It was
none of those.

  one `screenshot` call, emulator running:  10.8 s
  one `screenshot` call, emulator killed:    0.117 s

92x, measured at a 1-minute load average of 1.80 -- so it is contention
with the emulator through the X server, not background load.
skip_intro.sh takes two grabs per iteration plus a numpy import, giving a
median sampling interval of 41 s in the last run (38/82/41/20/30/30/35/
47/46/45/44/43/21/19). The title lasts "a few seconds" before the attract
loop reclaims it -- wait_title.sh's own header says so. The harness was
sampling slower than the event it was waiting for. That also explains why
runs at 16:43-18:05 the same day succeeded.

Withdrawn as CAUSES, though the observations stand: "the JP run never
reaches the interactive title", "neither locale reaches it without a pad
press", and "the game sat in the attract loop for 604 s". The English
control did control for locale -- it just shared the same defect.

Also recorded: I set kernel_display_gamma_type = 0 for the gamma control
run, which brightens the frame (mid-attract mean 122.8 vs 52.5/82.8 at
type 2) -- and skip_intro classifies movie-vs-static on an ABSOLUTE rmse
threshold, so the gamma change biased the very classifier the run
depended on. Changing a display setting and a capture behaviour in one
run confounds both. Config restored to type 2.

The fix is not applied: make the probe cheap enough to outpace the title
window (small region, no convert round trip, one long-lived process).
Every remaining emulator-side question is waiting on that.
2026-08-29 01:18:05 +00:00
Sylpheed RE agent
ee73de50be 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
2b4ec20f00 re(ui): a full-screen element is dropped on three port screens -- do not "fix" it
Audited what `screen render` silently omits on the port's five screens,
since an element the game draws but we skip is the one defect class the
port agent has actually hit. Everything is accounted for -- kind & 0x4
ghost instances, .prm primitives, loop* animations -- except pteff04.t32
on the title and pteff05.t32 on both menus. Those are kind 0x0, one
keyframe, rest a=255, pivot (640,360): full-screen and opaque.

Cause: the element declares pteff05.t32, but the T8aD behind its `opt `
link is registered under the name 8AX, so build.sprites.get() misses and
compose hits a silent continue. Bytes at 0x0e2035 of GP_TITLE entry 5:

  opt  ... 70 74 65 66 66 30 35 2e 74 33 32 00  38 41 58  54 38 61 44
           p  t  e  f  f  0  5  .  t  3  2  \0  8  A  X   T  8  a  D

8AX is 1280x720 and present in all six title-family bundles; it is a
sprite in builds 4/5/6 and never an element.

It does not currently show, and that is the useful half. ptbase.t32 is
640x360 drawn at 200% and carries THE SAME ARTWORK: its 2x upscale
differs from 8AX by mean abs diff 2.05 (max 80), and our rendered
background is pixel-identical to 8AX in every patch sampled. So resolving
the name and drawing it in addition would double-draw an opaque
full-screen layer -- invisible as a doubling, which is worse than a
visible bug. Written into HANDOFF as a do-not-do.

The free win, offered and not taken: use 8AX at 1:1 and drop ptbase
instead of upscaling a half-res copy. That is a rendering choice and
ptbase's element carries the keyframes, so it is the port's call.

Not established: which of the two the game actually draws. Both carry the
same art, so pixels cannot separate them; it needs a per-draw capture
recording texture base addresses, since the two differ in size.
2026-08-29 01:00:36 +00:00
Sylpheed RE agent
7d07eb945f re: the title capture is not reachable here either; stopping this line
Run 3 used the proven path rather than my own probe: launched exactly as
boot_menu.sh does (DISPLAY=:98, --apu=sdl, /dev/shm/xenia_* cleared, the
existing profile signed in) and drove skip_intro.sh, the detector that is
documented to work and that is stricter than mine (glyph >= 800 AND a
static frame).

It classified every one of 20 samples over 604 s as "movie", waited them
all out, and timed out. A direct check at 604 s says it was right: zero
green-glyph pixels, screen_id = other, warm mean (69,53,40), correlation
0.09 with build 7. The game really was playing attract movies for ten
minutes.

Three runs, two locales, two launch paths, ~35 minutes of emulator time,
no interactive title. Either the attract loop is far longer than the
600-780 s windows tried, or something regressed since
live-title-press-a.png was captured -- that one came from a pad-driven
boot (88b3ce9, "booting to the main menu and walking it").

Also settled a discrepancy I had flagged without checking: there is no
contradiction with title_states_capture.sh. It waits on screen_id.py, a
LOOSER oracle than is_title.py, and was written precisely to test whether
the interactive title draws ptbtn00 and the attract one does not. It
never claimed to reach the interactive title.

Stopping this line. The capture blocks none of the five menu screens and
has now cost five iterations. The locale mechanism, the fixed oracle and
the launch recipe are all committed, so what remains is patience with the
attract loop, not tooling. METHOD line on knowing when to stop paying for
a non-blocking answer.

Emulator stopped, lock cleared, locale English.
2026-08-29 00:49:49 +00:00
Sylpheed RE agent
8783adda14 re: the English control removes the locale from the title-capture problem
Two iterations framed the JP title capture as possibly locale-specific.
It is not. Same flags, same oracle, English locale: 75 samples over 734 s,
every one glyph = 0. The English boot does not present the interactive
title either. Canary was alive throughout, polling
XamInputGetKeystrokeEx (1801 calls); the frame at 734 s has content but
correlates 0.15 with build 4's render and 0.05 with the main menu -- an
attract-movie frame, not title art.

So neither locale reaches the interactive title in ~12 minutes without a
pad press. What is actually untested is the pad: pad.py and
nav_to_flight.sh exist, and title_states_capture.sh reaches title states
with a flag set this probe did not replicate (--log_ui_draws
--ui_draw_capture_frames, plus xdotool window focus). It is a capture
problem, not a locale one. MISSION.md updated.

Also resolved a caveat I had given the port agent without checking it:
"rotation is decoded but not rendered" does NOT affect their five screens
at rest. Title, main menu, EXTRAS and both splash halves have ZERO
top-level elements with a non-zero rotation. The only rotations on any of
them are the title's two nested ptloop records (r = 30 and -45), and at
rest those sit at x = 1521 and x = -839 -- a 399-wide sprite entirely off
both edges of a 1280 screen. The caveat now applies only to animating the
title build-in, where the sweeps cross the screen rotated.

Two METHOD lines: run the control before theorising about the difference,
and log every sample so a failure is a measurement rather than a silence.

Emulator stopped, lock cleared, locale English.
2026-08-29 00:35:49 +00:00
Sylpheed RE agent
f10edf1e79 re: fix wait_title.sh's stale oracle; the JP title still is not reached
Last iteration's "never reached the title in 787 s" was a broken tool
reporting on the world. wait_title.sh was still sampling the single pixel
(625,618) that is_title.py had already been written to replace -- its
docstring says why: a 1280x720 coordinate sampled against the 1279x675
game surface, so it always reads the copyright line. The replacement sat
in the same directory. wait_title.sh now delegates to it.

is_title.py passes its own controls before being trusted here: 753
green-glyph pixels on the committed English title capture, 327 on the
main menu, threshold 400.

Re-ran with the working oracle and the profile flag the English captures
use. The game STILL did not present the interactive title -- but that is
now a measurement rather than an artefact: not one frame showed a single
green-(A) glyph pixel, and content correlation against either build-7
render never exceeded 0.22. Canary was alive and polling
XamInputGetKeystrokeEx (601 calls), sitting in the attract movie.

So the open question narrowed again, and is written into MISSION.md:
whether the attract loop returns to the INTERACTIVE title without a pad
press. title_states_capture.sh claims it does on the English boot with no
pad input; if that holds, the difference is the locale.

Nothing decided about the keyframe-time association or the rest() rule.
Emulator stopped, lock cleared, locale restored to English.
2026-08-29 00:21:44 +00:00
Sylpheed RE agent
0f1c0f4e14 re: the JP-locale capture is not blocked -- I stopped one grep too early
Last iteration I wrote into MISSION.md that a Japanese-locale capture is
impossible here, because user_language is DECLARE_int32 at four call
sites with no DEFINE and no entry in xenia-canary.config.toml. That is
true, and it was not the question. The language is PERSISTED:
kernel_state.cc builds XConfig over <storage_root>/xconfig.settings,
SetDefaults() only supplies a value when the file has none, and the file
is writable. Checking where a setting is stored rather than where it is
configured turned "blocked, needs a human decision" into a two-line edit.
Withdrawn from MISSION.md; METHOD and REFUTED lines added.

The field is located from struct landmarks rather than a hard-coded
offset, and the check re-runs on every invocation so it fails loudly if
the layout moves:

  music_volume 0.7f at User+449  -> BE float at 2727 -> User base 0x8e6
  language          at User+44   -> reads 1 (kEnglish) at 0x912
  country           at User+64   -> reads 103 (US) at 0x926

XLanguage::kJapanese = 2 (xbox.h:307). set_console_language.py wraps it
with a backup and a --restore.

The capture itself is still NOT taken, for a smaller reason than I
claimed. A run with the locale set to Japanese booted fine but never
reached the title in 787 s: wait_title.sh's green-(A) oracle never fired
and burst-sampling found no frame correlating above 0.18 with either
build-7 render -- the run sat in the attract loop. So it needs a longer
or pad-driven run, not a rebuilt emulator. Emulator stopped, lock
cleared, locale restored to English.

Nothing is decided about the keyframe-time association or the rest()
rule; this only changes what standing between us and deciding them.
2026-08-29 00:05:58 +00:00
Sylpheed RE agent
6d246c7a97 re(ui): finish the top-level rotation census; record the JP-capture blocker
Two threads had converged on needing one capture this container cannot
take, so this iteration records that and finishes something reachable.

BLOCKED, written into MISSION.md rather than worked around: Q1's keyframe
time association and the rest() rule for plateau-less elements both now
hinge on a running capture of GP_TITLE build 7, the Japanese title. The
console language is not settable here -- user_language appears only as
DECLARE_int32 at four call sites with no DEFINE anywhere in the tree, and
it is absent from the registered cvars in xenia-canary.config.toml. There
is no flag to pass, and guessing one is specifically unsafe: run-canary's
own header records that xenia calls ShowSimpleMessageBox from
ParseLaunchArguments before logging starts, so a bad flag blocks forever
with an empty log. Rebuilding canary to add the cvar would be improvising
around the blocker; it needs a human decision. Neither question blocks
the five menu screens.

FINISHED: the disc-wide top-level rotation count, left running four
iterations ago as a shell loop over `screen info --geometry` that never
completed (it decodes every texture per build). Walking the placement
region directly takes seconds.

  top-level elements with a keyframe group   15 493
  carrying a non-zero rotation                2 152  (13.89 %)

Both controls pass: GP_TITLE build 4 reports 0 (its rotations are the
nested ptloop records) and GP_DIALOG build 0 reports the expected two.

The control earned its place -- the first version indexed the pak with a
`screen list` BUILD number and got 0 for a screen that has two, because
GP_DIALOG build 0 is entry 2. GP_TITLE maps 1:1, which is how the
assumption survived. METHOD line added.

Two free corroborations of the rotation decode. The rotated population is
dominated by tactical-map ship icons -- pbb_destroyer 444, pbr_destroyer
402, pbr_fighter 276 -- i.e. markers rotated to heading, the single
largest use of the field on the disc. And GP_TITLE entry 7's Japanese
wordmark pieces settle from ALTERNATING tilts:

  ptlogo3a  r = 0, -14, -4, -1, 0, ...
  ptlogo3b  r = 0, +14, +4, +1, 0, ...
  ptlogo3c  r = 0, -14, -4, -1, 0, ...

Same magnitudes, opposite signs, all decaying to upright. A misread field
does not produce that.
2026-08-28 23:46:54 +00:00
Sylpheed RE agent
b973d8cde3 re(ui): scale 0 means collapsed, not "unset" -- stop drawing it full size
blit() and fill_quad() both opened with

    let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };

so an element whose pose is collapsed to nothing rendered at FULL SIZE.
I first described this as "a 1-pixel sliver", reading the .max(1) in the
size arithmetic two lines below -- the guard above it meant .max(1) never
saw a zero. Read the whole function, not the lines you went looking for.

Control run before the change: if 0 meant "unset", something on the disc
would use it throughout. Nothing does.

  elements with a keyframe group                    15 493
  at least one keyframe at scale 0                   2 166
  EVERY keyframe at scale 0                              0
  zero is a transient the element grows out of       1 762

ptlogo_eff3.t32 runs 0% -> 200%. An "unset" marker no element ever uses
throughout is not a marker.

Both functions now return without drawing when either scale is 0.

Reach: 24 of 24 renders byte-identical across GP_TITLE (all 16 builds),
GP_PAUSE_MENU and GP_OPTIONS -- additive on every screen the port needs.
195 elements have a guessed rest pose at scale 0 and 126 of those have a
non-zero alpha, so the old code painted them; all 126 are in
GP_READY_ROOM.pak, which S1 already declared a no-go. They are
tactical-map sprites at sx=0/sy=100, mid-horizontal-wipe, that the
coercion drew full-width.

So: a real correctness fix with no visible effect on the menu port.
Suite green, 122 passed / 0 failed across 3 suites at commit time.
2026-08-28 23:38:37 +00:00
Sylpheed RE agent
c2c69b02be re(ui): size the rest() guess disc-wide, and refute my own proposed fix
Follows up the defect found last iteration: rest()'s dwell fallback is
guessing whenever it runs. Two things were open -- how big it is, and
whether "rest = the last keyframe" is the fix. Both are now answered, and
the second is answered no.

plateau_census.py walks the placement regions directly instead of going
through `screen info --geometry`, which decodes every texture and cannot
do a disc-wide pass in reasonable time. Its control reproduces GP_TITLE
build 7's three fallback elements and names ptlogo_eff3.t32 among them
before counting anything.

  elements with a keyframe group      15 493
  no plateau -> rest pose is guessed   3 807  (24.57 %)
    ... current rule returns invisible 1 711  (44.9 %)
    ... current rule returns scale=0     195  ( 5.1 %)
  the two candidate rules agree        1 911  (50.2 %)

195 elements get a rest pose with scale 0%, which is not a pose. And
disc-wide the choice of rule is not cosmetic: the candidates agree half
the time.

But the port's exposure is one element. Across main menu, EXTRAS, title
and the developer splash, 14 elements are plateau-less and the two rules
agree on 13. The single disagreement is palogo_anima_eff.t32.

And "last keyframe" loses there, on a control that needed no new capture:
the splash carries three sibling glows with identical structure and
identical times --

  palogo_gamearts_eff  15:a=0 30:a=255 45:a=255 -:a=0  plateau -> visible
  palogo_seta_eff      15:a=0 30:a=255 45:a=255 -:a=0  plateau -> visible
  palogo_anima_eff     15:a=0 30:a=255 45:a=212 -:a=0  no plateau

-- differing in one byte. "Last keyframe" makes anima alone invisible
while its two siblings stay lit. The capture agrees weakly: box-mean
ratios capture/render are gamearts 0.717, seta 0.723, anima 0.772, and a
glow we drew that the game does not would put anima below its siblings,
not above.

So the defect is measured and the fix is still undecided. Nothing in the
decoder changed.
2026-08-28 23:23:55 +00:00
Sylpheed RE agent
b947fa8e8e re(ui): withdraw the render evidence -- rest()'s dwell fallback is unsound
Last iteration I reported a 13.1% render change in GP_TITLE build 7 as
evidence AGAINST the keyframe-time shift, arguing that language twins
should match in brightness. Withdrawn. Localising the diff to an element
shows it is not about the time association at all.

The element is ptlogo_eff3.t32, a transient bloom:

   46: (98,42)  100%,100% a=0
   61: (108,72)   0%,0%   a=0
  103: (108,72) 200%,200% a=255 r=80
    -: (108,72)   0%,0%   a=0   r=150

No two adjacent poses are equal, so there is no plateau, and rest() falls
through to its longest-dwell fallback. The longest gap is 61->103, during
which the sprite grows from nothing to 200% at full alpha and then
collapses. The rule returns whichever end of that movement the indexing
lands on: the invisible frame as decoded, the 200% peak shifted. An
896x389 sprite at 200% is larger than the screen, which accounts for the
entire 13.1% and the entire 4.9-unit luminance gap. I was comparing a
heuristic against itself.

The defect generalises, and structurally. A dwell gap is time spent
interpolating FROM pose k TO pose k+1; neither pose is held during it
unless the two are equal -- which is a plateau, and the plateau path has
already returned by then. So the fallback is guaranteed to be guessing
every time it is reached, under any reading of the times. Every element
with no two adjacent identical poses has a guessed rest pose, in our
renderer and in anything built from it.

Consequences: the case FOR the shift (26x on the hold:fade-out ratio) is
no longer opposed by render evidence -- 10 of 11 builds byte-identical,
the 11th differing only through this heuristic. It is still not adopted,
now because it flips this element to the visibly wrong answer, so the
shift and a decision about plateau-less elements must land together, and
neither half has a capture to verify against.

Default-mode suite green (122 passed, 0 failed across 3 suites so far).
2026-08-28 23:10:41 +00:00
Sylpheed RE agent
6f23f4d113 re(ui): the keyframe-time shift is favoured 26x by timing, rejected by a render
Follow-up on last iteration's unadopted candidate (+36 holds the NEXT
pose's time, not its own). Two new results, pointing opposite ways, and
both are reported.

FOR, and calibration-free: the observed full-alpha hold : fade-out ratio
on palogo_gamearts is 83 : 13 frames = 6.38. The shifted reading predicts
8.00. The current reading predicts 0.25 -- off by 26x. With the glow's 2
units/frame fixed and nothing else free, the current reading says the
logo holds full alpha for 2.0 frames; the capture holds it for 83. This
is no longer the shape argument the candidate rested on.

Also for: rest()'s plain max-dwell fallback picks a=0 -- a transparent
pose, for a publisher logo -- under the current reading, and the visible
a=255 hold under the shift. Only the rest_plateau special case rescues
the render today, and that is the case the port agent reported a bug in.

AGAINST: rendering every build of six UI paks under both readings, 10 of
11 compared are byte-identical and one changes -- GP_TITLE build 7, the
Japanese twin of build 4, by 13.1% of pixels. Build 4, the one verified
against a live capture, is unchanged either way, so the single build the
shift moves is the one with no capture to adjudicate it. The proxy goes
against the shift: language twins are the same artwork, and build 7 reads
70.94 mean luminance as decoded against build 4's 71.41, but 76.32
shifted. Correlation does not separate them (0.6206 vs 0.6201).

These constrain different things -- timing versus pose selection -- and
rest() is a heuristic layered on the times, so moving the times moves its
tie-breaks. Adopting the shift means revisiting that heuristic in the
same change, with no build-7 capture to verify against.

Default UNCHANGED. Experiment reachable via SYLPHEED_KF_TIME_SHIFT=1.
Shifted-mode suite: 122 passed, 0 failed across 3 suites at commit time.
2026-08-28 23:03:55 +00:00
Sylpheed RE agent
531840b417 re(ui): Q1 -- the interpolation law holds, the group timeline does not
Q1's gate asks whether the ramp is linear. It is, and that result stands:
it rests on the splash's _eff glows, which reproduce exactly. This adds
the part that does not.

The test is a calibration, not a fit. Fix the clock on
palogo_gamearts_eff -- declared 15-unit fade-in 0@15 -> 255@30 against
captured alphas 34,68,102,136,170,204,238, a constant step of 34, giving
t = 2f - 171 -- then check that against the glow's own next landmark: its
declared hold ends t=45, predicted frame 108.0, observed last full-alpha
frame 107. Then apply it to palogo_gamearts in the same bundle and the
same frames, with no free parameter left:

  declared a=232 at t=206 -> frame 188.5, observed alpha 255
  declared a= 32 at t=210 -> frame 190.5, observed alpha 255

The logo is still at full alpha nine frames after it should read 32; its
fade-out runs ~17 frames late; its declared 80-frame fade-in is never
drawn. Not culling -- the same element is submitted down to a=7 on the
way out. Calibration-free version: the declared fade-out spends 12 of 16
units dropping 23/255 of the alpha, and the capture has no such plateau.

Candidate, offered and NOT adopted: if +36 held the NEXT keyframe's time,
the fade-out shape fits (RMS 4.05 vs 12.13, two elements) and the
decoder's "last block's time is unreadable" special case disappears --
the last block would simply have no successor. Rejected for now because
it explains neither the missing fade-in nor the lateness, and because the
_eff elements cannot discriminate between the readings at all (with four
blocks the shift only relabels the phases). Decoder unchanged.

Also withdrawn, mine, within the iteration: "the _eff glows hold a
constant alpha 33". They ramp 34 -> 255 in steps of 34. I printed the
series minimum and read it as its range, with a "14 distinct colours"
column sitting next to it saying otherwise.
2026-08-28 22:56:17 +00:00
Sylpheed RE agent
f11fde51e5 re(ui): rotation is not nested-only, and the pivot-anchored scale is measured
Two corrections and one new confirmation, all from finishing the sweep I
left running last iteration.

Refuted, by my own sweep, within the hour: "rotation appears to live only
in nested .rat leaf records". That held for GP_TITLE, GP_BUNK and
GP_CHALLENGE -- the three archives the sweep had reached when I wrote it
-- and fails on GP_DIALOG and GP_DEBRIEFING_PILOTLOG, which rotate
top-level elements. Scoping the claim made it cheap to withdraw, but the
sentence should have waited for the sweep.

Those top-level cases are the best evidence on the disc, and they show up
in `screen info --geometry` without a hex dump. GP_DIALOG build 0:
pceff03/pceff04 ramp r = 90 -> 30 -> 10 -> 3 -> 0 while alpha ramps
0 -> 255 and they slide into place -- a swing-in settling upright. Build
6: pzeff02 ramps 43 -> 61 -> 75 -> 90 while scaling 112% -> 200% and
fading to 0 -- a spin-out. Rows committed as reference data.

New: the pivot-anchored scale term in blit, kf.x - pivot*(scale-100)/100,
was implemented and reasoned about but never measured, because every
element previously examined sits at 100% scale where the term is exactly
zero. The ptloop pair scale 600% and 800%, where it is worth 450 and 630
px. Formula predicts centre y = 360.0 for both; capture measures 359.1
and 360.0. Top-left anchoring predicts 810 and 990. Horizontally it makes
t-from-position agree with t-from-alpha to 0.33 / 0.65 units against ~8
without it.

That agreement does NOT prove linear interpolation -- both fields were
inverted through the same linear map, so a shared easing curve cancels.
It shows position and alpha ride one shared parameter. Recorded as such.

The disc-wide sweep is still running; the count is incomplete, the
existence is settled.
2026-08-28 22:46:44 +00:00
Sylpheed RE agent
67fa1a1b0b 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
130ad8d1b3 re: the skewed draw is not the swoosh -- refuting the identification five
iterations of work were built on

I flagged last iteration that "draw 2 is the swoosh" was an inference I
had never checked. Checking it took one line, and it is wrong.

Converting the quads from NDC to screen space: quad A spans y -209 to
925, quad B spans y -292 to 1012. Both cover the full screen height and
run well off it. The swoosh, ptlogo_back2, rests at (71,126) and is a
234-pixel band at y 126 to 360. Draw 2 is not it.

A candidate offered as one: the two ptloop sweeps. pteff03a is 399x180
with element scale (100,800), so 399x1440, and pteff03 carries (100,600)
-- two long thin sprites, two quads, and the drawn vertex alphas 0xC3 and
0xB6 sit inside the 0x80 to 0xff ramp those records declare. Not
confirmed; no texture or position match was made.

If that is right it matters, and I have written it conditionally rather
than acting on it: our renderer parks those sweeps at their final
keyframe, both off screen, and draws nothing, while the game draws them
across the screen. That would mean the capture caught them mid-sweep
inside their t=150 to 600 window, and that the "groups hold" reading
needs re-examining for those two elements.

What this cost is worth stating plainly. Five iterations -- the additive
blend test, the pivot analysis, the vertex-colour capture -- were built
on an identification made by elimination on one screen and never checked
against the draw's own coordinates. The eliminations themselves stand,
because each was measured against the capture rather than against the
identification. The chain that pointed them at ptlogo_back2 did not.
METHOD takes the general form.
2026-08-28 22:21:42 +00:00
Sylpheed RE agent
5115b4894a re: three angle fields found in the keyframe -- and they are not the
title's rotation

Looking for where the rotated quads come from, the obvious candidates
were the three keyframe words at +4, +8 and +12 that ui_layout.rs
documents as zero.

They are not zero. Across 72287 keyframe blocks disc-wide they are
non-zero in 4.81, 4.56 and 15.82 percent of blocks, and read as signed
values clustering on 180, -180, 90, -90, 120 and 22 -- degrees. Three of
them, so plausibly rotation about three axes. I have marked that amber
because it is the shape of the numbers and nothing more; no observed
rotation has been tied to a value. The doc comment is corrected
regardless: "0 on every frame seen" was an artefact of the sample.

And they do not explain the screen I was chasing. Every element of
GP_TITLE build 4 has all three at zero, element by element, while the
game demonstrably submits rotated parallelograms there. So the title's
rotation comes from outside the keyframe data and is still unidentified.

One correction to my own last write-up, flagged rather than left: I
stated that the skewed draw IS the swoosh. It is the only skewed geometry
in the capture and the swoosh is the only diagonal element on the screen,
so the inference is reasonable -- but I never confirmed it by matching
the draw's texture or screen position to that element, and I should have
said so the first time.
2026-08-28 22:17:14 +00:00
Sylpheed RE agent
0ecb20bdbc re: the swoosh is solved -- the game draws it as rotated quads and our
blit cannot

Ran the route I corrected last iteration, and it answered the question I
had classified as undecodable.

First the ninth candidate died: every vertex colour in the whole capture
is <alpha>FFFFFF, white RGB with only alpha varying. The game passes no
colour, so that hypothesis is refuted from the running game rather than
from the file.

Then the geometry gave it away. Draw 2 submits TWO PARALLELOGRAMS,
neither axis-aligned -- edges (0.54,-0.56) and (0.44,0.79), roughly 45
and 61 degrees, both extending to y=+/-1.81 in NDC, well off screen.
That is the diagonal Z stroke. And ui_layout::blit walks rows and columns
of an axis-aligned rectangle; it has no rotation at all. So we blit the
sprite upright where the game draws it skewed, which is exactly the
signature I measured: right on average at +1.83, right in position with
the correlation peaking at zero shift, wrong in structure at 0.70.

Nine candidates, and the answer was never going to be a field, because
the difference is not in the sprite -- it is in how the quad is built.

Two things left open and written as such. The decoded keyframe carries
fade, scale, tint, x, y and time and NO rotation, so where the rotation
comes from is not decoded. And the pink-versus-white reading I have been
carrying was a visual comparison of two differently-shaped renderings; it
should be re-checked after geometry rather than treated as a separate
defect.
2026-08-28 22:13:50 +00:00
Sylpheed RE agent
6930d301c1 re: full suite green, and correcting the route I recommended last
iteration

Two loose ends, both on my own work.

The disc-gated suite finally completed: all six binaries, 131 passed,
0 failed, one pre-existing ignored. So the rest() fix and the T8adImage
flags field are fully verified rather than verified-on-three-binaries.
The reason earlier attempts produced empty logs was that the tool
timeout's SIGTERM reached the whole process group; setsid nohup survives
it, which is now a METHOD line.

And a correction that matters more. Last iteration I closed the swoosh as
undecodable from the disc and named a per-draw GPU capture as the next
route, "because it reads the actual blend state". It does not. Reading
command_processor.cc, each captured draw records primitive type, index
count, index-buffer address, VS and PS ucode hashes, the pixel shader's
texture bindings, and vertex attribute 0 of binding 0. There is no
RB_BLENDCONTROL dump.

So the route splits, and I have said so rather than leaving the wrong
version standing: the capture can test a per-draw VERTEX COLOUR today
with no code change, which would explain white-versus-pink directly, and
getting the blend mode itself needs a Canary change to dump the blend
registers. Either way it is instrumentation rather than another field.

The general lesson goes in METHOD too: validate a recommendation before
leaving it as advice. A named next step is a claim like any other, and I
made it without checking.
2026-08-28 22:09:07 +00:00
Sylpheed RE agent
e10a6d6f2c re: the capture is settled after all, and the swoosh is undecodable from
the disc

Testing my own caveat instead of leaving it hanging. I had worried the
plate-free title capture at t=4.0s was too early, since elements carry
keyframes out to t=600 and I had judged "settled" from mean luminance,
which cannot see a thin sprite still moving.

The plate sits at y 550-600 and the swoosh band at y 112-225, disjoint,
so a LATE capture works even with the plate present. The band correlates
0.7342 at t=4.0s and 0.7353 at t=21.5s -- identical to a thousandth over
17.5 seconds, with the band mean flat at 127. So the band is settled by
4 seconds, the capture I handed the port agent is sound, and my caveat is
withdrawn. It also corroborates the earlier finding that groups hold:
nothing crosses that band in 22 seconds.

That was the seventh candidate for the swoosh and the last one that was
not a format field. Pivot is inert at scale 100 and there is no
displacement anyway; fade is white-with-alpha; tint is white; the texture
is blue-leaning; additive via +0x04 bit 0x02 makes everything worse; the
capture is settled. The residual is stable and modest -- band mean +1.83,
edge-correlation 0.70 against 0.92 frame-wide.

So I have classified it rather than generating an eighth candidate:
undecodable from the disc, with the reach written out. And I have named
where a next attempt should start, which is not another field -- a
per-draw GPU capture of the running guest reads the actual blend state,
vertex colours and draw order for that band instead of inferring them
from the file. That is where static RE stops here.
2026-08-28 22:02:32 +00:00
Sylpheed RE agent
5d02d5cd60 re: additive blending refuted, the swoosh is not displaced, and the
residual is smaller than I said

Testing the candidate I raised last iteration rather than carrying it.
Blending bit-0x02 sprites additively moves every measure the wrong way --
whole-frame mean diff +0.55 to +1.04, swoosh-band mean +1.83 to +3.98,
band edge-correlation 0.6971 down to 0.5578. So the bit is real and
independent but does not select an additive blend. I reverted the
experiment and kept the word as T8adImage::flags, documented and not
acted on; the render is byte-identical to before.

Second refutation: the swoosh is not displaced. Shifting the band over
plus or minus 80 by 8 pixels peaks sharply at zero, 0.7342, falling to
0.22 at 24 px. So the pivot story is dead twice over -- inert at scale
100, and no displacement to explain anyway.

And I have restated the residual, because earlier sections overstated it.
The +16 to +34 band tiles I quoted were measured WITHOUT --primitives.
With the dim drawn the band's average is nearly right at +1.83; what is
wrong is its structure, tiles running -38.6 then +33.8 and cancelling.
Six candidates eliminated now and none confirmed.

One caveat I owe the port agent about the capture I gave them: it is at
t=4.0s, roughly 174 keyframe units into a screen whose elements have
keyframes out to t=600. I judged "settled" from mean luminance, which
cannot see a thin sprite still moving. It is settled for the bulk of the
screen and not proven settled for every element -- which is a live
alternative explanation for a structural difference in exactly the band
the sweeps cross.

METHOD: cargo build passing does not mean cargo test compiles. Adding the
field built the library in 1.48s and broke two test-only struct literals;
cargo test failed with exit 101.
2026-08-28 22:00:55 +00:00
Sylpheed RE agent
e0d02dce71 re: a candidate blend flag -- T8aD header +0x04, bit 0x02
Continuing the swoosh. Last iteration ended with "the next step is
finding where a blend mode would be encoded, and I do not know the format
carries one". It does carry a candidate.

The kind field is not it -- the swoosh sprites are kind 0x0, the same as
ordinary ones. But the T8aD header word at +0x04 splits the title's
sprites exactly along effect versus normal: pteff01, pteff03a,
ptlogo_back2eff1..5 and both ptlogoall_eff are 0x8832, while ptlogo1,
ptlogo2, ptlogo_tm, ptbase2, ptlogo_back2 and ptcopyright are 0x8830.
One bit, 0x02.

Disc-wide it behaves like a real flag rather than an artefact: 19216
sprites, 18 distinct values, bit 0x02 set in 27.1 percent, and it toggles
against otherwise identical words -- 0x8830 against 0x8832, 0x0830
against 0x0832, 0x0810 against 0x0812, 0x0030 against 0x0032.

Marked as correlation and not decode, because nothing here shows the bit
MEANS additive. The one thing that makes it more than a guess is
ptlogo_back2eff, which carries 0x8830 despite having eff in its name --
so the split is the field's and not my pattern-matching on names. The
test is to blend bit-0x02 sprites additively and re-correlate the title
against the plate-free capture.

METHOD gets the trap that cost the first attempt: searching for a
sprite's name and taking the next T8aD returns the SAME header for every
sprite, because the names all live together in the declaration table. It
failed its own control at once -- different sprites reporting identical
dimensions -- and the fix was to match on width and height instead of on
proximity.
2026-08-28 21:46:18 +00:00
Sylpheed RE agent
c10802536f re: the pivot mismatch cannot be the swoosh's cause -- refuting my own
candidate with the check I named

Last iteration I offered the pivot mismatch as a quantified candidate for
the swoosh rendering too thick, and said explicitly that whether it bites
depends on which pivot the compositor uses and that I had not checked. I
checked.

blit takes the drawn size from the TEXTURE and uses the pivot only as the
scale anchor, kf.x minus pivot times scale-minus-100 over 100. At scale
100 that term is zero. And every one of the seven swoosh elements --
ptlogo_back2, its glow and the five eff segments -- reports a single
scale of (100,100) across all its keyframes. So the mismatch, real as it
is in the data, cannot move or resize the swoosh in our render. Refuted,
under my own name.

It is not harmless everywhere, and I have said where it does bite:
ptlogo1 and ptlogo2 run 100, 101, 103, 112, 150 during the build-in, so
there the wrong pivot really does displace them -- during the animation
rather than at rest.

What that leaves is the blend, by elimination rather than by evidence.
Position and size are the texture's own and right, fade is
white-with-alpha, tint is white, the texture is blue-leaning rather than
pink. Seven overlapping sprites at 5 to 23 percent opacity stacked with
plain alpha-over saturate toward opaque white, which is what we draw and
would read as thicker beside the game's thin coloured stroke. Recorded as
the remaining candidate and explicitly not as a diagnosis -- no blend
mode has been identified in the data.
2026-08-28 21:40:46 +00:00
Sylpheed RE agent
2a0afc1405 port: land the pivot finding on the handoff (third assertion miss)
The scripted HANDOFF edit asserted on text an earlier edit had rewrapped,
so the pivot and colour findings landed in docs/re and not on the page
the port agent reads. Third time this session; METHOD now says the rule
was insufficient and names the mechanical fix.
2026-08-28 21:37:51 +00:00
Sylpheed RE agent
e249b2e2ff re: the swoosh's pivot belongs to the Japanese sprite, and the colour is
not in any field I can find

Chasing the swoosh defect. The colour half is a clean negative and the
geometry half now has a number.

Ruled out for the colour: every keyframe of every swoosh element carries
fade 0x??ffffff -- white RGB with only the alpha varying -- and no tint
is anything but white. The texture is not pink either; ptlogo_back2
decodes blue-leaning at (175,174,198) and its glow warm at (255,253,234).
So the magenta edge the game draws comes out of blending, and I have said
that rather than leaving "wrong colour" as an undifferentiated mystery.

The geometry half: the declared pivot does not match the element's own
texture. Build 4's ptlogo_back2 pivot is (500,117), which is exactly half
the JAPANESE sprite at 1000x234, not half its own English one at
1118x262 -- off by 59 px, the right order to make the swoosh too thick
and extend too far right. The layout record is authored once and shared
while the sprites are swapped per language, which ui-rat-layout.md warns
about in general; this is that warning measured on the screen where our
render disagrees with the capture. It cuts both ways -- the Japanese
build's ptlogo1 is off by 40.

Marked as a quantified candidate rather than a diagnosis, because whether
it bites depends on which pivot our compositor uses and I did not check.

One METHOD line, because the first version of that check was misleading:
a pivot-equality test conflates rounding with real breakage. Equality
flagged 74 percent of title elements. Split by magnitude it is 17 exact,
54 off by a pixel from odd dimensions, and 24 genuinely off -- and those
24 are the finding.
2026-08-28 21:37:24 +00:00
Sylpheed RE agent
cdb5a99def re: the title "slab" is the logo swoosh drawn white and thick, and it
lands on the known tie-break group

Chasing the residual I left open last iteration, and it turns out I had
attributed the port agent's slab to the wrong thing.

First the refutation. My hypothesis was that our dim is applied over the
whole frame rather than beneath the UI where its layer key puts it. If
that were true the logo would render too dark. It reads +2.36 against a
background of -0.74, so the compositor honours the paint order and the
hypothesis is dead.

What the residual actually is: with the dim in place the error is not
uniform but concentrated in one band, running -38.6 in one tile and +33.8
three tiles to its right. Cropping that band from capture and render
shows it immediately -- the game draws the logo's Z swoosh thin with a
pink edge, and we draw it thick and solid white. That, not the missing
dim, is the washed-out slab over the title logo the port agent reported.
The dim explains a uniform +13; this explains the slab.

The elements are ptlogo_back2, its glow, and the five ptlogo_back2eff1..5
segments -- which are also the group carrying this corpus's known
unsolved paint-order tie-break at key 0x8083, on the same screen. I have
recorded that as a lead and explicitly not as a diagnosis, because a
blend-order swap is a poor explanation for white instead of pink and I
would expect a second cause.
2026-08-28 21:31:52 +00:00
Sylpheed RE agent
d217c07db2 re: the title's arrived pose is rest, and the washed-out slab is a
MISSING dim quad

Both halves of what the port agent could not settle, answered against the
plate-free capture rather than against another renderer.

Rendering build 4 and edge-correlating to the capture of the real screen
gives 0.9163 at zero shift, 0.92/0.78/0.93 per band. The geometry of rest
is the title's arrived pose, so the timeline is not needed there.

The slab is the more useful half, and it is inverted from how it looked.
rest alone renders uniformly +13.14 too bright, R +12.35, G +13.58,
B +13.48. Drawing the .prm primitives collapses that to +0.55. The
element is pteff02.prm, the 25 percent dim at rest t=46 with fade 0x40,
and --primitives is OFF BY DEFAULT. So the washed-out cyan slab is not
something being drawn that should not be; it is a dim that should be
drawn and is not, and because the title art is blue-dominant the
shortfall reads as a cyan wash. Any consumer of screen render that omits
--primitives on this screen gets it.

Two residuals recorded rather than tidied. With the dim drawn the mean is
essentially exact but per-pixel agreement gets slightly WORSE --
edge-correlation 0.9163 down to 0.9066 and pixels off by more than 20
rising from 108051 to 162636 -- so the dim's average contribution is
right while its application is not exactly the game's. And separately the
per-tile map shows a +25 to +38 hot spot on the upper-right Z swoosh,
which a capture from the first iteration of this work already showed pink
in the game and white in ours. Same region, still undiagnosed.
2026-08-28 21:27:04 +00:00
Sylpheed RE agent
5414db34bf re: the rest() fix gets its disc-wide check, and one question back to the
port agent

Closing the two things I flagged as unconfirmed when I committed the fix.

The unit gate is green: cargo test -p sylpheed-formats with SYLPHEED_DISC
set gives 131 passed, 0 failed across six binaries including the
disc-gated ones. The earlier background run that produced an empty log
had died with SIGTERM, which is why it looked like nothing happened.

And the disc-wide check the classification requires. Reimplementing both
rules over every RATC bundle on the disc -- 2859 bundles, 13991 elements
with at least two keyframes -- the fix moves rest for 30 elements, which
is 0.21 percent. Four go invisible to visible. Zero go visible to
invisible, which is the safety property I wanted and did not have when I
committed. The four are ptframe1 and ptframe2 in GP_TITLE entries 5 and
8, the same pair once per language.

That surfaces a discrepancy worth handing back rather than smoothing
over. On the English main menu exactly TWO elements satisfy the port
agent's pose-equality condition, not six, so their six must span the
whole twelve-screen export. That fits their own observation that the
timeline and rest differ in exactly one region, the bounding box of
ptframe1 and ptframe2 and nothing else. But if any of their other four
have a TRANSPARENT trailing run, my alpha rule leaves them alone
deliberately -- that exclusion is what protects PAUSE -- so I have asked
which screens they are on and whether a capture shows any of them drawn.
If one is, the alpha rule is incomplete and needs a third discriminator.
2026-08-28 21:24:42 +00:00
Sylpheed RE agent
4bc970688f re: rest() fixed -- a trailing keyframe run is the hold when it is
VISIBLE, not when its pose repeats

The port agent's report was right about the defect and about which
elements it hits, and its proposed condition does not survive the case
the exclusion was written for.

Their test -- the final untimed keyframe has the same pose as the last
timed one -- is also true of pgptitle.rat, whose trailing run is two
identical transparent frames. Adopting it would erase the word PAUSE
again, which a committed capture disproves. What separates the two is
alpha: an exit fades the element out so its last keyframe is transparent,
while an element with no exit ends on the pose you can see. So
rest_plateau now accepts a trailing run exactly when it is visible.

Verified against a capture rather than against another renderer, which is
the point their own report made: ptframe1's rest moves from (620,108)
t=16 to (440,108) t=62; the changed pixels are 10082 in a bounding box of
x 440-839, y 108-577, which is exactly the 400x470 at (440,108) they
predicted; and correlation against main-menu-oracle.png over that region
improves from 0.9596 to 0.9748. The PAUSE wordmark is unchanged across
all three pause builds.

This also closes a question ui-paint-order-key.md has carried for a
while, that ptframe1 and ptframe2 rest at alpha 0 while the capture shows
the menu frame plainly.

One trap cost most of this iteration and goes in METHOD: CARGO_TARGET_DIR
is redirected in this container, so ./target/debug/sylpheed-cli is hours
stale and every render I made against it was old code. Byte-identical
before and after reads as "no effect" when it actually means "you ran the
old binary".

The full disc-gated test suite was still executing when this was
committed; the verification above is artifact-based.
2026-08-28 21:19:00 +00:00
Sylpheed RE agent
ed4e5c7b37 re: build 4 captured without the plate, and keyframe groups hold rather
than loop

The port agent ranked a plate-free capture of build 4 above any further
static RE, so that came first.

B from the main menu returns to the title and the plate fades in a beat
later, which opens a clean window. Recorded at 20 fps from the press: the
art appears at 1.10 s, builds in to 3.70 s, sits settled and unobstructed
until 5.00 s, and the plate arrives at 5.10 s -- the band jumps from 282
to 3755 bright pixels. Committed the frame at 4.0 s as the reference for
the cyan glow slab they report drawing and the game not having.

Their other sub-question -- whether a keyframe group loops or holds --
falls out of the decoded sweeps plus a measurement I already had, and the
two agree. ptloop01's final keyframe parks pteff03.t32 at x=1521 and
ptloop02's parks pteff03a.t32 at x=-839, both off-screen on a 1280-wide
design; and over 18 s of settled title the centre tiles sit at sd <= 0.01
when a looping group would recross the screen every 7.5 s. So groups HOLD
at the last keyframe. The loop*.rat name is misleading.

Also recorded, in the corpus rather than only in their report: the
rest_plateau bug, with their exact identifying condition -- the final
untimed keyframe has the same pose as the last timed one -- the six
elements it misses on main_menu, and the bracket it drops. That closes an
open question ui-paint-order-key.md has carried for a while about
ptframe1 and ptframe2 resting at alpha 0 while the capture shows the
frame plainly. Same two elements, same cause. Not fixed yet; the change
is in ui_layout's rest().

And a METHOD line I would not have written myself: two renderers agreeing
is not evidence the field is right. Their composite and screen render
matched to 3/255 on main_menu and both omitted two elements the game
draws, because both read one field through one decoder.
2026-08-28 21:05:35 +00:00
Sylpheed RE agent
1b95001feb re: the last keyframe time is the chunk terminator -- observed, not
assumed

Continuing the pulse's cycle length, which I had called a structural
limit of the format. It is, and now I can show it rather than assert it.

Dumping past what I thought was the final keyframe found an EIGHTH block:
fade 0x00ffffff at the same position, so the glow returns to fully
transparent and the pulse is a closed cycle rather than a one-shot ramp.
That is worth having on its own -- the port knows the animation ends
where it starts.

And the eighth block's time slot contains the four bytes "end ", the
record's ASCII terminator. So the corpus's rule that a group's last block
has no time of its own holds here in a second form: not the next group's
element index, but the chunk terminator. The value does not exist. That
goes into ui-rat-layout.md, because it generalises beyond this record.

The measured 2.3 s would need a final step of about 33 units. I have
written that number down explicitly labelled as fitted to the measurement
rather than read from the file, so nobody later re-derives it as a
decode.

One thing I had half-assumed and can now rule out: the word at +0x004 is
not a keyframe count. It reads 60 here with 8 keyframes and 30 in the
loop records with 3. Unknown, and marked so.
2026-08-28 21:02:14 +00:00
Sylpheed RE agent
c176fa53bc re: the 2.3 s pulse is the PRESS (A) plate, not the title
Closing the residual I left explicitly unidentified last iteration, and
correcting my own claim twice over in the process.

The cheap test first: is the oscillation global? No -- the bottom-right
corner is flat at sd 0.003 and uncorrelated with the whole frame. So a
per-tile amplitude map over an 8x6 grid, which localises it hard: sd 7.65
in the band x 318..954, y 560..672 at lag 2.3 s, against 0.06 on the
wordmark. That band is the PRESS (A) BUTTON plate's rest position.

Decoding ptbtn00f.rat, the plate's highlight variant, gives the pulse
itself: alpha 0x00 -> 0x06 -> 0x4a -> 0x50, held, then back down through
0x4a to 0x06 across t=6..105. A glow that fades in and out, which is
exactly what a press-start prompt does.

So "the title screen loops at 2.2 s" was wrong in both halves. The title
ART is near-static apart from the two decoded sweeps at 7.5 s and 9.5 s;
what pulses is the PLATE, and the plate is build 2, not build 4. REFUTED
carries it under my own name.

Still amber, and said so rather than rounded off: the cycle LENGTH is not
readable. The keyframe group's last block has no time -- that slot
belongs to the next group -- so the declared span is at least 105 units,
1.75 s, against a measured 2.3 s. Consistent with a final block
extending the tail. Not confirmed.
2026-08-28 20:58:49 +00:00
Sylpheed RE agent
83293c3f81 port: land the loop decode on the handoff, and note the edit trap
Second iteration running where a scripted HANDOFF edit failed its
assertion on rewrapped text, leaving the port's page stale while docs/re
had the finding. Patched by reading the file first, and METHOD now says
to check the commit's file count for a silently dropped edit.
2026-08-28 20:53:33 +00:00
Sylpheed RE agent
3717293fa9 re: the loop records decode -- and they are not what I measured
Two of my own claims from last iteration die here, and the decode is the
thing that kills them.

Searching for the SHAPE instead of an assumed count found it immediately:
fade words matching 0x??ffffff at alternating strides of 24 and 16, which
is a 40-byte block carrying fade at +0 and tint at +24 -- exactly the
build layout, starting at +0x68. So "a leaf record's keyframes are not in
the build's 40-byte layout" is withdrawn. They are. My scan failed
because I demanded 29 strictly-increasing times, having read the word at
+0x004 as a keyframe count; the records hold THREE.

Decoded, both are horizontal light sweeps. ptloop01 runs pteff03.t32
from x=-639 to x=1521 over t=150..600, which is 450 units or 7.5 s;
ptloop02 runs pteff03a.t32 the other way over 570 units or 9.5 s.

Which refutes the second thing. Last iteration I attributed the measured
2.2 s oscillation to these elements because build 4 declares them --
association, not evidence. A 7.5 s cycle would give about three peaks in
a 22 s capture and I counted eight. So the loops are not what I measured,
the 2.2 s stands as a measurement, and its source is now explicitly
unidentified rather than wrongly assigned.

METHOD gets the general form, because it is what cost the previous
iteration: do not hard-code an expected element count into a structure
scan. Scan for the shape and let the count fall out.
2026-08-28 20:53:08 +00:00
Sylpheed RE agent
00ce0f33d0 port: carry the loop-cycle negative into the handoff
The previous commit's handoff edit failed its own assertion -- the text
had been rewrapped since I wrote the pattern -- so the finding landed in
docs/re and REFUTED but not on the page the port agent actually reads.
An answer not reachable from HANDOFF.md has not been delivered.
2026-08-28 20:49:39 +00:00
Sylpheed RE agent
a048910497 re: the loop record is located, its cycle is still not readable
Continuing the one gap from last iteration -- turning the title's
measured 2.2 s loop into a decoded cycle. It did not turn.

The record itself is found and partly confirmed. ptloop01.rat is an
opt-linked leaf RATC at 0xbb5966 in build 4, placing pteff03.t32, with
0x1e -- thirty -- in the high half of the word at +0x004, which is the
~30 keyframes ui-rat-layout.md predicts for a loop record. Its pivot
fields read 200 and 90, matching exactly what screen info prints for
ptloop01, so this is the right blob and the header offsets hold.

The times are not there. A build's keyframes are 40-byte blocks with the
time at +36; scanning every 4-byte-aligned start across a 0xC0 window for
29 strictly increasing values at that stride finds nothing, in either
loop record. So a leaf record's keyframe layout is NOT the build
placement layout, which is now a line in REFUTED because it is the
obvious first assumption and it is wrong.

The 2.2 s stays measured and the port hardcodes it. What the next attempt
inherits is the record's address, a confirmed pivot, and one layout ruled
out -- which is the useful part of a negative.
2026-08-28 20:49:11 +00:00
Sylpheed RE agent
5f3d8f86a4 re: the title loops at about 2.2 s -- and the build-in test I intended
did not work

I set out to do for the title what worked for the splash: measure the
build-in and check it against the declared keyframes. That part failed,
and I am recording the failure rather than the number.

It failed for two reasons, both mine. The title was reached by skipping
the movie with A, which cuts to black and brings the title up on a path
that may not be the normal one. And the visible rise, about 2.7 s, is a
luminance curve -- which screen-transitions.md already establishes is not
the fade quad's ramp. So 2.7 s neither confirms nor contradicts build 4's
declared 16 -> 261, four point oh eight seconds. They are not measuring
the same thing, and I have said so instead of reporting a discrepancy.

What the recording did establish is worth having anyway: the title never
settles. Over 22 s the mean luminance oscillates continuously, peaks at
5.6, 7.8, 10.0, 12.7, 15.2, 17.1, 19.2 and 21.3 s, mean interval 2.24 s.
Marked as about 2.2 s plus or minus 0.4 rather than a precise period,
because peak-picking a low-amplitude signal is coarse.

The mechanism was already decoded and I did not re-derive it: build 4
declares ptloop01.rat and ptloop02.rat, and loop*.rat is a looping sprite
animation rather than a composition. What is new is the period, and that
it runs forever. For the port that matters -- drawing the resting pose
and stopping will look wrong.

Not confirmed, and flagged: that 2.2 s is the loop's OWN declared cycle.
screen info prints the element's placement keyframes but not the frame
count inside the .rat, and I did not open it.
2026-08-28 20:45:53 +00:00
Sylpheed RE agent
113c25aac1 re: the splash's timing is decoded, and the fade-out matches to 0.03 s
Closing the caveat I left last iteration -- the splash timing was +/-0.5s
because I sampled at 2 fps, and at that rate I could not see ramps at
all, only plateaus.

Re-recorded at 10 fps: the splash FADES, both in and out, rather than
cutting. And the bundles declare it. palogo_sqex.t32 carries keyframes
[15 30 235 239 251 255] and palogo_gamearts.t32 [15 30 190 194 206 210],
each with an _eff glow child on [15 30 45]. Under Q1's 1 unit = 1/60 s
that is a 0.25 s ramp in, a 3.42 s or 2.67 s hold, and a 0.33 s fade out
-- against measured holds of about 3.5 s and 2.4 s and fade-outs of about
0.3 s. The constant 0.35 s offset between declared and measured start is
just that my recording's t=0 is when the WINDOW appears, not when the
guest starts drawing.

So the first screen's animation moves from measured to decoded: the port
reads it off the disc instead of trusting my stopwatch.

It also explains something the capture showed and I had no account for.
The brightness overshoots on the way in -- peaks at 0.8 s, settles by
1.1 s -- which reads as a bloom. It is the _eff glow child, whose
keyframes run 15 -> 30 -> 45, ramping in after the logo and back down
while the logo holds. Mechanical, not a rendering artifact.
2026-08-28 20:39:17 +00:00
Sylpheed RE agent
9f39e8ee72 re: the splash confirmed against the running game, and timed
Closing the amber I raised last iteration: the splash renders had no
framebuffer capture to diff against. Recording the boot from the moment
the window appears, at 2 fps, catches it before the movie.

Both halves match their own render at zero shift, each with the OTHER
half as a negative control: publisher frame 0.9146 against entry 10 and
0.027 against entry 11; developer frame 0.9792 against entry 11 and
-0.033 against entry 10. The EN/JP twins score 0.876 and 0.966, so the
test tells a screen from a different screen but cannot tell a pair apart,
and I have said so rather than implying it discriminates.

The recording also times the front of the boot, which Q6 and Q7 only had
from the title onward: SQUARE ENIX from about 0.5 to 4.0 s, black,
developer logos from about 5.0 to 7.5 s, black, then ADV.wmv from about
9.5 s.

One trap avoided and written into METHOD. The frames around 9.5 to 12.5 s
show SQUARE ENIX again in cyan, and read from the capture alone that is a
third logo screen. It is not -- it is the intro movie's own opening,
which the milestone-2 notes describe as white SQUARE ENIX plus cyan glow
plus red diamonds. A logo appearing twice may be two different things.
2026-08-28 20:35:10 +00:00
Sylpheed RE agent
b302558d26 re: the developer splash renders -- screen 1 of 5 finally has a composite
Two pages of this corpus disagreed. MISSION says the splash "is the RATC
screen, which already renders"; ui-paint-order-key.md says it "cannot be
rendered by screen render at all". Running the tool both ways settles it.

screen list --all shows all 16 GP_TITLE entries instead of 12, and the
four the default listing drops are the splash, each half shipped twice:
entries 10 and 13 are the white SQUARE ENIX publisher logo, entries 11
and 14 the GAME ARTS / SETA / studio anima developer logos. Entry 11's
seven elements are the three logos, their three _eff glows and the
palogo_eff0.prm backdrop -- exactly the composition ui-paint-order-key.md
had measured for the splash without being able to draw it.

So the "cannot be rendered" line is wrong and is corrected in place. What
is true is narrower and worth keeping: the splash is invisible to the
DEFAULT listing because is_build wants a .rat child, so anyone who does
not pass --all concludes it is missing. That goes in METHOD -- a default
filter can hide a whole screen and the corpus will record it as absent.

The payoff is for the port rather than for the RE: the first of the five
screens now has a reference composite, which it did not have. Marked
amber on one point -- there is no framebuffer capture of the splash in
this repo, so the match to the running game is by description against the
milestone-1 notes, not by pixels.
2026-08-28 20:29:33 +00:00
Sylpheed RE agent
cc72f8b5a8 re: the phase transitions -- and withdrawing yesterday's refutation
because the test probed the wrong state

Nine stores to the phase field this+132, control-gated on the 136 query
returning at least the 18 known ones. Attributed to the handler each
lives in, they give the phase graph: entry -> 2, then 2 -> 0 (splash),
2 -> 3, 3 -> 4, and 4 -> 2 on event 0.

That last edge is the problem with what I did last iteration. The event-0
block sets TWO fields one instruction apart -- stw r28,136(r30) for state
0 and stw r11,132(r30) with r11 = 2 for phase 2. So B from the menu lands
in PHASE 2, the same phase as the boot title, and the test I ran
believing it probed phase 4 state 0 probed nothing of the kind.

So the refutation is withdrawn. Worse for me and better for the idea: A
working on the B-returned title is exactly what the hypothesis predicts,
since phase 2 is the phase that references BUTTON. The hypothesis is back
to untested, now consistent with two observations rather than one, and
what it still needs is the attract-returned title's phase -- which no
test so far has read.

The measurement from that iteration stands, because it does not depend on
the phase: the B-returned title accepts A, so only the ATTRACT-returned
title is inert. I have said so explicitly in the page rather than letting
the withdrawal take the good half down with it.

METHOD gets the lesson: before testing "state X behaves like this", check
your route actually reaches state X. When a transition writes more than
one field, read the whole block and not just the store you were looking
for.
2026-08-28 20:25:06 +00:00
Sylpheed RE agent
4830c63402 re: my phase hypothesis is dead, and the input puzzle is narrower than
this corpus thought

Last iteration I proposed that the boot title is phase 2 and the
attract-returned title is phase 4 state 0, and that only phase 2 wires up
A -- a candidate answer to the oldest open question in
canary-scripted-input-traps.md. I marked it untested and named a test
that needed reading this+132 at runtime.

There was a cheaper one. The decoded graph says B from the main menu is
the edge 4 -> 0, so the B-returned title IS phase 4 state 0. If that
state were the inert one, A would not work there.

A works. Control first -- A on the boot title opened the menu -- then A
on the B-returned title opened it too. So phase 4 state 0 handles A and
the hypothesis is refuted, one iteration after I raised it. Recorded in
REFUTED under my own name, and the conditional stated: the edge is
decoded, but which title it produces is my inference.

The test earned something better than it cost, though. That page frames
the problem as "the boot title accepts A, a later title does not", and
that is too broad -- the B-returned title accepts A perfectly well, a
case nobody had tried. Only the ATTRACT-returned title is inert. Whatever
causes it is tied to the attract cycle specifically rather than to the
title having been shown before, which is a much smaller haystack.

Also reproduced in passing: 4 s after B the plate is absent and fades in
after, the same near-miss this corpus has already been caught by once.
2026-08-28 20:21:23 +00:00
Sylpheed RE agent
4380305921 re: phase 0 is the splash, phase 2 is the title -- and a candidate answer
to this corpus's oldest input puzzle

Continuing Q6 on the phases left unread. Strings each phase handler
references, plus whether it carries its own jump table:

  phase 0  sub_821C5690   LOGO                                    no switch
  phase 2  sub_821C5818   BASE_INFO, BUTTON, TITLE_SCREEN         no switch
  phase 3  sub_821C5EC0   (none)                                  one switch
  phase 4  sub_821C6458   BASE_INFO, LOADING, TITLE_MENU, TITLE_SCREEN

Phase 0 referencing LOGO is a second independent confirmation that it is
the developer splash -- the iterate3E notes reached the same function
from the guest side and named the splash's LOGO items. Phase 2 draws the
title WITH the PRESS A plate, which the archive side had already
established is a build of its own.

Which produces something worth more than either: the title is installed
from TWO places, phase 2 and phase 4 state 0. Same screen, different
code. canary-scripted-input-traps.md has recorded for months, and never
explained, that the boot title accepts A while the attract-returned title
accepts nothing, with the giveaway that a draw capture in each is
identical. Two code paths installing one screen is exactly that shape. I
have written it into that page as a candidate with the cheap test named
-- read this+132 on each title -- and marked it untested, because it is.

One query in this iteration failed its own control and I threw its half
away: counting stw rX,136(r30) per phase returned 0 for phase 4, which
has 18, because the operand text has a space the pattern did not allow.
The bctr half passes its control and is reported. METHOD gets the
underlying trap: instructions.function is unpopulated for most rows, so a
query scoped on it silently returns nothing.
2026-08-28 20:15:02 +00:00
Sylpheed RE agent
86e52b01d0 re: the menu's music is BGM_103 -- found by accident while chasing Q6,
confirmed three ways

Chasing where the event code comes from, sub_821C5580 turned out to do
two things worth having.

For Q6 it arms the outer gate: li r11,3 ; stw r11,16(r28) is exactly the
value sub_821C7850 tests before dispatching, which answers the "what does
this+16 == 3 gate on" question I left open last iteration. It then passes
r5 = [r27+4], an event read out of a structure, so the vocabulary is
still not enumerable. Of the four callers of sub_821C7850, three pass
constants -- 0, 0 and 5 -- and this one does not.

The accident is a few instructions earlier: li r5, 1103 into a sound-play
call. 1103 is a BGM cue id, BGM_103. That closes a residual I had written
off as undecodable: which bank the menu plays. The cue table genuinely
cannot say, since its BGM entries are numeric -- but the code can, and it
checks out three independent ways. The census says BGM_103.slb is two
waves of 3876864 and 3930112 bytes; the XMA probe at the main menu saw
two stereo streams of 3876864 and 3930112 bytes. Byte for byte on both.

That also corrects me. bgm-two-stems.md said those observed sizes matched
no bank's declared waves and inferred the decoder gets a window rather
than a whole wave. Wrong on both counts -- I had checked only the BGM_0xx
rows of the census because that was the block on screen. METHOD gets it:
check a measurement against the whole reference set, not the part you
happened to be reading.
2026-08-28 20:10:41 +00:00
Sylpheed RE agent
a98e96b0f0 re: the ten states are ONE PHASE of five -- and phase 0 is the splash
Chasing where the event code comes from turned up the level above and
corrected my own framing three sections running.

sub_821C6458 has exactly one caller, sub_821C7850, and that caller is
itself a dispatcher -- on a SECOND, outer state field at this+132, five
phases, gated on this+16 == 3. Phase 4 is the title/menu machine I have
been decoding. Phase 0 is sub_821C5690, which the iterate3E notes had
already fingered as the splash mechanics from a completely different
direction. That agreement is the useful check here.

So GamePart_Title has two nested state fields: a phase at this+132
choosing which sub-machine runs, and the state at this+136 inside phase
4. Everything I decoded over the last three iterations -- ten states,
eighteen edges, the event dispatch -- is phase 4 ONLY. Phases 1, 2 and 3
are untouched. I have called that out in REFUTED against my own earlier
wording rather than quietly restating it.

And the event code is forwarded, not created: all five phase handlers are
called with the same (this, r29, r28), where r28 is sub_821C7850's own
third argument passed through untouched. So the event vocabulary is
defined at least one level further up, and finding what 3, 5, 8, 10 and
25 mean means going up again. Not done.
2026-08-28 20:05:27 +00:00
Sylpheed RE agent
52275ddd53 re: state 4's edges are selected by an event code -- 6 of 26 handled
Continuing Q6's last item: the condition on each edge.

sub_821C6458's third argument is an EVENT CODE, and state 4 -- reached
straight after the main menu is installed, so the input-waiting state --
dispatches on it through a second jump table at 0x821c6b7c, 26 cases.
Six are handled and twenty fall through changing nothing:

  event 0 -> state 0 (TITLE_SCREEN, back to the title)
  events 3, 5, 8, 25 -> state 8 (LOADING)
  event 10 -> state 5

So the edges I listed last iteration as "4 -> 0, 5, 8x4" are each now
attributed to a specific event, and the input state's graph is complete.

One thing I deliberately did NOT promote. The main menu has five items, B
returns to the title, and this table has one event to the title, four to
LOADING and one elsewhere. That is a tempting fit -- B plus four items
that load an external archive plus EXTRAS staying inside GP_TITLE -- and
it matches everything measured. It is also a count-match and nothing
more: the events are unnamed, nothing shows event 3 is a particular menu
row, and state 5 installs no named screen so the EXTRAS half has no
support at all. Written down as an observation with that said plainly,
because the same shape of reasoning already cost me DIFFICULTY and
EXTRA_MENU two iterations ago.

Still open: what the event numbers mean, the conditions on edges out of
the other states, and what states 1/3/5/6/7/9 do.
2026-08-28 20:00:49 +00:00
Sylpheed RE agent
6e92ae4597 re: the title's state machine is decoded -- ten states, eighteen edges
Q6's last open piece was which state leads to which, and it turned out to
be a plain switch.

state = this+136, compared against 9, dispatched through a jump table at
0x821C6498. The ten words at that address disassemble as lwz r16,N(r28)
instructions and are nothing of the kind -- they are the table. That is
the same "self-reference" I flagged two iterations ago as looking like a
jump table; it is one, and the disassembler was decoding its data.

Three states install a named screen: 0 is TITLE_SCREEN, 2 is TITLE_MENU,
8 is LOADING. Eighteen transitions, every one a literal li/stw pair into
the state field, giving the full graph.

It lines up with the behaviour measured weeks of iterations ago without
either side knowing about the other: boot reaches state 0 and A opens
state 2; B at the main menu returns to the title, and 4 -> 0 is the ONLY
edge back to state 0, reached from 2 -> 4; entering a submenu goes
through LOADING and comes back, which is 4 -> 8 at four separate sites
then 8 -> 2. I have marked that as corroboration and said plainly it is
me matching a graph to observations -- the conditions on the edges are
not decoded, so nothing here proves which input picks which branch.

Still open and written down as such: the condition on each edge, what
states 1/3/5/6/7/9 do, and the fact that state 3 is never a destination
in this function, so something outside sets it.
2026-08-28 19:56:22 +00:00
Sylpheed RE agent
c8c7677f6d re: the lookup argument is decoded at 46 of 48 sites -- and it refutes my
own corroboration from last iteration

Last iteration I listed a candidate name vocabulary from "upper-case
strings the callers reference", marked it amber because those are not
proven arguments, and named the check: confirm per call site which string
actually lands in r5. Ran the check. It fired.

The first extractor FAILED ITS OWN CONTROL -- a backward scan for
addi r5 recovered 7 of 48 sites and missed both title sites I had already
read by eye, because there the name is built in r27 and reaches r5 via a
later mr. Rewritten as a forward register simulation it reproduces all
three known sites and recovers 46 of 48, giving 28 distinct names.

Two of my own claims die. DIFFICULTY and EXTRA_MENU are NOT arguments at
any site -- they are strings that merely live in the same functions, so
the "independently corroborated by measurement" line I wrote was an
artifact of the loose filter. Only TUTORIAL_MENU survives. And
sub_821CC860 is not a screen factory at all: its real arguments include
BG, BLACK, FADE, FILE, KEY, PAD, SOUND and GAMMA_RGB. It is a generic
name-keyed lookup, mostly config, which the title part happens to call
with its three state names.

The title finding itself is untouched -- sub_821C6458 really does
transition by calling this with TITLE_SCREEN, TITLE_MENU and LOADING.
What is gone is the inference that the 28 names are a screen vocabulary.

Two METHOD lines, both earned here: strings a function references are not
arguments it passes, and a backward scan for a destination register
misses arguments staged through a scratch register.
2026-08-28 19:52:02 +00:00
Sylpheed RE agent
8b4965f0a5 re: a title-side transition is a screen lookup BY NAME, not by id
Continuing Q6's last residual -- the control flow I left unread.

All three state-name sites in sub_821C6458 compile to the same shape:
load the name, then bl 0x821CC860 with it in r5 and 0 in r6, then hand
the result to sub_82187B78. So a transition is lookup-by-string then
install: sub_821CC860(this+88, this+24, "TITLE_SCREEN", 0). Not a numeric
id and not a table index. That also explains something that had been
sitting unexplained -- GP_ADVERTISE_DEMO having zero xrefs -- because at
this level the screen graph is keyed by name rather than by GamePart id.

sub_821CC860 has 28 callers, and the upper-case identifiers in that
neighbourhood split cleanly into screen names and config keys. Three of
the screen names are corroborated by measurements I took before ever
opening this function: DIFFICULTY is what NEW GAME opens, EXTRA_MENU is
the EXTRAS submenu, TUTORIAL_MENU the lesson list. That is the static
side agreeing with the dynamic side on names neither knew about the
other.

Held at amber deliberately. The 35 strings are what those callers
REFERENCE, not proven arguments, and the list plainly mixes screen names
with things like TEXT_FONT and GAMMA_RGB. Confirming it means checking
per call site which string actually lands in r5, and I did not do that.

Still unread: which state leads to which. The three lookups sit in
different branches and TITLE_MENU's is guarded by a cmplwi/bne, but I did
not trace the branch structure, so the ORDER still comes from measurement
rather than from the code.
2026-08-28 19:46:43 +00:00
Sylpheed RE agent
2be5ba7934 re: the title part's states are named in the executable -- TITLE_SCREEN,
TITLE_MENU, LOADING

Taking the one residual I had ranked as worth doing: sub_821C6458, which
the previous note left explicitly unread.

It is the title part's screen-state function -- 4460 bytes, has EH,
called from exactly one place, sub_821C7850, which sits beside the
creator the registration site points at. Its string references are
TITLE_SCREEN, TITLE_MENU, LOADING and BASE_INFO. Those are precisely the
states measured off the running game, in the game's own words, and
BASE_INFO is this corpus's own marker for a screen-config lookup rather
than a table read.

That sharpens Q6's negative rather than overturning it. The title part
DOES ask for configuration keyed by TITLE_SCREEN and TITLE_MENU, and
config.ini -- the disc's only config -- has no such sections, only an
empty [SYSTEM] and [LANGUAGE]. So the game asks the question, the shipped
disc does not answer it, and the defaults live in code. The port can
transcribe the state names even though it must author their contents.

Marked amber, not green: this is characterisation from string references
and call counts, not a read of the control flow. Two self-references
inside the function look like jump tables, which is what a state machine
compiles to, but I did not confirm that and nothing here shows which
state leads to which.

One refutation recorded because it looked like a find for several
minutes: the {func, func, ptr} triples at 0x820a3b48 read convincingly as
a state/handler table and are nothing of the kind -- the bytes before
them are the tail of a RegisterToFactory diagnostic string and the data
column is zero-filled descriptors. Static-init records, not dispatch.
2026-08-28 19:42:39 +00:00
Sylpheed RE agent
2b0e66b6ca re: confirm, move and back -- every cue the five screens need is now
located, and two of them reproduce

Continuing Q8 rather than opening anything new. The gap that mattered was
the confirm cue: a menu needs a sound on A, and I had only cursor and
cancel.

The fix was where I was counting from. The first run started counting
streams at the main menu, so the confirm cue had already fired during
boot and showed up as "played before any input". Counting from the TITLE
instead attributes it cleanly: the A that advances title -> main menu
fires the 12288-byte wave at 0x5d6c0, together with the two stereo BGM
stems, which is the menu's music starting.

So move (0x1ec0, 0.533 s), confirm (0x5d6c0, 1.016 s) and back (0x0ec0,
0.344 s) are all located and decodable. Move and back came back with
IDENTICAL head bytes and sizes on a second independent boot, so the
dedup-keyed method is stable and those two are now n=2 rather than n=1.

Two honest limits recorded rather than smoothed over. The A press both
confirms and opens a screen, so its wave could be the cue the vocabulary
calls DECIDE or the one it calls SUB_WIN_OPN -- the port gets the sound
the game plays, not a name. And left/right fired no new stream, which
excludes a DISTINCT invalid cue but cannot exclude them quietly replaying
one of the three already heard, because the probe dedups on head bytes.
2026-08-28 19:38:09 +00:00
Sylpheed RE agent
b1b2576769 re: the UI cues decode -- 0.53 s, 0.34 s and 1.02 s of real audio
Finishing the step I left open last iteration rather than starting
something new. Offsets and packet counts were in hand; what was missing
was proof they are actually waves.

slb_extract_wave.py wraps a (bank, offset, packets, channels, rate) slice
in a synthesized XMA1 RIFF, following the layout the Rust decoder already
uses. The three located cues decode to 0.533 s, 0.344 s and 1.016 s of
mono 48 kHz audio, audible from sample 0, each with the percussive
attack-and-decay envelope of a UI blip. Bitrates come out at 12-15 kB/s,
about half the stereo BGM rate, which is what mono should be.

The control matters more than the results. The SAME wrapper applied to
BGM_001's first wave decodes to 173.808875 s -- identical to the duration
that bank's own on-disc RIFF header produced back when Q10 was answered.
So the header I synthesized is not approximately right, it reproduces a
known-good decode exactly, and the cue durations are trustworthy for the
same reason.

I did not commit the decoded audio. Three commands regenerate it from the
disc, and the corpus's job is measurements and tooling rather than
extracted game assets. The offsets, the packet counts and the tool are
the deliverable.
2026-08-28 19:32:16 +00:00
Sylpheed RE agent
0d7199bc22 re: the UI sound effects ARE extractable -- retracting "cannot be
extracted", and the tool was in the build all along

Two iterations ago I closed Q8 by declaring the SE audio undecodable:
Static.slb has no wave boundaries, there is no XACT container anywhere,
and I said the index "exists only at runtime" as though that put it out
of reach. The disc half of that stands. The conclusion did not.

This build of Canary carries a cvar called xma_param_probe, added by this
project, whose own comment says it logs each XMA stream's parameters and
head bytes so raw sound.pak entries can be matched to real decode params.
It has been sitting in the startup CONFIG DUMP of every log I have read
this session.

Run with it, driving the main menu: a d-pad move spawns a new mono 48 kHz
stream of 4 packets / 8192 bytes, and B spawns a different one of 2
packets / 4096 bytes. Searching their logged head bytes in Static.slb
finds each at exactly one offset -- 0x1ec0 and 0x0ec0 -- and the two are
contiguous, 0x0ec0 + 4096 = 0x1ec0. So the bank is a packed run of whole
2048-byte packets with no delimiters, which is precisely why the seek
scan found nothing: there is nothing to find. A wave is (offset, packet
count) and nothing else.

That splits Q8's binding cleanly. Event -> WAVE is now measured: the port
can have the audio. Event -> cue NAME is still a name match on the
authors' identifiers, and the page says so.

The same run settled something for Q10 too. Sitting on the main menu, TWO
stereo 48 kHz streams were decoding simultaneously. bgm-two-stems.md said
that observation was what it needed and that this container could not
make it; it can, and a music bank's two waves are now measured as
simultaneous rather than only inferred.

METHOD gets the general lesson, because it cost two iterations: check
what instrumentation the local build already has before declaring a
question blocked on tooling.
2026-08-28 19:30:03 +00:00
Sylpheed RE agent
aea529e7dc re: S00A.wmv confirmed off the running game -- the movie was never
ambiguous, the sampling was

Continuing last iteration's amber rather than starting something new. The
fix was already named there: stop using 5-second screenshots and record
the display.

Recorded with ffmpeg x11grab at 10 fps and matched every 0.5 s. Across
the 25 consecutive samples from capture 5.0 s to 17.5 s the S00A playhead
is strictly non-decreasing -- 1.0 through 11.0 s, advancing at
essentially real time -- with scores at or above 0.96 and four of them at
0.999 or 1.000 against a runner-up in the 0.78-0.83 range. S00A is the
top match on 23 of the 27 frames carrying signal. So MS00A -> S00A.wmv is
decoded from the manifest AND measured off the game, and the intro begins
about 4.5 s after A on the save slot.

The previous attempt's failure is worth its own METHOD line, because it
did not look like a sampling problem: it looked like weak evidence for
the wrong film. Sparse sampling produced contrast-23 frames, a playhead
that would not join up, and one frame preferring ADV. Sampling does not
weaken a signal gracefully; it turns it into noise shaped like a
different answer.

One aside recorded and not chased: in the S00A 5.5-10 s window, ADV also
scores 0.97-0.99 at its OWN monotone playhead of 33.5-37.5 s. Two films
matching strongly with two consistent playheads is not noise -- it reads
as the boot movie being a trailer cut from the story cutscenes, which
also explains why the sparse run kept flipping between them.
2026-08-28 19:23:57 +00:00
Sylpheed RE agent
dfcaac221e re: the new-game path completes, the intro is only weakly identified, and
this container kills long runs

Three results, and the middle one is deliberately not upgraded.

First: NEW GAME -> DIFFICULTY -> NORMAL -> SELECT DATA -> pick a slot ->
a movie plays, with no crash. The previous run's throw at 0x82307128 is
therefore not inherent to the menu path -- the same six presses got
through it -- which fits the incomplete-cache trigger the corpus already
names. n=1 either way, and the note says so. Worth recording because one
observation had already hardened once into "A on NEW GAME hangs", and it
would have hardened again into "the new-game path crashes".

Second: with the path open, the manifest's MS00A -> S00A.wmv finally got
a runtime test, and it did NOT reproduce the quality of the attract-movie
identification. The first four samples give a clean monotonic S00A
playhead of 2, 6, 9, 13 s and the strongest frame matches at 0.916, but
the run as a whole is not one advancing timeline, most frames sit at
contrast 23-37 where this page's own rule says discrimination is poor,
and one high-contrast frame prefers ADV. So it stays amber: consistent
with S00A, corroborating the manifest decode, not confirming it. A clean
answer needs the 30fps x11grab route rather than 5-second screenshots.

Third, and it bounds the rest: the emulator vanished at ~145 s with no
crash line in its own log, last entries routine MEM-WATCH at 1.15 GB RSS.
Second time this session -- the other was ~50 s into a boot. That is an
external kill on a box with ~1 GB free and swap exhausted, not a guest
fault. METHOD and the handoff's residue table both now say dynamic
experiments here have to fit in about two minutes of guest time, which is
why the DELTASABER plates went unseen again: they need a mission load and
the run never got there.
2026-08-28 19:19:14 +00:00
Sylpheed RE agent
bc6354dce0 re: the title presents at 28.5 fps idle -- the keyframe unit is settled at
1/60 s, and the 60 Hz reading is excluded

ui-keyframe-time-unit.md named its own re-test and nobody had run it: time
300 submitted frames on the IDLE title, where nothing is streaming from
the ISO. Reaching the title is cheap now that one A skips the intro, so
this cost one boot.

Two clean trials: 300 frames in 10.40 s and 10.60 s, i.e. 28.8 and 28.3
fps. That is the same rate as the 27.6 fps measured during the loading
splash, which is exactly the page's own criterion for "the game is 30 Hz
and the unit is 1/60 s". The 60 Hz alternative is now excluded rather
than disfavoured: it needs the emulator at 47% of real time while sitting
idle on a screen the capture says costs 1526 draws over 300 frames, about
five per frame. Nothing there halves an emulator.

So the durations stand as written -- title 4.2 s, main menu 1.1 s, EXTRAS
0.87 s -- and Q1 leaves the handoff's open-residue table. It stays
MEASURED, not decoded: no field on the disc says sixtieths of a second.

One trap, and it nearly cost the conclusion. My first attempt reported
39.5 fps, which sits between the two hypotheses and would have left the
question open. It was a harness bug: I polled the log for a "done" line
rather than for the COUNT of them increasing, so it matched a capture
that had already completed and timed 0.1 s of nothing. METHOD gets the
general form -- a log-polling probe must compare a count, not test for
presence.
2026-08-28 19:11:11 +00:00
Sylpheed RE agent
2c4005795b port: audit the handoff -- four stale statements, a broken link, and the
residue in one place

Every MISSION row is answered, so this iteration checked the deliverable
itself rather than opening new scope: an answer that is not reachable
from HANDOFF.md has not been delivered, and I have been editing that page
by surgical replacement for a dozen iterations.

Four things were wrong or stale:

- the Q5 row still said initial focus was 2x TUTORIAL, 1x NEW GAME while
  its own bullet said four boots -- I updated the body last time and not
  the row;
- the Q2 bullet still described the other four main-menu destinations as
  "likely by name -- an inference, not a measurement", which Q4 has since
  MEASURED, and it listed GP_MISSION_SELECT as a main-menu destination
  when MISSION SELECT lives under EXTRAS;
- the Q1 bullet claimed that if the game presents at 60 Hz "nothing else
  on this page changes". That is now false: Q7's declared fade-in lengths
  are keyframe-derived and would halve. The same bullet also missed that
  the transition capture is a SECOND independent measurement supporting
  1 unit = 1/60 s -- the quad is declared black for 12 units, 0.20 s
  under the conversion, and the capture measured the plateau at
  0.17-0.23 s. Recorded as corroboration, not proof, because the plateau
  spans two screens' fades and cannot separate them;
- bgm-two-stems.md linked ../port/MISSION.md from inside structures/,
  which resolves nowhere.

Also named the two videos actually in scope, since "3.3 GB of video, two
files are in scope" is not much use without the filenames.

And added a What Is Still Open section. With every row green the residue
was scattered across eight pages; it is now one table with why each item
is stuck. Two entries share a single unblocker -- an emulator whose audio
path can be observed -- and they are the only ones that would change a
port decision.
2026-08-28 19:02:49 +00:00
Sylpheed RE agent
f5e7426cfa re: there is no XACT container on this disc -- retracting my own lead and
closing Q8

Last iteration I named Pj_Silph.xgs as the route to the SE cue index, on
the reasoning that XACT keeps cue->wave there. That route is dead, and
the negative is properly bounded: its 533-byte entry is high entropy with
no XGSF magic, +/-8 KB around it has none either, all 1.08 GB of
sound.pak contains zero XGSF, SDBK and WBND, and the executable has no
XACT or .xgs string anywhere. Control run first -- the same scan finds
RIFF in BGM_001 exactly where the bank structure says it should be, so
the scan works and the magic genuinely is not there.

So the .xgs and .slb extensions came from the authoring tool and survived
into shipped data that is not XACT at all. That goes in METHOD as a
general trap: check for the magic before committing to a parser for the
format an extension advertises. Writing an XACT parser would have been
wasted work.

What that leaves for the SE audio is undecodable-with-reach rather than
pending: Static.slb has no wave boundaries, there is no project file, and
none of the sound table's five records carries an offset. The bank is
certainly the right one -- 8 353 472 bytes over 322 cues is 0.84 to 1.01
seconds each at the bitrates the disc uses elsewhere, exactly the shape
of a bank of short effects. Only the index into it is missing, and it
exists only at runtime.

Q8 closes: vocabulary and bank decoded, the event binding a name match on
the authors' own event names, and the audio explicitly something the port
must source or author rather than extract. With this every MISSION row is
answered.
2026-08-28 18:58:06 +00:00
Sylpheed RE agent
c760cdc509 re: the boot sequence is not data-driven -- closing Q6 with the negative
rather than leaving it amber

Q6's second half asked what the game READS to decide the boot order. The
answer is nothing, and the value here is the reach rather than a find.

Four places checked, the order in none of them: config.ini's [SYSTEM] is
empty and it is the disc's only config; the movie manifest carries the
boot-side assets but no transitions; the requested GamePart id was
already shown to exist only as a stack argument in flight, with no
persistent field and no literal store; and the string GP_ADVERTISE_DEMO
has zero xrefs of any kind, so nothing in the code reads the attract
entry of the id table. A transition is a call with an id argument.

Traced as far as it goes cheaply: the RegisterToFactory<0,
GamePart_Title> string is referenced from exactly one site,
sub_8280E148, which also takes the address of sub_821C7D98 -- where a
factory template puts its creator. Marked amber, because that is position
and convention rather than proof, and sub_821C7D98 has zero .rdata
references, which fits a new+ctor thunk and not a state machine. The
substantial function in that neighbourhood is sub_821C6458 and I did not
read it.

So Q6 closes as answered with the driver classified as code rather than
data, which means the port AUTHORS the sequence -- and that is fine,
because the sequence itself is measured end to end and the handoff now
carries it in one line.
2026-08-28 18:53:14 +00:00
Sylpheed RE agent
c8e8dc0427 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
935f7ecab8 re: one A skips the intro, and NEW GAME was never a hang
Two corpus claims died in one boot, and both were costing us.

Q9's last row: a movie IS skippable with a single A. One tap ~45s into
the boot put the title up at ~57s, against a ~193s no-input baseline
measured over three boots, and Canary's own keystroke counter went 3->4
so exactly one press was delivered. The skipped-to title is fully
functional -- it draws the PRESS A plate and a second A opens the main
menu. What actually breaks the boot is hammering: the 88-press run in the
traps doc. The scripts' "tapping breaks the title" comment is too broad
and costs every scripted boot two and a half minutes.

Q4's last row: A on NEW GAME does not hang. It opens DIFFICULTY
(EASY/NORMAL/HARD/BACK, focus on NORMAL), then SELECT DATA, and only then
does the guest throw -- at PC 0x82307128, which is inside sub_823070B0,
the cache-manager STL erase this corpus already documents and which has
nothing to do with the menu path. The screen sat unchanged for 90s
because it was a menu waiting for input from a loop that never pressed
anything. That is now a METHOD line: a screen that never changes is not
necessarily hung, and the fix is to look at it and press something.

Also METHOD: never run ps -ef in this container -- all three long-lived
processes carry the entire loop prompt as argv.
2026-08-28 18:45:59 +00:00
Sylpheed RE agent
1f822f8b30 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
e81dcad632 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
e64965b3f9 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
b3c8632006 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
Sylpheed RE agent
7fef19b3a0 re: the transition between screens is a fade through black, and most of
its timing is on the disc

Q7. Every title-side screen carries a full-screen black .prm quad that
paints last, and its keyframe group IS the transition: black at T0, clear
by T1, clear until T2, then back to black on exit. Read with the corpus's
start-of-a-ramp rule and Q1's time unit that gives 0.87s for EXTRAS,
0.97s for the main menu, 4.08s for the title -- from the file, not from a
stopwatch.

The disc-wide check is per-pak all-or-nothing rather than the 41% the
headline count suggests, and GP_TITLE's 6 of 12 is the useful row: the
six builds carrying a fade quad are exactly the six SCREENS, and the six
without are exactly the six overlays. GP_DIALOG is 0 of 133. That is
independent corroboration of the overlay finding from two iterations ago.

One piece is NOT on the disc and says so: the fade-OUT length. The fourth
keyframe has no time slot, because a group's last block stops four bytes
short. Measured instead, at 30fps, ~0.4s and the same both directions.

And a warning I earned: the luminance rise after a transition is NOT the
quad's ramp. The incoming screen's own elements animate in after the quad
has cleared -- 1.47s observed against a declared 0.97s. Time the fade
from where the frame is pure black.

Rig: screenshot samples at 0.5 Hz and cannot see a 0.4s fade at all,
which is why an earlier burst called this an instant cut. ffmpeg x11grab
at 30fps instead; both go in METHOD.
2026-08-28 18:05:19 +00:00
Sylpheed RE agent
4e745c8177 re: the title menu wraps -- Q5 measured, Q4 driven, and one of my own
method lines withdrawn

Q5, measured off two boots: up/down move one item and WRAP at both ends
on the 5-item main menu and the 3-item EXTRAS alike; left/right do
nothing; B goes up one level and restores focus to the item you came
from (4/4); B on the main menu returns to the title; B on the title does
nothing. The menu opens on TUTORIAL -- the middle item -- 2/2, though a
third recorded run implies NEW GAME, so that one is reproducible rather
than invariant and says so.

Q4 by driving: LOAD GAME opens the save-slot list, TUTORIAL the lesson
list, OPTIONS the settings menu, EXTRAS build 6, MISSION SELECT the stage
list. NEW GAME is not tested -- A on it hangs the emulator and this
iteration needed the session. The GamePart ID behind each is NOT
measured: it is the entry of the decoded id table whose name matches the
screen I saw, and the page says so rather than wearing the badge.

And the withdrawal. Last iteration I wrote that these menus drop d-pad
presses shorter than ~0.3s. They do not. Once wrap is measured, every
press count I had is exactly right -- four presses moved four steps
THROUGH the bottom, which lands one above where a non-wrapping menu
would. I invented hardware flakiness instead of testing the ends of the
list. METHOD keeps the withdrawal rather than deleting the line.

Also: label brightness is not a cursor oracle here -- the background art
outshines the highlight on some rows. menu_focus.py reads the focus ring
instead, 254 vs <82, no tuning.
2026-08-28 17:53:46 +00:00
Sylpheed RE agent
bebdb18a02 re: Q3 was already answered in docs/re -- deliver it, and test it on EXTRAS
The handoff had Q3 as "runtime-solved only". It is not: the layer key at
+0x0A of the T8aD sprite header, stable-sorted, is a file-derivable paint
order, already checked against five measured orders and already driving
the compositor for every build on the disc. That answer had simply never
reached the page the port agent reads.

The new evidence is EXTRAS -- the first screen composited from the rule
alone and scored against a framebuffer capture the rule had never seen.
0.9620 at zero shift, against a same-tooling control of 0.9657 on the
screen the rule was fitted to.

Written down with its limit rather than its headline: align_to_capture
correlates edges, and a paint-order change moves blends, not edges, by
=<45/255 on a few per cent of pixels. So the score is evidence for
placement and only CONSISTENT with the order. The sharp A/B needs a
rebuild and is named in the doc instead of being claimed.
2026-08-28 17:39:29 +00:00
Sylpheed RE agent
88b3ce9af5 re: which GP_TITLE build is which screen, measured against the game
Q2. The archive is eight screens shipped twice, English and Japanese --
not the "build 4 title, 5 main menu, 6/8/9 submenus" the handoff claimed.
Build 8 is the JAPANESE main menu; 6 and 9 are the EN and JP EXTRAS, and
EXTRAS is the only submenu GP_TITLE holds. The PRESS (A) BUTTON plate is
its own build (2/3), composited over the title art and faded in a beat
later, not a state of build 4.

Confirmed by booting to the main menu and walking it: title, PRESS (A),
main menu and EXTRAS each match their render element for element. Builds
0/1 and 10/11 -- a DELTASABER / SYLPHEED A.I. plate -- were looked for in
the whole boot filmstrip, every title-side screen and the attract loop,
and appear in none of them; the reach of that negative is written down
rather than filled in with a guess.

Two rig traps went into METHOD: the menus drop d-pad presses shorter than
~0.3 s, and a grab 2.5 s after a transition can catch a screen mid-fade
-- which nearly wrote "the returned title has no plate" into the corpus.
2026-08-28 16:55:33 +00:00
Sylpheed RE agent
27e8c51978 re: a keyframe time is worth half a rendered frame, and the ramp is linear
Q1 of the menu port, measured against the running game rather than reasoned
about. The developer-logo splash is the cheap target: it is the first thing the
guest draws and its bundle declares short, unambiguous ramps.

Two results, both frame-exact and both emulator-speed-independent (frame numbers
are VdSwap counts, the guest's own frames):

  * the ramp is LINEAR. A declared 15-unit fade lands on round(255*k/15) for all
    seven of its samples with zero error, k stepping 2,4,6,8,10,12,14. No ease
    can reproduce a constant step of 34 at both ends.
  * the animation clock advances 2.000 time units per submitted frame, over six
    consecutive intervals with no residual, with 1 unit as the quantum
    underneath (one frame in the fade-out advances by 1).

The conversion to seconds is one step further and is flagged as such: 300 frames
took 10.87 s = 27.6 present-frames/second, which reads as a 30 Hz title at 92 %
under the emulator and gives 1 unit = 1/60 s -- the title build 4.2 s, the main
menu build 1.1 s. That reading is not proven, because the rate was measured
while the guest was still streaming from the ISO; the page names the one test
that would settle it and says what changes if it goes the other way.

Committed beside it: the raw draw capture and the per-frame quad CSV, so the
numbers can be re-derived without a disc or an emulator.
2026-08-28 15:56:33 +00:00
139 changed files with 14937 additions and 70 deletions

View File

@@ -523,7 +523,7 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
println!();
println!("geometry — decoded sprite size vs the declared pivot, and every keyframe");
println!(
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=fade-alpha",
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=alpha r=rot°",
"#", "sprite", "decoded", "pivot*2", "same"
);
for el in &b.elements {
@@ -543,8 +543,13 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
.keyframes
.iter()
.map(|f| {
let rot = if f.rotation_deg != 0 {
format!(" r={}", f.rotation_deg)
} else {
String::new()
};
format!(
"{}: {},{} {}%,{}% a={}",
"{}: {},{} {}%,{}% a={}{rot}",
f.time.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
f.x,
f.y,

View File

@@ -0,0 +1,67 @@
//! Does any unread declaration word POINT at the element's T8aD child?
//!
//! `pteff05.t32`/`pteff04.t32` resolve to no sprite because the `T8aD` they want
//! is registered under the name `8AX`. Elimination says `8AX` is the one they
//! mean -- one unresolved element, one unclaimed non-focus-state child, in 6 of
//! 6 title-side builds. Elimination is not a pointer, so: the 60-byte
//! declaration reads name[0..28], parent@32, kind@40, pivot@48/52. The words at
//! +28, +36, +44 and +56 are unread. If one of them indexes the RATC child
//! table, the RESOLVED elements are the control -- their child index is known,
//! so a candidate field must reproduce it for them before it may be believed for
//! the unresolved one.
//!
//! cargo run -p sylpheed-formats --example decl_word_probe -- <pak> [entry]
use sylpheed_formats::{pak, ratc, ui_layout};
const OFFS: [usize; 4] = [28, 36, 44, 56];
fn be32(b: &[u8], o: usize) -> u32 {
if o + 4 > b.len() { return 0; }
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
}
fn main() {
let path = std::env::args().nth(1).expect("usage: decl_word_probe <pak> [entry]");
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
let ar = pak::PakArchive::open(&path).expect("open pak");
let entries: Vec<_> = ar.entries().to_vec();
// Per candidate offset, across every build: control hits / control total.
let mut hit = [0usize; 4];
let mut tot = 0usize;
for (i, e) in entries.iter().enumerate() {
if want.is_some_and(|w| w != i) { continue; }
let Ok(bytes) = ar.read(e) else { continue };
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
let Some(kids) = ratc::parse(&bytes) else { continue };
// Index space to test against: the T8aD children, in child order.
let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect();
if build.elements.iter().all(|el| el.sprite.is_some() || el.kind & 0x10 != 0) {
continue;
}
println!("== entry {i} ({} elements, {} T8aD children)", build.elements.len(), t8.len());
for (n, c) in t8.iter().enumerate() { println!(" child[{n:2}] {}", c.name); }
for el in &build.elements {
if el.kind & 0x10 != 0 { continue; }
let d = &bytes[0x20 + el.index * 60..0x20 + (el.index + 1) * 60];
let words: Vec<u32> = OFFS.iter().map(|&o| be32(d, o)).collect();
// The control: for a RESOLVED element, which T8aD child is it?
let truth = el.sprite.as_ref()
.and_then(|s| t8.iter().position(|c| &c.name == s));
if let Some(t) = truth {
tot += 1;
for (k, w) in words.iter().enumerate() {
if *w as usize == t { hit[k] += 1; }
}
}
println!(
" [{:2}] {:26} sprite={:?} child={:?} +28={} +36={} +44={} +56={}",
el.index, el.name, el.sprite, truth,
words[0] as i32, words[1] as i32, words[2] as i32, words[3] as i32
);
}
}
println!("\nCONTROL: resolved elements whose child index a word reproduces, of {tot}:");
for (k, o) in OFFS.iter().enumerate() {
println!(" +{o:<3} {:3}/{tot}", hit[k]);
}
}

View File

@@ -0,0 +1,51 @@
//! Why does an element's sprite fail to resolve? Dump the two name spaces.
//!
//! `parse_build` resolves an element to a sprite by looking its DECLARED name up
//! in (a) the `.rat` record table, then (b) the `T8aD` child table. `pteff05.t32`
//! is in neither -- the `T8aD` it wants is registered as `8AX` -- so it resolves
//! to None and `compose` drops it without recording it as missing. This prints
//! both spaces, so the link between the two can be CHECKED rather than assumed.
//!
//! cargo run -p sylpheed-formats --example name_resolution -- <pak> <entry>
use sylpheed_formats::{pak, ui_layout};
fn main() {
let path = std::env::args().nth(1).expect("usage: name_resolution <pak> [entry]");
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
let ar = pak::PakArchive::open(&path).expect("open pak");
let entries: Vec<_> = ar.entries().to_vec();
for (i, e) in entries.iter().enumerate() {
if want.is_some_and(|w| w != i) {
continue;
}
let Ok(bytes) = ar.read(e) else { continue };
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
let unresolved: Vec<&ui_layout::Element> = build
.elements
.iter()
.filter(|el| el.sprite.is_none() && el.kind & 0x10 == 0)
.collect();
let claimed: std::collections::HashSet<&str> =
build.elements.iter().filter_map(|e| e.sprite.as_deref()).collect();
let unclaimed: Vec<&String> =
build.sprites.keys().filter(|k| !claimed.contains(k.as_str())).collect();
if want.is_none() && unresolved.is_empty() && unclaimed.is_empty() {
continue;
}
let un: Vec<&str> = unresolved.iter().map(|e| e.name.as_str()).collect();
let uc: Vec<String> = unclaimed
.iter()
.map(|k| format!("{k}({} B)", build.sprites[*k].1))
.collect();
println!(
"entry {i:3} {:2} elements UNRESOLVED {:?} UNCLAIMED {:?}",
build.elements.len(), un, uc
);
if want.is_some() {
for el in &build.elements {
let mark = if el.sprite.is_none() && el.kind & 0x10 == 0 { " <-- UNRESOLVED" } else { "" };
println!(" [{:2}] kind {:#06x} {:28} -> {:?}{mark}", el.index, el.kind, el.name, el.sprite);
}
}
}
}

View File

@@ -0,0 +1,136 @@
//! Does the DERIVED paint order reproduce the ones measured from the game?
//!
//! `compose` uses a measured order for the three builds that have one and falls
//! back to `derived_paint_order` (a sort on each sprite's layer key) everywhere
//! else. The doc comment claims the derived order "reproduces both measured
//! orders up to ties" — this checks that claim against all three, and says what
//! the ties actually cost.
//!
//! cargo run -p sylpheed-formats --example paint_order_audit -- <GP_TITLE.pak>
use sylpheed_formats::{pak, ui_layout};
fn measured(names: &[&str]) -> Option<(&'static str, Vec<usize>)> {
const TITLE: [&str; 24] = [
"ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32",
"ptlogo2.t32", "pteff01.t32", "ptlogo_tm.t32", "pteff00.prm", "ptbase2.t32",
"pteff04.t32", "ptloop01.rat", "ptloop02.rat", "pteff02.prm",
"ptlogo_back2eff1.t32", "ptlogo_back2eff2.t32", "ptlogo_back2eff3.t32",
"ptlogo_back2eff4.t32", "ptlogo_back2eff5.t32", "ptlogo_back2.t32",
"ptlogo_back2eff.t32", "ptcopyright.t32", "ptlogoall_eff.t32",
"ptlogoall_eff2.t32",
];
const SPLASH: [&str; 7] = [
"palogo_eff0.prm", "palogo_gamearts.t32", "palogo_gamearts_eff.t32",
"palogo_seta.t32", "palogo_seta_eff.t32", "palogo_anima.t32",
"palogo_anima_eff.t32",
];
const MENU: [&str; 16] = [
"pteff00.prm", "ptbase.t32", "pteff05.t32", "ptloop01.rat",
"ptloop02.rat", "pteff02.prm", "ptframe1.t32", "ptframe2.t32",
"pteff10.t32", "pteff12.t32", "ptbtn01.rat", "ptbtn02.rat",
"ptbtn03.rat", "ptbtn04.rat", "ptbtn05.rat", "ptmsg.t32",
];
if names == TITLE {
return Some(("title", vec![9,11,12,10,13,6,20,19,14,15,18,16,17,0,2,4,7,1,3,5,22,23,21,8]));
}
if names == SPLASH { return Some(("splash", vec![0,2,4,6,1,3,5])); }
if names == MENU {
return Some(("main menu", vec![1,3,4,2,5,8,9,6,7,15,10,11,12,13,14,0]));
}
None
}
fn main() {
let path = std::env::args().nth(1).expect("usage: paint_order_audit <pak>");
let ar = pak::PakArchive::open(&path).expect("open pak");
let mut checked = 0;
let entries: Vec<_> = ar.entries().to_vec();
for (i, e) in entries.iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect();
// Every build: how exposed is it to tie-breaking? A tie between
// OVERLAPPING elements is where a derived order can go visibly wrong.
let keys_all: Vec<u32> = build.elements.iter()
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
.collect();
let mut tie_pairs = 0;
for a in 0..keys_all.len() {
for b in (a + 1)..keys_all.len() {
if keys_all[a] == keys_all[b] && keys_all[a] != u32::MAX { tie_pairs += 1; }
}
}
// Of the tied pairs, how many OVERLAP? Only those can paint visibly
// differently under an arbitrary tie-break. Rect from the declared
// pivot (= half the sprite for a .t32) at the resting placement.
let rect = |e: &ui_layout::Element| -> Option<(i32,i32,i32,i32)> {
let kf = e.rest()?;
let (w, h) = ((e.pivot_x * 2) as i32, (e.pivot_y * 2) as i32);
if w == 0 || h == 0 { return None; }
Some((kf.x, kf.y, w, h))
};
let mut tie_overlap = 0;
for a in 0..keys_all.len() {
for b in (a + 1)..keys_all.len() {
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
else { continue };
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
if ox > 0 && oy > 0 { tie_overlap += 1; }
}
}
let Some((label, want)) = measured(&names) else {
println!("entry {i:2} (no measured order) {} elements, {tie_pairs} tied pairs, \
{tie_overlap} of them OVERLAPPING", build.elements.len());
// Name them: these are the only pairs whose order can show.
for a in 0..keys_all.len() {
for b in (a + 1)..keys_all.len() {
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
else { continue };
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
if ox > 0 && oy > 0 {
println!(" overlapping tie: [{a}] {} x [{b}] {} key {} rect {:?} / {:?} overlap {}x{}",
build.elements[a].name, build.elements[b].name, keys_all[a], ra, rb, ox, oy);
}
}
}
continue;
};
checked += 1;
let got = ui_layout::derived_paint_order(&build, &bytes);
let keys: Vec<u32> = build.elements.iter()
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
.collect();
let exact = got == want;
// How many adjacent pairs in the MEASURED order does derived get wrong,
// and of those, how many are between elements sharing a layer key (a
// tie the sort cannot resolve) versus a genuine key-order conflict?
let pos_got: Vec<usize> = {
let mut p = vec![0; got.len()];
for (r, &e) in got.iter().enumerate() { p[e] = r; }
p
};
let (mut inv, mut tied) = (0, 0);
for a in 0..want.len() {
for b in (a + 1)..want.len() {
let (x, y) = (want[a], want[b]);
if pos_got[x] > pos_got[y] {
inv += 1;
if keys[x] == keys[y] { tied += 1; }
}
}
}
println!("entry {i:2} {label:10} {} elements", want.len());
println!(" derived == measured : {}", if exact { "YES" } else { "NO" });
println!(" inverted pairs : {inv} (of which same-layer-key ties: {tied})");
if !exact {
println!(" measured: {want:?}");
println!(" derived : {got:?}");
println!(" keys : {keys:?}");
}
}
println!("\n{checked} build(s) with a measured order were checked");
}

View File

@@ -0,0 +1,52 @@
//! Does DRAWING NOTHING beat guessing, for an element with no held pose?
//!
//! A keyframe group is entry → hold → exit, and the exit ends invisible (on the
//! five port screens the final keyframe is invisible for 21/24, 8/16, 12/18, 2/3
//! and 6/7 elements). So the screen "as seen" is the HOLD — which is why
//! `rest_plateau` is the primary rule. An element with **no** plateau has no
//! hold, and `rest()` currently falls back to guessing an endpoint of a movement.
//!
//! This renders each screen twice — as-is, and with every plateau-less element
//! suppressed via `compose`'s `visible` mask — and correlates both against the
//! live capture. If suppression wins, the fallback should draw nothing.
//! Writes both composites as raw RGBA (`<out>/entryNN_{asis,suppressed}.raw`,
//! 1280x720) so the correlation is done outside — this crate has no image
//! decoder and the comparison is not worth a dependency.
use sylpheed_formats::{pak, ui_layout};
fn main() {
let pak_path = std::env::args().nth(1).expect("usage: <GP_TITLE.pak> <outdir> <entry>...");
let ar = pak::PakArchive::open(&pak_path).expect("open");
let entries: Vec<_> = ar.entries().to_vec();
let outdir = std::env::args().nth(2).expect("outdir");
std::fs::create_dir_all(&outdir).ok();
for spec in std::env::args().skip(3) {
let idx: usize = spec.parse().unwrap();
let bytes = ar.read(&entries[idx]).expect("read");
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
// plateau-less = rest() had to guess: no two adjacent keyframes share a pose
// Default: suppress plateau-less elements. With SUPPRESS_SUBSTR set,
// suppress every element whose NAME contains it instead — used to test
// the entry→hold→exit model's prediction that the splash glows are all
// finished by the moment the logos are up.
let by_name = std::env::var("SUPPRESS_SUBSTR").ok();
let mask: Vec<bool> = build.elements.iter().map(|e| {
if let Some(sub) = &by_name {
return !e.name.to_lowercase().contains(sub.as_str());
}
let k = &e.keyframes;
(0..k.len().saturating_sub(1)).any(|i| {
k[i].fade == k[i+1].fade && k[i].scale_x == k[i+1].scale_x
&& k[i].scale_y == k[i+1].scale_y && k[i].x == k[i+1].x && k[i].y == k[i+1].y
})
}).collect();
let suppressed = mask.iter().filter(|m| !**m).count();
let opts = ui_layout::ComposeOptions::default();
let a = ui_layout::compose(&build, &bytes, opts, None);
let b = ui_layout::compose(&build, &bytes, opts, Some(&mask));
std::fs::write(format!("{outdir}/entry{idx:02}_asis.raw"), &a.rgba).unwrap();
std::fs::write(format!("{outdir}/entry{idx:02}_suppressed.raw"), &b.rgba).unwrap();
println!("entry {idx:2} {}x{} elements {:2} plateau-less suppressed {suppressed}",
a.width, a.height, build.elements.len());
}
}

View File

@@ -0,0 +1,71 @@
//! Does a `.rat` leaf record decode with the same reader as a whole bundle?
//!
//! The port needs the position of `ptbtneff01.t32`, the focus ring, which is
//! declared *inside* the nested `ptbtn0Nf.rat` leaf and is therefore invisible
//! to anything that walks only a bundle's top-level elements.
//!
//! The leaf's first 32 bytes have the same shape as a bundle header --
//! `"RATC"`, `0x3c` declaration-entry size at `+4`, element count at `+20`,
//! design size at `+24`/`+28` -- so the hypothesis is that `parse_build` reads
//! it unchanged. The control is the BASE record `ptbtn0N.rat`, whose single
//! element's position is already known independently: the parent screen's
//! `screen info` reports `ptbtn01.rat` resting at (542,162).
//!
//! cargo run -p sylpheed-formats --example rat_leaf_placement -- <GP_TITLE.pak>
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let path = std::env::args().nth(1).expect("usage: … <pak>");
let ar = pak::PakArchive::open(&path).expect("open pak");
let entries: Vec<_> = ar.entries().to_vec();
for (ei, e) in entries.iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
let Some(kids) = ratc::parse(&bytes) else { continue };
// Only the title-family bundles carry ptbtn records.
if !kids.iter().any(|c| c.name == "ptbtn01f.rat") {
continue;
}
println!("=== pak entry {ei}");
for c in &kids {
if c.kind != "RATC" || !c.name.starts_with("ptbtn") {
continue;
}
let leaf = &bytes[c.offset..c.offset + c.size];
match ui_layout::parse_build(leaf) {
None => println!(" {:16} …parse_build says no", c.name),
Some(b) => {
println!(
" {:16} {}x{} {} element(s), fallback={}",
c.name,
b.design_w,
b.design_h,
b.elements.len(),
b.from_fallback
);
for el in &b.elements {
let r = el.rest();
println!(
" [{}] {:18} pivot ({:4},{:4}) rest ({:5},{:5}) kf {}",
el.index,
el.name,
el.pivot_x,
el.pivot_y,
r.map(|k| k.x).unwrap_or(-1),
r.map(|k| k.y).unwrap_or(-1),
el.keyframes.len()
);
for (i, k) in el.keyframes.iter().enumerate() {
println!(
" kf{i} t={:?} pos=({},{}) scale={}%,{}% a={} rot={} tint={:#010x}",
k.time, k.x, k.y, k.scale_x, k.scale_y,
k.fade >> 24, k.rotation_deg, k.tint
);
}
}
}
}
}
println!();
}
}

View File

@@ -0,0 +1,78 @@
//! Is a RATC child's name the printable run before its magic, or the `opt ` block?
//!
//! `ratc::parse` names each child by scanning backwards for the ASCII run that
//! precedes its magic. That is usually right, but it is a HEURISTIC, and the
//! real format states the name explicitly: immediately before each child sits
//!
//! "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
//!
//! -- the same `opt ` block `ui_layout::opt_link` already decodes for a button's
//! focus link. When those 3 trailing bytes happen to be printable the heuristic
//! reads THEM as the name: the title screens' full-resolution background comes
//! out as `8AX` (bytes 38 41 58) instead of `pteff05.t32`, its element then
//! resolves to no sprite, and `compose` drops the screen's background.
//!
//! This compares the two readings for every RATC child in the paks given.
//!
//! cargo run -p sylpheed-formats --example ratc_child_names -- <pak>...
use sylpheed_formats::{pak, ratc};
/// The name stated by the `opt ` block that ends just before `at`.
fn opt_name(buf: &[u8], at: usize) -> Option<String> {
// The block is short; search back a bounded window for the tag.
let lo = at.saturating_sub(128);
let win = &buf[lo..at];
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
let len = u32::from_be_bytes(buf[pos + 4..pos + 8].try_into().ok()?) as usize;
if len == 0 || len > 64 || pos + 8 + len > at {
return None;
}
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]).to_string();
// It must be THIS child's block: name, NUL, then a short run to the magic.
if at - (pos + 8 + len) > 8 {
return None;
}
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
}
fn main() {
let mut children = 0usize;
let mut with_opt = 0usize;
let mut agree = 0usize;
let mut disagree: Vec<(String, usize, String, String)> = Vec::new();
for path in std::env::args().skip(1) {
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
let entries: Vec<_> = ar.entries().to_vec();
for (i, e) in entries.iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
let Some(kids) = ratc::parse(&bytes) else { continue };
for c in &kids {
children += 1;
let Some(o) = opt_name(&bytes, c.offset) else { continue };
with_opt += 1;
if o == c.name {
agree += 1;
} else {
disagree.push((short.clone(), i, c.name.clone(), o));
}
}
}
}
println!("RATC children scanned : {children}");
println!(" with an `opt ` block: {with_opt}");
println!(" scanned name AGREES : {agree}");
println!(" scanned name DIFFERS : {}", disagree.len());
let mut by_pair: std::collections::BTreeMap<(String, String), usize> = Default::default();
for (_, _, scanned, opt) in &disagree {
*by_pair.entry((scanned.clone(), opt.clone())).or_default() += 1;
}
println!("\ndistinct disagreements (scanned -> opt), with counts:");
for ((s, o), n) in &by_pair {
println!(" {s:24} -> {o:24} x{n}");
}
println!("\nfirst 20 occurrences:");
for (p, i, s, o) in disagree.iter().take(20) {
println!(" {p} entry {i:4} {s:20} -> {o}");
}
}

View File

@@ -0,0 +1,119 @@
//! The 60 RATC children that carry no `opt ` block — do they lack it, or is our
//! window too small?
//!
//! [`ratc::parse`] now prefers the name a child's own `opt ` block states, and
//! falls back to the old backwards printable-run scan when there is no block
//! within 128 bytes. That fallback fires for 60 of the disc's 18 002 children
//! (0.3 %), and the reach of the finding in `docs/re/structures/ratc-child-names.md`
//! stops there: "whether they genuinely lack the block or sit past the search
//! window is not established".
//!
//! This settles that. For every child with no accepted block it reports
//!
//! * whether an `opt ` tag exists at all further back, and how far;
//! * which guard rejected a tag that WAS in the window (length, gap, charset);
//! * the child's position in its bundle and its magic, in case the opt-less
//! ones are structurally distinct (e.g. always the first child);
//! * the raw bytes before the magic, so the fallback's answer can be judged.
//!
//! cargo run -p sylpheed-formats --example ratc_optless_children -- <pak>...
use sylpheed_formats::{pak, ratc};
/// Why a child has no accepted `opt ` name. Mirrors `ratc::opt_name`'s guards
/// one for one, so a rejection here is the same rejection the parser made.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
enum Why {
/// No `opt ` tag in the 128-byte window, and none anywhere before it either.
NoTagAtAll,
/// No tag in the window, but one exists further back, this many bytes away.
TagBeyondWindow(usize),
/// Tag found, but its BE32 length is 0 or > 64.
BadLength(usize),
/// Tag found, name ends more than 8 bytes before the magic — a neighbour's.
GapTooBig(usize),
/// Tag found, the named bytes are not all printable ASCII.
NotGraphic,
}
/// The parser's own window.
const WINDOW: usize = 128;
fn classify(buf: &[u8], at: usize) -> Option<Why> {
let lo = at.saturating_sub(WINDOW);
let pos = match buf[lo..at].windows(4).rposition(|w| w == b"opt ") {
Some(p) => lo + p,
None => {
// Widen to the whole buffer before the child: is it merely far away?
return Some(match buf[..at].windows(4).rposition(|w| w == b"opt ") {
Some(p) => Why::TagBeyondWindow(at - p),
None => Why::NoTagAtAll,
});
}
};
let len = u32::from_be_bytes(buf.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
if len == 0 || len > 64 || pos + 8 + len > at {
return Some(Why::BadLength(len));
}
let gap = at - (pos + 8 + len);
if gap > 8 {
return Some(Why::GapTooBig(gap));
}
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]);
if s.is_empty() || !s.chars().all(|c| c.is_ascii_graphic()) {
return Some(Why::NotGraphic);
}
None // accepted — this child is not one of the 60
}
fn main() {
let mut total = 0usize;
let mut rows: Vec<(String, usize, usize, String, String, Why)> = Vec::new();
let mut first_child_of_bundle = 0usize;
for path in std::env::args().skip(1) {
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
let entries: Vec<_> = ar.entries().to_vec();
for (ei, e) in entries.iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
let Some(kids) = ratc::parse(&bytes) else { continue };
for (ci, c) in kids.iter().enumerate() {
total += 1;
let Some(why) = classify(&bytes, c.offset) else { continue };
if ci == 0 {
first_child_of_bundle += 1;
}
let lo = c.offset.saturating_sub(24);
let hex = bytes[lo..c.offset]
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ");
rows.push((short.clone(), ei, ci, c.name.clone(), hex, why));
}
}
}
println!("RATC children scanned : {total}");
println!(" with NO accepted `opt ` block: {}", rows.len());
println!(" ... of which are child #0 : {first_child_of_bundle}");
let mut by_why: std::collections::BTreeMap<String, usize> = Default::default();
for r in &rows {
let k = match &r.5 {
Why::TagBeyondWindow(_) => "TagBeyondWindow".to_string(),
Why::BadLength(_) => "BadLength".to_string(),
Why::GapTooBig(_) => "GapTooBig".to_string(),
other => format!("{other:?}"),
};
*by_why.entry(k).or_default() += 1;
}
println!("\nwhy, by cause:");
for (k, n) in &by_why {
println!(" {k:20} x{n}");
}
println!("\nevery occurrence (name is what the FALLBACK scan returned):");
for (p, ei, ci, name, hex, why) in &rows {
println!(" {p:28} entry {ei:4} child {ci:3} {name:24} {why:?}");
println!(" 24 bytes before the magic: {hex}");
}
}

View File

@@ -0,0 +1,27 @@
//! Dump the menu's three SE cues as decodable `RIFF`s, to prove `se_wave_riff`
//! produces something ffmpeg actually accepts.
//!
//! cargo run -p sylpheed-formats --example se_wave_dump -- <outdir>
//! ffmpeg -i <outdir>/move.riff move.wav
use sylpheed_formats::media::{self, DirectorySource};
fn main() {
let out = std::env::args().nth(1).unwrap_or_else(|| "/tmp".into());
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
let src = DirectorySource::new(&disc);
for (name, off, pkts) in [("move", 0x1ec0usize, 4usize), ("back", 0x0ec0, 2), ("confirm", 0x5d6c0, 6)] {
match media::se_wave_riff(&src, "Static.slb", off, pkts, 1, 48000) {
Ok(riff) => {
let p = format!("{out}/{name}.riff");
std::fs::write(&p, &riff).unwrap();
println!("{p}: {} bytes ({pkts} packets at {off:#x})", riff.len());
}
Err(e) => println!("{name}: ERROR {e}"),
}
}
// The refusal path: a packet count the bank cannot satisfy.
match media::se_wave_riff(&src, "Static.slb", 0x1ec0, 1 << 20, 1, 48000) {
Ok(_) => println!("REFUSAL PATH FAILED — returned a short stream"),
Err(e) => println!("refusal path ok: {e}"),
}
}

View File

@@ -87,6 +87,62 @@ pub fn sound_bank_riffs<S: DiscSource + ?Sized>(
Ok(riffs_of(&bytes))
}
/// One sound-effect wave out of a **delimiter-less** bank, as a decodable `RIFF`.
///
/// `Static.slb` — where the menu's cues live — has no `RIFF`, no `seek` chunk and
/// no XACT container: it is a packed run of whole 2048-byte XMA1 packets. So
/// [`sound_bank_riffs`] finds nothing to split on, and a wave is defined *only*
/// by `(offset, packet_count)`. Both come from the running game, not from the
/// file: launch Canary with `--xma_param_probe=true`, trigger the sound, and the
/// log prints the stream's packet count and first 32 bytes; searching those bytes
/// in the bank gives the offset. ⚠️ The file order is **not** cue-id order, so the
/// index cannot be counted out — see `docs/re/menu-audio-cues.md`.
///
/// The three cues a menu needs, all mono 48 kHz:
///
/// | event | offset | packets |
/// |---|---|---|
/// | d-pad move | `0x1ec0` | 4 |
/// | Ⓑ back | `0x0ec0` | 2 |
/// | Ⓐ confirm | `0x5d6c0` | 6 |
///
/// Returns an error rather than a short stream if the bank does not actually
/// hold `packet_count` whole packets at `offset` — a truncated XMA stream decodes
/// to plausible-sounding garbage, which is the failure worth refusing.
pub fn se_wave_riff<S: DiscSource + ?Sized>(
source: &S,
bank: &str,
offset: usize,
packet_count: usize,
channels: u8,
rate: u32,
) -> Result<Vec<u8>, String> {
let len = packet_count * crate::slb::XMA1_PACKET;
// Read only the packets asked for, not the whole bank. That is not just an
// efficiency point: `Static.slb` is 8.97 MB and is the ONE entry in
// `sound.pak` whose declared extent runs past the end of the extracted
// segments (by 616 768 B), so reading it whole fails outright on a disc
// extraction that is short at the tail. Every cue we need sits in the first
// few hundred KB. See `docs/re/menu-audio-cues.md`.
let toc = source.read_file("dat/sound.pak")?;
let entries = PakArchive::parse_toc(&toc).map_err(|e| e.to_string())?;
let hash = crate::hash::name_hash(bank);
let idx = entries
.binary_search_by_key(&hash, |e| e.name_hash)
.map_err(|_| format!("{bank}: not present in sound.pak"))?;
let e = &entries[idx];
if offset + len > e.comp_size as usize {
return Err(format!(
"{bank}: {packet_count} packets at {offset:#x} need {len} bytes, \
but the bank declares only {} bytes",
e.comp_size
));
}
let packets =
source.read_segment_range("dat/sound", e.offset as u64 + offset as u64, len)?;
Ok(crate::slb::xma1_wave_riff(&packets, channels, rate))
}
/// The XMA `RIFF`s of a continuous byte region of the voice stream, as returned
/// by [`resolve_movie_voice_region`].
pub fn voice_region_riffs<S: DiscSource + ?Sized>(

View File

@@ -62,7 +62,7 @@ pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
for (idx, &(off, kind)) in offs.iter().enumerate() {
let next = offs.get(idx + 1).map(|&(o, _)| o).unwrap_or(bytes.len());
children.push(RatcChild {
name: name_before(bytes, off),
name: opt_name(bytes, off).unwrap_or_else(|| name_before(bytes, off)),
kind: kind.to_string(),
offset: off,
size: next.saturating_sub(off),
@@ -71,9 +71,51 @@ pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
Some(children)
}
/// The name a child's own `opt ` block states, if it has one.
///
/// The real format is explicit. Immediately before each child sits
///
/// ```text
/// "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
/// ```
///
/// -- the same `opt ` block `ui_layout`'s focus link already reads. Prefer it,
/// because [`name_before`] is a heuristic and those 3 trailing bytes are
/// sometimes printable, in which case the heuristic reads THEM as the name.
/// Measured disc-wide: of 18 002 RATC children, 17 942 carry an `opt ` block,
/// 17 918 of which agree with the scan and **24 do not** -- every one of the 24
/// a 3-byte tail (`8AX` x22, `'OX` x2) beating a real name. On the title screens
/// that cost the whole background: `pteff05.t32` came out as `8AX`, its element
/// then resolved to no sprite, and `compose` silently dropped it. See
/// `docs/re/structures/ratc-child-names.md`.
fn opt_name(bytes: &[u8], off: usize) -> Option<String> {
let lo = off.saturating_sub(128);
let win = &bytes[lo..off];
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
let len = u32::from_be_bytes(bytes.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
if len == 0 || len > 64 || pos + 8 + len > off {
return None;
}
// It must be THIS child's block: the name, its NUL and a short run to the
// magic. Anything further away is a neighbour's block, so fall back.
if off - (pos + 8 + len) > 8 {
return None;
}
let s = String::from_utf8_lossy(&bytes[pos + 8..pos + 8 + len]).to_string();
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
}
/// The nearest name string preceding `off`: the *last* printable run (len ≥ 3)
/// in the 96 bytes before the child magic. A few record-header bytes usually sit
/// between the name and the magic, so an exact-adjacency scan isn't enough.
///
/// Fallback only -- [`opt_name`] is the stated name. 60 of the disc's 18 002
/// children have no `opt ` block and still rely on this, and all 60 are
/// accounted for: they are the ten frames of the disc's only `.tan` frame
/// sequence, in six language copies of one `GP_READY_ROOM` bundle, where a
/// single `opt ` block names the whole run. See
/// `docs/re/structures/ratc-tan-frame-sequence.md` -- and note that this means
/// `parse` OVER-reports there, listing frames as children.
fn name_before(bytes: &[u8], off: usize) -> String {
let start = off.saturating_sub(96);
let window = &bytes[start..off];
@@ -101,6 +143,49 @@ fn name_before(bytes: &[u8], off: usize) -> String {
mod tests {
use super::*;
/// The `opt ` block wins over a printable tail.
///
/// This is the `pteff05.t32` / `8AX` case, byte for byte: the name is stated
/// with an explicit length, then a NUL, then three payload bytes that happen
/// to spell `8AX` in ASCII. The old backwards printable-run scan returned
/// `8AX` here, which is what dropped the background from every menu screen.
#[test]
fn opt_block_beats_a_printable_tail() {
let mut b = RATC_MAGIC.to_vec();
b.extend_from_slice(&[0u8; 28]);
b.extend_from_slice(b"opt ");
b.extend_from_slice(&11u32.to_be_bytes()); // len("pteff05.t32")
b.extend_from_slice(b"pteff05.t32\0");
b.extend_from_slice(b"8AX"); // payload, printable by accident
let off = b.len();
b.extend_from_slice(b"T8aD");
b.extend_from_slice(&[0u8; 16]);
let kids = parse(&b).expect("parse");
assert_eq!(kids.len(), 1);
assert_eq!(kids[0].name, "pteff05.t32");
assert_eq!(kids[0].offset, off);
// And the heuristic on its own really would have said `8AX` -- so this
// test fails for the right reason if the preference is ever reversed.
assert_eq!(name_before(&b, off), "8AX");
}
/// No `opt ` block: 60 of the disc's 18 002 children are like this, and they
/// must keep working off the scan.
#[test]
fn falls_back_to_the_scan_without_an_opt_block() {
let mut b = RATC_MAGIC.to_vec();
b.extend_from_slice(&[0u8; 28]);
b.extend_from_slice(b"plain.t32");
b.extend_from_slice(&[0x0e, 0x10, 0xa4]);
let off = b.len();
b.extend_from_slice(b"T8aD");
b.extend_from_slice(&[0u8; 16]);
let kids = parse(&b).expect("parse");
assert_eq!(kids[0].name, "plain.t32");
}
#[test]
fn lists_named_children() {
let mut b = RATC_MAGIC.to_vec();

View File

@@ -535,6 +535,28 @@ pub fn to_xma_riff_best(slb: &[u8]) -> Option<Vec<u8>> {
(!data.is_empty()).then(|| build_riff(&synth_xma1_fmt(2, 2, 48000), data))
}
/// Wrap a run of **raw XMA1 packets** as a standalone, decodable `RIFF/WAVE`.
///
/// For a bank with no internal delimiters — `Static.slb` is a packed run of whole
/// 2048-byte packets with no `RIFF`, no `seek` and no `WAVE` — a wave is defined
/// *only* by `(offset, packet count)`, both of which come from the running game
/// (`--xma_param_probe`). There is nothing in the file to parse, so the header
/// has to be synthesized, and that is the step worth doing exactly once, here,
/// rather than in each consumer.
///
/// `packets` must be a whole number of [`XMA1_PACKET`] bytes; anything else is a
/// short read and produces a stream the decoder will run off the end of.
/// The `channel_mask` follows the same convention as the rest of this module:
/// `1` for mono, `2` for stereo.
///
/// The three menu cues in `docs/re/menu-audio-cues.md` are
/// `(0x1ec0, 4)` d-pad move, `(0x0ec0, 2)` Ⓑ back and `(0x5d6c0, 6)` Ⓐ confirm,
/// all mono 48 kHz.
pub fn xma1_wave_riff(packets: &[u8], channels: u8, rate: u32) -> Vec<u8> {
let mask = if channels == 1 { 1 } else { 2 };
build_riff(&synth_xma1_fmt(channels, mask, rate), packets)
}
/// A minimal `fmt ` chunk carrying an XMA1 `XMAWAVEFORMAT` (one stream).
fn synth_xma1_fmt(channels: u8, channel_mask: u16, rate: u32) -> Vec<u8> {
let mut fmt = Vec::with_capacity(40);

View File

@@ -38,6 +38,18 @@ pub struct T8adImage {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
/// The header word at `+0x04`. A flag word; **bit `0x02`** is a candidate
/// blend selector — it separates the title's effect sprites from its ordinary
/// ones exactly, and disc-wide it toggles independently of the rest of the
/// word in 27.1 % of 19 216 sprites. See
/// `docs/re/structures/ui-paint-order-key.md`.
///
/// ⚠️ **Additive was tested and REFUTED.** Blending bit-`0x02` sprites
/// additively moved every measure against the title capture the wrong way:
/// whole-frame mean diff +0.55 → +1.04, swoosh-band mean +1.83 → +3.98, band
/// edge-correlation 0.6971 → 0.5578. The bit is real and independent, but it
/// does not select an additive blend. Carried, not acted on.
pub flags: u32,
}
/// Whether `bytes` starts with the T8aD magic.
@@ -108,6 +120,7 @@ pub fn parse(bytes: &[u8]) -> Option<T8adImage> {
}
}
Some(T8adImage {
flags: be32(bytes, 4),
width: width as u32,
height: height as u32,
rgba,

View File

@@ -59,9 +59,9 @@ const DESIGN_H: u32 = 720;
///
/// ```text
/// +0 u32 ARGB fade colour — alpha ramps 0x00 → 0x80 → 0xd5 … over the group
/// +4 u32 0
/// +8 u32 0
/// +12 u32 0
/// +4 i32 ⚠️ NOT always 0 — see below
/// +8 i32 ⚠️ NOT always 0 — see below
/// +12 i32 ⚠️ NOT always 0 — see below
/// +16 u32 scale X, percent
/// +20 u32 scale Y, percent
/// +24 u32 tint (0xffff_ffff on every frame seen)
@@ -69,10 +69,39 @@ const DESIGN_H: u32 = 720;
/// +32 i32 Y ← signed
/// +36 u32 time
/// ```
///
/// ## `+12` is the screen-plane ROTATION, in degrees (2026-08-28)
///
/// ✅ **Measured against the framebuffer, not against our own renderer.** The
/// title's two light sweeps are the nested leaf records `ptloop01.rat` /
/// `ptloop02.rat`, and their keyframe blocks read `+12` = `30` and `-45`. A
/// `log_ui_draws` capture of the live title submits those two quads rotated by
/// **+30.26°** and **-45.28°** — magnitude *and* sign, on two different values.
/// Positive is clockwise in screen space (Y down).
///
/// `+4` and `+8` are 🟡 still unexplained: signed, non-zero in ~4.7 % / 4.6 %
/// of blocks disc-wide, dominated by `±180` and `±90`. Plausibly rotation about
/// the other two axes, but nothing observed turns on them.
///
/// ⚠️ **`rotation_deg` is decoded but NOT rendered.** [`crate::ui_layout`]'s
/// blitter draws axis-aligned quads only, so `screen render` still paints a
/// rotated element upright. See `docs/re/ui-title-build-map.md`.
///
/// ⚠️ The earlier note here — *"every element of `GP_TITLE` build 4 has all
/// three at zero"* — was **wrong about reach, not about the bytes**: build 4's
/// top-level elements do read zero, but the rotated quads come from its two
/// **nested** `.rat` leaf records, which the census never opened.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Keyframe {
/// The fade colour, ARGB. Its alpha is what ramps an element in.
pub fade: u32,
/// Screen-plane rotation in **degrees**, clockwise-positive (`+12`).
/// Confirmed against a GPU capture; see the type's docs. Not rendered.
pub rotation_deg: i32,
/// `+4` / `+8` — signed, meaning unexplained. Carried rather than dropped
/// so a consumer can see them instead of assuming they are zero.
pub unknown_4: i32,
pub unknown_8: i32,
/// Scale in percent (100 = 1:1).
pub scale_x: u32,
pub scale_y: u32,
@@ -154,6 +183,15 @@ impl Element {
/// Falls back to the longest-dwell rule when no two adjacent keyframes
/// agree — a group that ramps through every frame and never holds.
pub fn rest(&self) -> Option<&Keyframe> {
// `lastall`: the LAST keyframe for every element, bypassing the plateau
// rule entirely. This is what the shifted time reading predicts — under
// it the final pose is reached at a definite time and nothing follows,
// so "rest" needs no heuristic. Testing it against the captures is an
// independent check on that reading, from static composites rather than
// from animation timing.
if std::env::var("SYLPHEED_REST_RULE").as_deref() == Ok("lastall") {
return self.keyframes.last();
}
if let Some(k) = self.rest_plateau() {
return Some(k);
}
@@ -161,6 +199,26 @@ impl Element {
0 => None,
1 => self.keyframes.first(),
n => {
// ⚠️ EXPERIMENT GATE, default off. Both published alternatives to
// the longest-dwell fallback died by argument rather than by
// measurement, and `compose` can score a rule against the live
// captures — so they are reachable here to be tested.
// SYLPHEED_REST_RULE=last -> the final keyframe
// SYLPHEED_REST_RULE=maxalpha -> the most opaque keyframe
match std::env::var("SYLPHEED_REST_RULE").as_deref() {
Ok("last") => return self.keyframes.last(),
Ok("maxalpha") => {
let mut best = (0usize, 0u32);
for (k, f) in self.keyframes.iter().enumerate() {
let a = (f.fade >> 24) & 0xff;
if a >= best.1 {
best = (k, a);
}
}
return self.keyframes.get(best.0);
}
_ => {}
}
let mut best = (0usize, 0u32);
for k in 0..n - 1 {
let (Some(t0), Some(t1)) =
@@ -227,7 +285,24 @@ impl Element {
any = Some(i);
any_len = len;
}
if j != n - 1 && len >= best_len {
// A trailing run is normally the EXIT and is excluded (see the
// doc comment) — but not always, and the tell is its ALPHA.
//
// An exit fades the element out, so its final keyframe is
// transparent: `pgptitle.rat`'s trailing run is `0x00ffffff`, and
// taking it erases the word PAUSE. An element with **no exit**
// ends on its hold, which is whatever it looks like on screen —
// opaque: `ptframe1`/`ptframe2` on the title main menu end on a
// run of three at `0xffffffff`, and excluding it dropped the
// bright circuit bracket the capture plainly shows.
//
// So a trailing run is the hold exactly when it is **visible**.
// (The port agent's report proposed "the final untimed keyframe
// has the same pose as the last timed one"; that is true of
// `pgptitle` too and would erase PAUSE, so it is the alpha and
// not the pose-equality that separates the two.)
let trailing_is_the_hold = j == n - 1 && (self.keyframes[i].fade >> 24) != 0;
if (j != n - 1 || trailing_is_the_hold) && len >= best_len {
best = Some(i);
best_len = len;
}
@@ -247,6 +322,20 @@ pub struct UiBuild {
pub elements: Vec<Element>,
/// Sprite name → (offset, size) of its `T8aD` child within the bundle.
pub sprites: HashMap<String, (usize, usize)>,
/// Record name → (offset, size) of its nested `.rat` **leaf** within the
/// bundle, e.g. `ptbtn01f.rat`.
///
/// Exposed because a leaf is where a focused button's extra elements live —
/// `ptbtn0Nf.rat` declares the focus ring `ptbtneff01.t32` **and** the bright
/// label, and the parent bundle declares no element for the `f` record at
/// all. A consumer that walks only top-level elements cannot see either.
///
/// A leaf is itself readable by [`parse_build`]: its first 32 bytes have the
/// same shape as a bundle header, so
/// `parse_build(&bundle[off..off + size])` returns its elements with
/// absolute design-space placements. See
/// `docs/re/structures/ui-button-focus-record.md`.
pub records: HashMap<String, (usize, usize)>,
/// A guessed context from the sprite naming (e.g. `"tutorial"`), if any.
pub context_hint: Option<String>,
/// True when the declaration table was unusable and the build was recovered
@@ -404,6 +493,8 @@ fn mark_focused_states(elements: &mut [Element]) {
/// Read the placement region that follows the declaration table, filling in each
/// element's keyframe group.
fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
// Experiment gate, default off; see the `time` field below.
let shift_times = std::env::var("SYLPHEED_KF_TIME_SHIFT").as_deref() == Ok("1");
let count = elements.len();
let mut order = Vec::with_capacity(count);
let mut pos = DECL_TABLE_AT + count * DECL_ENTRY;
@@ -430,13 +521,25 @@ fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
}
group.push(Keyframe {
fade: be32(bundle, blk),
rotation_deg: be32(bundle, blk + 12) as i32,
unknown_4: be32(bundle, blk + 4) as i32,
unknown_8: be32(bundle, blk + 8) as i32,
scale_x: be32(bundle, blk + 16),
scale_y: be32(bundle, blk + 20),
tint: be32(bundle, blk + 24),
x: be32(bundle, blk + 28) as i32,
y: be32(bundle, blk + 32) as i32,
// Only a block wholly inside the group carries a time.
time: (blk + 40 <= group_end).then(|| be32(bundle, blk + 36)),
//
// ⚠️ Which block a time word BELONGS TO is under test — see
// `docs/re/ui-keyframe-time-unit.md`. Set `SYLPHEED_KF_TIME_SHIFT=1`
// to read `W[k-1]` as block `k`'s time ("the word is the time the
// NEXT pose is reached") instead of `W[k]`. Default is unchanged.
time: if shift_times {
(k >= 1).then(|| be32(bundle, blk - KEYFRAME + 36))
} else {
(blk + 40 <= group_end).then(|| be32(bundle, blk + 36))
},
});
}
elements[idx].keyframes = group;
@@ -510,6 +613,7 @@ pub fn parse_build(bundle: &[u8]) -> Option<UiBuild> {
design_h,
elements,
sprites,
records,
context_hint,
from_fallback,
placement_order,
@@ -539,6 +643,9 @@ fn fallback_elements(bundle: &[u8], records: &HashMap<String, (usize, usize)>) -
pivot_y: be32(rec, 0x54),
keyframes: vec![Keyframe {
fade: 0xffff_ffff,
rotation_deg: 0,
unknown_4: 0,
unknown_8: 0,
scale_x: 100,
scale_y: 100,
tint,
@@ -811,8 +918,16 @@ pub fn compose(
// Measured paint order when one exists for this build, declaration order
// otherwise — see `measured_paint_order`.
// Measured order when this build is one of the two read off the running
// game; otherwise the order DERIVED from the sprites' layer keys, which
// reproduces both measured orders up to ties.
// game; otherwise the order DERIVED from the sprites' layer keys.
//
// ✅ Checked 2026-08-29 (`examples/paint_order_audit.rs`), because the
// previous wording here — "reproduces both measured orders up to ties" —
// was unmeasured and stale by one: there are THREE measured orders. The
// derived order reproduces the main menu and the developer splash EXACTLY
// (0 inverted pairs each) and differs on the title by 8 pairs, all of them
// same-layer-key ties, two being total occlusions. Of the port's five
// screens only `EXTRAS` rests on a derived order with ties: 15 tied pairs,
// 2 overlapping. See docs/re/structures/ui-paint-order-derived-check.md.
let order: Vec<usize> =
measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle));
for &ei in &order {
@@ -867,6 +982,13 @@ pub fn compose(
continue;
}
let Some(sprite) = el.sprite.as_ref() else {
// Report it, do not just skip it. This arm used to `continue`
// silently while the two arms below recorded into `missing`, so when
// a name-decoding defect left every menu screen's background
// unresolved, `screen render` still said "sprites that did not
// resolve: none". A diagnostic with a hole in it is worse than none.
// See docs/re/structures/ratc-child-names.md.
missing.push(format!("{} (element declares no resolvable sprite)", el.name));
continue;
};
let Some(&(off, size)) = build.sprites.get(sprite) else {
@@ -919,8 +1041,11 @@ fn fill_quad(
if sw == 0 || sh == 0 {
return false;
}
let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };
let sy_pct = if kf.scale_y == 0 { 100 } else { kf.scale_y };
// `scale = 0` is COLLAPSED TO NOTHING, not "unset" — see `blit`.
let (sx_pct, sy_pct) = (kf.scale_x, kf.scale_y);
if sx_pct == 0 || sy_pct == 0 {
return false;
}
let dw = (sw * sx_pct / 100).max(1);
let dh = (sh * sy_pct / 100).max(1);
let ox = kf.x - (pivot_x as i32 * (sx_pct as i32 - 100)) / 100;
@@ -977,8 +1102,16 @@ fn blit(
if sw == 0 || sh == 0 {
return;
}
let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };
let sy_pct = if kf.scale_y == 0 { 100 } else { kf.scale_y };
// `scale = 0` means COLLAPSED TO NOTHING, not "unset". This used to coerce
// 0 → 100 %, which drew a fully-collapsed element at full size. The disc
// settles it: of 15 493 elements with a keyframe group, **2 166 have at
// least one zero-scale keyframe and not one has zero on every keyframe** —
// and 1 762 of them grow back out of it (`ptlogo_eff3.t32` runs 0 % → 200 %).
// An "unset" marker that no element ever uses throughout is not a marker.
let (sx_pct, sy_pct) = (kf.scale_x, kf.scale_y);
if sx_pct == 0 || sy_pct == 0 {
return;
}
let dw = (sw * sx_pct / 100).max(1);
let dh = (sh * sy_pct / 100).max(1);
// Keep the pivot point fixed as the element scales.
@@ -1032,6 +1165,10 @@ fn blit(
continue;
}
let di = ((ty as u32 * cw + tx as u32) * 4) as usize;
// Straight alpha-over. `T8aD +0x04` bit 0x02 was tested as an
// ADDITIVE selector and REFUTED — it moved every metric against the
// title capture the wrong way (see the doc comment on
// `T8adImage::flags`), so the bit is carried but not acted on.
for (k, sc) in [sr, sg, sb].into_iter().enumerate() {
let dc = canvas[di + k] as u32;
canvas[di + k] = ((sc * sa + dc * (255 - sa)) / 255) as u8;
@@ -1084,6 +1221,9 @@ mod tests {
fn kf(x: i32, y: i32, time: u32) -> Keyframe {
Keyframe {
fade: 0xffff_ffff,
rotation_deg: 0,
unknown_4: 0,
unknown_8: 0,
scale_x: 100,
scale_y: 100,
tint: 0xffff_ffff,
@@ -1197,12 +1337,16 @@ mod tests {
// leaves the top-left quadrant bare. See
// `docs/re/structures/ui-rat-layout.md`.
let img = t8ad::T8adImage {
flags: 0,
width: 640,
height: 360,
rgba: vec![255u8; 640 * 360 * 4],
};
let k = Keyframe {
fade: 0xffff_ffff,
rotation_deg: 0,
unknown_4: 0,
unknown_8: 0,
scale_x: 200,
scale_y: 200,
tint: 0xffff_ffff,
@@ -1230,6 +1374,7 @@ mod tests {
// `ptcopyright.t32` is 694x20 at (293,655), and the capture's glyph run
// starts at x = 295 — inside that rect, not offset by a pivot.
let img = t8ad::T8adImage {
flags: 0,
width: 694,
height: 20,
rgba: vec![255u8; 694 * 20 * 4],

View File

@@ -16,7 +16,12 @@
# `sylph-agent`). They are never printed, never logged, and never passed on a
# command line.
#
# push-work push the current branch
# push-work push the current branch, and any annotated tags on it
#
# --follow-tags publishes ANNOTATED tags reachable from the pushed commits. That
# is what makes a pinned decoder state durable: the port depends on commits of
# ours by revision, and a commit reachable only from a topic branch is orphaned
# by a squash-merge. Lightweight tags are deliberately not pushed.
# push-work --dry-run say what it would do
set -euo pipefail
@@ -50,7 +55,17 @@ if [ ! -s "$HOME/.git-credentials" ]; then
fi
# `store` reads the file we mounted; nothing is written back (it is read-only).
git config --local credential.helper "store --file=$HOME/.git-credentials"
# Applied to THIS COMMAND ONLY, via `-c`, never `git config --local`.
#
# Writing it to --local config persists it in the repository, and this repo is a
# bind mount the host also uses -- so the host's git inherited
# `store --file=/sylph-home/re/.git-credentials`, a path that exists only inside
# the container, and every host push then failed with
# `unable to get credential storage lock: No such file or directory`.
#
# A tool that configures a shared repository to suit itself breaks every other
# user of that repository. Keep it to the invocation.
CRED_HELPER="store --file=$HOME/.git-credentials"
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD)
echo "push-work: $branch — $ahead commit(s) to publish"
@@ -63,5 +78,5 @@ fi
# --force-with-lease is deliberately NOT offered. If this is rejected as
# non-fast-forward, someone else moved the branch: fetch and merge, do not
# overwrite.
git push --set-upstream origin "$branch"
git -c "credential.helper=$CRED_HELPER" push --follow-tags --set-upstream origin "$branch"
echo "push-work: pushed $branch"

View File

@@ -120,3 +120,13 @@ turn ends — but sequential tool calls within a turn are fine.
The loop runs on a fixed interval set by the harness, so you do **not** need to
arm the next wakeup yourself. Spend that attention on the write-up instead.
**And never stop it.** Do not call `ScheduleWakeup` at all — not to re-pace the
loop, not to tidy up, and above all not with `stop`. Ending the loop ends the
run: the container exits, and the next iteration never happens. If the cadence
is genuinely wrong, say so in your reply and leave it to a human — the interval
is set outside this prompt and is not yours to optimise.
This is not hypothetical. A run ended at 2026-08-29 04:0x with a clean exit 0
while the display title read "Loop interval optimization", leaving four files of
work uncommitted in the tree.

View File

@@ -27,26 +27,488 @@ authored version can be deleted.
| | Question | State | Answer / link |
|---|---|---|---|
| Q1 | keyframe time unit + ramp shape | ❔ open | blocks all animation |
| Q2 | which build is which screen state | 🟡 partial | build 4 title, 5 main menu, 6/8/9 submenus, `palogo` splash — unconfirmed against captures |
| Q3 | paint order for the six screens | ❔ open | runtime-solved only; declaration table is refuted |
| Q4 | button → GamePart | ❔ open | labels are baked into sprites |
| Q5 | navigation semantics | ❔ open | |
| Q6 | boot sequence + what drives it | 🟡 partial | order observed; the driver is not decoded |
| Q7 | transitions | ❔ open | |
| Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not |
| Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified |
| Q10 | music-bank sub-wave roles (intro+loop?) | ❔ open | we concatenate blindly today |
| S1 | Ready Room go/no-go | ❔ open | probe not run |
| Q1 | keyframe time unit + ramp shape | ✅ answered, 🟡 one gap | ramp is **linear**; **2 units per rendered frame**; **`1 unit = 1/60 s` — settled**, the idle title presents at 28.5 fps so the game is 30 Hz. 🟡 **The interpolation law is settled; the group TIMELINE for multi-keyframe elements is not**`palogo_gamearts` is still at full alpha 9 frames after its declared `a=32`, and its declared 80-frame fade-in never draws — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md). ✅ **REPLICATED 2026-08-29 — for ANIMATION, read `+36` as the time the NEXT pose is reached.** Three elements across two screens: `palogo_gamearts` and `palogo_seta` hold full alpha for **83 frames** and `palogo_sqex` for **≥77**, where the current reading predicts **68** and the shifted one **80102**. The elements that cannot discriminate (the `_eff` glows, on which the linear law was measured) fit both. ⚠️ Our decoder still defaults to the other reading (`SYLPHEED_KF_TIME_SHIFT=1` to flip) because it changes `rest()` on one element — but that is an unsound fallback guessing either way, so **static rendering is unaffected and animation timing should use the shift** |
| Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, 0/1 and 10/11 two unidentified `DELTASABER` plates — [`ui-title-build-map.md`](../re/ui-title-build-map.md) |
| Q3 | paint order for the six screens | ✅ answered, ❔ tie-break | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md). ⚠️ **The key does not fully order a screen**: elements sharing a key are tied, and the tie-break is ❔ **undecodable from the bundle** — declaration table, `T8aD` header (exhaustive: every offset 0x000x7f at u8/u16/u32, both directions, **0** fields match the measured order against **64** for the control) and the RATC child order all give the same order the game does *not* use. Your exposure is **2 overlapping tied pairs on `EXTRAS`** — [`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md) |
| Q4 | button → GamePart | ✅ answered | **measured** which screen all **5** buttons open — `NEW GAME``DIFFICULTY``SELECT DATA`, not a hang. The **GamePart id is still a name match**, not a measurement — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
| Q5 | navigation semantics | ✅ answered | **measured**: initial focus varies boot to boot (2× `TUTORIAL`, 2× `NEW GAME`); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
| Q6 | boot sequence + what drives it | ✅ answered | sequence **measured** end to end; the driver is **code, not data** — four search spaces closed, so the port **authors** the sequence — [`boot-config-and-gamepart-registry.md`](../re/boot-config-and-gamepart-registry.md) |
| Q7 | transitions | ✅ answered | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. Fade-in ramp is **decoded** from its keyframes; the ~0.4 s fade-out is **measured** (not in the file) — [`screen-transitions.md`](../re/screen-transitions.md) |
| Q8 | menu audio bindings | ✅ answered | cue vocabulary + bank **decoded**; event binding is a **name match** (the authors' own event names). ✅ **You CAN have the SE audio** — ⚠️ an earlier version of this row said it was "undecodable from the disc"; that was **retracted** and the row was stale. Three cues are located in `Static.slb` and **decode to PCM**: d-pad move `0x1ec0` (4 packets), Ⓑ back `0x0ec0` (2), Ⓐ confirm `0x5d6c0` (6), all mono 48 kHz. The bank is a packed run of XMA waves with no delimiter, so a wave is only (offset, packet count) — and ⚠️ the file order is **not** cue-id order, so the index cannot be counted out — [`menu-audio-cues.md`](../re/menu-audio-cues.md) |
| Q9 | video binding + playback rules | ✅ answered | **decoded** from the movie manifest: `ADVERTISE_MOVIE``ADV.wmv` (boot intro *and* attract are one asset), `MS00A``S00A.wmv` is the new-game intro, `STAFF_ROLL`→the credits reel. ✅ **one Ⓐ skips a movie** (title at 57 s vs a 193 s baseline) — [`movie-binding.md`](../re/movie-binding.md) |
| Q10 | music-bank sub-wave roles (intro+loop?) | ✅ answered | **two stems of one performance, played together** — sample-synchronous, equal duration, 32/32 banks. **Concatenating is wrong.** Not a seamless loop either — [`structures/bgm-two-stems.md`](../re/structures/bgm-two-stems.md) |
| S1 | Ready Room go/no-go | **no-go** | it is 2D and enumerates fine (60 builds), but `GP_READY_ROOM.pak` holds **briefing/tactical-map** content, not the six-button Ready Room menu — [`ready-room-probe.md`](../re/ready-room-probe.md) |
## Already settled — the port can rely on these today
* **`GP_TITLE.pak` is the whole title-side tree.** Build 4 is the title with the
animating wordmarks, build 5 the five-button main menu, builds 6/8/9 are
submenus, and the developer splash is the `palogo` bundle in the same archive.
✅ decoded (enumeration), 🟡 the state labels are not yet capture-confirmed.
### ⬅ Answers to the port's five asks (2026-08-29)
* **1 — how to recognise the splash.** ❔ **No content rule exists; you are
authoring this.** Design size fails (every extra composable bundle sampled is
1280×720, same as every screen) and element count fails (the fragments run
2…15 elements, the splash halves have 3 and 7 — the ranges overlap).
**But `GP_TITLE` needs no rule.** There, `--all` adds exactly **four**
bundles and all four are real screens — no fragments at all — and the `--all`
index equals the pak **entry** index 1:1 across all 16, so addressing by entry
index does not mean something different from elsewhere.
🔴 **And there are TWO splash screens; you have one.** Entries **11/14** are the
developer logos (GAME ARTS / SETA / studio anima). Entries **10/13** are the
**SQUARE ENIX publisher** wordmark — the *first* thing the boot shows — and you
do not have them. The pairs are region twins (`™` on 10, `®` on 13). All four
draw every element they declare.
[`ui-splash-addressing.md`](../re/ui-splash-addressing.md) ·
[render grid](../re/captures/title-builds/splash-both-halves-rendered.png)
* **2 — the ~0.4 s fade-out is (a)**, and it is bigger than the fade quad.
Every element of a screen ends on **exactly one** untimed keyframe — so there
is one unknown duration per screen, which rules out (b). That final block is
where the screen plays out: the quad goes `a=255` (black) while the buttons,
`ptmsg` and the glows go `a=0` and the two frames hold. (c) is refuted by a
null test on the capture: a black quad alone keeps the button÷background
brightness **ratio constant**, and measured through the fade it falls
**6.50 → 1.94, a 3.4× monotonic drop**. So: write one authored constant
(~0.4 s / ~24 units) and **play the group to its end on every element** — do
not fade a black rectangle over a frozen screen.
[`screen-transitions.md`](../re/screen-transitions.md)
* **3 — focus: your choice is fine, and it is not your bug.** ✅ The focused
sprite **completely covers** the base — `f` alpha ≥ base alpha at **100.0 %** of
base-visible pixels on three pairs across both languages, once aligned properly
(the true offset is **(7,7)**, and at the centre alignment it reads a
misleading 7884 %). Compositing both ways differs by **RMSE 1.1 inside the
button rectangle**, max 12/255 on ~25 px — unmeasurable at frame level.
🔴 **What you are actually missing is the focus record's SECOND element.**
`ptbtn0Nf.rat` declares **two** sprites — `ptbtneff01.t32` (a 42×46 **glowing
ring**, focus only) then `ptbtn0Nf.t32` (the bright label) — where the base
record declares one. That ring is the marker you say you draw nowhere. ⚠️ Note
the small dot-in-circle at each underline's left end is *not* it: that is on
every button all the time, part of the base art.
[`structures/ui-button-focus-record.md`](../re/structures/ui-button-focus-record.md)
* **4 — rotation: not mine to decide alone.** Raised with the human; see
MISSION. What I can say without a decision: the two sub-questions are not
equally open. Rotation is about the **declared pivot** — that anchor is
*measured*, not assumed: the title's two `ptloop` sweeps scale 600 %/800 %
vertically, where the pivot term is worth 450 and 630 px, and the GPU capture
puts both quad centres at y **359.1**/**360.0** against the pivot formula's
**360.0**; top-left anchoring predicts 810/990 and centre-as-position 270. So
if you draw rotation, rotate about the declared pivot.
⚠️ It changes nothing on your five screens **at rest** — they have zero
top-level rotations, and the title's two nested ones sit entirely off-screen at
rest. [`structures/ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
* **5 — the capture is not gamma-neutral, and RMSE against it has a floor.**
Measured on flat patches (16×16, both images `std < 8`):
`capture ≈ 255·(render/255)^γ` with **γ ≈ 1.49** (main menu), **1.49**
(`EXTRAS`), **1.34** (title). The chain says this is a ramp **the game
installed**, not a capture-path artefact: canary's swap-path gamma stage is a
pure 256-entry LUT that defaults to identity, and the game is measured calling
`VdGetCurrentDisplayGamma` once at video init. ⚠️ **Reach: the flat patches are
almost all dark (render ~060), so nothing here constrains midtones or
highlights** — which is where γ 1.4 does its visible work. So: yes, there is a
floor; a γ ≈ 1.4 darkening gets closer and is **authored**, best applied where
it was measured rather than extrapolated. Do not chase RMSE below it.
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
* **`GP_TITLE.pak` is eight screens, each shipped twice — English and Japanese.**
Build 4 is the English title art and 7 its Japanese twin; **2/3 are the
`PRESS Ⓐ BUTTON` plate, a build of their own** composited over the title and
faded in a beat later; 5/8 are the five-button main menu; 6/9 are the `EXTRAS`
submenu — the **only** submenu inside this archive. Builds 0/1 and 10/11 are a
`DELTASABER / SYLPHEED A.I.` plate that was **never seen running**, in the boot
path, any title-side screen, or the attract loop. ✅ measured against live
captures for the four English screens the boot path shows;
[`ui-title-build-map.md`](../re/ui-title-build-map.md).
**Withdrawn:** the earlier "builds 6/8/9 are submenus" — 8 is the Japanese main
menu. The other four main-menu buttons leave the archive, and where each one
goes is **measured** — see the button-destination bullet below.
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
decoration, `0x10` = primitive. ✅ decoded.
decoration, `0x10` = primitive. ✅ decoded **for the title-side screens**.
⚠️ `0x3002` is one member of a `0x3000` family with sub-bits, and it is not the
only button kind on the disc: `GP_READY_ROOM`'s 902 bundles contain **zero**
`0x3002` and use `0x3000` / `0x3004` / `0x300c` / `0x3008` instead. Nothing in
this milestone changes — every screen in scope is `GP_TITLE` — but do not ship
`kind == 0x3002` as a general button test.
(The kind is the 4th `u32` of the 60-byte declaration entry, at `+40`.)
* **The title's settled pose is `rest` — and always pass `--primitives`.**
Against a plate-free capture of the real screen, `screen render --build 4
--black` edge-correlates **0.9163 at (0,0)**, so the geometry of `rest` is the
arrived pose; the timeline is not needed for the title.
⚠️ **But without `--primitives` the whole frame is +13.14 too bright** (R +12.35,
G +13.58, B +13.48); with them, **+0.55**. The missing element is
**`pteff02.prm`**, the 25 % dim, and `--primitives` is off by default. **That is
the "washed-out cyan slab"** — a dim that should be there and isn't, not a glow
that shouldn't. Because the art is blue-dominant, the shortfall reads cyan.
🔴 **A second, separate defect — and it is the "slab".** With the dim in place
the residual is localised to one band (y ≈ 112225): the game draws the logo's
`Z` **swoosh thin with a pink/magenta edge**, our render draws it **thick and
solid white**. Crop:
[`title-swoosh-capture-vs-render.png`](../re/captures/title-builds/title-swoosh-capture-vs-render.png).
The elements are `ptlogo_back2.t32`, `ptlogo_back2eff.t32` and
`ptlogo_back2eff1…5`. 🟡 Those five are also the group with the **known
unsolved paint-order tie-break** (key `0x8083`) — same screen, same elements —
but a blend-order swap explains white-instead-of-pink poorly.
🔴 **The pivot mismatch is NOT the cause — checked and refuted.** These elements'
declared pivots really do belong to the *other language's* sprite
(`ptlogo_back2`'s `(500,117)` is exactly half the Japanese 1000×234, not its own
English 1118×262; 24 of 109 title elements are off by > 8 px). **But it cannot
affect this render**: `blit` sizes a sprite from its **texture**, and applies the
pivot only as `kf.x pivot·(scale100)/100` — and **all seven swoosh elements
are scale `(100,100)` at every keyframe**, so the term is zero.
**That formula is now MEASURED, not just implemented (2026-08-28).** The
title's two `ptloop` sweeps scale **600 %** and **800 %** vertically, where the
pivot term is worth 450 and 630 px, and the GPU capture puts both quad centres
at y **359.1** and **360.0** — against the formula's **360.0** for both.
Top-left anchoring predicts 810 and 990; treating the position as the centre
predicts 270. Horizontally the same term makes the group's `t` solved from
**position** agree with `t` solved from **vertex alpha** to **0.33 / 0.65
units**, versus ~8 units without it. So: **anchor scaling on the pivot, not on
the top-left corner.**
🟡 It does *not* prove interpolation is linear — both fields were inverted
through the same linear map, so a shared easing curve would cancel. It does
show position and alpha ride **one shared parameter**.
⚠️ It *would* bite `ptlogo1`/`ptlogo2`, which scale 100 → 150 during the
build-in. Not at rest, and not on the swoosh.
🔴 **Ruled out for the COLOUR**: every swoosh keyframe's fade is `0x??ffffff`
(white RGB, alpha only), no tint is non-white, and the texture decodes
*blue*-leaning (175,174,198).
🟡 **What is left is the BLEND.** Size and position are the texture's own and
match; fade, tint and texture colour are all ruled out. Seven overlapping
mostly-transparent sprites (`ptlogo_back2` 5.4 % opaque, its glow 10.3 %, the
five `eff` segments 1023 %, all white or warm) stacked with plain alpha-over
saturate to opaque white — which is exactly what we draw, and would read as
"thicker" against the game's thin coloured stroke.
🟡 **And there is a candidate field for it.** The `T8aD` header word at
**`+0x04`** splits the title's sprites exactly along effect-vs-normal:
`pteff01`, `pteff03a`, `ptlogo_back2eff1…5`, `ptlogoall_eff`/`_eff2` are
**`0x8832`**; `ptlogo1`/`2`, `ptlogo_tm`, `ptbase2`, `ptlogo_back2`,
`ptcopyright` and `ptlogo_back2eff` are **`0x8830`**. One bit — **`0x02`**.
Disc-wide it is a real independent flag: 19 216 sprites, 18 distinct values,
bit `0x02` set in **27.1 %**, and it toggles against otherwise-identical words
(`0x8830`/`0x8832`, `0x0830`/`0x0832`, `0x0810`/`0x0812`, `0x0030`/`0x0032`).
⚠️ **Correlation only — untested.** Nothing yet shows it *means* additive; the
test is to blend bit-`0x02` sprites additively and re-correlate the title
against the capture. Note `ptlogo_back2eff` is `0x8830` despite its name, so
this is a field and not a naming pattern. ❔ Not diagnosed — but two candidate meanings are now dead: **not** "the name contains `eff`" — and not even the one-way "bit ⇒ `eff` name", which held 10/10 on the title but fails on **2 657 of 4 995** bit-set sprites disc-wide (`P(eff|set) = 0.468`). What survives is a 3.3× association, and the counterexamples are rings, glows and lights — effect-like art without the naming convention and **not** "the element is transient" (`pteff03`/`pteff03a` carry the bit and persist to `t=250`). 🛑 **Parked** — four candidate meanings are now dead (additive blend; `eff` name, both directions; transient element; **premultiplied alpha**, refuted because flagged sprites violate `RGB ≤ A` *more* than unflagged, 55.5 % vs 33.7 %) and none produced a positive account. It blocks nothing — your screens composite at 0.947 correlation against a capture without it. ⚠️ Practical note for an asset pipeline: `T8aD` headers appear in **RATC child order** (18/18 verified on build 4 against decoded dimensions), which is the only sound way to attribute a header to a name when two sprites share a size — and two here do.
⚠️ **The rotated draw is NOT the swoosh.** It was identified as the swoosh by
elimination; that is **refuted** — its quads span y 209…925 in screen space,
the swoosh is a band at y 126…360. ✅ **It is the two `ptloop` sweeps**
(`ptloop01.rat``pteff03.t32`, `ptloop02.rat``pteff03a.t32`), confirmed by
edge length: 400 × 1076 and 400 × 1444 against 399×180 at the two elements'
**different** declared scales, 600 % (= 1080) and 800 % (= 1440). What stands
from the old bullet: the game *does* submit rotated quads this compositor
cannot draw, and vertex colours are white.
**SOLVED 2026-08-28 by draw capture — it is the GEOMETRY.** The game submits
the swoosh as **two rotated parallelograms** (edges `(0.54,0.56)` and
`(0.44,0.79)`, ~45° and ~61°, extending to `y=±1.81` NDC).
`ui_layout::blit` draws **axis-aligned rectangles only**, so it blits the sprite
upright — right on average, right in position, wrong in shape, which is the
measured signature exactly. **A port that blits upright rects will have the same
defect.**
🔴 Vertex colour is refuted with it: every colour in the capture is
`<alpha>FFFFFF`, white RGB.
**CLOSED 2026-08-28 — the rotation IS on the disc, at keyframe `+12`.**
It is a signed angle in **degrees**, clockwise-positive in screen space
(Y down), and `ui_layout::Keyframe` now carries it as `rotation_deg`.
Confirmed against the framebuffer, not against our own renderer: the two
`ptloop` records declare `+12` = **30** and **45**, and the GPU capture
submits their quads at **+30.26°** and **45.28°** — magnitude and sign, on two
different values. Corroborated separately by shape: `GP_BUNK` entry `117ca14f`
holds a group whose `+12` ramps **0 → 360** with position, scale and alpha all
constant — a spin in place. Disc-wide `+12` is non-zero in **14.50 %** of
83 862 keyframe blocks.
⚠️ **Two things the port must know about it.**
(1) **Rotation lives in BOTH the top-level table and nested `.rat` leaf
records.** ⚠️ I told you one iteration ago that it looked nested-only; that was
three archives' worth of pattern and **it is refuted**`GP_DIALOG` and
`GP_DEBRIEFING_PILOTLOG` rotate top-level elements. The actionable half stands:
the *title's* rotations are nested, so a composer reading only the declaration
table gets zero rotation on exactly the elements that move there.
The clearest examples are top-level and show up in
`screen info --build 0 --geometry dat/GP_DIALOG.pak`: `pceff03.t32` and
`pceff04.t32` ramp `r=` **90 → 30 → 10 → 3 → 0** while their alpha ramps
0 → 255 and they slide into place — a swing-in that settles upright; and build
6's `pzeff02.t32` ramps **43 → 61 → 75 → 90** while scaling 112 % → 200 % and
fading to 0 — a spin-out burst.
(2) **`sylpheed-cli screen render` still does not rotate.** The field is
decoded, 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 — and per your own rule, the two of them agreeing about it means
nothing.
**Checked 2026-08-29: this does NOT affect your five screens at rest.**
Title, main menu, `EXTRAS` and both splash halves have **zero** top-level
elements with a non-zero rotation. The only rotations on any of them are the
title's two nested `ptloop` records (`r = 30` and `45`), and at rest those sit
at `x = 1521` and `x = 839` — a 399-wide sprite entirely off both edges of a
1280 screen. So a static composite is unaffected; the caveat applies only if
you animate the title's build-in, where the sweeps cross the screen rotated.
🟡 The neighbouring words `+4` and `+8` are still unexplained: signed, non-zero
in ~4.8 % / 4.6 % of blocks, almost entirely `±180`/`±90`. That distribution
looks like a flip flag rather than a free angle, but nothing observed turns on
them — **do not transcribe them as X/Y rotation.**
[`ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
⚠️ The earlier "pink versus white" reading compared two differently-shaped
renderings and should be re-checked after geometry, not carried as a separate
defect.
**Classified: undecodable from the disc, with reach.** Seven candidates
eliminated — pivot (inert at scale 100 *and* no measured displacement), `fade`,
`tint`, texture colour, additive blend via `+0x04` bit `0x02`, and
capture-not-settled (the band is identical from t = 4.0 s to t = 21.5 s). The
residual is stable and modest: band mean **+1.83**, edge-corr **0.70** vs ≈ 0.92
frame-wide. A next attempt should use a **per-draw GPU capture** of the running
guest — not another field — but ⚠️ that capture records prim/indices/shader
hashes/**texture bindings**/**vertex attributes** and **no blend state**, so it
can test a per-draw *vertex colour* today and would need a Canary change to dump
`RB_BLENDCONTROL`. ✅ And the plate-free capture is sound; use it.
🔴 **Refuted:** it is *not* that our dim covers the whole frame instead of
sitting beneath the UI — the logo reads +2.36 against a background of 0.74, so
the paint order is being honoured.
* **The title's motion, decoded and attributed.** After building in, the *title
art* is essentially static — a 22 s capture measures the wordmark region at
sd 0.06 and the bottom-right corner at sd 0.003. Two things do move:
***Two slow light sweeps in build 4.** `ptloop01.rat` (an `opt `-linked
`RATC` at `0xbb5966`) sweeps `pteff03.t32` left→right over **450 units =
7.5 s**; `ptloop02.rat` sweeps `pteff03a.t32` right→left over **570 units =
9.5 s**. Ordinary 40-byte keyframe blocks from `+0x68`, three keyframes each.
***The `PRESS Ⓐ BUTTON` plate pulses**, and it is the loudest thing on
screen — a capture's per-tile amplitude map puts **sd 7.65** in the band
x ≈ 318954, y ≈ 560672 against **0.06** on the wordmark. It is
`ptbtn00f.rat`, the plate's highlight variant (build **2**, not build 4),
whose alpha ramps **`0x00 → 0x06 → 0x4a → 0x50` (hold) `→ 0x4a → 0x06 → 0x00`**
over **eight** keyframes at `t = 6, 29, 35, 50, 58, 97, 105, ?` — a glow that
fades in and back out, **closing on fully transparent**, so it is a complete
cycle rather than a one-shot ramp.
🟡 Its cycle **length** is not readable, and this is now observed rather than
assumed: the eighth block's time slot literally contains the ASCII terminator
`end `, so the record ends there and the value does not exist. Declared span is
therefore **≥ 105 units = 1.75 s** against a measured **≈ 2.3 s** — which would
need a final step of ≈ 33 units. That number is **fitted to the measurement, not
read**; the port should take ≈ 2.3 s as measured.
⚠️ An earlier version of this bullet said "the title screen loops at ≈ 2.2 s"
and attributed it to `ptloop01/02`. Both halves were wrong: it is the **plate**,
and it is a different build.
* 🟡 **Our composite is brighter than the emulator's frame — measured, and you
would be *authoring* if you apply it.** Alignment is exact (best offset
dy=0 dx=0, correlation **0.9466**), so only the tone differs. Fitting on 16×16
patches that are flat in **both** images: `capture ≈ 255·(render/255)^γ` with
γ = **1.491** (main menu), **1.493** (`EXTRAS`), **1.338** (title).
⚠️ **Narrow reach.** Those flat patches span only render values ~060, where a
gamma and a plain scale are nearly indistinguishable — on both menus the errors
are 0.28 vs 0.34 and 0.22 vs 0.28. Only the title separates them (1.08 vs
9.02). Nothing here constrains midtones or highlights.
🔴 The held-out control **failed to discriminate**: the splash's 2 918 flat
patches are pure black (render 04), so every model scores ≈ 0.
🔴 **My "it may be the emulator" caveat is withdrawn.** I said canary applies
`kernel_display_gamma_type = 2` (BT.709) on output. It does not — that cvar is
a value a **kStub getter reports to the guest** (`VdGetCurrentDisplayGamma`),
which the game uses to build its own ramp; canary then applies **the guest's**
ramp from the `DC_LUT` registers in the swap path (`apply_gamma_table.ps` /
`apply_gamma_pwl.ps`). So there is no emulator post-process to subtract, and
any gamma in a capture is one the game installed.
**Measured 2026-08-29: the game DOES query the display gamma.** Booted with
Kernel logging on (`--log_mask=12 --log_level=3`, which changes nothing about
the output), `VdGetCurrentDisplayGamma` is called once at video init, between
`VdGetSystemCommandBuffer` and `VdSetDisplayMode` — the moment a ramp-builder
would ask. Control in the same log: 359 `VdRetrainEDRAM` lines, so an absent
call would have shown.
🟡 **That it then writes the ramp is inferred, not observed** — but the chain
is closed: canary's swap-path gamma stage is a **pure 256-entry LUT** with no
other transfer (`apply_gamma_table.xesli`), and that LUT **defaults to
identity** (`CommandProcessor::Initialize`, whose comment says the linear
default is "what games set when starting with the sRGB return value"). Identity
cannot produce the measured γ ≈ 1.4, so a non-identity ramp was written.
⚠️ The weak joint is that this assumes our composite reproduces the *pre-ramp*
framebuffer; what carries it is that a systematic ~1.4 across three screens is
not the shape of a compositor bug. A fixed sRGB stage does not fit either
direction (encode brightens; decode darkens far more).
**Practical upshot for you is unchanged:** the darkening is the game's own
display ramp, so it belongs in a port as a display profile, not baked in.
⚠️ Note the ramp depends on the display type the game is *told*; canary
hard-codes TV/BT.709, which on hardware is a console setting. **So this is a
display profile, not a fixed property of the game** — reasonable to expose as a
setting rather than bake in.
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
* ✅ **FIXED 2026-08-29 — the dropped background was a name-decoding defect, and
`screen render` now draws it.** The reference composites for all five screens
changed; regenerate anything you diffed against before that date. Cause: a
RATC child's name is stated by an **`opt ` block** immediately before it
(`"opt " | BE32 len | name | NUL | 3 bytes | magic`), and our parser instead
guessed it from the last printable run of bytes. For this one child the 3
trailing bytes are `38 41 58` = `"8AX"`, which beat the real name
`pteff05.t32`. **`8AX` was never a name** — earlier text on this page treating
it as one was wrong. Disc-wide: 18 002 children, 17 918 already agreed with the
`opt ` reading and **24 did not**, every one the same 3-byte-tail failure.
Effect on your five screens: mean brightness unmoved, high-frequency detail
**×1.15…×1.30** — the same art at twice the resolution, which is exactly what
[8AX](../re/structures/ui-8ax-fullres-background.md) said the game draws.
⚠️ Both backgrounds are now drawn (`ptbase` upscaled, then the full-res one over
it): correct, but wasted fill. Draw only the full-res one, taking its *timing*
from `ptbase`'s element, which carries the keyframes.
[`structures/ratc-child-names.md`](../re/structures/ratc-child-names.md)
**And the fix has no remaining hole.** 60 of the disc's 18 002 RATC children
still have no `opt ` block; all 60 are now accounted for and **none is on your
screens**. They are the ten frames of the disc's only `.tan` **frame sequence**
(`pb_f15_eg_anm.tan`, six language copies of one `GP_READY_ROOM` bundle), where
a single `opt ` block names the whole run — so a name-resolution miss is not
hiding anything else the way `8AX` was. ⚠️ Two notes if you ever read outside
`GP_TITLE`: `ratc::parse` **over-reports** there, listing a `.tan`'s frames as
anonymous children; and a RATC bundle names exactly six kinds of resource —
`.t32` (14 756), `.rat` (3 311), `.prm` (367), `.tbm` (224), `.sbo` (54),
`.tan` (6).
[`structures/ratc-tan-frame-sequence.md`](../re/structures/ratc-tan-frame-sequence.md)
<details><summary>the original entry, kept because its reasoning still stands</summary>
🟡 **`screen render` silently drops one full-screen element per screen — and
you must NOT simply draw it.** Auditing what the composer omits on your five
screens: everything is accounted for (`kind & 0x4` ghost instances, `.prm`
primitives, `loop*` animations) except **`pteff04.t32`** on the title and
**`pteff05.t32`** on both menus. Those are `kind 0x0`, one keyframe, rest
`a = 255`, pivot `(640,360)` — full-screen and opaque.
**Cause:** the element declares `pteff05.t32`, but the `T8aD` behind its `opt `
link is registered under the name **`8AX`**, so the sprite lookup misses and a
silent `continue` drops it.
**It does not currently show,** because `ptbase.t32` (640×360, drawn at
200 %) is *the same artwork at half resolution* — its 2× upscale differs from
`8AX` by mean 2.05, and our background is pixel-identical to `8AX` in every
patch sampled.
**SETTLED 2026-08-29 — the game draws the full-res `8AX`, so use it.**
Previously parked as "needs a per-draw capture"; it did not. The two carry the
same art at two resolutions, so what separates them is the detail `8AX` has
that an upscale cannot. Correlating the capture's departure-from-upscale
against the 8AX-only detail (both first mapped through the measured gamma):
main menu **+0.0475** vs controls +0.0032 / 0.0075, title **+0.0634** vs
+0.0095 / +0.0086 — **two screens, both 68 % of the theoretical ceiling, 715×
their matched controls**.
**So: resolve the name and draw `8AX` at 1:1.** Upscaling the 640×360 `ptbase`
2× is *wrong*, not merely softer. ⚠️ Do not draw **both** — an opaque
full-screen layer over an identical one costs fill and hides later changes; and
note `ptbase`'s element is the one carrying the keyframes, so you need its
timing with `8AX`'s pixels.
⚠️ It does not show whether `ptbase` is *also* drawn underneath — the full-res
background is
~86 % opaque and would hide it either way.
[`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md)
</details>
***Paint order: your exposure is two element pairs, on one screen.** We use
an order *measured from the running game* where one exists and a derived order
(a sort on each sprite's layer key) elsewhere. Checked, rather than assumed:
the derived order reproduces the measured one **exactly** on the main menu
(0 inverted pairs) and the developer splash (0). On the **title** it differs by
8 pairs — **all same-layer-key ties** — and two of those are total occlusions
(`back2eff5` is 1133×280 and *fully contains* `back2eff3` and `back2eff4`;
derived puts it on top, the game puts it underneath). The title is unaffected
in practice because it has a measured order.
Per screen: title **measured**, main menu **measured**, developer splash
**measured**, publisher splash derived but with **0 ties** (fully determined),
and **`EXTRAS` derived with 15 tied pairs of which only 2 overlap**.
**That narrows again to ONE, and the capture is consistent with it.** Of the
two overlapping pairs, `ptloop01`×`ptloop02` are `loop*` animations that
`compose` skips by default, so their tie is unreachable. The remaining pair is
`ptframe3`×`ptframe4`, overlapping 102×132 px — and against `live-extras.png`
that contested region correlates **+0.9622**, *better* than the whole frame
(+0.9440) and inside the range of regions where order cannot matter (+0.8502 /
+0.9903). 🟡 Consistent with, not proof — correlation cannot see a swap between
locally similar art. **15 → 2 → 1 → consistent** is the whole paint-order risk
on your five screens.
[`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md)
***How good are the five screens, actually?** One page with the numbers:
[`five-screens-acceptance.md`](../re/five-screens-acceptance.md). Rendered and
correlated against the live captures — title **0.9500**, main menu **0.9460**,
`EXTRAS` **0.9440**, publisher splash **0.9600**, developer splash **0.9643**,
and every one aligns at **exactly dy=0 dx=0** over a ±2 px search, so placement
and scale are right and the residual is tone and detail rather than geometry.
Every undrawn element is accounted for (ghost instances, `.prm` primitives,
`loop*` animations, and the `8AX` name mismatch whose art is on screen anyway) —
the counts add up exactly, with nothing unexplained. The residual, ranked: tone
(γ≈1.4, the game's own ramp), `8AX` resolution, one `EXTRAS` paint-order tie,
and rotation-not-rendered which does not affect these five at rest.
⚠️ Reach: static composites at rest against single frames — this says nothing
about animation.
* ✅ **A static composite is only meaningful for a screen that SETTLES — the two
splashes are animations.** Measured with its control: suppressing the `_eff`
glows takes the **publisher splash 0.9604 → 0.9982** and the **developer splash
0.9659 → 0.9980**, while the same edit makes the title 0.002, the main menu
**0.092** and `EXTRAS` **0.107** worse. The draw log says why — on the
developer splash the glows draw on frames 94115 and the logos on 116211, so
at the captured moment every glow is already finished, including the two with
plateaus that `rest_plateau` renders visible.
So `rest_plateau` is right where a screen settles and over-draws where it does
not. **There is no "resting pose" for the splashes** — they play through and
leave, and a static composite of them is a picture of one arbitrary frame
(≈0.998 for the frame these captures hold). **Play the timeline for the two
splashes; composite statically for title / main menu / `EXTRAS`.**
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
***A group's DURATION is in the data; its START TIME is not.** Testing whether
the splash timeline *played* reproduces the capture: each element is on screen
for its declared span to within 2 % (glows 44 units observed vs 45 declared;
logos 192 vs 195). But every glow declares the same times `15,30,45` and every
logo the same `15,30,190,194,206,210`, so on one clock they would overlap almost
entirely — and they **do not overlap at all**. The glows run frames 94115, the
logos 116211, strictly sequential.
🔴 The obvious candidate is dead: each group header carries an undecoded
**lead-in word**, and it is `0x00000000` for all seven elements. Not the
keyframe times, not that word, not declaration order, not the RATC child order.
**Here is the sequence to author, measured** (1 frame = 1/30 s):
publisher `palogo_sqex` **3.00 s+** (a floor — it is already at full alpha on
the capture's first frame), a **0.10 s** gap with nothing drawn, then the
developer **glows for 0.73 s**, then the developer **logos for 3.20 s**, the
glow→logo switch being a single frame boundary with no overlap. Each phase is
within 2 % of its element's declared span, so the *durations* come from the
bundle and only the *ordering* is authored.
⚠️ One capture, one run; and `palogo_anima`/`_eff` get 0 draws in all 214
frames, so a third pair's phase is not in this measurement.
[`data/splash-phase-timeline.txt`](../re/data/splash-phase-timeline.txt)
⚠️ **So you must author the sequencing.** The observed order on the developer
splash — both glows, then both logos — is *measured for one screen*, not a
decoded rule.
**Verified against its own refutation:** across all 235 captured frames,
**zero** contain both a glow and a logo; the switch is one clean boundary with
two sprites either side.
🔴 **And it goes further than start times — a bundle's declared elements are
not what gets drawn.** ✅ **And the mechanism is now established, by
elimination.** The competing story — "two compositions shown in sequence" —
needs a bundle declaring the glows *without* the logos, and no such bundle
exists: only four entries in `GP_TITLE` carry `palogo` elements, and each
developer entry (11, 14) declares **all six** logos and glows. So whichever
bundle was active, only a subset of its elements was drawn at a time.
(An earlier texture-base test could not separate the two — it failed its own
control, since the publisher splash is a different bundle and shares the base
`0x11C30000`, a reused upload slot rather than an identity.) Entry 11 declares *three* logo/glow pairs; only two are ever drawn.
`palogo_anima` gets **0 frames** while `palogo_gamearts` gets **95**, from
byte-identical keyframe times. (Reach: the capture covers frames 1214, so
"never in the window".) **Compositing every element of a bundle does not
reproduce what the game shows over time** — it is right for a static screen
that settles, and it is not a timeline.
[`structures/ui-group-start-time.md`](../re/structures/ui-group-start-time.md)
* ✅ **The splash's backdrop is an opaque black `.prm` from the bundle, painted
first.** Measured: every frame opens with the clear and then an **untextured
1280×720 quad, vertex colour `FF000000` in all 212 frames** — matching
`palogo_eff0.prm`'s declaration (`kind 0x10`, pivot 640×360, one keyframe,
`a=255`). That is why a splash render needs `--black` rather than the default
backdrop. ⚠️ Not a general rule: the main menu's measured order puts one `.prm`
at position 4 and the other **last** (the transition fade).
✅ **The splash's recorded paint order `[0,2,4,6,1,3,5]` is a real depth
order** — read off the runtime child array, and independently backed by the
static layer key (glows `0xa100` sort before logos `0xa110`;
`paint_order_audit` reports derived == measured, 0 ties). A same-day claim on
this page that it was "a temporal sequence, not depth" was **withdrawn**; see
the ❌ section in the linked page for why, if you read it before the fix.
⚠️ Separately and still true — and it is an **activation** fact, not a
paint-order one: the splash's glows and logos are never on screen together
(glows f94115, logos f116211,
[timeline](../re/data/splash-phase-timeline.txt)). Compositing all seven at
once reproduces no frame of the real screen. Depth says *what covers what*;
it does not say *what is up*.
[`structures/ui-prm-primitives.md`](../re/structures/ui-prm-primitives.md)
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
@@ -54,37 +516,402 @@ authored version can be deleted.
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
* **Highlighted states pair by name** — `ptbtn01.rat``ptbtn01f.rat`. 🟡 a
naming convention that holds for all 54 real pairs, not a decoded field.
* **`rest()` was wrong for elements with no exit animation — fixed 2026-08-28.**
A trailing run of identical keyframes was always treated as the exit and
excluded; on an element that has no exit it *is* the hold, and `rest()` fell
back to the element's **first** keyframe — off-position and transparent. Six
elements on the main menu were affected, including `ptframe1`/`ptframe2`, the
bright circuit bracket around the menu, which both the port's composite **and**
`sylpheed-cli screen render` were dropping.
The rule now: **a trailing run is the hold exactly when it is visible** (alpha
≠ 0). ⚠️ Not the pose-equality test the report proposed — `pgptitle.rat`'s
trailing run also matches its last timed keyframe, and adopting that would erase
the word PAUSE. Oracle correlation over the bracket region improved
**0.9596 → 0.9748**; the PAUSE control is unchanged.
**Disc-wide**: over 2 859 bundles / 13 991 elements, `rest` moves for **30**
(0.21 %) — **4 invisible → visible, 0 visible → invisible**. Tests green
(131 passed).
**Back to the port agent:** on the English main menu exactly **two** elements
satisfy your pose-equality condition (`ptframe1`/`ptframe2`), so your six span
the whole export. If any of the other four have a **transparent** trailing run,
this rule leaves them alone on purpose. **Which screens are they on, and does a
capture show any of them drawn?** If so the alpha rule is incomplete.
This also closes the old ❔ on `ptframe1`/`ptframe2` "resting at alpha 0 but the
capture shows the frame plainly".
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
a keyframe is the **start of a ramp**.
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
* **That ramp is linear, and it runs at 2 keyframe time units per rendered
frame.** Measured frame-by-frame off the running game's own draw stream: a
declared 15-unit fade lands on `round(255·k/15)` for all seven of its samples,
with `k` stepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames.
**measured**, not decoded — the disc says `t=30`, it does not say what a `t` is.
🟡 **But a multi-keyframe element's TIMELINE does not reproduce (2026-08-28).**
The linear law and the 2-units-per-frame rate rest on the splash's `_eff`
glows, and those are exact. Applying *their* calibration — with no free
parameter left — to `palogo_gamearts` in the same bundle and the same frames:
the logo is still at `a=255` nine frames after its declared `a=32`, its
fade-out runs ~17 frames late, and its declared 80-frame fade-in is never
drawn at all (and that is not culling — the same element is submitted down to
`a=7` on the way out). Its declared fade-out also spends 12 of 16 units
dropping only 23/255 of the alpha; the capture shows no such plateau.
🔵 **Followed up, and the candidate is now strongly favoured — but not
adopted.** That `+36` holds the *next* pose's time is supported by a
calibration-free measurement: the observed full-alpha **hold : fade-out** ratio
is 83 : 13 frames = **6.38**, the shifted reading predicts **8.00**, and the
current reading predicts **0.25** — off by **26×**. With the glow's 2
units/frame fixed and nothing else free, the current reading says the logo
holds full alpha for **2.0 frames**; the game holds it for **83**. The shift
also makes `rest()`'s plain dwell rule pick the visible hold instead of a
fully transparent pose, and removes the decoder's "last block's time is
unreadable" special case.
~~🔴 **What stops it:** … `GP_TITLE` build 7 … twins should match in
brightness …~~ **Withdrawn 2026-08-28.** That render difference is **not**
evidence about the time association. It is one element — `ptlogo_eff3.t32`, a
transient bloom that grows `0 % → 200 %` at full alpha while rotating, then
collapses — and it has **no resting pose at all**. `rest()` falls through to
its dwell fallback and returns whichever end of that movement the indexing
lands on: the invisible frame as decoded, the 200 % peak shifted. An 896×389
sprite at 200 % is larger than the screen, which is the whole 13.1 % and the
whole luminance gap. I was comparing a heuristic, not a decode.
🔴 **And that is a real defect you should know about:** `rest()`'s dwell rule
is unsound *whenever it runs*. A dwell gap is time spent interpolating **from**
pose k **to** pose k+1; neither is held unless they are equal — which is a
plateau, and the plateau path has already returned by then. **So any element
with no two adjacent identical poses has a guessed rest pose**, in our renderer
and in anything built from it. Measured disc-wide: **2 305 of 15 493 elements
(14.88 %)** — ⚠️ **corrected 2026-08-29 down from a published 3 807 (24.57 %)**,
which counted 1 502 *single-keyframe* elements as guesses; those have one pose
and it is unambiguously their rest. Of the real 2 305, **195 get a degenerate
`scale = 0 %` pose**, and the two
candidate rules agree only **50.2 %** of the time.
✅ **This does not block you — your exposure is TWO elements, both on the
splashes.** The fallback is reached only by an element that is plateau-less
*and* multi-keyframe; a single-keyframe element short-circuits. Title, main
menu and `EXTRAS` reach it **zero** times, which is why three different
fallback rules render them to identical correlations. The publisher and
developer splashes reach it once each — and there, `SYLPHEED_REST_RULE=last`
(the final keyframe) scores **+0.9982** and **+0.9758** against the current
rule's +0.9600 and +0.9643. ⚠️ Measured against single frames of a transient
animation, so it fixes which pose matches *those* captures, not which is
canonically at rest. Default unchanged — it is better on both screens where it
fires and identical on the other three, but it would move 2 305 elements
disc-wide on two measurements.
**And the picture is now coherent.** Applying the last keyframe to *every*
element (not just the plateau-less ones) collapses all five screens — title
0.9500→0.6819, main menu 0.9460→0.6416, `EXTRAS` 0.9440→0.5745, and both
splashes render **blank**. The reason is the model: a group is **entry → hold →
exit**, and the exit is the screen's *dismissal*. A displayed screen is sitting
at the **hold**, not at its final pose — so `rest_plateau` is right, and the
last keyframe is the *post-exit* state. It is right for a **transient** element
precisely because a transient's settled state is "gone". The draw capture agrees
independently: on the developer splash the `_eff` glows draw on frames 94115
and the logos on 116211, so the glows are already over when the logos are up.
**Three independent observables — animation timing, static composites and the
per-frame draw log — all support: plateau where there is one, last keyframe
where there is not.**
📊 **Structurally, `last` is defensible for 2 293 of the 2 305 (99.5 %)**: 1 618
end invisible (a transient, gone at rest), 675 end at maximum alpha (faded in
and stopped — the final pose *is* settled), and only **12** end visible below
full alpha, which are genuinely unclear. The dwell rule returns a mid-movement
frame by construction. ⚠️ The 1 618 rest on an assumption worth seeing: that
such an element's animation has finished by the time the screen settles — shown
by the draw log for the two splash glows, unshown for the rest.
📊 The disc-wide blast radius, for whoever decides: the rules **differ on 82.3 %**
of those 2 305, so "either is fine" is not available — and the current rule
returns a **zero-scale** (collapsed, pre-roll) pose for **195** of them against
**43** under `last`, a 4.5× reduction in provably-degenerate results. That
argues the same way as the captures, from the data's own structure. The single disagreement
is `palogo_anima_eff.t32` on the developer splash, and the current answer is
the defensible one there — see below. Still worth flagging plateau-less
elements in an export rather than silently inheriting our guess; it is one pass
over the keyframes.
✅ **One concrete part of it is fixed (2026-08-28): `scale = 0` no longer
renders at full size.** `blit`/`fill_quad` coerced `scale == 0` to 100 %, so an
element collapsed to nothing was drawn full-size. The disc settles the reading:
2 166 elements have a zero-scale keyframe, **not one is zero throughout**, and
1 762 grow back out of zero — so 0 means collapsed, not "unset". **Renders are
24/24 byte-identical** on `GP_TITLE` (all 16 builds), `GP_PAUSE_MENU` and
`GP_OPTIONS`, so nothing you rely on moves; the 126 elements the old
code actually painted are **all in `GP_READY_ROOM.pak`**, already a no-go.
[`structures/ui-rat-layout.md`](../re/structures/ui-rat-layout.md)
🔴 **"rest = last keyframe" is refuted** as the fix. The splash has three
sibling glows with identical structure and times, differing in one byte
(`a=212` vs `a=255` at `t=45`); that rule would make `anima_eff` alone
invisible while its two siblings stay lit. The capture agrees weakly — box-mean
ratios capture/render are gamearts 0.717, seta 0.723, **anima 0.772**, and a
glow we drew that the game does not would put anima *below* its siblings.
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
🟡 **The shift is still not adopted**, now for a different reason: it flips
this element to the visibly wrong answer, so it and a decision about
plateau-less elements have to land together, and neither half has a capture to
verify against.
**Default unchanged**, experiment reachable via `SYLPHEED_KF_TIME_SHIFT=1`.
**What this means for you:** the interpolation *law* is settled (linear, 2
units/frame); a multi-keyframe group's *timing* is not — do not expect a
2-frame hold where the game holds 83.
**The seconds conversion is settled: `1 unit = 1/60 s`, a 30 Hz title.** The
re-test this page used to name has been run — 300 submitted frames timed on the
**idle** title (nothing loading) came out at **28.8 and 28.3 fps**, the same
rate as the 27.6 fps measured during the loading splash. The competing 60 Hz
reading is excluded: it needs the emulator at 47 % of real time while idling on
a screen that costs ~5 draws per frame.
A second, independent line agrees — the transition quad is declared black for
12 units (0.20 s under this conversion) and a capture measured the pure-black
plateau at 0.170.23 s
([`screen-transitions.md`](../re/screen-transitions.md)).
⚠️ Still **measured, not decoded**: no field on the disc says "sixtieths of a
second".
* **The paint order is derivable from the file.** Each `T8aD` sprite header
carries a **`u16` layer key at `+0x0A`** (the upper half of the 32-bit word at
`+0x08` is zero in all 21 184 sprites on the disc). Paint order is that key,
**stable-sorted** so equal keys keep declaration order; elements with no sprite
(`.prm` primitives, `.tbm`) have no key and take an implied one — the backdrop
and dim quads sort early, the screen-transition fade (`pteff00.prm`,
`pfeff00.prm`) sorts **last**. ✅ decoded. Checked against five paint orders read
off the running game, one of them (`GP_SAVE_LOAD`'s slot-list header, 6
instances) **exact and independent of the screens the rule was fitted to**, and
against a fresh `EXTRAS` capture. It reorders 341 of the disc's 965 builds, so
it is not a no-op dressed as a rule.
🟡 **The one residual: ties.** Where two elements share a key the game
sometimes paints them in an order nothing predicts — eight candidates refuted,
including declaration order, RATC child order, keyframe times, resting X/Y and
`kind`. Measured cost: on the three screens with ground truth it changes the
blend of **one element on one screen** (a title glow). Take the stable sort and
accept that.
* **Ⓑ returns to the title, and that title still works.** Ⓐ on the Ⓑ-returned
title opens the main menu — measured, with Ⓐ on the boot title as the control in
the same run. (Only the *attract*-returned title is inert, which is an emulator-
harness curiosity, not a port concern.)
* **Menu movement, measured off the running game.** ⬆⬇ move one item per press
and **wrap at both ends** (5-item main menu and 3-item `EXTRAS` both). ⬅➡ do
nothing. Ⓑ goes up one level **and restores focus to the item you came from**;
Ⓑ on the main menu returns to the title; Ⓑ on the title does nothing. **Initial
focus is not stable**: four boots of the same script gave `TUTORIAL`,
`TUTORIAL`, `NEW GAME`, `NEW GAME`. Do not hardcode it; pick one and say you
picked it. All **measured**, none of it on the disc.
* **Each button's destination is measured; its GamePart id is not.**
**`NEW GAME``DIFFICULTY`** (`EASY`/`NORMAL`/`HARD`/`BACK`, opening on
`NORMAL`) **`SELECT DATA`** — it does *not* hang; the run then hits the
already-documented `sub_823070B0` cache crash, which is not a menu problem.
`LOAD GAME` → the save-slot list, `TUTORIAL` → the lesson list, `OPTIONS`
the settings menu, `EXTRAS``GP_TITLE` build 6, `EXTRAS ▸ MISSION SELECT`
the stage list. The
GamePart ids (`3`, `25`, `8`, `5`, `7`) are the entries of the decoded id table
whose **names match the screens seen**; that binding is authored, not measured.
* **A screen change is a fade through black.** Each screen carries a full-screen
black `.prm` quad that paints last (`pteff00.prm` / `pfeff00.prm`) whose
keyframe group *is* the transition: black at `T0`, clear by `T1`, clear until
`T2`, then back to black on exit. ✅ decoded, with a disc-wide check — and in
`GP_TITLE` exactly the six **screen** builds carry it while the six overlays do
not. The fade-in length is `T1 T0` and is read from the file (0.87 s for
`EXTRAS`, 0.97 s main menu, 4.08 s title). ❔ **The fade-OUT length is not on
the disc** — the last keyframe has no time slot; **measured ~0.4 s**, twice.
The black hold measures 0.170.23 s. ⚠️ Do not time the fade-in off a capture's
brightness: the incoming screen's own element animations dominate it and run
much longer than the quad.
* **The movie manifest names every boot-side video by role.** `dat/tables.pak`
entry `0x5b983a08`: `LOGO1``LOGO4``logo1.wmv``logo4.wmv` (**not on the
disc** — this is why the splash is a screen), **`ADVERTISE_MOVIE``ADV.wmv`**,
`STAFF_ROLL``SYLPH_HD720p_8M-CBR_2ch.wmv`, **`MS00A``S00A.wmv`** (the
new-game intro, 93.9 s, with subtitle + `VOICE_S00A` + a text overlay),
`MS01A``S01A.wmv`. ✅ decoded — and **`S00A.wmv` is now also measured**: matched off the running game at 0.961.000 with a strictly monotone playhead over 25 consecutive 0.5 s samples. It starts ~4.5 s after Ⓐ on the save slot.
**The boot intro and the attract movie are the SAME asset** — there is no
separate boot slot, and 15 of 19 captured attract frames match `ADV.wmv` with a
monotonically advancing playhead ending at its full 137 s. One video, not two.
✅ The attract movie **plays to its end**; nothing cuts it short.
**A movie is skippable with a single Ⓐ.** Measured: one tap ~45 s into the
boot brought the title at ~57 s against a ~193 s no-input baseline over three
boots, with Canary's own keystroke counter proving exactly one press was
delivered — and the skipped-to title is **fully functional** (`PRESS Ⓐ` plate
present, Ⓐ opens the main menu). What breaks the boot is *hammering*: 88
presses left a permanent black screen. One press is fine.
* **The menu's sound events are named on the disc.** `tables.pak`'s `SOUNDS`
record carries 322 `SE_*` cues, and the low block is the UI vocabulary — named
after the *event*: `SE_UI_CURSOR` (2), `SE_UI_DECIDE` (3), `SE_UI_CANSEL` (4),
`SE_UI_IMPOSI` (5, the error), `SE_UI_SUB_WIN_OPN`/`_CLS` (8/9),
`SE_UI_SPLASH_IN`/`_OUT` (12/13). ✅ decoded, full list in
[`data/se-ui-cues.txt`](../re/data/se-ui-cues.txt). They all live in **one**
bank — `BANK_SE` is a single field naming `Static.slb`, and 0 of the 322 has
its own `FILES` entry.
🟡 **Which event fires which cue is a name match**, not a measurement — strong,
because these are the authors' own event names, but nobody has watched the game
emit cue 2 on a d-pad press. The port is authoring it.
**The SE audio IS extractable — by playing it.** (This corrects an earlier
"cannot be extracted" on this page.) The disc carries no index: `Static.slb` has
**0 `RIFF`/`seek`/`WAVE`** and there is **no XACT container anywhere** (0 ×
`XGSF`/`SDBK`/`WBND` in 1.08 GB of `sound.pak`; no `XACT` string in the
executable — those extensions are the authoring tool's). But Canary's
`--xma_param_probe=true` logs every stream's head bytes, and searching them in
`Static.slb` locates the wave exactly:
**d-pad move → `0x1ec0` (4 pkts / 8 192 B, 0.533 s); Ⓐ confirm → `0x5d6c0`
(6 pkts / 12 288 B, 1.016 s); Ⓑ back → `0x0ec0` (2 pkts / 4 096 B, 0.344 s)** —
every cue the five screens need. Move and back reproduce with identical head
bytes across **two independent boots**. Each matched at one offset only, and
the first two are contiguous
(`0x0ec0 + 4096 = 0x1ec0`) — the bank is a packed run of whole 2 048-byte
packets with no delimiters, which is why nothing could be scanned for.
**⬅ and ➡ play nothing distinct** — no new stream on either, so leave them
silent (the probe dedups, so this excludes a *distinct* invalid cue, not a
quiet replay of an already-heard one).
🟡 The Ⓐ press both confirms and opens a screen, so whether its wave is the cue
named `SE_UI_DECIDE` or `SE_UI_SUB_WIN_OPN` is not separated.
⚠️ The order is **not** cue-id order, so the index must be observed per cue, not
counted. ✅ **The slices decode**: 0.533 s, 0.344 s and 1.016 s of mono 48 kHz
audio with the attack-and-decay shape of UI blips, via
`tools/re-capture/slb_extract_wave.py` — whose wrapper reproduces a known-good
`BGM_001` decode to the same 173.808875 s, so it is verified, not assumed.
* **The boot sequence is not data-driven — the port authors it.** ❔ Four places
were checked and the order is in none: `config.ini`'s `[SYSTEM]` is empty, the
movie manifest carries assets not transitions, the requested GamePart id lives
**only as a stack argument in flight** (no persistent field, no literal store),
and the string `GP_ADVERTISE_DEMO` has **zero xrefs**. A transition is a call
with an id argument, chosen by code.
🟡 **But the states have names, and the transition uses them.** `sub_821C6458`,
the title part's state function, calls **`sub_821CC860(…, "<NAME>", 0)`** with
`TITLE_SCREEN`, `TITLE_MENU` and `LOADING` — the three states measured off the
game, in the game's own words — and installs the result. So a transition is a
call with a **name** argument, which is also why `GP_ADVERTISE_DEMO` has no
xrefs: at this level the screen graph is name-keyed, not id-keyed.
✅ The argument is now **decoded** at 46 of that function's 48 call sites (28
distinct names). ⚠️ It is a **generic name-keyed lookup**, not a screen factory —
its arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`,
`GAMMA_RGB`. An earlier version of this page claimed `DIFFICULTY` and
`EXTRA_MENU` as corroborated screen names; **neither is ever the argument**, and
only `TUTORIAL_MENU` survives.
**And the state machine itself is decoded**: `state = this+136`, ten states
dispatched through a jump table at `0x821C6498`, with **18 transitions** each a
literal `li`/`stw`. States **0, 2, 8** install `TITLE_SCREEN`, `TITLE_MENU`,
`LOADING`. `4 → 0` is the only edge back to the title, reached from `2 → 4`
which matches Ⓑ-returns-to-title as measured. Full graph in
[`data/title-state-machine.txt`](../re/data/title-state-machine.txt).
⚠️ **All of that is ONE PHASE.** `GamePart_Title` dispatches on an outer phase
field at `this+132` (five values) before reaching any of it: phase 0 is the
**developer splash** (`sub_821C5690`, the same function the corpus fingered
independently), phase 4 is the title/menu machine. The ten states and
eighteen edges above live inside phase 4 alone.
**State 4's edge conditions are an event code**`sub_821C6458`'s third
argument. State 4 is the input-waiting state (reached straight after the menu
is installed) and handles 6 of 26 events: **`0` → title**, **`3`, `5`, `8`,
`25``LOADING`**, `10` → state 5.
🟡 What the event *numbers* mean is not decoded — button id, menu row, or
message id — so the port still takes the button→destination map from
measurement. The one-to-title / four-to-loading shape matches the five-item
menu with Ⓑ, but that is a **count-match, not a mapping**. The sequence itself is fully measured:
splash → `ADV.wmv` → title + `PRESS Ⓐ` → (idle ~810 s → `ADV.wmv` in full →
title) → Ⓐ → main menu, with Ⓑ from the main menu returning to the title.
* **`config.ini` picks the language, and that is what picks the EN/JP build.**
The disc's **only** config file (400 bytes, at the root; one `find` over the
whole extract). Its `[LANGUAGE]` section maps the console's `XC_LANGUAGE_*`
value to `eng`/`jpn`/`deu`/`fra`/`esp`/`ita`, defaulting to `eng` — that code
selects `GP_TITLE`'s English or Japanese build and the `<lang>.pak` families.
✅ decoded.
❔ Its `[SYSTEM]` section — which the file's own 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.
* **Five GameParts are named but never registered.** Pulling every
`RegisterToFactory<N, class silph::GamePart_X>` diagnostic string binds **24 of
the 29 ids to a C++ class**
([`data/gamepart-class-ids.txt`](../re/data/gamepart-class-ids.txt)). Ids `1`,
`2`, `16`, `18`, `28` have no registration site — including
**`GP_ADVERTISE_DEMO` (1)**, which agrees with the measurement that the attract
loop is the *title* replaying `ADV.wmv` rather than a separate part. 🟡 an
argument from a diagnostic string, not from the code.
Also: **`3` and `4` are both `GamePart_SaveLoad`** — one part, two ids.
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
executable's own registration strings. ✅ This is the screen vocabulary; which
button reaches which entry is Q4 and is *not* part of it.
* **The logo splash is a screen, not a video.** `logo1``logo4` are
manifest-bound with no `.wmv` on the disc. ✅
* **The logo splash is a screen, not a video, and it renders.** `logo1``logo4`
are manifest-bound with no `.wmv` on the disc. ✅ The screen is **two** bundles
in `GP_TITLE.pak`, each shipped twice: entries **10/13** the white
`SQUARE ENIX` publisher logo, entries **11/14** `GAME ARTS`/`SETA`/`studio
anima`. ⚠️ They are **invisible to the default `screen list`/`render`**
`is_build` rejects them for having no `.rat` child. Pass **`--all`**, which
renumbers `--build`. So the first of the five screens does have a reference
composite. ✅ **And a capture**: both halves edge-correlate to their renders at
**0.91** and **0.98** at zero shift, each rejected by the other (0.03, 0.03).
**Its timing is DECODED, not just measured.** The splash **fades both ways**
and the bundles carry the keyframes: `SQUARE ENIX` `[15 30 235 239 251 255]`,
developer logos `[15 30 190 194 206 210]`, each with an `_eff` glow child on
`[15 30 45]`. Under `1 unit = 1/60 s` that is a 0.25 s ramp in, a **3.42 s**
/ **2.67 s** hold, and a **0.33 s** fade out — and a 10 fps capture measures
≈ 3.5 s / ≈ 2.4 s holds and **≈ 0.3 s** fade-outs. The visible brightness
overshoot on the way in is the `_eff` glow ramping after the logo, not a
rendering artifact. Wall-clock: logos ≈ 0.44.7 s and ≈ 4.98.4 s, then
`ADV.wmv` from ≈ 8.9 s. ⚠️ The cyan `SQUARE ENIX` at ≈ 9.5 s is the
**movie's** opening, not a third splash screen.
* **Sprites carry their own labels.** No font rendering or localisation is needed
for this milestone. ✅
for this milestone. ✅ — and the localisation is *already baked in*: the
Japanese screens are separate builds in the same pak, not a text swap.
## Facts the port will trip over
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
plays only Ogg Theora natively. How to handle that is the port's decision, not
ours — but it is not optional.
* **The disc holds 3.3 GB of video.** Only the boot intro and the one new-game
intro are in scope.
* **The disc holds 3.3 GB of video, and exactly two files are in scope:**
`dat/movie/ADV.wmv` (the boot intro *and* the attract loop — one asset) and
`dat/movie/S00A.wmv` (the new-game intro, 93.9 s). Named from the movie
manifest, ✅ decoded.
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
highest-offset entry in `sound.pak` and its size field is an allocation size. A
reader must allow a short read there and only there.
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
for spoken lines and discards half a music mix.
* **A music bank has several sub-waves and we glue them together.** `BGM_001`
is 10 KB + 4.47 MB + 4.67 MB, concatenated into one 347 s track. Nobody has
established whether those are intro + loop, two variations, or two halves —
see Q10. Do not build menu looping on the concatenated track until it is
answered.
* **A music bank is TWO STEMS THAT PLAY TOGETHER — do not concatenate.**
✅ The "10 KB + 4.47 MB + 4.67 MB" reading was wrong: the 10 KB is the bank
header, and a bank is exactly **two waves of identical duration** (32/32 banks
on the disc). `BGM_001`'s two are **sample-synchronous** — transient
correlation peaks at lag 0.00 s over ±5 s, and both stop at the same
millisecond, 167.663 s. Wave 1 is quieter, far more L/R-decorrelated and has
almost no bass, so it reads as a surround-rear pair or a second intensity
layer — 🟡 which of those is unsettled, and `ChannelMask` is `0x0002` on both,
so the file will not say. Today's 347 s concatenation plays the piece twice,
the second time as a bass-less stem.
**And it is not a seamless loop**: `BGM_001` fades out at 167.663 s and is
followed by 6.15 s of silence, with no loop-point field identified. A menu loop
is authored.
**The menu's music is `BGM_103`.** The cue *table* cannot say — its BGM
entries are numeric — but `GamePart_Title`'s `sub_821C5580` plays **cue 1103**,
and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are
byte-for-byte the two streams the XMA probe saw decoding at the main menu.
Static code, disc census and runtime all agree. The port does **not** have to
choose a track.
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
number of XMA1 packets from any known data offset.
## What is still open
Every question above is answered, so this is the honest residue rather than a
work queue. **None of it blocks the five screens.** (Q1's seconds conversion was
here until 2026-08-28 and is now settled.)
| | what | why it is stuck |
|---|---|---|
| 🟡 | **cue NAME → event binding** (Q8) | event→**wave** is measured for move/confirm/back; that the cursor's wave is the cue *named* `SE_UI_CURSOR` is still read off the authors' identifiers |
| ❔ | **the other ~319 SE cues** (Q8) | located one at a time by triggering them; only the three the menu needs have been done |
| 🟡 | **the paint-order tie-break** (Q3) | eight candidates refuted; costs one element's blend on one screen |
| 🟡 | **GamePart ids behind the buttons** (Q4) | the *screens* are measured; the ids are a name match onto the executable's class names |
| 🟡 | **the boot transitions in code** (Q6) | both levels decoded — phase at `this+132` (`entry→2`, `2→0`, `2→3`, `3→4`, `4→2`) and state at `this+136` inside phase 4. Phase 0 = splash (`LOGO`), phase 2 = title + `PRESS Ⓐ`, phase 4 = menu. Unknown: what the event *numbers* mean |
| ❔ | **builds 0/1 and 10/11**, the `DELTASABER` plates (Q2) | never seen anywhere in the boot path, the title-side screens or the attract loop. A mission load is the remaining candidate and this container kills runs before one completes |
(An earlier version of this table called the audio items blocked on "an emulator
whose audio path can be observed". That was wrong — this build already has
`--xma_param_probe`, and using it settled both.)
⚠️ **A container caveat that bounds all of the above:** the emulator has twice
been killed mid-run with no crash line in its own log (at ~50 s and ~145 s), on a
box sitting at ~1 GB free with swap exhausted. Dynamic experiments here have to
fit in roughly two minutes of guest time, which is why several of these residuals
are unfinished rather than unattempted.
## Reference data
Committed alongside the findings, so the port can be built without a disc in the

View File

@@ -75,7 +75,160 @@ alongside it.
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
| **S1** | **Ready Room probe.** *Gated* — one iteration, then stop | A written go/no-go (see below) |
| **S1** | ~~**Ready Room probe.**~~ **DONE 2026-08-28 — [no-go](../re/ready-room-probe.md).** It is 2D and enumerates fine, but the pak is briefing/tactical-map content, not the Ready Room menu | ✅ go/no-go written |
## 🔴 Emulator-side questions are blocked — the title is not reachable here
**Status 2026-08-29, instrument-verified.** Two open items need a running menu:
the gamma control behind [tone curve](../re/structures/ui-render-tone-curve.md),
and separating `8AX` from `ptbase` in
[8AX](../re/structures/ui-8ax-fullres-background.md). Both are blocked.
**The negative is now solid.** A probe that restarts its capture stream every
30 s and cross-checks itself against an independent grabber every 60 s
(9 checks, 8 agreeing to within 2 %) ran **560 continuous seconds from launch at
3.30 fps**: the interactive title's green Ⓐ plate never appeared, while the game
rendered throughout. The final frame correlates 0.0145 / 0.0047 / 0.0102 with
our title / main-menu / `EXTRAS` renders — attract-movie content.
**Why is unknown.** `live-title-press-a.png` (753 glyph pixels) proves it was
reachable from this container on 2026-08-28. Clearing the shader cache fixed a
*black surface* but not this.
⚠️ Neither blocked item blocks the five menu screens. See
[capture-harness-status](../re/capture-harness-status.md) for the full trail,
including three earlier "the title never appears" claims that were withdrawn
because the instrument was broken each time.
## 🟡 Needs one more run — a Japanese-locale capture
Recorded rather than worked around, per "do not improvise around a blocker".
Two questions have converged on needing **one capture we cannot take**: a running
capture of `GP_TITLE` **build 7**, the Japanese title screen.
* Q1's keyframe-time association. The shifted reading is favoured 26× by a
calibration-free measurement, and the only render it changes on the whole disc
is build 7 ([`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md)).
* What `rest()` should return for a plateau-less element. The one element that
discriminates, `ptlogo_eff3.t32`, is also in build 7
([`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)).
**🔴 The "cannot" was wrong, and is withdrawn (2026-08-29).** It is true that
`user_language` is only ever `DECLARE_int32` here, with no `DEFINE` and no entry
in `xenia-canary.config.toml`, so there is no flag to pass — and passing an
unknown one is specifically dangerous, because `run-canary`'s own header records
that xenia calls `ShowSimpleMessageBox` from `ParseLaunchArguments` *before*
logging starts. But the cvar is not the only route, and I stopped at the first
one I checked.
**The language is persisted, and canary's own file is writable.**
`kernel_state.cc` builds `XConfig` over `<storage_root>/xconfig.settings`, which
exists here at `/sylph-home/re/.local/share/Xenia/xconfig.settings` (6 680 B).
`user.language` is a **big-endian u32 at file offset `0x912`**, located by three
independent landmarks rather than guessed:
| landmark | expected | found |
|---|---|---|
| `music_volume` (`User+449`) | `0.7f` | BE float at 2727 → `User` base `0x8e6` |
| `language` (`User+44`) | `1` = `kEnglish`, the hard-coded default | **1** at `0x912` |
| `country` (`User+64`) | United States | `103` at `0x926` |
`XLanguage::kJapanese = 2` (`xbox.h:307`). Writing 2 there and restoring
afterwards is using canary's own persistence, not patching its code.
**🔴 DIAGNOSED 2026-08-29 — the harness, not the game.** `screenshot` costs
**10.8 s while xenia is running** and **0.117 s once it is killed** (92×, at a
1-minute load average of 1.80, so it is contention with the emulator). A
two-grab polling loop therefore samples every **~41 s**, against a title screen
this corpus documents as lasting *a few seconds*. Four runs — two locales, two
launch paths, two gamma settings — were all blinking slower than the event.
🔴 **…and that diagnosis is itself refuted (same day).** The faster probe was
built — 3.98 s → **0.29 s** per sample, 13.7×, control-verified at 753/327 — and
at 3.99 fps for **420 continuous seconds (1 674 samples)** the interactive title
*still* never appeared. Sampling rate was a real defect and not the cause.
✅ So "the interactive title does not appear mid-run without a pad press" is
reinstated, now as a dense measurement. ⚠️ Its reach is a **mid-run** window; it
says nothing about the boot title.
🟡 Leading hypothesis, unconfirmed: the plate appears only in the **boot** title
window and the attract loop's title has none — which is precisely what
`title_states_capture.sh` was written to test. The experiment is to start the
fast probe from t=0, not attach to a run already in progress.
See [capture-harness-status](../re/capture-harness-status.md).
**🟡 Still not settled — two runs, and the reason moved again.**
*Run 1 (2026-08-28)* reported "title not seen" in 787 s. **That was a broken
tool, not the game.** `wait_title.sh` still carried the single-pixel oracle that
`is_title.py` was written to replace — a 1280×720 coordinate sampled against the
1279×675 game surface, so it always reads the copyright line. Fixed; it now
delegates to `is_title.py`.
*Run 2 (2026-08-29)*, with the working oracle and the profile flag the English
captures use, **still did not reach the interactive title.** Not one frame in the
run showed a single green-Ⓐ glyph pixel, and content correlation against either
build-7 render never exceeded **0.22**. The game sat in the attract movie
throughout, polling `XamInputGetKeystrokeEx` (601 calls) — i.e. alive and waiting
for input, not hung.
**✅ The control was run (2026-08-29), and it removes the locale from the
picture.** Same flags, same oracle, English locale: **75 samples over 734 s, every
one glyph = 0.** The English boot does not present the interactive title either.
Canary was alive throughout and polling `XamInputGetKeystrokeEx` (1 801 calls);
the frame at 734 s has content but correlates only **0.15** with build 4's render
and 0.05 with the main menu, i.e. it is an attract-movie frame, not title art.
So the Japanese run was **not** failing because of the locale — neither locale
reaches the interactive title in ~12 minutes of no pad input. **🔴 Tried the proven path too, and I am stopping this line (2026-08-29).**
Run 3 launched exactly the way `boot_menu.sh` does — `DISPLAY=:98`, `--apu=sdl`,
`/dev/shm/xenia_*` cleared, the existing profile signed in — and drove
`skip_intro.sh`, the detector that is documented to work. It classified **every
one of 20 samples over 604 s as "movie"** and waited them all out, then timed
out. A direct check at 604 s confirms it was right: zero green-glyph pixels,
`screen_id` = other, warm mean (69,53,40), correlation **0.09** with build 7. The
game really was playing attract movies for ten minutes.
So: **three runs, two locales, two launch paths, ~35 minutes of emulator time, no
interactive title.** Either the attract loop is far longer than the 600780 s
windows tried, or something has regressed since `live-title-press-a.png` was
captured (that one came from a pad-driven boot — commit `88b3ce9`, "booting to
the main menu and walking it").
⚠️ **Not worth more loop iterations.** This capture does not block any of the
five menu screens, and it has now cost five. It is written down so a later
session with a reason to spend an hour on the boot path can pick it up; the
locale mechanism, the fixed oracle and the launch recipe are all in place, so
what is left is patience with the attract loop, not tooling.
The locale is restored to English; `set_console_language.py ja` flips it back in
one command.
⚠️ Neither question blocks the five menu screens. Q1's *interpolation law* is
settled and only multi-keyframe absolute timing is open; `rest()` differs from
its alternative on **one** element across all five screens, and the current
answer there is the defensible one.
## 🔵 Needs a human decision — rotation (raised 2026-08-29)
The port agent asks whether it should **render** `rotation_deg` (decoded at
keyframe `+12`) when `sylpheed-cli screen render` deliberately does not. Its own
framing is the reason this is not mine to settle: if the port rotates and the
reference renderer does not, then `verify-screen` reports a large title diff that
means *"the port is right"* — a silently inverted signal.
The RE half is answered and is in HANDOFF: rotation is about the **declared
pivot** (measured against a GPU capture, not assumed), and it changes nothing on
the five screens **at rest**.
What needs a decision is which way the divergence gets closed:
* teach `ui_layout::blit` a rotating path, so the two renderers stay comparable
and the diff keeps meaning "someone is wrong" — costs work in the reference
renderer, which is otherwise not on the port's critical path; or
* let the port render rotation and mark the title as a known-divergent screen in
`verify-screen`, accepting a check that no longer guards the title.
Recorded rather than chosen, per "do not improvise around a blocker".
## Known unknowns — say so, do not fill them in
@@ -118,6 +271,35 @@ answer is no-go by definition, not "try harder".
**Then stop and write the go/no-go.** Do not start Ready Room work on your own
authority.
## Publishing a decoder state the port pins
The port's exporter depends on `sylpheed-formats` **by revision**, so a commit of
yours becomes part of its build. That creates an obligation that is easy to miss:
⚠️ **A commit reachable only from an `auto/*` branch is not safe to pin.** If the
branch is later deleted, or — worse — **squash-merged**, the object is orphaned.
Squash creates *new* commits, so `main` appears to contain the work while the
pinned sha becomes unreachable, and the port stops building for anyone doing a
fresh checkout. Silently, at their build, long after the moment of breakage.
**So when you land something the port needs, tag it:**
```bash
git tag -a formats-pin-$(date +%F) -m "what the port gets from this state"
push-work # --follow-tags publishes annotated tags with the branch
```
A tag is a permanent ref. It survives branch deletion and squash-merge, it is
self-documenting in the port's `Cargo.toml`, and it fails loudly at *fetch* if it
ever goes missing rather than silently at build.
Then tell the port over the message channel that a new pin exists. It bumps
deliberately, as its own commit — that is how it stays current without floating,
which would only give it staleness it cannot see.
`formats-pin-2026-08-29` at `7eeae30` is the first, created because the port had
already pinned a commit that lived on one topic branch and nowhere else.
## Handing it over
[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it

View File

@@ -19,9 +19,9 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
| IDXD nameless field keys | ✅/❌ | [idxd-unnamed-keys](structures/idxd-unnamed-keys.md) + [`tools/re-capture/idxd_unnamed_keys.py`](../../tools/re-capture/idxd_unnamed_keys.py) | Census of every field entry whose `name_off` is `0xFFFFFFFF`, disc-wide: **7 750 objects, 2 757 039 field entries, 0 parse failures**, `tag_hash` reproducing **1 271 462/1 271 462** named keys. **7 094 distinct keys are never named — and 7 052 of them are not hashes at all**, but author-assigned element ids (equal to the field's own index in 1 404 924 of 1 485 577 cases; `tag_hash("BGM_001")` is `0xC662435B` while the key valued `BGM_001.slb` is `0x000003E9`). ⚠️ **The "504 hash-keyed nameless fields" figure is 504 ENTRIES, not 504 names** — 42 distinct keys × 6 language copies × 2 records. All 42 are **ISL script-symbol hashes** in `<lang>\script\ID.tbl` (GP_READY_ROOM.pak), the link map built by `PrepareScript`'s "isl script prescanning"; 41 of 42 appear as little-endian call targets inside the `.isb` bytecode, forming a coherent launcher/helper call graph. The hash's own algebra pins the **trailing digits of 30 of the 42 names** (deltas of exactly `+0x01000001` across `stage01..09`, `stage10..16`, `challenge01..06`; `+0x01010000` across `tutorial0101..0601`). ❌ **No name was cracked, and the negative is quantified**: seven attacks up to a 3.5×10⁸ composition space found nothing above the noise floor; exhaustive preimage search recovers `"Stage01"` from its own hash but returns nothing for the real targets at ≤6 characters, and at 7 characters one target already has **1 176** preimages — a 24-bit modulus cannot name an 8+ character identifier uniquely |
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔ |
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔. **2026-08-29, for the UI path only:** our composite is brighter than the emulator's frame by a gamma of **≈1.341.49** across three screens ([tone curve](structures/ui-render-tone-curve.md)) — 🟡 measured, not decoded, constrained only over render values ~060, and possibly canary's own `kernel_display_gamma_type = 2` (BT.709) output stage rather than the game's |
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 144 of 10 148 references resolve**. The 4 that do not are one dangling asset — `pmbase.rat``pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, and `pmbase.t32` is **on the disc nowhere** |
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | **✅ 2026-08-29: a child's NAME is stated by an `opt ` block** (`"opt " | BE32 len | name | NUL | 3 bytes | magic`), not by the printable bytes before it. We had been guessing it from the last printable run, which is right 17 918 times and **wrong 24**, each time because the 3-byte tail is itself printable — `8AX` (×22) and `'OX` (×2). `8AX` is **not a name**; it hid `pteff05.t32`/`pteff04.t32`, the full-resolution background of all five menu screens, which `compose` then dropped with no diagnostic ([ratc-child-names](structures/ratc-child-names.md)). ✅ 60 of 18 002 children carry no `opt ` block, and all 60 are explained: they are the ten frames of the disc's only `.tan` **frame sequence** (`pb_f15_eg_anm.tan`, six language copies of one `GP_READY_ROOM` bundle, 6 × 10 = 60), where one `opt ` block names the whole run — so `parse` over-reports frames as children there. A census of all **18 718** `opt ` names shows a bundle names exactly six kinds of resource: `.t32` 14 756, `.rat` 3 311, `.prm` 367, `.tbm` 224, `.sbo` 54, `.tan` 6 ([tan-frame-sequence](structures/ratc-tan-frame-sequence.md)). Child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 148 of 10 148 references resolve, as of 2026-08-29.** The 4 that did not were `pmbase.rat``pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, recorded as "`pmbase.t32` is on the disc nowhere". It was on the disc all along, as the child the printable-run scan named `8AX` — the `opt ` name fix above resolves it in all four builds (3 686 767 B each). A dangling reference that closes itself when an unrelated decode lands is the corroboration that decode wanted |
| LSTA sprite list | ✅ | `sylpheed-formats/src/lsta.rs` | A display list of inline elements: **T8aD sprites and `PRMD` primitives**. The `count` at `0x04` is **exact and counts both**`count == T8aD + PRMD` for **64/64** lists on the disc, which retires the old "a few entries disagree" note (it compared sprites against a total including primitives). **All 1 281 sprite frames decode** after the T8aD rectangle-list fix |
| IXUD subtitle + caption text | ✅ | `sylpheed-formats/src/ixud.rs` + `movie_subtitle.rs` ([container](structures/idxd-container.md) · [movie link](movie-subtitle-link.md)) | **The IXUD record/field table is decoded and wired in (2026-08-26)**`IxudObject` mirrors `IdxdObject`; uniform 16-byte records, 12-byte fields, every offset in **chars**, and the word at `0x08` is record 0's hash, not a schema id. Verified disc-wide: **1104/1104** objects, **1476/1476** records, **628 165/628 165** named fields reproducing their `ixud_hash` (`tests/ixud_records_disc.rs`). **Caption text: 537 → 8800 lines, which is 8800 of 8800 distinct keys.** Two steps — generalising the key parser from `MSG_DEMO_*` to all **eight** families (`ACRO ADAN ADPL BIRD DEMO RHIN TCAF` use `MSG_<FAM>_<id>_<page>_<line>`, `VOICE` alone inserts a family letter) took 537 → 8074; switching from **token adjacency to record fields** took it to 8800. ⚠️ An earlier "1.3 % of the game's text" figure of mine counted *occurrences across blocks* — the honest denominator is **8800 distinct keys**, so the real starting point was 6.1 %. The `DEMO` control shows why the field route matters: token adjacency finds 537 lines there, fields find **541** — it was dropping lines in the one family it was written for. | timed cues. **The movie↔subtitle↔voice link is solved — statically**, and as of 2026-08-25 read from the IDXD **record table** rather than scraped from the string pool: **104 cutscene slots binding 101 distinct movies**, 99 slots / 96 movies with a subtitle, 99 / 96 with a voice track, 22 / 22 with a telop. ⚠️ The previous counts (94 / 83 / 21) were the numbers of **distinct pool strings** — a repeat reference contributes no token, so 13 later `VOICE_D_450..454` references read as "no binding". **All 18 hokyu movies are bound**, not five. 93 of the 94 distinct subtitle members resolve; `SUBTITLE_S12B.tbl` resolves in none of the six languages — a dangling reference on the disc. The ~104 **script ids are no longer ❔**: they are literal positional field keys in `BASE_INFO`, each naming its record, and all 104 resolve. `movie_manifest::parse` now reads the record table; CSV regenerated by `examples/movie_map_csv.rs` |
| Fonts (ttf/otf/ttc) | ✅ | `sylpheed-formats/src/font.rs` | standard OpenType, parsed via ttf-parser |
@@ -30,7 +30,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| Weapon fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/weapon-struct-runtime.md) · [DATA SHEET route](weapon-datasheet-runtime.md) | **Solved.** Canary maps guest RAM into `/dev/shm`, so the parsed `Weapon`/`Shell` objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — [4 393 values](captures/weapon-runtime-fields.csv). ⚠️ **CORRECTED 2026-08-26: the disc DOES carry a third of them.** Re-read through the [record table](structures/idxd-container.md), **1 514 of the 4 393** rows labelled `defaulted-on-disc` have a value on disc (2 879 genuinely absent) — measured here as an upper bound, since a field is counted when it appears in *any* record of the object; a per-record count gives ~1 448. The old reader could not name a record, so per-record fields read as absent. Spot-checked exactly: `wep_05`/`wep_60` `TriggerShotCount` = **4**, `wep_02` `Power` = **100.0**, `wep_60` `Power` = **1000.0** (refuting the recorded "C band ≈150…500" bracket), `wep_25` `MaximumRange` = **4000.0**, `wep_11/28/36/70` `LoadingCount` = **6/5/5/0**. The runtime capture's numbers all match the disc — what is withdrawn is the premise that it was reaching values the disc lacks. See [weapon-datasheet-runtime](weapon-datasheet-runtime.md) Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check |
| Unit (craft/vessel) fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. **Re-derived independently 2026-08-13 from the loader's own key strings** (`sub_82341A20`; the field name for each store is a string in the image): **159 fields**, agreeing with this solver on **25 of 25 shared offsets**, verified at **406 values matching the disc and 0 disagreeing** over 11 live objects spanning UNIT and VESSEL — landed as `data/unit_definition_layout.txt` + `sylpheed_formats::unit_layout` + a no-emulator test, with **121 defaulted fields** read out ([live-unit-definitions](live-unit-definitions.md)). ⚠️ **CORRECTED 2026-08-26:** those fields are **on the disc**. The ~30-field player-craft table is on disc at exactly the values the runtime "recovered", spread across the `Generic` / `Shield` / `Mass` / `SE` records — which is why a reader that could not name a record saw them as absent. And **"18 of 23 vessel records are missing at least one of `Size_X/Y/Z/HP`" is false: 0 of 114** objects with a `Generic.Type` (43 Craft + 71 Vessel) miss any of the four. See [unit-struct-runtime](structures/unit-struct-runtime.md) Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — ❌ **the sibling-default rules are WITHDRAWN (2026-08-25)**`Size_Y` is on disc for **114/114** unit tables and **differs from `Size_X` in 90**; the old reader missed it exactly when the two were equal (a string-pool dedup artefact, cross-tab `seen+equal = 0` for all three pairs), so the rule was re-deriving the condition that hid the field. It also predicts wrong twice — `UN_e104_ADAN_Carrier.DefencePoint` and `UN_e011_ADAN_Attacker_B_HF_Wayne.FCSRange`. Read the record table instead — [values](captures/unit-runtime-fields.csv) |
| Arsenal develop economy | ✅/❔ | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space, now **solved**: the blob follows **`strings.tbl`'s** order — the display order *plus* the cut items only the localisation file lists (`Adhesive Mine B2A`, `Ballista GSH`, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. `weapon.tbl`'s id list is **not** the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered |
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly |
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly. **✅ 2026-08-28: the keyframe block's `+12` is a screen-plane ROTATION in degrees**, clockwise-positive — confirmed against a GPU capture (declared 30 / 45 vs measured +30.26° / 45.28°) and non-zero in 14.50 % of 83 862 blocks disc-wide ([ui-keyframe-rotation](structures/ui-keyframe-rotation.md)). ⚠️ Every rotation on the disc is inside a **nested** `.rat` leaf record, and `ui_layout::blit` is axis-aligned, so the field is decoded but **not rendered**. 🟡 `+4`/`+8` remain unexplained |
| UI screen paint order | ✅ | [runtime screen object](structures/ui-screen-runtime.md) + [title capture](ui-title-paint-order-capture.md) | **SOLVED**: the game's screen object keeps a second, reordered list of its elements — the child array at `+0x30` — and that is the paint order, not the declaration table. Read live from guest memory (found by the item vtable `0x820b30b4`) and checked against the draw capture: the seven nameable elements sit at child slots 0, 6, 7, 13, 16, 17, 22, strictly ascending, exactly as captured; it also settles the one pair no static field could order. 🟡 deriving that order from the bundle — what the port needs — is still open |
| Scripted input / profile traps | ✅/🟡 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Why a scripted run appears unable to press Ⓐ: **F10 opens the emulator menu bar**, and any Xenia UI makes `XamInputGetKeystrokeEx` return SUCCESS with an empty keystroke *before* any driver is asked (Canary now logs `[RE-INPUT] … swallowed by IsUIActive`); the title needs a **signed-in profile** (hence `--create_profile_if_none`); and a **FIFO trace consumer that exits stalls the emulator**, which reads exactly like a dead pad. 🟡 The main menu HAS been reached — Ⓐ works, but only intermittently (1 in ~4), which is the open question |
| Title-screen guest crash | ✅ | [title-crash-stl-tree](title-crash-stl-tree.md) | The guest throws **`std::out_of_range`** from its cache-manager flush (`sub_823070B0`, an STL map/set erase that builds `'invalid map/set<T> iterator'`); the access violation after it is only the throw **returning**, because this build does not unwind guest EH. Trigger found and controlled: an **incomplete on-disc cache** (`~/.local/share/Xenia/cache/aab216c3`) throws ~100 s into a boot, a complete one never does — 2 runs each way. ❌ `mem_watch`, the handoff's suspect #1, is **eliminated**: cold cache + `--mem_watch=false` throws anyway |
@@ -103,6 +103,7 @@ files, which is how the same ground got covered twice.
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` data offset = `(cumulative start of the `.pNN` segment) mod 2048`; the leading bytes are the previous bank's audio, not a header | ✅ CONFIRMED — exact for 8 783/8 783 banks, 0 mismatches; the four values (1392/1468/1600/1728) are the running sums of the five segment sizes mod 2048, which supersedes the earlier `first_riff % 2048` scan heuristic. Decoding at the right offset yields more audio in 85 of 140 sampled banks (median 70×), 54 identical controls. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
| [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md) | A music bank is two stems of one performance, played together | ✅ decoded structure, 32/32 banks two waves of equal duration (4 explained by the leading-region straddle); stems measured sample-synchronous; 🟡 rear-pair vs intensity-layer unsettled; ❔ no bank names a screen |
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
| [`structures/preset-message-rules.md`](structures/preset-message-rules.md) | The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the `Sperkers` roster | ✅ CONFIRMED, the seven fields read off `sub_82213980`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128; the cooldown is Interval + Fluctuation*rand held in the lingering queue node; the 14 player-facing events are voiced by the player set alone (13/15 Katana tables, 0/129 others) (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue |
| [`structures/mission-scoring.md`](structures/mission-scoring.md) | Mission scoring and the S/A/B/C/D rank thresholds — 24 tables x 3 difficulties | ✅ CONFIRMED, one 22-field schema over 72 records; difficulty moves 10 of 22 fields and never the RankScore thresholds; ✅ all 24 named stage\StageParameter_S<NN>.tbl (+ _Tutorial shared by six, + _Test) |
@@ -134,6 +135,7 @@ files, which is how the same ground got covered twice.
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`→field-count holds except the two `_Test` templates** |
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
| [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) | What a keyframe time is worth, and what shape the ramp has | ✅ CONFIRMED from the running game's own draw stream — the ramp is **linear** (a declared 15-unit fade lands on `round(255·k/15)` for all seven samples) and the animation clock advances **2 time units per submitted frame**. 🟡 the seconds conversion (`1 unit = 1/60 s`) rests on a measured 27.6 present-frames/second |
| [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | ✅ CONFIRMED by measurement over the disc, with the artifact to |
| [`structures/ui-focus-and-effect-elements.md`](structures/ui-focus-and-effect-elements.md) | `_eff` glow layers are not focused-state records | ✅ CONFIRMED by measurement over all 965 screen builds on the disc, |
| [`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md) | The paint order comes from a layer key in the T8aD sprite header | ✅ CONFIRMED on both screens whose paint order has been measured — |
@@ -148,6 +150,13 @@ files, which is how the same ground got covered twice.
| [`title-crash-stl-tree.md`](title-crash-stl-tree.md) | The title-screen crash is an STL `map`/`set` erase on a bad iterator | ✅ CONFIRMED — the guest throws std::out_of_range from an STL |
| [`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md) | A third measured paint order — tool built and validated, screen not reached | ✅ the reader works and is CONFIRMED against both previously |
| [`ui-quad-class-foothold.md`](ui-quad-class-foothold.md) | The guest's UI quad class — a foothold found from the capture's vertex layout | 🟡 PROBABLE for the identification below (it is a static read, but |
| [`menu-navigation-semantics.md`](menu-navigation-semantics.md) | The title menu — how it moves, and where each button goes | ✅ measured: wraps both ends, Ⓑ restores focus, ⬅➡ inert; 4 of 5 destinations driven. 🟡 GamePart id is a name match, ❔ `NEW GAME` untested |
| [`screen-transitions.md`](screen-transitions.md) | Between two screens — a fade through black, and where its timing lives | ✅ the fade quad's keyframe group is decoded (disc-wide: per-pak all-or-nothing; `GP_TITLE` = the 6 screens, not the 6 overlays); the ~0.4 s fade-OUT is measured, not on the disc |
| [`menu-audio-cues.md`](menu-audio-cues.md) | Menu audio — the event vocabulary is on the disc, the binding is not | ✅ `SE_UI_*` cue names/ids decoded and `BANK_SE``Static.slb` (0/322 in FILES); 🟡 event binding is a name match; ❔ `Static.slb` has no wave boundaries, so SE audio is not extractable |
| [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) | What the game reads at boot — `config.ini`, and which GameParts exist | ✅ `config.ini` selects the language (the disc's only config); ❔ its `[SYSTEM]` is empty so the boot order is not in config; 🟡 24/29 ids bind to a class, `GP_ADVERTISE_DEMO` is never registered |
| [`movie-binding.md`](movie-binding.md) | Which movie plays where — boot intro, attract loop, new-game intro | ✅ decoded from the movie manifest (`ADVERTISE_MOVIE``ADV.wmv`, `MS00A``S00A.wmv`); attract identity confirmed independently by frame matching; 🟡 skippability unsettled |
| [`ready-room-probe.md`](ready-room-probe.md) | S1 — the Ready Room probe: no-go, and not for the reason expected | ✅ it is 2D and enumerates (60 builds), but the pak is briefing/tactical-map content; and `kind == 0x3002` finds 0 buttons there |
| [`ui-title-build-map.md`](ui-title-build-map.md) | Which `GP_TITLE` build is which screen state | ✅ CONFIRMED for title / `PRESS Ⓐ` / main menu / `EXTRAS` against live captures; the archive is 8 screens × EN/JP, and "6/8/9 are submenus" is withdrawn |
| [`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md) | The title screen's paint order, measured from the guest's draw submissions | ✅ CONFIRMED — the order in which the running game paints the title |
| [`upstream-baseline.md`](upstream-baseline.md) | A stock-upstream baseline runs Stage 02 crash-free | ✅ CONFIRMED — upstream canary_experimental + only the pad |
| [`weapon-datasheet-runtime.md`](weapon-datasheet-runtime.md) | Weapon DATA SHEET — runtime capture (Route B) | 🟡 first dynamic capture, 2026-07-28. The Arsenal's Gallery Mode panel is a |

View File

@@ -103,3 +103,796 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
out of noise.
* **A probe that never performs the action will "prove" the action does not
exist.**
* **The container's Canary binary can be older than the Canary source tree, and
the failure mode is a hang, not an error.** After a merge into `sylpheed-re`
the prebuilt `xenia_canary` had no `log_ui_draws`, no `mem_watch`, no
`create_profile_if_none` — and an unknown cvar makes xenia open an SDL message
box before logging is up, which headless is an unexplained freeze. Check before
trusting a harness flag: `nm -C <binary> | grep cvars::<flag>`, and
`build-canary Release` if it is missing.
* **A capture armed *at* a screen only ever sees the steady state.** Anything
about how a screen is built or animated has to be armed *before* it exists.
Re-arming every few seconds and keeping every log tiles the approach: each F10
opens a new numbered file and closes the previous one complete.
* **Measure animation in submitted frames, not in seconds.** `VdSwap` counts are
the guest's own frames, so an emulator at 80 % of real time does not move them;
a stopwatch reading does, silently and by an unknown factor.
* **~~This game's menus drop d-pad presses shorter than ~0.3 s.~~ WITHDRAWN
2026-08-28 — the menu WRAPS, and I had not measured that.** The claim came from
reading a cursor that ended up "one item short"; once wrap-around at both ends
was measured ([`menu-navigation-semantics.md`](menu-navigation-semantics.md)),
every one of those press counts is exactly right — four presses at 0.12 s moved
four steps *through the bottom*, which lands one above where a non-wrapping
menu would put it. **No press was ever dropped.** The real lesson is the
general one: *a step count is only readable once you know the topology*, and I
invented a hardware-flakiness story rather than testing the ends of the list.
Still true and worth keeping: screenshot after every step and read the cursor,
rather than trusting arithmetic over the press count.
* **A screenshot taken right after a transition can catch a screen mid-fade.**
A grab 2.5 s after Ⓑ returned the game to the title showed the title art with
no `PRESS Ⓐ BUTTON` plate; one second later the plate was there. That very
nearly went into the corpus as "the returned title has no plate". Sample a
changing screen several times before writing down what it does *not* contain.
* **Do not identify a menu cursor by label brightness.** The obvious oracle —
"the focused label is the brightest row" — fails on this game's menus, because
the background art is brighter behind some rows than the highlight is. It
confidently named the wrong item on a frame whose ring was plainly elsewhere.
Detect the **focus ring** in the gutter left of the labels instead
(`tools/re-capture/menu_focus.py`, 254 vs <82 — no threshold tuning needed),
and look at the PNG before believing either.
* **`screenshot` samples at 0.5 Hz — it cannot time an animation.** Measured:
~2 s per grab (an `import` of the root plus an ImageMagick crop). A 0.4 s fade
falls entirely between two samples, which is why a 40-frame burst across a
screen change looked like an instant cut. For anything timed, record the
display instead: `ffmpeg -f x11grab -framerate 30 -video_size <W>x<H> -i
:98+<X>,<Y> -t <secs>`, then read per-frame statistics off the file. Take the
geometry from `xwininfo -root -tree`, the same way `bin/screenshot` does.
* **A screen's brightness curve is not its fade quad.** The incoming screen's own
elements animate in *after* the transition quad has cleared, so mean luminance
keeps rising long after the fade is over — 1.47 s against a declared 0.97 s on
one screen. Time the fade from where the frame is *pure black*, and take the
ramp itself from the keyframes.
* **A `sound.pak` entry is not a bank.** Entry windows straddle bank boundaries
(the leading-region effect), so a naive "walk the RIFFs in this entry" gives
the tail of the previous bank, then one of your own, then the *next* bank's
header. It looks like a bank with the wrong number of waves. The tell is a
`BANK id=` that is not the entry's own number, and the fix is the `seek` packet
counts — they pin each wave's join exactly, so a straddle can be realigned
rather than thrown away. 4 of 32 BGM banks look like counterexamples until this
is applied.
* **Do not time a video whose start you did not see.** An attract cycle sampled
from partway through measured "≈85 s", which was used to argue it was not the
137 s `ADV.wmv`. It was `ADV.wmv`; the sampling began 39 s in. Identify a video
by **content** — match frames against the candidates and check the matched
playhead advances monotonically — before reasoning about its duration.
* **A near-black frame has no signature.** In frame-matching, low-contrast frames
drive every correlation toward zero and the ranking becomes noise; one control
frame scored 0.000 and another tied the wrong movie at 0.98. Read a dark
frame's runner-up as *no evidence*, not as evidence for the runner-up.
* **A screen that never changes is not necessarily hung — it may be a menu.** A
90-second screenshot loop that presses nothing will report a perfectly healthy
`DIFFICULTY` menu as a standing hang, and that reading survived in this corpus
for months. Before calling a static screen a hang, *look at it*, and press
something.
* **Never run `ps -ef` in this container.** Each of the three long-lived
processes carries the whole loop prompt as its argv, so the listing is tens of
kilobytes of the prompt you already have. Use `pgrep -x <name>` or
`ps -o pid=,stat= -C <name>`.
* **A file extension is not a format.** `sound.pak` is full of `.slb` banks and
names a `.xgs` project, and neither is an XACT container: the whole 1.08 GB
archive contains zero `XGSF`, `SDBK` or `WBND` magic, and the executable has no
XACT string at all. The names came from the authoring tool and survived into the
shipped data. Check for the magic before committing to a parser for the format
the extension advertises.
* **A log-polling probe must compare a COUNT, not test for presence.** Timing an
operation by waiting for its "done" line in a log gives an instant false
positive if any earlier run left one there. This produced a confident
"39.5 fps" that was really 0.1 s of matching a stale line — and it disagreed
with the true 28.5 fps by enough to have flipped a conclusion. Snapshot
`log.count(marker)` before starting and wait for it to *increase*.
* **This container kills long emulator runs.** Twice in one session the
`xenia_canary` process vanished with **no crash line in its own log** — once
~50 s into a boot, once ~145 s in at 1.15 GB RSS — while the box sat at
~1 GB free with swap exhausted. Budget dynamic experiments to fit in ~2 minutes
of guest time, take the measurement early, and never read a disappearance as a
guest crash without checking the log tail for one.
* **When a frame-match will not resolve, sample denser before doubting the
hypothesis.** Identifying a movie from 5-second screenshots gave contrast-23
frames, a playhead that would not join up, and a runner-up winning one frame —
it looked like weak evidence for the wrong film. The same question sampled every
0.5 s (`ffmpeg x11grab` at 10 fps) answered at **0.999 with a strictly monotone
playhead**. Sparse sampling does not weaken a signal gracefully; it turns it
into noise that looks like a different answer.
* **Check what instrumentation the local build already has before declaring a
question blocked on tooling.** Two Q8 residuals were written off as needing "an
emulator whose audio path can be observed". The emulator in this container
already had `xma_param_probe` — visible in the startup CONFIG DUMP the whole
time — added by this project for exactly that purpose. Read the cvar dump, or
`nm -C <binary> | grep cvars::`, before concluding you cannot measure something.
* **"Strings a function references" is not "arguments a function passes".** A
loose filter over the strings in a caller produced a tidy-looking screen
vocabulary; recovering the actual register argument at each call site removed
half of it, including two names that appeared to be corroborated by independent
measurement. If the claim is about an argument, simulate the registers — the
cheap proxy manufactures agreement.
* **A backward scan for `addi rN, …` misses arguments staged through another
register.** PPC code often builds a pointer in a scratch register and moves it
into place later (`addi r27, r11, X``mr r5, r27`). A backward search for the
destination register alone found 7 of 48 sites and missed the two known-good
ones. Simulate forward over a window instead, tracking `lis`/`addi`/`mr`.
* **Check a measurement against the WHOLE reference set, not the part you were
reading.** Two observed BGM stream sizes were recorded as matching "no bank's
declared waves", which spawned a wrong theory that the decoder gets a window
rather than a whole wave. They match `BGM_103` exactly — the check had covered
only the `BGM_0xx` rows of the census, because that is the block that had been
on screen.
* **`instructions.function` is unpopulated for most rows in `sylpheed.db`.** A
query keyed on it returns *nothing* rather than erroring — a `bctr` search
scoped that way reported "no jump tables" for a function with two known ones.
Scope by `address between <start> and <end_address>` instead, and gate any such
query on a function whose answer you already know.
* **Before testing "state X behaves like this", check that your route actually
reaches state X.** A test of "does Ⓐ work in phase 4 state 0" was run by
pressing Ⓑ, on the strength of a decoded `4 → state 0` edge — but the same basic
block also writes the *phase* field, sending it to phase 2. The test probed the
wrong state and produced a confident refutation of a live hypothesis. When a
transition sets more than one field, read the whole block, not the one store you
were looking for.
* **A default filter can hide a whole screen, and the corpus will record it as
absent.** `screen list`/`render` filter on `is_build`, which needs a `.rat`
child; the developer splash has none, so it never appeared — and a page ended up
stating it "cannot be rendered at all". It renders fine under `--all`. When two
pages disagree about whether something exists, run the tool both ways before
believing either.
* **A logo that appears twice may be two different things.** The boot shows
`SQUARE ENIX` at ~2 s and again at ~10 s; the first is the splash screen, the
second is the intro movie's own opening frame. Reading the capture alone gives
"three logo screens". Cross-check a repeat against what is *supposed* to be
playing at that moment before counting it as its own screen.
* **Do not hard-code an expected element count into a structure scan.** Looking
for a `.rat` record's keyframes, I required 29 strictly-increasing times because
a header word read `30`. The records hold **three** keyframes in exactly the
layout I was testing for, and the over-constrained filter reported the layout as
refuted. Scan for the *shape* (here: `0x??ffffff` fade words at a constant
stride) and let the count fall out.
* **Read the target text before writing a patch pattern for it.** Two iterations
running, a scripted edit to `HANDOFF.md` failed its assertion because the
paragraph had been rewrapped since the pattern was written — and both times the
finding landed in `docs/re/` while the page the port agent reads kept the stale
version. The commit's file count is the tell: if it is one short, an edit was
silently dropped.
* **Two renderers agreeing is not evidence the field is right.** The port's
composite and `sylpheed-cli screen render` matched on `main_menu` to 3/255 —
and *both* omitted two elements the game draws, because both read one field
through one decoder. Agreement between consumers of the same decode measures
the decode's determinism, not its truth. Only a framebuffer capture is an
independent check. (Reported by the port agent, 2026-08-28, after a capture
crop found the missing bracket.)
* **`./target/debug/<bin>` is STALE in this container — `CARGO_TARGET_DIR` is
redirected.** The Dockerfile sets `CARGO_TARGET_DIR=/sylph-home/re/target-container`
so a fresh `cargo build` writes there, while `./target/debug/sylpheed-cli`
remains whatever was built on the host, hours old. A decoder fix was verified
against the stale binary and produced byte-identical "before" and "after"
renders — which reads exactly like "the change had no effect" rather than "you
ran the old code". Run the binary cargo actually wrote, or `cargo run`.
* **A pivot-equality test conflates rounding with real breakage.** Checking
`pivot == texture/2` over `GP_TITLE` flagged 74 % as mismatched, which reads as
a systemic decode failure. Split by magnitude it is 17 exact, 54 off by ≤ 1 px
(odd dimensions rounding), and only **24 off by > 8 px** — and those 24 are the
real finding, concentrated on one element family. Bucket a mismatch by size
before reporting a rate.
* **I have now broken the "read the target text first" rule three times.** Each
time a scripted `HANDOFF.md` edit asserted on a paragraph that had been rewrapped
by an earlier edit in the same session, and each time the finding landed in
`docs/re/` while the port's page kept the stale text. Writing the rule down was
not enough; the working fix is mechanical — `grep`/`sed -n` the target paragraph
in the same command that patches it, and check the commit's file count.
* **"The nearest header after the name" is not a name→header mapping.** Looking
for each sprite's `T8aD` header by searching for its name and taking the next
`T8aD` returned *the same header for every sprite* — all the names live together
in the declaration table near the top of the bundle, so "next" is always the
first one in the file. It failed its own control instantly: different sprites
reported identical dimensions. Match on something the header itself carries
(here width × height against the decoded PNGs) rather than on proximity.
* **`cargo build` passing does not mean `cargo test` compiles.** Adding a field to
`T8adImage` built the library fine in 1.48 s — and broke two *test-only* struct
literals, so `cargo test` failed to compile with exit 101. A green build on the
binary is not a gate for a struct change; run the tests before believing a
data-structure edit is safe.
* **Detach long background work with `setsid`, or a tool-call timeout kills it.**
Three attempts to run the disc-gated test suite in the background died at exit
143 with an empty log — the timeout's SIGTERM reached the whole process group.
`setsid nohup cargo test … &` survives, and the run then completes across later
tool calls. An empty log from a "background" job usually means it was killed,
not that it produced nothing.
* **Validate a recommendation before leaving it as advice.** Having concluded a
question was undecodable from the disc, I named a per-draw GPU capture as the
next route "because it reads the blend state". It does not — checking the
capture's own source showed it records textures and vertex attributes and no
blend registers. A named next step is a claim like any other.
* **Name the element from the draw's own coordinates before building on it.** A
skewed draw in a title capture was called "the swoosh" because the swoosh is the
screen's only diagonal element — an identification by elimination, never checked
against the draw's NDC extent. It was wrong: the quads span the full screen and
beyond, the swoosh is a 234-pixel band. Five iterations of analysis pointed at
the wrong element on the strength of it. Converting the coordinates takes one
line and should come first.
* **A census over a bundle's top-level table is not a census over the bundle.**
The keyframe rotation field read "always zero on this screen" for several
iterations because every scan walked `GP_TITLE` build 4's **declaration
table**, and the rotated elements are **nested leaf records** reached through
an `opt ` link. The bytes were right; the *reach* was wrong, and a negative
stated without its reach reads like a fact about the disc. Say which region a
negative covers, and check whether the thing you are looking for lives outside
it.
* **Do not assume 4-byte alignment when scanning raw bundle bytes.** A nested
`RATC` blob starts wherever the parent's chunk stream leaves it — `ptloop01.rat`
sits at `0xbb5966` — so its 40-byte keyframe blocks are odd-aligned. A scanner
that filtered candidates on `%4 == 0` found **0/3** of its own control blocks
and under-counted the corpus by **16 341** blocks, all of them nested. It cost
nothing to catch, because the control was in the script.
* **`pkill -f <pattern>` matches the shell running it.** `pkill -f kfscan.py`
from a `bash -c` whose command line contains `kfscan.py` kills its own shell:
the tool call returns exit 143/144 and the edit that was queued behind it never
runs. The same trap makes `pgrep -f` self-report — a "still running? yes" that
was the shell seeing itself, on a job that had already finished. Use
`pgrep -x`, or match on a path the current command line does not contain.
(This is the second `ps`/`pgrep` entry in this file; the first is about
`ps -ef` dumping the loop prompt.)
* **Grep a legend and you count the legend.** A sweep for elements with a
rotation reported "1 element" in every build on the disc — the header line
`a=alpha r=rot°` matched the ` r=` pattern. A uniform count across
heterogeneous inputs is the tell. Make the pattern require the *value*
(` r=-?[0-9]+`), and sanity-check that a known-negative build reports zero.
* **A pattern over the archives you happened to check is not a negative.** I
wrote "rotation appears to live only in nested leaf records", correctly scoped
to the three paks I had swept — and the sweep refuted it on the fourth, within
the hour, because the alphabet had not reached `GP_DIALOG` yet. Scoping the
claim was what made it cheap to withdraw, but the better move is to let the
sweep finish before writing the sentence at all: a negative that is still
running is not a finding, it is a prediction.
* **A field that is implemented is not a field that is measured.** The
pivot-anchored scale term had been in `blit` for months and reasoned about in
the handoff, and no capture had ever exercised it, because every element anyone
had looked at was at 100 % scale where the term is identically zero. Look for
the case that makes a term *large* — here 600 % and 800 % scale, worth 450 px —
and check it there. A term you cannot distinguish from zero has not been
verified by any amount of agreement.
* **Printing a series' minimum and reading it as its range.** I summarised a
captured alpha series as "constant α ≈ 33" and built a contradiction on it —
the summary printed `min_alpha` and no maximum, and the series actually ramps
34 → 255 → 33. The tell was there in the same table: the column beside it said
*14 distinct colours*, which a constant series cannot have. When a summary
statistic and a distinct-value count disagree, the summary is wrong.
* **Calibrate on one element, test on another.** Fitting a declared ramp to a
capture has two free parameters (rate and offset) and will "succeed" against
almost anything — my first attempt scored RMS 128/255 and I nearly read the
numbers rather than noticing the search could not reach the ramp at all. The
version that means something: fix the clock from element A's ramp, check that
fix against A's own next landmark, then apply it to element B in the same
frames with **nothing left to tune**. That is what turned "the shapes look
different" into "still at 255 nine frames after it should read 32".
* **Two measurements can both be right and still disagree, when a heuristic sits
between them.** A change to the keyframe time association is favoured 26× by an
animation measurement and rejected by a static-render comparison. Both are
sound: `rest()` picks a pose by *longest dwell*, which is a heuristic layered on
top of the times, so moving the times moves its tie-breaks. The mistake would
be to treat one as refuting the other and drop the loser. Name what each
measurement actually constrains — here *timing* versus *pose selection* — before
deciding they conflict.
* **Before believing a render diff refutes a decode, find out which element moved.**
I reported a 13 % render change as evidence against a keyframe-time hypothesis,
on a brightness argument about language twins. One element accounted for all of
it, and that element is a transient flash with **no resting pose** — so the
difference was our own `rest()` heuristic guessing, and it would have guessed
under either reading. A whole-image statistic (pixels changed, mean luminance,
correlation) tells you *that* something moved, never *what*, and an argument
built on it can be entirely about your own code. Localise the diff to an
element and read its keyframes first.
* **A fallback that only runs when its precondition fails is not a fallback.**
`rest()` picks the longest-dwell keyframe when no plateau exists — but a dwell
gap is time spent *moving between* two poses, and the only case where an
endpoint is genuinely held is when the two poses are equal, which is exactly
the plateau the first path already caught. So the fallback is guaranteed to be
guessing every time it is reached. When a heuristic's justification is the same
condition that routes around it, it has no justification.
* **Siblings adjudicate a rule that a single element cannot.** Deciding what
`rest()` should return for an element with no held pose looked like it needed a
capture we do not have. It did not: the developer splash carries three glows
with identical structure and identical times, differing in one alpha byte, and
any rule that makes one of them behave completely differently from the other
two is producing an artefact. Parallel constructions in the same bundle are a
free control — look for them before concluding a question needs new measurement.
* **Backgrounding with `&` in a compound command loses the working directory.**
`cd X && cmd &` followed by more work in the same call left the shell reset, and
a heredoc two lines later wrote nowhere — while the `echo` after it still
printed "written". A success message after a failed redirect is not a success.
Use `(setsid nohup … &)` and keep the file-writing in its own call.
* **Read the whole function before describing what it does.** I wrote "a
zero-scale pose paints a 1-pixel sliver" into a tool's docstring, from the
`.max(1)` in the size arithmetic. Two lines above it, a guard coerced
`scale == 0` to **100 %**, so the `.max(1)` never saw a zero and the element
rendered at *full size* — a much larger bug than the one I described. I had
read the lines I went looking for and stopped.
* **Never diff a file a background job might still be writing.** A comparison
loop over an output directory reported `GP_TITLE` build 6 as changed; the pixel
diff was then all zeros, because `cmp` had read a half-written PNG. Compare
only after the job signals done, or skip the newest file. A "difference" that
vanishes on re-examination is usually a race, not a flake.
* **A `screen list` BUILD index is not a pak ENTRY index.** `GP_TITLE` happens to
map 1:1 (16 builds, entries 015), which is how the assumption survived;
`GP_DIALOG` does not — its build 0 is **entry 2**. Indexing a pak directly with
a build number silently reads a different bundle and reports a confident wrong
answer: a census control asked for the two rotated elements of `GP_DIALOG`
build 0 and got zero, from a bundle that genuinely has none. `screen list`
prints the mapping; use it rather than assuming identity.
* **"There is no flag" is not "there is no way".** I declared a Japanese-locale
capture impossible in this container because `user_language` is `DECLARE`d and
never `DEFINE`d, so no cvar exists to pass. That was true and not the question.
The language is *persisted*: canary builds `XConfig` over
`<storage_root>/xconfig.settings`, `SetDefaults()` only supplies the value when
the file has none, and the file is writable. One grep further — for where the
setting is **stored** rather than where it is **configured** — turned a
"blocked, needs a human decision" into a two-line edit. When you conclude a
capability is absent, check the persistence layer before writing it up.
* **Locate a binary field from struct landmarks, not from a hard-coded offset.**
`user.language` was found by scanning for `music_volume`'s `0.7f` (a known
default at a known relative offset), then cross-checking `language == 1` and
`country == 103` against the emulator's own `SetDefaults()`. Three agreeing
landmarks, no offset assumed, and the check re-runs every invocation — so the
tool fails loudly if the layout moves instead of silently patching a
neighbouring field.
* **A stale oracle reports the world, not itself.** A 787 s run concluded "the
game never reached the title". It had, repeatedly: `wait_title.sh` was still
sampling the single pixel (625,618) that `is_title.py` had already been written
to replace — a 1280×720 coordinate against a 1279×675 game surface, always
reading the copyright line. The replacement existed *in the same directory*,
with a docstring naming the bug. Before believing a negative from a probe,
check whether something in the toolkit already supersedes it, and run the probe
against a known-positive capture first — `is_title.py` scores 753 on a real
title frame and 327 on the main menu, which takes one command to confirm.
* **Run the control before theorising about the difference.** A Japanese-locale
run failed to reach the interactive title, and I wrote up the open question as
"if English reaches it, the difference is the locale". English does not reach
it either — 75 samples over 734 s, every one a miss. The locale had nothing to
do with it, and two iterations framed the problem around a variable that was
never implicated. The control cost one run and would have cost the same one
iteration earlier.
* **Log every sample, not just the hit.** The first probe printed nothing until
it succeeded, so a 787 s failure was indistinguishable from a probe that never
ran. Printing the oracle's value each tick turns "no title" into "75 samples,
max glyph 0", which is a measurement — and it is what showed the black-screen
and attract-movie phases were being sampled correctly all along.
* **Know when to stop paying for a non-blocking answer.** A capture that would
settle two 🟡 residuals — neither of which blocks the deliverable — absorbed
five iterations. Each one produced a real finding (a broken oracle, a wrong
"impossible", a locale red herring), so none was wasted, but the *question*
never moved. The signal to stop is not "this is hard", it is "the thing this
unblocks is not blocking anything". Write down the recipe you built and leave
it for a session with a reason to spend the time.
* **Audit what a renderer silently SKIPS, not only what it draws.** `compose` has
half a dozen `continue` paths, and a sprite-name miss is one of them — an
element vanishes with no error, which is exactly how the port agent's missing
bracket happened. `screen render` already prints its omissions; reading that
list for the five screens that matter took one command and turned up a
full-screen opaque element dropped on three of them. Any composer with skip
conditions should be asked to name what it skipped before it is trusted.
* **An element's declared name is not necessarily its texture's name.** The
`opt ` link on `pteff05.t32` leads to a `T8aD` registered as `8AX`, so a
lookup by declared name misses. Before concluding a texture is absent, look at
the bytes the link actually points at — the name in the declaration table and
the name the sprite table keys on can differ.
* **A fit whose residual is large for every model is a broken method, not a
close call.** Comparing our composite to a capture pixel-wise gave mean abs
errors of 1014 for gamma, for a linear scale, for everything — and a
non-monotonic transfer curve (render 96127 mapping *brighter* than render
128159). The cause was edge misalignment: at correlation 0.947 a bright pixel
in one image routinely lands on a dark one in the other. Restricting to patches
that are flat in **both** images dropped the residual to 0.21.1. When every
candidate model fits badly, stop choosing between them and look at what the
comparison is actually measuring.
* **Say when a control failed to discriminate, rather than reporting it as a
pass.** The held-out screen for a tone-curve fit was the developer splash,
whose flat regions are pure black — every model scored ≈ 0.00 error there. That
is not corroboration; it is a test with no power, and reporting the 0.00 as
agreement would have dressed an untested claim as a verified one.
* **Time your probe against the thing you are probing for.** Four runs concluded
"the game never reaches the title". `screenshot` costs **10.8 s while xenia is
running** and **0.117 s once it is killed** — 92× — so a two-grab polling loop
samples every ~41 s, against a title screen this corpus documents as lasting a
few seconds. The harness was blinking slower than the event. Before believing a
negative from a polling loop, measure its interval and compare it to the
duration of what you are waiting for; and measure the probe's cost *under the
same load as the run*, because idle timing here was off by two orders of
magnitude.
* **Do not change a display setting and a capture behaviour in the same run.**
`kernel_display_gamma_type = 0` brightens the frame, and `skip_intro.sh`
classifies movie-vs-static on an *absolute* rmse threshold — so the gamma
change biased the very classifier the run depended on. Harness thresholds tuned
on one output configuration are not portable to another.
* **Fixing the instrument is how you test the explanation that blamed it.** I
diagnosed four failed runs as "the probe samples slower than the event",
wrote it up, and withdrew three earlier conclusions on that basis. Building the
faster probe refuted it: at 100× the sampling density, over 420 unbroken
seconds, the event still never occurred. The diagnosis was a real defect that
happened not to be the cause — the most seductive kind, because fixing it feels
like confirmation. A plausible mechanism that explains a failure is a
hypothesis, and the fix is its experiment, not its proof.
* **Ask whether the screen is drawing anything before explaining why a feature of
it is missing.** Four iterations produced four explanations for "the title is
not detected" — a stale oracle, the locale, the sampling rate, a one-shot boot
window — and three of them were refuted in turn. Splitting a raw root grab into
bands then showed the game surface at **0.08 % non-black, mean 0.07**: it was
rendering black. The check costs one command and belongs at the *front* of that
sequence, not after the fourth hypothesis. A detector reporting absence is
ambiguous between "the feature is not there" and "nothing is there".
* **A newly found fault does not retroactively explain older failures.** The
black surface is real and reproducible *in the run where it was measured*.
Earlier runs in the same session had measurable content (mean 33.1, mean 122.8,
non-zero frame-to-frame rmse), so the failure mode changed over the session.
Reaching back to re-explain earlier results with the newest cause would have
been the fourth over-reach in a row.
* **Validating a probe on static test images tests its ANALYSIS, not its
ACQUISITION.** The fast probe's counter was controlled against committed
captures and matched exactly (753 / 327), which proved the numpy expression
right and the frame source untested. The source was the broken half: a
long-lived x11grab stream degrades from 3.98 to 1.60 fps and then freezes,
repeating one stale frame. The check that catches it is to read the same screen
with an **independent grabber at the same moment**`import` said 125.65 where
the stream said 5.21. Do that during a run, not only at design time.
* **A dense negative from one instrument is worth less than one cross-check.**
"2 391 frames, zero hits" reads as overwhelming and collapses to nothing once
the frames may all be the same frame. Sample count is not evidence of coverage
unless the samples are known to be independent.
* **A probe that cross-checks itself turns "no result" into a result.** Three
successive "the title never appears" claims were withdrawn because the
instrument was broken each time. The version that finally held prints its own
reading beside an independent grabber's every 60 s — 9 checks, 8 agreeing to
within 2 % — so the negative arrives with its own evidence that the frames were
live. Building the cross-check into the tool costs a few lines and is what
separates a measurement from a silence.
* **A hedge in a code comment is an unmeasured claim.** `compose` said the
derived paint order "reproduces both measured orders up to ties" — a sentence
that sounds like a result and was neither measured nor kept current: there were
three measured orders by then, not two. Measuring it took one example program
and turned a hedge into a bounded number (exact on 4 of 5 bundles; the 5th off
by 8 pairs, all ties). Grep your own comments for "up to", "roughly",
"essentially" — each one is a claim nobody has checked.
* **Count the cases that can actually bite, not the cases that match the
pattern.** `EXTRAS` has 15 tied pairs in its derived paint order, which reads
alarming. Only **2** of them overlap, and a tie between non-overlapping
elements paints identically either way. Reporting 15 would have overstated the
risk by 7×; the useful number is the one filtered by whether the difference can
reach a pixel.
* **An exhaustive field search needs a positive control, or "found nothing" is
worthless.** Scanning a header for a field that reproduces a measured ordering
returned zero hits — which could equally mean the field is absent or the scan
is broken. Running the same scan against a *known* ordering (declaration order)
returned **64** hits, proving the scan finds ordering fields when they exist.
Only then is the zero a finding. The control costs four extra lines and turns a
silence into a negative with reach.
* **A stale row on the handoff page is worse than a missing one.** The port was
told SE audio was "undecodable from the disc" while the linked doc had already
retracted that and located three cues decoding to PCM. The finding landed in
`docs/re/` and the page the port reads kept the superseded text — the fourth
time in this corpus. Writing the rule down has not worked, so there is now a
tool: `tools/re-capture/handoff_lint.py` flags every HANDOFF line that makes a
strong negative claim and links a doc containing retraction language. It found
this row, and one benign false positive, on its first run.
* **Narrow a risk before reporting it, then narrow it again.** "15 tied pairs on
`EXTRAS`" became 2 (only overlapping ties can paint differently), then 1 (one
of those two is a `loop*` animation the compositor skips), then "consistent
with the capture". Each step is a filter that costs one query and changes the
number by more than an order of magnitude in total. A raw count is almost never
the number a consumer needs.
* **Nothing was checking that the docs' cited evidence exists.** A sweep of every
relative link under `docs/` found **16 broken**, and two of them were the
figures backing the UI layout decode's headline claim — the port's foundation,
unreachable from its own page, because a path in `structures/` was written one
directory too shallow. Eleven were wrong relative depth with the target present;
five name files that do not exist. Evidence that cannot be opened is not
evidence, and a link is exactly the kind of thing no one re-reads.
`tools/re-capture/doc_link_check.py` now does it, and also flags targets that
resolve to a **zero-byte** file — which looks correct in every listing.
* **Repair in bulk only when the counts pair.** The fixer rewrote 11 links; the
checker went from 1 038 resolving / 16 missing to 1 049 / 5. +11 and 11
against 11 edits is the confirmation that the pass did what it said and touched
nothing else. A bulk edit without that arithmetic is a hope.
* **When two candidates carry the same content, compare their DIFFERENCE against
the oracle, not themselves.** `8AX` (1280×720) and `ptbase` (640×360 at 200 %)
are the same artwork, so no comparison of either against a capture can separate
them — every such test had been read as "inconclusive, needs a per-draw
capture". What separates them is `8AX upscale(ptbase)`: the detail only one
of them has. Correlating the capture's residual against *that* answered it
statically, on two screens, with matched controls.
* **A ratio that saturates at 1 under noise is not a discriminator.** A pixel-pair
test cleanly separated upscales (0.000.72) from native (0.98) and put the
capture at 1.01 — apparently decisive. Additive noise raises both terms of the
ratio equally and drives *any* value toward 1, and fitting a noise term showed
both hypotheses reproduce the observed numbers. Before believing a ratio, ask
what it does as noise grows.
* **Read what a cvar DOES before building an experiment around it.** I planned a
run to decide whether a measured gamma was "canary's BT.709 output stage",
by setting `kernel_display_gamma_type = 0` and re-fitting. The cvar is not an
output stage: it is the value a `kStub` **getter returns to the guest**, which
the game uses to build its own ramp, and canary applies *that* ramp in the swap
path. The experiment could never have isolated a stage that does not exist —
and it would have perturbed the capture harness as a side effect. One grep for
the cvar's definition replaced a planned emulator run with a better one.
* **An absence in a log is only evidence if the log would have shown it.** No
gamma lines appear in any run log here — but kernel exports log at Debug and
this harness masks Kernel logging entirely (`log_mask = 13`), so their absence
is guaranteed regardless of what the game did. Check the logging configuration
before reading silence as a result.
* **Check what an experiment actually needs before filing it behind a blocker.**
The gamma question sat parked behind "needs the emulator to reach a menu" for
several iterations. It needed the emulator only to **boot** — video init, and
the `VdGetCurrentDisplayGamma` call, happen in the first seconds, long before
any title screen. A blocker that stops one experiment does not stop every
experiment in the same area, and it is worth re-reading the parked list against
what each item really requires rather than against the area it belongs to.
* **A default value is evidence.** Whether the game writes a gamma ramp looked
like it needed a GPU trace. It mostly did not: canary initialises the ramp
table to **identity** and applies it through a shader that is a pure LUT lookup
with no other transfer. An unwritten ramp is therefore a no-op, and any
non-identity transfer in the output implies a write. Reading what a field holds
when nobody has touched it turns "I cannot observe the write" into "the write
must have happened" — cheaper than instrumenting, though it stays an inference
and should be labelled one.
* **Name the weak joint of an inference in the same breath as the conclusion.**
The chain above assumes our composite reproduces the pre-ramp framebuffer,
which is the one step that could be wrong. Writing that down beside the
conclusion — rather than only the supporting facts — is what lets a later
reader attack it instead of inheriting it.
* **The startup config dump is the config FILE, not the effective flags.** A run
passed `--log_mask=12 --log_level=3` and its dump printed `log_mask = 0,
log_level = 2` — while Kernel Debug logging was demonstrably on, which is how
that run's finding was obtained. So a dump can neither confirm nor refute a
command-line override, and reading one as "my flag was ignored" is a mistake I
nearly made with `trace_gpu_stream`. Verify a flag by its *effect*, not by the
dump. (This does not undo the earlier `user_language` conclusion: absence of a
*name* from the dump still shows the cvar is unregistered.)
* **`kill -9` destroys anything that finalises on shutdown.** Canary starts a GPU
trace at init but only closes it in `GraphicsSystem::Shutdown()`, so the hard
kills this session used routinely could never have produced a trace. Before
concluding a feature is broken, check whether the way you stop the program is
what discards its output.
* **Bound a risky experiment from inside, not by watching it.** A boot-time GPU
trace on a disk at 95 % could have filled it between two tool calls. The runner
carried its own watchdog that killed the emulator the moment the output passed
a 2 GiB cap, so the experiment was safe regardless of how coarsely I polled.
The watchdog never fired, which is the point — it cost nothing and removed the
need to gamble on timing.
* **A cvar existing does not mean the feature is compiled in.** `trace_gpu_stream`
parses, is documented, sets state, and does nothing: the code that opens the
trace sits behind `#if XE_ENABLE_TRACE_WRITER_INSTRUMENTATION == 1`, which
`trace_writer.h` ties to `#ifdef NDEBUG` — off in release. Two runs and two
wrong hypotheses before reading the `#if`. When a switch produces no effect,
follow the code from the switch to the output and look for a compile-time gate
before theorising about runtime causes.
* **Test a compile-time gate against the binary, with a control.** A string that
exists only inside the guarded block (`_stream.xtr`) settles it in one command:
0 occurrences in the release binary, **1** in the debug binary. The debug build
is the control that proves the test can find the string when it is there —
without it, "0 occurrences" is just as consistent with a bad grep.
* **`T8aD` headers sit in RATC child order — use that, not the size, to name
one.** Attributing a sprite header to a sprite name by matching decoded
dimensions fails whenever two sprites share a size, and `GP_TITLE` build 4 has
such a pair (`ptlogo_back2eff` / `ptlogo_back2eff5`, both 1133×280) that differ
in exactly the flag being studied. Header order matches child order — verified
18/18 on that build against the dimensions as an independent check — so
ordering resolves what size cannot.
* **Identical summary statistics are not identical data.** Two same-sized sprites
reported the same %opaque, %clear and mean alpha to one decimal, which reads as
"the bundle stores this texture twice". Pixel-comparing them gives max abs diff
**21**: two different renditions of one image. Three matching summaries over
300 000 pixels is weak evidence of identity; the comparison costs one line.
* **A pattern that is perfect on one screen can be near-chance on the disc.** The
`0x02` bit matched `eff` names 10/10 on `GP_TITLE` build 4, which is exactly
the sample size that makes a rule feel found. Disc-wide it holds 46.8 % of the
time — the build's artists simply named their effect sprites consistently. An
18-element bundle cannot distinguish a format rule from a local naming habit;
before reporting an implication, run it over the corpus, not the example that
suggested it.
* **When an association survives a refuted implication, the counterexamples are
the finding.** The bit is 3.3× enriched for `eff` names and the sprites that
break the rule are `pv_loading_ring0`, `pv_loading_light0``3`,
`px_bunk_line` — rings, glows, lights, thin lines. That the exceptions are all
effect-like artwork *without* the naming convention says more about the field
than the rule it broke did.
* **Report a classifier's lift over its base rate, not its accuracy.** The
`0x02` bit's association with bright-RGB/low-alpha art gives a best single
threshold of 76.5 % accuracy — which sounds like a finding until you notice
that always guessing the majority class scores 64.1 %. The lift is 12 points,
the distributions overlap, and it is a tendency rather than a rule. An accuracy
quoted without its base rate is not interpretable.
* **Park a field after N failed hypotheses, and say what was eliminated.** Four
candidate meanings for one flag bit were each refuted at a cost of roughly an
iteration apiece, and the bit blocks nothing measurable. Stopping is the right
call, but only if the negative space is written down — additive blend, name,
lifetime, premultiplied alpha — so the next attempt starts where this one ended
rather than at the beginning.
* **Nothing was checking that a doc's figures match its committed data.** A
number is written once from a run and then lives in prose that gets edited
around it; the data file beside it is regenerated independently. All 19
headline figures across four censuses do currently agree
(`tools/re-capture/doc_figure_check.py`), which is worth knowing rather than
assuming — but the checker had to be written **numerically**, because the first
version grepped for the doc's formatting (`14 709` with a thin space, `33.7`
rounded from `33.66`) and reported almost every figure as a mismatch. A
consistency check that fails on formatting will train you to ignore it.
* **A detached job you never check can outlive many iterations.** Two
`setsid nohup cargo test … &` runs from earlier iterations were still alive
**four hours** later, one child at 89 % CPU for 3 h 26 m, holding the load
average at 14 on 12 cores. `setsid` was added precisely so a tool-call timeout
could not kill them — which also means nothing kills them. Check that a
backgrounded run actually exited before launching the next one, and prefer
`ps -o etime=` over assuming.
* **Know whether your verification gate can terminate.** `build-reborn test`
includes a disc test that decodes 166 `.xpr` files (1.4 GB) and had not
finished after 3 h 26 m of CPU. Every "green run" from it is partial unless the
suite is stated to have terminated — so report the suite count and elapsed
state rather than the word "green", which is what I should have been doing all
session.
* **"Cannot finish" and "takes an hour" are different claims — measure before
choosing.** I wrote that the verification gate *cannot terminate*, from having
watched it run 3 h 26 m. Timing the work directly: 19 of 166 containers exceed
25 s, `Stage_S02` completes in **144 s**, and one full pass is ~4560 minutes.
Nothing hangs. The 3 h 26 m was that hour of work at a load average of 914,
inflated by my own duplicate runs. A slow thing observed under contention looks
like a stuck thing, and the correction matters: an hour-scale gate can be run
deliberately, a hung one cannot be run at all.
* **Separate the question with evidence from the question tangled with it.** The
keyframe-time reading was held back for several iterations by one objection:
adopting it changes `rest()` on a single element. But `rest()` reaches the times
only through a fallback that is unsound whenever it runs, and the shift does not
fix that fallback either — so the objection was never about the times. Timing
had three discriminating measurements; pose-selection had a heuristic guessing.
When a conclusion is blocked, check whether the blocker is actually evidence
about the same thing.
* **A predicate over adjacent PAIRS silently misclassifies a one-element list.**
"Has a plateau" was implemented as *any two adjacent keyframes share a pose*
which is false for a single-keyframe element, so 1 502 static elements were
counted as having a *guessed* rest pose and the published defect rate was 65 %
too high. The error only surfaced when acting on it: suppressing those elements
dropped the title's correlation by 0.27, because they include the backgrounds.
Whenever a rule quantifies over pairs, ask what it says about a list of one.
* **Acting on a claim is a better test of it than re-reading it.** The
single-keyframe flaw survived a disc-wide census, a write-up and a handoff row.
It died the moment the rule was used to change a rendering, because the result
was visibly worse. If a measurement implies an action, take the action on
something you can score.
* **Score a rule where it can actually differ, or you will measure nothing.**
Three rest-pose rules rendered builds 4, 5 and 6 to *identical* correlations —
not because they agree, but because the code they change is unreachable on
those screens. The signal was on the two splashes, the only builds whose
elements reach the fallback at all. Identical results across variants is a
clue that the variant is not being exercised, not evidence that the choice does
not matter.
* **An argument from symmetry is a prediction, not a refutation.** I killed the
last-keyframe rule because it treats one of three sibling glows differently,
which felt like an artefact. Measured, it is the better rule on both screens
where it applies. Aesthetic expectations about how authored data "should" look
are worth stating as hypotheses and worth nothing as verdicts.
* **A blank render is a correlation of NaN, not a low score — notice which.**
Applying "rest = last keyframe" to every element made both splashes render with
zero variance, so the correlation was undefined rather than poor. Reading the
NaN as "the tool failed" would have hidden the result; it *was* the result, and
the strongest form of it. Check for degenerate output before treating a missing
number as a broken measurement.
* **When a model predicts something and the measurement refuses, the model is
usually incomplete rather than wrong.** The shifted time reading says when each
pose is reached, which looked like it implied the final pose is the resting
one. It does not: it says nothing about whether the group is *played to
completion* while the screen is still up. The exit is the dismissal, so a
displayed screen never reaches its last keyframe. Both claims survive; what was
wrong was the step between them that I supplied.
* **The control is what turns a helpful edit into a finding.** Suppressing the
`_eff` glows raised both splashes from ≈0.96 to ≈0.998 — on its own that reads
as "we over-draw glows". Running the same edit on the three screens that settle
made them **worse** (0.002, 0.092, 0.107), which is what makes the result
mean something specific: the over-draw is confined to transient screens, where
a plateau mid-animation is not evidence the element is on screen at rest. An
edit that improves one set of cases is only interesting once you have shown it
damages the cases where it should.
* **When a model reproduces durations but not positions, the missing piece is an
origin, not a rate.** Playing the splash timeline matched every element's
on-screen *length* to within 2 % while placing the elements in the wrong part
of the run. That pattern names the gap precisely — the rate and the shape are
right, so what is absent is a per-group start — and it pointed straight at the
one undecoded word in the group header. (Which was zero, but the diagnosis was
still what made the next step obvious rather than a search.)
* **Two elements with identical data and different outcomes is the strongest
possible evidence that the decision is elsewhere.** `palogo_anima` and
`palogo_gamearts` carry byte-identical keyframe times; one is drawn for 95
frames and the other not at all. No amount of re-reading the placement data can
explain that, and the pair is worth more than a survey — when a hypothesis says
"the data decides", look for two records that agree and behave differently.
* **A shared resource address does not identify the resource's owner.** The draw
log's `tex[base=…]` looked like it would say whether two animation phases came
from the same bundle. All three splash phases report `0x11C30000` — including
the publisher splash, which is certainly a *different* bundle. The address is a
reused upload slot. The control was free and sitting in the same table; without
it the shared base would have read as proof of a shared bundle.
* **State the mechanism as a separate claim from the observation.** "Declared
elements are not what gets drawn" is measured and holds. "Because one bundle
activates its elements selectively" was an explanation I attached to it, and it
is not established. Bundling the two let the weaker claim inherit the stronger
one's evidence.
* **Ask what the competing hypothesis would REQUIRE on the disc.** Two
explanations for the splash's disjoint phases looked separable only by a
capture I could not take, and the capture-side test failed its control. The
alternative needed a bundle declaring the glows without the logos — a
four-entry enumeration showed none exists, and the question closed statically.
A hypothesis that predicts an artefact can be killed by looking for the
artefact, which is often far cheaper than measuring the behaviour.
* **Check what a "measured" value was measured *from* before reasoning about
its limits.** Two things that never co-occur have no observable relative order
— true, and a real limit on any order read from a *draw capture*. I applied it
to the splash's paint order and withdrew it hours later: that vector is a read
of the runtime **child array**, which has a definite order whether or not its
children are ever drawn together, and the two halves in question also carry
distinct static layer keys. Both the provenance and an independent static field
said the order was real. The general trap is not the co-occurrence rule, it is
applying a source-specific limit to a value from a different source: the word
"measured" covers several kinds of evidence, and the page that recorded it
usually says which — `ui-screen-runtime.md` said "child slots" in as many
words. Read that line before building an argument on top of it.
* **A heuristic that is right 99.9 % of the time still has a shape to its
failures — find it before trusting the field.** RATC child names were read by
scanning backwards for the last printable run of bytes. That agrees with the
format's own `opt ` declaration on 17 918 of 17 942 children, which is the kind
of agreement that stops people looking. The 24 exceptions were not random: all
24 are the *same* case, a 3-byte binary tail that happens to be printable ASCII
(`8AX`), and one of them was the full-resolution background of every menu
screen we care about. Ask what the format *states* before settling for what a
scan *infers*, especially when the stated version is already decoded elsewhere
in the same file — `opt ` was being read for button focus links the whole time.
* **A `continue` that silently skips is a defect even when the skip is correct.**
`compose` drops an element whose sprite does not resolve. Two arms above it
record the name into `missing` first; the `el.sprite.is_none()` arm does not.
So a screen lost its background and `screen render` still reported "sprites
that did not resolve: none" — the diagnostic was structurally unable to see it.
When adding an early-out to a loop that already reports what it discards, make
it report through the same channel, or it becomes a place findings go to die.
* **"It has no name" can mean "it is not a thing that gets named."** Sixty RATC
children had no `opt ` name block and the open question was whether the block
was absent or merely outside our search window. It was neither: the sixty are
*frames*, ten each of six copies of one `.tan` animation, and one `opt ` block
names the whole run. The give-away was in the data before any hypothesis was —
the distances back to the nearest tag were an exact arithmetic progression
(`213 + n·60600`), i.e. ten different records finding the *same* tag. When a
negative result's measurements come out evenly spaced, the thing you are
counting is probably not the thing the format counts.
* **This container OOM-kills `slb_leading_segment_disc` under default test
parallelism.** It dies with `signal: 9, SIGKILL` and no assertion — eight
threads each holding a slice of a ~1.1 GB bank. It is not a regression and not
a flake, and it reproduces when run alone. `-- --test-threads=1` passes 8/8 in
20 s. Before believing a SIGKILL in this repo, re-run the suite serially;
before believing a *pass*, check nothing else heavy was sharing the box.
⚠️ And budget for `mesh_consistency_disc`: it takes **22 minutes** (1 318 s
measured, serial) and produces no output while it runs, so `build-reborn test`
looks hung for a third of an hour. It is not. Two runs were killed for looking
stuck before it was timed. If the change under test is not in the mesh path,
`build-reborn t -p <crate>` scopes around it — note that `build-reborn test`
itself passes `--workspace` and will ignore a `-p`.
* **Never measure a file another process is still writing.** `ffprobe` on a
half-written transcode reported 33 s against a 137 s source — the shape of
catastrophic truncation, with no error and no warning, and it nearly became a
filed bug. The tell was the mtime and a climbing packet count, not anything in
the tool's output. Before comparing an artifact you did not produce, check that
whoever produces it has finished; across two agents sharing a repo this is a
race, not an edge case.
* **A difference-signal RMS means nothing until the two are aligned.** Comparing a
transcode against its source gave source RMS 25.3 dB and difference RMS
34.2 dB — only ~9 dB down, which looks like a failure. It is inconclusive: a
one-sample offset makes the difference nearly as loud as the source. Any such
test needs cross-correlation to align first and an agreed downmix, and only then
is a pass mark like ">40 dB down" meaningful. Reporting the 9 dB as a result
would have been a confident wrong number.

View File

@@ -35,6 +35,159 @@ neighbourhood, not just the line.
## Screens, classes and RTTI
* "the rotated draw in the title's capture is the `Z` swoosh
(`ptlogo_back2*`)" → **mine, and refuted.** Its quads span y 209…925 and
292…1012 in screen space; the swoosh is a band at y 126…360.
[`ui-title-build-map.md`](ui-title-build-map.md)
* "the keyframe words at `+4`/`+8`/`+12` are always zero" → they are non-zero in
**4.76 %, 4.62 % and 14.50 % of 83 862** blocks disc-wide, reading as
**degrees** (±180, ±90, 120, 360). The original note was a sample artefact.
(Superseded figures: an earlier count of 72 287 blocks missed every nested
record — see the alignment entry below.)
* ~~"those angle fields are where the title's rotated quads come from" → **no** —
every `GP_TITLE` build 4 element has all three at zero.~~ → **that refutation
was itself wrong, and is withdrawn (2026-08-28).** `+12` *is* exactly where
they come from. Build 4's **top-level** elements do all read zero; the rotated
quads belong to its two **nested** leaf records, `ptloop01.rat` (`+12` = 30)
and `ptloop02.rat` (`+12` = 45), which the census never opened. Measured
off the GPU: **+30.26°** and **45.28°**.
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
* "the rotated draw's element cannot be named from the capture" → refuted; its
quads' **edge lengths** name it. 400 × 1076 and 400 × 1444 match `pteff03`
399×180 at 600 % and `pteff03a` 399×180 at 800 % — two different heights, both
landing. [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
* "a keyframe-block scanner may assume 4-byte alignment" → refuted; it found
**0/3** of its own control blocks and under-counted the corpus by 16 341
blocks. Nested `RATC` blobs start at odd offsets (`0xbb5966`).
* "keyframe rotation lives only in **nested** `.rat` leaf records" → **mine, and
refuted within the hour by my own sweep.** It held for the three archives I had
checked (`GP_TITLE`, `GP_BUNK`, `GP_CHALLENGE`) and failed on the next:
`GP_DIALOG` and `GP_DEBRIEFING_PILOTLOG` rotate **top-level** elements, and
those are the clearest examples on the disc.
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
* "the pivot-anchored scale formula `kf.x pivot·(scale100)/100` is our
renderer's reading, not a measurement" → **now measured.** At the `ptloop`
pair's 600 %/800 % scale it predicts both quad centres at y = 360.0 against a
captured 359.1/360.0, where top-left anchoring predicts 810/990.
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
* "the game passes a pink per-vertex colour for the title swoosh" → **refuted by
draw capture.** Every vertex colour in the capture is `<alpha>FFFFFF`, white RGB.
* "the swoosh discrepancy is undecodable" → **solved**: the game submits it as two
**rotated parallelograms**; `ui_layout::blit` only does axis-aligned rectangles.
[`ui-title-build-map.md`](ui-title-build-map.md)
* "the `--log_ui_draws` per-draw capture reads the guest's blend state" →
**mine, and wrong.** It records primitive type, index count, index-buffer
address, VS/PS ucode hashes, texture bindings and vertex attribute 0 — no
`RB_BLENDCONTROL`. It can test vertex colour as-is; blend state needs a Canary
change. [`ui-title-build-map.md`](ui-title-build-map.md)
* "the plate-free title capture (t ≈ 4.0 s) may be too early to be settled" →
**mine, and refuted.** The swoosh band correlates 0.7342 at t = 4.0 s and
0.7353 at t = 21.5 s — identical to 0.001 over 17.5 s.
* "`T8aD +0x04` bit `0x02` selects an additive blend" → **mine, and refuted.**
Blending those sprites additively worsens every measure against the capture.
[`ui-title-build-map.md`](ui-title-build-map.md)
* "the title logo elements' wrong pivots (off by up to 59 px, authored against the
other language's sprite) explain the swoosh rendering too thick" → **mine, and
refuted.** `blit` sizes from the texture and applies the pivot only as
`pivot·(scale100)/100`; all seven swoosh elements are scale `(100,100)` at every
keyframe, so the term is zero. The mismatch is real but inert here — it would
bite `ptlogo1`/`ptlogo2`, which scale to 150 during the build-in.
[`ui-title-build-map.md`](ui-title-build-map.md)
* "the title screen loops at ≈ 2.2 s" → **mine, and doubly wrong.** The title
*art* is near-static (wordmark sd 0.06); the 2.3 s pulse is the
**`PRESS Ⓐ BUTTON` plate**, which is a *different build* (2, not 4). Localised
by a per-tile amplitude map and decoded in `ptbtn00f.rat`.
[`ui-title-build-map.md`](ui-title-build-map.md)
* ~~"a `.rat` leaf record's keyframes use the build's 40-byte layout" → they do
not~~ — **withdrawn.** They do. The scan that "found nothing" required 29
increasing times; the records hold **three**. Decoded in
[`ui-title-build-map.md`](ui-title-build-map.md).
* "the title's measured ≈ 2.2 s oscillation is the `ptloop01`/`ptloop02`
elements" → **mine, and refuted by decoding them.** Their sweeps run 7.5 s and
9.5 s; a 22 s capture showed 8 peaks, not 3. The period's source is unidentified.
* "the developer splash cannot be rendered by `screen render` at all" → it can,
with **`--all`**. It is only invisible to the *default* listing, which filters on
`is_build`. [`ui-title-build-map.md`](ui-title-build-map.md)
* "the four `GP_TITLE` entries that are not screen builds are unidentified" → they
are the **splash**: 10/13 the `SQUARE ENIX` logo, 11/14 the developer logos.
* ~~"the boot title is phase 2 and the attract title is phase 4 state 0, and only
phase 2 handles Ⓐ" → refuted~~ — **the REFUTATION is withdrawn.** The test
assumed Ⓑ lands in phase 4 state 0; the event-0 block actually sets
**phase = 2** and state = 0 together, so it probed phase 2. The hypothesis is
untested, not dead.
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
* "any title after the first one refuses input" → too broad. The **Ⓑ-returned
title accepts Ⓐ**; only the **attract**-returned title is inert.
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md)
* "`sub_821C6458` is `GamePart_Title`'s state machine" → **mine, imprecise.** It
is the machine for **phase 4** of a five-way outer dispatch at `this+132`;
phase 0 is the splash. The ten states and eighteen edges are phase 4 only.
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
* "`sub_821CC860` is the game's by-name screen factory" → **mine, and wrong.** Its
decoded arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`,
`GAMMA_RGB` — it is a **generic name-keyed lookup**, mostly config.
* "`DIFFICULTY` and `EXTRA_MENU` are corroborated screen names" → **mine, and
wrong.** Neither appears in `r5` at any of the 48 call sites; they were strings
merely referenced by the same functions. Only `TUTORIAL_MENU` survives.
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
* "the `{func, func, ptr}` triples at `0x820a3b48` are a GamePart state table" →
**static-initialiser records trailing the `RegisterToFactory` strings.** The
bytes before them are the tail of a diagnostic string and the data column is
zero-filled descriptors.
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
* "the boot sequence is driven by a table the game reads" → it is **not
data-driven**; four search spaces closed, transitions are calls with an id
argument. [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
* "`config.ini` is a GamePart settings table, so the boot order is in it" → its
`[SYSTEM]` section is **empty**; the only populated section is `[LANGUAGE]`.
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
* "the attract loop is `GP_ADVERTISE_DEMO` (GamePart 1)" → 🟡 id 1 has **no
registration site** in the shipped build; the attract is the title replaying
`ADV.wmv`.
* "the 29 id-table names are 29 distinct GameParts" → 24 register, and `3`/`4`
are the **same class** (`GamePart_SaveLoad`).
* "Ⓐ on `NEW GAME` leads to a standing black-screen hang" → it opens
**`DIFFICULTY`**, then **`SELECT DATA`**. What looked like a hang was a menu
waiting for input that nobody pressed; the crash that follows is the already
documented `sub_823070B0` cache throw.
[`menu-navigation-semantics.md`](menu-navigation-semantics.md)
* "tapping Ⓐ during the boot movie breaks the title" → **one** tap skips the
movie cleanly and the title works normally. It is *hammering* (88 presses) that
breaks it. [`movie-binding.md`](movie-binding.md)
* "the attract movie runs ~85 s, so it is not `ADV.wmv` (137 s)" → **mine, and
wrong.** Sampling began 39 s into the movie, so what was timed was its tail.
The attract movie **is** `ADV.wmv`, played in full.
[`movie-binding.md`](movie-binding.md)
* "the boot intro and the attract movie are different videos" → one asset, one
manifest slot (`ADVERTISE_MOVIE`).
* "the new-game intro is unidentified" → `MS00A``S00A.wmv`, decoded from the
movie manifest.
* "`GP_READY_ROOM.pak` holds the Ready Room screen" → its 317 distinct element
names contain **none** of the six visible labels; it is the briefing /
tactical-map content behind the `BRIEFINGS` item.
[`ready-room-probe.md`](ready-room-probe.md)
* "the Ready Room might be 3D with a UI overlay" → it is **2D**; the pak carries
zero 3D containers.
* "element kind `0x3002` is *the* button kind" → title-side only. All 902
`GP_READY_ROOM` bundles have **zero** `0x3002`; that pak uses `0x3000`,
`0x3004`, `0x300c`, `0x3008`. `0x3002` is one member of a `0x3000` family.
* "the transition between menu screens is a cut" → it is a **fade through
black**; a 0.5 Hz screenshot burst simply samples too slowly to see it.
[`screen-transitions.md`](screen-transitions.md)
* "the main menu's initial focus is fixed" → three boots of one script gave
`TUTORIAL`, `TUTORIAL`, `NEW GAME`.
* "the title menus drop d-pad presses shorter than ~0.3 s" → **refuted by my
own data.** The menu **wraps at both ends**; every press registered, and the
"missing" step was the wrap. See [`menu-navigation-semantics.md`](menu-navigation-semantics.md).
* "the main menu opens with `NEW GAME` focused" → it opens on **`TUTORIAL`**,
2/2 boots (🟡 a third recorded run implies `NEW GAME`, so this is reproducible,
not invariant).
* "`GP_TITLE` builds 6/8/9 are three submenus" → **8 is the JAPANESE main
menu**, and 6/9 are the English and Japanese `EXTRAS` submenu. `GP_TITLE`
holds eight screens shipped twice (EN/JP), and exactly one submenu. See
[`ui-title-build-map.md`](ui-title-build-map.md).
* "the `PRESS Ⓐ BUTTON` plate is a state of the title build" → it is **its own
build** (2/3), composited over build 4 and faded in a beat later.
* "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150
`ANON_`, 0 `rtti_present`, 0 base classes.
* "the sibling vtable methods name the class" → they cannot.
@@ -102,6 +255,46 @@ neighbourhood, not just the line.
* "a set-difference over file names can see reuse" → it cannot; **join per
USER**. Per-pak copies are ×6.
## Audio
* "which bank the menu plays is not on the disc" → the **cue table** cannot say
(all BGM cues are numeric), but the **code** can: `sub_821C5580` plays cue
**1103 = `BGM_103`**, and its two declared waves match the two streams the XMA
probe saw byte-for-byte.
* "the observed BGM stream sizes match no bank's declared waves, so the game hands
the decoder a window" → **mine, and wrong** — I checked only the `BGM_0xx` rows.
They are `BGM_103`'s two waves exactly; the game hands over the whole wave.
* "an individual SE cue's audio cannot be extracted" → **mine, and wrong.**
`--xma_param_probe=true` logs each stream's head bytes; searching them in
`Static.slb` locates the wave exactly. [`menu-audio-cues.md`](menu-audio-cues.md)
* "`Static.slb` has no wave boundaries, so its layout is unknown" → it is a
**packed run** of whole 2 048-byte XMA packets with no delimiters — the two
located cues are contiguous. There is nothing to scan for, by design.
* "`Pj_Silph.xgs` holds the cue→wave index, so parse XACT" → **no XACT container
exists on this disc**: 0 × `XGSF`/`SDBK`/`WBND` in all 1.08 GB of `sound.pak`,
and no `XACT`/`.xgs` string in the executable. The extensions are the authoring
tool's, not the format's. [`menu-audio-cues.md`](menu-audio-cues.md)
* "every sound cue resolves to its own `.slb` bank" → the 322 `SE_*` cues do not;
**0 of 322** are in `FILES`, and `BANK_SE` puts them all in `Static.slb`.
* "`Static.slb` can be split into waves like any other bank" → it holds **0
`RIFF`, 0 `seek`, 0 `WAVE`** across all 8 353 472 readable bytes.
[`menu-audio-cues.md`](menu-audio-cues.md)
* "`BGM_001.slb` is three sub-waves (10 KB + 4.47 MB + 4.67 MB)" → the 10 KB is
the **bank header**; a bank is **two** waves.
* "a music bank's two waves might be intro + loop, two variations, or two halves"
→ they are **two stems of one performance, played together** — equal duration
in 32/32 banks, and sample-synchronous.
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md)
* "`BGM_106``BGM_109` break the two-wave rule" → they are the leading-region
straddle; realigned across entry boundaries they obey it.
* "the cue table names which BGM belongs to which screen" → all 32 BGM cues are
numeric (`BGM_001``BGM_109`).
## Units, weapons, effects and assets
* "`Generic` (394) is the unit datasheet" → refuted.
@@ -183,3 +376,244 @@ neighbourhood, not just the line.
* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328.
* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**.
* "the splash `_eff` glows hold a constant α ≈ 33, contradicting their declared
255 plateau" → **mine, and refuted within the iteration.** They ramp 34 → 255
in exact steps of 34. I had printed the series' minimum and read it as its
range. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
* "the declared keyframe timeline reproduces the captured splash" → **refuted for
multi-keyframe elements.** `palogo_gamearts` is still at `a=255` nine frames
after its declared `a=32`, and its declared 80-frame fade-in is never drawn.
The `_eff` glows do reproduce, exactly — so this is about the group timeline,
not about the interpolation law. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
* "the `_eff` elements' agreement is the whole case for the keyframe-time shift,
so it stays a shape argument" → superseded. The **hold duration** is
calibration-free and decides it: observed 83 frames of full alpha against a
predicted **2.0** as decoded and **80.0** shifted. The shift is nonetheless
**not adopted** — it moves `GP_TITLE` build 7 by 13 % of pixels, away from its
verified English twin's brightness. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
* "the `GP_TITLE` build 7 render difference is evidence against the keyframe-time
shift" → **mine, and withdrawn.** It is one element, `ptlogo_eff3.t32`, a
transient bloom with no resting pose; `rest()`'s dwell fallback returns a
different endpoint of the same movement under each reading. The brightness
comparison measured our heuristic, not the decode.
[`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)
* "`rest()`'s longest-dwell fallback picks the pose the element rests at" →
**refuted structurally.** A dwell gap is time spent interpolating *between*
poses; an endpoint is only held when the two poses are equal, which is a
plateau, which the earlier path already returned for. Every element that
reaches the fallback has a guessed rest pose.
* "`rest()` for a plateau-less element should be the **last keyframe**" → **mine,
and refuted.** The developer splash's three sibling glows are structurally
identical and differ by one byte (`a=212` vs `a=255` at `t=45`); that rule
makes `palogo_anima_eff` alone invisible while `gamearts_eff` and `seta_eff`
stay lit. Capture box-mean ratios (0.717 / 0.723 / **0.772**) go the other way
too. [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)
* "a keyframe `scale` of 0 means *unset*, so render at 100 %" → **refuted by a
disc-wide control.** 2 166 elements have a zero-scale keyframe and **not one is
zero on every keyframe**, while 1 762 grow back out of zero (`ptlogo_eff3.t32`
runs 0 % → 200 %). Zero means collapsed; the renderer now draws nothing.
[`structures/ui-rat-layout.md`](structures/ui-rat-layout.md)
* "a Japanese-locale capture is impossible in this container, because canary has
no `user_language` cvar" → **mine, and refuted the next iteration.** The cvar
really is absent, but the language is persisted in
`<storage_root>/xconfig.settings` (`user.language`, BE u32 at file offset
`0x912`, located from three struct landmarks) and that file is writable. The
capture is still not *taken* — a Japanese run never reached the title in 787 s
— but it needs a longer run, not a rebuilt emulator.
[`tools/re-capture/set_console_language.py`](../../tools/re-capture/set_console_language.py)
* "the Japanese-locale run never reached the title in 787 s" → **the measurement
was broken, not the run.** `wait_title.sh` carried the superseded single-pixel
oracle. Re-run with `is_title.py`: the game still did not present the
interactive title, but that is now a measured statement (zero green-glyph
pixels, correlation ≤ 0.22 to either build-7 render) rather than an artefact.
* "the Japanese-locale run fails to reach the interactive title *because of the
locale*" → **refuted by the English control.** 75 samples over 734 s with the
same flags and oracle, every one glyph = 0. Neither locale presents the
interactive title without a pad press.
* "neither locale reaches the interactive title without a pad press" /
"the game sat in the attract loop for 604 s" → **withdrawn as causes.** Both
rest on runs whose polling loop sampled every ~41 s, because `screenshot`
costs 10.8 s while the emulator runs (0.117 s idle, 92×). A title lasting a few
seconds would be missed. The observations stand; the conclusions drawn from
them do not. [`capture-harness-status.md`](capture-harness-status.md)
* "the boot harness fails because its polling loop samples every ~41 s, slower
than the title screen lasts" → **mine, and refuted by my own fix.** The
sampling defect was real (3.98 s → 0.29 s per sample, 13.7×, control-verified
at 753/327), but a probe running at 3.99 fps for **420 continuous seconds —
1 674 samples — still saw zero green-Ⓐ pixels.** Sampling rate was not the
cause. [`capture-harness-status.md`](capture-harness-status.md)
* ~~"neither locale reaches the interactive title without a pad press"~~ →
withdrawn last iteration for want of evidence, now **reinstated as a
measurement**: 1 674 dense samples over 420 s, English, zero glyph frames.
⚠️ Reach: a mid-run window only; it says nothing about the boot title.
* "the PRESS Ⓐ plate appears only in the **boot** title window, which mid-run
sampling could never catch" → **mine, and refuted.** The fast probe was
attached at t=0: **2 391 frames over 600 s at 3.98 fps from launch**, max glyph
0. The plate did not appear at any point in the first ten minutes.
[`capture-harness-status.md`](capture-harness-status.md)
* "2 391 frames over 600 s from t=0, max glyph 0, therefore the title never
appears in the first ten minutes" → **withdrawn: the instrument stalls.** A
single long-lived x11grab stream degrades 3.98 → 1.60 fps and then freezes,
repeating one stale frame; cross-checked, it read surface mean 5.21 where
`import` read 125.65 at the same moment. A dense negative from a frozen stream
is not a negative. [`capture-harness-status.md`](capture-harness-status.md)
* "the `T8aD` layer key fully determines a screen's paint order" → **refuted, and
the remainder is undecodable.** Elements sharing a key are tied; on the title
the game paints the five tied `ptlogo_back2eff` glows `1,2,5,3,4` while the
declaration table, the RATC child order and **every** field in the `T8aD`
header (exhaustive 0x000x7f, u8/u16/u32, both directions — 0 matches against
64 for the declaration-order control) all give `1,2,3,4,5`.
[`ui-paint-order-derived-check.md`](structures/ui-paint-order-derived-check.md)
* "SE audio is undecodable from the disc — no XACT container exists anywhere"
(as it stood on the **handoff page**) → **stale**: `menu-audio-cues.md` had
already retracted it and located three cues in `Static.slb` that decode to PCM.
The retraction never reached the row the port agent reads. Handoff row fixed;
`tools/re-capture/handoff_lint.py` now checks for this class.
* "which of `8AX` and `ptbase` the game draws needs a per-draw capture recording
texture base addresses" → **mine, and refuted — it is settled statically.** The
two carry the same art at two resolutions, so neither compares usefully against
a capture; their *difference* does. Correlating the capture's
departure-from-upscale against the 8AX-only detail gives +0.0475 (main menu)
and +0.0634 (title), both 68 % of ceiling against matched controls of ≤0.0095.
The game draws the full-res `8AX`.
[`ui-8ax-fullres-background.md`](structures/ui-8ax-fullres-background.md)
* "the pixel-pair ratio shows the capture is native, not an upscale" → **mine,
and withdrawn as evidence.** Upscales give 0.000.72, native 0.98, capture 1.01
— but additive noise pushes any such ratio toward 1, and both "native + noise"
and "bilinear upscale + noise" fit the observed values. The conclusion happens
to be right; this test does not establish it.
* "the render-vs-capture gamma may be canary's own BT.709 output transform, since
`kernel_display_gamma_type = 2`" → **mine, and refuted from the source.** That
cvar is the value a `kStub` getter (`VdGetCurrentDisplayGamma`) hands the
**guest**; the game builds its own ramp from it and canary applies the guest's
`DC_LUT` ramp in the swap path. No emulator-side gamma post-process exists to
subtract. 🟡 Whether this game installs a ramp at all is still unestablished.
[`ui-render-tone-curve.md`](structures/ui-render-tone-curve.md)
* "the GPU trace produced nothing because either the CLI flag did not reach the
cvar or `BeginTracing()` failed silently" → **both wrong.** The trace writer is
**compiled out**: `trace_writer.h` gates it on `#ifdef NDEBUG`, so a release
build has no writer at all. Confirmed with a control — the format string
`_stream.xtr` appears **once** in the Debug binary and **zero** times in the
Release binary `run-canary` actually uses.
[`capture-harness-status.md`](capture-harness-status.md)
* "the `T8aD` `+0x04` bit `0x02` means the sprite's name contains `eff`" →
**refuted, now on evidence.** `ptlogo_back2eff` is an `eff` name with the bit
clear; the attribution is confirmed by header-order pairing (18/18 on build 4)
rather than by a size match, which cannot separate it from the same-sized
`ptlogo_back2eff5`. All 10 bit-set sprites *are* `eff` names, so the
implication runs one way only.
* "the bit `0x02` marks a transient element" → **refuted.** `pteff03` and
`pteff03a` carry the bit and run to `t=250`, ramping to `a=255` and holding.
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
* "bit `0x02` set ⇒ the sprite's name contains `eff`" (the one-way reading that
survived the biconditional's refutation) → **mine, and refuted disc-wide the
next iteration.** True 10/10 on `GP_TITLE` build 4; over 14 709 sprites it
fails on **2 657 of 4 995** bit-set ones. `P(eff|set) = 0.468` against
`P(eff|clear) = 0.144` — an association, not an implication.
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
* "`T8aD +0x04` bit `0x02` selects premultiplied alpha" → **refuted.**
Premultiplied requires `RGB ≤ A` everywhere; over 170 decoded textures the
flagged group violates it on a median **52.5 %** of pixels against **30.2 %**
unflagged — both far from premultiplied, and the flagged group *further*.
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
* "`build-reborn test` cannot terminate" → **mine, and too strong; corrected the
next iteration.** It is heavy, not hung: 19 of 166 `.xpr` containers exceed
25 s, `Stage_S02` completes in **144 s** with `rc = 0`, and one full pass is
~4560 minutes. The 3 h 26 m observed was that work at a load average of 914,
inflated by my own two duplicate runs.
[`test-suite-runtime.md`](test-suite-runtime.md)
* "the case for the keyframe-time shift rests on a single element" → **no longer
true.** Three elements across two screens discriminate and all favour it:
`palogo_gamearts` and `palogo_seta` hold full alpha 83 frames, `palogo_sqex`
≥77, against 68 predicted by the current reading and 80102 by the shifted
one. The `_eff` glows fit both and argue against neither.
[`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
* "an element with no held pose should be drawn as NOTHING rather than at a
guessed endpoint" → **mine, and refuted.** Suppressing every plateau-less
element and re-correlating against the live captures: title +0.9500 → +0.6839,
main menu +0.9460 → +0.9037, `EXTRAS` +0.9440 → +0.9094. Worse on all three.
* "`rest()` guesses for 24.57 % of elements (3 807 of 15 493)" → **mine, and
overstated by 65 %.** The plateau test marks a **single-keyframe** element as
plateau-less because it has no adjacent pair — but its one pose is
unambiguously its rest. 1 502 of the 3 807 are those; the genuinely ambiguous
population is **2 305 (14.88 %)**.
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
* "`rest()` for a plateau-less element should be the last keyframe → refuted by
the sibling argument" → **that refutation is itself refuted, this time by
measurement.** Rendering under the rule and correlating against the live
captures: publisher splash +0.9600 → **+0.9982**, developer splash +0.9643 →
**+0.9758**. Making `palogo_anima_eff` invisible *improves* the match; the
sibling symmetry was my expectation, not evidence.
* "the port's exposure to the rest-guessing defect is 14 elements" → **two.** The
fallback needs an element to be plateau-less **and** multi-keyframe; title,
main menu and `EXTRAS` reach it **zero** times, the two splashes once each.
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
* "the shifted time reading implies rest = the last keyframe, so the plateau rule
can be dropped" → **mine, and refuted by measurement.** Applying it to every
element collapses all five screens (title 0.9500→0.6819, main menu
0.9460→0.6416, `EXTRAS` 0.9440→0.5745) and renders both splashes **blank**. A
group is entry → hold → exit and the exit is the screen's *dismissal*: a
displayed screen sits at the hold, not at its final pose.
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
* "`rest_plateau` renders elements the game has already finished with" (as a
general claim) → **narrowed by its control.** It holds on **transient** screens
only: suppressing the finished glows takes the two splashes from 0.9604/0.9659
to **0.9982/0.9980**, while the same edit costs the title 0.002, the main menu
**0.092** and `EXTRAS` **0.107**. A plateau mid-animation is evidence the
element is held at that point in the timeline, not that it is on screen once the
screen has settled — and where a screen does settle, the rule is right.
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
* "the group header's undecoded lead-in word carries a per-element start offset"
**refuted immediately.** It is `0x00000000` for all seven elements of the
developer splash — glows and logos alike — while those two families are
observed to run sequentially (frames 94115 and 116211) despite declaring
overlapping times. [`ui-group-start-time.md`](structures/ui-group-start-time.md)
* "the splash elements share one clock origin" → **refuted.** Fitting a single
origin needs f₀ ≈ 93.5 for `palogo_gamearts_eff` and f₀ ≈ 103 for
`palogo_gamearts`, ~19 units apart, and aligning one throws the other off by
~9 frames at both ends. Durations match (97.8 %, 98.5 %); starts do not.
* "the glows and logos might overlap and my size-grouping merged them" → **tested
and refuted.** Across all 235 captured frames, **zero** contain both a glow and
a logo; f110115 draw two glows and f116 onward two logos, with no transition
frame. The sequencing is real.
* "a bundle's declared elements are what the screen shows" → **refuted.** Entry 11
declares three logo/glow pairs and only two are ever drawn — `palogo_anima`
gets 0 frames against `palogo_gamearts`'s 95, from byte-identical keyframe
times. (Reach: within the capture's frames 1214.)
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
* "the glow and logo phases are one bundle with elements selectively activated"
**mine, and withdrawn as unestablished.** The alternative — two compositions
shown in sequence — fits equally. The texture-base test fails its control: the
publisher splash is a different bundle and shares the base `0x11C30000`, so
that address is a reused upload slot, not a bundle identity. What survives is
that declared elements ≠ drawn elements.
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
* "two compositions shown in sequence" (as the alternative to selective
activation) → **refuted statically.** It requires a bundle declaring the glows
without the logos; no such bundle exists. Only four `GP_TITLE` entries carry
`palogo` elements, and both developer entries declare **all six** logos and
glows — so whichever was active, a subset of its elements was drawn at a time.
Selective activation is reinstated on evidence.
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
* ~~"the splash's `measured_paint_order` records a front-to-back depth order" →
mis-typed; between its glow and logo halves it records only the temporal order
they were seen in."~~ → **that refutation is itself refuted (same day).** The
vector is a read of the runtime **child array** (`ui-screen-runtime.md`:
"paint order (child slots)"), not of the draw capture, so co-occurrence does
not bear on it; and the halves carry *distinct* T8aD layer keys
(`0xa100` < `0xa110`, `paint_order_audit`: 0 same-key ties), so the file orders
them regardless. The no-overlap measurement was correct; the inference from it
was not. What survives: a capture of this screen can only cross-check the order
*within* each half. [`ui-prm-primitives.md`](structures/ui-prm-primitives.md)
* "`8AX` is the name a `T8aD` is registered under" → **it is not a name at all.**
It is three bytes of the preceding record's payload (`38 41 58`) that happen to
be printable ASCII, which our backwards printable-run scan preferred over the
name the format actually states in its `opt ` block. The claim sat in
`HANDOFF.md` and `ui-8ax-fullres-background.md` as though `8AX` were a real
identifier, and cost every menu screen its full-resolution background.
[`ratc-child-names.md`](structures/ratc-child-names.md)
* "`pmbase.t32` is on the disc nowhere" (the one dangling asset behind
`10 144 of 10 148 references resolve`) → **withdrawn; it is on the disc.** It is
the `GP_STAGE_CLEAR` child the same scan named `8AX`. With the name decoded the
count is **10 148 of 10 148**. [`ratc-child-names.md`](structures/ratc-child-names.md)

View File

@@ -0,0 +1,583 @@
# What the game reads at boot — `config.ini`, and which GameParts actually exist
**Status:**`CONFIRMED` and **decoded** for the language selection. ❔ a bounded
**negative** for the boot *order*. 🟡 for what the registration strings imply
about the attract loop.
Contributes to [MISSION Q6](../port/MISSION.md), and firms up Q4's ids.
## ✅ `config.ini` — the disc's only config, and it selects the language
`config.ini` sits at the **disc root**, is 400 bytes, and is the **only** `.ini`,
`.cfg` or `.txt` anywhere on the disc. Its own header comment (Shift-JIS) names
it:
> `アプリケーション/ゲームパート初期設定テーブル`
> — *"Application / GamePart initial settings table"*
>
> `SYSTEM セクションには、ゲーム及び各ゲームパートで共通にアクセスする情報を記述する`
> — *"the SYSTEM section describes information accessed in common by the game and
> each game part"*
```ini
[SYSTEM]
[LANGUAGE]
= eng ; default
#0x01 = eng ; XC_LANGUAGE_ENGLISH
#0x02 = jpn ; XC_LANGUAGE_JAPANESE
#0x03 = deu ; XC_LANGUAGE_GERMAN
#0x04 = fra ; XC_LANGUAGE_FRENCH
#0x05 = esp ; XC_LANGUAGE_SPANISH
#0x06 = ita ; XC_LANGUAGE_ITALIAN
```
**This is the mechanism behind the EN/JP screen pairs.** The console's
`XC_LANGUAGE_*` setting selects a three-letter code, and that code is what picks
`GP_TITLE`'s English or Japanese build ([`ui-title-build-map.md`](ui-title-build-map.md))
and the `<lang>.pak` families. Default is `eng`, keyed by the empty line — an
unlisted language falls back to English. ✅ decoded. The executable does read the
file: the string `config.ini` is at `.rdata 0x82062b44`.
## ❔ But the boot ORDER is not in it — and this is the whole search space
`[SYSTEM]` — the section the file's own comment says holds what the game and every
game part share — is **empty**. So the file the game itself calls the *GamePart
initial settings table* says nothing about which part runs first or what follows
what.
**Reach of the negative:** this is the only config file on the disc (one `find`
over the whole extract). The boot order is therefore not in disc-side
configuration at all; it is in code, or in a table inside the executable that has
not been located.
## 🟡 `GP_ADVERTISE_DEMO` is never registered — the attract loop is not its own part
The executable carries one diagnostic string per GamePart registration site
(`silph::GamePartTask::RegisterToFactory<N, class silph::GamePart_X>::RegisterToFactory is failed!`)
— the same evidence the corpus used to pin the id table
([`challenge-mission-gate.md`](challenge-mission-gate.md) §3). Extracting all of
them gives **24 of the 29 ids bound to a C++ class**; full list in
[`data/gamepart-class-ids.txt`](data/gamepart-class-ids.txt).
**The five ids with no registration site are `1`, `2`, `16`, `18`, `28`** — in the
id table's naming, `GP_ADVERTISE_DEMO`, `GP_SELECT_STORAGE`, `GP_DEMO`,
`GP_SELECTOR`, `GP_TEST`.
That `GP_ADVERTISE_DEMO` (1) is among them matters for Q6, and it agrees with
what was measured: the attract loop is **the title screen replaying `ADV.wmv`**
([`movie-binding.md`](movie-binding.md)), not a transition into a separate
advertise part. The id table names a part the shipped build never registers.
🟡 **not ✅**, because this is an argument from a *diagnostic string*: no error
message for id 1 implies no registration site for id 1. That is how the corpus
already reads these strings, and it is sound, but it is not the code.
### Two bonuses for Q4
* **`3` and `4` are the same class** — `GamePart_SaveLoad` is registered twice.
The id table's separate `GP_LOAD` / `GP_SAVE` names are two ids on one part.
* The ids behind the menu buttons now match the executable's own **class** names,
not just a list of table names: `GamePart_Options` = 8, `GamePart_Tutorial` =
25, `GamePart_Extras` = 5, `GamePart_MissionSelect` = 7,
`GamePart_MovieTheater` = 6, `GamePart_Title` = 0. Still a **name match**
screen title ↔ class name — but anchored one level closer to the code.
## What is still missing for Q6
The *transitions*. Nothing found so far says "title, then movie, then title" — the
manifest gives the boot-side **assets** in play order
([`movie-binding.md`](movie-binding.md)), `config.ini` gives the **language**, and
the registry gives **which parts exist**. What decides to advance is in
`GamePart_Title`'s own code, and reading it is a static PPC job that has not been
started.
## ❔ The transitions are code, not data — the reach of that negative
Q6's remaining half asked what the game *reads* to decide the boot order. The
answer is: **nothing. It is not data-driven.** Four independent places were
checked, and the sequence is in none of them.
| looked in | result |
|---|---|
| **disc configuration** | `config.ini` is the only config file on the disc, its `[SYSTEM]` section is empty (above) |
| **the movie manifest** | carries the boot-side *assets* in play order, and no transitions — [`movie-binding.md`](movie-binding.md) |
| **a persistent part-id field** | already refuted: the requested GamePart id exists **only as a stack argument in flight**, with no literal store anywhere — [`challenge-mission-gate.md`](challenge-mission-gate.md) §5 |
| **the id table's attract entry** | the string `GP_ADVERTISE_DEMO` at `0x820a1fe8` has **zero xrefs of any kind**; nothing in the code reads it |
So a transition is a **call with an id argument**, chosen by code. There is no
table to read and nothing to poke.
**Where that code is, as far as it was traced.** The `RegisterToFactory<0, class
silph::GamePart_Title>` diagnostic string at `0x820a3d60` is referenced from
exactly one place, `sub_8280E148` — the registration site — which also takes the
address of **`sub_821C7D98`** (`addi`), the position a factory template puts its
creator. 🟡 That identification is by position and convention, not proven;
`sub_821C7D98` itself has **0 `.rdata` references**, consistent with a small
`new`+ctor thunk rather than the state machine. The substantial function in the
same class neighbourhood is `sub_821C6458` (4 460 bytes, has EH, 15 `.rdata`
refs), and **reading it has not been attempted**.
### What this means for the port
The boot sequence is **authored**, not transcribed — and that is fine, because the
sequence itself is measured end to end:
```
developer splash (a RATC screen, not a video)
→ ADV.wmv
→ title + PRESS Ⓐ ──idle ~810 s──> fade to black → ADV.wmv in full → title
→ Ⓐ → main menu
```
with the fade-through-black timings in [`screen-transitions.md`](screen-transitions.md)
and Ⓑ from the main menu returning to the title.
## 🟡 `sub_821C6458` read — the title's states are NAMED in the executable
The page above left this function as the named next step: "the substantial
function in `GamePart_Title`'s neighbourhood, and **reading it has not been
attempted**". It has now been looked at — not disassembled line by line, but
characterised, which is enough to sharpen Q6.
**It is the title part's screen-state function.** 4 460 bytes, has EH, and called
from **exactly one** place — `sub_821C7850`, which sits in the same neighbourhood
as the creator the registration site points at (`sub_821C7D98`). It makes 33
distinct calls.
**What it names.** Its `.rdata` string references are:
| string | at |
|---|---|
| **`TITLE_SCREEN`** | `0x820a3d3c` |
| **`TITLE_MENU`** | `0x820a3d30` |
| **`LOADING`** | `0x820a214c` |
| `BASE_INFO` (×2) | `0x820a20ec` |
Those are the states measured off the running game, in the game's own words: the
title carrying `PRESS Ⓐ BUTTON`, the five-button menu, and a loading state. And
`BASE_INFO` is this corpus's own marker for a **screen-config lookup** rather than
a table read ([`REFUTED.md`](REFUTED.md), "`BASE_INFO` discriminates
screen-config from table-read, 9/9 vs 10/10").
### The sharper negative
So the title part **does ask for configuration keyed by `TITLE_SCREEN` and
`TITLE_MENU`** — and [`config.ini`](#-configini--the-discs-only-config-and-it-selects-the-language),
the disc's only config file, contains **no such sections**. Its only sections are
an empty `[SYSTEM]` and `[LANGUAGE]`. The lookups find nothing, and the values are
whatever the code defaults to.
That is a better answer than "the order is not in config": the game *asks the
question*, the shipped disc *does not answer it*, and the defaults live in code.
For the port it means the state **names** are transcribable even though their
contents are not.
🟡 **Not ✅.** This is a characterisation from string references and call counts,
not a read of the control flow. Two self-references (`0x821c6498`, `0x821c6b7c`)
inside the function look like jump tables — a switch, which is what a state
machine compiles to — but that was **not confirmed**, and nothing here shows which
state leads to which.
### 🔴 Refuted on the way, because it looked like a find
The words at `0x820a3b48`… resolve as neat `{func, func, ptr}` triples and read
convincingly as a state/handler table. **They are not.** The bytes immediately
before them are the tail of
`…SaveLoad>::RegisterToFactory is failed!`, and the `0x8210c1xx` targets are
zero-filled descriptors — static-initialiser records trailing the registration
strings, not a dispatch table. A plausible-looking array of function pointers next
to relevant strings is not evidence of anything until its neighbours are read.
## 🟡 The title's transition is a screen lookup BY NAME — `sub_821CC860`
Reading the code around each of the three state names shows the **same four
instructions** at all three sites:
```
lwz r29, 20(r30) ; an object off the part
lwz r3, 4(...) ; ...
bl 0x822F2328
lis r11, 0x820A
li r6, 0
lwz r4, 24(r30)
addi r5, r11, 15676 ; "TITLE_SCREEN" (15664 = "TITLE_MENU",
lwz r3, 88(r30) ; 8524 = "LOADING")
bl 0x821CC860 ; <- lookup(this+88, this+24, NAME, 0)
mr r4, r3
bl 0x82187B78 ; <- install the result
```
So a title-side transition is **`sub_821CC860(…, "<NAME>", 0)` followed by
`sub_82187B78(result)`** — a *string-keyed* screen lookup, then an install. Not a
numeric id, not a table index. That also fits `GP_ADVERTISE_DEMO` having zero
xrefs: at this level the screen graph is keyed by **name**, not by GamePart id.
### The candidate name vocabulary
`sub_821CC860` is called from **28 distinct functions**. Collecting the
upper-case identifier strings those callers reference gives 35 names, and they
split into two obvious families:
* **screen/state names** — `TITLE_SCREEN`, `TITLE_MENU`, `LOADING`,
**`DIFFICULTY`**, `EXTRA_MENU`, `TUTORIAL_MENU`, `STANDARD_MENU`, `DEBRIEFING`,
`CHALLENGE`, `MISSIONS`, `LIVE_BOARD`, `LOCAL_BOARD`, `EXTRA_MENU`;
* **config keys** — `TEXT_FONT`, `TEXT_HEIGHT`, `LINE_SPACE`, `GAMMA_RGB`,
`GAMMA_WB`, `TEXT_SPEED_PER_LETTER`, `EXIT_VALUE`, `INPUT_DIR`,
`MENU_ENABLE_SKIP`/`_DISABLE_SKIP`, `PAUSE_SE`, `JINGLE`, …
plus `BASE_INFO` in **19 of the 28** callers, which is this corpus's existing
marker for a screen-config function.
**Three of the screen names are independently corroborated by measurement**:
`DIFFICULTY` is exactly the screen `NEW GAME` opens, `EXTRA_MENU` matches the
`EXTRAS` submenu and `TUTORIAL_MENU` the lesson list — all three measured off the
running game in [`menu-navigation-semantics.md`](menu-navigation-semantics.md)
before this function was ever looked at.
⚠️ **Why this is 🟡 and not ✅.** The 35 strings are what those callers
*reference*, **not** proven arguments to `sub_821CC860` — the list plainly mixes
screen names with config keys, so it is a **candidate vocabulary**, not a decoded
one. Confirming it means checking, per call site, which string actually lands in
`r5`. That was not done.
### What is still unread
Which state leads to which. The three lookups sit in different branches of one
function and at least one (`TITLE_MENU`) is guarded by a `cmplwi`/`bne`, but the
branch structure was not traced, so the *order* still comes from measurement, not
from the code.
## ✅ The argument is now decoded — and it refutes my own corroboration
The section above listed a **candidate** vocabulary from "upper-case strings the
callers reference", flagged 🟡 because those are not proven arguments, and named
the check: *confirm per call site which string actually lands in `r5`.* That check
has been run, and it fired.
**Method.** Forward register simulation over the 100 instructions before each of
the **48** call sites of `sub_821CC860`, tracking `lis` / `addi` / `subi` / `mr`.
Full table in [`data/name-lookup-callsites.txt`](data/name-lookup-callsites.txt).
**Gated on a control, and the first version failed it.** A backward scan for
`addi r5, …` recovered only 7/48 and **missed both title sites I had read by
eye** — because there the name goes into `r27` first and reaches `r5` via a later
`mr`. The rewritten forward simulation reproduces all three known sites
(`TITLE_SCREEN`, `TITLE_MENU`, `LOADING`) and then recovers **46 of 48**.
### 🔴 `DIFFICULTY` and `EXTRA_MENU` are *not* arguments
Last section I wrote that three names were "independently corroborated by
measurement": `DIFFICULTY`, `EXTRA_MENU`, `TUTORIAL_MENU`. **Only
`TUTORIAL_MENU` survives.** `DIFFICULTY` and `EXTRA_MENU` never appear in `r5` at
any of the 48 sites — they are strings that merely live in the same functions.
The corroboration I claimed was an artifact of the loose filter, and the 🟡 I put
on it is exactly what caught it.
### The 28 names that ARE passed
```
BASE BASE(x3) BG(x3) BLACK(x2) BUTTON DEBRIEFING DETAIL EXTRA FADE(x3)
FILE(x5) GAME GAMMA_RGB GAMMA_TITLE INFO KEY LIVE_BOARD(x2) LOADING(x2)
LOCAL_BOARD(x2) MENU(x2) MENU_DISABLE_SKIP MESSAGE NEW_ITEM(x2) PAD SOUND
TITLE(x2) TITLE_MENU TITLE_SCREEN(x2) TUTORIAL_MENU WINDOW
```
### 🔴 So `sub_821CC860` is not a screen factory
The previous section called it one. The real argument list says otherwise:
`BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`, `WINDOW`, `GAMMA_RGB`,
`MENU_DISABLE_SKIP` are **not screens**. It is a **generic name-keyed lookup**
a named-entry getter used for config and resources throughout the executable, of
which the title part happens to call it with its three state names.
That weakens nothing about the title finding itself — `sub_821C6458` really does
transition by calling this with `TITLE_SCREEN` / `TITLE_MENU` / `LOADING` — but it
removes the inference that the 28 names are a screen vocabulary. They are a
**lookup-key vocabulary**, mostly config.
## ✅ The title's state machine — decoded
The last open piece of Q6 was *which state leads to which*. It is a plain
`switch` and it is now read.
```
821c6474 lwz r11, 136(r30) ; state = this+0x88
821c6478 cmplwi cr6, r11, 0x9 ; ten states, 0..9
821c647c bgt cr6, <default>
821c6480 lis/addi r12, 0x821C6498 ; jump table
821c648c lwzx r0, r12, r0
821c6494 bctr
```
⚠️ The table at `0x821C6498` disassembles as ten `lwz r16, N(r28)` instructions.
**It is data.** That is the "self-reference" this page flagged two sections ago as
*looking* like a jump table — it is one, and the disassembler was decoding its
words as code.
**Three of the ten states install a named screen**: **0 → `TITLE_SCREEN`**,
**2 → `TITLE_MENU`**, **8 → `LOADING`**.
**Eighteen transitions**, every one a literal `li rX, N ; stw rX, 136(r30)`:
```
0 → 1, 2 2 → 4 4 → 0, 5, 8×4 6 → 7, 9, 2 8 → 2
1 → 2, 2 3 → 4 5 → 6 7 → 9 9 → (none)
```
Full table with addresses in
[`data/title-state-machine.txt`](data/title-state-machine.txt).
### 🟡 It lines up with what was measured — read as corroboration, not proof
The boot reaches the title (state 0) and Ⓐ opens the main menu (state 2); the
graph has `0 → 2` directly and `0 → 1 → 2`. Ⓑ at the main menu returns to the
title, and `4 → 0` is the **only** edge back to state 0, reached from `2 → 4`.
Entering a submenu goes through `LOADING` and comes back, and `4 → 8` (four
separate sites) then `8 → 2` is exactly that shape.
Those readings are **mine, matching a graph to observed behaviour** — the
conditions on the edges are not decoded, so nothing here proves which input picks
which branch.
### What is still not decoded
* **the condition on each edge** — which input or event selects it;
* **states 1, 3, 5, 6, 7, 9** install no named screen, so what they do is unknown;
* **state 3 is never a destination** in this function, so something outside sets
it.
## ✅ The edge conditions, for the state that has them — an EVENT CODE
The previous section left "the condition on each edge" open. For the state that
matters it is now read.
**`sub_821C6458`'s third argument is an event code.** State 4 — reached from
`2 → 4`, i.e. immediately after the main menu is installed — is the
**input-waiting state**, and it dispatches on that argument:
```
821c6b5c cmplwi cr6, r27, 0x19 ; 26 events, 0..25
821c6b60 bgt cr6, <default>
821c6b68 addi r12, r12, 27516 ; second jump table, at 0x821c6b7c
821c6b78 bctr
```
**Six of the 26 are handled**; the other twenty fall through and change nothing.
| event | → state | |
|---|---|---|
| **0** | **0** | `TITLE_SCREEN` — back to the title |
| 3 | 8 | `LOADING` |
| 5 | 8 | `LOADING` |
| 8 | 8 | `LOADING` |
| 10 | 5 | — |
| 25 | 8 | `LOADING` |
So last section's edges `4 → 0, 5, 8×4` are each now attributed to a specific
event, and the shape of the state graph is complete for the input state.
### 🟡 A correspondence worth noticing, and not more than that
The main menu has five items, Ⓑ returns to the title, and this table has **one
event to state 0, four to `LOADING`, one elsewhere**. It is tempting to read that
as *Ⓑ = event 0, four items load an external archive, `EXTRAS` stays inside
`GP_TITLE`* — which would match everything measured.
**That is a count-match, not a decode.** The event numbers are not named, nothing
here shows event 3 is a particular menu row, and state 5 installs no named screen,
so the `EXTRAS` half of the story has no support at all. Recorded as an
observation so the next person sees the shape; it is **not** a button→event map.
### What is still not decoded
* **what the event numbers mean** — button id, menu-item index, or message id;
* **conditions on edges out of the other states** (`0 → 1` vs `0 → 2`, `6 → 7/9/2`);
* states 1, 3, 5, 6, 7, 9, which install no named screen.
## 🔴 Correcting my own framing: `sub_821C6458` is ONE PHASE, not the whole part
Chasing where the event code comes from turned up the level above, and it revises
what the previous three sections called "the title part's state machine".
`sub_821C6458` has exactly one caller, `sub_821C7850`, and that caller is itself a
dispatcher — on a **second, outer state field**:
```
821c786c lwz r11, 16(r30) ; only runs at all when this+16 == 3
821c7874 bne cr6, <exit>
821c787c lwz r11, 132(r30) ; phase = this+0x84
821c7880 cmplwi cr6, r11, 0x4 ; FIVE phases, 0..4
821c789c bctr ; table at 0x821C78A0
```
| phase | handler | |
|---|---|---|
| 0 | `sub_821C5690` | **the splash** — independently identified as the splash mechanics in the `iterate3E` notes, before any of this |
| 1 | inline at `0x821c790c` | |
| 2 | `sub_821C5818` | |
| 3 | `sub_821C5EC0` | |
| **4** | **`sub_821C6458`** | the title/menu machine — `TITLE_SCREEN` / `TITLE_MENU` / `LOADING` |
So `GamePart_Title` has **two nested state fields**: a phase at `this+132` (five
values) selecting which sub-machine runs, and the state at `this+136` (ten values)
*inside phase 4*. Everything the previous sections decoded — the ten states, the
eighteen edges, the event dispatch — is **phase 4 only**. Phases 03 are
untouched, and one of them is the developer splash.
That phase 0 lands on the function the corpus had already fingered as the splash,
from a completely different direction, is the useful check here.
### And the event code is forwarded, not created
All five phase handlers are called as `(this, r29, r28)` with the *same*
arguments, and `r28` is `sub_821C7850`'s own third argument, passed through
untouched. So the event vocabulary is defined **at least one level above** this
function. Finding what `3`, `5`, `8`, `10`, `25` mean means going up again, and
that was not done.
## Going up one more level — three of four callers pass a constant event
`sub_821C7850` has **four** direct callers. Recovering the `r5` argument at each:
| caller | event passed |
|---|---|
| `sub_821C7CB8` | **0** (→ back to the title) |
| `sub_821C7BA0` | **0** |
| `sub_821C47A0` | **5** (→ `LOADING`) |
| `sub_821C5580` | **not constant**`lwz r5, 4(r27)`, read out of a structure |
So the vocabulary is still not enumerable from here: the interesting caller reads
its event from a field. ❔ What `3`, `8`, `10` and `25` mean remains open.
**`sub_821C5580` is also where the outer gate is set:**
```
821c5640 li r11, 3
821c5644 stw r11, 16(r28) ; this+16 = 3 — the exact value sub_821C7850 tests
821c5650 lwz r5, 4(r27) ; event from a struct field
821c5658 bl 0x821C7850
```
That answers the "what does `this+16 == 3` gate on" question from the previous
section: this function arms it.
### 🎁 And it names the title's music
The same function, a few instructions earlier:
```
821c560c li r5, 1103
821c5610 li r4, 4
821c561c lwz r3, 0(r29)
821c5620 bl 0x8217ACF8 ; a sound-play call
```
**1103 is a BGM cue id**`BGM_103`. That closes an open residual from Q8/Q10
("which bank is the menu's music is not on the disc"), and it checks out three
ways; see [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md).
## The other phases, characterised — and phase 0 confirms the splash twice over
Same treatment for the phase handlers that were still unread. Strings each one
references, and whether it carries a jump table of its own:
| phase | handler | size | strings referenced | own switch |
|---|---|---|---|---|
| 0 | `sub_821C5690` | 380 | **`LOGO`** | none |
| 2 | `sub_821C5818` | 1 576 | `BASE_INFO`, **`BUTTON`**, **`TITLE_SCREEN`** | none |
| 3 | `sub_821C5EC0` | 1 220 | *(none)* | one, `bctr` at `0x821c5ef8` |
| 4 | `sub_821C6458` | 4 460 | `BASE_INFO`, `LOADING`, `TITLE_MENU`, `TITLE_SCREEN` | two (known) |
**Phase 0 references `LOGO`** — a second, independent confirmation that it is the
developer splash. The `iterate3E` notes reached `sub_821C5690` from the guest side
and named the splash's `LOGO` items; this reaches the same function from the
registration site and finds the same string.
**Phase 2 draws the title *with* the `PRESS Ⓐ BUTTON` plate** — it references
`TITLE_SCREEN` and `BUTTON`, and [`ui-title-build-map.md`](ui-title-build-map.md)
established from the archive that the plate is its own build (2/3), composited
over the title art.
### 🟡 A hypothesis for a puzzle this corpus has had open for months
**The title is installed from two different places**: phase 2, and phase 4's
state 0. Same screen, different code.
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) has long
recorded, and never explained, that *the title which ends the boot accepts Ⓐ while
the title the attract loop returns to accepts nothing* — with the giveaway that a
draw capture in each state is identical, 13 quads at the same rects. Two code
paths installing the same screen is exactly the shape that would produce that.
**Candidate:** the boot title is **phase 2**; the attract-returned title is
**phase 4, state 0**. Only phase 2 wires up the Ⓐ handling.
⚠️ **Tested, and dead.** Ⓑ from the main menu is the decoded edge `4 → 0`, so the
Ⓑ-returned title *is* phase 4 state 0 — and **Ⓐ works there**, opening the main
menu, with Ⓐ on the boot title as the control in the same run. Phase 4 state 0
handles Ⓐ, so the two-code-paths explanation is refuted. See
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md), which the
test did narrow usefully: only the **attract**-returned title is inert, not every
title after the first.
### ⚠️ One query in this section failed its own control
Counting `stw rX, 136(r30)` per phase returned **0 for phase 4**, which is known
to have 18 — the operand text is `r11, 136(r30)` and the pattern did not allow the
space. The per-phase store counts were discarded. The `bctr` half *does* pass its
control (phase 4 shows both known tables) and is reported above.
## ✅ The phase transitions — and they invalidate yesterday's test
Nine stores to the phase field `this+132` across the `GamePart_Title`
neighbourhood, attributed to the handler each lives in (control: the same query
shape finds 30 stores to `this+136`, ≥ the 18 known in phase 4):
| store | sets phase | lives in |
|---|---|---|
| `0x821c4fbc` | 2 | outside the handlers (entry/setup) |
| `0x821c4fe0`, `0x821c4fec` | 4 | outside the handlers |
| `0x821c5010` | *(not an immediate)* | outside the handlers |
| `0x821c592c` | **0** | phase 2 |
| `0x821c5b00` | **3** | phase 2 |
| `0x821c6370` | **4** | phase 3 |
| **`0x821c6e04`** | **2** | **phase 4, in the event-0 block** |
| `0x821c7950` | 2 | the dispatcher |
```
entry → 2 2 → 0 (splash) 2 → 3 3 → 4 4 → 2 (event 0)
```
### 🔴 Withdrawing the refutation from the previous iteration
The event-0 block sets **both** fields, one instruction apart:
```
821c6df8 li r11, 2
821c6e00 stw r28, 136(r30) ; state = 0
821c6e04 stw r11, 132(r30) ; phase = 2
```
So **Ⓑ from the menu lands in phase 2**, not phase 4 state 0. The previous section
tested "Ⓐ on the Ⓑ-returned title" believing it was probing phase 4 state 0 —
**it was probing phase 2**, the same phase as the boot title.
The test therefore says nothing about phase 4 state 0, and the refutation it
produced is **withdrawn**. Worse for me and better for the idea: Ⓐ working there
is exactly what the hypothesis predicts, since phase 2 is the phase that
references `BUTTON`.
**Status of the hypothesis: untested, and now consistent with two observations**
(Ⓐ works on the boot title and on the Ⓑ-returned title, both phase 2). What it
still needs is the **attract**-returned title's phase — the one thing no test so
far has read.

View File

@@ -643,3 +643,58 @@ Two lessons, both cheap:
The evidence recovered cleanly once the interference stopped — the interrupted
batch's own run 3 had already reached the menu with the same 40 calls / 6
`ResolvePath` signature.
## 2026-08-28 — a candidate explanation, from the static side
This page's oldest open question is **why the attract-returned title accepts no
input while the boot title does**, given that a draw capture in each state is
identical (13 quads, same rects).
`GamePart_Title` turns out to dispatch on an outer **phase** field at `this+132`,
and **the title screen is installed from two different phases** — phase 2
(`sub_821C5818`, which references `TITLE_SCREEN` *and* `BUTTON`) and phase 4's
state 0 (`sub_821C6458`). Same screen, different code. See
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md).
**Candidate:** the boot title is phase 2; the attract-returned title is phase 4
state 0; only one of them wires up Ⓐ.
### ⚠️ Tested the same day, and the TEST was invalid — see the withdrawal below
There was a cheaper test than reading `this+132`: the state graph says Ⓑ from the
main menu goes `4 → 0`, so **the Ⓑ-returned title *is* phase 4 state 0**. If phase
4 state 0 were the inert one, Ⓐ would not work there.
**It works.** Measured:
| | |
|---|---|
| *control* — Ⓐ on the boot title | → main menu ✅ |
| Ⓐ on the **Ⓑ-returned** title | → **main menu** ✅ |
**🔴 Withdrawn.** Reading the event-0 block in full shows it sets **two** fields,
one instruction apart: `stw r28, 136(r30)` (state = 0) *and* `stw r11, 132(r30)`
with `r11 = 2`**phase = 2**. So Ⓑ lands in **phase 2**, the same phase as the
boot title, and this test never probed phase 4 state 0 at all.
The refutation is withdrawn. Ⓐ working on the Ⓑ-returned title is exactly what
the hypothesis predicts — phase 2 is the phase that references `BUTTON`.
### ✅ What the test DID establish — the puzzle is narrower than this page says
(This part stands: it is a measurement, independent of which phase the returned
title turns out to be.)
This page frames the problem as *the boot title accepts Ⓐ, a later title does
not*. That is too broad. **The Ⓑ-returned title accepts Ⓐ perfectly well** — a
case nobody had tried. Only the **attract-returned** title is inert.
So whatever makes the title stop responding is tied to the attract cycle
specifically — the movie playing and coming back — not to "the title has been
shown before". That is a much smaller haystack than the one this page has been
pointing at.
⚠️ Also reproduced in passing: 4 s after Ⓑ the plate is **absent** (169 plate
pixels), and it fades in after. A test that samples too early will read the
returned title as plate-less, which this corpus has already been caught by once.

View File

@@ -0,0 +1,364 @@
# 🔴 Why the boot harness stopped reaching the title — `screenshot` costs 10.8 s
**Status:****diagnosed, with a control.** Four consecutive runs on
2026-08-28/29 failed to reach the interactive title, across two locales, two
launch paths and two display-gamma settings. The cause is none of those.
## The measurement
| condition | one `screenshot` call |
|---|---|
| while Xenia Canary is running | **10.8 s** |
| immediately after killing it | **0.117 s** |
**92×.** The 1-minute load average at the slow measurement was 1.80, so this is
contention with the emulator (both go through the same X server), not general
system load.
## Why that breaks the harness
`skip_intro.sh` takes **two** grabs 0.6 s apart per iteration, plus an
`is_title.py` numpy load. Its actual sample timestamps in the last run:
```
57, 95, 177, 218, 238, 298, 333, 380, 426, 471, 515 → intervals
38, 82, 41, 20, 30, 30, 35, 47, 46, 45, 44, 43, 21, 19 median 41 s
```
A **41-second** sampling interval against a title screen that the corpus already
documents as lasting *"a few seconds"* before auto-returning to the attract loop
(`wait_title.sh`'s own header). The harness is not seeing a stuck game; it is
blinking slower than the thing it is looking for.
That is also why runs at 16:4318:05 the same day succeeded and later ones did
not — nothing about the game changed.
## 🔴 What this retracts
Three earlier conclusions were built on these runs and are **withdrawn as
causes**, though the observations stand:
* *"the Japanese-locale run never reaches the interactive title"* — it may well
have appeared, unsampled.
* *"neither locale reaches the interactive title without a pad press"* — the
English control shared the same defect, so it controlled for locale but not for
the sampling rate.
* *"the game sat in the attract loop for 604 s"* — what was observed is that
**every one of ~15 samples** landed on movie content, which at a 41 s interval
is a much weaker statement than it reads as.
## ⚠️ A confound I introduced
Setting `kernel_display_gamma_type = 0` makes the frame substantially brighter
(a mid-attract frame measured mean **122.8** against **52.5** and **82.8** on
comparable phases at type 2). `skip_intro.sh` classifies movie-vs-static on an
**absolute** rmse threshold of 1500 between two grabs, so a brighter output
inflates that difference and biases every frame toward "movie". **The capture
harness's tuning is coupled to the display settings** — changing gamma and
capture behaviour in one run confounds both.
## 🔴 The fix works — and it REFUTES the diagnosis above
Built and measured (`tools/re-capture/fast_title_probe.py`): one long-lived
`ffmpeg` x11grab stream, raw RGB frames, glyph counted in numpy. No per-sample
process startup, no PNG encode, no `convert -crop`.
| probe | seconds per sample, emulator running |
|---|---|
| the wrapper `screenshot` | **3.98** |
| `import -window root` → PPM | 1.20 |
| **long-lived x11grab stream** | **0.29** |
**13.7× faster**, and the counter is control-verified against the committed
frames — it returns **753** on `live-title-press-a.png` and **327** on
`live-main-menu.png`, byte-identical to `is_title.py`.
Then it was pointed at a running game:
```
332 frames in 85.3 s = 3.89 fps; max glyph 0
1674 frames in 420.0 s = 3.99 fps; max glyph 0
```
**1 674 consecutive samples over seven unbroken minutes, four per second, and the
interactive title never appeared.** So the sampling rate was a real defect and
*not* the cause. The hypothesis on this page — that the harness was blinking
slower than the event — is **mine, and refuted by my own fix**.
## What that restores
Last iteration I withdrew three conclusions on the strength of that hypothesis.
The withdrawal was right at the time (15 samples at 41 s intervals cannot support
them) and is now **superseded by better evidence**: dense sampling says the
interactive title genuinely does not appear in a mid-run window. Reinstated as a
**measurement**, with its reach:
* ✅ over **420 continuous seconds**, English, `gamma_type = 2`, ~13 minutes into
a run with no pad input, **zero** frames carried the green Ⓐ glyph.
* ⚠️ Reach: this covers a **mid-run** window only. It says nothing about the
first minutes of boot.
## 🟡 The leading hypothesis, not confirmed
The corpus already suspects the answer. `title_states_capture.sh` exists to test
*"whether the interactive one draws `ptbtn00` (the PRESS Ⓐ plate) and the other
does not"* — i.e. the title appears **twice**: once at the end of the boot
sequence, and again from the attract loop, and only the first may carry the
plate. If so, the plate's window is early and one-shot, and no amount of
mid-run sampling will ever find it.
That is consistent with everything measured, and it is **not confirmed**. The
test is to start the fast probe *before* the boot title — from t=0 rather than
attaching to a run already in progress.
## The original fix note, kept
Make the probe cheap enough to sample faster than the title window: grab a small
region rather than the full surface, drop the ImageMagick `convert` round trip,
or keep the glyph test in one long-lived process instead of re-importing numpy
per sample. None of that is done — this page is the diagnosis, and it is what
every remaining emulator-side question is waiting on.
---
## 🔴 The boot-window hypothesis is refuted too — and the surface is BLACK
**2026-08-29, third explanation and third refutation.** The named experiment was
to attach the fast probe at **t = 0** so the boot title could not be missed.
Done, on the default config, English:
```
2391 frames in 600.4 s = 3.98 fps; max glyph 0; hits 0
```
Ten minutes, sampled four times a second **from launch**, and the green Ⓐ glyph
never appeared. So "the plate only shows in an early boot window I keep missing"
is **mine, and refuted**.
Then the thing I should have checked first. Splitting the raw root grab by band:
| band | non-black | mean |
|---|---|---|
| y 044 (the GTK menu bar) | **100 %** | 210.5 |
| **y 45719 (the game surface)** | **0.08 %** | **0.07** |
**The game is rendering black**, reproducibly across back-to-back samples, while
the guest is alive and polling input (`XamInputGetKeystrokeEx` past 1 201 calls)
and `MEM-WATCH` keeps reporting. The crop and every pixel oracle were correct;
there was nothing on the surface to detect.
### ⚠️ What this does and does not explain
**It does not retroactively explain the earlier failures**, and saying so would
be the fourth over-reach in a row. Those runs had *content*: run 2 sampled frames
at mean 33.1 (warm), run 3's classifier measured real frame-to-frame rmse, and
the `gamma_type = 0` run measured mean 122.8. A black surface is **this run's**
symptom.
So the honest reading is that the failure mode **changed** over the session, and
the black screen is a new and worse one.
### 🟡 Hypothesis for the regression, untested
Canary's shader/pipeline cache at `~/.local/share/Xenia/cache` is **47 MB** and
was last written **23:49 on 2026-08-28** — during the failed runs. This session
has `kill -9`'d the emulator repeatedly, which can leave a partially written
cache. A corrupt pipeline cache is a plausible route to a guest that runs while
nothing reaches the screen.
**The test is one line and one run:** move `cache*` aside and boot again. Not
done — it needs a fresh 10-minute run, and this iteration had spent its emulator
budget proving the negative above.
---
## 🔴 The fast probe STALLS — and that invalidates its own dense negatives
**2026-08-29.** The instrument built last iteration is unfit for long runs, found
by cross-checking it against an independent grabber *while both watched the same
screen*.
A single long-lived `ffmpeg` x11grab stream degrades and then freezes:
```
862 frames in 540.1 s = 1.60 fps (it starts at 3.98 fps)
t=450s surface mean 5.21 nonblack 10.1%
t=480s surface mean 5.21 nonblack 10.1%
t=510s surface mean 5.21 nonblack 10.1%
t=540s surface mean 5.21 nonblack 10.1%
```
Four consecutive 30-second marks reporting an identical value. At that same
moment an `import` grab of the same display read **surface mean 125.65**, and a
*freshly started* ffmpeg stream read **122.43** — agreeing with `import` to 3 %.
So the acquisition, not the analysis, was broken: the stream was replaying a
stale frame while the screen was 24× brighter.
🔴 **Therefore the previous section's headline is withdrawn.** "2 391 frames over
600 s from t=0, max glyph 0" cannot distinguish *the title never appeared* from
*the stream froze early and repeated one frame 2 391 times*. Its 3.98 fps was
measured over the first 20 s, before the degradation.
**Fixed** in `tools/re-capture/fast_title_probe.py`: the stream is torn down and
restarted every 30 s. Startup costs ~0.3 s, cheap against the title's window, and
it buys a guarantee the frames are live.
## ✅ Clearing the shader cache restored rendering
The cache hypothesis from the previous section was tested: `cache`, `cache0`,
`cache1` and `cache_host` moved aside (to `/tmp/xenia-cache-aside`, not deleted),
then a fresh boot.
The surface **renders again**, confirmed with the independent grabber:
`import` reads surface mean **54.8** and **68.6** on successive samples with 100 %
non-black warm content, against **0.07** and 0.08 % non-black in the black run.
The probe's own summary agrees where it can be trusted — **773 of 862 frames had
> 2 % non-black**.
🟡 **Supported, not proven.** One run each side, and the emulator was `kill -9`'d
many times before the black run, so a corrupt pipeline cache is plausible but a
single A/B does not exclude run-to-run variation. The old caches are kept in
`/tmp/xenia-cache-aside` if anyone wants to reproduce the failure.
**Still no title.** Even rendering, `max glyph 0` — but that number now comes
from a stalling probe, so it establishes nothing either way.
---
## ✅ A negative I can finally stand behind — measured with a self-validating probe
**2026-08-29.** Three earlier "the title never appears" claims came from
instruments that were later found broken (a stale pixel oracle, a 41 s sampling
interval, a freezing stream). This run used
[`tools/re-capture/title_probe_xchecked.py`](../../tools/re-capture/title_probe_xchecked.py),
which restarts its stream every 30 s **and cross-checks itself against an
independent `import` grab every 60 s**, printing both numbers.
```
1851 frames in 560.2 s = 3.30 fps
cross-checks 9, disagreements 1
max glyph 0
```
| t | stream | import | |
|---|---|---|---|
| 62 s | 6.05 | 0.07 | disagree — a fade, the boot logos are mid-transition |
| 123 s | 7.40 | 7.49 | agree |
| 183 s | 8.18 | 8.29 | agree |
| 243 s | 0.23 | 0.10 | agree |
| 311 s | 89.68 | 89.51 | agree |
| 371 s | 80.97 | 81.58 | agree |
| 426 s | 117.43 | 117.72 | agree |
| 487 s | 77.71 | 76.25 | agree |
| 546 s | 70.43 | 70.55 | agree |
Eight of nine agree to within 2 %, the fps held at 3.30 (no collapse to 1.60),
and the surface plainly moved through dark and bright phases. **The frames were
live and the negative is real:**
> ✅ **Measured — over 560 continuous seconds from launch, sampled 3.3 times a
> second by a cross-validated probe, the interactive title's green Ⓐ plate never
> appears, while the game renders throughout.**
The final frame correlates **0.0145** with our title render, **0.0047** with the
main menu and **0.0102** with `EXTRAS` — it is attract-movie content, not a UI
screen at all.
## ❔ Why, still unknown
`live-title-press-a.png` is committed and carries 753 glyph pixels, so the title
*was* reachable from this container on 2026-08-28. Nothing found so far explains
the change. Clearing the shader cache fixed the *black surface* but not this.
## Where this leaves the emulator-side questions
The two open items that need a running menu — the gamma control
([tone curve](structures/ui-render-tone-curve.md)) and separating `8AX` from
`ptbase` ([8AX](structures/ui-8ax-fullres-background.md)) — remain blocked, now
on a well-characterised and instrument-verified failure rather than on a
suspicion. Neither blocks the five menu screens. **Returning to static work**;
the probe is committed for whoever picks this up.
---
## ❔ The GPU trace route: attempted, produced nothing, characterised
**2026-08-29.** To turn the gamma-ramp *inference* into an observation, canary's
`trace_gpu_stream` should work — it records gamma ramps as their own command type
(`kGammaRamp`, index 11 in `TraceCommandType`). Two bounded runs produced **no
trace file at all**: nothing under the prefix, no `.xtr` anywhere, no
`scratch/gpu/`.
Bounded deliberately: the container's disk is at **95 % (50 GiB free)** and a
boot-time trace of all GPU packets includes video decode, so the runner carried a
watchdog killing the emulator the moment the trace passed a 2 GiB cap. It never
fired — there was nothing to cap. Disk was unchanged at 95 % throughout.
What the attempt did establish:
* `BeginTracing()` is called at GPU init when the cvar is set
(`graphics_system.cc:237`), but `EndTracing()` runs only from
`GraphicsSystem::Shutdown()`. **A `kill -9` — which this session has used
routinely — can never finalise a trace.** The second run was therefore stopped
with `SIGTERM` and exited cleanly. Still no file, so that is not the whole
story.
* ~~Two candidates remain and were **not** separated: the CLI flag not reaching
the cvar, or `BeginTracing()` failing silently.~~
### ✅ Explained — and it was neither candidate
**The trace writer is compiled out of the build in use.** Following the code:
`BeginTracing()` only sets `trace_state_ = kStreaming` — *"Streaming starts on
the next primary buffer execute"* — and the file is opened later, in
`ExecutePrimaryBuffer`, inside
```cpp
#if XE_ENABLE_TRACE_WRITER_INSTRUMENTATION == 1
```
which `trace_writer.h` defines as:
```cpp
#ifdef NDEBUG
#define XE_ENABLE_TRACE_WRITER_INSTRUMENTATION 0 // release
#else
#define XE_ENABLE_TRACE_WRITER_INSTRUMENTATION 1 // debug
#endif
```
Confirmed in the binaries themselves, with a control. The format string
`"{:08X}_stream.xtr"` exists only inside that guard:
| binary | `_stream.xtr` occurrences |
|---|---|
| `build/bin/Linux/Release/xenia_canary` | **0** |
| `build/bin/Linux/Debug/xenia_canary` | **1** |
| `/sylph-home/re/canary-build/.../Release/xenia_canary`**the one `run-canary` uses** | **0** |
So `trace_gpu_stream` is a **no-op in this container's emulator**: the cvar
parses, `BeginTracing` runs, and nothing can ever open a file. Neither the CLI
flag nor `BeginTracing` was at fault, and neither was the `kill -9` — though that
would have destroyed the trace too, had one existed.
🟡 **The route exists but is not cheap.** A Debug build with the writer compiled
in is present at `build/bin/Linux/Debug/xenia_canary` (253 MB against Release's
18 MB). Running it means a much slower boot and a trace of every GPU packet on a
disk at 95 %. Recorded as available rather than attempted — the thing it would
confirm (the `DC_LUT` write) is already a well-supported inference, so the cost
is out of proportion to the gain.
## ⚠️ The config dump in a log is the FILE, not the effective command line
Nearly a wrong conclusion here. The dump printed at startup showed
`trace_gpu_stream = false` after I had passed `--trace_gpu_stream=true`, which
reads as "the flag was ignored". It is not evidence either way:
| | passed on the CLI | shown in the dump | actual behaviour |
|---|---|---|---|
| gamma run | `--log_mask=12 --log_level=3` | `log_mask = 0`, `log_level = 2` | **Kernel Debug logging demonstrably ON** |
The gamma run's flags plainly took effect — that run is where
`VdGetCurrentDisplayGamma` was captured — while its dump showed the file's
values. So the dump reflects the config file and cannot confirm or refute a
command-line override.

View File

@@ -0,0 +1,53 @@
# The oracle frames — what to verify a render against
**These are framebuffer captures of the real game running under Xenia Canary.**
They are the reference. `sylpheed-cli screen render` is **not** — Reborn is a GUI
explorer and extraction CLI built to check that our *decoding* is right, and it
may very well be wrong. Where a render and a capture disagree, the capture wins,
and the render is the thing to go and fix.
⚠️ Two renderers agreeing proves nothing: they share our assumptions. This corpus
has been bitten by exactly that three times — the dropped `pteff05` background,
the scale-0 rect, and `rest()`. Each was invisible to any render-vs-render diff
and visible immediately against a capture.
## The frames
All are **1279×675**, top-left aligned, cropped to the game surface by the
screenshot tool (the guest renders 1280×720; the missing row/column is the crop,
not a scale).
| screen | capture |
|---|---|
| publisher splash (SQUARE ENIX) | [`title-builds/live-splash-publisher.png`](title-builds/live-splash-publisher.png) |
| developer splash (GAME ARTS / SETA / anima) | [`title-builds/live-splash-developer.png`](title-builds/live-splash-developer.png) |
| title, **without** the `PRESS Ⓐ` plate | [`title-builds/live-title-build4-no-plate.png`](title-builds/live-title-build4-no-plate.png) |
| title, **with** the plate | [`title-builds/live-title-press-a.png`](title-builds/live-title-press-a.png) |
| main menu | [`title-builds/live-main-menu.png`](title-builds/live-main-menu.png) · [`main-menu-oracle.png`](main-menu-oracle.png) |
| main menu, **`OPTIONS` focused** | [`title-builds/live-main-menu-options-focused.png`](title-builds/live-main-menu-options-focused.png) |
| `EXTRAS` | [`title-builds/live-extras.png`](title-builds/live-extras.png) |
| title (alternate) | [`title-screen-oracle.png`](title-screen-oracle.png) |
| a screen transition, 13 frames | [`transitions/transition-filmstrip.png`](transitions/transition-filmstrip.png) + [`transition-luminance.csv`](transitions/transition-luminance.csv) |
The **focused** pair is the useful one for button states: the same screen with a
different button highlighted, so the difference isolates what focus changes.
## ⚠️ Before you compute an RMSE against one
* **They are not gamma-neutral.** `capture ≈ 255·(render/255)^γ` with γ ≈ 1.341.49,
and that is a ramp **the game installed**, not a capture-path artefact. So RMSE
against these has a floor and chasing it below that floor is chasing the ramp.
[`../structures/ui-render-tone-curve.md`](../structures/ui-render-tone-curve.md)
* **Geometry is sound**: cross-correlating a render against `live-main-menu.png`
over ±6 px puts the best alignment at exactly (0,0), correlation 0.9466. So a
positional disagreement is real, not a crop artefact.
* **A capture is one moment.** Several of these screens are still animating; the
title's two `ptloop` sweeps move continuously. Compare settled poses, or
compare regions you know are at rest.
## What is NOT here
No capture of the interactive title reached mid-run without a pad press — three
runs across two locales and two launch paths never reached it in ~35 minutes.
See [`../capture-harness-status.md`](../capture-harness-status.md). And no
`GP_READY_ROOM` capture; S1 ruled it out of scope.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

View File

@@ -0,0 +1,43 @@
# every draw in SUBMISSION ORDER, undeduplicated, frames 1..3
# tex dimensions identify the sprite; base is the guest address
0 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
vb=0x15036638 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [-0.50,-0.50,z=0.00000] [1279.50,-0.50,z=0.00000] [1279.50,719.50,z=0.00000]
1 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
vb=0x15036690 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
2 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
vb=0x150366F0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [0.70,1.58,z=0.00000,col=C3FFFFFF] [1.24,1.02,z=0.00000,col=C3FFFFFF] [0.40,-1.57,z=0.00000,col=C3FFFFFF] [-0.14,-1.02,z=0.00000,col=C3FFFFFF] [-1.29,1.02,z=0.00000,col=B6FFFFFF] [-0.85,1.81,z=0.00000,col=B6FFFFFF] [0.75,-1.02,z=0.00000,col=B6FFFFFF] [0.31,-1.81,z=0.00000,col=B6FFFFFF]
3 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
vb=0x150367B0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
4 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x15036810 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.90,0.68,z=0.00000,col=FFFFFFFF] [0.87,0.68,z=0.00000,col=FFFFFFFF] [0.87,-0.10,z=0.00000,col=FFFFFFFF] [-0.90,-0.10,z=0.00000,col=FFFFFFFF] [-0.89,0.65,z=0.00000,col=FFFFFFFF] [0.86,0.65,z=0.00000,col=FFFFFFFF] [0.86,-0.08,z=0.00000,col=FFFFFFFF] [-0.89,-0.08,z=0.00000,col=FFFFFFFF]
5 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
vb=0x150368D0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.71,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.15,z=0.00000,col=FFFFFFFF] [-0.71,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.10,z=0.00000,col=FFFFFFFF] [0.76,-0.10,z=0.00000,col=FFFFFFFF]
6 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
vb=0x15036990 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.79,0.15,z=0.00000,col=FFFFFFFF] [0.76,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.14,z=0.00000,col=FFFFFFFF] [-0.79,-0.14,z=0.00000,col=FFFFFFFF]
7 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
vb=0x150369F0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.67,z=0.00000,col=FFFFFFFF] [-0.40,-0.67,z=0.00000,col=FFFFFFFF]
8 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x15036AB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.42,-0.49,z=0.00000,col=31FFFFFF] [0.42,-0.49,z=0.00000,col=31FFFFFF] [0.42,-0.70,z=0.00000,col=31FFFFFF] [-0.42,-0.70,z=0.00000,col=31FFFFFF]
9 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
10 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
--- frame 3 ---
11 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
vb=0x15076C38 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [-0.50,-0.50,z=0.00000] [1279.50,-0.50,z=0.00000] [1279.50,719.50,z=0.00000]
12 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
vb=0x15076C90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
13 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
vb=0x15076CF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [0.73,1.58,z=0.00000,col=C5FFFFFF] [1.27,1.02,z=0.00000,col=C5FFFFFF] [0.43,-1.57,z=0.00000,col=C5FFFFFF] [-0.11,-1.02,z=0.00000,col=C5FFFFFF] [-1.32,1.02,z=0.00000,col=B8FFFFFF] [-0.88,1.81,z=0.00000,col=B8FFFFFF] [0.71,-1.02,z=0.00000,col=B8FFFFFF] [0.27,-1.81,z=0.00000,col=B8FFFFFF]
14 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
vb=0x15076DB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
15 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x15076E10 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.90,0.68,z=0.00000,col=FFFFFFFF] [0.87,0.68,z=0.00000,col=FFFFFFFF] [0.87,-0.10,z=0.00000,col=FFFFFFFF] [-0.90,-0.10,z=0.00000,col=FFFFFFFF] [-0.89,0.65,z=0.00000,col=FFFFFFFF] [0.86,0.65,z=0.00000,col=FFFFFFFF] [0.86,-0.08,z=0.00000,col=FFFFFFFF] [-0.89,-0.08,z=0.00000,col=FFFFFFFF]
16 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
vb=0x15076ED0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.71,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.15,z=0.00000,col=FFFFFFFF] [-0.71,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.10,z=0.00000,col=FFFFFFFF] [0.76,-0.10,z=0.00000,col=FFFFFFFF]
17 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
vb=0x15076F90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.79,0.15,z=0.00000,col=FFFFFFFF] [0.76,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.14,z=0.00000,col=FFFFFFFF] [-0.79,-0.14,z=0.00000,col=FFFFFFFF]
18 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
vb=0x15076FF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.67,z=0.00000,col=FFFFFFFF] [-0.40,-0.67,z=0.00000,col=FFFFFFFF]
19 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
vb=0x150770B0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.42,-0.49,z=0.00000,col=1EFFFFFF] [0.42,-0.49,z=0.00000,col=1EFFFFFF] [0.42,-0.70,z=0.00000,col=1EFFFFFF] [-0.42,-0.70,z=0.00000,col=1EFFFFFF]
20 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
21 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -0,0 +1,421 @@
frame,seconds,mean_luminance
0,0.0000,26.045
1,0.0333,26.054
2,0.0667,26.051
3,0.1000,26.054
4,0.1333,26.049
5,0.1667,26.054
6,0.2000,26.052
7,0.2333,26.053
8,0.2667,26.046
9,0.3000,26.053
10,0.3333,26.049
11,0.3667,26.052
12,0.4000,26.047
13,0.4333,26.050
14,0.4667,26.048
15,0.5000,26.049
16,0.5333,26.047
17,0.5667,26.049
18,0.6000,26.048
19,0.6333,26.048
20,0.6667,26.045
21,0.7000,26.048
22,0.7333,26.049
23,0.7667,26.048
24,0.8000,26.046
25,0.8333,26.048
26,0.8667,26.048
27,0.9000,26.049
28,0.9333,26.047
29,0.9667,26.048
30,1.0000,26.048
31,1.0333,26.048
32,1.0667,26.047
33,1.1000,26.048
34,1.1333,26.047
35,1.1667,26.047
36,1.2000,26.046
37,1.2333,26.046
38,1.2667,26.046
39,1.3000,26.047
40,1.3333,26.046
41,1.3667,26.047
42,1.4000,26.047
43,1.4333,26.048
44,1.4667,26.047
45,1.5000,26.049
46,1.5333,26.050
47,1.5667,26.052
48,1.6000,26.051
49,1.6333,26.052
50,1.6667,26.052
51,1.7000,26.052
52,1.7333,26.052
53,1.7667,26.055
54,1.8000,26.056
55,1.8333,26.056
56,1.8667,26.055
57,1.9000,26.058
58,1.9333,26.059
59,1.9667,26.059
60,2.0000,26.057
61,2.0333,26.062
62,2.0667,26.022
63,2.1000,22.032
64,2.1333,21.744
65,2.1667,17.909
66,2.2000,13.996
67,2.2333,13.966
68,2.2667,11.113
69,2.3000,8.072
70,2.3333,7.862
71,2.3667,6.270
72,2.4000,3.375
73,2.4333,1.771
74,2.4667,0.020
75,2.5000,0.020
76,2.5333,0.020
77,2.5667,0.020
78,2.6000,0.020
79,2.6333,0.020
80,2.6667,0.020
81,2.7000,5.985
82,2.7333,5.991
83,2.7667,5.990
84,2.8000,5.991
85,2.8333,5.986
86,2.8667,7.432
87,2.9000,9.300
88,2.9333,9.283
89,2.9667,9.484
90,3.0000,11.294
91,3.0333,11.263
92,3.0667,11.606
93,3.1000,12.608
94,3.1333,12.807
95,3.1667,12.790
96,3.2000,14.517
97,3.2333,18.497
98,3.2667,26.935
99,3.3000,27.008
100,3.3333,27.010
101,3.3667,27.039
102,3.4000,27.036
103,3.4333,27.083
104,3.4667,27.239
105,3.5000,27.294
106,3.5333,27.293
107,3.5667,27.294
108,3.6000,27.289
109,3.6333,27.291
110,3.6667,27.290
111,3.7000,27.291
112,3.7333,27.289
113,3.7667,27.292
114,3.8000,27.291
115,3.8333,27.292
116,3.8667,27.291
117,3.9000,27.294
118,3.9333,27.291
119,3.9667,27.293
120,4.0000,27.290
121,4.0333,27.292
122,4.0667,27.291
123,4.1000,27.292
124,4.1333,27.290
125,4.1667,27.293
126,4.2000,27.293
127,4.2333,27.293
128,4.2667,27.292
129,4.3000,27.297
130,4.3333,27.296
131,4.3667,27.297
132,4.4000,27.296
133,4.4333,27.299
134,4.4667,27.300
135,4.5000,27.302
136,4.5333,27.301
137,4.5667,27.304
138,4.6000,27.304
139,4.6333,27.304
140,4.6667,27.302
141,4.7000,27.306
142,4.7333,27.308
143,4.7667,27.309
144,4.8000,27.309
145,4.8333,27.312
146,4.8667,27.311
147,4.9000,27.311
148,4.9333,27.310
149,4.9667,27.317
150,5.0000,27.319
151,5.0333,27.321
152,5.0667,27.322
153,5.1000,27.327
154,5.1333,27.327
155,5.1667,27.328
156,5.2000,27.327
157,5.2333,27.333
158,5.2667,27.334
159,5.3000,27.335
160,5.3333,27.334
161,5.3667,27.344
162,5.4000,27.347
163,5.4333,27.350
164,5.4667,27.351
165,5.5000,27.359
166,5.5333,27.359
167,5.5667,27.360
168,5.6000,27.358
169,5.6333,27.365
170,5.6667,27.366
171,5.7000,27.369
172,5.7333,27.367
173,5.7667,27.383
174,5.8000,27.386
175,5.8333,27.387
176,5.8667,27.386
177,5.9000,27.399
178,5.9333,27.400
179,5.9667,27.402
180,6.0000,27.400
181,6.0333,27.413
182,6.0667,27.414
183,6.1000,27.415
184,6.1333,27.413
185,6.1667,27.432
186,6.2000,27.434
187,6.2333,27.438
188,6.2667,27.436
189,6.3000,27.449
190,6.3333,27.448
191,6.3667,27.451
192,6.4000,27.449
193,6.4333,27.466
194,6.4667,27.471
195,6.5000,27.472
196,6.5333,27.470
197,6.5667,27.480
198,6.6000,27.478
199,6.6333,27.480
200,6.6667,27.476
201,6.7000,27.509
202,6.7333,27.510
203,6.7667,27.516
204,6.8000,27.514
205,6.8333,27.519
206,6.8667,27.519
207,6.9000,27.518
208,6.9333,27.516
209,6.9667,27.538
210,7.0000,27.537
211,7.0333,27.539
212,7.0667,27.538
213,7.1000,27.558
214,7.1333,27.556
215,7.1667,27.558
216,7.2000,27.554
217,7.2333,27.577
218,7.2667,27.576
219,7.3000,27.577
220,7.3333,27.572
221,7.3667,27.596
222,7.4000,27.598
223,7.4333,27.603
224,7.4667,27.597
225,7.5000,27.610
226,7.5333,27.611
227,7.5667,27.613
228,7.6000,27.607
229,7.6333,27.621
230,7.6667,27.621
231,7.7000,27.622
232,7.7333,27.617
233,7.7667,27.624
234,7.8000,27.626
235,7.8333,27.630
236,7.8667,27.622
237,7.9000,27.640
238,7.9333,27.637
239,7.9667,27.639
240,8.0000,27.633
241,8.0333,27.642
242,8.0667,27.639
243,8.1000,27.642
244,8.1333,27.637
245,8.1667,27.658
246,8.2000,27.653
247,8.2333,27.655
248,8.2667,27.649
249,8.3000,27.645
250,8.3333,27.648
251,8.3667,26.704
252,8.4000,23.887
253,8.4333,23.723
254,8.4667,16.471
255,8.5000,13.119
256,8.5333,13.102
257,8.5667,12.118
258,8.6000,10.955
259,8.6333,10.956
260,8.6667,5.365
261,8.7000,3.422
262,8.7333,3.416
263,8.7667,2.820
264,8.8000,0.020
265,8.8333,0.020
266,8.8667,0.020
267,8.9000,0.020
268,8.9333,0.020
269,8.9667,3.267
270,9.0000,5.012
271,9.0333,5.449
272,9.0667,7.339
273,9.1000,9.208
274,9.1333,10.415
275,9.1667,10.418
276,9.2000,10.462
277,9.2333,10.459
278,9.2667,10.569
279,9.3000,10.701
280,9.3333,10.868
281,9.3667,12.924
282,9.4000,14.688
283,9.4333,14.756
284,9.4667,17.184
285,9.5000,19.441
286,9.5333,19.438
287,9.5667,19.492
288,9.6000,19.492
289,9.6333,19.492
290,9.6667,19.508
291,9.7000,23.405
292,9.7333,23.417
293,9.7667,23.414
294,9.8000,23.416
295,9.8333,23.409
296,9.8667,23.467
297,9.9000,24.927
298,9.9333,24.929
299,9.9667,24.926
300,10.0000,25.788
301,10.0333,25.785
302,10.0667,25.789
303,10.1000,25.893
304,10.1333,25.908
305,10.1667,25.905
306,10.2000,25.905
307,10.2333,25.976
308,10.2667,25.986
309,10.3000,25.980
310,10.3333,25.981
311,10.3667,25.976
312,10.4000,25.984
313,10.4333,26.020
314,10.4667,26.022
315,10.5000,26.016
316,10.5333,26.021
317,10.5667,26.018
318,10.6000,26.020
319,10.6333,26.016
320,10.6667,26.021
321,10.7000,26.021
322,10.7333,26.021
323,10.7667,26.019
324,10.8000,26.022
325,10.8333,26.022
326,10.8667,26.022
327,10.9000,26.020
328,10.9333,26.025
329,10.9667,26.025
330,11.0000,26.025
331,11.0333,26.023
332,11.0667,26.029
333,11.1000,26.027
334,11.1333,26.028
335,11.1667,26.024
336,11.2000,26.024
337,11.2333,26.024
338,11.2667,26.024
339,11.3000,26.029
340,11.3333,26.029
341,11.3667,26.027
342,11.4000,26.029
343,11.4333,26.024
344,11.4667,26.027
345,11.5000,26.027
346,11.5333,26.027
347,11.5667,26.025
348,11.6000,26.031
349,11.6333,26.030
350,11.6667,26.033
351,11.7000,26.028
352,11.7333,26.032
353,11.7667,26.032
354,11.8000,26.032
355,11.8333,26.029
356,11.8667,26.034
357,11.9000,26.039
358,11.9333,26.041
359,11.9667,26.041
360,12.0000,26.040
361,12.0333,26.046
362,12.0667,26.047
363,12.1000,26.046
364,12.1333,26.050
365,12.1667,26.051
366,12.2000,26.051
367,12.2333,26.051
368,12.2667,26.051
369,12.3000,26.051
370,12.3333,26.051
371,12.3667,26.051
372,12.4000,26.050
373,12.4333,26.055
374,12.4667,26.056
375,12.5000,26.056
376,12.5333,26.055
377,12.5667,26.056
378,12.6000,26.056
379,12.6333,26.056
380,12.6667,26.055
381,12.7000,26.056
382,12.7333,26.056
383,12.7667,26.056
384,12.8000,26.055
385,12.8333,26.055
386,12.8667,26.056
387,12.9000,26.056
388,12.9333,26.056
389,12.9667,26.056
390,13.0000,26.055
391,13.0333,26.071
392,13.0667,26.072
393,13.1000,26.074
394,13.1333,26.082
395,13.1667,26.083
396,13.2000,26.083
397,13.2333,26.083
398,13.2667,26.083
399,13.3000,26.110
400,13.3333,26.111
401,13.3667,26.114
402,13.4000,26.113
403,13.4333,26.118
404,13.4667,26.119
405,13.5000,26.121
406,13.5333,26.119
407,13.5667,26.140
408,13.6000,26.140
409,13.6333,26.144
410,13.6667,26.142
411,13.7000,26.155
412,13.7333,26.157
413,13.7667,26.158
414,13.8000,26.157
415,13.8333,26.165
416,13.8667,26.165
417,13.9000,26.168
418,13.9333,26.165
419,13.9667,26.165
1 frame seconds mean_luminance
2 0 0.0000 26.045
3 1 0.0333 26.054
4 2 0.0667 26.051
5 3 0.1000 26.054
6 4 0.1333 26.049
7 5 0.1667 26.054
8 6 0.2000 26.052
9 7 0.2333 26.053
10 8 0.2667 26.046
11 9 0.3000 26.053
12 10 0.3333 26.049
13 11 0.3667 26.052
14 12 0.4000 26.047
15 13 0.4333 26.050
16 14 0.4667 26.048
17 15 0.5000 26.049
18 16 0.5333 26.047
19 17 0.5667 26.049
20 18 0.6000 26.048
21 19 0.6333 26.048
22 20 0.6667 26.045
23 21 0.7000 26.048
24 22 0.7333 26.049
25 23 0.7667 26.048
26 24 0.8000 26.046
27 25 0.8333 26.048
28 26 0.8667 26.048
29 27 0.9000 26.049
30 28 0.9333 26.047
31 29 0.9667 26.048
32 30 1.0000 26.048
33 31 1.0333 26.048
34 32 1.0667 26.047
35 33 1.1000 26.048
36 34 1.1333 26.047
37 35 1.1667 26.047
38 36 1.2000 26.046
39 37 1.2333 26.046
40 38 1.2667 26.046
41 39 1.3000 26.047
42 40 1.3333 26.046
43 41 1.3667 26.047
44 42 1.4000 26.047
45 43 1.4333 26.048
46 44 1.4667 26.047
47 45 1.5000 26.049
48 46 1.5333 26.050
49 47 1.5667 26.052
50 48 1.6000 26.051
51 49 1.6333 26.052
52 50 1.6667 26.052
53 51 1.7000 26.052
54 52 1.7333 26.052
55 53 1.7667 26.055
56 54 1.8000 26.056
57 55 1.8333 26.056
58 56 1.8667 26.055
59 57 1.9000 26.058
60 58 1.9333 26.059
61 59 1.9667 26.059
62 60 2.0000 26.057
63 61 2.0333 26.062
64 62 2.0667 26.022
65 63 2.1000 22.032
66 64 2.1333 21.744
67 65 2.1667 17.909
68 66 2.2000 13.996
69 67 2.2333 13.966
70 68 2.2667 11.113
71 69 2.3000 8.072
72 70 2.3333 7.862
73 71 2.3667 6.270
74 72 2.4000 3.375
75 73 2.4333 1.771
76 74 2.4667 0.020
77 75 2.5000 0.020
78 76 2.5333 0.020
79 77 2.5667 0.020
80 78 2.6000 0.020
81 79 2.6333 0.020
82 80 2.6667 0.020
83 81 2.7000 5.985
84 82 2.7333 5.991
85 83 2.7667 5.990
86 84 2.8000 5.991
87 85 2.8333 5.986
88 86 2.8667 7.432
89 87 2.9000 9.300
90 88 2.9333 9.283
91 89 2.9667 9.484
92 90 3.0000 11.294
93 91 3.0333 11.263
94 92 3.0667 11.606
95 93 3.1000 12.608
96 94 3.1333 12.807
97 95 3.1667 12.790
98 96 3.2000 14.517
99 97 3.2333 18.497
100 98 3.2667 26.935
101 99 3.3000 27.008
102 100 3.3333 27.010
103 101 3.3667 27.039
104 102 3.4000 27.036
105 103 3.4333 27.083
106 104 3.4667 27.239
107 105 3.5000 27.294
108 106 3.5333 27.293
109 107 3.5667 27.294
110 108 3.6000 27.289
111 109 3.6333 27.291
112 110 3.6667 27.290
113 111 3.7000 27.291
114 112 3.7333 27.289
115 113 3.7667 27.292
116 114 3.8000 27.291
117 115 3.8333 27.292
118 116 3.8667 27.291
119 117 3.9000 27.294
120 118 3.9333 27.291
121 119 3.9667 27.293
122 120 4.0000 27.290
123 121 4.0333 27.292
124 122 4.0667 27.291
125 123 4.1000 27.292
126 124 4.1333 27.290
127 125 4.1667 27.293
128 126 4.2000 27.293
129 127 4.2333 27.293
130 128 4.2667 27.292
131 129 4.3000 27.297
132 130 4.3333 27.296
133 131 4.3667 27.297
134 132 4.4000 27.296
135 133 4.4333 27.299
136 134 4.4667 27.300
137 135 4.5000 27.302
138 136 4.5333 27.301
139 137 4.5667 27.304
140 138 4.6000 27.304
141 139 4.6333 27.304
142 140 4.6667 27.302
143 141 4.7000 27.306
144 142 4.7333 27.308
145 143 4.7667 27.309
146 144 4.8000 27.309
147 145 4.8333 27.312
148 146 4.8667 27.311
149 147 4.9000 27.311
150 148 4.9333 27.310
151 149 4.9667 27.317
152 150 5.0000 27.319
153 151 5.0333 27.321
154 152 5.0667 27.322
155 153 5.1000 27.327
156 154 5.1333 27.327
157 155 5.1667 27.328
158 156 5.2000 27.327
159 157 5.2333 27.333
160 158 5.2667 27.334
161 159 5.3000 27.335
162 160 5.3333 27.334
163 161 5.3667 27.344
164 162 5.4000 27.347
165 163 5.4333 27.350
166 164 5.4667 27.351
167 165 5.5000 27.359
168 166 5.5333 27.359
169 167 5.5667 27.360
170 168 5.6000 27.358
171 169 5.6333 27.365
172 170 5.6667 27.366
173 171 5.7000 27.369
174 172 5.7333 27.367
175 173 5.7667 27.383
176 174 5.8000 27.386
177 175 5.8333 27.387
178 176 5.8667 27.386
179 177 5.9000 27.399
180 178 5.9333 27.400
181 179 5.9667 27.402
182 180 6.0000 27.400
183 181 6.0333 27.413
184 182 6.0667 27.414
185 183 6.1000 27.415
186 184 6.1333 27.413
187 185 6.1667 27.432
188 186 6.2000 27.434
189 187 6.2333 27.438
190 188 6.2667 27.436
191 189 6.3000 27.449
192 190 6.3333 27.448
193 191 6.3667 27.451
194 192 6.4000 27.449
195 193 6.4333 27.466
196 194 6.4667 27.471
197 195 6.5000 27.472
198 196 6.5333 27.470
199 197 6.5667 27.480
200 198 6.6000 27.478
201 199 6.6333 27.480
202 200 6.6667 27.476
203 201 6.7000 27.509
204 202 6.7333 27.510
205 203 6.7667 27.516
206 204 6.8000 27.514
207 205 6.8333 27.519
208 206 6.8667 27.519
209 207 6.9000 27.518
210 208 6.9333 27.516
211 209 6.9667 27.538
212 210 7.0000 27.537
213 211 7.0333 27.539
214 212 7.0667 27.538
215 213 7.1000 27.558
216 214 7.1333 27.556
217 215 7.1667 27.558
218 216 7.2000 27.554
219 217 7.2333 27.577
220 218 7.2667 27.576
221 219 7.3000 27.577
222 220 7.3333 27.572
223 221 7.3667 27.596
224 222 7.4000 27.598
225 223 7.4333 27.603
226 224 7.4667 27.597
227 225 7.5000 27.610
228 226 7.5333 27.611
229 227 7.5667 27.613
230 228 7.6000 27.607
231 229 7.6333 27.621
232 230 7.6667 27.621
233 231 7.7000 27.622
234 232 7.7333 27.617
235 233 7.7667 27.624
236 234 7.8000 27.626
237 235 7.8333 27.630
238 236 7.8667 27.622
239 237 7.9000 27.640
240 238 7.9333 27.637
241 239 7.9667 27.639
242 240 8.0000 27.633
243 241 8.0333 27.642
244 242 8.0667 27.639
245 243 8.1000 27.642
246 244 8.1333 27.637
247 245 8.1667 27.658
248 246 8.2000 27.653
249 247 8.2333 27.655
250 248 8.2667 27.649
251 249 8.3000 27.645
252 250 8.3333 27.648
253 251 8.3667 26.704
254 252 8.4000 23.887
255 253 8.4333 23.723
256 254 8.4667 16.471
257 255 8.5000 13.119
258 256 8.5333 13.102
259 257 8.5667 12.118
260 258 8.6000 10.955
261 259 8.6333 10.956
262 260 8.6667 5.365
263 261 8.7000 3.422
264 262 8.7333 3.416
265 263 8.7667 2.820
266 264 8.8000 0.020
267 265 8.8333 0.020
268 266 8.8667 0.020
269 267 8.9000 0.020
270 268 8.9333 0.020
271 269 8.9667 3.267
272 270 9.0000 5.012
273 271 9.0333 5.449
274 272 9.0667 7.339
275 273 9.1000 9.208
276 274 9.1333 10.415
277 275 9.1667 10.418
278 276 9.2000 10.462
279 277 9.2333 10.459
280 278 9.2667 10.569
281 279 9.3000 10.701
282 280 9.3333 10.868
283 281 9.3667 12.924
284 282 9.4000 14.688
285 283 9.4333 14.756
286 284 9.4667 17.184
287 285 9.5000 19.441
288 286 9.5333 19.438
289 287 9.5667 19.492
290 288 9.6000 19.492
291 289 9.6333 19.492
292 290 9.6667 19.508
293 291 9.7000 23.405
294 292 9.7333 23.417
295 293 9.7667 23.414
296 294 9.8000 23.416
297 295 9.8333 23.409
298 296 9.8667 23.467
299 297 9.9000 24.927
300 298 9.9333 24.929
301 299 9.9667 24.926
302 300 10.0000 25.788
303 301 10.0333 25.785
304 302 10.0667 25.789
305 303 10.1000 25.893
306 304 10.1333 25.908
307 305 10.1667 25.905
308 306 10.2000 25.905
309 307 10.2333 25.976
310 308 10.2667 25.986
311 309 10.3000 25.980
312 310 10.3333 25.981
313 311 10.3667 25.976
314 312 10.4000 25.984
315 313 10.4333 26.020
316 314 10.4667 26.022
317 315 10.5000 26.016
318 316 10.5333 26.021
319 317 10.5667 26.018
320 318 10.6000 26.020
321 319 10.6333 26.016
322 320 10.6667 26.021
323 321 10.7000 26.021
324 322 10.7333 26.021
325 323 10.7667 26.019
326 324 10.8000 26.022
327 325 10.8333 26.022
328 326 10.8667 26.022
329 327 10.9000 26.020
330 328 10.9333 26.025
331 329 10.9667 26.025
332 330 11.0000 26.025
333 331 11.0333 26.023
334 332 11.0667 26.029
335 333 11.1000 26.027
336 334 11.1333 26.028
337 335 11.1667 26.024
338 336 11.2000 26.024
339 337 11.2333 26.024
340 338 11.2667 26.024
341 339 11.3000 26.029
342 340 11.3333 26.029
343 341 11.3667 26.027
344 342 11.4000 26.029
345 343 11.4333 26.024
346 344 11.4667 26.027
347 345 11.5000 26.027
348 346 11.5333 26.027
349 347 11.5667 26.025
350 348 11.6000 26.031
351 349 11.6333 26.030
352 350 11.6667 26.033
353 351 11.7000 26.028
354 352 11.7333 26.032
355 353 11.7667 26.032
356 354 11.8000 26.032
357 355 11.8333 26.029
358 356 11.8667 26.034
359 357 11.9000 26.039
360 358 11.9333 26.041
361 359 11.9667 26.041
362 360 12.0000 26.040
363 361 12.0333 26.046
364 362 12.0667 26.047
365 363 12.1000 26.046
366 364 12.1333 26.050
367 365 12.1667 26.051
368 366 12.2000 26.051
369 367 12.2333 26.051
370 368 12.2667 26.051
371 369 12.3000 26.051
372 370 12.3333 26.051
373 371 12.3667 26.051
374 372 12.4000 26.050
375 373 12.4333 26.055
376 374 12.4667 26.056
377 375 12.5000 26.056
378 376 12.5333 26.055
379 377 12.5667 26.056
380 378 12.6000 26.056
381 379 12.6333 26.056
382 380 12.6667 26.055
383 381 12.7000 26.056
384 382 12.7333 26.056
385 383 12.7667 26.056
386 384 12.8000 26.055
387 385 12.8333 26.055
388 386 12.8667 26.056
389 387 12.9000 26.056
390 388 12.9333 26.056
391 389 12.9667 26.056
392 390 13.0000 26.055
393 391 13.0333 26.071
394 392 13.0667 26.072
395 393 13.1000 26.074
396 394 13.1333 26.082
397 395 13.1667 26.083
398 396 13.2000 26.083
399 397 13.2333 26.083
400 398 13.2667 26.083
401 399 13.3000 26.110
402 400 13.3333 26.111
403 401 13.3667 26.114
404 402 13.4000 26.113
405 403 13.4333 26.118
406 404 13.4667 26.119
407 405 13.5000 26.121
408 406 13.5333 26.119
409 407 13.5667 26.140
410 408 13.6000 26.140
411 409 13.6333 26.144
412 410 13.6667 26.142
413 411 13.7000 26.155
414 412 13.7333 26.157
415 413 13.7667 26.158
416 414 13.8000 26.157
417 415 13.8333 26.165
418 416 13.8667 26.165
419 417 13.9000 26.168
420 418 13.9333 26.165
421 419 13.9667 26.165

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,790 @@
frame,draw,x,y,w,h,rot,col
1,0,0,0,1280,720,,
1,1,0,0,1280,720,,FF000000
1,2,307,331,666,65,,FFFFFFFF
2,5,0,0,1280,720,,
2,6,0,0,1280,720,,FF000000
2,7,307,331,666,65,,FFFFFFFF
3,10,0,0,1280,720,,
3,11,0,0,1280,720,,FF000000
3,12,307,331,666,65,,FFFFFFFF
4,15,0,0,1280,720,,
4,16,0,0,1280,720,,FF000000
4,17,307,331,666,65,,FFFFFFFF
5,20,0,0,1280,720,,
5,21,0,0,1280,720,,FF000000
5,22,307,331,666,65,,FFFFFFFF
6,25,0,0,1280,720,,
6,26,0,0,1280,720,,FF000000
6,27,307,331,666,65,,FFFFFFFF
7,30,0,0,1280,720,,
7,31,0,0,1280,720,,FF000000
7,32,307,331,666,65,,FFFFFFFF
8,35,0,0,1280,720,,
8,36,0,0,1280,720,,FF000000
8,37,307,331,666,65,,FFFFFFFF
9,40,0,0,1280,720,,
9,41,0,0,1280,720,,FF000000
9,42,307,331,666,65,,FFFFFFFF
10,45,0,0,1280,720,,
10,46,0,0,1280,720,,FF000000
10,47,307,331,666,65,,FFFFFFFF
11,50,0,0,1280,720,,
11,51,0,0,1280,720,,FF000000
11,52,307,331,666,65,,FFFFFFFF
12,55,0,0,1280,720,,
12,56,0,0,1280,720,,FF000000
12,57,307,331,666,65,,FFFFFFFF
13,60,0,0,1280,720,,
13,61,0,0,1280,720,,FF000000
13,62,307,331,666,65,,FFFFFFFF
14,65,0,0,1280,720,,
14,66,0,0,1280,720,,FF000000
14,67,307,331,666,65,,FFFFFFFF
15,70,0,0,1280,720,,
15,71,0,0,1280,720,,FF000000
15,72,307,331,666,65,,FFFFFFFF
16,75,0,0,1280,720,,
16,76,0,0,1280,720,,FF000000
16,77,307,331,666,65,,FFFFFFFF
17,80,0,0,1280,720,,
17,81,0,0,1280,720,,FF000000
17,82,307,331,666,65,,FFFFFFFF
18,85,0,0,1280,720,,
18,86,0,0,1280,720,,FF000000
18,87,307,331,666,65,,FFFFFFFF
19,90,0,0,1280,720,,
19,91,0,0,1280,720,,FF000000
19,92,307,331,666,65,,FFFFFFFF
20,95,0,0,1280,720,,
20,96,0,0,1280,720,,FF000000
20,97,307,331,666,65,,FFFFFFFF
21,100,0,0,1280,720,,
21,101,0,0,1280,720,,FF000000
21,102,307,331,666,65,,FFFFFFFF
22,105,0,0,1280,720,,
22,106,0,0,1280,720,,FF000000
22,107,307,331,666,65,,FFFFFFFF
23,110,0,0,1280,720,,
23,111,0,0,1280,720,,FF000000
23,112,307,331,666,65,,FFFFFFFF
24,115,0,0,1280,720,,
24,116,0,0,1280,720,,FF000000
24,117,307,331,666,65,,FFFFFFFF
25,120,0,0,1280,720,,
25,121,0,0,1280,720,,FF000000
25,122,307,331,666,65,,FFFFFFFF
26,125,0,0,1280,720,,
26,126,0,0,1280,720,,FF000000
26,127,307,331,666,65,,FFFFFFFF
27,130,0,0,1280,720,,
27,131,0,0,1280,720,,FF000000
27,132,307,331,666,65,,FFFFFFFF
28,135,0,0,1280,720,,
28,136,0,0,1280,720,,FF000000
28,137,307,331,666,65,,FFFFFFFF
29,140,0,0,1280,720,,
29,141,0,0,1280,720,,FF000000
29,142,307,331,666,65,,FFFFFFFF
30,145,0,0,1280,720,,
30,146,0,0,1280,720,,FF000000
30,147,307,331,666,65,,FFFFFFFF
31,150,0,0,1280,720,,
31,151,0,0,1280,720,,FF000000
31,152,307,331,666,65,,FFFFFFFF
32,155,0,0,1280,720,,
32,156,0,0,1280,720,,FF000000
32,157,307,331,666,65,,FFFFFFFF
33,160,0,0,1280,720,,
33,161,0,0,1280,720,,FF000000
33,162,307,331,666,65,,FFFFFFFF
34,165,0,0,1280,720,,
34,166,0,0,1280,720,,FF000000
34,167,307,331,666,65,,FFFFFFFF
35,170,0,0,1280,720,,
35,171,0,0,1280,720,,FF000000
35,172,307,331,666,65,,FFFFFFFF
36,175,0,0,1280,720,,
36,176,0,0,1280,720,,FF000000
36,177,307,331,666,65,,FFFFFFFF
37,180,0,0,1280,720,,
37,181,0,0,1280,720,,FF000000
37,182,307,331,666,65,,FFFFFFFF
38,185,0,0,1280,720,,
38,186,0,0,1280,720,,FF000000
38,187,307,331,666,65,,FFFFFFFF
39,190,0,0,1280,720,,
39,191,0,0,1280,720,,FF000000
39,192,307,331,666,65,,FFFFFFFF
40,195,0,0,1280,720,,
40,196,0,0,1280,720,,FF000000
40,197,307,331,666,65,,FFFFFFFF
41,200,0,0,1280,720,,
41,201,0,0,1280,720,,FF000000
41,202,307,331,666,65,,FFFFFFFF
42,205,0,0,1280,720,,
42,206,0,0,1280,720,,FF000000
42,207,307,331,666,65,,FFFFFFFF
43,210,0,0,1280,720,,
43,211,0,0,1280,720,,FF000000
43,212,307,331,666,65,,FFFFFFFF
44,215,0,0,1280,720,,
44,216,0,0,1280,720,,FF000000
44,217,307,331,666,65,,FFFFFFFF
45,220,0,0,1280,720,,
45,221,0,0,1280,720,,FF000000
45,222,307,331,666,65,,FFFFFFFF
47,225,0,0,1280,720,,
47,226,0,0,1280,720,,FF000000
47,227,307,331,666,65,,FFFFFFFF
48,230,0,0,1280,720,,
48,231,0,0,1280,720,,FF000000
48,232,307,331,666,65,,FFFFFFFF
49,235,0,0,1280,720,,
49,236,0,0,1280,720,,FF000000
49,237,307,331,666,65,,FFFFFFFF
50,240,0,0,1280,720,,
50,241,0,0,1280,720,,FF000000
50,242,307,331,666,65,,FFFFFFFF
51,245,0,0,1280,720,,
51,246,0,0,1280,720,,FF000000
51,247,307,331,666,65,,FFFFFFFF
52,250,0,0,1280,720,,
52,251,0,0,1280,720,,FF000000
52,252,307,331,666,65,,FFFFFFFF
53,255,0,0,1280,720,,
53,256,0,0,1280,720,,FF000000
53,257,307,331,666,65,,FFFFFFFF
54,260,0,0,1280,720,,
54,261,0,0,1280,720,,FF000000
54,262,307,331,666,65,,FFFFFFFF
55,265,0,0,1280,720,,
55,266,0,0,1280,720,,FF000000
55,267,307,331,666,65,,FFFFFFFF
56,270,0,0,1280,720,,
56,271,0,0,1280,720,,FF000000
56,272,307,331,666,65,,FFFFFFFF
57,275,0,0,1280,720,,
57,276,0,0,1280,720,,FF000000
57,277,307,331,666,65,,FFFFFFFF
58,280,0,0,1280,720,,
58,281,0,0,1280,720,,FF000000
58,282,307,331,666,65,,FFFFFFFF
59,285,0,0,1280,720,,
59,286,0,0,1280,720,,FF000000
59,287,307,331,666,65,,FFFFFFFF
60,290,0,0,1280,720,,
60,291,0,0,1280,720,,FF000000
60,292,307,331,666,65,,FFFFFFFF
61,295,0,0,1280,720,,
61,296,0,0,1280,720,,FF000000
61,297,307,331,666,65,,FFFFFFFF
62,300,0,0,1280,720,,
62,301,0,0,1280,720,,FF000000
62,302,307,331,666,65,,FFFFFFFF
63,305,0,0,1280,720,,
63,306,0,0,1280,720,,FF000000
63,307,307,331,666,65,,FFFFFFFF
64,310,0,0,1280,720,,
64,311,0,0,1280,720,,FF000000
64,312,307,331,666,65,,FFFFFFFF
65,315,0,0,1280,720,,
65,316,0,0,1280,720,,FF000000
65,317,307,331,666,65,,FFFFFFFF
66,320,0,0,1280,720,,
66,321,0,0,1280,720,,FF000000
66,322,307,331,666,65,,FFFFFFFF
67,325,0,0,1280,720,,
67,326,0,0,1280,720,,FF000000
67,327,307,331,666,65,,FFFFFFFF
68,330,0,0,1280,720,,
68,331,0,0,1280,720,,FF000000
68,332,307,331,666,65,,FFFFFFFF
69,335,0,0,1280,720,,
69,336,0,0,1280,720,,FF000000
69,337,307,331,666,65,,FFFFFFFF
70,340,0,0,1280,720,,
70,341,0,0,1280,720,,FF000000
70,342,307,331,666,65,,FFFFFFFF
71,345,0,0,1280,720,,
71,346,0,0,1280,720,,FF000000
71,347,307,331,666,65,,FFFFFFFF
72,350,0,0,1280,720,,
72,351,0,0,1280,720,,FF000000
72,352,307,331,666,65,,FFFFFFFF
73,355,0,0,1280,720,,
73,356,0,0,1280,720,,FF000000
73,357,307,331,666,65,,FFFFFFFF
74,360,0,0,1280,720,,
74,361,0,0,1280,720,,FF000000
74,362,307,331,666,65,,FFFFFFFF
75,365,0,0,1280,720,,
75,366,0,0,1280,720,,FF000000
75,367,307,331,666,65,,FFFFFFFF
76,370,0,0,1280,720,,
76,371,0,0,1280,720,,FF000000
76,372,307,331,666,65,,FFFFFFFF
77,375,0,0,1280,720,,
77,376,0,0,1280,720,,FF000000
77,377,307,331,666,65,,FFFFFFFF
78,380,0,0,1280,720,,
78,381,0,0,1280,720,,FF000000
78,382,307,331,666,65,,FEFFFFFF
79,385,0,0,1280,720,,
79,386,0,0,1280,720,,FF000000
79,387,307,331,666,65,,F9FFFFFF
80,390,0,0,1280,720,,
80,391,0,0,1280,720,,FF000000
80,392,307,331,666,65,,E7FFFFFF
81,395,0,0,1280,720,,
81,396,0,0,1280,720,,FF000000
81,397,307,331,666,65,,D7FFFFFF
82,400,0,0,1280,720,,
82,401,0,0,1280,720,,FF000000
82,402,307,331,666,65,,B5FFFFFF
83,405,0,0,1280,720,,
83,406,0,0,1280,720,,FF000000
83,407,307,331,666,65,,A5FFFFFF
84,410,0,0,1280,720,,
84,411,0,0,1280,720,,FF000000
84,412,307,331,666,65,,83FFFFFF
85,415,0,0,1280,720,,
85,416,0,0,1280,720,,FF000000
85,417,307,331,666,65,,73FFFFFF
86,420,0,0,1280,720,,
86,421,0,0,1280,720,,FF000000
86,422,307,331,666,65,,51FFFFFF
87,425,0,0,1280,720,,
87,426,0,0,1280,720,,FF000000
87,427,307,331,666,65,,41FFFFFF
88,430,0,0,1280,720,,
88,431,0,0,1280,720,,FF000000
88,432,307,331,666,65,,1FFFFFFF
89,435,0,0,1280,720,,
89,436,0,0,1280,720,,FF000000
89,437,307,331,666,65,,0FFFFFFF
90,440,0,0,1280,720,,
90,441,0,0,1280,720,,FF000000
90,442,307,331,666,65,,07FFFFFF
91,445,0,0,1280,720,,
91,446,0,0,1280,720,,FF000000
92,449,0,0,1280,720,,
92,450,0,0,1280,720,,FF000000
93,453,0,0,1280,720,,
93,454,0,0,1280,720,,FF000000
94,457,0,0,1280,720,,
94,458,0,0,1280,720,,FF000000
94,459,378,155,525,90,,22FFFFFF
94,459,512,306,262,108,,22FFFFFF
95,462,0,0,1280,720,,
95,463,0,0,1280,720,,FF000000
95,464,378,155,525,90,,44FFFFFF
95,464,512,306,262,108,,44FFFFFF
96,467,0,0,1280,720,,
96,468,0,0,1280,720,,FF000000
96,469,378,155,525,90,,66FFFFFF
96,469,512,306,262,108,,66FFFFFF
97,472,0,0,1280,720,,
97,473,0,0,1280,720,,FF000000
97,474,378,155,525,90,,88FFFFFF
97,474,512,306,262,108,,88FFFFFF
98,477,0,0,1280,720,,
98,478,0,0,1280,720,,FF000000
98,479,378,155,525,90,,AAFFFFFF
98,479,512,306,262,108,,AAFFFFFF
99,482,0,0,1280,720,,
99,483,0,0,1280,720,,FF000000
99,484,378,155,525,90,,CCFFFFFF
99,484,512,306,262,108,,CCFFFFFF
100,487,0,0,1280,720,,
100,488,0,0,1280,720,,FF000000
100,489,378,155,525,90,,EEFFFFFF
100,489,512,306,262,108,,EEFFFFFF
101,492,0,0,1280,720,,
101,493,0,0,1280,720,,FF000000
101,494,378,155,525,90,,FFFFFFFF
101,494,512,306,262,108,,FFFFFFFF
102,497,0,0,1280,720,,
102,498,0,0,1280,720,,FF000000
102,499,378,155,525,90,,FFFFFFFF
102,499,512,306,262,108,,FFFFFFFF
103,502,0,0,1280,720,,
103,503,0,0,1280,720,,FF000000
103,504,378,155,525,90,,FFFFFFFF
103,504,512,306,262,108,,FFFFFFFF
104,507,0,0,1280,720,,
104,508,0,0,1280,720,,FF000000
104,509,378,155,525,90,,FFFFFFFF
104,509,512,306,262,108,,FFFFFFFF
105,512,0,0,1280,720,,
105,513,0,0,1280,720,,FF000000
105,514,378,155,525,90,,FFFFFFFF
105,514,512,306,262,108,,FFFFFFFF
106,517,0,0,1280,720,,
106,518,0,0,1280,720,,FF000000
106,519,378,155,525,90,,FFFFFFFF
106,519,512,306,262,108,,FFFFFFFF
107,522,0,0,1280,720,,
107,523,0,0,1280,720,,FF000000
107,524,378,155,525,90,,FFFFFFFF
107,524,512,306,262,108,,FFFFFFFF
108,527,0,0,1280,720,,
108,528,0,0,1280,720,,FF000000
108,529,378,155,525,90,,FEFFFFFF
108,529,512,306,262,108,,FEFFFFFF
109,532,0,0,1280,720,,
109,533,0,0,1280,720,,FF000000
109,534,378,155,525,90,,DCFFFFFF
109,534,512,306,262,108,,DCFFFFFF
110,537,0,0,1280,720,,
110,538,0,0,1280,720,,FF000000
110,539,378,155,525,90,,BAFFFFFF
110,539,512,306,262,108,,BAFFFFFF
111,542,0,0,1280,720,,
111,543,0,0,1280,720,,FF000000
111,544,378,155,525,90,,98FFFFFF
111,544,512,306,262,108,,98FFFFFF
112,547,0,0,1280,720,,
112,548,0,0,1280,720,,FF000000
112,549,378,155,525,90,,76FFFFFF
112,549,512,306,262,108,,76FFFFFF
113,552,0,0,1280,720,,
113,553,0,0,1280,720,,FF000000
113,554,378,155,525,90,,54FFFFFF
113,554,512,306,262,108,,54FFFFFF
114,557,0,0,1280,720,,
114,558,0,0,1280,720,,FF000000
114,559,378,155,525,90,,43FFFFFF
114,559,512,306,262,108,,43FFFFFF
115,562,0,0,1280,720,,
115,563,0,0,1280,720,,FF000000
115,564,378,155,525,90,,21FFFFFF
115,564,512,306,262,108,,21FFFFFF
116,567,0,0,1280,720,,
116,568,0,0,1280,720,,FF000000
116,569,390,162,499,72,,FFFFFFFF
116,569,518,317,243,86,,FFFFFFFF
117,572,0,0,1280,720,,
117,573,0,0,1280,720,,FF000000
117,574,390,162,499,72,,FFFFFFFF
117,574,518,317,243,86,,FFFFFFFF
118,577,0,0,1280,720,,
118,578,0,0,1280,720,,FF000000
118,579,390,162,499,72,,FFFFFFFF
118,579,518,317,243,86,,FFFFFFFF
119,582,0,0,1280,720,,
119,583,0,0,1280,720,,FF000000
119,584,390,162,499,72,,FFFFFFFF
119,584,518,317,243,86,,FFFFFFFF
120,587,0,0,1280,720,,
120,588,0,0,1280,720,,FF000000
120,589,390,162,499,72,,FFFFFFFF
120,589,518,317,243,86,,FFFFFFFF
121,592,0,0,1280,720,,
121,593,0,0,1280,720,,FF000000
121,594,390,162,499,72,,FFFFFFFF
121,594,518,317,243,86,,FFFFFFFF
122,597,0,0,1280,720,,
122,598,0,0,1280,720,,FF000000
122,599,390,162,499,72,,FFFFFFFF
122,599,518,317,243,86,,FFFFFFFF
123,602,0,0,1280,720,,
123,603,0,0,1280,720,,FF000000
123,604,390,162,499,72,,FFFFFFFF
123,604,518,317,243,86,,FFFFFFFF
124,607,0,0,1280,720,,
124,608,0,0,1280,720,,FF000000
124,609,390,162,499,72,,FFFFFFFF
124,609,518,317,243,86,,FFFFFFFF
125,612,0,0,1280,720,,
125,613,0,0,1280,720,,FF000000
125,614,390,162,499,72,,FFFFFFFF
125,614,518,317,243,86,,FFFFFFFF
126,617,0,0,1280,720,,
126,618,0,0,1280,720,,FF000000
126,619,390,162,499,72,,FFFFFFFF
126,619,518,317,243,86,,FFFFFFFF
127,622,0,0,1280,720,,
127,623,0,0,1280,720,,FF000000
127,624,390,162,499,72,,FFFFFFFF
127,624,518,317,243,86,,FFFFFFFF
128,627,0,0,1280,720,,
128,628,0,0,1280,720,,FF000000
128,629,390,162,499,72,,FFFFFFFF
128,629,518,317,243,86,,FFFFFFFF
129,632,0,0,1280,720,,
129,633,0,0,1280,720,,FF000000
129,634,390,162,499,72,,FFFFFFFF
129,634,518,317,243,86,,FFFFFFFF
130,637,0,0,1280,720,,
130,638,0,0,1280,720,,FF000000
130,639,390,162,499,72,,FFFFFFFF
130,639,518,317,243,86,,FFFFFFFF
131,642,0,0,1280,720,,
131,643,0,0,1280,720,,FF000000
131,644,390,162,499,72,,FFFFFFFF
131,644,518,317,243,86,,FFFFFFFF
132,647,0,0,1280,720,,
132,648,0,0,1280,720,,FF000000
132,649,390,162,499,72,,FFFFFFFF
132,649,518,317,243,86,,FFFFFFFF
133,652,0,0,1280,720,,
133,653,0,0,1280,720,,FF000000
133,654,390,162,499,72,,FFFFFFFF
133,654,518,317,243,86,,FFFFFFFF
134,657,0,0,1280,720,,
134,658,0,0,1280,720,,FF000000
134,659,390,162,499,72,,FFFFFFFF
134,659,518,317,243,86,,FFFFFFFF
135,662,0,0,1280,720,,
135,663,0,0,1280,720,,FF000000
135,664,390,162,499,72,,FFFFFFFF
135,664,518,317,243,86,,FFFFFFFF
136,667,0,0,1280,720,,
136,668,0,0,1280,720,,FF000000
136,669,390,162,499,72,,FFFFFFFF
136,669,518,317,243,86,,FFFFFFFF
137,672,0,0,1280,720,,
137,673,0,0,1280,720,,FF000000
137,674,390,162,499,72,,FFFFFFFF
137,674,518,317,243,86,,FFFFFFFF
138,677,0,0,1280,720,,
138,678,0,0,1280,720,,FF000000
138,679,390,162,499,72,,FFFFFFFF
138,679,518,317,243,86,,FFFFFFFF
140,682,0,0,1280,720,,
140,683,0,0,1280,720,,FF000000
140,684,390,162,499,72,,FFFFFFFF
140,684,518,317,243,86,,FFFFFFFF
141,687,0,0,1280,720,,
141,688,0,0,1280,720,,FF000000
141,689,390,162,499,72,,FFFFFFFF
141,689,518,317,243,86,,FFFFFFFF
142,692,0,0,1280,720,,
142,693,0,0,1280,720,,FF000000
142,694,390,162,499,72,,FFFFFFFF
142,694,518,317,243,86,,FFFFFFFF
143,697,0,0,1280,720,,
143,698,0,0,1280,720,,FF000000
143,699,390,162,499,72,,FFFFFFFF
143,699,518,317,243,86,,FFFFFFFF
144,702,0,0,1280,720,,
144,703,0,0,1280,720,,FF000000
144,704,390,162,499,72,,FFFFFFFF
144,704,518,317,243,86,,FFFFFFFF
145,707,0,0,1280,720,,
145,708,0,0,1280,720,,FF000000
145,709,390,162,499,72,,FFFFFFFF
145,709,518,317,243,86,,FFFFFFFF
146,712,0,0,1280,720,,
146,713,0,0,1280,720,,FF000000
146,714,390,162,499,72,,FFFFFFFF
146,714,518,317,243,86,,FFFFFFFF
147,717,0,0,1280,720,,
147,718,0,0,1280,720,,FF000000
147,719,390,162,499,72,,FFFFFFFF
147,719,518,317,243,86,,FFFFFFFF
148,722,0,0,1280,720,,
148,723,0,0,1280,720,,FF000000
148,724,390,162,499,72,,FFFFFFFF
148,724,518,317,243,86,,FFFFFFFF
149,727,0,0,1280,720,,
149,728,0,0,1280,720,,FF000000
149,729,390,162,499,72,,FFFFFFFF
149,729,518,317,243,86,,FFFFFFFF
150,732,0,0,1280,720,,
150,733,0,0,1280,720,,FF000000
150,734,390,162,499,72,,FFFFFFFF
150,734,518,317,243,86,,FFFFFFFF
151,737,0,0,1280,720,,
151,738,0,0,1280,720,,FF000000
151,739,390,162,499,72,,FFFFFFFF
151,739,518,317,243,86,,FFFFFFFF
152,742,0,0,1280,720,,
152,743,0,0,1280,720,,FF000000
152,744,390,162,499,72,,FFFFFFFF
152,744,518,317,243,86,,FFFFFFFF
153,747,0,0,1280,720,,
153,748,0,0,1280,720,,FF000000
153,749,390,162,499,72,,FFFFFFFF
153,749,518,317,243,86,,FFFFFFFF
154,752,0,0,1280,720,,
154,753,0,0,1280,720,,FF000000
154,754,390,162,499,72,,FFFFFFFF
154,754,518,317,243,86,,FFFFFFFF
155,757,0,0,1280,720,,
155,758,0,0,1280,720,,FF000000
155,759,390,162,499,72,,FFFFFFFF
155,759,518,317,243,86,,FFFFFFFF
156,762,0,0,1280,720,,
156,763,0,0,1280,720,,FF000000
156,764,390,162,499,72,,FFFFFFFF
156,764,518,317,243,86,,FFFFFFFF
157,767,0,0,1280,720,,
157,768,0,0,1280,720,,FF000000
157,769,390,162,499,72,,FFFFFFFF
157,769,518,317,243,86,,FFFFFFFF
158,772,0,0,1280,720,,
158,773,0,0,1280,720,,FF000000
158,774,390,162,499,72,,FFFFFFFF
158,774,518,317,243,86,,FFFFFFFF
159,777,0,0,1280,720,,
159,778,0,0,1280,720,,FF000000
159,779,390,162,499,72,,FFFFFFFF
159,779,518,317,243,86,,FFFFFFFF
160,782,0,0,1280,720,,
160,783,0,0,1280,720,,FF000000
160,784,390,162,499,72,,FFFFFFFF
160,784,518,317,243,86,,FFFFFFFF
161,787,0,0,1280,720,,
161,788,0,0,1280,720,,FF000000
161,789,390,162,499,72,,FFFFFFFF
161,789,518,317,243,86,,FFFFFFFF
162,792,0,0,1280,720,,
162,793,0,0,1280,720,,FF000000
162,794,390,162,499,72,,FFFFFFFF
162,794,518,317,243,86,,FFFFFFFF
163,797,0,0,1280,720,,
163,798,0,0,1280,720,,FF000000
163,799,390,162,499,72,,FFFFFFFF
163,799,518,317,243,86,,FFFFFFFF
164,802,0,0,1280,720,,
164,803,0,0,1280,720,,FF000000
164,804,390,162,499,72,,FFFFFFFF
164,804,518,317,243,86,,FFFFFFFF
165,807,0,0,1280,720,,
165,808,0,0,1280,720,,FF000000
165,809,390,162,499,72,,FFFFFFFF
165,809,518,317,243,86,,FFFFFFFF
166,812,0,0,1280,720,,
166,813,0,0,1280,720,,FF000000
166,814,390,162,499,72,,FFFFFFFF
166,814,518,317,243,86,,FFFFFFFF
167,817,0,0,1280,720,,
167,818,0,0,1280,720,,FF000000
167,819,390,162,499,72,,FFFFFFFF
167,819,518,317,243,86,,FFFFFFFF
168,822,0,0,1280,720,,
168,823,0,0,1280,720,,FF000000
168,824,390,162,499,72,,FFFFFFFF
168,824,518,317,243,86,,FFFFFFFF
169,827,0,0,1280,720,,
169,828,0,0,1280,720,,FF000000
169,829,390,162,499,72,,FFFFFFFF
169,829,518,317,243,86,,FFFFFFFF
170,832,0,0,1280,720,,
170,833,0,0,1280,720,,FF000000
170,834,390,162,499,72,,FFFFFFFF
170,834,518,317,243,86,,FFFFFFFF
171,837,0,0,1280,720,,
171,838,0,0,1280,720,,FF000000
171,839,390,162,499,72,,FFFFFFFF
171,839,518,317,243,86,,FFFFFFFF
172,842,0,0,1280,720,,
172,843,0,0,1280,720,,FF000000
172,844,390,162,499,72,,FFFFFFFF
172,844,518,317,243,86,,FFFFFFFF
173,847,0,0,1280,720,,
173,848,0,0,1280,720,,FF000000
173,849,390,162,499,72,,FFFFFFFF
173,849,518,317,243,86,,FFFFFFFF
174,852,0,0,1280,720,,
174,853,0,0,1280,720,,FF000000
174,854,390,162,499,72,,FFFFFFFF
174,854,518,317,243,86,,FFFFFFFF
175,857,0,0,1280,720,,
175,858,0,0,1280,720,,FF000000
175,859,390,162,499,72,,FFFFFFFF
175,859,518,317,243,86,,FFFFFFFF
176,862,0,0,1280,720,,
176,863,0,0,1280,720,,FF000000
176,864,390,162,499,72,,FFFFFFFF
176,864,518,317,243,86,,FFFFFFFF
177,867,0,0,1280,720,,
177,868,0,0,1280,720,,FF000000
177,869,390,162,499,72,,FFFFFFFF
177,869,518,317,243,86,,FFFFFFFF
178,872,0,0,1280,720,,
178,873,0,0,1280,720,,FF000000
178,874,390,162,499,72,,FFFFFFFF
178,874,518,317,243,86,,FFFFFFFF
179,877,0,0,1280,720,,
179,878,0,0,1280,720,,FF000000
179,879,390,162,499,72,,FFFFFFFF
179,879,518,317,243,86,,FFFFFFFF
180,882,0,0,1280,720,,
180,883,0,0,1280,720,,FF000000
180,884,390,162,499,72,,FFFFFFFF
180,884,518,317,243,86,,FFFFFFFF
181,887,0,0,1280,720,,
181,888,0,0,1280,720,,FF000000
181,889,390,162,499,72,,FFFFFFFF
181,889,518,317,243,86,,FFFFFFFF
182,892,0,0,1280,720,,
182,893,0,0,1280,720,,FF000000
182,894,390,162,499,72,,FFFFFFFF
182,894,518,317,243,86,,FFFFFFFF
183,897,0,0,1280,720,,
183,898,0,0,1280,720,,FF000000
183,899,390,162,499,72,,FFFFFFFF
183,899,518,317,243,86,,FFFFFFFF
184,902,0,0,1280,720,,
184,903,0,0,1280,720,,FF000000
184,904,390,162,499,72,,FFFFFFFF
184,904,518,317,243,86,,FFFFFFFF
185,907,0,0,1280,720,,
185,908,0,0,1280,720,,FF000000
185,909,390,162,499,72,,FFFFFFFF
185,909,518,317,243,86,,FFFFFFFF
186,912,0,0,1280,720,,
186,913,0,0,1280,720,,FF000000
186,914,390,162,499,72,,FFFFFFFF
186,914,518,317,243,86,,FFFFFFFF
187,917,0,0,1280,720,,
187,918,0,0,1280,720,,FF000000
187,919,390,162,499,72,,FFFFFFFF
187,919,518,317,243,86,,FFFFFFFF
188,922,0,0,1280,720,,
188,923,0,0,1280,720,,FF000000
188,924,390,162,499,72,,FFFFFFFF
188,924,518,317,243,86,,FFFFFFFF
189,927,0,0,1280,720,,
189,928,0,0,1280,720,,FF000000
189,929,390,162,499,72,,FFFFFFFF
189,929,518,317,243,86,,FFFFFFFF
190,932,0,0,1280,720,,
190,933,0,0,1280,720,,FF000000
190,934,390,162,499,72,,FFFFFFFF
190,934,518,317,243,86,,FFFFFFFF
191,937,0,0,1280,720,,
191,938,0,0,1280,720,,FF000000
191,939,390,162,499,72,,FFFFFFFF
191,939,518,317,243,86,,FFFFFFFF
192,942,0,0,1280,720,,
192,943,0,0,1280,720,,FF000000
192,944,390,162,499,72,,FFFFFFFF
192,944,518,317,243,86,,FFFFFFFF
193,947,0,0,1280,720,,
193,948,0,0,1280,720,,FF000000
193,949,390,162,499,72,,FFFFFFFF
193,949,518,317,243,86,,FFFFFFFF
194,952,0,0,1280,720,,
194,953,0,0,1280,720,,FF000000
194,954,390,162,499,72,,FFFFFFFF
194,954,518,317,243,86,,FFFFFFFF
195,957,0,0,1280,720,,
195,958,0,0,1280,720,,FF000000
195,959,390,162,499,72,,FFFFFFFF
195,959,518,317,243,86,,FFFFFFFF
196,962,0,0,1280,720,,
196,963,0,0,1280,720,,FF000000
196,964,390,162,499,72,,FFFFFFFF
196,964,518,317,243,86,,FFFFFFFF
197,967,0,0,1280,720,,
197,968,0,0,1280,720,,FF000000
197,969,390,162,499,72,,FFFFFFFF
197,969,518,317,243,86,,FFFFFFFF
198,972,0,0,1280,720,,
198,973,0,0,1280,720,,FF000000
198,974,390,162,499,72,,FFFFFFFF
198,974,518,317,243,86,,FFFFFFFF
199,977,0,0,1280,720,,
199,978,0,0,1280,720,,FF000000
199,979,390,162,499,72,,FEFFFFFF
199,979,518,317,243,86,,FEFFFFFF
200,982,0,0,1280,720,,
200,983,0,0,1280,720,,FF000000
200,984,390,162,499,72,,F3FFFFFF
200,984,518,317,243,86,,F3FFFFFF
201,987,0,0,1280,720,,
201,988,0,0,1280,720,,FF000000
201,989,390,162,499,72,,EDFFFFFF
201,989,518,317,243,86,,EDFFFFFF
202,992,0,0,1280,720,,
202,993,0,0,1280,720,,FF000000
202,994,390,162,499,72,,D7FFFFFF
202,994,518,317,243,86,,D7FFFFFF
203,997,0,0,1280,720,,
203,998,0,0,1280,720,,FF000000
203,999,390,162,499,72,,B5FFFFFF
203,999,518,317,243,86,,B5FFFFFF
204,1002,0,0,1280,720,,
204,1003,0,0,1280,720,,FF000000
204,1004,390,162,499,72,,94FFFFFF
204,1004,518,317,243,86,,94FFFFFF
205,1007,0,0,1280,720,,
205,1008,0,0,1280,720,,FF000000
205,1009,390,162,499,72,,83FFFFFF
205,1009,518,317,243,86,,83FFFFFF
206,1012,0,0,1280,720,,
206,1013,0,0,1280,720,,FF000000
206,1014,390,162,499,72,,62FFFFFF
206,1014,518,317,243,86,,62FFFFFF
207,1017,0,0,1280,720,,
207,1018,0,0,1280,720,,FF000000
207,1019,390,162,499,72,,41FFFFFF
207,1019,518,317,243,86,,41FFFFFF
208,1022,0,0,1280,720,,
208,1023,0,0,1280,720,,FF000000
208,1024,390,162,499,72,,30FFFFFF
208,1024,518,317,243,86,,30FFFFFF
209,1027,0,0,1280,720,,
209,1028,0,0,1280,720,,FF000000
209,1029,390,162,499,72,,17FFFFFF
209,1029,518,317,243,86,,17FFFFFF
210,1032,0,0,1280,720,,
210,1033,0,0,1280,720,,FF000000
210,1034,390,162,499,72,,0FFFFFFF
210,1034,518,317,243,86,,0FFFFFFF
211,1037,0,0,1280,720,,
211,1038,0,0,1280,720,,FF000000
211,1039,390,162,499,72,,07FFFFFF
211,1039,518,317,243,86,,07FFFFFF
212,1042,0,0,1280,720,,
212,1043,0,0,1280,720,,FF000000
213,1046,0,0,1280,720,,
213,1047,0,0,1280,720,,FF000000
214,1050,0,0,1280,720,,
214,1051,0,0,1280,720,,FF000000
215,1054,0,0,1280,720,,
216,1057,0,0,1280,720,,
217,1060,0,0,1280,720,,
218,1063,0,0,1280,720,,
219,1066,0,0,1280,720,,
220,1069,0,0,1280,720,,
221,1072,0,0,1280,720,,
221,1073,0,0,1280,720,,
223,1076,0,0,1280,720,,
223,1077,0,0,1280,720,,
224,1080,0,0,1280,720,,
224,1081,0,0,1280,720,,
226,1084,0,0,1280,720,,
226,1085,0,0,1280,720,,
227,1088,0,0,1280,720,,
227,1089,0,0,1280,720,,
228,1092,0,0,1280,720,,
228,1093,0,0,1280,720,,
229,1096,0,0,1280,720,,
229,1097,0,0,1280,720,,
230,1100,0,0,1280,720,,
230,1101,0,0,1280,720,,
231,1104,0,0,1280,720,,
231,1105,0,0,1280,720,,
232,1108,0,0,1280,720,,
232,1109,0,0,1280,720,,
233,1112,0,0,1280,720,,
233,1113,0,0,1280,720,,
234,1116,0,0,1280,720,,
234,1117,0,0,1280,720,,
236,1120,0,0,1280,720,,
236,1121,0,0,1280,720,,
237,1124,0,0,1280,720,,
237,1125,0,0,1280,720,,
238,1128,0,0,1280,720,,
238,1129,0,0,1280,720,,
239,1132,0,0,1280,720,,
239,1133,0,0,1280,720,,
240,1136,0,0,1280,720,,
240,1137,0,0,1280,720,,
241,1140,0,0,1280,720,,
241,1141,0,0,1280,720,,
1 frame draw x y w h rot col
2 1 0 0 0 1280 720
3 1 1 0 0 1280 720 FF000000
4 1 2 307 331 666 65 FFFFFFFF
5 2 5 0 0 1280 720
6 2 6 0 0 1280 720 FF000000
7 2 7 307 331 666 65 FFFFFFFF
8 3 10 0 0 1280 720
9 3 11 0 0 1280 720 FF000000
10 3 12 307 331 666 65 FFFFFFFF
11 4 15 0 0 1280 720
12 4 16 0 0 1280 720 FF000000
13 4 17 307 331 666 65 FFFFFFFF
14 5 20 0 0 1280 720
15 5 21 0 0 1280 720 FF000000
16 5 22 307 331 666 65 FFFFFFFF
17 6 25 0 0 1280 720
18 6 26 0 0 1280 720 FF000000
19 6 27 307 331 666 65 FFFFFFFF
20 7 30 0 0 1280 720
21 7 31 0 0 1280 720 FF000000
22 7 32 307 331 666 65 FFFFFFFF
23 8 35 0 0 1280 720
24 8 36 0 0 1280 720 FF000000
25 8 37 307 331 666 65 FFFFFFFF
26 9 40 0 0 1280 720
27 9 41 0 0 1280 720 FF000000
28 9 42 307 331 666 65 FFFFFFFF
29 10 45 0 0 1280 720
30 10 46 0 0 1280 720 FF000000
31 10 47 307 331 666 65 FFFFFFFF
32 11 50 0 0 1280 720
33 11 51 0 0 1280 720 FF000000
34 11 52 307 331 666 65 FFFFFFFF
35 12 55 0 0 1280 720
36 12 56 0 0 1280 720 FF000000
37 12 57 307 331 666 65 FFFFFFFF
38 13 60 0 0 1280 720
39 13 61 0 0 1280 720 FF000000
40 13 62 307 331 666 65 FFFFFFFF
41 14 65 0 0 1280 720
42 14 66 0 0 1280 720 FF000000
43 14 67 307 331 666 65 FFFFFFFF
44 15 70 0 0 1280 720
45 15 71 0 0 1280 720 FF000000
46 15 72 307 331 666 65 FFFFFFFF
47 16 75 0 0 1280 720
48 16 76 0 0 1280 720 FF000000
49 16 77 307 331 666 65 FFFFFFFF
50 17 80 0 0 1280 720
51 17 81 0 0 1280 720 FF000000
52 17 82 307 331 666 65 FFFFFFFF
53 18 85 0 0 1280 720
54 18 86 0 0 1280 720 FF000000
55 18 87 307 331 666 65 FFFFFFFF
56 19 90 0 0 1280 720
57 19 91 0 0 1280 720 FF000000
58 19 92 307 331 666 65 FFFFFFFF
59 20 95 0 0 1280 720
60 20 96 0 0 1280 720 FF000000
61 20 97 307 331 666 65 FFFFFFFF
62 21 100 0 0 1280 720
63 21 101 0 0 1280 720 FF000000
64 21 102 307 331 666 65 FFFFFFFF
65 22 105 0 0 1280 720
66 22 106 0 0 1280 720 FF000000
67 22 107 307 331 666 65 FFFFFFFF
68 23 110 0 0 1280 720
69 23 111 0 0 1280 720 FF000000
70 23 112 307 331 666 65 FFFFFFFF
71 24 115 0 0 1280 720
72 24 116 0 0 1280 720 FF000000
73 24 117 307 331 666 65 FFFFFFFF
74 25 120 0 0 1280 720
75 25 121 0 0 1280 720 FF000000
76 25 122 307 331 666 65 FFFFFFFF
77 26 125 0 0 1280 720
78 26 126 0 0 1280 720 FF000000
79 26 127 307 331 666 65 FFFFFFFF
80 27 130 0 0 1280 720
81 27 131 0 0 1280 720 FF000000
82 27 132 307 331 666 65 FFFFFFFF
83 28 135 0 0 1280 720
84 28 136 0 0 1280 720 FF000000
85 28 137 307 331 666 65 FFFFFFFF
86 29 140 0 0 1280 720
87 29 141 0 0 1280 720 FF000000
88 29 142 307 331 666 65 FFFFFFFF
89 30 145 0 0 1280 720
90 30 146 0 0 1280 720 FF000000
91 30 147 307 331 666 65 FFFFFFFF
92 31 150 0 0 1280 720
93 31 151 0 0 1280 720 FF000000
94 31 152 307 331 666 65 FFFFFFFF
95 32 155 0 0 1280 720
96 32 156 0 0 1280 720 FF000000
97 32 157 307 331 666 65 FFFFFFFF
98 33 160 0 0 1280 720
99 33 161 0 0 1280 720 FF000000
100 33 162 307 331 666 65 FFFFFFFF
101 34 165 0 0 1280 720
102 34 166 0 0 1280 720 FF000000
103 34 167 307 331 666 65 FFFFFFFF
104 35 170 0 0 1280 720
105 35 171 0 0 1280 720 FF000000
106 35 172 307 331 666 65 FFFFFFFF
107 36 175 0 0 1280 720
108 36 176 0 0 1280 720 FF000000
109 36 177 307 331 666 65 FFFFFFFF
110 37 180 0 0 1280 720
111 37 181 0 0 1280 720 FF000000
112 37 182 307 331 666 65 FFFFFFFF
113 38 185 0 0 1280 720
114 38 186 0 0 1280 720 FF000000
115 38 187 307 331 666 65 FFFFFFFF
116 39 190 0 0 1280 720
117 39 191 0 0 1280 720 FF000000
118 39 192 307 331 666 65 FFFFFFFF
119 40 195 0 0 1280 720
120 40 196 0 0 1280 720 FF000000
121 40 197 307 331 666 65 FFFFFFFF
122 41 200 0 0 1280 720
123 41 201 0 0 1280 720 FF000000
124 41 202 307 331 666 65 FFFFFFFF
125 42 205 0 0 1280 720
126 42 206 0 0 1280 720 FF000000
127 42 207 307 331 666 65 FFFFFFFF
128 43 210 0 0 1280 720
129 43 211 0 0 1280 720 FF000000
130 43 212 307 331 666 65 FFFFFFFF
131 44 215 0 0 1280 720
132 44 216 0 0 1280 720 FF000000
133 44 217 307 331 666 65 FFFFFFFF
134 45 220 0 0 1280 720
135 45 221 0 0 1280 720 FF000000
136 45 222 307 331 666 65 FFFFFFFF
137 47 225 0 0 1280 720
138 47 226 0 0 1280 720 FF000000
139 47 227 307 331 666 65 FFFFFFFF
140 48 230 0 0 1280 720
141 48 231 0 0 1280 720 FF000000
142 48 232 307 331 666 65 FFFFFFFF
143 49 235 0 0 1280 720
144 49 236 0 0 1280 720 FF000000
145 49 237 307 331 666 65 FFFFFFFF
146 50 240 0 0 1280 720
147 50 241 0 0 1280 720 FF000000
148 50 242 307 331 666 65 FFFFFFFF
149 51 245 0 0 1280 720
150 51 246 0 0 1280 720 FF000000
151 51 247 307 331 666 65 FFFFFFFF
152 52 250 0 0 1280 720
153 52 251 0 0 1280 720 FF000000
154 52 252 307 331 666 65 FFFFFFFF
155 53 255 0 0 1280 720
156 53 256 0 0 1280 720 FF000000
157 53 257 307 331 666 65 FFFFFFFF
158 54 260 0 0 1280 720
159 54 261 0 0 1280 720 FF000000
160 54 262 307 331 666 65 FFFFFFFF
161 55 265 0 0 1280 720
162 55 266 0 0 1280 720 FF000000
163 55 267 307 331 666 65 FFFFFFFF
164 56 270 0 0 1280 720
165 56 271 0 0 1280 720 FF000000
166 56 272 307 331 666 65 FFFFFFFF
167 57 275 0 0 1280 720
168 57 276 0 0 1280 720 FF000000
169 57 277 307 331 666 65 FFFFFFFF
170 58 280 0 0 1280 720
171 58 281 0 0 1280 720 FF000000
172 58 282 307 331 666 65 FFFFFFFF
173 59 285 0 0 1280 720
174 59 286 0 0 1280 720 FF000000
175 59 287 307 331 666 65 FFFFFFFF
176 60 290 0 0 1280 720
177 60 291 0 0 1280 720 FF000000
178 60 292 307 331 666 65 FFFFFFFF
179 61 295 0 0 1280 720
180 61 296 0 0 1280 720 FF000000
181 61 297 307 331 666 65 FFFFFFFF
182 62 300 0 0 1280 720
183 62 301 0 0 1280 720 FF000000
184 62 302 307 331 666 65 FFFFFFFF
185 63 305 0 0 1280 720
186 63 306 0 0 1280 720 FF000000
187 63 307 307 331 666 65 FFFFFFFF
188 64 310 0 0 1280 720
189 64 311 0 0 1280 720 FF000000
190 64 312 307 331 666 65 FFFFFFFF
191 65 315 0 0 1280 720
192 65 316 0 0 1280 720 FF000000
193 65 317 307 331 666 65 FFFFFFFF
194 66 320 0 0 1280 720
195 66 321 0 0 1280 720 FF000000
196 66 322 307 331 666 65 FFFFFFFF
197 67 325 0 0 1280 720
198 67 326 0 0 1280 720 FF000000
199 67 327 307 331 666 65 FFFFFFFF
200 68 330 0 0 1280 720
201 68 331 0 0 1280 720 FF000000
202 68 332 307 331 666 65 FFFFFFFF
203 69 335 0 0 1280 720
204 69 336 0 0 1280 720 FF000000
205 69 337 307 331 666 65 FFFFFFFF
206 70 340 0 0 1280 720
207 70 341 0 0 1280 720 FF000000
208 70 342 307 331 666 65 FFFFFFFF
209 71 345 0 0 1280 720
210 71 346 0 0 1280 720 FF000000
211 71 347 307 331 666 65 FFFFFFFF
212 72 350 0 0 1280 720
213 72 351 0 0 1280 720 FF000000
214 72 352 307 331 666 65 FFFFFFFF
215 73 355 0 0 1280 720
216 73 356 0 0 1280 720 FF000000
217 73 357 307 331 666 65 FFFFFFFF
218 74 360 0 0 1280 720
219 74 361 0 0 1280 720 FF000000
220 74 362 307 331 666 65 FFFFFFFF
221 75 365 0 0 1280 720
222 75 366 0 0 1280 720 FF000000
223 75 367 307 331 666 65 FFFFFFFF
224 76 370 0 0 1280 720
225 76 371 0 0 1280 720 FF000000
226 76 372 307 331 666 65 FFFFFFFF
227 77 375 0 0 1280 720
228 77 376 0 0 1280 720 FF000000
229 77 377 307 331 666 65 FFFFFFFF
230 78 380 0 0 1280 720
231 78 381 0 0 1280 720 FF000000
232 78 382 307 331 666 65 FEFFFFFF
233 79 385 0 0 1280 720
234 79 386 0 0 1280 720 FF000000
235 79 387 307 331 666 65 F9FFFFFF
236 80 390 0 0 1280 720
237 80 391 0 0 1280 720 FF000000
238 80 392 307 331 666 65 E7FFFFFF
239 81 395 0 0 1280 720
240 81 396 0 0 1280 720 FF000000
241 81 397 307 331 666 65 D7FFFFFF
242 82 400 0 0 1280 720
243 82 401 0 0 1280 720 FF000000
244 82 402 307 331 666 65 B5FFFFFF
245 83 405 0 0 1280 720
246 83 406 0 0 1280 720 FF000000
247 83 407 307 331 666 65 A5FFFFFF
248 84 410 0 0 1280 720
249 84 411 0 0 1280 720 FF000000
250 84 412 307 331 666 65 83FFFFFF
251 85 415 0 0 1280 720
252 85 416 0 0 1280 720 FF000000
253 85 417 307 331 666 65 73FFFFFF
254 86 420 0 0 1280 720
255 86 421 0 0 1280 720 FF000000
256 86 422 307 331 666 65 51FFFFFF
257 87 425 0 0 1280 720
258 87 426 0 0 1280 720 FF000000
259 87 427 307 331 666 65 41FFFFFF
260 88 430 0 0 1280 720
261 88 431 0 0 1280 720 FF000000
262 88 432 307 331 666 65 1FFFFFFF
263 89 435 0 0 1280 720
264 89 436 0 0 1280 720 FF000000
265 89 437 307 331 666 65 0FFFFFFF
266 90 440 0 0 1280 720
267 90 441 0 0 1280 720 FF000000
268 90 442 307 331 666 65 07FFFFFF
269 91 445 0 0 1280 720
270 91 446 0 0 1280 720 FF000000
271 92 449 0 0 1280 720
272 92 450 0 0 1280 720 FF000000
273 93 453 0 0 1280 720
274 93 454 0 0 1280 720 FF000000
275 94 457 0 0 1280 720
276 94 458 0 0 1280 720 FF000000
277 94 459 378 155 525 90 22FFFFFF
278 94 459 512 306 262 108 22FFFFFF
279 95 462 0 0 1280 720
280 95 463 0 0 1280 720 FF000000
281 95 464 378 155 525 90 44FFFFFF
282 95 464 512 306 262 108 44FFFFFF
283 96 467 0 0 1280 720
284 96 468 0 0 1280 720 FF000000
285 96 469 378 155 525 90 66FFFFFF
286 96 469 512 306 262 108 66FFFFFF
287 97 472 0 0 1280 720
288 97 473 0 0 1280 720 FF000000
289 97 474 378 155 525 90 88FFFFFF
290 97 474 512 306 262 108 88FFFFFF
291 98 477 0 0 1280 720
292 98 478 0 0 1280 720 FF000000
293 98 479 378 155 525 90 AAFFFFFF
294 98 479 512 306 262 108 AAFFFFFF
295 99 482 0 0 1280 720
296 99 483 0 0 1280 720 FF000000
297 99 484 378 155 525 90 CCFFFFFF
298 99 484 512 306 262 108 CCFFFFFF
299 100 487 0 0 1280 720
300 100 488 0 0 1280 720 FF000000
301 100 489 378 155 525 90 EEFFFFFF
302 100 489 512 306 262 108 EEFFFFFF
303 101 492 0 0 1280 720
304 101 493 0 0 1280 720 FF000000
305 101 494 378 155 525 90 FFFFFFFF
306 101 494 512 306 262 108 FFFFFFFF
307 102 497 0 0 1280 720
308 102 498 0 0 1280 720 FF000000
309 102 499 378 155 525 90 FFFFFFFF
310 102 499 512 306 262 108 FFFFFFFF
311 103 502 0 0 1280 720
312 103 503 0 0 1280 720 FF000000
313 103 504 378 155 525 90 FFFFFFFF
314 103 504 512 306 262 108 FFFFFFFF
315 104 507 0 0 1280 720
316 104 508 0 0 1280 720 FF000000
317 104 509 378 155 525 90 FFFFFFFF
318 104 509 512 306 262 108 FFFFFFFF
319 105 512 0 0 1280 720
320 105 513 0 0 1280 720 FF000000
321 105 514 378 155 525 90 FFFFFFFF
322 105 514 512 306 262 108 FFFFFFFF
323 106 517 0 0 1280 720
324 106 518 0 0 1280 720 FF000000
325 106 519 378 155 525 90 FFFFFFFF
326 106 519 512 306 262 108 FFFFFFFF
327 107 522 0 0 1280 720
328 107 523 0 0 1280 720 FF000000
329 107 524 378 155 525 90 FFFFFFFF
330 107 524 512 306 262 108 FFFFFFFF
331 108 527 0 0 1280 720
332 108 528 0 0 1280 720 FF000000
333 108 529 378 155 525 90 FEFFFFFF
334 108 529 512 306 262 108 FEFFFFFF
335 109 532 0 0 1280 720
336 109 533 0 0 1280 720 FF000000
337 109 534 378 155 525 90 DCFFFFFF
338 109 534 512 306 262 108 DCFFFFFF
339 110 537 0 0 1280 720
340 110 538 0 0 1280 720 FF000000
341 110 539 378 155 525 90 BAFFFFFF
342 110 539 512 306 262 108 BAFFFFFF
343 111 542 0 0 1280 720
344 111 543 0 0 1280 720 FF000000
345 111 544 378 155 525 90 98FFFFFF
346 111 544 512 306 262 108 98FFFFFF
347 112 547 0 0 1280 720
348 112 548 0 0 1280 720 FF000000
349 112 549 378 155 525 90 76FFFFFF
350 112 549 512 306 262 108 76FFFFFF
351 113 552 0 0 1280 720
352 113 553 0 0 1280 720 FF000000
353 113 554 378 155 525 90 54FFFFFF
354 113 554 512 306 262 108 54FFFFFF
355 114 557 0 0 1280 720
356 114 558 0 0 1280 720 FF000000
357 114 559 378 155 525 90 43FFFFFF
358 114 559 512 306 262 108 43FFFFFF
359 115 562 0 0 1280 720
360 115 563 0 0 1280 720 FF000000
361 115 564 378 155 525 90 21FFFFFF
362 115 564 512 306 262 108 21FFFFFF
363 116 567 0 0 1280 720
364 116 568 0 0 1280 720 FF000000
365 116 569 390 162 499 72 FFFFFFFF
366 116 569 518 317 243 86 FFFFFFFF
367 117 572 0 0 1280 720
368 117 573 0 0 1280 720 FF000000
369 117 574 390 162 499 72 FFFFFFFF
370 117 574 518 317 243 86 FFFFFFFF
371 118 577 0 0 1280 720
372 118 578 0 0 1280 720 FF000000
373 118 579 390 162 499 72 FFFFFFFF
374 118 579 518 317 243 86 FFFFFFFF
375 119 582 0 0 1280 720
376 119 583 0 0 1280 720 FF000000
377 119 584 390 162 499 72 FFFFFFFF
378 119 584 518 317 243 86 FFFFFFFF
379 120 587 0 0 1280 720
380 120 588 0 0 1280 720 FF000000
381 120 589 390 162 499 72 FFFFFFFF
382 120 589 518 317 243 86 FFFFFFFF
383 121 592 0 0 1280 720
384 121 593 0 0 1280 720 FF000000
385 121 594 390 162 499 72 FFFFFFFF
386 121 594 518 317 243 86 FFFFFFFF
387 122 597 0 0 1280 720
388 122 598 0 0 1280 720 FF000000
389 122 599 390 162 499 72 FFFFFFFF
390 122 599 518 317 243 86 FFFFFFFF
391 123 602 0 0 1280 720
392 123 603 0 0 1280 720 FF000000
393 123 604 390 162 499 72 FFFFFFFF
394 123 604 518 317 243 86 FFFFFFFF
395 124 607 0 0 1280 720
396 124 608 0 0 1280 720 FF000000
397 124 609 390 162 499 72 FFFFFFFF
398 124 609 518 317 243 86 FFFFFFFF
399 125 612 0 0 1280 720
400 125 613 0 0 1280 720 FF000000
401 125 614 390 162 499 72 FFFFFFFF
402 125 614 518 317 243 86 FFFFFFFF
403 126 617 0 0 1280 720
404 126 618 0 0 1280 720 FF000000
405 126 619 390 162 499 72 FFFFFFFF
406 126 619 518 317 243 86 FFFFFFFF
407 127 622 0 0 1280 720
408 127 623 0 0 1280 720 FF000000
409 127 624 390 162 499 72 FFFFFFFF
410 127 624 518 317 243 86 FFFFFFFF
411 128 627 0 0 1280 720
412 128 628 0 0 1280 720 FF000000
413 128 629 390 162 499 72 FFFFFFFF
414 128 629 518 317 243 86 FFFFFFFF
415 129 632 0 0 1280 720
416 129 633 0 0 1280 720 FF000000
417 129 634 390 162 499 72 FFFFFFFF
418 129 634 518 317 243 86 FFFFFFFF
419 130 637 0 0 1280 720
420 130 638 0 0 1280 720 FF000000
421 130 639 390 162 499 72 FFFFFFFF
422 130 639 518 317 243 86 FFFFFFFF
423 131 642 0 0 1280 720
424 131 643 0 0 1280 720 FF000000
425 131 644 390 162 499 72 FFFFFFFF
426 131 644 518 317 243 86 FFFFFFFF
427 132 647 0 0 1280 720
428 132 648 0 0 1280 720 FF000000
429 132 649 390 162 499 72 FFFFFFFF
430 132 649 518 317 243 86 FFFFFFFF
431 133 652 0 0 1280 720
432 133 653 0 0 1280 720 FF000000
433 133 654 390 162 499 72 FFFFFFFF
434 133 654 518 317 243 86 FFFFFFFF
435 134 657 0 0 1280 720
436 134 658 0 0 1280 720 FF000000
437 134 659 390 162 499 72 FFFFFFFF
438 134 659 518 317 243 86 FFFFFFFF
439 135 662 0 0 1280 720
440 135 663 0 0 1280 720 FF000000
441 135 664 390 162 499 72 FFFFFFFF
442 135 664 518 317 243 86 FFFFFFFF
443 136 667 0 0 1280 720
444 136 668 0 0 1280 720 FF000000
445 136 669 390 162 499 72 FFFFFFFF
446 136 669 518 317 243 86 FFFFFFFF
447 137 672 0 0 1280 720
448 137 673 0 0 1280 720 FF000000
449 137 674 390 162 499 72 FFFFFFFF
450 137 674 518 317 243 86 FFFFFFFF
451 138 677 0 0 1280 720
452 138 678 0 0 1280 720 FF000000
453 138 679 390 162 499 72 FFFFFFFF
454 138 679 518 317 243 86 FFFFFFFF
455 140 682 0 0 1280 720
456 140 683 0 0 1280 720 FF000000
457 140 684 390 162 499 72 FFFFFFFF
458 140 684 518 317 243 86 FFFFFFFF
459 141 687 0 0 1280 720
460 141 688 0 0 1280 720 FF000000
461 141 689 390 162 499 72 FFFFFFFF
462 141 689 518 317 243 86 FFFFFFFF
463 142 692 0 0 1280 720
464 142 693 0 0 1280 720 FF000000
465 142 694 390 162 499 72 FFFFFFFF
466 142 694 518 317 243 86 FFFFFFFF
467 143 697 0 0 1280 720
468 143 698 0 0 1280 720 FF000000
469 143 699 390 162 499 72 FFFFFFFF
470 143 699 518 317 243 86 FFFFFFFF
471 144 702 0 0 1280 720
472 144 703 0 0 1280 720 FF000000
473 144 704 390 162 499 72 FFFFFFFF
474 144 704 518 317 243 86 FFFFFFFF
475 145 707 0 0 1280 720
476 145 708 0 0 1280 720 FF000000
477 145 709 390 162 499 72 FFFFFFFF
478 145 709 518 317 243 86 FFFFFFFF
479 146 712 0 0 1280 720
480 146 713 0 0 1280 720 FF000000
481 146 714 390 162 499 72 FFFFFFFF
482 146 714 518 317 243 86 FFFFFFFF
483 147 717 0 0 1280 720
484 147 718 0 0 1280 720 FF000000
485 147 719 390 162 499 72 FFFFFFFF
486 147 719 518 317 243 86 FFFFFFFF
487 148 722 0 0 1280 720
488 148 723 0 0 1280 720 FF000000
489 148 724 390 162 499 72 FFFFFFFF
490 148 724 518 317 243 86 FFFFFFFF
491 149 727 0 0 1280 720
492 149 728 0 0 1280 720 FF000000
493 149 729 390 162 499 72 FFFFFFFF
494 149 729 518 317 243 86 FFFFFFFF
495 150 732 0 0 1280 720
496 150 733 0 0 1280 720 FF000000
497 150 734 390 162 499 72 FFFFFFFF
498 150 734 518 317 243 86 FFFFFFFF
499 151 737 0 0 1280 720
500 151 738 0 0 1280 720 FF000000
501 151 739 390 162 499 72 FFFFFFFF
502 151 739 518 317 243 86 FFFFFFFF
503 152 742 0 0 1280 720
504 152 743 0 0 1280 720 FF000000
505 152 744 390 162 499 72 FFFFFFFF
506 152 744 518 317 243 86 FFFFFFFF
507 153 747 0 0 1280 720
508 153 748 0 0 1280 720 FF000000
509 153 749 390 162 499 72 FFFFFFFF
510 153 749 518 317 243 86 FFFFFFFF
511 154 752 0 0 1280 720
512 154 753 0 0 1280 720 FF000000
513 154 754 390 162 499 72 FFFFFFFF
514 154 754 518 317 243 86 FFFFFFFF
515 155 757 0 0 1280 720
516 155 758 0 0 1280 720 FF000000
517 155 759 390 162 499 72 FFFFFFFF
518 155 759 518 317 243 86 FFFFFFFF
519 156 762 0 0 1280 720
520 156 763 0 0 1280 720 FF000000
521 156 764 390 162 499 72 FFFFFFFF
522 156 764 518 317 243 86 FFFFFFFF
523 157 767 0 0 1280 720
524 157 768 0 0 1280 720 FF000000
525 157 769 390 162 499 72 FFFFFFFF
526 157 769 518 317 243 86 FFFFFFFF
527 158 772 0 0 1280 720
528 158 773 0 0 1280 720 FF000000
529 158 774 390 162 499 72 FFFFFFFF
530 158 774 518 317 243 86 FFFFFFFF
531 159 777 0 0 1280 720
532 159 778 0 0 1280 720 FF000000
533 159 779 390 162 499 72 FFFFFFFF
534 159 779 518 317 243 86 FFFFFFFF
535 160 782 0 0 1280 720
536 160 783 0 0 1280 720 FF000000
537 160 784 390 162 499 72 FFFFFFFF
538 160 784 518 317 243 86 FFFFFFFF
539 161 787 0 0 1280 720
540 161 788 0 0 1280 720 FF000000
541 161 789 390 162 499 72 FFFFFFFF
542 161 789 518 317 243 86 FFFFFFFF
543 162 792 0 0 1280 720
544 162 793 0 0 1280 720 FF000000
545 162 794 390 162 499 72 FFFFFFFF
546 162 794 518 317 243 86 FFFFFFFF
547 163 797 0 0 1280 720
548 163 798 0 0 1280 720 FF000000
549 163 799 390 162 499 72 FFFFFFFF
550 163 799 518 317 243 86 FFFFFFFF
551 164 802 0 0 1280 720
552 164 803 0 0 1280 720 FF000000
553 164 804 390 162 499 72 FFFFFFFF
554 164 804 518 317 243 86 FFFFFFFF
555 165 807 0 0 1280 720
556 165 808 0 0 1280 720 FF000000
557 165 809 390 162 499 72 FFFFFFFF
558 165 809 518 317 243 86 FFFFFFFF
559 166 812 0 0 1280 720
560 166 813 0 0 1280 720 FF000000
561 166 814 390 162 499 72 FFFFFFFF
562 166 814 518 317 243 86 FFFFFFFF
563 167 817 0 0 1280 720
564 167 818 0 0 1280 720 FF000000
565 167 819 390 162 499 72 FFFFFFFF
566 167 819 518 317 243 86 FFFFFFFF
567 168 822 0 0 1280 720
568 168 823 0 0 1280 720 FF000000
569 168 824 390 162 499 72 FFFFFFFF
570 168 824 518 317 243 86 FFFFFFFF
571 169 827 0 0 1280 720
572 169 828 0 0 1280 720 FF000000
573 169 829 390 162 499 72 FFFFFFFF
574 169 829 518 317 243 86 FFFFFFFF
575 170 832 0 0 1280 720
576 170 833 0 0 1280 720 FF000000
577 170 834 390 162 499 72 FFFFFFFF
578 170 834 518 317 243 86 FFFFFFFF
579 171 837 0 0 1280 720
580 171 838 0 0 1280 720 FF000000
581 171 839 390 162 499 72 FFFFFFFF
582 171 839 518 317 243 86 FFFFFFFF
583 172 842 0 0 1280 720
584 172 843 0 0 1280 720 FF000000
585 172 844 390 162 499 72 FFFFFFFF
586 172 844 518 317 243 86 FFFFFFFF
587 173 847 0 0 1280 720
588 173 848 0 0 1280 720 FF000000
589 173 849 390 162 499 72 FFFFFFFF
590 173 849 518 317 243 86 FFFFFFFF
591 174 852 0 0 1280 720
592 174 853 0 0 1280 720 FF000000
593 174 854 390 162 499 72 FFFFFFFF
594 174 854 518 317 243 86 FFFFFFFF
595 175 857 0 0 1280 720
596 175 858 0 0 1280 720 FF000000
597 175 859 390 162 499 72 FFFFFFFF
598 175 859 518 317 243 86 FFFFFFFF
599 176 862 0 0 1280 720
600 176 863 0 0 1280 720 FF000000
601 176 864 390 162 499 72 FFFFFFFF
602 176 864 518 317 243 86 FFFFFFFF
603 177 867 0 0 1280 720
604 177 868 0 0 1280 720 FF000000
605 177 869 390 162 499 72 FFFFFFFF
606 177 869 518 317 243 86 FFFFFFFF
607 178 872 0 0 1280 720
608 178 873 0 0 1280 720 FF000000
609 178 874 390 162 499 72 FFFFFFFF
610 178 874 518 317 243 86 FFFFFFFF
611 179 877 0 0 1280 720
612 179 878 0 0 1280 720 FF000000
613 179 879 390 162 499 72 FFFFFFFF
614 179 879 518 317 243 86 FFFFFFFF
615 180 882 0 0 1280 720
616 180 883 0 0 1280 720 FF000000
617 180 884 390 162 499 72 FFFFFFFF
618 180 884 518 317 243 86 FFFFFFFF
619 181 887 0 0 1280 720
620 181 888 0 0 1280 720 FF000000
621 181 889 390 162 499 72 FFFFFFFF
622 181 889 518 317 243 86 FFFFFFFF
623 182 892 0 0 1280 720
624 182 893 0 0 1280 720 FF000000
625 182 894 390 162 499 72 FFFFFFFF
626 182 894 518 317 243 86 FFFFFFFF
627 183 897 0 0 1280 720
628 183 898 0 0 1280 720 FF000000
629 183 899 390 162 499 72 FFFFFFFF
630 183 899 518 317 243 86 FFFFFFFF
631 184 902 0 0 1280 720
632 184 903 0 0 1280 720 FF000000
633 184 904 390 162 499 72 FFFFFFFF
634 184 904 518 317 243 86 FFFFFFFF
635 185 907 0 0 1280 720
636 185 908 0 0 1280 720 FF000000
637 185 909 390 162 499 72 FFFFFFFF
638 185 909 518 317 243 86 FFFFFFFF
639 186 912 0 0 1280 720
640 186 913 0 0 1280 720 FF000000
641 186 914 390 162 499 72 FFFFFFFF
642 186 914 518 317 243 86 FFFFFFFF
643 187 917 0 0 1280 720
644 187 918 0 0 1280 720 FF000000
645 187 919 390 162 499 72 FFFFFFFF
646 187 919 518 317 243 86 FFFFFFFF
647 188 922 0 0 1280 720
648 188 923 0 0 1280 720 FF000000
649 188 924 390 162 499 72 FFFFFFFF
650 188 924 518 317 243 86 FFFFFFFF
651 189 927 0 0 1280 720
652 189 928 0 0 1280 720 FF000000
653 189 929 390 162 499 72 FFFFFFFF
654 189 929 518 317 243 86 FFFFFFFF
655 190 932 0 0 1280 720
656 190 933 0 0 1280 720 FF000000
657 190 934 390 162 499 72 FFFFFFFF
658 190 934 518 317 243 86 FFFFFFFF
659 191 937 0 0 1280 720
660 191 938 0 0 1280 720 FF000000
661 191 939 390 162 499 72 FFFFFFFF
662 191 939 518 317 243 86 FFFFFFFF
663 192 942 0 0 1280 720
664 192 943 0 0 1280 720 FF000000
665 192 944 390 162 499 72 FFFFFFFF
666 192 944 518 317 243 86 FFFFFFFF
667 193 947 0 0 1280 720
668 193 948 0 0 1280 720 FF000000
669 193 949 390 162 499 72 FFFFFFFF
670 193 949 518 317 243 86 FFFFFFFF
671 194 952 0 0 1280 720
672 194 953 0 0 1280 720 FF000000
673 194 954 390 162 499 72 FFFFFFFF
674 194 954 518 317 243 86 FFFFFFFF
675 195 957 0 0 1280 720
676 195 958 0 0 1280 720 FF000000
677 195 959 390 162 499 72 FFFFFFFF
678 195 959 518 317 243 86 FFFFFFFF
679 196 962 0 0 1280 720
680 196 963 0 0 1280 720 FF000000
681 196 964 390 162 499 72 FFFFFFFF
682 196 964 518 317 243 86 FFFFFFFF
683 197 967 0 0 1280 720
684 197 968 0 0 1280 720 FF000000
685 197 969 390 162 499 72 FFFFFFFF
686 197 969 518 317 243 86 FFFFFFFF
687 198 972 0 0 1280 720
688 198 973 0 0 1280 720 FF000000
689 198 974 390 162 499 72 FFFFFFFF
690 198 974 518 317 243 86 FFFFFFFF
691 199 977 0 0 1280 720
692 199 978 0 0 1280 720 FF000000
693 199 979 390 162 499 72 FEFFFFFF
694 199 979 518 317 243 86 FEFFFFFF
695 200 982 0 0 1280 720
696 200 983 0 0 1280 720 FF000000
697 200 984 390 162 499 72 F3FFFFFF
698 200 984 518 317 243 86 F3FFFFFF
699 201 987 0 0 1280 720
700 201 988 0 0 1280 720 FF000000
701 201 989 390 162 499 72 EDFFFFFF
702 201 989 518 317 243 86 EDFFFFFF
703 202 992 0 0 1280 720
704 202 993 0 0 1280 720 FF000000
705 202 994 390 162 499 72 D7FFFFFF
706 202 994 518 317 243 86 D7FFFFFF
707 203 997 0 0 1280 720
708 203 998 0 0 1280 720 FF000000
709 203 999 390 162 499 72 B5FFFFFF
710 203 999 518 317 243 86 B5FFFFFF
711 204 1002 0 0 1280 720
712 204 1003 0 0 1280 720 FF000000
713 204 1004 390 162 499 72 94FFFFFF
714 204 1004 518 317 243 86 94FFFFFF
715 205 1007 0 0 1280 720
716 205 1008 0 0 1280 720 FF000000
717 205 1009 390 162 499 72 83FFFFFF
718 205 1009 518 317 243 86 83FFFFFF
719 206 1012 0 0 1280 720
720 206 1013 0 0 1280 720 FF000000
721 206 1014 390 162 499 72 62FFFFFF
722 206 1014 518 317 243 86 62FFFFFF
723 207 1017 0 0 1280 720
724 207 1018 0 0 1280 720 FF000000
725 207 1019 390 162 499 72 41FFFFFF
726 207 1019 518 317 243 86 41FFFFFF
727 208 1022 0 0 1280 720
728 208 1023 0 0 1280 720 FF000000
729 208 1024 390 162 499 72 30FFFFFF
730 208 1024 518 317 243 86 30FFFFFF
731 209 1027 0 0 1280 720
732 209 1028 0 0 1280 720 FF000000
733 209 1029 390 162 499 72 17FFFFFF
734 209 1029 518 317 243 86 17FFFFFF
735 210 1032 0 0 1280 720
736 210 1033 0 0 1280 720 FF000000
737 210 1034 390 162 499 72 0FFFFFFF
738 210 1034 518 317 243 86 0FFFFFFF
739 211 1037 0 0 1280 720
740 211 1038 0 0 1280 720 FF000000
741 211 1039 390 162 499 72 07FFFFFF
742 211 1039 518 317 243 86 07FFFFFF
743 212 1042 0 0 1280 720
744 212 1043 0 0 1280 720 FF000000
745 213 1046 0 0 1280 720
746 213 1047 0 0 1280 720 FF000000
747 214 1050 0 0 1280 720
748 214 1051 0 0 1280 720 FF000000
749 215 1054 0 0 1280 720
750 216 1057 0 0 1280 720
751 217 1060 0 0 1280 720
752 218 1063 0 0 1280 720
753 219 1066 0 0 1280 720
754 220 1069 0 0 1280 720
755 221 1072 0 0 1280 720
756 221 1073 0 0 1280 720
757 223 1076 0 0 1280 720
758 223 1077 0 0 1280 720
759 224 1080 0 0 1280 720
760 224 1081 0 0 1280 720
761 226 1084 0 0 1280 720
762 226 1085 0 0 1280 720
763 227 1088 0 0 1280 720
764 227 1089 0 0 1280 720
765 228 1092 0 0 1280 720
766 228 1093 0 0 1280 720
767 229 1096 0 0 1280 720
768 229 1097 0 0 1280 720
769 230 1100 0 0 1280 720
770 230 1101 0 0 1280 720
771 231 1104 0 0 1280 720
772 231 1105 0 0 1280 720
773 232 1108 0 0 1280 720
774 232 1109 0 0 1280 720
775 233 1112 0 0 1280 720
776 233 1113 0 0 1280 720
777 234 1116 0 0 1280 720
778 234 1117 0 0 1280 720
779 236 1120 0 0 1280 720
780 236 1121 0 0 1280 720
781 237 1124 0 0 1280 720
782 237 1125 0 0 1280 720
783 238 1128 0 0 1280 720
784 238 1129 0 0 1280 720
785 239 1132 0 0 1280 720
786 239 1133 0 0 1280 720
787 240 1136 0 0 1280 720
788 240 1137 0 0 1280 720
789 241 1140 0 0 1280 720
790 241 1141 0 0 1280 720

View File

@@ -0,0 +1,33 @@
Frame-signature match of captured frames against five candidate movies.
32x18 normalised grayscale, movie frames cropped to the 675/720 the game
surface shows, 1 fps sampling, best correlation over the whole movie.
CONTROL - frames captured during the BOOT movie, known to be ADV.wmv:
p2.png -> ADV @116s 0.644 (runner-up S13A 0.438)
p3.png -> ADV @121s 0.994 (runner-up S03A 0.650)
p4.png -> S13A @ 35s 0.984 (runner-up ADV 0.926)
p5.png -> ADV @129s 0.933 (runner-up S13A 0.809)
p6.png -> ADV @134s 0.992 (runner-up S13A 0.499)
p7.png -> S15B @ 88s 0.000 (runner-up S13A 0.000)
p8.png -> S13A @ 65s 0.576 (runner-up ADV 0.534)
TEST - frames captured during the ATTRACT loop, 5 s apart:
attract01.png -> ADV @ 39s 0.990 (runner-up S13A 0.487)
attract02.png -> ADV @ 45s 0.961 (runner-up S13A 0.411)
attract03.png -> S03A @ 37s 0.731 (runner-up ADV 0.422)
attract04.png -> ADV @ 56s 0.955 (runner-up S03A 0.532)
attract05.png -> ADV @ 62s 0.934 (runner-up S15B 0.245)
attract06.png -> S15B @ 65s 0.885 (runner-up ADV 0.743)
attract07.png -> ADV @ 75s 0.961 (runner-up S03A 0.355)
attract08.png -> ADV @ 80s 0.938 (runner-up S03A 0.753)
attract09.png -> ADV @ 85s 0.974 (runner-up S03A 0.484)
attract10.png -> ADV @ 92s 0.892 (runner-up S13A 0.754)
attract11.png -> S03A @ 24s 0.606 (runner-up ADV 0.588)
attract12.png -> ADV @102s 0.994 (runner-up S03A 0.374)
attract13.png -> ADV @108s 0.977 (runner-up S13A 0.496)
attract14.png -> ADV @113s 0.836 (runner-up S06B 0.464)
attract15.png -> ADV @119s 0.958 (runner-up S15B 0.424)
attract16.png -> S13A @ 32s 0.996 (runner-up ADV 0.980)
attract17.png -> ADV @131s 0.865 (runner-up S13A 0.769)
attract18.png -> ADV @137s 0.993 (runner-up S13A 0.502)
attract19.png -> S13A @ 65s 0.551 (runner-up ADV 0.534)

View File

@@ -0,0 +1,35 @@
bank waves per-wave (bytes, bytes/s, Hz, seconds)
BGM_001.slb 2 4466688 25697 48000 173.82s | 4673536 26887 48000 173.82s EQUAL-LENGTH
BGM_002.slb 2 4382720 26376 48000 166.16s | 4667392 28090 48000 166.16s EQUAL-LENGTH
BGM_003.slb 2 3555328 26201 48000 135.69s | 3414016 25159 48000 135.70s EQUAL-LENGTH
BGM_004.slb 2 6311936 32874 48000 192.00s | 5787648 30143 48000 192.01s EQUAL-LENGTH
BGM_005.slb 2 4005888 28013 48000 143.00s | 3917824 27397 48000 143.00s EQUAL-LENGTH
BGM_006.slb 2 3946496 26377 48000 149.62s | 3915776 26172 48000 149.62s EQUAL-LENGTH
BGM_007.slb 2 7565312 29269 48000 258.48s | 8099840 31337 48000 258.48s EQUAL-LENGTH
BGM_008.slb 2 3258368 29947 48000 108.80s | 3397632 31227 48000 108.80s EQUAL-LENGTH
BGM_009.slb 2 3819520 25636 48000 148.99s | 3614720 24262 48000 148.99s EQUAL-LENGTH
BGM_010.slb 2 4190208 28457 48000 147.25s | 4100096 27845 48000 147.25s EQUAL-LENGTH
BGM_011.slb 2 4222976 25232 48000 167.37s | 4311040 25758 48000 167.37s EQUAL-LENGTH
BGM_012.slb 2 5562368 31605 48000 176.00s | 5404672 30709 48000 176.00s EQUAL-LENGTH
BGM_013.slb 2 6539264 29904 48000 218.68s | 6457344 29530 48000 218.67s EQUAL-LENGTH
BGM_014.slb 2 6656000 31438 48000 211.72s | 6948864 32821 48000 211.72s EQUAL-LENGTH
BGM_015.slb 2 8017920 29230 48000 274.30s | 7974912 29074 48000 274.30s EQUAL-LENGTH
BGM_016.slb 2 3549184 28341 48000 125.23s | 2775040 22159 48000 125.23s EQUAL-LENGTH
BGM_017.slb 2 5353472 30690 48000 174.44s | 5062656 29023 48000 174.44s EQUAL-LENGTH
BGM_018.slb 2 3620864 26878 48000 134.71s | 2920448 21679 48000 134.71s EQUAL-LENGTH
BGM_019.slb 2 6834176 30665 48000 222.87s | 6713344 30123 48000 222.86s EQUAL-LENGTH
BGM_020.slb 2 4804608 29592 44100 162.36s | 4675584 28798 44100 162.36s EQUAL-LENGTH
BGM_021.slb 2 7780352 28034 44100 277.53s | 7694336 27724 44100 277.53s EQUAL-LENGTH
BGM_022.slb 2 5545984 28006 44100 198.03s | 5867520 29630 44100 198.03s EQUAL-LENGTH
BGM_023.slb 2 4962304 28001 44100 177.22s | 5044224 28463 44100 177.22s EQUAL-LENGTH
BGM_101.slb 2 4800512 27481 48000 174.68s | 2635776 15089 48000 174.68s EQUAL-LENGTH
BGM_102.slb 2 1150976 30703 48000 37.49s | 1269760 33872 48000 37.49s EQUAL-LENGTH
BGM_103.slb 2 3876864 44181 48000 87.75s | 3930112 44788 48000 87.75s EQUAL-LENGTH
BGM_104.slb 2 2672640 31503 48000 84.84s | 2674688 31527 48000 84.84s EQUAL-LENGTH
BGM_105.slb 2 3581952 35879 48000 99.83s | 3850240 38566 48000 99.84s EQUAL-LENGTH
BGM_106.slb 2 4098048 45389 48000 90.29s | 2383872 36575 48000 65.18s
BGM_107.slb 2 2418688 37109 48000 65.18s | 3696640 28676 48000 128.91s
BGM_108.slb 2 3696640 28676 48000 128.91s | 2887680 35432 48000 81.50s
BGM_109.slb 4 2932736 35985 48000 81.50s | 112640 13377 48000 8.42s | 225280 14228 48000 15.83s | 235520 14795 48000 15.92s
32 BGM banks; 28 are exactly two waves of equal duration

View File

@@ -0,0 +1,10 @@
# tools/re-capture/doc_link_check.py -- 2026-08-29, after the repair pass
1050 link(s) resolve
5 MISSING target(s):
docs/re/autopilot-knowledge-sources.md -> ../../MEMORY.md
docs/re/challenge-mission-gate.md -> ../../../xenia-canary-native/src/xenia/hid/file/file_input_driver.h
docs/re/entities-live-roster.md -> ../../MEMORY.md
docs/re/mission-freeze-resume-spin.md -> canary-build-verified-env-confound.md
docs/re/stage-drift-is-navigation-not-save.md -> structures/weapon-datasheet-runtime.md

View File

@@ -0,0 +1,7 @@
matched 170 decoded textures to a T8aD flag word
bit SET n= 61 mean %(RGB>A) 55.52 median 52.52
bit clear n=109 mean %(RGB>A) 33.66 median 30.17
premultiplied would require ~0% for the flagged group -> REFUTED
best single-threshold accuracy: 76.5% at %(RGB>A) > 83.5
(base rate, always-guess-majority: 64.1%)

View File

@@ -0,0 +1,24 @@
# tools/re-capture/eff_bit_census.py over dat/GP_*.pak -- 2026-08-29
sprites with a resolvable preceding name: 14709
bit SET & name has 'eff' : 2338
bit SET & name lacks 'eff': 2657 <-- counterexamples to 'set => eff'
bit clear & name has 'eff' : 1399
bit clear & name lacks 'eff': 8315
P(name has 'eff' | bit set) = 0.468
P(name has 'eff' | bit clear) = 0.144
counterexamples (bit set, no 'eff'):
('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126')
('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27')
('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126')
('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27')
('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101')
('GP_BUNK.pak', 'pv_loading_ring0.t32', '00000832', '142x142')
('GP_BUNK.pak', 'pv_loading_light0.t32', '00000832', '29x25')
('GP_BUNK.pak', 'pv_loading_light1.t32', '00000832', '30x29')
('GP_BUNK.pak', 'pv_loading_light2.t32', '00000832', '29x30')
('GP_BUNK.pak', 'pv_loading_light3.t32', '00000832', '25x29')
('GP_BUNK.pak', 'pv_loading_ring1.t32', '00000832', '72x72')
('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101')

View File

@@ -0,0 +1,2 @@
main menu: corr +0.0475 controls +0.0032 (shift) -0.0075 (flip) ceiling 0.070 -> 68% of it
title: corr +0.0634 controls +0.0095 (shift) +0.0086 (flip) ceiling 0.094 -> 68% of it

View File

@@ -0,0 +1,29 @@
GamePart ids bound to a C++ class by the executable's own registration strings
(24 strings, 24 distinct ids)
0 GamePart_Title
3 GamePart_SaveLoad
4 GamePart_SaveLoad
5 GamePart_Extras
6 GamePart_MovieTheater
7 GamePart_MissionSelect
8 GamePart_Options
9 GamePart_Movie
10 GamePart_Bunk
11 GamePart_ReadyRoom
12 GamePart_Hangar
13 GamePart_Arsenal
14 GamePart_PilotLog
15 GamePart_System
17 GamePart_MainGame
19 GamePart_PauseMenu
20 GamePart_StageClear
21 GamePart_MissionLog
22 GamePart_GameOver
23 GamePart_Debriefing
24 GamePart_Dialog
25 GamePart_Tutorial
26 GamePart_ChallengeMission
27 GamePart_Leaderboard
ids 0..28 with NO registration string: [1, 2, 16, 18, 28]

View File

@@ -0,0 +1,9 @@
# canary --log_mask=12 --log_level=3 (Kernel logging ON), 2026-08-29
# control: the same log carries 359 VdRetrainEDRAM and 358 VdGetSystemCommandBuffer lines,
# so a missing call would have been visible.
F 820006F4 8284E27C 1B9 ( 441) VdGetCurrentDisplayGamma
F 8200070C 8284E2BC 1BA ( 442) VdGetCurrentDisplayInformation
F 82000710 8284E2CC 1D3 ( 467) VdSetDisplayMode
d> F8000008 VdGetSystemCommandBuffer(701CF830, 701CF804)
d> F8000008 VdGetCurrentDisplayGamma(701CE1F8(00000000), 701CE1F0(0))

View File

@@ -0,0 +1,51 @@
# tools/re-capture/kf_rotation_census.py over dat/GP_*.pak — 2026-08-28
# CONTROL must read 3/3; see the tool docstring for why.
CONTROL ptloop01: 3/3 blocks found, +12 = {30} (want 30)
CONTROL ptloop02: 3/3 blocks found, +12 = {-45} (want -45)
blocks scanned disc-wide: 83862
+4: non-zero in 3990 ( 4.76 %)
+8: non-zero in 3878 ( 4.62 %)
+12: non-zero in 12164 (14.50 %)
value histogram (non-zero), top 25:
+4 = 180 x3880
+8 = 180 x3102
+12 = 90 x1824
+12 = -90 x1176
+12 = 360 x1173
+12 = 120 x492
+12 = 180 x474
+12 = -58 x402
+12 = 53 x396
+12 = -125 x378
+12 = -120 x366
+12 = -66 x294
+12 = 114 x240
+8 = 90 x201
+12 = 129 x200
+12 = -43 x199
+12 = 115 x186
+12 = -360 x180
+8 = -180 x156
+12 = 58 x156
+8 = 178 x144
+12 = -33 x138
+12 = 124 x138
+12 = -133 x126
+12 = 130 x126
examples:
+12 ('GP_BUNK', '117ca14f', '0x73961b', 360)
+12 ('GP_BUNK', '117ca14f', '0x7398ac', 360)
+12 ('GP_BUNK', '117ca14f', '0x739a93', 360)
+12 ('GP_BUNK', '117ca14f', '0x739d24', 360)
+4 ('GP_BUNK', '117ca14f', '0x73ae5d', -180)
+4 ('GP_BUNK', '117ca14f', '0x73ae85', -180)
+4 ('GP_BUNK', '1ff0bcb0', '0x733d68', -180)
+4 ('GP_BUNK', '1ff0bcb0', '0x733d90', -180)
+8 ('GP_DEBRIEFING_PILOTLOG', '18ab1d66', '0x6f8', 90)
+8 ('GP_DEBRIEFING_PILOTLOG', '18ab1d66', '0x770', 90)
+8 ('GP_DEBRIEFING_PILOTLOG', '1c8a0129', '0x6f8', 90)
+8 ('GP_DEBRIEFING_PILOTLOG', '1c8a0129', '0x770', 90)

View File

@@ -0,0 +1,97 @@
ADV.wmv,wmapro,48000,6,5.1
RT01A.wmv,wmav2,48000,2,unknown
RT01B.wmv,wmav2,48000,2,unknown
RT01C_1.wmv,wmav2,48000,2,unknown
RT01C_2.wmv,wmav2,48000,2,unknown
RT02A.wmv,wmav2,48000,2,unknown
RT02B.wmv,wmav2,48000,2,unknown
RT02C.wmv,wmav2,48000,2,unknown
RT02D_1.wmv,wmav2,48000,2,unknown
RT02D_2.wmv,wmav2,48000,2,unknown
RT03A.wmv,wmav2,48000,2,unknown
RT03B.wmv,wmav2,48000,2,unknown
RT03C.wmv,wmav2,48000,2,unknown
RT03D.wmv,wmav2,48000,2,unknown
RT04A.wmv,wmav2,48000,2,unknown
RT04B.wmv,wmav2,48000,2,unknown
RT05A.wmv,wmav2,48000,2,unknown
RT05B.wmv,wmav2,48000,2,unknown
RT05C.wmv,wmav2,48000,2,unknown
RT06A.wmv,wmav2,48000,2,unknown
RT06B.wmv,wmav2,48000,2,unknown
RT06C.wmv,wmav2,48000,2,unknown
RT06D.wmv,wmav2,48000,2,unknown
RT07A.wmv,wmav2,48000,2,unknown
RT07B.wmv,wmav2,48000,2,unknown
RT07C.wmv,wmav2,48000,2,unknown
RT08A.wmv,wmav2,48000,2,unknown
RT08B.wmv,wmav2,48000,2,unknown
RT08C.wmv,wmav2,48000,2,unknown
RT09A.wmv,wmav2,48000,2,unknown
RT09B.wmv,wmav2,48000,2,unknown
RT09C.wmv,wmav2,48000,2,unknown
RT09D.wmv,wmav2,48000,2,unknown
RT10A.wmv,wmav2,48000,2,unknown
RT10B.wmv,wmav2,48000,2,unknown
RT11A.wmv,wmav2,48000,2,unknown
RT11B.wmv,wmav2,48000,2,unknown
RT11C.wmv,wmav2,48000,2,unknown
RT12A.wmv,wmav2,48000,2,unknown
RT12B_1.wmv,wmav2,48000,2,unknown
RT12B_2.wmv,wmav2,48000,2,unknown
RT13A.wmv,wmav2,48000,2,unknown
RT13B_1.wmv,wmav2,48000,2,unknown
RT13B_2.wmv,wmav2,48000,2,unknown
RT14A.wmv,wmav2,48000,2,unknown
RT14B.wmv,wmav2,48000,2,unknown
RT14C.wmv,wmav2,48000,2,unknown
RT15A.wmv,wmav2,48000,2,unknown
RT15B.wmv,wmav2,48000,2,unknown
RT15C.wmv,wmav2,48000,2,unknown
RT16C.wmv,wmav2,48000,2,unknown
S00A.wmv,wmapro,48000,6,5.1
S01A.wmv,wmapro,48000,6,5.1
S02A.wmv,wmapro,48000,6,5.1
S02B.wmv,wmapro,48000,6,5.1
S02C.wmv,wmapro,48000,6,5.1
S03A.wmv,wmapro,48000,6,5.1
S04A.wmv,wmapro,48000,6,5.1
S04B.wmv,wmapro,48000,6,5.1
S05A.wmv,wmapro,48000,6,5.1
S06A.wmv,wmapro,48000,6,5.1
S06B.wmv,wmapro,48000,6,5.1
S07A.wmv,wmapro,48000,6,5.1
S07B.wmv,wmapro,48000,6,5.1
S09B.wmv,wmapro,48000,6,5.1
S10B.wmv,wmapro,48000,6,5.1
S11A.wmv,wmapro,48000,6,5.1
S11C.wmv,wmapro,48000,6,5.1
S12A.wmv,wmapro,48000,6,5.1
S12B.wmv,wmapro,48000,6,5.1
S12C.wmv,wmapro,48000,6,5.1
S13A.wmv,wmapro,48000,6,5.1
S13B.wmv,wmapro,48000,6,5.1
S14A.wmv,wmapro,48000,6,5.1
S15A.wmv,wmapro,48000,6,5.1
S15B.wmv,wmapro,48000,6,5.1
S15C.wmv,wmapro,48000,6,5.1
S16A.wmv,wmapro,48000,6,5.1
hokyu_DS_s02A.wmv,wmav2,48000,2,unknown
hokyu_DS_s07A.wmv,wmav2,48000,2,unknown
hokyu_DS_s07H.wmv,wmav2,48000,2,unknown
hokyu_DS_s08A.wmv,wmav2,48000,2,unknown
hokyu_DS_s13A.wmv,wmav2,48000,2,unknown
hokyu_DS_s14H.wmv,wmav2,48000,2,unknown
hokyu_LS_s02A.wmv,wmav2,48000,2,unknown
hokyu_LS_s02H.wmv,wmav2,48000,2,unknown
hokyu_LS_s03A.wmv,wmav2,48000,2,unknown
hokyu_LS_s03H.wmv,wmav2,48000,2,unknown
hokyu_LS_s06A.wmv,wmav2,48000,2,unknown
hokyu_LS_s06H.wmv,wmav2,48000,2,unknown
hokyu_LS_s09A.wmv,wmav2,48000,2,unknown
hokyu_LS_s09H.wmv,wmav2,48000,2,unknown
hokyu_LS_s11A.wmv,wmav2,48000,2,unknown
hokyu_LS_s14A.wmv,wmav2,48000,2,unknown
hokyu_LS_s15A.wmv,wmav2,48000,2,unknown
hokyu_LS_s24A.wmv,wmav2,48000,2,unknown
hokyu_LS_s27A.wmv,wmav2,48000,2,unknown
1 ADV.wmv wmapro 48000 6 5.1
2 RT01A.wmv wmav2 48000 2 unknown
3 RT01B.wmv wmav2 48000 2 unknown
4 RT01C_1.wmv wmav2 48000 2 unknown
5 RT01C_2.wmv wmav2 48000 2 unknown
6 RT02A.wmv wmav2 48000 2 unknown
7 RT02B.wmv wmav2 48000 2 unknown
8 RT02C.wmv wmav2 48000 2 unknown
9 RT02D_1.wmv wmav2 48000 2 unknown
10 RT02D_2.wmv wmav2 48000 2 unknown
11 RT03A.wmv wmav2 48000 2 unknown
12 RT03B.wmv wmav2 48000 2 unknown
13 RT03C.wmv wmav2 48000 2 unknown
14 RT03D.wmv wmav2 48000 2 unknown
15 RT04A.wmv wmav2 48000 2 unknown
16 RT04B.wmv wmav2 48000 2 unknown
17 RT05A.wmv wmav2 48000 2 unknown
18 RT05B.wmv wmav2 48000 2 unknown
19 RT05C.wmv wmav2 48000 2 unknown
20 RT06A.wmv wmav2 48000 2 unknown
21 RT06B.wmv wmav2 48000 2 unknown
22 RT06C.wmv wmav2 48000 2 unknown
23 RT06D.wmv wmav2 48000 2 unknown
24 RT07A.wmv wmav2 48000 2 unknown
25 RT07B.wmv wmav2 48000 2 unknown
26 RT07C.wmv wmav2 48000 2 unknown
27 RT08A.wmv wmav2 48000 2 unknown
28 RT08B.wmv wmav2 48000 2 unknown
29 RT08C.wmv wmav2 48000 2 unknown
30 RT09A.wmv wmav2 48000 2 unknown
31 RT09B.wmv wmav2 48000 2 unknown
32 RT09C.wmv wmav2 48000 2 unknown
33 RT09D.wmv wmav2 48000 2 unknown
34 RT10A.wmv wmav2 48000 2 unknown
35 RT10B.wmv wmav2 48000 2 unknown
36 RT11A.wmv wmav2 48000 2 unknown
37 RT11B.wmv wmav2 48000 2 unknown
38 RT11C.wmv wmav2 48000 2 unknown
39 RT12A.wmv wmav2 48000 2 unknown
40 RT12B_1.wmv wmav2 48000 2 unknown
41 RT12B_2.wmv wmav2 48000 2 unknown
42 RT13A.wmv wmav2 48000 2 unknown
43 RT13B_1.wmv wmav2 48000 2 unknown
44 RT13B_2.wmv wmav2 48000 2 unknown
45 RT14A.wmv wmav2 48000 2 unknown
46 RT14B.wmv wmav2 48000 2 unknown
47 RT14C.wmv wmav2 48000 2 unknown
48 RT15A.wmv wmav2 48000 2 unknown
49 RT15B.wmv wmav2 48000 2 unknown
50 RT15C.wmv wmav2 48000 2 unknown
51 RT16C.wmv wmav2 48000 2 unknown
52 S00A.wmv wmapro 48000 6 5.1
53 S01A.wmv wmapro 48000 6 5.1
54 S02A.wmv wmapro 48000 6 5.1
55 S02B.wmv wmapro 48000 6 5.1
56 S02C.wmv wmapro 48000 6 5.1
57 S03A.wmv wmapro 48000 6 5.1
58 S04A.wmv wmapro 48000 6 5.1
59 S04B.wmv wmapro 48000 6 5.1
60 S05A.wmv wmapro 48000 6 5.1
61 S06A.wmv wmapro 48000 6 5.1
62 S06B.wmv wmapro 48000 6 5.1
63 S07A.wmv wmapro 48000 6 5.1
64 S07B.wmv wmapro 48000 6 5.1
65 S09B.wmv wmapro 48000 6 5.1
66 S10B.wmv wmapro 48000 6 5.1
67 S11A.wmv wmapro 48000 6 5.1
68 S11C.wmv wmapro 48000 6 5.1
69 S12A.wmv wmapro 48000 6 5.1
70 S12B.wmv wmapro 48000 6 5.1
71 S12C.wmv wmapro 48000 6 5.1
72 S13A.wmv wmapro 48000 6 5.1
73 S13B.wmv wmapro 48000 6 5.1
74 S14A.wmv wmapro 48000 6 5.1
75 S15A.wmv wmapro 48000 6 5.1
76 S15B.wmv wmapro 48000 6 5.1
77 S15C.wmv wmapro 48000 6 5.1
78 S16A.wmv wmapro 48000 6 5.1
79 hokyu_DS_s02A.wmv wmav2 48000 2 unknown
80 hokyu_DS_s07A.wmv wmav2 48000 2 unknown
81 hokyu_DS_s07H.wmv wmav2 48000 2 unknown
82 hokyu_DS_s08A.wmv wmav2 48000 2 unknown
83 hokyu_DS_s13A.wmv wmav2 48000 2 unknown
84 hokyu_DS_s14H.wmv wmav2 48000 2 unknown
85 hokyu_LS_s02A.wmv wmav2 48000 2 unknown
86 hokyu_LS_s02H.wmv wmav2 48000 2 unknown
87 hokyu_LS_s03A.wmv wmav2 48000 2 unknown
88 hokyu_LS_s03H.wmv wmav2 48000 2 unknown
89 hokyu_LS_s06A.wmv wmav2 48000 2 unknown
90 hokyu_LS_s06H.wmv wmav2 48000 2 unknown
91 hokyu_LS_s09A.wmv wmav2 48000 2 unknown
92 hokyu_LS_s09H.wmv wmav2 48000 2 unknown
93 hokyu_LS_s11A.wmv wmav2 48000 2 unknown
94 hokyu_LS_s14A.wmv wmav2 48000 2 unknown
95 hokyu_LS_s15A.wmv wmav2 48000 2 unknown
96 hokyu_LS_s24A.wmv wmav2 48000 2 unknown
97 hokyu_LS_s27A.wmv wmav2 48000 2 unknown

View File

@@ -0,0 +1,57 @@
The r5 (name) argument at every call site of sub_821CC860.
Recovered by forward register simulation over the 100 instructions before
each call, tracking lis/addi/subi/mr. Gated on three sites read by eye:
0x821c6524 TITLE_SCREEN, 0x821c67c8 TITLE_MENU, 0x821c73c8 LOADING -- all three
reproduce, so the remaining rows are trusted.
call site caller r5 name
0x82185f14 sub_82185E80 0x820a2160 EXTRA
0x821860b4 sub_82186050 0x820a214c LOADING
0x82186350 sub_82186270 0x820a2140 NEW_ITEM
0x821863f8 sub_82186270 0x820a2140 NEW_ITEM
0x8218c094 sub_8218BFE0 0x820a2384 MENU
0x8219403c sub_82193648 - (not recovered)
0x82196b44 sub_82196A50 0x820a2168 FILE
0x82198540 sub_82198440 0x820a2570 BASE
0x821988e4 sub_82198808 0x820a270c MESSAGE
0x82198a5c sub_82198808 0x820a28ec MENU_DISABLE_SKIP
0x8219c3b4 sub_8219C328 0x820a2acc INFO
0x821a0b4c sub_821A0AD0 0x820a255c FADE
0x821a0b98 sub_821A0AD0 0x820a2ae8 BG
0x821a0bdc sub_821A0AD0 0x820a2adc DEBRIEFING
0x821a1028 sub_821A0F80 0x820a2ab4 LOCAL_BOARD
0x821a113c sub_821A0F80 0x820a255c FADE
0x821a1188 sub_821A0F80 0x820a2ae8 BG
0x821a129c sub_821A0F80 0x820a2554 TITLE
0x821a12e8 sub_821A0F80 0x820a2ad4 BLACK
0x821a13b4 sub_821A0F80 0x820a2ab4 LOCAL_BOARD
0x821a1a6c sub_821A19B0 0x820a2ac0 LIVE_BOARD
0x821a1bd4 sub_821A19B0 0x820a255c FADE
0x821a1c20 sub_821A19B0 0x820a2ae8 BG
0x821a1d34 sub_821A19B0 0x820a2554 TITLE
0x821a1d80 sub_821A19B0 0x820a2ad4 BLACK
0x821a1e4c sub_821A19B0 0x820a2ac0 LIVE_BOARD
0x821a2b48 sub_821A2A80 0x820a2a74 DETAIL
0x821b368c sub_821B34E8 0x820a2570 BASE
0x821b3780 sub_821B34E8 0x820a32ac WINDOW
0x821b9210 sub_821B9118 0x820a2168 FILE
0x821bc60c sub_821BC568 - (not recovered)
0x821bca58 sub_821BC978 0x820a2384 MENU
0x821be188 sub_821BDF88 0x820a2570 BASE
0x821be1ec sub_821BDF88 0x820a38ac TUTORIAL_MENU
0x821c1d30 sub_821C1B58 0x820a2168 FILE
0x821c2f04 sub_821C2EA0 0x820a2168 FILE
0x821c5c88 sub_821C5818 0x820a3d3c TITLE_SCREEN
0x821c5d70 sub_821C5818 0x820a339c BUTTON
0x821c6524 sub_821C6458 0x820a3d3c TITLE_SCREEN
0x821c67c8 sub_821C6458 0x820a3d30 TITLE_MENU
0x821c73c8 sub_821C6458 0x820a214c LOADING
0x821c9488 sub_821C93C8 0x820a2168 FILE
0x822b2e04 sub_822B2D80 0x820ab24c KEY
0x822b581c sub_822B5790 0x820ab2a4 GAMMA_TITLE
0x822b5914 sub_822B5790 0x820ab298 GAMMA_RGB
0x822b71ac sub_822B7128 0x820a99dc GAME
0x822b8700 sub_822B8670 0x820a3124 PAD
0x822b9ebc sub_822B9E38 0x820a1784 SOUND
46/48 recovered.

View File

@@ -0,0 +1,23 @@
# How much of each recorded paint order its draw capture actually cross-checked.
# 2026-08-29, tools/re-capture/order_crosscheck_reach.py
#
# The three measured_paint_order vectors are reads of the runtime CHILD ARRAY
# (ui-screen-runtime.md: "paint order (child slots)"). These captures are the
# cross-check, not the source. Quads = indices/4: a single draw can carry
# several (the menu's draw 9 has indices=24 = 6 quads), so counting draws
# under-reads what is on screen.
== docs/re/captures/title-screen-draw-order.log
frame 1: 8 draws, 12 quads, 5 textures
frame 2: 8 draws, 12 quads, 5 textures
frame 3: 8 draws, 12 quads, 5 textures
textures in EVERY captured frame: 5 of 5
== docs/re/captures/title-draw-order-with-alpha.log
frame 1: 8 draws, 12 quads, 5 textures
frame 2: 8 draws, 12 quads, 5 textures
textures in EVERY captured frame: 5 of 5
== docs/re/captures/main-menu-draw-order.log
frame 0: 9 draws, 16 quads, 5 textures
frame 3: 7 draws, 9 quads, 4 textures
textures in EVERY captured frame: 4 of 5
absent from some frame: ['11C30000']

View File

@@ -0,0 +1,35 @@
# cargo run -p sylpheed-formats --example paint_order_audit -- dat/GP_TITLE.pak
# 2026-08-29
1682 | let decl = &decls[0];
290 | let mut flush = |base: u32,
entry 0 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
entry 1 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
entry 2 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 3 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 4 title 24 elements
derived == measured : NO
inverted pairs : 8 (of which same-layer-key ties: 8)
measured: [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8]
derived : [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, 22, 23, 21, 8]
keys : [32928, 32928, 32928, 32928, 32928, 32928, 32832, 32928, 4294967295, 32768, 4294967295, 32784, 32784, 4294967295, 32899, 32899, 32899, 32899, 32899, 32898, 32897, 33024, 32936, 32937]
entry 5 main menu 16 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 6 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
entry 7 (no measured order) 30 elements, 37 tied pairs, 16 of them OVERLAPPING
entry 8 main menu 16 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 9 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
entry 10 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 11 splash 7 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 12 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
entry 13 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
entry 14 splash 7 elements
derived == measured : YES
inverted pairs : 0 (of which same-layer-key ties: 0)
entry 15 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
5 build(s) with a measured order were checked

View File

@@ -0,0 +1,25 @@
CONTROL GP_TITLE build 7: fallback elements [8, 13, 14] (want [8, 13, 14])
index 8 is 'ptlogo_eff3.t32' (want ptlogo_eff3.t32)
elements with a keyframe group, disc-wide: 15493
no plateau -> rest pose is GUESSED: 3807 (24.57 %)
most common guessed elements:
1002 pb_name_eff.t32
90 gbp301i.t32
84 pb_warp_eff.t32
78 pbb_destroyer.t32
70 gbp303i.t32
60 pb_w_line.t32
54 pbmwindow_eff.t32
54 pbfriendly.t32
32 pzeff00.prm
32 pzeff02.t32
32 px_mission_area.rat
30 pbr_fighter.t32
--- the 3807 plateau-less elements, by what each candidate rule returns
current rule (longest dwell) returns an INVISIBLE pose : 1711 ( 44.9 %)
current rule returns a ZERO-SCALE (degenerate) pose : 195 ( 5.1 %)
'rest = last keyframe' returns an INVISIBLE pose : 1618 ( 42.5 %)
the two rules agree : 1911 ( 50.2 %)

View File

@@ -0,0 +1,16 @@
# tools/re-capture/plateauless_endstate.py over dat/GP_*.pak -- 2026-08-29
plateau-less, multi-keyframe, ending VISIBLE: 687
alpha never decreases (a fade/slide IN that stops): 472 (68.7 %)
ends at its maximum alpha but dips on the way : 203 (29.5 %)
ends visible but NOT at maximum alpha : 12 (1.7 %)
examples of the monotone kind (alpha sequence, then scale pairs):
GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
GP_MAIN_GAME_D2D.pak pghud_speed_cut.t32 [0, 255] [(100, 100), (100, 100)]
GP_MAIN_GAME_D2D.pak pgmsg_restart_sub.rat [255, 255] [(106, 106), (100, 100)]

View File

@@ -0,0 +1,52 @@
# RATC child names: the printable-run scan vs the `opt ` block, all 33 dat/*.pak
# 2026-08-29, crates/sylpheed-formats/examples/ratc_child_names.rs
#
# RUN 1 was taken BEFORE the fix, when ratc::parse still named children by
# scanning for the last printable run. It is the evidence for the defect.
RATC children scanned : 18002
with an `opt ` block: 17942
scanned name AGREES : 17918
scanned name DIFFERS : 24
distinct disagreements (scanned -> opt), with counts:
'OX -> po_keys_win1.t32 x2
8AX -> pbbg.t32 x12
8AX -> pmbase.t32 x4
8AX -> pteff04.t32 x2
8AX -> pteff05.t32 x4
first 20 occurrences:
GP_OPTIONS.pak entry 20 'OX -> po_keys_win1.t32
GP_OPTIONS.pak entry 22 'OX -> po_keys_win1.t32
GP_READY_ROOM.pak entry 132 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 141 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 211 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 219 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 233 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 234 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 264 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 265 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 967 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 999 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 1097 8AX -> pbbg.t32
GP_READY_ROOM.pak entry 1099 8AX -> pbbg.t32
GP_STAGE_CLEAR.pak entry 2 8AX -> pmbase.t32
GP_STAGE_CLEAR.pak entry 4 8AX -> pmbase.t32
GP_STAGE_CLEAR.pak entry 7 8AX -> pmbase.t32
GP_STAGE_CLEAR.pak entry 8 8AX -> pmbase.t32
GP_TITLE.pak entry 4 8AX -> pteff04.t32
GP_TITLE.pak entry 5 8AX -> pteff05.t32
# RUN 2, same command AFTER the fix. ratc::parse now prefers the `opt ` name, so
# the two readings must agree everywhere -- which is the check that the fix is
# complete rather than partial.
RATC children scanned : 18002
with an `opt ` block: 17942
scanned name AGREES : 17942
scanned name DIFFERS : 0
distinct disagreements (scanned -> opt), with counts:
first 20 occurrences:

View File

@@ -0,0 +1,26 @@
# Effect on the five port screens of naming RATC children from the `opt `
# block. 2026-08-29. `sylpheed-cli screen render --build N GP_TITLE.pak`,
# before vs after the ratc.rs change. The newly-resolved element is the
# full-resolution background (pteff04.t32 on the title, pteff05.t32 on the
# menus), which the old name `8AX` hid.
build screen mean brightness high-frequency detail
4 title 72.77 -> 72.81 8.722 -> 10.062 x1.15 pixels changed >2: 30.0%
5 main menu (JP) 42.73 -> 42.74 3.978 -> 5.051 x1.27 pixels changed >2: 20.0%
6 EXTRAS (JP) 44.08 -> 44.09 3.953 -> 5.025 x1.27 pixels changed >2: 19.4%
8 main menu 41.57 -> 41.58 3.830 -> 4.970 x1.30 pixels changed >2: 21.0%
9 EXTRAS 42.99 -> 43.00 3.867 -> 5.000 x1.29 pixels changed >2: 20.2%
# Brightness unmoved, detail up ~a quarter = the same artwork at twice the
# resolution replacing a 2x upscale. New CONTENT would move the mean.
# Covering check: the background is opaque and full-screen and paints 4th
# of 16 on the menu, so it could hide what follows. Standard deviation
# inside each element's resting rect (build 8), before vs after:
ptbtn01 sd 59.98 -> 60.03
ptbtn03 sd 63.40 -> 63.45
ptbtn05 sd 66.54 -> 66.58
ptframe1 sd 51.16 -> 51.20
ptmsg sd 56.11 -> 56.12
loop-area sd 50.99 -> 51.04
# Nothing is covered.

View File

@@ -0,0 +1,58 @@
# The 60 RATC children with no `opt ` block — probe output
```
$ cargo run -p sylpheed-formats --example ratc_optless_children -- $SYLPHEED_DISC/dat/*.pak
RATC children scanned : 18002
with NO accepted `opt ` block: 60
... of which are child #0 : 0
why, by cause:
TagBeyondWindow x60
every occurrence (name is what the FALLBACK scan returned):
GP_READY_ROOM.pak entry 26 child 1 TagBeyondWindow(213)
24 bytes before the magic: 00 00 00 07 00 00 00 28 00 00 00 08 00 00 00 28 00 00 00 09 00 00 00 28
GP_READY_ROOM.pak entry 26 child 2 TagBeyondWindow(60813)
24 bytes before the magic: 0a eb 61 00 08 eb 61 00 06 eb 61 00 05 eb 61 00 03 eb 61 00 01 eb 61 00
... (the remaining 54 rows are the same five bundles' children 1..10;
every distance is 213 + n*60600, i.e. the SAME tag)
```
## Reading bundle 26 directly — the ten are frames of one `.tan`
```
entry 26: 15 children, payload 706609 bytes
0 @0x00000444 T8aD size 1933 opt@-40 name='pbf15_energie_generator2.t32'
1 @0x00000bd1 T8aD size 60600 opt@-213 name='pb_f15_eg_anm.tan'
2 @0x0000f889 T8aD size 60600 opt@-60813 name='pb_f15_eg_anm.tan'
3 @0x0001e541 T8aD size 60600 opt@-121413 name='pb_f15_eg_anm.tan'
4 @0x0002d1f9 T8aD size 60600 opt@-182013 name='pb_f15_eg_anm.tan'
5 @0x0003beb1 T8aD size 60600 opt@-242613 name='pb_f15_eg_anm.tan'
6 @0x0004ab69 T8aD size 60600 opt@-303213 name='pb_f15_eg_anm.tan'
7 @0x00059821 T8aD size 60600 opt@-363813 name='pb_f15_eg_anm.tan'
8 @0x000684d9 T8aD size 60600 opt@-424413 name='pb_f15_eg_anm.tan'
9 @0x00077191 T8aD size 60600 opt@-485013 name='pb_f15_eg_anm.tan'
10 @0x00085e49 T8aD size 60632 opt@-545613 name='pb_f15_eg_anm.tan'
11 @0x00094b21 T8aD size 17523 opt@-32 name='pbf15_pd_inside2.t32'
12 @0x00098f94 T8aD size 75068 opt@-35 name='pbenergie_generator.t32'
13 @0x000ab4d0 T8aD size 4697 opt@-28 name='pbf15_eg_eff.t32'
14 @0x000ac729 RATC size 264 opt@-25 name='pb_s15_eg.rat'
```
## Disc-wide: every `opt ` name, by extension
```
`opt ` blocks disc-wide: 18718
by extension:
.t32 x14756
.rat x3311
.prm x367
.tbm x224
.sbo x54
.tan x6
.tan resources with >=1 child: 6
GP_READY_ROOM.pak pb_f15_eg_anm.tan frames= 10 sizes=[60600, 60632] x6 bundles
```

View File

@@ -0,0 +1,8 @@
# tools/re-capture/rest_rule_blast_radius.py over dat/GP_*.pak -- 2026-08-29
genuinely ambiguous elements (plateau-less, 2+ keyframes): 2305
the two rules AGREE on : 409 (17.7 %)
they DIFFER on : 1896 (82.3 %)
rule dwell : returns an INVISIBLE pose 1711 ( 74.2 %), zero-scale 195 ( 8.5 %)
rule last : returns an INVISIBLE pose 1618 ( 70.2 %), zero-scale 43 ( 1.9 %)

View File

@@ -0,0 +1,7 @@
# `sylpheed-cli screen info --geometry`, rows with a non-zero rotation (r=).
# GP_DIALOG builds 0 and 6; GP_DEBRIEFING_PILOTLOG build 10. 2026-08-28
4 pceff03.t32 247x281 38x38 NO 8: 630,348 100%,100% a=0 r=90 12: 550,273 100%,100% a=128 r=30 14: 510,235 100%,100% a=192 r=10 16: 490,217 100%,100% a=224 r=3 52: 470,198 100%,100% a=255 58: 470,198 100%,100% a=255 -: 470,198 100%,100% a=0
5 pceff04.t32 257x311 478x586 NO 8: 413,77 100%,100% a=0 r=90 12: 483,167 100%,100% a=128 r=30 14: 518,212 100%,100% a=192 r=10 16: 536,234 100%,100% a=224 r=3 52: 553,247 100%,100% a=255 58: 553,247 100%,100% a=255 -: 553,247 100%,100% a=0
1 pzeff02.t32 529x519 528x524 NO 197: 376,58 0%,0% a=0 217: 376,58 112%,112% a=255 r=43 232: 376,58 145%,145% a=64 r=61 237: 376,58 173%,173% a=32 r=75 -: 376,58 200%,200% a=0 r=90
5 pjeff24a.t32 382x140 610x622 NO -: 335,49 210%,210% a=53 r=90

View File

@@ -0,0 +1,32 @@
CONTROL GP_TITLE build 4 (nested rotations only): 0 top-level rotated (want 0)
CONTROL GP_DIALOG build 0: 2 top-level rotated (want 2)
[4] pceff03.t32 r = [90, 30, 10, 3, 0, 0, 0]
[5] pceff04.t32 r = [90, 30, 10, 3, 0, 0, 0]
top-level elements with a keyframe group, disc-wide: 15493
carrying a non-zero rotation: 2152 (13.89 %)
by archive:
1868 GP_READY_ROOM.pak
40 GP_DIALOG.pak
37 GP_MAIN_GAME_D2D.pak
37 GP_MAIN_GAME_E2D.pak
37 GP_MAIN_GAME_F2D.pak
37 GP_MAIN_GAME_I2D.pak
37 GP_MAIN_GAME_J2D.pak
37 GP_MAIN_GAME_S2D.pak
10 GP_GAMEOVER.pak
8 GP_DEBRIEFING_PILOTLOG.pak
4 GP_TITLE.pak
most common rotated element names:
444 pbb_destroyer.t32
402 pbr_destroyer.t32
276 pbr_fighter.t32
174 pbb_fighter.t32
168 pbb_aircraft.t32
84 pb_warp_eff.t32
48 pbtab.t32
35 gbp012i.t32
32 pzeff02.t32
30 gbp001i.t32

View File

@@ -0,0 +1,39 @@
SE cue waves located inside Static.slb by playing them.
Method: run Canary with --xma_param_probe=true, drive the main menu, and read the
per-stream XMA-PARAM lines. Each line carries the stream's first 32 bytes; those
bytes are then searched for in Static.slb's 8 353 472 readable bytes.
event packets bytes channels/rate offset in Static.slb
------------------------- ------- ------ -------------- --------------------
d-pad move (cursor) 4 8192 mono 48000 Hz 0x1ec0
B / back (cancel) 2 4096 mono 48000 Hz 0x0ec0
A / confirm (title->menu) 6 12288 mono 48000 Hz 0x5d6c0
Each head matched at exactly ONE offset. The first two are contiguous:
0x0ec0 + 4096 = 0x1ec0.
Simultaneously at the main menu, two STEREO 48 kHz streams were decoding:
1893 packets / 3 876 864 B and 1919 packets / 3 930 112 B. See
structures/bgm-two-stems.md -- this is the runtime observation that the two
stems of a music bank play at the same time.
DECODED 2026-08-28 with tools/re-capture/slb_extract_wave.py + ffmpeg's xma1:
event offset packets decoded rms peak envelope shape
---------------- -------- ------- --------- ----- ----- ------------------------
d-pad move 0x1ec0 4 0.533 s 2085 29813 sharp attack, monotonic decay
B / back 0x0ec0 2 0.344 s 2985 16973 attack, peak in frame 2
(pre-input cue) 0x5d6c0 6 1.016 s 4327 32767 peaks in frame 3, long decay
CONTROL: the same synthesized RIFF wrapper applied to BGM_001.slb's first wave
(offset 14336, stereo) decodes to 173.808875 s -- identical to the duration
obtained from that bank's own on-disc RIFF header. The wrapper is correct.
SECOND RUN 2026-08-28 -- independent boot, same probe:
* d-pad move and B reproduced with IDENTICAL head bytes and sizes (2 of 2 runs).
* The 12288-byte wave fires on the A that advances title -> main menu, together
with the two stereo BGM stems (the menu's music starting). Counting from the
title rather than the menu is what attributed it.
* Left and Right fired NO new stream -- no distinct invalid cue. (Dedup caveat:
a press replaying an already-heard wave would also log nothing.)

326
docs/re/data/se-ui-cues.txt Normal file
View File

@@ -0,0 +1,326 @@
Every SE cue on the disc, from tables.pak SOUNDS. 322 cues.
BANK_SE names the single bank they all live in: Static.slb
0 of the 322 has its own entry in FILES (5 135 bank paths).
1 SE_UI_START
2 SE_UI_CURSOR
3 SE_UI_DECIDE
4 SE_UI_CANSEL
5 SE_UI_IMPOSI
6 SE_UI_WAIT
7 SE_UI_NEXT
8 SE_UI_SUB_WIN_OPN
9 SE_UI_SUB_WIN_CLS
10 SE_UI_ALART_WIN
11 SE_UI_PAUSE
12 SE_UI_SPLASH_IN
13 SE_UI_SPLASH_OUT
14 SE_UI_LOAD_CMP
15 SE_TUT_BOX_CLEAR
16 SE_TUT_UNIT_IN
17 SE_UI_WEAPON_PLAN
18 SE_UI_WEAPON_CREATE
21 SE_HUD_MIS_LOCK
22 SE_HUD_LOCK_ALART
23 SE_HUD_WP_EMPTY
24 SE_HUD_WP_CHARGED
25 SE_HUD_DMG_ALARM
26 SE_HUD_OH_ALART
27 SE_HUD_OVER_HEAT
28 SE_HUD_CALL
29 SE_HUD_EMERG_CALL
30 SE_HUD_MES_END
31 SE_HUD_MES_SHAT
32 SE_HUD_MIS_SEARCH
33 SE_HUD_INVALID
34 SE_HUD_MW_CHG
35 SE_HUD_TRG_CHG
36 SE_HUD_MOD_CHG
37 SE_HUD_RADIO_MENU
38 SE_HUD_RADIO_ORDER
39 SE_HUD_RADIO_CANSEL
40 SE_HUD_TM_USE
41 SE_HUD_TM_INP
42 SE_HUD_TM_COM
43 SE_HUD_TM_OK
44 SE_HUD_TM_ERROR
45 SE_HUD_RADER_MARKER
46 SE_HUD_GUIDE
47 SE_HUD_SUB_TARGET
48 SE_HUD_SP_CHARGE
49 SE_HUD_SP_LV_UP
50 SE_BR_PASS
51 SE_BR_SHAT
52 SE_BR_PROGRESS
53 SE_BR_DECO_1
54 SE_BR_FRAME
55 SE_BR_DECO_2
56 SE_BR_HELP
57 SE_BR_DISP
58 SE_BR_COWIN
59 SE_BR_WAIT
60 SE_BR_NEXT
61 SE_BR_FRIENDRY
62 SE_BR_ENEMY
63 SE_BR_MESS
64 SE_BR_POINT
65 SE_BR_MARK
66 SE_BR_OUT
67 SE_BR_MAIN_OP
68 SE_BR_MAIN_CLS
69 SE_BR_JUMP_OUT
70 SE_BR_JUMP_IN
71 SE_BR_PROMETEUS
72 SE_BR_PLANET_BROKEN
73 SE_HUD_SP_RETURN
74 SE_HUD_SP_ATACK
75 SE_HUD_SP_SLOW
76 SE_HUD_AAM_SITE_OPN
77 SE_HUD_AAM_SITE_CLS
78 SE_HUD_ASM_SITE_OPN
79 SE_HUD_ASM_SITE_CLS
81 SE_UI_MISSION_START
82 SE_UI_MISSION_UPDATE
83 SE_UI_MISSION_END
101 SE_SW_nbe01_1
102 SE_SW_nbe02_1
103 SE_SW_nbe03_1
104 SE_SW_nla01_1
105 SE_SW_nvu01_1
106 SE_SW_nvu02_1
107 SE_SW_nvu03_1
108 SE_SW_nvu04_1
109 SE_SW_nsh01_1
110 SE_SW_nsh02_1
111 SE_SW_nsh03_1
112 SE_SW_nsp01_1
113 SE_SW_nsp02_1
114 SE_SW_nvu01_2
115 SE_SW_nvu01_3
116 SE_SW_nvu02_2
117 SE_SW_nvu02_3
118 SE_SW_nvu02_4
119 SE_SW_nvu03_2
120 SE_SW_nvu03_3
121 SE_SW_nvu04_2
122 SE_SW_nla02_1
123 SE_SW_SPIT_1
201 SE_MW_wvu01_1
202 SE_MW_wvu02_1
203 SE_MW_wvu03_1
204 SE_MW_wvu04_1
205 SE_MW_wbe01_1
206 SE_MW_wbe02_1
207 SE_MW_whb01_1
208 SE_MW_whb02_1
209 SE_MW_wla01_1
210 SE_MW_wla02_1
211 SE_MW_wla03_1
212 SE_MW_wla04_1
213 SE_MW_wAA01_1
214 SE_MW_wAA02_1
215 SE_MW_wAA03_1
216 SE_MW_wAA04_1
217 SE_MW_wAA04_2
218 SE_MW_wAA05_1
219 SE_MW_wAA07_1
220 SE_MW_wAA0x_2
221 SE_MW_wro01_1
222 SE_MW_wro02_1
223 SE_MW_wro03_1
224 SE_MW_wro04_1
225 SE_MW_wAS01_1
226 SE_MW_wAS02_1
227 SE_MW_wAS03_1
228 SE_MW_wAS04_1
229 SE_MW_wAS05_1
230 SE_MW_wAS0x_2
231 SE_MW_wAS0x_3
232 SE_MW_wto01_1
233 SE_MW_wto01_2
234 SE_MW_wto02_1
235 SE_MW_wto02_2
236 SE_MW_wto03_1
237 SE_MW_wto03_2
238 SE_MW_wto04_1
239 SE_MW_wto04_2
240 SE_MW_wbo01_1
241 SE_MW_wbo02_1
242 SE_MW_wbo03_1
243 SE_MW_wbo04_1
244 SE_MW_wbo05_1
245 SE_MW_wbo06_1
246 SE_MW_wbo07_1
247 SE_MW_wbo08_1
248 SE_MW_wbo09_1
249 SE_MW_wKE01_1
250 SE_MW_wKE02_1
251 SE_MW_wKE03_1
252 SE_MW_wgu01_1
253 SE_MW_wgu02_1
254 SE_MW_wgu03_1
255 SE_MW_wgu04_1
256 SE_MW_wgu05_1
257 SE_MW_wgu05_2
258 SE_MW_wgu06_1
259 SE_MW_wgu07_1
260 SE_MW_wsp01_1
261 SE_MW_wsp01_2
262 SE_MW_wsp01_3
263 SE_MW_L3GP_1
264 SE_MW_THOR_1
301 SE_SPE_mcm01_1
302 SE_SPE_mcm02_1
304 SE_SPE_mpr01_1
351 SE_SWP_BEAM_1
352 SE_SWP_RAILGUN_1
353 SE_SWP_PCANNON_1
354 SE_SWP_LASER_1
355 SE_SWP_BVULCAN_1
356 SE_SWP_VULCAN_1
357 SE_SWP_AAM_1
358 SE_SWP_ASM_1
359 SE_SWP_BEAM_2
401 SE_EXT_OVER_HEAT
402 SE_EXT_OH_RECOVER
403 SE_EXT_EMPTY
404 SE_EXT_EJECT_H
405 SE_EXT_EJECT_M
406 SE_EXT_EJECT_L
407 SE_EXT_EJECT_FAE
408 SE_EXT_MGT_OPN
409 SE_EXT_MGT_CLS
410 SE_EXT_PHOTON_CHG
411 SE_EXT_GRAVITY_CHG
412 SE_EXT_RECHARGE
413 SE_EXT_RECHG_CMP
414 SE_EXT_DOUBLER
415 SE_EXT_SPIT_CHG
501 SE_DS_ENGINE
502 SE_DS_POWER_UP
503 SE_DS_AFTER_BURNER
504 SE_DS_REVERSE
505 SE_DS_COCKPIT
506 SE_DS_POWER_DOWN
507 SE_DS_AB_END
508 SE_DS_SIDE
509 SE_DS_ROLL
511 SE_ENG_ERAN
512 SE_ENG_ERAN_Q
513 SE_ENG_VINDI
514 SE_ENG_DELTA
515 SE_ENG_UNE
516 SE_ENG_ADAN
517 SE_ENG_SUPPLY
518 SE_ENG_ATK_S
519 SE_ENG_ATK_L
551 SE_ENG_ACRO
552 SE_ENG_JUN
553 SE_ENG_KUCHI
554 SE_ENG_OOGATA
555 SE_ENG_YUSOU
556 SE_ENG_BS
557 SE_ENG_AAF
558 SE_ENG_ASF
559 SE_ENG_CARRIER
560 SE_ENG_MISSILE
561 SE_ENG_SIDE_S
562 SE_ENG_SIDE_L
601 SE_EXP_CRAFT
602 SE_EXP_EX_CRAFT
603 SE_EXP_ATK
604 SE_EXP_OUT
605 SE_EXP_BS_PART_L
606 SE_EXP_BS_PART_M
607 SE_EXP_BS_PART_S
608 SE_EXP_BS_BREAK_LL
609 SE_EXP_BS_BREAK_L
610 SE_EXP_BS_BREAK_M
611 SE_EXP_BS_BREAK_S
612 SE_EXP_BS_BREAK_SS
613 SE_EXP_BS_OMEN
614 SE_EXP_BS_BOMB
615 SE_EXP_MISSILE
616 SE_EXP_TCAF_CRUISER
617 SE_EXP_MINE
651 SE_IMP_BM_DMG
652 SE_IMP_BM
653 SE_IMP_LS_DMG
654 SE_IMP_LS
655 SE_IMP_VULCAN
656 SE_IMP_SHOTGUN
657 SE_IMP_FIRE
658 SE_IMP_MISSILE
659 SE_IMP_ROCKET
660 SE_IMP_SMOKE
661 SE_IMP_TORPEDO
662 SE_IMP_BOMB
663 SE_IMP_KINETIC
681 SE_IMP_BULLET
683 SE_IMP_BS_BEAM
684 SE_IMP_BS_RGUN
685 SE_IMP_BS_PCANNON
686 SE_IMP_BS_LASER
687 SE_IMP_BS_BVULCAN
688 SE_IMP_BS_VULCAN
689 SE_IMP_BS_AAM
690 SE_IMP_BS_ASM
691 SE_IMP_GRV
701 SE_SHIELD_DOWN
702 SE_SHIELD_RECHARGE
703 SE_SHIELD_DOWN_BS
751 SE_BOSS_CRY
752 SE_BOSS_CLOSE
753 SE_BOSS_OPEN
754 SE_BOSS_STUMBLE
755 SE_BOSS_MOMENTS
756 SE_BOSS_ATK_A
758 SE_BOSS_ATK_C
759 SE_BOSS_LASER
760 SE_BOSS_HOMING
761 SE_BOSS_CHARGE
762 SE_BOSS_CORE_CHARGE
763 SE_BOSS_SPREAD
764 SE_CORE_COLLAPSE
781 SE_PROME_AMB_1
782 SE_PROME_AMB_2
783 SE_PROME_AMB_3
784 SE_PROME_FIRE
785 SE_ENV_MINE
786 SE_HNG_ARM_MV
787 SE_HNG_MOUNT
788 SE_HNG_UN_MOUNT
789 SE_HNG_ARM_UP
790 SE_HNG_ARM_DWN
791 SE_HNG_MOUNT_2
792 SE_HNG_UN_MOUNT_2
793 SE_HNG_MOUNT_3
794 SE_HNG_UN_MOUNT_3
795 SE_HNG_MOUNT_4
796 SE_HNG_UN_MOUNT_4
807 SE_ETC_JMP_OP
808 SE_ETC_JMP_IN
809 SE_ETC_JMP_OUT
810 SE_ENV_BS_M
811 SE_ENV_BS_S
812 SE_ENV_BS_L
813 SE_ENV_BS_LL
814 SE_ENV_CARRIER
815 SE_ETC_HP_UNE
816 SE_ETC_HP_ADAN
817 SE_ETC_HP_ACRO
818 SE_ENV_WIND
819 SE_ENV_WIND_TURN
820 SE_ETC_TURRET_L
821 SE_ETC_TURRET_M
822 SE_ETC_TURRET_S
823 SE_PASS_DS
824 SE_PASS_ADAN
825 SE_PASS_ERAN
826 SE_PASS_PLUS
827 SE_PASS_TYPEQ
828 SE_PASS_VINDI
829 SE_ETC_COLLISION
830 SE_ETC_SHIELD_DMG
900 SE_LFE_BOMB
901 SE_LFE_ENG

View File

@@ -0,0 +1,17 @@
# se_wave_riff — the three menu cues, decoded end to end
$ cargo run -p sylpheed-formats --example se_wave_dump -- /tmp/se
/tmp/se/move.riff: 8252 bytes (4 packets at 0x1ec0)
/tmp/se/back.riff: 4156 bytes (2 packets at 0xec0)
/tmp/se/confirm.riff: 12348 bytes (6 packets at 0x5d6c0)
refusal path ok: Static.slb: 1048576 packets at 0x1ec0 need 2147483648 bytes, but the bank declares only 8970240 bytes
$ ffmpeg -i <cue>.riff <cue>.wav # then measure the PCM
move 48000 Hz mono 0.533 s rms 2084.7 peak 29813 non-quiet 47.5%
back 48000 Hz mono 0.344 s rms 2984.7 peak 16973 non-quiet 95.1%
confirm 48000 Hz mono 1.016 s rms 4327.0 peak 32767 non-quiet 92.5%
Non-silent, plausible envelopes, durations consistent with a UI blip.
The refusal path is exercised in the same run: an impossible packet count is
rejected rather than returning a short stream.

View File

@@ -0,0 +1,22 @@
# .xpr files whose `mesh info` decode exceeds 25 s (debug build), of 166 scanned
# 2026-08-29. Stage_S02 measured at 144 s to completion, rc=0 -- heavy, not hung.
Hangar.xpr
Stage_S02.xpr
Stage_S03.xpr
Stage_S04.xpr
Stage_S06.xpr
Stage_S07.xpr
Stage_S08.xpr
Stage_S09.xpr
Stage_S11.xpr
Stage_S12.xpr
Stage_S13.xpr
Stage_S15.xpr
Stage_S24.xpr
Stage_S25.xpr
Stage_S26.xpr
Stage_S27.xpr
Stage_S28.xpr
Stage_S29.xpr
ptc_pack.xpr

View File

@@ -0,0 +1,11 @@
# Measured splash phase timeline -- from docs/re/captures/ui-timing/splash-build-draws.log
# 2026-08-29. Frames are VdSwap counts; 1 frame = 1/30 s (2 units/frame, 1 unit = 1/60 s).
#
# phase frames n seconds start (rel. capture)
publisher: SQUARE ENIX logo 1-90 90 3.00+ 0.00 <- floor: already at a=255 on f1
(gap, nothing drawn) 91-93 3 0.10 3.00
developer: both glows 94-115 22 0.73 3.10
developer: both logos 116-211 96 3.20 3.83
#
# palogo_anima / palogo_anima_eff: 0 draws in the whole 214-frame window.
# The glow->logo switch is a single frame boundary (f115 -> f116), no overlap.

View File

@@ -0,0 +1,20 @@
CONTROL — the eff glow's fade-in must be linear at a constant step
alphas [34, 68, 102, 136, 170, 204, 238] steps [34, 34, 34, 34, 34, 34]
=> 2.000 units/frame; t(f) = 2*f - 171
check: declared hold ends t=45 -> predicted frame 108.0; observed last full-alpha frame 107
LOGO — palogo_gamearts, checked against that calibration
observed: first drawn frame 116 at alpha 255; full alpha through 198; fade-out 199..211
declared a= 0 at t= 15 -> frame 93.0 observed alpha NOT DRAWN
declared a= 0 at t= 30 -> frame 100.5 observed alpha NOT DRAWN
declared a=255 at t=190 -> frame 180.5 observed alpha 255
declared a=255 at t=194 -> frame 182.5 observed alpha 255
declared a=232 at t=206 -> frame 188.5 observed alpha 255
declared a= 32 at t=210 -> frame 190.5 observed alpha 255
SHAPE, independent of any calibration:
declared fade-out spans t=194..210 (16 units); of that,
12/16 units drop only 23/255 of the alpha (a near-flat leg),
4/16 units drop 200/255 (a cliff).
captured fade-out: [255, 254, 243, 237, 215, 181, 148, 131, 98, 65, 48, 23, 15, 7]
per-frame drops: [1, 11, 6, 22, 34, 33, 17, 33, 33, 17, 25, 8, 8] -> no near-flat leg

View File

@@ -0,0 +1,97 @@
GamePart_Title has TWO nested state fields.
OUTER -- sub_821C7850, the part's dispatcher
821c786c lwz r11, 16(r30) ; only runs when this+16 == 3
821c7870 cmpwi cr6, r11, 3
821c7874 bne cr6, <exit>
821c787c lwz r11, 132(r30) ; phase = this+0x84
821c7880 cmplwi cr6, r11, 0x4 ; five phases, 0..4
821c7888 lis/addi r12, 0x821C78A0 ; jump table
821c789c bctr
phase 0 -> sub_821C5690 (the splash -- independently identified as the
splash mechanics in the iterate3E notes)
phase 1 -> inline block at 0x821c790c
phase 2 -> sub_821C5818
phase 3 -> sub_821C5EC0
phase 4 -> sub_821C6458 (the title/menu machine, below)
All four call sites forward the SAME three arguments: (this, r29, r28),
where r28 is this function's own third argument -- an EVENT CODE passed
through unchanged from ITS caller.
INNER -- sub_821C6458, the title/menu machine (phase 4 only)
DISPATCH
821c6474 lwz r11, 136(r30) ; state = this+0x88
821c6478 cmplwi cr6, r11, 0x9 ; 10 states, 0..9
821c647c bgt cr6, 0x821C75B8 ; out of range -> default
821c6480 lis r12, 0x821C
821c6484 addi r12, r12, 25752 ; jump table at 0x821C6498
821c6488 slwi r0, r11, 2
821c648c lwzx r0, r12, r0
821c6490 mtctr r0
821c6494 bctr
NOTE: a disassembler decodes 0x821C6498..0x821C64BC as `lwz r16, N(r28)`
instructions. They are the jump TABLE's ten words, not code.
STATES
case 0 -> 0x821c64c0 installs "TITLE_SCREEN"
case 1 -> 0x821c65d0
case 2 -> 0x821c66e4 installs "TITLE_MENU"
case 3 -> 0x821c6b18
case 4 -> 0x821c6b5c nested switch, table at 0x821c6b7c (live: 0,3,5,8)
case 5 -> 0x821c6fd4
case 6 -> 0x821c7028
case 7 -> 0x821c72dc
case 8 -> 0x821c733c installs "LOADING"
case 9 -> 0x821c7558
TRANSITIONS -- every `li rX,N ; stw rX,136(r30)` in the function
from to at
0 1 0x821c6598
0 2 0x821c65c8
1 2 0x821c66a4
1 2 0x821c66dc
2 4 0x821c6afc
3 4 0x821c6b54
4 0 0x821c6e00
4 5 0x821c6e70
4 8 0x821c6ed0
4 8 0x821c6f20
4 8 0x821c6f7c
4 8 0x821c6fcc
5 6 0x821c7020
6 7 0x821c725c
6 9 0x821c72a8
6 2 0x821c72d4
7 9 0x821c7334
8 2 0x821c7548
(state 9 stores nothing -- terminal within this function)
THE CONDITION ON STATE 4's EDGES -- decoded
sub_821C6458(this, ?, r5) takes an EVENT CODE in its third argument.
State 4 -- the input-waiting state -- dispatches on it:
821c6b5c cmplwi cr6, r27, 0x19 ; 26 events, 0..25
821c6b60 bgt cr6, <default>
821c6b68 addi r12, r12, 27516 ; table at 0x821c6b7c
821c6b78 bctr
Only 6 of the 26 are handled; the other 20 fall through with no state change.
event 0 -> block 0x821c6be4 -> state 0 (TITLE_SCREEN -- back to the title)
event 3 -> block 0x821c6e78 -> state 8 (LOADING)
event 5 -> block 0x821c6f84 -> state 8 (LOADING)
event 8 -> block 0x821c6f34 -> state 8 (LOADING)
event 10 -> block 0x821c6e28 -> state 5
event 25 -> block 0x821c6ed8 -> state 8 (LOADING)
WHAT IS NOT DECODED
* what the event NUMBERS mean -- they are forwarded unchanged from
sub_821C7850's caller, so the vocabulary is defined at least one level up -- button id, menu-item id or message id;
* the conditions on edges out of states other than 4;
* what states 1, 3, 5, 6, 7 and 9 do (they install no named screen);
* state 3 is never a destination here, so something outside this function
sets it.

View File

@@ -0,0 +1,24 @@
# tools/re-capture/zero_scale_census.py -- 2026-08-28
elements with a keyframe group: 15493
at least one keyframe at scale 0: 2166
EVERY keyframe at scale 0 (would vanish): 0
zero is a transient the element grows out of: 1762
rest poses with scale 0%%: 195
... of which alpha > 0, so blit paints a 1-pixel sliver: 126
... alpha == 0, harmless: 69
by archive:
126 GP_READY_ROOM.pak
examples (pak, entry, element idx, name, sx, sy, alpha):
('GP_READY_ROOM.pak', 0, 2, 'pbacropolis.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 0, 6, 'pbFriendly.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 0, 10, 'pbFriendly.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 0, 14, 'pbFriendly.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 7, 2, 'pbacropolis.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 7, 6, 'pbFriendly.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 7, 10, 'pbFriendly.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 7, 14, 'pbFriendly.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 74, 2, 'pbr_enemy.t32', 0, 100, 255)
('GP_READY_ROOM.pak', 76, 2, 'pbr_enemy.t32', 0, 100, 255)

50
docs/re/doc-link-audit.md Normal file
View File

@@ -0,0 +1,50 @@
# ✅ Do the docs' cited artifacts exist? — 11 broken links repaired
**Status:** ✅ checked mechanically and fixed, with the count verified both ways.
The brief's rule is *"commit the reference data beside the finding, so the port
can be built without a disc in the loop"*. An answer whose evidence is not
reachable cannot be used. Nothing had ever checked that.
[`tools/re-capture/doc_link_check.py`](../../tools/re-capture/doc_link_check.py)
walks every markdown file under `docs/`, resolves each relative link, and reports
targets that do not exist — and, separately, targets that exist but are **empty**,
which is the sneakier failure since a zero-byte file looks fine in any listing.
| | before | after |
|---|---|---|
| links resolving | 1 038 | **1 049** |
| missing targets | **16** | 5 |
| empty targets | 0 | 0 |
**+11 resolving, 11 missing — the two numbers pair exactly**, which is the check
that the repair did what it claimed and nothing else.
## 🔴 Two of them were the evidence for the UI decode itself
`structures/ui-rat-layout.md` is the layout decode the port is built on. Its two
figures — the ones backing *"the tutorial PAUSE menu rebuilds pixel-accurately
from its sprites"* and *"the same method reproduces the main menu"* — were
written as `captures/ui-layout/…` from a file in `structures/`, one directory
too shallow. **The headline evidence for the decode was unreachable from its own
document.**
## What was wrong, and what still is
Eleven links had the **wrong relative depth** while their targets existed — a
missing or surplus `../`, or a missing `structures/`. Those are repaired; each
was rewritten only when exactly one candidate path resolved, so nothing was
guessed.
**Five remain genuinely absent** and are left alone rather than invented:
| doc | target |
|---|---|
| `autopilot-knowledge-sources.md`, `entities-live-roster.md` | `../../MEMORY.md` — outside the repo |
| `challenge-mission-gate.md` | a header in the separate `xenia-canary-native` tree |
| `stage-drift-is-navigation-not-save.md` | `structures/weapon-datasheet-runtime.md` — never written |
| `mission-freeze-resume-spin.md` | `canary-build-verified-env-confound.md` — never written |
None is port-relevant: they are mission, entity and emulator-side documents. Two
name documents that do not exist, which is a different problem from a bad path
and is not something to paper over with a link fix.

View File

@@ -79,7 +79,7 @@ looks stuck on the main menu while it is in fact three screens further on.
## Still open
* The **world-unit measurement** ([collisionset](collisionset.md)) — positions
* The **world-unit measurement** ([collisionset](structures/collisionset.md)) — positions
in world units against the HUD's own distance readout. This run reached the
mission but the flight HUD was not yet up (green 0.03 %, vs 1.31.5 % in
flight), so no distance readout was available to compare against.

View File

@@ -0,0 +1,63 @@
# ✅ The five screens, end to end — what each one is actually worth
**Status:** ✅ measured, 2026-08-29. One page, because the answers for these five
screens are otherwise spread across a dozen documents and none of them says how
good the result *is*.
Rendered with `sylpheed-cli screen render` (adding `--all --black` for the two
splash halves) and correlated against the committed live captures.
| screen | build | elements drawn | corr vs capture | alignment |
|---|---|---|---|---|
| title | 4 | 15 / 24 | **+0.9500** | dy=0 dx=0 |
| main menu | 5 | 11 / 16 | **+0.9460** | dy=0 dx=0 |
| `EXTRAS` | 6 | 13 / 18 | **+0.9440** | dy=0 dx=0 |
| publisher splash | 10 | 2 / 3 | **+0.9600** | dy=0 dx=0 |
| developer splash | 11 | 6 / 7 | **+0.9643** | dy=0 dx=0 |
Every one aligns at **exactly zero offset** — searched ±2 px in both axes — so
placement and scale are right and the residual is tone and detail, not geometry.
## Every omission is accounted for
The counts are not slack; each undrawn element has a reason already documented:
| screen | not drawn | why |
|---|---|---|
| title | `ptlogo1`×2, `ptlogo2`×2 | `kind & 0x4` **ghost instances** — the draw capture shows one quad per wordmark |
| | `pteff00.prm`, `pteff02.prm` | `.prm` primitives, off without `--primitives` |
| | `ptloop01.rat`, `ptloop02.rat` | `loop*` animations, off without `--animated`; at rest both sit off-screen |
| | `pteff04.t32` | the **`8AX` name mismatch** — the art *is* on screen, drawn via `ptbase2` ([8AX](structures/ui-8ax-fullres-background.md)) |
| main menu, `EXTRAS` | 2 `.prm` + 2 `loop*` + `pteff05.t32` | same three reasons |
| both splashes | `palogo_eff0.prm` | `.prm` primitive |
4 + 2 + 2 + 1 = 9 on the title; 2 + 2 + 1 = 5 on each menu; 1 on each splash.
**Nothing is unexplained.**
## What the residual is, per screen
Ranked by what a port would notice:
1. **Tone.** The capture is darker than the composite by a gamma of ≈1.341.49
([tone curve](structures/ui-render-tone-curve.md)) — the game's own display
ramp, not an emulator artefact. This is the largest single contributor to the
0.940.96 rather than higher.
2. **`8AX` resolution.** The background is drawn from the 640×360 `ptbase`
upscaled 2×, where the game uses the native 1280×720 `8AX`
([measured](structures/ui-8ax-fullres-background.md), two screens at 68 % of
ceiling).
3. **Paint-order ties on `EXTRAS` only.** One drawable overlapping tie
(`ptframe3`×`ptframe4`), and the capture is consistent with our order
([check](structures/ui-paint-order-derived-check.md)). The other four screens
use orders measured from the game.
4. **Rotation is decoded but not rendered** — and does not affect any of these
five at rest ([rotation](structures/ui-keyframe-rotation.md)).
## Reach
* These are **static composites at the resting pose** against **single frames**.
Nothing here says the animation is right; Q1's interpolation law is settled but
a multi-keyframe group's absolute timing is not
([time unit](ui-keyframe-time-unit.md)).
* Correlation on a whole frame is dominated by large areas. It is a sanity figure,
not a per-element check — the per-element work is in the documents linked above.

263
docs/re/menu-audio-cues.md Normal file
View File

@@ -0,0 +1,263 @@
# Menu audio — the event vocabulary is on the disc; the binding is not
**Status:**`CONFIRMED` and **decoded** for the cue vocabulary and the bank
that holds it. 🟡 the cue↔event binding is a **name match**, not a measurement.
❔ two negatives with their reach: no cue names a screen's music, and the audio
for an individual SE cue cannot yet be extracted.
Answers most of [MISSION Q8](../port/MISSION.md).
## ✅ The UI cue vocabulary — decoded
`dat/tables.pak`'s sound table has a `SOUNDS` record of 5 798 cues. 322 are `SE_*`
and the **low block is the user-interface vocabulary**, named by the game's
authors after the *events*, not after the sounds:
| id | cue | id | cue |
|---|---|---|---|
| 1 | `SE_UI_START` | 9 | `SE_UI_SUB_WIN_CLS` |
| **2** | **`SE_UI_CURSOR`** | 10 | `SE_UI_ALART_WIN` |
| **3** | **`SE_UI_DECIDE`** | 11 | `SE_UI_PAUSE` |
| **4** | **`SE_UI_CANSEL`** *(sic)* | 12 | `SE_UI_SPLASH_IN` |
| **5** | **`SE_UI_IMPOSI`** | 13 | `SE_UI_SPLASH_OUT` |
| 6 | `SE_UI_WAIT` | 14 | `SE_UI_LOAD_CMP` |
| 7 | `SE_UI_NEXT` | 17/18 | `SE_UI_WEAPON_PLAN` / `_CREATE` |
| 8 | `SE_UI_SUB_WIN_OPN` | 8183 | `SE_UI_MISSION_START` / `_UPDATE` / `_END` |
Full 322-cue list in [`data/se-ui-cues.txt`](data/se-ui-cues.txt). The remaining
blocks are gameplay: `SE_HUD_*` (2183), `SE_BR_*` briefing (5072), `SE_SW_*` /
`SE_MW_*` weapons, `SE_DS_*` engine, `SE_EXP_*` explosions, `SE_SHIELD_*`.
## ✅ They all live in one bank — decoded
The sound table's `BANK_SE` record is a **single field**: `Static.slb`. And the
disc-wide check agrees — **0 of the 322 `SE_*` cues has an entry in `FILES`**, the
5 135-path list where every voice, briefing and BGM bank is named. SE is the one
family that is not one-cue-one-file.
`SETTINGS` names the XACT project too: `PATH` = `game:\dat\sound.pak+`,
`PARAM` = `Pj_Silph.xgs`.
## 🟡 The binding: a name match, and I am calling it that
MISSION asks which cue fires on **move / confirm / back / error**. Read off the
names:
| event | cue |
|---|---|
| cursor moves | `SE_UI_CURSOR` (2) |
| Ⓐ confirm | `SE_UI_DECIDE` (3) |
| Ⓑ back | `SE_UI_CANSEL` (4) |
| invalid / error | `SE_UI_IMPOSI` (5) |
| submenu opens / closes | `SE_UI_SUB_WIN_OPN` / `_CLS` (8/9) |
| the splash | `SE_UI_SPLASH_IN` / `_OUT` (12/13) |
**Nobody has watched the game emit cue 2 on a d-pad press.** This is inference
from identifiers, and it belongs in the same box as Q4's GamePart ids — with one
honest difference worth stating: these are the *authors' own event names*, chosen
to describe when the sound plays, not asset labels I am interpreting. It is a
strong name match. It is still a name match, and the port is authoring it.
Measuring it needs the guest's cue-play call observed with its argument — the
same instrumentation Q4's residual wants, and not something this container can do
today (it runs `--mute=true` against an SDL dummy device, so there is no audio
path to watch either).
## ✅ Which BGM per screen — the TABLE cannot say, but the CODE can
All 32 BGM cues are named `BGM_001``BGM_109`, and nothing in `SOUNDS`, `FILES` or
the bank headers names a screen. That negative stands **for the tables**.
It does not stand for the executable. `GamePart_Title`'s phase handler
`sub_821C5580` contains `li r5, 1103` feeding a sound call — **cue 1103 =
`BGM_103`** — and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 bytes)
are byte-for-byte the two streams the XMA probe saw decoding at the main menu.
Three routes, one answer; see
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md).
So the per-screen BGM binding is recoverable **per screen, from the code that
plays it** — not from any table.
## ❔ And a new negative: an individual SE's audio is not extractable yet
`Static.slb` is **8 353 472 readable bytes** (declared 8 970 240 — exactly the
616 768 over-declaration the corpus already records), and it contains
**0 `RIFF`, 0 `seek`, 0 `WAVE`** — scanned over the whole buffer, so this is not
an offset problem. Every other bank on the disc is delimited by `seek` magic at
`data_at + declared_size` (7 620/7 620 in
[`slb-data-offset.md`](structures/slb-data-offset.md)); `Static.slb` is outside
that population. So the route that extracts every voice line and every BGM does
**not** give you `SE_UI_CURSOR`'s audio: the cue is named, the bank is named, and
the wave inside the bank is not locatable by any boundary marker.
**The named next step is `Pj_Silph.xgs`** — the XACT project file `SETTINGS`
points at, which in XACT is exactly where cue→wave-index lives. It **is** in
`sound.pak`: `name_hash("Pj_Silph.xgs")` resolves to TOC index 9454. ⚠️ Its entry
is 533 bytes at a 2048-aligned offset and carries no `XGSF` magic — and its region
has phase 1728, so those 533 bytes are very likely the *previous* bank's tail
rather than its own (the leading-region effect, [`slb-data-offset.md`]
(structures/slb-data-offset.md)). Locating its real bytes is the first job, not
parsing XACT.
## For the port
* the cue **names and ids** are disc facts — use them as the event vocabulary;
* which event fires which cue is **authored from the table above**;
* which BGM plays on which screen is **authored** — nothing on the disc says;
* and the UI **sound effects cannot be exported yet**. That is a gap in the
assets, not in the naming.
## ❔ `Pj_Silph.xgs` is a dead end — retracting the lead, with the reach
The section above named `Pj_Silph.xgs` as "the named next step", on the reasoning
that in XACT the cue→wave index lives in the project file. **That route is dead.**
| check | result |
|---|---|
| the entry's own bytes | 533 bytes, high entropy, **no `XGSF` magic** |
| ±8 KB around the entry in the flat concatenation | no `XGSF`, `SDBK`, `WBND`, `RIFF` or `seek` |
| **all of `sound.pak` — 1.08 GB across `.p00``.p04`** | **0 × `XGSF`, 0 × `SDBK`, 0 × `WBND`** |
| the executable | **0** occurrences of `XGSF`/`XACT`; no string matching `xact`, `.xgs`, `wavebank` or `soundbank` |
**Control, run first:** the same magic scan over `BGM_001.slb`'s neighbourhood
finds `RIFF` exactly where the bank structure says it should be. The scan works;
the magic genuinely is not there.
So the `.xgs` / `.slb` names are **inherited from the authoring tool, not from the
shipped format** — nothing on this disc is a parseable XACT container. There is no
XACT project file to read, and writing an XACT parser would have been wasted work.
### Where that leaves the SE audio: undecodable, with reach
Locating an individual SE cue's wave inside `Static.slb` is **not possible from
anything found on the disc**. Looked in:
* `Static.slb` itself — 8 353 472 readable bytes, **0 `RIFF` / 0 `seek` / 0 `WAVE`**,
where every one of the other 7 620 banks is delimited by `seek` magic at
`data_at + declared_size` ([`structures/slb-data-offset.md`](structures/slb-data-offset.md));
* the XACT project file — absent, as above;
* the sound table's five records — `SOUNDS` gives cue→id, `FILES` gives bank
paths, `BANK_SE` gives the one bank name, `SETTINGS` gives a path and the
(absent) project file, `STAGES` is empty. **No record carries an offset.**
**What does corroborate the binding:** `Static.slb` is 8 353 472 bytes, and at the
bitrates the disc uses elsewhere (25 69731 000 B/s) that is **269325 seconds of
audio — 0.84 to 1.01 s per cue over 322 cues.** Exactly the shape of a bank of
short UI/HUD/weapon effects, which is what `BANK_SE` says it is. The bank is the
right bank; only the index into it is missing.
**If it is ever picked up**, the route is the running game, not the disc: watch
the guest hand a wave offset/length to the decoder when a cue fires. That is the
same instrumentation the cue↔event binding needs, and the same one this container
cannot run.
## ✅ Retracting "cannot be extracted" — the waves are locatable, by playing them
The section above concluded that an individual SE cue's audio "cannot be
extracted". **That was too strong, and the tool to do it was already in this
build.** Canary here carries a cvar `xma_param_probe`, added for exactly this
purpose — its own comment says it logs each XMA stream's parameters and head
bytes "so raw sound.pak entries can be matched to real decode params".
Run with `--xma_param_probe=true`, drive the menu, and each newly-decoded stream
prints one line with its packet count, channel count, sample rate and **first 32
bytes**. Searching those bytes in `Static.slb` finds the wave.
| event | packets | bytes | format | offset in `Static.slb` |
|---|---|---|---|---|
| **d-pad move** (cursor) | 4 | 8 192 | mono 48 kHz | **`0x1ec0`** |
| **Ⓑ back** (cancel) | 2 | 4 096 | mono 48 kHz | **`0x0ec0`** |
| **Ⓐ confirm** (title → main menu) | 6 | 12 288 | mono 48 kHz | **`0x5d6c0`** |
Each head matched at **exactly one** offset. Reference data in
[`data/se-cue-runtime-offsets.txt`](data/se-cue-runtime-offsets.txt).
**Structurally this also cracks the bank's layout.** The first two are
*contiguous*`0x0ec0 + 4096 = 0x1ec0` — so `Static.slb` is a packed run of
XMA waves, each a whole number of 2 048-byte packets, with no delimiter between
them. That is why the `seek`-magic scan found nothing: there is nothing to find.
A wave is defined only by (offset, packet count), and those live in the game's
own tables, not in the bank.
⚠️ **The order is not cue-id order.** `Ⓑ`'s wave precedes the cursor's in the
file, while the name match puts `SE_UI_CURSOR` at id 2 and `SE_UI_CANSEL` at 4.
So the index cannot be derived by counting; it has to be observed per cue.
### What is now measured, and what is still a name match
***measured** — a d-pad move and a Ⓑ press each play a *specific, different*
wave, and both waves are located in `Static.slb`. The port can have the audio.
* 🟡 **still a name match** — that the cursor's wave *is the cue called*
`SE_UI_CURSOR`. The event→wave binding is measured; the event→*name* binding is
still read off the authors' identifiers.
***decoded to PCM** — see below. The cues play.
### ✅ The waves decode — artifact, not assertion
[`tools/re-capture/slb_extract_wave.py`](../../tools/re-capture/slb_extract_wave.py)
wraps a `(bank, offset, packet count, channels, rate)` slice in a synthesized
XMA1 `RIFF` for ffmpeg's `xma1` decoder:
```
slb_extract_wave.py Static.slb 0x1ec0 4 # -> Static_0x1ec0.riff -> 0.533 s
```
| event | offset | packets | decodes to | RMS | peak | envelope |
|---|---|---|---|---|---|---|
| d-pad move | `0x1ec0` | 4 | **0.533 s** | 2 085 | 29 813 | sharp attack, monotonic decay |
| Ⓑ back | `0x0ec0` | 2 | **0.344 s** | 2 985 | 16 973 | attack, peak in frame 2 |
| **Ⓐ confirm** | `0x5d6c0` | 6 | **1.016 s** | 4 327 | 32 767 | peaks in frame 3, long decay |
All three are mono 48 kHz, audible from sample 0, and have the percussive
attack-and-decay shape of UI blips — not silence, not noise.
**Control, run first:** the *same* synthesized wrapper applied to `BGM_001.slb`'s
first wave decodes to **173.808875 s** — identical to the duration obtained from
that bank's own on-disc `RIFF` header in
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md). The wrapper is not
approximately right; it reproduces a known-good decode exactly.
The bitrates are consistent too: 8 192 B / 0.533 s ≈ 15.4 kB/s, 4 096 / 0.344 ≈
11.9 kB/s, 12 288 / 1.016 ≈ 12.1 kB/s — mono at roughly half the stereo BGM rate.
🟡 **The decoded audio is deliberately not committed.** Three commands regenerate
it from the disc, and the corpus keeps measurements and tooling rather than
extracted game audio. The offsets, packet counts and this tool are the
deliverable.
## The three cues the milestone needs are bound — 2026-08-28, second run
A second, independent boot repeated the probe and pinned the one that was still
unattributed.
| event | wave | packets / bytes | runs agreeing |
|---|---|---|---|
| **d-pad move** | `Static.slb+0x1ec0` | 4 / 8 192 | **2 of 2** — identical head bytes |
| **Ⓑ back** | `Static.slb+0x0ec0` | 2 / 4 096 | **2 of 2** — identical head bytes |
| **Ⓐ confirm** | `Static.slb+0x5d6c0` | 6 / 12 288 | newly bound this run |
The `0x5d6c0` wave was previously logged as "played before any input", because
the first run only started counting at the main menu. Counting from the **title**
instead shows it appears exactly on the Ⓐ that advances title → main menu, along
with the two stereo BGM stems (the menu's music starting).
So move / confirm / back — everything the five-screen milestone needs — are
located and decodable.
🟡 **What that Ⓐ measurement does not separate.** The same press both *confirms*
and *opens a screen*, so this wave could be the cue the vocabulary calls
`SE_UI_DECIDE` or the one it calls `SE_UI_SUB_WIN_OPN`. The port needs *a* sound
on confirm and this is the one the game plays there; which name it carries is not
established.
## ❔ ⬅ and ➡ play nothing distinct
[`menu-navigation-semantics.md`](menu-navigation-semantics.md) measured that
left/right do not move the cursor. They also **fire no new XMA stream** — so
there is no distinct "invalid" cue on the main menu, and `SE_UI_IMPOSI` was not
observed. The port should leave left/right silent.
⚠️ **Reach of that negative:** the probe dedups on head bytes, so a press that
replayed an *already-heard* wave would log nothing. At that point in the run the
cursor, confirm and BGM waves had all been heard. So this excludes a **distinct**
invalid cue; it cannot exclude left/right quietly replaying one of those three.

View File

@@ -0,0 +1,136 @@
# The title menu — how it moves, and where each button goes
**Status:**`CONFIRMED` (**measured**, by driving the running game) for the
movement rules and for four of the five main-menu destinations. 🟡 the GamePart
*id* behind each destination is a **name match onto the decoded id table**, not a
measurement. ❔ `NEW GAME` deliberately untested.
Answers [MISSION Q5](../port/MISSION.md) and most of Q4. Nothing here is on the
disc in any form found so far — the port is **authoring** these rules from this
page, not transcribing a field.
## Q5 — movement
Two boots via `tools/re-capture/boot_menu.sh`, cursor read off the focus ring
with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py).
| | behaviour | evidence |
|---|---|---|
| **initial focus, main menu** | **`TUTORIAL`** — the *middle* item, not the top | 2/2 boots, the first frame after the menu appears |
| **initial focus, `EXTRAS`** | `MISSION SELECT` — the top item | [`extras-wrap.png`](captures/menu-nav/extras-wrap.png) |
| **up / down** | one item per press, no auto-repeat at the durations tried | |
| **wrap at the top** | ⬆ from the first item goes to the **last** | [`wrap-montage.png`](captures/menu-nav/wrap-montage.png), panels 1→2 |
| **wrap at the bottom** | ⬇ from the last item goes to the **first** | same, panels 3→4, and 4 presses from `EXTRAS` landing on `OPTIONS` — i.e. wrapping — is what makes the count come out |
| **left / right** | **nothing**, on the main menu | cursor unmoved across one ⬅ and one ➡ |
| **Ⓑ on a submenu** | returns to the parent **with focus restored to the item you entered from**`LOAD GAME``LOAD GAME`, `TUTORIAL``TUTORIAL`, `OPTIONS``OPTIONS`, `EXTRAS``EXTRAS` | 4/4 |
| **Ⓑ on the main menu** | goes to the **title**, which re-draws `PRESS Ⓐ BUTTON` after a beat | |
| **Ⓑ on the title** | **nothing** | |
Wrap holds on both screens tested — the 5-item main menu and the 3-item `EXTRAS`
submenu — so it is a menu rule, not a per-screen table.
**🟡 Initial focus is reproducible but not established as invariant.** Both of my
boots opened on `TUTORIAL`, and both used `boot_menu.sh`. The run recorded in
[`menu-state-in-memory.md`](menu-state-in-memory.md) reached `EXTRAS` with *four*
downs from the main menu, which only works from `NEW GAME`. Either the harness
path matters or something persists. **Do not hardcode `TUTORIAL` without
re-testing it**; what is solid is that the menu does *not* open on the top item
in this harness.
## Q4 — where each button goes
Measured by driving: focus the item, press Ⓐ, read the screen's own title.
| button | screen it opens | evidence | GamePart id |
|---|---|---|---|
| `NEW GAME` | ❔ **not tested** | — | — |
| `LOAD GAME` | the save-slot list, `LOAD GAME` / `Current Storage` | [`q4-destinations.png`](captures/menu-nav/q4-destinations.png) left | 🟡 `3 GP_LOAD` |
| `TUTORIAL` | the lesson list, `TUTORIAL`, Level 1 / Level 2 | same, middle | 🟡 `25 GP_TUTORIAL` |
| `OPTIONS` | `OPTIONS` — GAME / CONTROL / SOUND / SCREEN SETTINGS / BACK | same, right | 🟡 `8 GP_OPTIONS` |
| `EXTRAS` | **`GP_TITLE.pak` build 6** — MISSION SELECT / MOVIE THEATER / BACK | [`ui-title-build-map.md`](ui-title-build-map.md) | 🟡 `5 GP_EXTRAS` |
| `EXTRAS ▸ MISSION SELECT` | the stage list + Wide Area Space Map | | 🟡 `7 GP_MISSION_SELECT` |
| `EXTRAS ▸ MOVIE THEATER` | ❔ not tested | | 🟡 `6 GP_MOVIE_THEATER` |
**Say which, as the gate asks.** The *screen* each button opens is **measured**
I pressed the button and read the title off the framebuffer. The **GamePart id is
not measured**: it is the entry of the decoded 29-id table at `.rdata 0x820A1630`
([`challenge-mission-gate.md`](challenge-mission-gate.md) §3) whose *name* matches
the screen I saw. The table is decoded; the *binding* of a button to an entry in
it is a name match I made by eye. The port should treat these ids as authored.
Worth noting that the ids and the paks are not one-to-one: `GP_EXTRAS` is id 5
with **no pak of its own** — its artwork is a build inside `GP_TITLE.pak`.
**`NEW GAME` was deliberately not pressed.** Ⓐ on it leads to a standing
black-screen hang that ends the run
([`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md)),
and this iteration needed the session. It is the one destination still unmeasured.
### The cheap way to finish this, and to make it a measurement
`0x828A690C` holds a **live screen id**`1` title, `3` main menu, `4` extras —
and `0x828F38AC` the cursor ([`menu-state-in-memory.md`](menu-state-in-memory.md)).
Reading those while pressing Ⓐ turns "the screen said OPTIONS" into a measured
transition, and works under `--gpu=null` with no screenshots at all. ⚠️ Note that
those values are **not** GamePart ids — `GP_TITLE` is GamePart 0 and `GP_EXTRAS`
is 5, but the word reads 1 and 4 — so it is a third enumeration and mapping it to
the id table is itself unfinished work.
## ✅ `NEW GAME` — measured 2026-08-28, and it is not a hang
The one destination this page could not test has been driven. **Ⓐ on `NEW GAME`
does not hang.** It opens two more menus first:
```
NEW GAME -> DIFFICULTY -> SELECT DATA -> guest crash
```
* **`DIFFICULTY`** — `EASY` / `NORMAL` / `HARD` / `BACK`, footer
`Ⓐ : OK Ⓑ : Back`, opening focused on **`NORMAL`**
([capture](captures/newgame-path/newgame-difficulty.png)). It sat unchanged for
90 s with the emulator healthy — a menu waiting for input, which is exactly
what "a standing hang" looks like to a screenshot loop that never presses
anything.
* Ⓐ on `NORMAL` → **`SELECT DATA`**, a save-slot picker headed
`Current Storage: Dummy HDD`, prompting to pick a file for the auto-save.
* Then the guest throws, and Xenia pauses with `PC: 0x82307128`
([capture](captures/newgame-path/newgame-selectdata-crash.png)).
**That crash is already in the corpus and is not new**: `0x82307128` is inside
`sub_823070B0`, the cache-manager STL erase documented in
[`title-crash-stl-tree.md`](title-crash-stl-tree.md), whose trigger is an
incomplete on-disc shader/code cache — not the menu path. The corpus also already
holds `captures/select-data-crash.png`.
So Q4's last row closes as **measured**:
| button | screen it opens |
|---|---|
| `NEW GAME` | **`DIFFICULTY`** (then `SELECT DATA`) |
🟡 The GamePart ids for these two are unmeasured, like the rest — `24 GP_DIALOG`
and `3 GP_LOAD` / `2 GP_SELECT_STORAGE` are name-match candidates and nothing
more.
### Initial focus — a fourth data point, and it still varies
This boot opened the main menu on **`NEW GAME`**. Running tally across four
boots of the same harness: `TUTORIAL`, `TUTORIAL`, `NEW GAME`, `NEW GAME`.
Unchanged conclusion: **do not hardcode it.**
### The `NEW GAME` path completes — the `SELECT DATA` crash is state, not path
A second run of the same path, 2026-08-28, **did not crash**:
```
NEW GAME → DIFFICULTY → (Ⓐ on NORMAL) → SELECT DATA → (Ⓐ on a slot) → a movie plays
```
The previous run's throw at `PC 0x82307128` is therefore **not inherent to this
menu path** — the same six presses got through it. That is consistent with the
trigger `title-crash-stl-tree.md` already names (an incomplete on-disc cache) and
with nothing about `NEW GAME`. 🟡 n = 1 either way; do not read it as "fixed".
Worth recording because the first observation could easily have hardened into
"the new-game path crashes", which is what "A on NEW GAME hangs" had already
become once.

View File

@@ -264,7 +264,7 @@ and the boot never happens.
A Stage 02 run froze after ~4 minutes of flight (screen still `flight`, not GAME
OVER), and `gdb_bt.sh` took backtraces of all **79** threads
([`captures/stage02-freeze-gdb-backtraces.txt`](../captures/stage02-freeze-gdb-backtraces.txt)).
([`captures/stage02-freeze-gdb-backtraces.txt`](captures/stage02-freeze-gdb-backtraces.txt)).
**Every single one is in a wait.** Not one thread is executing guest code or
sitting in a xenia loop:
@@ -394,7 +394,7 @@ and the control are all in place; what is missing is one frozen sample.
The fourth run froze **9 seconds** into the watcher's window, in flight
(`freeze_watch.sh` confirmed the HUD was still on screen), and the probe built
for exactly this moment reported **the healthy-run baseline and nothing else**
([`captures/stage02-freeze-stuck-wait-probe.txt`](../captures/stage02-freeze-stuck-wait-probe.txt)):
([`captures/stage02-freeze-stuck-wait-probe.txt`](captures/stage02-freeze-stuck-wait-probe.txt)):
```
FROZEN IN FLIGHT at 9s

178
docs/re/movie-binding.md Normal file
View File

@@ -0,0 +1,178 @@
# Which movie plays where — the boot intro, the attract loop, the new-game intro
**Status:**`CONFIRMED`. The bindings are **decoded** from the movie manifest,
and the attract movie is independently **measured** from captured frames. One
part — whether playback is skippable — is 🟡 and the corpus contradicts itself
about it; that is written down rather than resolved by assertion.
Answers [MISSION Q9](../port/MISSION.md), and contributes to Q6.
## The manifest names the boot-side slots — decoded
The movie manifest (`dat/tables.pak` entry `0x5b983a08`, schema `0x067025b9`,
[`movie_manifest.rs`](../../crates/sylpheed-formats/src/movie_manifest.rs)) is an
array of slots whose **key is the semantic role**, with a parallel array of value
records in the same order. Its first eight slots are the whole boot-side flow:
| slot key | movie | also bound |
|---|---|---|
| `LOGO1``LOGO4` | `logo1.wmv``logo4.wmv` | — |
| **`ADVERTISE_MOVIE`** | **`ADV.wmv`** | `VOICE_ADV` |
| `STAFF_ROLL` | `SYLPH_HD720p_8M-CBR_2ch.wmv` | subtitle table |
| **`MS00A`** | **`S00A.wmv`** | `SUBTITLE_S00A`, `VOICE_S00A`, a `pwterop_s01a.prt` overlay |
| `MS01A` | `S01A.wmv` | `SUBTITLE_S01A`, `VOICE_S01A` |
The alignment is not in doubt here even though the manifest has valueless slots
elsewhere: `logo1..4` zip 1:1 onto `LOGO1..4`, and `MS00A``S00A.wmv`,
`MS01A``S01A.wmv` are the manifest's own anchor rule.
**The `logo1``logo4` slots are bound to `.wmv` files that are not on the disc**
already established, and the reason the developer splash is a screen, not a video.
## ✅ The boot intro and the attract movie are the same asset
There is **no separate boot-intro slot**. `ADV.wmv` is bound to
`ADVERTISE_MOVIE` — it *is* the advertise movie, and the boot simply plays it
first. For the port that means one video, not two.
### Measured independently, from captured frames
Frames captured 5 s apart during an attract cycle were matched against five
candidate movies by frame signature (32×18 normalised grayscale, movie frames
cropped to the 675 of 720 rows the game surface shows, 1 fps sampling).
`tools/re-capture/frame_match.py`; full table in
[`data/attract-frame-match.txt`](data/attract-frame-match.txt).
**15 of 19 attract frames matched `ADV.wmv`, and the matched timestamps advance
monotonically at the sampling rate** — 39, 45, 56, 62, 75, 80, 85, 92, 102, 108,
113, 119, 131, 137 s — ending at 137 s, which is `ADV.wmv`'s full length, after
which the title returned. That is not a similarity score, it is a playhead.
**The control was run first and behaves the same way.** Frames captured during
the *boot* movie — known to be `ADV.wmv` — matched `ADV` at 116, 121, 129, 134 s.
Its failure mode is worth stating: on near-black frames the correlation collapses
(one control frame scored 0.000, another tied `S13A` 0.984 against `ADV` 0.926).
Those are exactly the 4 attract frames that did not match. **A dark frame carries
no signature; it is not evidence for the runner-up.**
### 🔴 This corrects an earlier claim of mine
Two iterations ago I recorded the attract movie as "≈85 s, so probably not
`ADV.wmv`'s 137 s". That was wrong, and the mistake was arithmetic on an
unobserved start: my sampling began when the movie was already **39 s in**, so
what I timed was the *tail* of it, not the whole thing.
## ✅ The new-game intro is `S00A.wmv`
Slot `MS00A` — the prologue story intro, 93.87 s, with its own subtitle table,
`VOICE_S00A`, and a text overlay. Decoded from the manifest, **not** measured:
Ⓐ on `NEW GAME` hangs the emulator
([`menu-navigation-semantics.md`](menu-navigation-semantics.md)), so this
binding has not been watched happening.
## 🟡 Skippable — the corpus contradicts itself, and I did not settle it
* [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) states
plainly that "Ⓐ during a movie skips the movie, every time".
* But `boot_menu.sh` / `skip_intro.sh` as they stand **deliberately do not tap
during a movie**, logging `movie … -> waiting it out (tapping breaks the
title)`, and the same document records run G: 88 Ⓐ presses through the boot
left a permanent black screen.
Both cannot be the whole story. **The test**: boot, tap Ⓐ exactly once well
inside the movie, and record whether the title arrives early. One boot, and it
was not run this iteration.
## ✅ What ends the attract movie
Nothing intervenes: it **plays to its end**. The last matched frame is `ADV` at
137 s — the file's full duration — and the title is back on the next sample. So
the attract cycle is: title, idle ~810 s, fade to black, `ADV.wmv` in full, back
to the title (which redraws `PRESS Ⓐ BUTTON`).
## What this contributes to Q6
The manifest is **data the game reads** to sequence the boot side: the slot keys
are in play order and the key *is* the role. That is not the whole driver — it
says what plays, not what decides to advance — but it is the first file-side
piece of Q6's second half, and it means the boot-side asset order does not have
to be authored.
## ✅ Skippable — settled 2026-08-28: **one Ⓐ skips the movie**
The test named above was run, and the answer is unambiguous.
| | |
|---|---|
| baseline: title arrives with **no** input | ~193 s, ~196 s, ~193 s (three boots) |
| **one Ⓐ tapped at ~45 s into the boot** | **title at ~57 s** |
The press is provably the cause and provably singular: Canary's own
`[RE-INPUT] XamInputGetKeystrokeEx reached the driver` counter went **3 → 4**
across the tap, so exactly one keystroke was delivered, and the title arrived
~12 s later instead of ~150 s later.
**And the skipped-to title is fully functional**, which is the part that matters
for the harness folklore: it draws the `PRESS Ⓐ BUTTON` plate (2 408 plate pixels
by the same probe used elsewhere), and a second Ⓐ opened the main menu normally
([`title-after-single-A-skip.png`](captures/newgame-path/title-after-single-A-skip.png)).
### So what is `skip_intro.sh` protecting against?
Not a single tap. The failure recorded in
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) is run G —
**88 presses at 4 s intervals through the whole boot** — which left a permanent
black screen. Hammering breaks it; one press does exactly what the button is for.
The scripts' `waiting it out (tapping breaks the title)` comment is **too broad**,
and it costs every scripted boot ~2.5 minutes.
## ✅ `S00A.wmv` watched — measured 2026-08-28
The manifest's `MS00A → S00A.wmv` has now been confirmed against the running
game. The first attempt, with 5-second screenshots, gave an ambiguous partial
playhead; re-run with **`ffmpeg x11grab` at 10 fps** and matched every 0.5 s, it
is unambiguous:
| capture t | `S00A` score @ playhead | `ADV` (runner-up) |
|---|---|---|
| 5.0 s | **0.998** @ 1.0 s | 0.787 @ 126.5 s |
| 6.0 s | **0.999** @ 1.5 s | 0.820 @ 126.5 s |
| 7.5 s | **1.000** @ 3.0 s | 0.836 @ 126.5 s |
| 9.0 s | **0.999** @ 4.5 s | 0.823 @ 126.5 s |
| 12.0 s | **0.998** @ 7.0 s | 0.985 @ 34.5 s |
| 16.0 s | **0.999** @ 10.0 s | 0.886 @ 37.5 s |
| 17.5 s | **0.967** @ 11.0 s | 0.460 @ 87.0 s |
Across the 25 consecutive samples from capture 5.0 s to 17.5 s the `S00A`
playhead is **strictly non-decreasing** — 1.0, 1.5, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
4.5, 5.0, 5.5, 5.5, 6.0, 6.5, 7.0, 7.0, 7.5, 8.0, 8.5, 9.0, 9.0, 9.5, 10.0, 10.5,
11.0 — advancing at essentially real time, with scores at or above 0.96. **`S00A`
is the top match on 23 of the 27 frames that carry signal** (contrast > 35;
the other 25 of 52 frames are the transition and are correctly ignored).
So the new-game intro is `S00A.wmv`: **decoded** from the manifest and now
**measured** off the game. It begins ~4.5 s after Ⓐ on the save slot.
### 🟡 An aside worth recording: `ADV.wmv` reuses `S00A` footage
In the window `S00A` 5.510 s, `ADV` also scores 0.970.99 — at its *own*
monotonically advancing playhead, 33.537.5 s. Two different movies both matching
strongly with two consistent playheads is not noise; it reads as **the boot movie
being a trailer cut from the story cutscenes**, `S00A` among them. That would
explain why the sparse first attempt kept flipping between them. Not investigated
further.
### ⚠️ Sparse sampling was the whole problem
The earlier attempt sampled every 5 s and produced contrast 2337 frames, a
playhead that would not join up, and one high-contrast frame preferring `ADV`. The
same question at 0.5 s spacing answers itself at 0.999. **The movie was never
ambiguous; the sampling was.**
### The run was killed before the mission load
An earlier run of this path disappeared at ~145 s with **no crash line in its own
log** — routine `MEM-WATCH rss=1153MB` to the end — so an external kill, not a
guest fault, and the second this session. The `DELTASABER` plates of
[`ui-title-build-map.md`](ui-title-build-map.md) need a mission load and were not
reached.

View File

@@ -0,0 +1,90 @@
# S1 — the Ready Room probe: **no-go**, and not for the reason expected
**Status:** one iteration, as [MISSION §S1](../port/MISSION.md) specifies. This is
the written go/no-go. No Ready Room work has been started.
## Recommendation: no-go
Not because it is 3D — **it is not** — and not because the names are
unrecoverable. Because the probe found that **`GP_READY_ROOM.pak` does not appear
to contain the Ready Room's own menu screen at all**, so the question the mission
framed ("only 6 of 1 106 names resolve, is that a week or a quarter?") is aimed at
the wrong target. And it is outside the five-screen milestone regardless.
## What the probe measured
### ✅ It is 2D. This is not a 3D room with a UI overlay.
The corpus already holds a framebuffer capture of the running screen,
[`captures/ready-room-extra-mode.png`](captures/ready-room-extra-mode.png): a flat
menu — a `READY ROOM` wordmark, six labelled items (`TAKE OFF`, `BRIEFINGS`,
`HANGAR`, `PILOT LOG`, `OPTIONS`, `MISSION CHANGE`), a focus ring, and an
`Ⓐ : OK / Ⓑ : Mission change` footer — in exactly the visual language of the
title menus. There is no model viewport. The pak's own entry list carries **zero
3D containers** (no `XPR2`, no `XBG7`).
So the mission's own no-go-by-definition criterion is **not** met. That part is a
go.
### ✅ Enumeration by content works — 60 builds
`sylpheed-cli screen list` finds **60 screen builds**. The 1 106 entries break
down as **902 RATC bundles, 192 IDXD objects, 6 `ttf`, 6 `LSTA`**. The worry that
unresolvable *paths* would mean unrenderable *screens* is unfounded here.
### 🔴 But none of it is the Ready Room menu
Two independent signals, and they agree.
**Rendered composites are briefing panels.** Four of the 60 builds — the largest
and most element-rich (18, 26, 44 and 9 elements) — composite to a planet
information panel, unit markers and direction arrows:
[`ready-room-pak-builds.png`](captures/ready-room-probe/ready-room-pak-builds.png).
Tactical-map material, not a six-item menu. ⚠️ **4 of 60 sampled** — this alone
does not close it.
**The name census does close it much harder.** The 902 bundles declare **317
distinct element names**, all under a `pb*` prefix family:
`pbfriendly` (330), `pbacropolis` (168), `pbrhino` (48), `pbsub` (204),
`pbtab` (48), `pbcm` (132)… Searching all 317 for any of the six visible labels —
`takeoff`, `take_off`, `brief`, `hangar`, `pilot`, `option`, `mission`, `ready`
returns **nothing**. The only `main` hits are `pbmain_btn1..3.t32`,
`pbmain_window.t32` and their `_eff` glows, which appear in 9- and 4-element
*fragments*, not a screen, and are declared **kind 0** (decoration).
Read together: this pak is the **briefing / tactical-map** content that sits
*behind* the Ready Room's `BRIEFINGS` item, and the Ready Room's own menu lives
somewhere else. Note the GamePart table has both `10 GP_BUNK` and
`11 GP_READY_ROOM` ([`challenge-mission-gate.md`](challenge-mission-gate.md) §3),
and `GP_BUNK.pak` is a separate archive that was not examined here.
### 🔴 And a caveat that reaches back into the delivered corpus
**`kind == 0x3002` finds zero buttons in this pak.** All 902 bundles, 0 hits —
while `GP_TITLE` has 16. What `GP_READY_ROOM` uses instead:
| kind | count |
|---|---|
| `0x3000` | 491 |
| `0x3004` | 406 |
| `0x300c` | 135 |
| `0x3008` | 60 |
| `0x3001` | 6 |
| `0x3002` | **0** |
So `0x3002` is one member of a `0x3000` **family** with sub-bits, and "element
kind `0x3002` is a button" — a ✅ row in [`HANDOFF.md`](../port/HANDOFF.md) — is
sound for the title screens and **must not be applied as an equality test
elsewhere**. For the five-screen milestone nothing changes; for anything beyond
it, test the family.
*(The kind field is the 4th `u32` of the 60-byte declaration entry, at `+40`.
Calibrated on `GP_TITLE` build 6, where it reads `0x3002` for exactly the three
`EXTRAS` buttons and `0x0` for the decorations.)*
## If it is ever picked up
The first job is **not** naming this pak's keys. It is finding where the Ready
Room's six-button screen actually lives — `GP_BUNK.pak` is the obvious first
place to look, and the same content-based `screen list` / name-census pass costs
one iteration there.

View File

@@ -0,0 +1,170 @@
# What happens between two screens — a fade through black, and where its timing lives
**Status:**`CONFIRMED`. The quad and its ramp are **decoded** (a keyframe
group on the disc, with a disc-wide check); the wall-clock timings are
**measured** off the running game at 30 fps. One piece is **undecodable from this
field** and is called out below.
Answers [MISSION Q7](../port/MISSION.md).
## The mechanism — decoded
Every title-side screen carries a full-screen untextured primitive that is
**black, and paints last**: `pteff00.prm` in `GP_TITLE`, `pfeff00.prm` in
`GP_SAVE_LOAD`. That it sorts last was already established
([`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md)); what is
new here is that its **keyframe group is the transition**.
The group is always four blocks, and always this shape:
| block | alpha | meaning |
|---|---|---|
| 1 | `0xff` at `t = T0` | the screen starts **black** |
| 2 | `0x00` at `t = T1` | ramp to fully clear — the screen **fades in** |
| 3 | `0x00` at `t = T2` | clear; this is the resting pose, the quad is invisible |
| 4 | `0xff`, **no time** | ramp back to black — the screen **fades out** on exit |
Read with the corpus's rule that a keyframe is the *start* of a ramp
([`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)).
```
$ tools/re-capture/fade_quads.py 4 5 6 # GP_TITLE
build 4 (title) pteff00.prm t=16 α=255 t=261 α=0 t=269 α=0 t=— α=255
build 5 (main menu) pteff00.prm t=12 α=255 t= 70 α=0 t= 80 α=0 t=— α=255
build 6 (EXTRAS) pteff00.prm t=12 α=255 t= 64 α=0 t= 74 α=0 t=— α=255
```
Under [Q1](ui-keyframe-time-unit.md)'s `1 unit = 1/60 s`: the screen holds black
for **0.20 s**, then fades in over **0.87 s** (`EXTRAS`), **0.97 s** (main menu)
or **4.08 s** (the title).
### ❔ The fade-OUT duration is not in this field
The fourth block has **no time** — a group's last block stops 4 bytes short and
that word is already the next group's element index
([`ui_layout.rs`](../../crates/sylpheed-formats/src/ui_layout.rs) documents the
packing). So the disc gives the ramp's *target* (black) and not its length. That
duration is **measured** below, and the port is authoring it.
### The disc-wide check, and what it shows about overlays
Over every `GP_*.pak`, counting bundles that have a `.prm` element **and** ≥8
elements (screen-sized rather than a two-element fragment):
> **40 of 97** carry a 255 → 0 → 255 quad; 56 of the 60 found disc-wide have
> exactly 4 keyframes.
41 % sounds weak until it is read per pak, where it is nearly all-or-nothing:
| pak | with quad / screen-sized |
|---|---|
| `GP_MISSION_SELECT`, `GP_MOVIE_THEATER`, `GP_OPTIONS`, `GP_TUTORIAL`, `GP_SYSTEM` | 2/2 each |
| `GP_BUNK`, `GP_CHALLENGE` | 6/6 |
| `GP_STAGE_CLEAR` | 4/4 |
| `GP_SAVE_LOAD` | 10/12 |
| **`GP_TITLE`** | **6/12** |
| `GP_DIALOG` | 0/133 |
| `GP_DEBRIEFING_PILOTLOG` | 4/102 |
**`GP_TITLE`'s 6 of 12 is the interesting row, and it is not a gap.** The six
that carry the quad are exactly the six *screen* builds — title, main menu and
`EXTRAS`, English and Japanese. The six that do not are exactly the six
**overlays**: the `PRESS Ⓐ BUTTON` plate and the two `DELTASABER` plates
([`ui-title-build-map.md`](ui-title-build-map.md)). An overlay composited onto a
screen has no transition of its own, so it has no fade quad — which is
independent corroboration that those builds are overlays rather than screens.
`GP_DIALOG`'s 0/133 says the same thing about dialog boxes.
## The timing — measured
Recorded with `ffmpeg -f x11grab -framerate 30` over the game surface, mean
frame luminance per frame; raw data in
[`captures/transitions/transition-luminance.csv`](captures/transitions/transition-luminance.csv),
filmstrip in
[`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png).
| | main menu → `EXTRAS` (Ⓐ) | `EXTRAS` → main menu (Ⓑ) |
|---|---|---|
| press → first visible change | 0.07 s | 0.37 s |
| **fade-out to black** | **0.367 s** | **0.400 s** |
| **pure black** (luminance 0.02) | 0.233 s | 0.167 s |
| luminance rise until settled | 0.567 s | 1.467 s |
The fade-out is the number the file cannot give, and it comes out the same both
ways: **~0.4 s**, i.e. ~24 units under Q1's rule.
The black hold is *consistent with* the file's 12 units (0.20 s) but does not
confirm it — the plateau spans the tail of the outgoing screen's fade-out and the
head of the incoming screen's black, and this measurement cannot separate them.
### ⚠️ The luminance rise is **not** the quad's ramp
The two columns differ by 2.6× where the quad's declared ramps differ by only
1.12× (58 units vs 52). The filmstrip says why: the background reappears *first*
and the labels arrive after it, so what the luminance curve is timing is the
incoming screen's **own element animations**, not the fade quad. Quoting 1.47 s
as "the main menu's fade" would be wrong. The quad's ramp is decoded; the
screen's build-in is a separate, longer thing.
## For the port
* a screen change is: **fade the outgoing screen to black over ~0.4 s**, hold
black briefly, then **fade the incoming screen in over its own declared ramp**
while its elements play their own keyframes;
* the fade-in ramp is **read from the file** (`T1 T0` on the screen's fade
quad);
* the ~0.4 s fade-out and the black hold are **authored from this page** — the
disc does not carry them.
## ✅ Which quantity the ~0.4 s is — measured 2026-08-29
Asked by the port: is 0.4 s **(a)** the ramp from the hold to the exit pose, i.e.
exactly the missing duration of that untimed keyframe, **(b)** several keyframes'
worth, or **(c)** something the game does independently of the group?
**It is (a)** — and it is bigger than the fade quad. Two facts.
**1. There is exactly one untimed keyframe, and every element has it.** Reading
`screen info --build 5 --geometry` for the main menu, all 16 elements end on a
single timeless block; none has two. So there is one unknown duration per screen,
not a chain of them — which rules out (b) outright. And that final block is not
idle: it is where the screen *plays out*.
| elements | final untimed block | what it does |
|---|---|---|
| `pteff00.prm` (the fade quad) | `a = 255` | goes **black** |
| `pteff10`, `pteff12`, `ptbtn01…05`, `ptmsg` | `a = 0` | **fade out** |
| `ptframe1`, `ptframe2` | `a = 255` | hold, and get covered |
| `ptbase`, `pteff05`, `ptloop*`, `pteff02.prm` | single keyframe | hold |
**2. The capture shows the content fading, not just a black quad arriving.** This
has a null hypothesis that discriminates: under (c) — the game blackens the frame
independently — every region is scaled by the same `1 α`, so the **ratio**
between a button region and a background region is *constant* through the
fade-out. Under (a) it must fall, because the buttons ramp to `a = 0` while the
background elements hold at 255 and are only dimmed.
Measured on [`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png),
button column ÷ upper-right background art, frame by frame through the fade-out:
```
frame 0 1 2 3 4 5
ratio 6.495 5.574 3.105 2.125 1.935 (black)
```
**A 3.4× monotonic fall.** Constant is refuted. The buttons really are fading
independently of the overall dim, exactly as their declared final block says.
(The incoming screen runs it in reverse, 2.22 → 3.47 over frames 712.)
⚠️ **Reach.** The filmstrip is downsampled and the "button" region unavoidably
contains some background, so the ratio is a direction, not a clean alpha
measurement. It refutes the constant-ratio null decisively; it does not by itself
pin the 0.4 s to ±0.05 s. And it is measured on **one** transition pair.
### For the port
Write **one** authored constant — the duration of the final untimed keyframe,
~0.4 s / ~24 units — and **play the group to its end on every element**. Do not
model the exit as a black rectangle fading over a frozen screen: the buttons and
labels ramp to transparent at the same time, and that difference is visible.

View File

@@ -146,7 +146,7 @@ the encoding: **1 = not yet deployed, 2 = active, 4 = destroyed**.
### ✅ The mission-over branch, observed exactly as disassembled
The phase ended at 694.9 s, but **the ordinal did not advance** — and the reason
is the branch [mission-phase-advance](../mission-phase-advance.md) read out of
is the branch [mission-phase-advance](mission-phase-advance.md) read out of
`sub_82260710`:
```

View File

@@ -0,0 +1,153 @@
# A music bank is two stems of one performance, played together
**Status:**`CONFIRMED` for the structure — **decoded**, with a disc-wide check
over all 32 BGM banks. 🟡 for the *role* of the second wave — **measured** by
signal analysis, and the two readings that survive are named below. ❔ which bank
is the menu's music is **undecodable from the disc**, with the reach stated.
Answers [MISSION Q10](../../port/MISSION.md). It also **withdraws the premise**: the
handoff said `BGM_001.slb` is *three* sub-waves of 10 KB / 4.47 MB / 4.67 MB. The
10 KB is the bank **header**, not a wave.
## The structure — decoded
```
BGM_001.slb (9 178 040 B)
0 BANK id=1001 data_size=9175992 hdr = 5 blocks (10240 B) 16bit/2ch
10240 RIFF data@14336 size=4466688 (2181 packets) 25697 B/s → 173.82 s
4481024 seek 2181 packets
4491264 RIFF data@4495360 size=4673536 (2282 packets) 26887 B/s → 173.82 s
9168896 seek 2282 packets
```
A bank is a 10 240-byte header and then **exactly two waves**, and the two always
have the **same duration** — different byte sizes and different bitrates, same
number of seconds. Duration is `data_size / PsuedoBytesPerSec` (the u32 at
`RIFF+0x20`; `RIFF+0x24` is the sample rate, 48 000 Hz except `BGM_020``023`
at 44 100).
### Disc-wide
`tools/re-capture/bgm_wave_census.py`, full output in
[`../data/bgm-wave-census.txt`](../data/bgm-wave-census.txt):
> **28 of the 32 BGM entries** read straight off their own pak entry as two waves
> of equal duration — equal to 0.01 s, over lengths from 37 s to 277 s.
The other four — `BGM_106``BGM_109` — are **not counterexamples**, they are the
known leading-region straddle ([`slb-data-offset.md`](slb-data-offset.md)): those
entry windows start mid-bank, so an entry contains the tail of the previous bank,
one whole wave of its own, and the `BANK` header of the *next* one.
`slb_segment_phase.py bank` shows it directly — the entry named `BGM_107.slb`
contains `BANK id=1108`. Realigning across the boundaries with the `seek` packet
counts, which pin each join exactly (bank 1107: a `seek` for 1164 packets and a
2 383 872-byte wave = 1164 packets), restores the same shape:
| bank | wave 0 | wave 1 |
|---|---|---|
| 1107 | 2 383 872 B → 65.18 s | 2 418 688 B → 65.18 s |
| 1108 | 3 696 640 B → 128.91 s | 3 696 640 B → 128.91 s |
| 1109 | 2 887 680 B → 81.50 s | 2 932 736 B → 81.50 s |
Bank 1106's wave 0 begins in unclaimed bytes before its entry and is known only
by its `seek` count (1 718 packets), so it is 🟡 rather than measured.
**So: intro + loop is dead, and so is two halves of one piece.** Both would give
unequal durations; 32 banks give equal ones.
## The role of the second wave — measured
Decoded `BGM_001`'s two waves to PCM and compared them.
| | wave 0 | wave 1 |
|---|---|---|
| duration | 173.809 s | 173.809 s |
| first sound / last sound | 0.001 s / **167.663 s** | 0.003 s / **167.663 s** |
| RMS | 6 824 | 3 961 |
| L↔R correlation | 0.61 | **0.14** |
| energy below 200 Hz | 16.5 % | **6.7 %** |
| energy 14 kHz | 31.1 % | 42.5 % |
**They are sample-synchronous.** Transient-envelope cross-correlation searched
over ±5 s peaks at **lag +0.00 s** (0.5624), and both waves stop at the *same
millisecond*, 167.663 s. Two takes, two halves or two alternates would not do
that. They are two stems of one performance and are meant to sound **at the same
time**.
**Therefore concatenating them is wrong**, and not subtly: it plays the piece
twice, the second time as a bass-less secondary stem.
### 🟡 Which kind of second stem — two readings survive
* **The rear pair of a 4-channel mix.** Fits every number: quieter, far more
decorrelated between L and R, and with the bass managed away to the front pair.
* **A second intensity layer** to be mixed in or crossfaded.
**Not settled, and the obvious field does not settle it.** `ChannelMask` is
`0x0002` on *both* waves, and [`sound-slb.md`](sound-slb.md) already records that
this game writes meaningless channel metadata (movie voices declare 2 channels
over mono content). What would settle it is a runtime observation — whether the
game submits both waves to the mixer at once — which needs an emulator with audio
this container does not have.
## ❔ Which bank is the menu's music — undecodable, and here is the reach
The cue table binds cue names to sound ids and banks
([`sound-cue-table.md`](sound-cue-table.md)), and its **32 BGM cues are named
`BGM_001``BGM_109`** — pure numbers, no `TITLE`, no `MENU`, no semantic name
anywhere. Looked in: the `SOUNDS` record (5 798 cues), `FILES` (5 135 bank
paths), and the bank headers themselves (a `BANK id` that simply repeats the cue
number, `id=1001` for `BGM_001`). Nothing names a screen.
So the menu↔bank binding has to come from the running game — the same route Q8's
event bindings need — and until then the port is **choosing** a track, not
transcribing one.
## What the port needs to know
* a music bank is **two waves that play together**, not one track and not a
sequence — do not concatenate;
* both are full length; align them at sample 0;
* **the track is not a seamless loop.** `BGM_001` fades out and is followed by
**6.15 s of silence** (last sound 167.663 s of 173.809 s; the final second
before silence is at RMS 168 against 4 788 at the head). Looping the wave as
stored gives a fade-out and six seconds of nothing every cycle. No loop-point
field has been identified in the XMA header, so a menu loop is **authored**.
## ✅ Confirmed at runtime — two stereo streams decode at once
The page above said the "played together" reading needed "a runtime observation
of whether the game submits both waves to the mixer at once", and that this
container could not do it. It can: `--xma_param_probe=true` logs every XMA stream
the decoder is handed.
Sitting on the **main menu**, two stereo 48 kHz streams were decoding
simultaneously — **1 893 packets / 3 876 864 B** and **1 919 packets /
3 930 112 B**. Two stereo streams alive at the same moment on a screen with one
piece of music playing is the direct observation this page was missing: **a music
bank's two waves are simultaneous, not sequential.** Concatenating them is wrong,
now measured as well as inferred.
🟡 It still does **not** separate the two readings — surround-rear pair versus a
second intensity layer — because both predict simultaneity.
## ✅ The menu's music is `BGM_103` — three independent routes agree
An earlier version of this section said the two observed byte sizes "do not match
any bank's declared wave sizes". **That was wrong** — I had checked only the
`BGM_0xx` rows. They match `BGM_103` exactly, and the code names it:
| route | evidence |
|---|---|
| **static, code** | `GamePart_Title`'s phase handler `sub_821C5580` does `li r5, 1103` into a sound call (`sub_8217ACF8`, with `r4 = 4`) — **cue 1103 = `BGM_103`** |
| **static, disc** | [`../data/bgm-wave-census.txt`](../data/bgm-wave-census.txt): `BGM_103.slb` = two waves of **3 876 864** and **3 930 112** bytes, 87.75 s each |
| **runtime** | the XMA probe at the **main menu** saw two stereo 48 kHz streams decoding simultaneously, of **3 876 864** and **3 930 112** bytes |
Byte-for-byte on both waves. So:
* **which bank the menu plays is decoded** — `BGM_103`, from the executable, since
the cue *table*'s BGM entries are numeric and name no screen;
* and the two-stems-play-together finding is now confirmed **on a named bank**,
with the runtime stream sizes equal to the bank's declared wave sizes — so the
game hands the decoder the whole wave, not a window, which is the opposite of
what the earlier note guessed.

View File

@@ -82,7 +82,7 @@ names had no text to resolve *to*, and the 100 % figure was unreachable.
* **Which recording plays.** The caption is the *text*; the voice bank binding
is a separate and still-unresolved question — see
[voice-bank-leading-region.md](voice-bank-leading-region.md) and the
[voice-bank-leading-region.md](../voice-bank-leading-region.md) and the
known case of a generic line playing against a specific subtitle.
* **Which page a `MSG_DEMO_*` id belongs to.** The `DEMO` family is not called
from the stage scripts at all — 78 of its ids are multi-page, so something

View File

@@ -270,7 +270,7 @@ result of the two.
## ✅ 2026-08-23 (third pass) — the counter is at `0xbdb59668` again, and the refutation above is *refined*, not reversed
Run 4, with the hunt automated end to end
([`ob_hunt.py`](../../tools/re-capture/ob_hunt.py) + the HUD reader), produced
([`ob_hunt.py`](../../../tools/re-capture/ob_hunt.py) + the HUD reader), produced
**exactly one** surviving address:
```
@@ -322,7 +322,7 @@ With the per-entity searches refuted at both word and bit level
counter. It sits at `0xbdb59668`, inside the entity-heap window, so the answer is
readable directly: sample ±0x200 around it across one transition and keep the
words that move **with** it
([`ob_neighbourhood.py`](../../tools/re-capture/ob_neighbourhood.py),
([`ob_neighbourhood.py`](../../../tools/re-capture/ob_neighbourhood.py),
[`captures/ob-counter-neighbourhood-stage02.json`](../captures/ob-counter-neighbourhood-stage02.json)).
Control first: over an 8-second interval while the counter sat still, **0 of the

View File

@@ -0,0 +1,47 @@
# ✅ The disc ships movies in **two** audio profiles — and one of them is 5.1
**Status:**`DECODED`, disc-wide (97 of 97 movies probed). Found while checking
a transcode, not while looking for it.
## The split
```
$ ffprobe -select_streams a -show_entries stream=codec_name,sample_rate,channels,channel_layout
```
| profile | count | codec | rate | channels | which |
|---|---|---|---|---|---|
| **surround** | **28** | `wmapro` | 48 kHz | **6 (5.1)** | `ADV.wmv` + every `S*.wmv` story cutscene |
| stereo | 69 | `wmav2` | 48 kHz | 2 | every `RT*.wmv` and `hokyu_*.wmv` |
Full list in [`data/movie-audio-layouts.csv`](../data/movie-audio-layouts.csv).
The split is not arbitrary: the **cinematics** are 5.1 and the **in-mission radio
chatter** is stereo. `ADV.wmv` (137.714 s) and `S00A.wmv` — the boot/attract intro
and the new-game intro, i.e. **both movies the menu milestone needs** — are in the
surround group.
## ⚠️ Why this matters to anything that transcodes
A single `ffmpeg` command over `dat/movie/` produces **two different kinds of
result** and records neither:
* the 69 stereo files pass through channel-wise unchanged;
* the 28 surround files are **downmixed 5.1 → stereo by ffmpeg's default matrix**,
folding centre-channel dialogue into L/R at a weighting nobody chose.
That default is a *content* decision — it sets how loud the dialogue is against
the music — and it is **not stable across ffmpeg versions**. A pipeline that wants
a reproducible result has to state the downmix explicitly rather than inherit it,
and record it beside the command.
🟡 Whether the game itself plays these in surround, and with what downmix when the
console is set to stereo, is **not established here**. This page is about what is
on the disc.
## How it was found, and the trap next to it
Checking a transcode against its source, `ffprobe` reported the output as 33 s
against the source's 137 s — which reads as catastrophic truncation and is not:
the file was **being written at that moment**. Measuring a live write produces a
confident wrong answer with no error. See [`../METHOD.md`](../METHOD.md).

View File

@@ -0,0 +1,141 @@
# A RATC child's name is stated by an `opt ` block, not by the bytes before it
**Status:**`DECODED`, with a disc-wide check. This fixes a decoder defect that
silently dropped the **full-resolution background from all five menu screens**.
## The field
Immediately before nearly every child of a `RATC` bundle — 17 942 of the disc's
18 002 — sits an `opt ` block:
```text
"opt " | BE32 length | name | NUL | 3 bytes | <child magic>
```
Two children of the main menu bundle (`GP_TITLE.pak` entry 8), raw:
```text
opt 00 00 00 0a p t b a s e . t 3 2 00 0e 10 a4 T8aD
opt 00 00 00 0b p t e f f 0 5 . t 3 2 00 38 41 58 T8aD
^^^^^^^^
"8AX"
```
This is the **same `opt ` block** `ui_layout::opt_link` already decodes for a
button's focus link. Nothing new had to be discovered to read it — only noticed.
## The defect it fixes
`ratc::parse` named each child with `name_before`: the last printable run in the
96 bytes before the child's magic. That is a heuristic, and it is *usually* right
`ptbase.t32`'s three trailing bytes are `0e 10 a4`, not printable, so the scan
walks back to the real name.
But `pteff05.t32`'s trailing three bytes are `38 41 58`, which is `"8AX"` in
ASCII. The scan takes those, the child is registered under the name `8AX`, the
element that declares `pteff05.t32` matches nothing in the sprite table, and
`compose` drops it through
```rust
let Some(sprite) = el.sprite.as_ref() else { continue };
```
*before* the arm that records a `missing` sprite. So the screen's background
vanished with **no diagnostic at all**: `screen render` reported "all resolved".
⚠️ `8AX` was carried in `docs/` as though it were a name the game uses — the old
text read "the `T8aD` behind its `opt ` link is registered under the name `8AX`".
It is not a name. It is three bytes of the preceding record's payload.
## The disc-wide check
[`examples/ratc_child_names.rs`](../../../crates/sylpheed-formats/examples/ratc_child_names.rs)
compares the two readings for every RATC child in all 33 `dat/*.pak`
([data](../data/ratc-child-name-audit.txt)):
| | |
|---|---|
| RATC children scanned | **18 002** |
| carrying an `opt ` block | 17 942 |
| scanned name **agrees** with it | **17 918** |
| scanned name **differs** | **24** |
Every one of the 24 is the same failure: a 3-byte printable tail beating a real
name.
| scanned | actual | count | where |
|---|---|---|---|
| `8AX` | `pbbg.t32` | 12 | `GP_READY_ROOM` |
| `8AX` | `pteff05.t32` | 4 | `GP_TITLE` 5, 6, 8, 9 — the menus |
| `8AX` | `pmbase.t32` | 4 | `GP_STAGE_CLEAR` |
| `8AX` | `pteff04.t32` | 2 | `GP_TITLE` 4, 7 — the title |
| `'OX` | `po_keys_win1.t32` | 2 | `GP_OPTIONS` |
⚠️ **What I checked about the 24, precisely.** That the recovered name is the one
the bundle actually wants is verified for the ten `GP_TITLE` and `GP_STAGE_CLEAR`
cases: on the title screens `pteff04.t32`/`pteff05.t32` are *declared elements*
that previously resolved to nothing and now resolve, and `pmbase.t32` is the
target of `GP_STAGE_CLEAR`'s long-standing dangling reference (below). For the 12
`GP_READY_ROOM` (`pbbg.t32`) and 2 `GP_OPTIONS` (`po_keys_win1.t32`) cases I
checked only that no element in those bundles is left unresolved afterwards —
which is consistent with, not proof of, the same story. None of the 14 is on the
five menu screens.
**The control is the 17 918 the heuristic already got right**: the `opt ` reading
reproduces every one of them. A reading that fixed the 24 but disturbed the rest
would be a different rule, not this one.
**Reach — closed 2026-08-29.** 60 children (0.3 %) have **no** `opt ` block
within 128 bytes and fall back to the scan. **They are not children.** They are
the ten frames of the disc's only `.tan` resource, `pb_f15_eg_anm.tan`, in the
six language copies of one `GP_READY_ROOM` bundle — 6 × 10 = 60, the whole
population with nothing left over. One `opt ` block names the whole run, which is
why nine of the ten find no block of their own. None is on the five menu screens.
[`ratc-tan-frame-sequence.md`](ratc-tan-frame-sequence.md)
## What it changes in the composite
Resolving the name makes the element resolve, so `compose` now draws it. On all
five port screens ([before/after](../data/ratc-name-fix-render-effect.txt)):
| build | screen | mean brightness | high-frequency detail |
|---|---|---|---|
| 4 | title | 72.77 → 72.81 | **×1.15** |
| 5 | main menu (JP) | 42.73 → 42.74 | **×1.27** |
| 6 | `EXTRAS` (JP) | 44.08 → 44.09 | **×1.27** |
| 8 | main menu | 41.57 → 41.58 | **×1.30** |
| 9 | `EXTRAS` | 42.99 → 43.00 | **×1.29** |
The brightness is unmoved and the detail is up by a quarter — which is exactly
the signature of *the same artwork at twice the resolution* replacing a 2×
upscale, and not of new content appearing. That it *should* be the full-res art
was settled separately and against the running game, in
[`ui-8ax-fullres-background.md`](ui-8ax-fullres-background.md); this page only
supplies the name that lets the renderer find it.
**Nothing is covered.** The background is opaque and full-screen, and on the
menu it paints 4th of 16, so the worry is real. Measured at each element's
resting rect, before vs after: `ptbtn01` sd 59.98 → 60.03, `ptbtn03` 63.40 →
63.45, `ptbtn05` 66.54 → 66.58, `ptframe1` 51.16 → 51.20, `ptmsg` 56.11 → 56.12.
Everything survives; only the two `loop*` elements paint beneath it, and those
are excluded from the default composite anyway.
⚠️ **Both backgrounds are now drawn**`ptbase.t32` upscaled 2×, then the
full-res one opaquely over it. Correct output, wasted fill. The port should draw
only the full-res one, and take its *timing* from `ptbase`'s element, which is
the one carrying the keyframes.
## What is NOT decoded
**No declaration word points at the child.** Before reading the bytes I tested
whether the 60-byte element declaration indexes the T8aD child table. Its unread
words are `+28`, `+36`, `+44` and `+56`; the control is the resolved elements,
whose child index is known. On the main menu, of 13 controls the words reproduce
the child index **1, 0, 0 and 1** times — and both 1s are the trivial index-0
case. There is no pointer; the association is by name, and the name is the `opt `
string. [`decl_word_probe.rs`](../../../crates/sylpheed-formats/examples/decl_word_probe.rs)
✅ Incidental, from the same probe: **`+44` is a button ordinal.** It is `1…5` on
exactly the five `ptbtn0N.rat` elements of the main menu, in order, and `1` on
every other element. Not needed for anything open, and recorded rather than
chased.

View File

@@ -0,0 +1,120 @@
# `.tan` — one name over a run of frames, and the 60 "nameless" children it explains
**Status:**`DECODED`, with a disc-wide check. This closes the 🟡 reach caveat
left open by [`ratc-child-names.md`](ratc-child-names.md): *"60 children have no
`opt ` block within 128 bytes and still fall back to the scan — whether they
genuinely lack the block or sit past the search window is not established."*
**Neither.** They are not children. They are the **ten frames of a single `.tan`
resource**, and the one `opt ` block that names the whole run sits up to 545 KB
behind the last of them.
## What was measured
[`examples/ratc_optless_children.rs`](../../../crates/sylpheed-formats/examples/ratc_optless_children.rs)
re-runs `ratc::parse`'s own guards over every child on the disc and reports, for
each rejection, *which* guard fired and whether a tag exists further back
([data](../data/ratc-tan-frame-sequence.txt)):
| | |
|---|---|
| RATC children scanned | 18 002 |
| with no accepted `opt ` block | **60** |
| of those, rejected by **length**, **gap** or **charset** | **0** |
| of those, rejected because the only tag is **beyond the 128-byte window** | **60** |
| of those, that are child #0 of their bundle | **0** |
| archives involved | **1**`GP_READY_ROOM.pak` |
| bundles involved | **6** — entries 26, 30, 159, 160, 1029, 1050, each exactly 706 609 B |
| children involved | **1…10 of each**, never 0 and never 11+ |
The distances are the tell. Within one bundle they are
```text
213, 60 813, 121 413, 182 013, 242 613, 303 213, 363 813, 424 413, 485 013, 545 613
```
— an exact arithmetic progression, step **60 600**. Ten different children all
find the **same** `opt ` tag, because there is only one. Nine of them do not have
a block that is merely far away; they have no block.
## What they are
Reading bundle 26 directly, without the Rust parser, the 15 "children" resolve:
| # | offset | kind | size | `opt ` at | name |
|---|---|---|---|---|---|
| 0 | `0x000444` | T8aD | 1 933 | 40 | `pbf15_energie_generator2.t32` |
| **1…9** | `0x000bd1` … | T8aD | **60 600** each | 213 … 485 013 | **`pb_f15_eg_anm.tan`** |
| **10** | `0x085e49` | T8aD | **60 632** | 545 613 | **`pb_f15_eg_anm.tan`** |
| 11 | `0x094b21` | T8aD | 17 523 | 32 | `pbf15_pd_inside2.t32` |
| 12 | `0x098f94` | T8aD | 75 068 | 35 | `pbenergie_generator.t32` |
| 13 | `0x0ab4d0` | T8aD | 4 697 | 28 | `pbf15_eg_eff.t32` |
| 14 | `0x0ac729` | RATC | 264 | 25 | `pb_s15_eg.rat` |
So the format is doing something perfectly ordinary that our scan had no concept
of: **`.tan` is a frame sequence.** One `opt ` block declares the resource, and
its payload is a run of equal-size `T8aD` blocks, one per frame. The name is on
the disc and always was. What was missing was the idea that one name can cover
more than one block.
`anm` in `pb_f15_eg_anm` is the authors' own abbreviation, and it agrees.
## The disc-wide check
Every `opt ` block in every RATC bundle in all 33 `dat/*.pak`, by the extension
it names
([`tools/re-capture/ratc_opt_name_census.py`](../../../tools/re-capture/ratc_opt_name_census.py)):
| extension | count | what it is |
|---|---|---|
| `.t32` | 14 756 | a `T8aD` sprite |
| `.rat` | 3 311 | a nested RATC leaf |
| `.prm` | 367 | a primitive |
| `.tbm` | 224 | — |
| `.sbo` | 54 | — |
| **`.tan`** | **6** | **a frame sequence** |
| | **18 718** | |
A RATC bundle names exactly six kinds of resource, and **`.tan` occurs six times
on the whole disc** — all of them `pb_f15_eg_anm.tan`, one per language copy of
the same bundle, each holding **10 frames**.
**6 × 10 = 60.** That is the entire population of opt-less children, with nothing
left over. The negative is closed, not narrowed.
## ⚠️ What this says about `ratc::parse`
The child list **over-reports**. `parse` finds children by scanning for the four
child magics, so a `.tan`'s ten frames are listed as ten anonymous children of the
bundle rather than as one named resource with ten frames. The disc's "18 002
children" is therefore 18 002 *magic-delimited blocks*, of which 60 are frames.
**Not changed here**, deliberately: nothing in the menu milestone reads a `.tan`,
and a rewrite of the child model is a bigger change than the one fact it would
buy. Recorded so that a later consumer of `.tan` knows the shape it needs.
## ❔ Not established
* **The frame timing.** Ten frames of the same size is a sequence; nothing here
shows the rate, whether it loops, or whether the frames are equal-duration. No
field was looked for.
* **The pixel layout of a 60 600-byte frame.** They decode as `T8aD` like any
other sprite as far as the magic goes; their dimensions were not read.
* **What `.tbm` and `.sbo` are.** They surfaced from the same census and are
recorded above as counts only.
* **The two `opt ` totals do not reconcile exactly** and are not forced to.
This census counts **18 718** blocks; the Rust audit in
[`ratc-child-names.md`](ratc-child-names.md) counts **17 942** children *with* a
block. They apply different guards — the Rust one additionally requires the
named thing to be one of the four child magics and to follow within 8 bytes,
which `.prm` / `.tbm` / `.sbo` (645 blocks) never satisfy. That accounts for
most of the 776 difference but not all of it, and the remainder was not chased.
Each number is reported as what its own script measured.
## Scope
`GP_READY_ROOM.pak` is **out of scope** for the menu milestone ([S1 is a
no-go](../ready-room-probe.md)), and `.tan` occurs in no other archive. **None of
the five menu screens contains a `.tan`**, so nothing the port draws changes.
This closes a caveat on a decode the port *does* depend on, rather than adding a
capability.

View File

@@ -40,7 +40,7 @@ resolves all twenty-four:
| `stage\StageParameter_Test.tbl` | 1 — the developer stage |
**That is exactly the 24, and it explains 24 against the 29 stage records**
([`challenge-mission-gate.md`](challenge-mission-gate.md) counts 29:
([`challenge-mission-gate.md`](../challenge-mission-gate.md) counts 29:
16 story + 6 tutorial + 6 challenge + `Test`): the six tutorials do not get one
table each, they **share a single `_Tutorial` table**, and `_Test` accounts for
the last. 16 + 6 + 1 + 1 = 24.

Some files were not shown because too many files have changed in this diff Show More