Commit Graph

1163 Commits

Author SHA1 Message Date
sylph-decoder
55cdcac61b handoff: tell the port the settle-time numbers carry an unmeasured real-time factor
The pulse-period argument for trusting the settle-time run is withdrawn, and the
port authors from those numbers so it belongs on the page they read. The plate
delay survives because it agrees with three independent prior readings; the menu
build-in and B-to-title are anchored by nothing, so a few per cent of emulator
slowdown sits inside them undetected. That is a second, independent reason to
treat those two as provisional beyond their being one-run figures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:28:23 +00:00
sylph-decoder
02a99bc3e9 re: withdraw the pulse-period argument, and the shared WAV declares itself empty
Two corrections to my own recent work, both prompted by the port checking it.

First, the settle-time page argued the plate delay discrepancy was an instrument
artefact on two legs, and one of them is withdrawn. It said the plate pulse
period acts as an internal clock for presentation rate and measured 2.369 s
against the corpus s 2.3. That estimate rests on ONE interval between two
distinct troughs at a 125 ms sample interval -- uncertainty 0.177 s or 6.7
percent -- and trough-picking on a noisy plateau is fragile enough that
re-running it gives 2.628 s, because an adjacent local minimum had been counted
as a separate trough. Against the corpus s 2.24 that is +17.3 percent, about two
sigma. So the pulse period does not show the run at normal speed; it is too weak
to show anything, and cannot resolve a real-time factor below about 7 percent.

The conclusion survives on the other leg, which is the sound one: the
content-measured 2.247 s agrees with three independent prior readings
(2.13 / 2.132 / 2.138), and both its landmarks are sharp content transitions
rather than a trough on a plateau. A 17 percent slowdown would have put it at
2.49 s.

What that leaves open matters because the port authors from these numbers: the
run carries an unmeasured real-time factor under about 7 percent. The plate delay
is anchored by agreement with prior runs; the menu build-in and B-to-title are
anchored by nothing, so that is a second reason to treat them as provisional.

Second, the shared capture is worse than truncated: parec writes the WAV header
with zero sizes and patches them on clean exit, so the mid-write copy has RIFF
size 8 and data size 0 against 183 MB of actual bytes. Python s wave module
refuses to open it; ffmpeg and ffprobe recover by scanning and report a plausible
duration, which is exactly why it went unnoticed -- the lenient reader hid it.

Also corrects the attribution of the starvation numbers: 39.3 percent and
16680453 frames were measured on the finished local recording, not on the shared
artefact. The port measured the shared copy and got 35.6 percent and 15289876
frames, with burst and gap medians agreeing to 0.1 ms. The diagnosis is
unaffected but a number must say which artefact it came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:28:09 +00:00
sylph-decoder
5938d3b1fc notes: build-canary points at a source root that does not exist here
Recorded as a blocker rather than worked around, because it changes what the
next session can plan.

The faithful-capture route is an internal tap at SDLAudioDriver::SubmitFrame,
which receives exactly frame_size_ bytes of the guest s own frame in guest order
with no wall clock in the loop. A cvar-gated WAV writer there would record what
the guest PRODUCED rather than what a device CONSUMED, so it would be gap-free
however slowly the emulator runs -- which is precisely the defect that made both
ADV captures unusable.

The change is small. The build is not. build-canary builds
${PROJECT_DIR:-/work}/xenia-canary, which does not exist in this container; the
source is at /canary. The warm 235 MB tree at /sylph-home/re/canary-build is
configured with CMAKE_HOME_DIRECTORY=/work/xenia-canary, also missing, and its
build-Release.ninja carries no per-file rules -- it re-runs CMake first, and that
reconfigure fails on the absent root. So any Canary change is a full reconfigure
against /canary plus a full compile, at SYLPH_JOBS=4 on a box sitting at about
700 MB free with a documented history of full-parallel builds OOM-killing the
host.

Not attempted: that is a whole session s risk for one probe, and the next session
should decide with the cost in front of it rather than discover it halfway
through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:22:13 +00:00
sylph-decoder
3a5f215179 re: withdraw a wrong explanation of the BGM_001 duration gap
I wrote that BGM_001 s declared 173.821 s disagreed with a decoded 167.663 s,
and explained the gap as declared covering the encoded stream including trailing
silence while decoded is where the audio stops. The port decoded it fully: the
bank yields 173.809 s of PCM. There is no disagreement -- 167.663 s is where the
music fades out, measured from the audio, and the stream continues silent to its
declared end inside that same decode.

So the declared-rate method is better than this page claimed, and is now
cross-checked on three banks against independent decodes: BGM_103 87.750 vs
87.744, BGM_102 37.487 vs 37.482, BGM_001 173.821 vs 173.809 -- agreement 5 to
12 ms.

The conclusion survives unchanged and is the useful half: trust it for lengths,
not for musical boundaries. A declared length includes whatever silence the
encode carries, so it is not a loop point.

Also records in METHOD a defect shape the port hit three times in one pipeline,
each invisible to every check except a level: normalising by how many inputs
there are rather than how many carry signal. A silent chunk in a voice sum, a
silent channel in a mono fold, and a silent sub-wave -- the 10240-byte bank
header wrapped to 10300 B -- counted as a third stem in a music sum, which put
every real stem at 1/3 instead of 1/2 and cost 3.52 dB on all menu music for two
iterations. This corpus s census said two waves and the exporter s divisor said
three; the count that disagrees with a census is the one that is wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:20:49 +00:00
sylph-decoder
c05c8e1719 re: take 2 is also unusable -- the sink is starved, 39.3 percent digital silence
The port could not find the movie bed or the cutscene voice in take 2 either,
this time with a correlator they had rebuilt and calibrated in both directions
after retracting the first one. Their negative stands. They named two readings:
the capture path is still losing the guest s mix, or the guest is not emitting
these sources -- and flagged the second as landing on them hard, because if the
game never plays the .wmv s WMA track the port s intro audio has been wrong
since P4.

It is the first, and take 2 says so on its face:

  digital silence on all six channels   6557892 / 16680453 = 39.3 percent
  non-silent runs   10595, median 13.60 ms, longest 1.19 s
  silent runs       10596, median 3.94 ms
  burst+gap period  about 17.5 ms, 57 Hz, duty cycle 60.7 percent

The recording is chopped into 13 ms fragments separated by 4 ms holes, ten
thousand times over -- a starved sink, PulseAudio filling underruns with silence.
That destroys envelope correlation by construction, since the envelope is
dominated by a 57 Hz chop unrelated to the content. The file s strongest
periodicity is 5.2 s rather than BGM_102 s 37.487 s loop; the estimator was
controlled first, recovering a synthetic 37.487 s loop as 37.480 and scoring
non-repeating noise at 0.019.

So the port s alarming hypothesis is NOT supported. Nothing here says the game
fails to play the movie s audio; it says this capture cannot answer either way.

A monitor sink cannot fix it: parec reads a monitor that advances at wall clock
and substitutes silence, so every moment the emulator runs below real time is a
hole and the timebase is warped non-uniformly. The route that would work is an
internal tap at SDLAudioDriver::SubmitFrame, which sees every frame the guest
produces in guest order with no wall clock -- same shape as xma_param_probe,
additive and default-off. Not attempted this iteration.

Also corrects a provenance number I got wrong: I told the port take 2 was 253.3 s
when the shared file is 318.5 and the full recording 349. I read ffprobe while
the recorder was still writing and copied the file before it finished, so the
shared artefact is itself a truncation. Corrected provenance: movie 10-251, title
262-318, back to movie at 329 -- meaning the file includes the title screen,
contrary to what I told them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:19:46 +00:00
sylph-decoder
b07f8984e3 handoff: deliver the BGM_102 identification and the header-derived durations
States the identification, the caution that it does NOT establish which screen
BGM_102 belongs to, and the one thing the port can use today: BGM durations from
the corrected XMA1 PsuedoBytesPerSec with no decoder. Flags that those durations
include trailing silence -- BGM_001 reads 173.821 declared against 167.663
decoded, a gap matching the 6.15 s of silence this page already records -- so a
menu loop point must use the decoded figure.

Also records that a refutation attempt on this page s own BGM_103 wave sizes
failed: both match the disc exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:16:34 +00:00
sylph-decoder
05d278173d re: the two unexplained XMA streams are BGM_102, and BGM_103 s sizes survive
Closes the open question left by the take-2 audio capture, where the probe
logged five streams on one boot when only ADV s three were accounted for.

Both unexplained sizes are whole packet counts, 562 and 620. Searching every
inter-descriptor span of the voice stream and every sound.pak entry large enough
finds zero hits in the voice stream and ONE entry carrying both -- hash
9799c546, which candidate enumeration recovers as BGM_102.slb, two streams of
1150976 and 1269760 B. One entry holding both sizes is the two-stem shape rather
than two coincidental matches. So the boot s five streams were ADV s three voice
streams plus one music bank s two stems, and nothing is unaccounted for.

What it does not establish is which screen it belongs to. The window ran from
launch to t=253 s with the title arriving at 262, so BGM_102 was decoded
somewhere inside a launch-to-just-before-title window -- but the probe fires on
first decode and its lines carry a thread id rather than a timestamp, so a title
BGM decoded moments before the title appears is equally consistent with the
evidence. Cue 1103 is already the main menu, which makes 1102 as the title at
least suggestive. The settling experiment is written down and not done.

Refutation attempt on HANDOFF s BGM_103 wave sizes: exact match on both
(3876864 / 3930112). The claim survives unchanged.

Also a third route to two-stems-of-identical-duration, from the XMA1 header
alone now that PsuedoBytesPerSec is read correctly: BGM_102 37.487/37.487,
BGM_103 87.750/87.749, BGM_001 173.821/173.821. The one apparent disagreement
resolves in the corpus s favour -- BGM_001 reads 173.821 here against the port s
decoded 167.663, a gap of 6.158 s, and HANDOFF already records 6.15 s of trailing
silence after its fade-out. Declared duration covers the encoded stream including
that silence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:16:13 +00:00
sylph-decoder
b047788d37 re: the capture recipe that passes, and a five-stream observation it turned up
Take 2 of the ADV audio capture verified with the port s independent
tools/port/check-capture -- six distinct channel MD5s, PASS -- run before
sharing and deliberately using their tool rather than the hand that made the
file.

Records the recipe: sink channel_map set equal to Canary s own stream map and
the same map passed to parec, so PulseAudio does no remapping; both of
run-canary s mutes off; recorder started before the emulator so WAV t=0 precedes
process launch; and a screenshot every ~11 s keyed to the recording s own clock.

That last pair is what makes it self-checking, and both were the port s asks.
Classified against the committed references, this run reads movie/other for
t=10..251 and then title_noplate at t=262 (r=+0.998) and title_plate at 277/289
-- so the 253 s of audio sits wholly inside the movie with the title arriving
just after. A miss is now diagnosable instead of ambiguous, which is the whole
difference from take 1.

Still the full mix: movie WMA bed plus voice, nothing at this boundary separates
them.

Also records something unexplained that the run turned up: the probe logged FIVE
distinct XMA byte_size values, not three -- ADV s 1294336 / 1118208 / 1171456
plus 1150976 and 1269760. The extra pair belongs to some other cue, is not
BGM_103 s two waves, and a pair is the shape bgm-two-stems documents for music
banks. Untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:10:28 +00:00
sylph-decoder
930ab1016d re: a per-channel level check is blind to remap corruption, by construction
The port made this point while building a checker for the channel-map trap, and
it refutes a sentence in my own write-up.

In the known-bad control all six channels report a peak of -18.063656 dB,
identical to six decimals, while the file contains three duplicate pairs. Equal
tone amplitudes make the peak table uniform however the channels are permuted or
duplicated; on real content the peaks simply differ from one another, which looks
equally healthy. The table is uninformative either way.

So "the WAV has plausible per-channel levels" was not weak evidence that a
capture was sound, it was none, and this page implied otherwise. The per-channel
peak table is the natural thing to eyeball after a capture and it cannot see this
failure at all. What detects it is hashing each channel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:08:10 +00:00
sylph-decoder
e2c599fa72 re: a 6-channel capture scrambles and duplicates channels unless the maps match
The ADV audio capture I shared with the port is withdrawn as evidence. They
could not match it against anything -- the movie bed, any of the three voice
streams, BGM_103, S00A -- with best-vs-runner-up margins of 0.001 to 0.016
everywhere, and they controlled that three ways before saying so. They also
noticed capture channels 3 and 6 were byte-identical.

That duplicate pair reproduces without the emulator, and it is my capture chain.
Six channels each carrying a different tone, played to the null sink and
recorded from its monitor with the same parec invocation:

  sink map NOT matching the client (the original setup)
    expected  400  800  200 1600 3200 6400
    captured  400 3200  200  800  800  200     ch2 == ch5 byte-identical

  sink map made identical to Canary s stream map, and passed to parec too
    captured  400  800  200 1600 3200 6400     no duplicates -- CONTROL PASSED

PulseAudio remaps when the maps differ, and a 6-channel remap silently drops
channels and duplicates others. No error, no warning; the WAV has the right
length, channel count and plausible per-channel levels.

Withdrawn with it: "all six channels carry signal", and the observation that
non-zero surround and LFE weakly supported the 5.1 reading of a voice cue s
three streams. The port said a duplicated channel is not an independent one and
they were right before this control existed.

Unaffected: the three-XMA-context concurrency result, which is read from the
emulator s own log rather than the audio path, on two independent boots.

The control needed no emulator, no disc and thirty seconds. It was not run, an
artefact was published, and the person who found the defect was the one who
could not see the instrument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:04:52 +00:00
sylph-decoder
105a1f65e4 notes: run-canary is silent TWICE over, and the 6-channel stream is a red herring
Completes an entry committed an hour ago that was incomplete, which is worse
than absent because it looked authoritative. Fixing SDL_AUDIODRIVER alone still
records silence: run-canary also passes --mute=true on its own command line
(line 98). With the driver fixed and the mute left alone, Canary attaches a
healthy 6-channel stream, holds it at 100 percent volume, reports Corked: no,
and emits nothing. Both layers have to go, and "$@" is last so --mute=false on
the caller s side wins.

Also records that parec defaults to stereo/44.1 kHz and will resample a
6-channel monitor without saying so -- the first successful-looking capture came
back 2ch 44100 from a 6ch sink.

And a red herring I nearly published as a finding. pactl shows Canary s stream
as float32le 6ch 48000Hz with a full 5.1 channel map, which reads as the guest
requesting 5.1 and would have been strong support for the hypothesis that a
voice cue s three streams are 5.1 channel pairs. It is not evidence about the
game at all: AudioDriver::kFrameChannelsDefault is a hardcoded 6, and the code
path actually used, SDLAudioSystem::CreateDriver(index, semaphore, &driver),
constructs SDLAudioDriver(semaphore) taking every default. The format is
Xenia s; only the content of those six channels is the guest s.

That is the same failure this corpus recorded in METHOD earlier today -- the
specific observation and the general rule reading identically -- caught this
time before it was written down rather than after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:56:24 +00:00
sylph-decoder
a8a828644a notes: run-canary is silent by default, which records perfect silence
Both of these cost time in one session and both are the kind that look like
success.

run-canary line 82 is SDL_AUDIODRIVER=${SDL_AUDIODRIVER:-dummy}, and its own
header explains why: --apu=nop stalls the guest in the intro movie, so the SDL
driver against a dummy device is what lets the title advance. But the comment s
premise -- "there is no PulseAudio here" -- stopped being true when
tools/audio-capture landed, since that starts a daemon on demand. So a capture
through the null sink records pure silence, of the right length, behind a run
that looks perfectly healthy. The override is
PULSE_SINK=cap SDL_AUDIODRIVER=pulseaudio run-canary, and the live check is
pactl list sink-inputs: empty means Canary never attached and the sink sits at
IDLE. audio-capture s own -inf peak warning is the backstop, but it only fires
after the whole run.

Separately, pkill -f and pgrep -f match the caller s OWN command line. Hit twice
here: pkill -9 -f adv_audio_cap.sh killed the shell running it, and an
until ! pgrep -f "probe.py --run" loop never exited because the loop s own
command line contained the pattern -- which looks exactly like the job hanging.
Kill by process name with ps -o pid= -C instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:52:05 +00:00
sylph-decoder
4362ea21b7 handoff: deliver the concurrent-streams refutation to the page the port reads
The finding landed in docs/re/ in the previous commit; an answer not reachable
from HANDOFF is not delivered. States plainly that "take one stream" was mine,
that the port implemented it, and that it is withdrawn -- together with the
caution that summing is not thereby right, because an equal-gain 1/n sum of
channel pairs is not a downmix and the port s measured 6.02 dB loss was real.
Neither rule is established, so the manifest should say the value is authored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:49:32 +00:00
sylph-decoder
062e17bf34 re: the three streams of a voice cue are decoded CONCURRENTLY, not alternatives
Refutes a framing of mine that two documents and the port s exporter were built
on, so it is a new page rather than an edit.

voice-region-leading-chunk.md read a long cue s three streams as three
presentations of one take, and from that came the instruction "take one stream,
do not sum", which the port implemented. The open question was which
presentation the game plays.

It has no answer. Booted with --xma_param_probe=true, the cvar whose own comment
says it is keyed to reveal which sub-wave of a movie s .slb the game actually
decodes. The guest opens three XMA contexts and decodes all three concurrently:

  ctx=0  packets=632  byte_size=1294336  ch=2  48000   ADV stream 1
  ctx=1  packets=546  byte_size=1118208  ch=2  48000   ADV stream 2
  ctx=2  packets=572  byte_size=1171456  ch=2  48000   ADV stream 3

Byte-exact against the three streams payloads taken independently off the disc
(RIFF size minus 60). Only these three contexts appear in the run.

So a consumer that picks one discards two thirds of what the game mixes. Both
"three presentations of one take" and "take one stream" are withdrawn -- and the
previous behaviour is not thereby right either, because an equal-gain 1/n sum of
channel pairs is not a downmix and the port s measured 6.02 dB loss was real.
Neither rule is established; a consumer is authoring.

Three concurrent stereo streams is six channels and N stereo streams is how XMA
carries multichannel on the 360, which would also explain the 1-or-3-never-2
span census. Recorded as a hypothesis, not a result: all three fmt chunks
declare ChannelMask 0x0002 identically, which is not what distinct channel roles
should look like.

Everything byte-level survives: the leading chunk being stream 1 clipped by our
own guard, the 70 + 8 + 17 decomposition, the bank-header discriminator.

Reach: one cue, one boot. That 28 cues are 3-stream is decoded; that all three
decode concurrently is measured on ADV alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:48:35 +00:00
sylph-decoder
a98a66190e formats: XMA1 is not a WAVEFORMATEX -- audio info was reading three wrong fields
parse_riff_wave read every fmt chunk as a WAVEFORMATEX. XMA1 (tag 0x0165) is
not one, so audio info reported the disc s movie voices as 16 channels,
4310 Hz, 2-bit: 16 is wBitsPerSample read as a channel count and 4310 is
wEncodeOptions (0x10d6) read as a sample rate. This misled me earlier in the
session and I recorded it as a limitation before finding the cause.

XMA1 carries XMAWAVEFORMAT followed by one XMASTREAMFORMAT per stream. The
reader now branches on the tag and reads bits at +2, PsuedoBytesPerSec at +12,
SampleRate at +16 and Channels at +29. The same three files now report 2
channels, 48000 Hz, 16-bit.

The consequence worth having: this crate has no XMA decoder, and
data_bytes / PsuedoBytesPerSec is the only route to a duration. Checked against
durations decoded independently by the port:

  ADV presentation 1   137.34 s declared   137.324 s decoded   +0.012 percent
  ADV presentation 2   137.33 s declared   137.324 s decoded   +0.004 percent
  S00A presentation 1   93.71 s declared    93.694 s decoded   +0.017 percent

So the corpus can now get XMA1 durations off the disc without a decoder, which
is a capability I had written down as absent. It is a declared rate rather than
a measurement of the samples, and the CLI labels it as such.

Regression test pins the real on-disc header bytes and asserts the duration
against the independently decoded 137.324 s. 115 lib tests and 3 media disc
tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:42:52 +00:00
sylph-decoder
9f34829776 method: the specific observation and the general rule read identically
Five corrections across two agents in two days share one shape, and it is worth
naming as a family rather than fixing one at a time. None was carelessness about
the measurement -- every underlying observation was true of the asset actually
looked at. The failure is reaching for the general form in the same breath as the
specific one, where the two are indistinguishable on the page and the general one
is what the next reader uses.

Three were the port s and two were mine, and the entry names both sides:

  the two chunks are two stems of one performance -- true of a music bank,
    written as a fact about voice, where one of the two is digital silence
  the extra bytes are a duplicated channel, not fidelity -- true of ADV, and the
    size ratio it implies runs 0.0778 to 2.9163 across the disc
  everything the sequencer paces off rest.t is late -- true of the title, and
    false of the screens actually checked
  a three-stream cue is a movie cue -- mine, and BIRD_224 is neither
  take the highest-rate, highest-gain stream -- mine, and on ADV those two
    criteria select different streams

The counter is the same every time: run the census before writing the rule.
Where the census cannot be run, write the specific sentence and say it is
specific.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:40:11 +00:00
sylph-decoder
b82f75979e re: the dual-mono explanation for the extra stream bytes does not generalise
The port chose a voice presentation on the argument that ADV chunk 1 is
mono-in-stereo and chunk 2 is dual-mono, so chunk 2 s extra bytes encode a
duplicated channel rather than fidelity -- which would explain its higher
declared PsuedoBytesPerSec without appealing to encode quality.

Their ADV channel measurement stands. The generalisation does not. If stream 3
were systematically the same take with its channel duplicated, its size ratio to
stream 2 would be tight across the 28 three-stream cues. Measured:

  min 0.0778 (S00A, the silent one)
  median 1.2565
  max 2.9163 (S06A)
  sd 0.5057
  within 15 percent of 1.0: 12 of 28

A 37x spread is not a duplicated channel, and the declared rates scatter with
them -- S06A is 5661 against 16513 B/s. Whatever distinguishes the three streams
varies per cue rather than being a fixed channel-configuration triple.

This does not touch the port s decision, which is to take the loudest
presentation: that is a per-asset content measurement, not a structural rule, so
a scattering ratio cannot undermine it. It touches the explanation, which should
not harden into a fact about the format.

Two curiosities recorded: S12B s three streams are byte-size identical at 14396
each, and BIRD_224 is 3-stream while being a non-movie cue, so the shape is not
exclusive to cutscenes.

Also narrows the settle-time page s own generalisation. The port measured its
boot the way this corpus measured the game and found the sequencer NOT late --
its 0.6 s discrepancy was arrival-to-arrival timestamps compared against visible
spans, the plate-delay trap in a second place. So what is supported is that
rest.t is the wrong landmark for the TITLE, not that everything paced off it is
late. And the offered re-take of the one-run menu figures is recorded as
declined, with the reason, rather than left looking unfinished.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:37:46 +00:00
sylph-decoder
63573d2b50 re: settle_time measured -- the title is drawn at 2 s, not at rest.t s 4.18
The port s boot sequencer paces every screen off rest.t, which is the last hold
keyframe rather than when a screen arrives. Measured on one cold boot: the
container had no Xenia storage root at all, so this is a fresh profile with no
shader cache, the slowest case.

  title build-in (first ink -> art fully drawn)   0.23 s
  title settled -> PRESS A plate on              2.247 s   (disc declares 120 units)
  plate pulse period                            ~2.37 s
  main menu build-in                             0.531 s
  B -> title                                     0.482 s
  A -> menu                                      3.763 s   DO NOT AUTHOR, see below

The title s rest.t is 251 units = 4.183 s and its art is finished at about 2 s,
so a sequencer pacing off rest.t holds it roughly twice as long as the game does.

Instrument controlled before the run: 9/9 on the content classifier including
the movie-frame and difficulty-screen negatives, 4/4 on the plate detector; the
run sampled 7.99 fps against a requested 8 with an independent one-shot grab
cross-checking every 20 s.

Records a refutation attempt of mine that FAILED. The probe s own marks gave a
plate delay of 3.203 s against the corpus s 2.13 s, which on a cold-cache boot
looked like a real effect. It was the instrument: the plate pulse period is an
internal clock for presentation rate and measures 2.369 s here against the
corpus s 2.3, so the run is not slowed, and re-measuring from content gives
2.247 s. The probe s title_static mark fires during the crossfade out of the
attract movie, before the wordmark has drawn -- glyph was still 0 when it fired.

Also a third independent reproduction of the A-path load stall: 13 frames,
1.53 s, surface mean 26.631 against the earlier 14/1.53 and 12/1.39 at 26.626.
This boot had no shader cache, so it is not a warm-cache artefact. Noted that
the earlier pair agreed to six decimals and mine agrees to three.

Reach: one run. The menu build-in and B->title rest on it alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:21:49 +00:00
sylph-decoder
6011512334 method: the plate glyph counter false-positives on the attract movie by 13x
Found while controlling the timing probe before a settle_time run, not by
reasoning about it.

title_timing_probe.py s plate detector thresholds a green-glyph pixel count at
400, and its control checks two committed movie frames that both score 0. A real
boot disagrees: in one 100 s attract window, 17 frames scored at or above 400
and the peak was 5393. The attract movie has green content in the plate region.

The probe is not wrong -- its state machine refuses to look at the glyph until
the content classifier has already labelled the frame title_noplate or
title_plate, so the false positives never reach the drive. But it is safe
because of that gate, not because the threshold discriminates, and the
distinction matters for anyone reusing glyph() on its own.

Recorded with the general form: a two-frame control over a three-and-a-half
minute movie is not a control over that movie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:17:08 +00:00
sylph-decoder
cf3b60180e re: withdraw a stream-selection recommendation that contradicted itself
The port implemented "take the highest-rate stream" and reported that on ADV it
selects the QUIETER presentation -- chunk 2, 1171516 B at -8.3 dBFS, over
chunk 1, 1118268 B at 0.0. They were right to flag it rather than accept it.

My sentence was "the highest-rate, highest-gain one is chunk 1". Those two
criteria do not select the same stream and the sentence should never have joined
them; the parenthetical named chunk 1 while the rule named chunk 2. Withdrawn.

What the header does decode, read off the bytes: the fmt chunk is a 32-byte
XMAWAVEFORMAT, little-endian, and +0x20 is a declared PsuedoBytesPerSec -- 8142
and 8530 on ADV s two presentations, agreeing with the computed rates to 0.02
percent, with 48000 Hz at +0x24. So the rate is decoded rather than inferred.

What it does not decode: wEncodeOptions (0x10d6), channel count and channel mask
are byte-identical across the presentations. Nothing in the header ranks them,
so stream selection stays an authored choice and the port must know it is
authoring. Settleable in one emulator run -- a capture of the intro with the
dialogue audible says which level the game plays -- and not yet done.

Also records that sylpheed-cli audio info is misaligned for XMA1: its "16
channels / 4310 Hz / 2-bit" is wBitsPerSample, wEncodeOptions and the channel
fields read at the wrong offsets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:13:20 +00:00
sylph-decoder
3f945808d6 re: a long voice cue is three presentations of one take, and our guard clips the first
Closes the last open question on the voice regions: why one cue s byte span
decodes to ~2.6x the movie s length.

The port measured, with controls including a cross-movie negative, that a
region s leading chunk is the TAIL of the full-length chunk that follows it --
r = 0.998 at a lag that puts it flush against that chunk s end, residual 16.7 dB
down over 84.5 s. They withdrew their own earlier 0.768, which came from a
search that scored best on the boundary of its own lag range.

Checked it here by an independent route that needs no decoder. If the leading
chunk is the tail of a full-length first stream, the whole leading stream should
be one complete take of chunk 1 s duration. For ADV: 504464 + 808304 = 1312768 B
at chunk 0 s byte rate of 9559.7 B/s is 137.323 s, against chunk 1 s measured
137.324 s. One millisecond over 137 seconds, from byte rates rather than from
envelope correlation.

And the byte structure settles the shape disc-wide. Counting stream starts inside
every inter-descriptor span: 258 hold exactly 1 stream, 28 hold exactly 3, and
nothing holds 2 or any other number. All 20 spans over 1.5 MB are 3-stream. The
95 movie regions decompose 70 + 8 + 17, and the 8 are independently the same 8
the first census found as bank-header-with-3-chunks.

So 359 s = 84.55 + 137.32 + 137.32: three presentations of one take, the first
clipped by resolve_movie_voice_region s own 1.5 MB guard.

Consequences recorded for the port: dropping the leading chunk is removing a
duplicate rather than truncating, so the hedge is lifted; but summing chunk 1
and chunk 2 is wrong, because they are the same take at different gain, not two
stems. Take one stream.

Also flags a coincidence I nearly built on: the 504464 B constant is structural,
not proportional -- ADV s proportional prediction lands within 8 bytes of it and
S00A s is 4305 B out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:09:40 +00:00
sylph-decoder
432fb7450b re: the voice region s leading chunk is the movie s OWN dialogue, and a guard puts it there
My own leading hypothesis -- that the leading chunk is an in-mission VOICE_D_*
line -- is refuted, on the route the port suggested: widen the enumeration past
the 95 manifest-bound movies and the byte-span test settles it without anyone
listening.

Scanning the stream for every trailer descriptor (the (id, 0x11) pair whose id
repeats at +0x800) gives the complete cue partition, mission lines included:
287 descriptors in a 116.2 MB window, all 287 carrying an id the 4280-name
registry names. Every one of the 17 leading spans is bracketed by
desc(N-1)..desc(N) where desc(N) is that movie s OWN cue id. Zero mission lines.

The mechanism is a guard in our own resolver. resolve_movie_voice_region takes
the predecessor trailer as the region start, guards it with
end - start < 1_500_000, and falls back to the .slb TOC anchor when that fails.
Cues with a true span over the guard: 17, of which 17 are stream-opening. Cues
under it: 78, of which 0. Perfect discrimination both ways. The anchor sits a
constant 504464 B after the true predecessor trailer on all 17, which is
unexplained.

Not established, and stated as such: this does NOT mean the export truncates N
seconds. The port s decode already has ADV s region at 359 s against a 137 s
movie, so it over-covers and the byte-to-time mapping is not linear. No XMA1
decoder in this container to check.

Also withdraws a claim this page had adopted from the port -- that chunks 1 and
2 are two stems of one performance. The port refuted its own claim by decoding:
S00A chunk 2 is digital silence, ADV chunk 2 is 0.60x chunk 1 with the residual
26.8 dB down. Equal duration was a shape match and Q10 s music census should not
have been carried across to voice on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:03:58 +00:00
sylph-decoder
de2fe4a110 re: the voice-region third chunk is a different structure from the BGM one
The port hit a 2+1 chunk signature on a resolved movie-voice region and asked
whether the bank-header explanation that closed HANDOFF Q10 also covers it,
rather than assuming it. It does not, and the discriminator is mechanical.

Disc-wide over the 95 English movie-voice regions the manifest binds:

  78 open with a bank header -- bank_header_len fires, 10240 B = 5 packets
     exactly, every time. That is the BGM case.
  17 open with a leading headerless stream -- bank_header_len is None, and all
     17 have length congruent to 1392 mod 2048, the disc s own derived data
     offset. No other residue occurs.
   0 begin at a RIFF.

Counting chunks does not discriminate: 8 bank-header regions also yield three
chunks. slb.rs already predicted this in its own doc comment -- the header
signature has "zero false positives on the 7993 mid-bank windows, where the
leading region IS real" -- and a voice region is a mid-bank window by
construction.

Also tested the obvious defence of dropping the leading chunk, that it is the
predecessor cue s audio: 0 of 17 leading spans lie inside any other resolved
region, 0.0 percent on every one. The test finds overlaps where they exist (16
overlapping pairs among the regions, 60 exactly-adjacent boundaries, 73 of 78
bank-header regions starting where another ends), so the zero is not the
instrument.

Left open, with reach: the census covers movie-voice regions only, and the same
stream carries the in-mission VOICE_D_* cues, which are not enumerated -- the
leading bytes plausibly belong to one of those. Could not be settled by
listening: no XMA1 decoder in this container, and sylpheed-cli audio info
reports these chunks as 16 channels / 4310 Hz / 2-bit, which is visibly wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:56:17 +00:00
sylph-decoder
3e7a258a9a re: a cross-reference kept recommending a route the cited page had killed
`menu-navigation-semantics.md` advertised "the cheap way to finish this":
read 0x828A690C as a live screen id and 0x828F38AC as the cursor, under
--gpu=null with no screenshots. `menu-state-in-memory.md` withdrew exactly that
identity ON THE SAME DAY it was published -- three back presses send the
"cursor" 36 -> 38 -> 40 -> 41, and a cursor returns when you go back. They are
monotonic counters; the cross-run agreement is the same key sequence producing
the same count.

The recommendation stood for three days after the page it cited had killed it,
and either document would have been believed on its own. Marked withdrawn where
it was recommended, with what the words ARE still good for (did the game react?)
and the consequence: a measured button->GamePart-id binding stays unfinished
because no screen enum has been located.

Also delivers Q4 to HANDOFF in the shape the port asked for -- exactly one
main-menu button opens a GP_TITLE entry (EXTRAS -> entry 6/9); the other four
leave the archive. That was measured on 2026-08-28 and was reachable only from
docs/re/, which the protocol counts as undelivered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:51:15 +00:00
sylph-decoder
b40f18eeb6 re: a refutation attempt that failed -- EXTRAS really is a three-button screen
The port reported GP_TITLE entries 6/9 as a three-button submenu. This page and
HANDOFF call 6/9 EXTRAS and never recorded a button count, and 18 elements
looked like too many for three buttons, so I challenged it -- from the count,
without listing the elements.

Wrong. `screen info --all --build 6` shows ptbtn11/12/13 among fifteen frame,
title, background and effect layers, and entry 9 is identical. Both things are
true: 6/9 are EXTRAS (our composite correlates +0.944 whole-frame with the
committed live-extras.png) and EXTRAS is a three-button screen. The port
established the button count; the corpus did not have it.

Recorded per the adversarial duty, which is worth nothing if only the successful
challenges get written down. The retraction has gone to the port as well.

Also checked in the same pass, and it constrains Q2: entries 5 and 8 have
identical element lists and identical button placements, as do 6 and 9. The
EN/JP difference lives in the baked sprite pixels, so no layout field will ever
separate the members of either pair -- that needs the sprite images or a
capture, and the "English is the first half of the data segment" rule stays a
heuristic rather than something a field will replace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:48:46 +00:00
sylph-decoder
fb0755d190 handoff: say which index space a build number is in -- 10/11 meant two different screens
The port challenged HANDOFF's loading-screen row and was right. The Q2 row said
"0/1 and 10/11 are the LOADING screen"; the dated section above it says
"entries 0, 1, 12, 15". Both are true, in different index spaces, and the page
did not say which.

Verified against the bytes rather than the table:

  screen list       GP_TITLE.pak -> 12 builds, ordinals 0..11
  screen list --all GP_TITLE.pak -> 16 builds, ordinals 0..15

Only under --all does the ordinal equal the pak entry. Without it ordinal 10 is
entry 12 and ordinal 11 is entry 15. `screen info --all --build 10` shows
palogo_sqex; --build 11 shows palogo_gamearts / seta / anima; 12 and 15 show
pgloading_*. So in ENTRY space 10/11 are the publisher and developer splashes,
which is exactly the screen the wrong reading would have renamed.

It would have validated silently: the port's screen_names.json is keyed by
entry. Q2 row corrected to entry space and marked; the trap is in METHOD under
"Mechanics that have bitten", with the rule that a number leaving this
repository says "entry N", never "build N".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:47:30 +00:00
sylph-decoder
884822ebe3 re: the paint-order tie-break costs the port zero pixels, not 24 pairs
The census bounded WHERE a wrong tie-break could show and said outright that
nobody had measured how many of those pairs change a pixel. Measured.

`compose_with_order` renders a bundle in a caller-supplied order; the new
example renders each screen twice, once derived and once with one tied pair
swapped, and diffs. Same-key elements are contiguous under a stable sort on
(key, i), so a swap paints nothing else in between.

Controlled per entry: swapping an OVERLAPPING pair with DIFFERENT keys moves
36 305 to 771 479 px (max delta 254). Where no such pair is drawn the output
says so rather than reporting an uninterpretable zero.

* EXTRAS (entries 6/9) and the main menu (5/8): 0 px. The tied ptframe pairs
  ink ~3 600 px each and share NONE of them -- the 102x132 rect overlap was an
  artefact of approximating an element as pivot x 2. Blend-independent.
* Across all 31 drawable overlapping tied pairs in GP_TITLE, the largest change
  any of them makes to any channel is 3/255.
* Withdrawn: "a wrong tie-break can be wrong by a whole layer". That rested on
  ptlogo_back2eff5 geometrically containing two other glows. Rendered, the swap
  moves 6 390 px by max delta 2. Containment is not occlusion when the container
  is a near-transparent glow, and nobody had rendered it before asserting it.

Reach: this measures our compositor's sensitivity to order, not the game's. The
zero-shared-ink results hold under any per-pixel blend; the delta<=3 figures
assume ours.

13 disc-gated ui_paint_order_disc tests and 114 lib tests pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:47:19 +00:00
sylph-decoder
a0eb509cec Merge remote-tracking branch 'origin/main' into auto/no-disc-and-menu-captures 2026-08-29 14:34:10 +00:00
MechaCat02
0cceaaf4a9 containers: an expired token could never be replaced
Credentials were seeded only when the container's copy was MISSING. So when a
session expired, the file still existed, the copy was skipped, and restarting
changed nothing -- the one recovery path a human has, re-logging in on the host,
could not reach the containers at all.

Now re-seeds whenever the host's copy is newer. Newer-wins rather than
always-copy, because a container refreshes its own token mid-run and that copy
may legitimately be the fresher of the two.

Found when both sessions expired: host credentials at 16:30, containers holding
14:20 and 14:24.
2026-08-29 16:31:53 +02:00
MechaCat02
002df51173 viewer: show where a cutscene's voice actually is, and let you hear it
The Cutscenes window printed the voice token as text and offered no way to play
it, which left the most confusing thing on the disc invisible.

The movie voices are one continuous XMA stream chunked into VOICE_*.slb entries
whose boundaries do NOT match the cutscene cues, so the bank named after a movie
need not hold that movie's audio. Measured, on the retail disc:

  ADV     region 433930240..437044592  inside VOICE_ADV.slb        name honest
  S00A    region 452798464..455499120  inside VOICE_S00A.slb       name honest
  RT01A   region 437044592..437345648  inside VOICE_ADV.slb        NAME LIES

RT01A's voice sits in bytes belonging to the entry named after the intro movie.
A viewer that played the name-matched bank would be confidently wrong for
exactly the cutscenes where it matters, and would look right on the two that are
easiest to check.

So the window now shows BOTH locations -- the named bank with its byte range,
and the resolved region -- and states plainly whether the name is honest,
highlighting it when it is not. Play routes through the movie form of
RequestAudio, which resolves the region rather than reading the bank.

Static data only: sound.pak and tables.pak, both on the disc.
2026-08-29 16:21:34 +02:00
sylph-decoder
6c05b72099 re: the paint-order tie-break costs 24 overlapping pairs, not one element
The port challenged HANDOFF's "costs one element's blend on one screen" with a
census of 105 elements sharing a layer key across 12 of 16 screens. The two
numbers count different things -- elements vs overlapping pairs -- so it is not
the contradiction it looked like, but the objection stands and the line was
wrong.

paint_order_audit already reports overlapping ties per entry, and over all 16
GP_TITLE entries: 5 use a measured order and carry no tie risk; of the 11 that
fall back to the derived order, 7 have overlapping ties, 24 pairs in total. The
Japanese title (entry 7) alone has 16, because it is the twin of the one build
whose measured order exists and has none of its own.

Overlap bounds where a wrong tie-break COULD show, not what it costs; nobody has
measured how many of the 24 change a pixel, and the port is right about that too.

The rule itself is unaffected: the layer key is still decoded and the derived
order still reproduces every measured order exactly except the title's eight
tied pairs.

Census committed at docs/re/data/paint-order-ties-gp_title.txt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsxw1A9JseUw99Yw1ZRQzY
2026-08-29 14:17:40 +00:00
sylph-decoder
406517cc68 handoff: address the loading bundles by pak entry, not by is_build ordinal
The port caught this before writing a name: over the twelve bundles is_build
accepts, ordinals 10/11 are entries 12/15, while ENTRIES 10/11 are the two
splash bundles. A key written from an ordinal names the publisher wordmark as a
loading screen and still validates. Restated in entry space throughout.
2026-08-29 14:15:50 +00:00
sylph-decoder
fe49c70da1 re: the boot shows the publisher first -- the TSV that says otherwise attached late
The port flagged docs/re/data/boot-timeline-2026-08-29.tsv, whose label column
runs splash_dev before splash_pub, as a possible boot-order bug in its tree.

Three cold boots, t=0 at launch, no pad input, and the frames looked at rather
than only correlated: SQUARE ENIX 3.05-7.34 / 1.18-5.78 / 1.19-5.56 s, then a
~0.25 s black hold, then GAME ARTS/SETA/studio anima. Publisher first, 3/3.

The TSV is not wrong about any frame; its t=0 is ~7.7 s into the guest's boot,
so the publisher splash had been and gone before the stream opened. The tell is
in the file: its first twelve rows are byte-identical to four decimals -- one
held frame sampled twelve times -- and those exact numbers reappear in my run 1
at 8.42-10.94 s.

Second trap, new: ADV.wmv opens with its own SQUARE ENIX card, bloomed and below
centre, scoring 0.59-0.75 against live-splash-publisher.png. The classifier
fires splash_pub twice per boot and the second one is a movie frame. The real
splash holds perfectly still and scores 0.93-0.94.

And the dwells are DECODED, not measured: the publisher declares 240 units
(4.000 s) and the developer 195 (3.250 s), against measured 4.30/4.60/4.37 and
3.51/3.50/3.37. Measured over declared is 1.085 on average across six spans --
a 30 Hz timeline at 27.6 fps, which is the presentation rate this corpus has
measured independently three times. The port authors nothing here.

Instrument control run first: 11/11 content, 4/4 plate, the two splash
references rejecting each other at 0.035.

docs/re/boot-order-and-splash-dwell.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsxw1A9JseUw99Yw1ZRQzY
2026-08-29 14:15:28 +00:00
sylph-decoder
d71a74f938 re: index and cross-references for the record layout and the loading screens formats-pin-2026-08-29c 2026-08-29 14:06:10 +00:00
sylph-decoder
2e04e24ce9 handoff: the keyframe record layout, and the loading screens are named 2026-08-29 14:05:19 +00:00
sylph-decoder
3e4864b7fa re: the DELTASABER plates are the loading screen, and the pak is packed EN-then-JP 2026-08-29 14:04:42 +00:00
sylph-decoder
43f61a8d93 re: the splash timing table was already reading the corrected pairing 2026-08-29 14:04:04 +00:00
sylph-decoder
5744f379b2 formats: a keyframe's time comes before its pose, and none of them was missing
The placement region is `frames` records of `{u32 time; 36-byte pose}` after an
8-byte header, so the time word PRECEDES the pose it belongs to. Our parser's
40-byte window opened at the pose, four bytes into the record, and then read the
word at its `+36` as that pose's time -- which is the NEXT pose's. Every pose
field was right; only the time association slipped by one.

Two things the corpus has carried for weeks are that off-by-one and nothing
else: "a group's data stops 4 bytes short of its final block's time slot", and
"the last keyframe carries no time". The group is not short (8 + frames*40 is
exact) and no time is missing -- the first pose's time is the lead-in word at
`header + 8` that `parse_placements` skipped without asking what it was.

Disc-wide, 33 archives, 13 991 groups, each test with a control:

  A  lead-in prepended to the shifted times is non-decreasing  13991/13991
  B  a non-zero lead-in is strictly below the next time         5058/5058
     control (another group's lead-in, same bundle)            70.9%
  C  multi-segment alpha ramp at a constant rate, corrected    857/1540
     the same, under the old reading                             0/1042

C is the one that cannot be argued with: interpolation between keyframes is
linear, and under the old reading not one multi-keyframe ramp on the disc comes
out at a constant rate.

Adoption is free on every static composite, which is what the corpus previously
declined it over. `SYLPHEED_KF_TIME_SHIFT=1` moved GP_TITLE build 7 by 13.1% of
its pixels because it left pose 0 untimed; with the lead-in restored, all 12
GP_TITLE builds render byte-identically, and across 217 builds in six archives
only two elements pick a different rest pose -- both times between two poses
that are equally invisible.

`SYLPHEED_KF_TIME_SHIFT` is gone; `SYLPHEED_KF_TIME_LEGACY=1` restores the old
reading for A/B work.

ui_header_time_disc needed one line: 546 bundles whose every group is a single
static pose now report max_time = 0 where they previously reported no time at
all. Excluding them, the result it guards strengthened -- the bound holds over
2 859 bundles instead of 2 313, still with zero violations.

Not established: the executable's own parser. Reach is written down.

docs/re/ui-keyframe-record-layout.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsxw1A9JseUw99Yw1ZRQzY
2026-08-29 14:01:39 +00:00
sylph-decoder
b7104c6668 Merge remote-tracking branch 'origin/main' into auto/no-disc-and-menu-captures 2026-08-29 13:24:03 +00:00
MechaCat02
f5315ddf59 decoder: tell it about the reference assets, and that the DB can be wrong
The mounts landed but the agent could not learn of them: I documented them in
CONTAINER-NOTES.md, which the decoder's prompt does not list, and then restarted
the container -- so a fresh session with no memory of the exchange had a 586 MB
database and a decompressed image sitting unmentioned in its filesystem.

Now in the PROMPT itself, not only in a document, because the prompt is the one
thing a new session is guaranteed to read. CONTAINER-NOTES.md is also added to
its reading list.

And the caveat that matters more than the asset. The .pe is PRIMARY -- the bytes
the console executed. The database is somebody's ANALYSIS of them, produced by a
disassembler that had to guess, and it is wrong in the ways disassemblers are
wrong: misdecoded mnemonics where data was read as code, function boundaries
short or long or merged or split, coverage missing entirely for code reached only
by indirect dispatch, and names that are derived rather than symbols.

So a finding resting on a database row is not established until the bytes agree:
read the same address out of the .pe and check. Where they disagree the image
wins, and the disagreement is itself worth recording, because it tells the next
reader which parts of the database to distrust.

A fast index into 9.2 MB of machine code, not a source of truth.
2026-08-29 15:23:47 +02:00
sylph-decoder
aafd7c1b6f Merge remote-tracking branch 'origin/main' into auto/no-disc-and-menu-captures 2026-08-29 13:18:09 +00:00
MechaCat02
d2d2592c92 decoder: mount the disassembly DB and the flat VA image
The decoder had neither, and reported the gap precisely: four scripts in this
repo READ /work/xenia-rs/sylpheed.db and nothing produces it, so the whole static
PPC route was consumers with the producer missing.

Both exist on the host and are now mounted read-only: the 586 MB database (25 481
functions, 851 classes with RTTI, EH tables, imports, 1.8M indirect-dispatch
candidates) and the decompressed image.

The image is the more useful of the two. It is a FLAT VA DUMP -- file offset =
VA - 0x82000000 -- so reading a known address needs no XEX decrypt, no LZX, and
no booted emulator. The decoder had independently recovered the same bytes by
dumping /dev/shm/xenia_memory_* and validating against the GamePart table, which
is good work and a sound method, but it noted itself that needing a running
emulator is a bad dependency for something the entire static corpus rests on. It
does not need one.

Also recorded that an earlier claim the .pe was STALE was tested and refuted, so
nobody re-litigates it, and that instructions.raw is an INT rather than hex.

Written down as reference material, explicitly NOT a deliverable: they are
read-only, they come from outside the repository, and a fresh checkout elsewhere
has neither. Reimplementing the producer belongs in sylpheed-formats, and until
it exists every static finding rests on an artefact this project cannot rebuild.
2026-08-29 15:17:52 +02:00
sylph-decoder
14fced07b6 re: I cannot measure this emulator's clock -- and that answers the port's 2%
The port put two of my pages against each other: settle->plate 2.135 s and one
focus-ring revolution 2.177 s, both a declared 120 units during a static hold,
2% apart against a 6 ms run-to-run agreement. Fair challenge.

The resolution is that the question assumes a stable wall clock. Same interval,
same container, same day: 2.138, 2.132, and 2.549 s -- a 19% swing, caused by
adding --log_ui_draws=true. The 2% is a fifth of that. The two pages were never
in conflict about the game; they are three readings of one declared quantity
through a clock that moves. What settles the quantity is the disc.

Wall clock cannot separate the hypotheses, so I tried to measure frames instead.
Both instruments are recorded as failures rather than published as numbers:

  * Canary's own [UI-CAP] counter -- the one that produced the corpus's 28.5 fps
    -- costs a third of the frame rate. 300 frames in 16.567 s = 18.11 fps on a
    screen that gives ~28 without it. That reclassifies 28.5 as a load-dependent
    lower bound; it does not overturn it.
  * A distinct-frame counter over the spinning ring FAILED its decisive control:
    15.88 fps against the game's own 17.59 in the same window, 10% low, so the
    ring does not change on every presented frame. Its static control also read
    2.63 instead of ~0. Dead, not tuneable, per METHOD.md.

The rule that follows, and it applies to everything I hand the port: a measured
interval landing near a round number of declared units almost certainly IS that
number of units. Ship the units.

Also recovered here, because the same question needed it: the static PPC route.
Four tools open /work/xenia-rs/sylpheed.db and nothing in this repository builds
it -- no disassembler, no PPC decoder, and default.xex is encrypted (zero
plaintext "GamePart"). Xenia decompresses the image at load, so dump_image.py
reads it out of guest memory and validates it against the corpus's own landmarks:
the 29-entry GamePart id table at 0x820A1630 and the Xbox 360 D3D runtime
strings. String search and table dumps work again; instruction-level work does
not, and the present interval I wanted is an immediate, not a string.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Also settled, and also not:

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 12:15:22 +00:00
sylph-decoder
3ee1a25f47 tools: a title timing probe that costs 8.7 ms a frame, not 1503
The four durations withdrawn yesterday were produced by a classifier costing
1503 ms/frame draining an 8 fps x11grab at 0.64 fps -- a backlog, which
preserves ordering and destroys durations. This is the instrument for retaking
them.

What makes it cheap: every committed capture aligns at exactly dy=0 dx=0
(five-screens-acceptance), so the +/-8 px offset search screen_match does at
full resolution is 25 ZNCCs buying nothing on this path. Decimate 4x, do one
ZNCC per reference. Measured 8.7 ms per frame including the glyph count -- 173x.

Controls, run before the measurement and not after it:
  * 9/9 content controls, including the two committed movie frames that are the
    class this oracle exists to reject;
  * 4/4 on the plate detector itself, which is a threshold on the green-glyph
    counter and so needs its own control (no-plate title 159, plate title 753,
    movie frames 0).

And three things learned from run 1, folded back in:
  * do NOT restart the stream once the measurement is under way. Run 1's restart
    landed 0.25 s after the (A) press and its stale frames straddled exactly the
    interval being timed;
  * press INLINE, not through pad.py's subprocess -- an interpreter start plus
    the 0.25 s hold sat between the press and the timestamp;
  * count the longest run of byte-identical surface means and report it. That is
    the freeze signature, and it is how run 2 showed the 26.626 hold is the
    guest rather than the capture path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 12:15:06 +00:00
sylph-decoder
dd4f30a79f tools: fade_quads could not run -- two pre-monorepo paths
`screen-transitions.md` cites `fade_quads.py` as the command behind its decoded
fade ramp, and the command had been dead since the monorepo migration: it read
regn_decode.py from /work/Syplheed-Reborn and defaulted its pak to
/work/sylph_extract, neither of which exists. Resolve the helper beside the
script and default the pak under $SYLPHEED_DISC, the way run-canary and
sylpheed-cli already do.

A cited command that no longer runs is a citation nobody can check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 12:15:06 +00:00
sylph-decoder
06c32a0fd4 Merge remote-tracking branch 'origin/main' into auto/no-disc-and-menu-captures 2026-08-29 11:53:43 +00:00
MechaCat02
7492b45095 agents: merge main at the start of every iteration
Both agents read the protocol, their mission and the shared tooling from their
OWN checkout, and both work on topic branches -- so without an explicit sync
they follow whichever version of the rules existed when the branch started.

Found concretely: tools/audio-capture and two protocol revisions were on main
while the decoder worked for hours from a branch that had neither. The port had
merged on its own initiative and did have them, which is exactly the kind of
divergence nobody notices until the two disagree about what the rules say.
2026-08-29 13:53:00 +02:00