Commit Graph

128 Commits

Author SHA1 Message Date
Sylpheed port agent
ff8d608b1e port: check-capture passed a file that was 36% holes -- it now catches starvation
The Decoder diagnosed take 2 as a STARVED capture and I verified it here rather
than take it on trust: 35.6% of frames silent on all six channels, 10482
alternating runs, median burst 13.5 ms and gap 3.9 ms, a 17.4 ms period at 57 Hz.
Their untruncated original reads 39.3% and 10595 runs; the difference is exactly
the truncation and every other number agrees.

So my rebuilt correlator was working correctly on a file that could not carry the
signal. The alarming reading it produced -- that the game may not play the .wmv's
WMA track, so ADV.ogv's audio has been wrong since P4 -- is NOT SUPPORTED by this
capture and is not refuted either. Withdrawn as a concern arising from evidence,
with nothing changed in either direction. It was the most expensive-to-act-on
hypothesis in the port and it came from a file that could not speak to it.

THE REAL DEFECT WAS MINE: `check-capture` tested only for duplicated channels, so
it cleared a recording that was 36% holes. A provenance check that passes the
artefact it was built in response to is not a check.

It now measures starvation, and TWO THRESHOLDS I INVENTED WERE BOTH WRONG:

  counting exact-zero frames -- real audio crosses zero constantly, so a clean
    voice track scored 5947 "gaps" of median 0.0 ms and was called starved. A gap
    is a RUN, not a sample; only runs over 1 ms count.
  gap count and median length -- a genuine music bed shows 454 gaps at a median
    of 1.4 ms, because quiet 16-bit passages really are zero for milliseconds.

What separates them is the RATE: 32.9 gaps/s starved, 3.3 for a real bed, 0.03
for a voice track that is 53% pauses. Bar at 20/s, derived from those controls
rather than chosen and then justified. Controlled both directions: real stereo
bed PASS, six distinct tones PASS, starved capture FAIL. It also reports a `data`
chunk declaring 0 bytes -- what a file copied mid-write looks like, which is what
happened.

VOICE CHANNEL ROLES ARE NOT OBTAINABLE THIS SESSION. Both routes closed: the
monitor sink is starved by construction, and the internal tap at
SDLAudioDriver::SubmitFrame needs a Canary rebuild the Decoder has costed at a
whole session. That is the human's call, not an agent's. The port keeps authoring
with the known recorded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 16:25:17 +00:00
Sylpheed port agent
c1113b8cd6 port: the menu music was 3.52 dB quiet -- a bank header was being summed as a stem
Checking my export against the Decoder's declared XMA1 durations turned up a
defect of mine that has been shipping since P6.

`export_bgm` summed every sub-wave `media` returned and scaled by 1/n. Decoded
and timed, all three banks have the same shape:

  BGM_103  sub-wave 0: 10300 B -> 0.009 s, peak -inf   1: 87.744 s   2: 87.744 s
  BGM_102  sub-wave 0: 10300 B -> 0.009 s, peak -inf   1: 37.482 s   2: 37.482 s
  BGM_001  sub-wave 0: 10300 B -> 0.009 s, peak -inf   1: 173.809 s  2: 173.809 s

Sub-wave 0 is DIGITALLY SILENT in all three, and 10300 B is 10240 plus a 60-byte
RIFF wrapper -- 10240 being exactly the bank header the Decoder's census
identifies. Counting it in the divisor put every real stem at 1/3 instead of 1/2:
3.52 dB on all the menu music since P6. Dropping a silent input is arithmetic,
not a decoding decision. Measured after: main_menu.ogg -7.69 -> -4.20 dBFS,
+3.49 dB against 3.52 predicted.

THIRD INSTANCE OF ONE DEFECT: a silent chunk in the voice sum, a silent channel
in the mono fold, now a silent sub-wave in the music sum. Each invisible to every
check except a level, and each time the divisor was computed from how many inputs
there are rather than how many carry signal. That is the shape, not the bug.

Closes a red row open since P6 -- "sound_bank_riffs returns three sub-waves where
Q10's census says two". The census was right, and this corroborates the Decoder's
c1f3608 by decoding rather than by counting headers. The export reports 2
sub-waves and the warning is gone.

REFUTATION ATTEMPT, conclusion survives and the reasoning does not: the Decoder
explained BGM_001 as "173.821 s declared against your decoded 167.663 s, a gap of
6.158 s -- declared is the encoded stream, decoded is where the audio stops." A
full decode yields 173.809 s of PCM, not 167.663. The 167.663 is where the music
FADES OUT, measured from the audio; the stream continues silent to its declared
end. Declared and decoded agree to 12 ms, and the trailing silence is inside the
decode rather than the difference between two methods. The cross-check is
stronger than stated -- three banks, 5-12 ms -- and the explanation should go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 16:19:31 +00:00
Sylpheed port agent
eb45f9320d port: my correlation instrument failed its own control -- the negative had to be re-earned
Take 2 is a good file: it passes check-capture (I re-ran it rather than cite the
Decoder's run), carries a screen log, and was recorded with the sink's
channel_map set equal to Canary's own.

BEFORE REPORTING A SECOND NEGATIVE I ASKED WHETHER MY METHOD COULD DO THE JOB,
by building a synthetic mix -- the bed plus the three voice streams -- and
hunting the bed inside it. It failed: r=0.415, against the r>0.8 bar my earlier
negatives were judged against.

So the instrument that produced "the capture contains no ADV audio" could not
have found ADV audio in a mix even when it was certainly there. That conclusion
was right -- the Decoder's tone control proved take 1 corrupt independently --
but it was right BY LUCK and I reported it as measurement. The three controls I
was pleased with tested that the method finds a clean signal in a clean
reference, which was never the task.

REBUILT AND CALIBRATED IN BOTH DIRECTIONS. Band-limit so the target dominates,
then judge on LAG and MARGIN rather than absolute r -- r>0.8 is correct
clean-against-clean and meaningless for a component in a mix.

  bed, 40-180 Hz    in a mix containing it   r=0.663  lag 0.0 s   margin +0.111
  bed, 40-180 Hz    against a voice-only mix r=0.262  lag wrong   margin +0.005
  voice, 300-3000   in a mix containing it   r=0.810  lag 0.0 s   margin +0.248
  voice, 300-3000   against the bed alone    r=0.358  lag wrong   margin +0.005

A 20-50x separation in the discriminating statistic. Written up as
AUDIO-VERIFICATION.md section 6, retraction included.

THE NEGATIVE NOW STANDS ON SOMETHING. All six of take 2's channels, against both
targets, sit in the known-absent regime: margins 0.000-0.017, lags scattered from
-72 to +255 s. Take 2 contains neither the movie's WMA bed nor the cutscene
voice.

Two captures, differently configured, the second provably free of the channel-map
fault, with a screen log saying the movie was on screen, and neither carries
either source. Handed back: a capture path still losing the mix, or the guest not
emitting these sources during the movie, and only one side of the wall can tell
those apart. If it is the second it reaches the port directly -- the export's
movie audio comes from the .wmv's WMA track.

Also noted: the message gives 253.3 s, the file is 318.539 s. The screen log
agrees with the file, so it is a mis-stated number, but a length quoted in a
provenance claim should match the artefact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 16:15:59 +00:00
Sylpheed port agent
c346c65568 port: a capture must pass a provenance check before anyone analyses it
The 6-channel capture I spent an iteration refuting was corrupt, and the Decoder
found the cause: PulseAudio remapping between two mismatched channel maps, where
a 6-channel remap SILENTLY DROPS AND DUPLICATES. Its control -- six channels, six
tones, same sink, same parec -- came back 400/3200/200/800/800/200 for an input
of 400/800/200/1600/3200/6400. Two source channels gone entirely.

So the negative was right, and the byte-identical pair I reported was the thread
that unravelled it. Worth recording precisely, because it nearly went unreported:
it began as an idle look at two channels whose peak AND RMS matched to six
decimals, and it only became evidence because a coincidence at six decimals is
cheaper to hash than to explain.

`tools/port/check-capture` makes that check one command -- split the file, hash
every channel, fail on any duplicate pair -- and AUDIO-VERIFICATION.md gains a
section 5 saying to run it FIRST, every time, plus the two conditions the same
incident produced: start the recorder before the process, and log what was on
screen against the recording's own clock so a miss is diagnosable.

Controlled both directions, because a checker nobody controlled is what this
incident is about: six distinct tones PASS; the remap's own output pattern FAILS
naming all four pairs; the corrupt capture FAILS on ch2 == ch5.

THE KNOWN-BAD CONTROL IS THE POINT. All six of its channels report a peak of
-18.063656 dB, identical to six decimals, while containing three duplicate pairs.
A level check cannot see this failure. That is why the tool hashes rather than
measures, and why the corrupt capture's "plausible per-channel levels" were never
evidence. The tool says of itself that it is necessary, not sufficient.

Withdrawn with the file, both the Decoder's: "all six channels carry signal", and
the non-zero-surround observation offered as weak support for 5.1. Unaffected:
the three-XMA-context concurrency result, read from the emulator's log rather
than the audio path, on two independent boots.

The corrupt file is dropped from the exchange so the next agent cannot pick it up
and repeat the work. Nothing in the export changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 16:07:19 +00:00
Sylpheed port agent
b0d24c8af3 port: the oracle capture does not contain the intro -- a controlled negative
The Decoder took the capture I asked for and described it as the full mix, the
movie's WMA track plus the three XMA streams. It contains none of them.

Envelope cross-correlation, sliding with overhang, normalised over the overlap,
minimum 30 s of overlap so a short window cannot win on an edge. Against the ADV
bed, against each of the three voice streams, against BGM_103 and against S00A,
every pairing is a PLATEAU rather than a peak: best-to-runner-up margins of
0.001-0.016. The margin is the number that matters, not r.

Three controls, because a negative from an uncontrolled instrument is worthless:

  the instrument finds matches on this data -- bed vs bed r=1.000 margin +0.115,
    voice stream 2 vs stream 3 r=1.000 margin +0.300;
  my reference really is the movie -- the .ogv transcode against the disc's own
    ADV.wmv, r=1.000 margin +0.114, so a miss is not my transcode;
  drift is excluded -- a stretched playback would still match locally with the
    best lag climbing monotonically, and 5 s windows give 4.95, 15.30, 119.35,
    50.75, 29.35, 83.95 s, scattered across the movie.

WHAT IT IS has been handed back rather than guessed. It is 70 s of something,
all six channels carrying signal at RMS about -27 dBFS, matching nothing this
port exports. Diagnosing it is the other side of the wall.

One measurement that may help: split as 5.1, channels 3 and 6 are BYTE-IDENTICAL
-- same MD5, not merely the same peak and RMS. An exact duplicate pair inside a
six-channel surround output is consistent with the Decoder's own warning that the
6-channel frame is Xenia's hardcoded kFrameChannelsDefault rather than the
guest's request, and weakens any reading of that file as evidence of a 5.1 mix.

THE VOICE EXPORT IS UNCHANGED: still one stream of three, still marked red in the
manifest, the console line and authored/audio.json. A failed measurement should
change nothing, and what would have been wrong is treating an expensive recording
as an oracle because it was expensive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 16:01:10 +00:00
Sylpheed port agent
bb9626defe port: the game decodes all three voice streams at once, and two baseline rows were comparing blank frames
TWO FINDINGS, one mine and one handed to me, and the second retires a premise I
built on twice.

THE P1 BASELINE HAD ROWS THAT PROVED NOTHING. `build_12` and `build_15` render
pure black in BOTH renderers -- mean 0, max 0 -- so the difference is zero and
`verify-screen` scored them `max 0  over3 0  OK`, the strongest verdict it has.
Two of sixteen rows were comparing nothing against nothing. Worse than a missing
test, because a missing test is visible in the count.

Cause isolated by a control, not by reading: `build_00`/`build_01` are the same
loading screen minus three elements and render fine (mean 1.913, max 214.5). The
dressed variants add `pgloading_eff00`, a 1280x720 primitive resting OPAQUE BLACK
at t=38 inside its own opening black hold, with no layer key so paint order puts
it last.

The rule I was about to write -- "rest.t before the last timed keyframe is the
pathology" -- was killed by running the census first: 152 of 212 elements in this
export have rest.t earlier than their last timed keyframe. It is the norm. What
is actually unusual is the CONTENT, and its reach is one: `pgloading_eff00` is
the only element in the export whose resting pose is a fully opaque full-frame
quad. One instance is not a rule, so the renderer is unchanged and the HARNESS is
fixed: a blank pair now reports BLANK -- both renderers drew nothing; this row
proves nothing. `status` is untouched, so an unrelated DIFFERS still fails.

THE VOICE EXPORT IS KNOWN INCOMPLETE. The Decoder booted Canary with
--xma_param_probe and the game decodes ALL THREE streams CONCURRENTLY, in three
XMA contexts whose byte sizes match the disc payloads exactly. So "three
presentations of one take, pick one" is refuted by the running game and the
question I had been arguing -- WHICH presentation -- has no answer.

This one no census could have caught. Every measurement was right: the streams
are equal-duration, one is silence, one is 0.60x another with the residual 26.8
dB down. The frame around them was wrong, and the file says ChannelMask 0x0002 on
all three. It took the running game -- which is the mission's own sentence
arriving in practice.

BEHAVIOUR HELD DELIBERATELY. An equal-gain 1/n sum of channel pairs is not a
downmix either -- MISSION section 6 pins an explicit matrix for exactly that
reason -- and summing cost S00A 6.02 dB when one stream was silence. Swapping one
guess for another on a message is what produced this entry twice. What changed is
that the wrongness is now LOUD, because this failure sounds like success: one
stream is clean audible dialogue. A top-level manifest warning per movie, the
console line, and the authored entry all say `1 of 3 streams`.

"They are 5.1" is recorded as the Decoder's HYPOTHESIS with its own
counter-evidence attached, and nothing builds on it. What settles it is asked: a
recording of the game's own output over ADV through the null sink, which turns
channel roles into a fit against an oracle.

Refutation attempt, survived: the Decoder's loading-screen variant map. Entries
0/1 carry 7 elements and 12/15 carry those seven plus baseeff, eff00 and loop5 --
exact in count and identity, and it is what made build_00 a control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 15:50:02 +00:00
Sylpheed port agent
92f1436836 port: withdraw the dual-mono generalisation -- the measurement stands, the rule does not
I argued `highest_rate` had no case because ADV's higher-rate presentation is
dual-mono while its louder one is mono-in-stereo, so the extra bytes buy a
duplicated channel rather than fidelity. The Decoder tested that disc-wide over
the 28 three-stream cues: the stream-3/stream-2 size ratio runs min 0.0778,
median 1.2565, max 2.9163, sd 0.5057, with only 12 of 28 within 15% of 1.0, and
declared rates scatter with them. A 37x spread is not a duplicated channel.

The CHANNEL MEASUREMENT STANDS -- ADV chunk 1 is mono-in-stereo and chunk 2 is
dual-mono at -8.318574, this port's own decode, which the Decoder could not
re-run and did not dispute. What fails is the step from one asset to the format.

NOTHING IN THE EXPORT CHANGES. `loudest` is a per-asset content rule -- it reads
the peak of the streams in front of it -- so a scattering structural ratio cannot
undermine it. What changes is the REASON, in four places: authored/audio.json's
presentation_why, the selector comment in audio.rs, BLOCKED.md's row, and
DECISIONS.md. The honest statement is narrower: `highest_rate` was never refuted,
it was never argued for, and neither is `loudest`. That is why the entry is
marked CHOSEN rather than measured, and why one capture deletes it.

Recorded on the pattern rather than just the instance: this is the third claim of
mine in two iterations that generalised a single-asset observation, after "the
chunks are two stems" and "everything the sequencer paces off rest.t is late".
All three were true of the thing I looked at. The failure is reaching for the
rule a measurement would imply if it held everywhere and writing that down in the
same breath as the measurement.

Also noted, not mine and not affecting export_voice: S12B's three streams are
byte-size identical, and BIRD_224 is three-stream while not being a movie cue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 15:39:22 +00:00
Sylpheed port agent
81ea5cb324 port: settle times measured -- the pacing was already right, and my own red flag was half wrong
THE TRANSCODE CACHE HAD NEVER HIT. `video::transcode` has carried one since P4 --
a `.cmd` sidecar with the command, the source size and the channel count -- and
`main.rs` clears the output tree wholesale immediately before the check, deleting
the sidecar and the file it stamps. Six exports in this session paid the full
Theora encode and produced five byte-identical files, roughly 48 minutes. Nothing
reported it, and nothing could: a cache is silent either way and the only symptom
is a wall clock that looks like the job being slow. The wipe now spares `video/`
and `prune_videos` deletes anything in it this run did not claim, so the
wholesale guarantee is kept rather than traded. A re-export is 20 s.

SETTLE TIMES, MEASURED, and they refute more of my row than they confirm. The
principle holds -- the title's rest.t is 251 units = 4.183 s where its art
finishes at ~2 s -- but "everything the sequencer paces off that landmark is
therefore late" does not. Measured the port the way the game was measured, by
VISIBLE SPAN rather than arrival-to-arrival:

  publisher wordmark  port 4.25 s   game 4.297 / 4.604 / 4.370
  developer logos     port 3.50 s   game 3.508 / 3.503 / 3.366
  black hold          port ~0.25 s  game 0.2 - 0.3
  title -> plate      port 2.000 s (declared 120 units)   game 2.247 s

Dead on. My earlier reading compared the port's transition timestamps against the
game's visible spans, which differ by the exit ramp plus the black hold -- the
whole of the discrepancy I was about to chase, and the same definitional trap
that cost this corpus 0.48 s on the plate delay. Nothing in the sequencer is
changed. `dwell_seconds` stays null, now for a measured reason rather than an
absent one: `timing.json` said "if a capture ever times the real boot, this is
where that number goes", and the answer is that nothing goes there.

Not authored, deliberately: an (A)->menu dwell, which measures 3.763 s and
contains a 1.53 s guest load stall on a cold cache; and the menu build-in and
(B)->title, which rest on one run where the port is already within ~0.1 s.

THE VOICE PRESENTATION MOVED TO authored/, because the recommendation behind it
was withdrawn as self-contradictory and the choice is now unambiguously mine.
`voice.presentation` = `loudest`, and the reason to switch is a measurement, not
a preference: ADV chunk 1 is MONO-IN-STEREO and chunk 2 is DUAL-MONO, so chunk
2's extra bytes encode a duplicate channel rather than fidelity. That explains
the byte-rate difference and removes the only argument for `highest_rate`. ADV's
dialogue now exports at +0.3 dBFS instead of -8.7, which is the SE bound's
documented decode overshoot on a wave mastered at full scale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 15:35:24 +00:00
Sylpheed port agent
d3b37b2a1e port: a voice region is three presentations of one take -- keep one stream, sum nothing
Third reading of the same bytes in one session, and the first one that is
decoded rather than inferred.

  1. concatenate  -> 359 s of dialogue for a 137 s movie. Dead on measurement.
  2. sum as Q10's two stems -> refuted here: S00A's second full-length chunk is
     DIGITAL SILENCE and ADV's is 0.60x the first with 26.8 dB of residual. That
     claim was mine, and the Decoder had already adopted it before I tested it;
     it is withdrawn in both places.
  3. keep ONE stream. Decoded disc-wide by the Decoder, counting stream starts
     inside every inter-descriptor span: 258 spans hold one stream, 28 hold
     three, nothing holds two. So 359 = 84.55 + 137.32 + 137.32.

Summing was therefore wrong a third time, and for a third reason: a take plus a
0.60x copy of itself is ~4 dB louder and coloured, not a mix of parts. The filter
is now `[0:a]anull` plus the mono fold -- no gain applied at all.

`check` moves `voice` off the strict peak bound as a consequence. It sat with
`bgm` because it was a sum this exporter produced; it is now a single wave off
the disc, mastered near full scale -- ADV's louder presentation measures +0.0003
dBFS at source -- so refusing that would be refusing the disc's own mastering.

The leading chunk is dropped as a DUPLICATE, not a truncation. It is this
movie's own dialogue (the Decoder, 17 of 17) and I measured it to be the TAIL of
the kept stream: sliding envelope correlation r=0.998 / 0.932 with the lag flush
against that stream's end, controls 1.000 self and 0.289 for a different movie,
confirmed in the sample domain at 16.7 / 23.2 dB of residual.

STILL OPEN, and flagged rather than absorbed: WHICH presentation to keep. Highest
byte rate is the Decoder's recommendation and not a decoded field, and on ADV it
selects the QUIETER of two (-8.3 dBFS against 0.0). Said in the manifest with the
consequence, so it is visible and reversible. A capture of the intro with
dialogue audible settles it.

Not converted, and the Decoder has since withdrawn the ask with a better reason
than mine: its 504464 B anchor constant is structural, not proportional --
identical on all 17 regions, and a proportional prediction lands within 8 bytes
on ADV while being 4305 B out on S00A.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 15:12:41 +00:00
Sylpheed port agent
4817e5ea9e port: fold only the channels that carry signal, and measure what the leading chunk actually is
TWO DEFECTS AND ONE MEASUREMENT, all from verifying the previous commit rather
than from reading it.

Channel 2 of both voice streams is DIGITALLY SILENT -- peak -inf over the whole
file. The voice is a mono recording carried in a nominally stereo stream, and
averaging it with silence cost 5.94 dB. The doc comment directly above the code
that did it warned that "a stereo matrix applied to a mono voice track is not an
error, it is a -6 dB attenuation that nothing reports", and then the code checked
the DECLARED channel count instead of the content. `live_channels` now measures
which channels carry signal and averages only those.

Three defects this iteration were the same shape: a silent chunk in a sum, a
silent channel in a fold, and a pan matrix naming channels that do not exist.
Each is an input contributing nothing while still counting in a divisor, and none
is visible in anything but a level.

THE LEADING CHUNK IS THE TAIL OF THE FULL-LENGTH ONE. The Decoder settled by
byte-span analysis that it is the movie's own dialogue, 17 of 17 -- killing its
own hypothesis that it was an in-mission line -- and asked whether dropping it is
a truncation, having no XMA1 decoder. Sliding envelope correlation with overhang
allowed and normalised over the overlap: ADV r=0.998 at +52.8 s, S00A r=0.932 at
+25.6 s, against controls of 1.000 (self) and 0.289 (a different movie). Both
lags put chunk 0 flush against the END of chunk 1. Sample domain, lag refined to
one sample then a scalar best-fit: residuals 16.70 dB and 23.15 dB below target.

So dropping it removes a DUPLICATE and the exporter was right for a worse reason
than it gave. The manifest note is NOT rewritten to claim that -- the structural
conclusion belongs on the Decoder's page, not in my manifest -- but it no longer
equates the chunk with BGM_103's third sub-wave, which a disc-wide census showed
is a different structure, and it now says in words that the omission must not be
read as junk removal.

Not converted, deliberately: the Decoder's 504464 B anchor constant. Bytes per
second is not constant even inside one region -- chunk 1 is 1118268 B and chunk 2
is 1171516 B for the SAME 137.324 s -- so any figure in seconds off it would be
invented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 15:07:30 +00:00
Sylpheed port agent
cb8d77febc port: withdraw my own "two stems" reading of a voice region, and stop summing silence
The Decoder asked me to decode a voice region's leading chunk -- it has no XMA1
decoder in its container -- and the decoder run refuted a claim of mine that it
had already adopted into `docs/re/structures/voice-region-leading-chunk.md`.

I wrote that a region's two equal-length chunks are HANDOFF Q10's decoded
two-stem shape. Equal duration was a SHAPE match and I carried the music census
across on the strength of it. The content does not support it:

  S00A chunk 2 is DIGITAL SILENCE -- 4497300 samples, peak -inf.
  ADV  chunk 2 is 0.60x chunk 1, best-fit scalar, residual 26.8 dB below the
       target: about 95% of its energy is a -4.4 dB copy of the first chunk.

That cost real level. Summing chunk 1 with silence at 1/n put S00A's dialogue
6.02 dB down for nothing -- the exported file peaked at -16.2 dBFS against a
source chunk peaking at -4.2. `export_voice` now drops a digitally silent chunk
before the sum, which is arithmetic and not a judgement about content.

WHAT ADV'S NEAR-DUPLICATE SECOND CHUNK IS REMAINS OPEN AND IT IS STILL SUMMED.
Whether the game plays both is a decoding question, 26.8 dB of residual is not
nothing, and dropping a chunk because it correlates with another would be
answering it.

The leading chunk, answered as far as a measurement goes: ADV region + 1392, 394
packets, 84.553 s, stereo 48 kHz, peak -2.48 dBFS, 6 silent gaps over 0.4 s
totalling 45.3 s -- 54% silence, the same duty cycle as the full-length chunks.
Speech-structured, so not a header and not padding. "Cutscene or mission" is an
identification and this agent has no ears and no oracle; envelope correlation
peaks at 0.768 at the last lag in the search range, which is where a statistic
lands when it has found nothing, and it is not an answer.

Not taken yet, and said so in BLOCKED: the discriminator should be
`bank_header_len`, not a duration tie. This exporter never used `riffs.len()`, so
it already handles both of the Decoder's cases, but a tie is an observation and
`bank_header_len` is decoded. It switches when `c1f3608` reaches `main`;
`sylpheed-formats` is a path dependency and merging another agent's topic branch
is not the port's to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 15:01:01 +00:00
Sylpheed port agent
9c55ca4801 port: the voice is in the mix, proved by a null control that needed no test code
Three things verified against the re-export, none of them by reading the source.

THE STEM READING IS CORROBORATED BY A NUMBER IT WAS NOT FITTED TO. Chunks are
selected as "the longest, and everything tying with it" -- the movie's own length
is not an input to that rule. The result now matches the picture to -0.114 s
(ADV) and -0.085 s (S00A), where the concatenated version was +221 s and +161 s.

THE VOICE REACHES THE MASTER BUS. `+ voice ADV` in the log proves only that a
stream was found and played. The control is MODDING rule 4: 140 s of silence at
data/mods/audio/voice/ADV.ogg shadows the exported file and changes nothing else,
so no test-only flag was added to the runtime. The muted run reproduces the
movie's own bed to 0.01 dB peak and 0.19 dB RMS; the unmuted run measures -22.913
dBFS RMS against -23.184 predicted for two incoherent sources at -24.941 and
-27.965. 0.27 dB out.

(A) SKIPS THE INTRO IN THIS BUILD. `--skip-at=25` ends the movie at 25.02 s
through `Input.parse_input_event` and `_unhandled_input`, so the press-to-skip
wiring is live and the play-test's report is not that bug. Untested here: a real
key from a focused window. Unsettled anywhere: whether the GAME permits skipping
an attract movie -- Q9, still amber, and if the answer is no this path is deleted
rather than debugged.

`check` accepts the new `voice` kind and holds it to the strict peak bound, with
`bgm`, because it is a sum this exporter produced rather than a wave off the
disc. FORMAT.md says what a `voice` entry is, that it keys by MOVIE NAME rather
than by a role, and that its `why` names every chunk dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 14:56:35 +00:00
Sylpheed port agent
53a93e2e9e port: the intro had no dialogue because the voice is a separate asset, and I concatenated it wrongly first
A human play-test heard music under the boot intro and no voices. The obvious
reading -- the 5.1 fold dropped the centre channel -- is wrong. `ADV.wmv` carries
music and effects only; a cutscene's voice is a separate continuous XMA stream in
`sound.pak`, bound to the movie by the manifest in `tables.pak`. Nothing was
dropped. The exporter had never been asked for it, so every fidelity measurement
in AUDIO-VERIFICATION.md would have come back clean.

`audio::export_voice` resolves it with `media::resolve_movie_voice_region` and
never by filename: `RT01A`'s voice lives inside `VOICE_ADV.slb`, so a name match
is correct on exactly the two movies this port would have spot-checked. Decoded,
not authored -- so it runs outside the `authored/audio.json` block.

THE FIRST VERSION CONCATENATED THE REGION'S CHUNKS AND WAS WRONG. It produced
359 s of dialogue for a 137 s movie. Decoding and timing each chunk shows two of
them equal to six decimals and each spanning the whole movie -- HANDOFF Q10's
decoded two-stem shape on a second asset kind -- so they are summed at 1/n. The
error was visible only because the first version recorded the decoded length
against the movie's instead of clamping to it; the clamp `media`'s own doc
comment invites, and which `sylpheed-viewer` applies, would have produced a file
of exactly the right duration containing the wrong audio.

The dropped leading chunk matches no duration in its region and is NOT closed
here. It is the same signature as `BGM_103`'s third sub-wave, already open in
BLOCKED.md, now corroborated on an independent asset kind. Raised with the
Decoder; the manifest names every chunk dropped and its length.

Also in this commit, and separable:

* `--skip-at=SECONDS` -- `--script` structurally cannot press during a movie,
  because `_script_settled` waits while `_player != null`. That is why "does (A)
  skip the intro" had been read out of the source rather than measured.
* MISSION section 6 pins a 5.1->stereo matrix and this exporter has shipped a
  different one since P4 -- the same weighting, 7.65 dB quieter -- and said so
  nowhere. Re-measured with the right instrument (float decode, whole file, count
  the samples that would clamp, not a peak reading): the pinned matrix puts ADV
  at +4.26 dBFS on 4406 samples, while S00A never clips. So the pin overloads one
  movie and the constant is over-broad for the other. NOT changed -- the level of
  a mix is what section 6 reserves to a human. The export now carries a warning
  with the numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
2026-08-29 14:51:38 +00:00
Sylpheed port agent
f33aeca8f5 port: file the keyframe-layout change and the loading-screen answer in BLOCKED
Neither is taken. The keyframe record layout (every pose timed, exit_ramp_units
decodable) reaches me only when that branch lands on main -- sylpheed-formats is
a path dependency and merging another agent's topic branch is not mine to do.
The loading-screen names are answered but carry two of the RE agent's own
amber flags plus the index-space problem I found, so no name is written yet.

Recorded with what limits the risk: static composites are byte-identical across
the keyframe change, so this iteration's verify-screen baseline is the control
to re-run the day the pin lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 14:10:37 +00:00
Sylpheed port agent
cdea236713 port: the P1 regression harness could not have run since the monorepo merge
verify-screen resolves its reference binary to a path build-reference-cli
stopped being able to produce: that script greps Cargo.toml for a
`Syplheed-Reborn.git", rev = "..."` pin, and 65cefa7 replaced it with a path
dependency. The grep returns nothing, the script exits 1, and the binary left
behind predates the merge -- three hours older than the sources, from a revision
nothing points at. The consumer only checked that the file EXISTED.

DECISIONS already carries "the reference renderer was stale for three diff runs"
from P2. This would have been the fourth, by a different mechanism: not a
forgotten rebuild but a build step that could no longer succeed.

The fix is a deletion. Revision-keying solved a two-repo problem -- /reborn's
target/ was a live mount that moved mid-run -- and the monorepo removed that
problem by construction. verify-screen now builds sylpheed-cli from the
workspace, which is the same decoder the exporter and the port read.

BASELINE, all 16 screens, after four milestones of renderer change: no new
drift. The only substantial disagreement is title_jp (max 155, 20498 px), which
is the same one P1 recorded and left open -- ptlogo_eff2 at 125 % scale, where
blit samples the destination pixel's corner and a GPU samples its centre. The
port still does not match it, because matching means reproducing a half-pixel
bias on purpose to make a number smaller.

`max` alone could not tell 2 pixels from 25 000: main_menu and title_jp produced
the same DIFFERS verdict. The script now also reports how many pixels are over
the bar. The bar is NOT raised -- tuning a threshold until things match is the
failure its own header warns about.

Stated plainly because sixteen OK rows look reassuring: the harness renders
--pose=rest, so NONE of the last four milestones' visible work is under test --
not the spinning ring, not the plate composite, not any timeline behaviour, not
audio.

REFUTATION: the RE agent's answer naming "builds 0/1 and 10/11" as the loading
screen is false in the index space this export uses -- entries 10 and 11 are
palogo_sqex and palogo_gamearts, the splashes, which the same agent identified
as such answering my ask 1. Over the twelve is_build bundles, ordinals 10/11 ARE
entries 12/15, so the finding is right and the enumeration is not mine.
authored/screen_names.json is keyed by ENTRY, and someone writing keys "10" and
"11" from that message would name the publisher wordmark and the developer logos
as loading screens, and everything would still validate and run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 14:10:16 +00:00
Sylpheed port agent
7132c4a326 port: implement MODDING rule 4, and withdraw a red flag that was my own bad measurement
MODDING.md calls base-and-overrides "a design constraint on the exporter today,
not a milestone to add later". Nothing read `data/mods/` at all -- the directory
has existed since the monorepo merge with a .gitkeep and no code path anywhere.
Eight milestones shipped past it.

ExportTree.resolve() now shadows by path, and every read goes through it:
screens, sprites, cues, the music bed, movies. MenuAudio was reading tree.root
directly and would otherwise have made audio the one asset kind a mod could not
touch, for no reason a modder could have guessed. No manifest, no registration
step -- the path IS the registration, which is the whole of the rule. One tree,
not a stack: layering needs a load order and nobody has asked for one, so
data/mods/README.md says that rather than inventing it.

Every shadowed file is printed as it is read. The first version summarised in
_ready, before any asset had been read, so it always said "nothing shadowed yet"
-- a report structurally incapable of reporting anything, which is worse than
none because it looks like an answer.

data/mods/ was NOT gitignored, and that is a hole in a hard rule: a mod is
usually an edited game asset, and this was the one directory a user is invited
to put modified sprites in and git would have taken them. Now excluded except
the README.

Gate: a synthetic 203x43 magenta PNG (nothing disc-derived) at
data/mods/sprites/title/main_menu/ptbtn01.png changes 8501 pixels in a bounding
box of exactly 203x43 at the button's position, and `check` still passes.

RAISED, NOT RESOLVED: MODDING.md says the tree is data/base/, PORT-MISSION.md §3
and the exporter and .gitignore say export/. Both are mission files and only the
human changes a mission.

REFUTATION on Q3's paint-order key: 2 of 16 screens did not match a stable sort
by layer key -- but that was my test. pgloading_eff00.prm carries NO layer key
(layer_source "none"): a primitive with no sprite header and no implied-name
fallback. I sorted keyless first; the decoders put it last, which is right,
since it is the full-screen black quad and HANDOFF's own sentence is that the
fade quad paints last. Completing the rule to "keyless last" gives 16 of 16.
SURVIVES. Recorded because the published claim does not say where a keyless
element goes and there is one in the archive. Separately the tie-break's reach
looks understated: 105 elements share a layer key across 12 of 16 screens, where
HANDOFF characterises the cost as "one element's blend on one screen".

WITHDRAWN, and it was mine: I filed "the runtime mix has no headroom" in red
twice, off a peak reading. Measured properly it is 43 samples at full scale in
5.9 s and 24 in 98.5 s, longest run 0.25 ms -- the disc's own confirm cue on a
transient, possibly only in the 16-bit save. Nothing changed, deliberately:
attenuating would be an unmeasured level decision of the kind I refused for the
loop point. A peak reading is not a clipping measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 13:41:00 +00:00
Sylpheed port agent
d2100c227a port: say at the top of BLOCKED.md that it is the ask list, and that nobody reads it
The port's open asks have now been delivered by message three times and lost
three times, because the decoder container was recreated each time and a message
dies with it. The asks themselves were never lost -- they are in this file, with
the HANDOFF sha each row came from -- but `docs/agents/decoder-loop.md`'s read
list does not name this page, so a fresh decoder has no route to it.

This does not fix that. It makes the page introduce itself, so that ONE pointer
at it is enough for a session that has never seen it, and states the gap plainly
where both agents and the human will read it. Changing the loop brief is the
human's call and I have not touched it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 13:26:24 +00:00
Sylpheed port agent
d45da73bba port: P7 -- the new-game intro plays, and the two screens it skips are named out loud
S00A has been exported since P4; what P7 needed was something to play it and a
defined place to land. Both are here, and the interesting part is the gap.

The real chain is NEW GAME -> DIFFICULTY -> SELECT DATA -> (A) on a save slot ->
~4.5 s -> S00A. DIFFICULTY and SELECT DATA are MEASURED destinations that are not
GP_TITLE builds, so no screen file exists to go to. The port jumps from NEW GAME
to the one thing in that chain it has -- and the whole design is about not
letting that read as a sequence:

  * MenuFlow.accept returns a new kind, `video`, rather than folding this into
    `blocked`, because the caller has to announce the skip and a distinct kind is
    what forces it to;
  * the runtime prints the skipped screens by name on every run;
  * flow.json carries `skipped_chain` as DATA, so what is missing lives beside
    the decision instead of inside a GDScript string.

After the movie the port returns to the title. Authored, and it has to be: the
game goes into mission 1 and gameplay is out of scope. The ~4.5 s before the
movie is left EMPTY on purpose -- GP_TITLE does carry a loading screen and 4.5 s
is about the right shape for one, which is exactly why that belongs in BLOCKED.md
and not in flow.json.

`--script`'s 20 s per-step timeout would have killed every movie run at step 1.
Raising the constant would have been wrong the other way: a movie stuck at frame
0 would then hang the job, and a job that waits is worse than one that fails. The
test is now LIVENESS -- while get_stream_position() advances the deadline moves
with it, and a stalled movie still trips the same 20 s.

Found while looking: GP_TITLE's four unnamed builds (entries 0, 1, 12, 15) are
LOADING screens -- every element in all four is pgloading_*, and LOADING is one of
the three names the decoder read out of the title part's state function. NOT
renamed here: which member of each pair is which locale is an inference, and a
name stops being questioned once written. Handed over.

One of them is a second casualty of the rest.t problem, and a worse one:
pgloading_eff00.prm rests OPAQUE BLACK at t=38, so anything drawing that screen
at its declared rest paints a black rectangle over all of it. The title's case
only dimmed a frame.

REFUTATION, attempted and SURVIVED: HANDOFF says "exactly the six screen builds
carry the black .prm quad while the six overlays do not". Counting bundles with a
full-screen black primitive gives 8 and 4 -- build_12/15 carry one too. But
theirs runs black -> held -> clear where the transition quad runs black -> clear
-> black, so read strictly as "the quad whose group is the transition" the claim
holds. Recorded anyway: there are two kinds, and the naive census over-counts.

Gate: NEW GAME -> S00A plays 93.75 s against a declared 93.9 -> title, with
98.453 s recorded off the Master bus. What that does NOT show is that S00A's own
audio is in the mix -- bed and movie were not separated in this run, and the
write-up says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 13:12:41 +00:00
Sylpheed port agent
59fba56a0f port: end the boot when BOTH builds have arrived, not when the plate lands
Moving the plate onto the shared clock moved the boot's exit with it: the run
quit at the overlay's settle (t=238) while build 4's own fade-in from black runs
to t=261. pteff00 is still ~7 % opaque there, so the capture came out visibly
darker than the previous one -- with nothing failing, no warning, and no line in
the log to say why. Caught only because there was an earlier capture beside it.

The boot now ends at max(view.settle_time(), overlay.settle_time()) and prints
the unit it is waiting for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 12:58:28 +00:00
Sylpheed port agent
f8a8d17327 port: the ring spins, the plate needs no constant, and rest.t was never the settle
Two milestones' known-wrong bits, both now answered by the RE agent, both taken.

P5 -- the focus ring. It was drawn at 0 with a comment saying so. The period is
now measured (continuous spin, eight evenly spaced autocorrelation peaks over
nine revolutions, no angle estimated anywhere) and it needs NO authored
constant: the period is the element's own declared t=120, and what the
measurement adds is only that the turn repeats rather than stopping -- which
"groups hold" could not decide, because 0 and 360 are the same pose.

`spin_period_units` is structural and narrow on purpose: two keyframes,
differing in nothing but rotation_deg, by a full 360, first timed and second
untimed. 16 of 212 elements in this export match and all 16 are focus rings,
zero false positives. That check is the point -- the measurement was taken on
ONE button of ONE screen, and a rule that caught anything else would be
extrapolating it to elements nobody watched.

Verified on the port's own render with the RE agent's own control: bit-identical
one period apart across the whole frame, 3.6/255 inside the ring's box at
quarter-period steps, and box luminance conserved to 0.027 % over eight phases
-- which is the observable they used to separate rotation from a pulse. Not
claimed: direction (no signed angle was ever measured) and phase across a focus
change (their run held focus throughout).

P3 -- the plate. Last iteration I refuted their authoring instruction and
shipped it anyway rather than pick between two of their numbers. The refutation
held and the answer came back better than either option I offered: AUTHOR
NOTHING. Both builds run on one clock started together and the plate arrives at
its own declared t=238. The 2.13 s constant is deleted.

The premise that failed was mine: rest.t IS NOT WHEN A SCREEN SETTLES. It is the
last hold keyframe before the exit. ptlogo1 stops MOVING at t=42 and then creeps
5 px and 31 alpha steps to t=251. Reading rest.t put build 4's arrival at 4.350 s
instead of 1.967 s, and the "2.51 s, which is not a landmark of anything" I sent
them is that error wearing a decimal point. 238 - 118 = 120 units = 2.000 s
against a measured 2.135 s at 28.1 fps presentation. Checked against my own
export before touching anything.

`ScreenView.settle_time()` still uses rest.t, and so the boot sequencer paces
every screen off the wrong landmark. NOT changed here: "visible arrival" is a
heuristic and getting it wrong re-paces everything. Filed, and asked for a timed
boot instead now that their oracle is live.

REFUTATION: two of their pages measure the same declared 120 units of wall clock
during a static hold and disagree by 2 % -- plate 2.135 s (28.10 fps implied),
ring 2.177 s (27.56 fps). That is seven times the plate page's own 6 ms
run-to-run agreement, and it lands on the argument that page uses to justify
itself: "the build-in is where frames are dropped; the static hold is not". Also
the ring page's band, 27.6-28.8 fps, does not contain its own measurement -- the
mean needs 27.56 and four of seven spacings are outside. Filed, not worked
around: my port uses the declared 120 units either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 12:50:35 +00:00
Sylpheed port agent
4f767e72f6 port: P3 -- the boot title gets its PRESS (A) plate, and two of the RE agent's numbers do not agree
ScreenView now draws two builds at once, which it never had to before. It is a
second ScreenView in the same SubViewport rather than a subordinate screen
inside one: each build has its own timeline, its own textures and its own hold,
which is the entire content of the finding, and Node2D siblings already paint in
tree order. `paint_order` still means what it meant -- an ordering WITHIN a
build.

The delay is authored in flow.json on the BOOT STEP, not on the `title` screen.
What was measured is the boot title; whether the plate is there when the title
is reached again -- (B) from the menu, or after the attract movie -- is not, and
hanging it on the screen would quietly claim that it is.

REFUTATION, and it is the substance of this commit: the RE agent's authoring
instruction does not reproduce the RE agent's own measurement, and the gap is
3.97 s. The instruction is "when build 4 has settled, wait 2.13 s, composite
build 2". But build 2 has a group and this port plays groups -- ptbtn00 is alpha
0x00 at t=214, still 0x00 at t=236 while it slides 10 px up, and 0xff only at
t=238, which is 3.967 s at 60 units/s. So the plate is first VISIBLE at
settle+6.10 s, while what was measured -- the glyph counter leaving 154 -- is
visibility at settle+2.13 s. Both groups starting together puts it 0.38 s BEFORE
settle; build 2 starting at settle puts it at settle+3.97 s; landing on the
measurement needs build 2's group to start 2.51 s after build 4's, which is not
a landmark of anything.

The measurement is untouched -- it is an observation of the running game and I
have no standing to doubt it. What is refuted is the step that turns it into an
authoring rule. So the port ships the instruction, prints the discrepancy on
every boot, and files the row. Same call as the BGM sub-waves: a port that
quietly picks the number that looks right destroys the evidence, because a
corrected boot looks exactly like a correct one.

Also refuted, and it was mine: BLOCKED.md has said since P2 that "no element's
alpha reverses direction anywhere in this export, so nothing pulses". ptbtn00
reverses -- 0x00 -> 0xff -> 0x00 -- and it was in the export the whole time. The
claim had been checked against the screens P2 happened to be animating. The port
still draws no pulse, because no reading of this group yields the measured
2.24 s: the whole group is 4.47 s and from its first keyframe 0.90 s.

Gate: `--boot --capture=` writes one frame of the composited end state, instead
of the 600-PNG filmstrip that was previously the only boot artifact.
`--screen=title --overlay=press_start` raises the same composite in two seconds
for anyone who does not want to sit through 137 s of Theora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 12:40:50 +00:00
Sylpheed port agent
2ad839460c port: P6 -- the menu has sound, and the BGM I "chose" was decoded all along
The three Static.slb cues and the menu bed now export to Ogg Vorbis and play.
`sylpheed_formats::media` does the assembly; nothing in port/ has heard of XMA.

Three things this milestone got wrong before it got right, all recorded in
docs/port/DECISIONS.md because the corrections are the useful part:

1. The cue offsets were a Rust `const` in the exporter. They are MEASURED, not
   decoded -- a measured value compiled into the exporter is a measurement
   wearing the costume of a decoded field, and nobody deletes it because nobody
   can see it. They are authored/audio.json now.

2. I picked BGM_001 and wrote a careful `why` calling the choice arbitrary. The
   menu's music is BGM_103, and it is in HANDOFF at 9ca1eb5 -- the exact commit
   BLOCKED.md says that row was reconciled against. Not stale: wrong when
   written. I had summarised a negative without its reach, so "the TABLES cannot
   say which BGM a screen plays" became "it is not on the disc". One word of
   scope was the whole answer, and the export failed only because BGM_001
   without its .slb extension hashes to nothing. That is luck, not design.

3. The comment above the BGM sum argued for unity gain "because halving is a mix
   decision nobody made". It clipped at +1.8 dBFS. 1/n is the smallest constant
   that provably cannot clip -- the same reasoning video.rs already carried for
   its 5.1 downmix, in this repository, unread.

Unsettled and shipped as such: media::sound_bank_riffs returns THREE sub-waves
for BGM_103.slb where HANDOFF Q10's census says exactly two (the third is the
leading headerless region slb.rs emits for the voice path). The exporter sums all
three and writes a manifest warning, because which bytes belong together is the
decoders' question, not this exporter's -- and dropping one would destroy the
evidence, since a corrected export looks exactly like a correct one. Raised with
the Decoder; row in BLOCKED.md.

The gate is a null control, not a peak reading. A master-bus WAV that is
non-silent proves nothing -- the bed alone would look identical. So the same
scripted walk was run with <- in place of <v>, which fires no cue (Q5, measured),
and the difference is one 0.55 s burst at t=1.10 s and silence everywhere else.
The first attempt at that control returned bit-identical zero and I nearly filed
it as "cues never reach the bus": both runs ended at 1.115 s and the first press
lands at 1.17 s. A null result from an instrument that was not running is not a
null result.

Refutation attempt: HANDOFF Q8's three cue durations. They looked attackable --
0.133/0.172/0.169 s per packet, no shared rate -- but an XMA1 packet carries a
variable number of 512-sample frames, and the three come to 50.0/32.3/95.3
frames. Measured off the decoded Ogg: 0.533, 0.344, 1.016 s, every published
digit. SURVIVES, with its reach stated -- it confirms the assembly path and my
transcription, not the event bindings, which only an oracle can retake.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
2026-08-29 12:29:13 +00:00
Sylpheed port agent
3ddaa74df4 Merge remote-tracking branch 'origin/main' into auto/port-p6-audio 2026-08-29 11:48:28 +00:00
MechaCat02
a8d2491366 audio: actually install the capture path I kept deferring
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m25s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
The audio work was three parts and I shipped two. The transcode-fidelity method
and the pinned 5.1 downmix landed; the null sink -- the only one that answers
"what does the GAME play" -- I deferred to "the next natural rebuild window" and
then rebuilt both images four times without doing it.

pulseaudio-utils is now in both, with tools/audio-capture wrapping it: a null
sink is a real device as far as an application is concerned, so Canary and Godot
open it normally and parec records what they emit.

This unblocks the decoder's Q8. The cue-to-event bindings are currently a name
match against the authors' own identifiers -- a plausible guess, not a
measurement -- and capturing what the game plays on a menu move converts them.

`audio-capture run` reports the peak level and warns when the capture is silent,
because silence is the failure that looks like success: a WAV of exactly the
right duration, full of zeroes, because the application opened a different sink.
A duration check alone passes it, which is how a confident wrong number gets
made.
2026-08-29 13:41:02 +02:00
Sylpheed port agent
9256722a11 port: P5 end to end -- and the port's title never says PRESS (A)
The P5 gate walk starts on a screen. This runs the whole objective instead, and
it is the only thing that would have found what it found:

  xvfb-run -a godot --path port -- --boot --play \
    --script=accept,down,down,down,down,accept,cancel,cancel --shots=/tmp/e2e

publisher wordmark -> developer logos -> ADV (151.9 s) -> title -> (A) -> main
menu -> navigate -> (A) -> EXTRAS -> (B) with focus restored to ptbtn05 -> (B)
-> title. 166.76 s, exit 0, nine frames. Shared as 1788003274-e68367e787d5.

THE PORT'S TITLE DOES NOT TELL THE PLAYER TO PRESS (A). The boot's last step is
`title` = GP_TITLE build 4, and build 4 has NO `PRESS (A) BUTTON` plate. P5 has
just made (A) the only way off that screen.

Not a guess about the art -- both states are captured off the running game and
differ by exactly that plate (live-title-build4-no-plate.png vs
live-title-press-a.png), and the plate is ALREADY EXPORTED as `press_start`,
build 2, sitting in export/screens/title/ unused by anything.

RECORDED, NOT FIXED, and the distinction is the point. This is P3's gate that
P5 exposed, and fixing it needs two things the port does not have:

  * WHICH state an idle post-boot title shows -- build 4 alone, build 4 with the
    plate over it, or build 4 THEN the plate after a delay -- is BEHAVIOURAL.
    The game demonstrably has both states and nothing says which follows the
    intro. The port has no oracle for a sequence; that is the Decoder's.
  * showing it means DRAWING TWO BUILDS AT ONCE, which this port has never done
    -- every mode loads exactly one screen. That is a change to ScreenView, not
    a line in flow.json, and it is not being smuggled in under a navigation
    milestone on the strength of "it looks more right".

Filed in BLOCKED.md. P5's gate is (A) into a submenu and (B) back; both work.

Two smaller things the same run found, both fixed:

  * the boot step's `why` still said "nothing takes the title's place until P5
    gives it somewhere to go". P5 has. Now says what is true: `--boot` STOPS on
    the title (a boot that ends by fading to black looks like a crash) and
    `--play` HANDS THE HELD TITLE OVER -- the stop is not a bug and the handover
    is not another boot step.
  * an empty focus printed as a line that trailed off, reading like a value had
    gone missing rather than like there is none. The title is a screen with no
    `buttons` that still takes (A), so it now prints
    "(none -- this screen has no focusable item)".

Also confirmed: entering a submenu directly (`--menu=extras`) and pressing (B)
enters the parent at its AUTHORED initial focus, not a restored one. There is no
history to restore and MenuFlow.cancel only claims a restored focus when the
stack agrees about where it is going.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtmUw5N5LJaMW1Njb8Ziey
2026-08-29 11:34:52 +00:00
Sylpheed port agent
eef45ecfd6 port: P5 -- the menus navigate, and the focus ring is drawn wrong on purpose
P5's gate is "a human clicks through it". The artifact is a scripted walk that
proves the wiring rather than the intent -- up (wraps 01->05), five down, (A)
into EXTRAS, down, (B) back, landing on the main menu with focus RESTORED to
EXTRAS, ten PNGs one per settled step:

  xvfb-run -a godot --path port -- --menu \
    --script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5

--script posts InputEventAction through Input.parse_input_event so the presses
arrive at _unhandled_input exactly as a d-pad's would. Calling MenuFlow directly
would have been shorter and would have proved nothing: the wiring between a
press and the cursor is the part most likely to be broken, and a direct call is
exactly the part that skips it.

Derived vs authored, which P5 is the easiest place to blur:

  * DERIVED -- the ORDER of the items, from each screen file's `buttons`, which
    the exporter already fills from button-role elements sorted by resting Y.
  * AUTHORED -- destinations, initial focus, what (B) does, and left/right being
    a no-op. All measured off the running game (HANDOFF Q4/Q5) or chosen, none
    on the disc, all in authored/flow.json with a why.

Four of five main-menu destinations are `goto: null` with a `blocked` note. That
is a MILESTONE BOUNDARY, not an unknown -- DIFFICULTY, the save list, the lesson
list and OPTIONS were all measured and live in archives this export does not
carry. `blocked` and `none` are kept apart so nobody later "discovers" the gap.

--headless CANNOT DRAW, and the port hung instead of saying so.

Measured, not assumed: under --headless Godot's dummy renderer never emits
RenderingServer.frame_post_draw, so every capture path awaited it forever --
--capture since P1, --film since P3, --shots as of now. With stdout block-
buffered the observable behaviour was SILENCE, FOREVER, which in a loop reads as
a job still working. Isolated by `--quit` (prints, exits 0) vs `--capture` (zero
bytes, killed at 40 s). Now those three flags refuse at STARTUP naming the
xvfb-run line that works, and --script no longer waits for a frame it is not
going to photograph -- so headless walks the menus in 4.5 s as a cheap
regression check needing no X server.

REFUTATION ATTEMPT, against the Decoder's 7eeae30 point 2 ("the oracle confirms
the game renders the ring's rotation"). Aimed there because PROTOCOL says to aim
at a claim the port is about to build on that rests on an estimator whose own
control the Decoder reported as +/-19.8 deg. IT SURVIVES, more strongly than
claimed.

Both captures draw the SAME sprite (ptbtneff01) 240 px apart, so "is it drawn
rotated" becomes "are these two crops one image at a different angle" -- no crop
offset needed and no reference to our own renderer. 360-bin angular luminance
profile over the annulus, circularly cross-correlated. Two controls first: known
rotations 0/30/90/150/210/270/330 recovered with 0 deg error, and a ring-free
patch of the same capture peaks at 0.369, so the estimator does not manufacture
matches. Then: A vs B 134 deg (corr 0.968), sprite vs A 76 deg, sprite vs B
210 deg -- and 210-76 = 134, which nothing in the method forced.

So 0 deg is NOT A POSE THE GAME SHOWS, and screen_view.gd draws the ring at
0 deg. That is now stated in the code as known-wrong rather than suspected. The
port did NOT start spinning it: the period has two unknowns and both are the
Decoder's -- the second keyframe is untimed, and "groups hold" predicts a stop
at 360 = 0 which contradicts both captures. Two frames of one focused button a
known time apart settle it. Filed in BLOCKED.md and asked over the channel.

BLOCKED.md's staleness check was half a check. It tested whether that page is
stale relative to HANDOFF; it cannot see the other direction, and the other
direction is what happened -- 7eeae30 lands 27 minutes AFTER HANDOFF was last
written and answers a question HANDOFF still lists as open. Added the missing
half: `git log --oneline 9ca1eb5..HEAD -- docs/re/`.

Also recorded, since the two were nearly confused: the ring's annulus centroid
lands within ~0.4 px of its design position under a ZERO crop offset, which
corroborates ORACLE-CAPTURES' "1279x675, top-left aligned" on a feature nobody
chose for the purpose. The earlier "text bands at design y + 23" is an offset
WITHIN the button sprite, not a crop offset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtmUw5N5LJaMW1Njb8Ziey
2026-08-29 11:27:05 +00:00
Sylpheed port agent
60595d4062 port: P5 groundwork -- the focus record, measured and checked against a capture
P5 is the lowest unfinished milestone. This does not implement navigation; it
settles how a focused button is drawn, because three claims sat under that and
none had been checked from this side.

Refutation attempts, all three failed -- recorded either way, per PROTOCOL:

  * HANDOFF ask 3's '(7,7)' focus offset SURVIVES, and more strongly than
    stated: over a 15x14 scan of the whole offset space it is a UNIQUE
    ISOLATED cell at 100% coverage on all five buttons, with (6,6) and (7,6)
    both below 90%. The centre and pivot alignments reproduce the 78-84%
    band the RE agent called misleading.
  * ORACLE-CAPTURES' 'a crop, not a scale' SURVIVES. Its own evidence -- a
    +/-6 px cross-correlation -- cannot tell a crop from a 0.9375 scale, so
    it was re-tested with a scale-sensitive one: button text bands land at
    design y + 23 for all three unoccluded buttons, an exactly 1:1 vertical
    mapping. (The prose understates 45 missing rows as 'the missing row'.)
  * My own suspicion that the declared geometry disagreed with the capture
    by 6 px was MY ARITHMETIC ERROR, written up rather than quietly dropped:
    I took the top-left as pos - pivot. pos IS the top-left; the pivot is the
    anchor scale grows about and cancels at 100%, exactly the 'can be got
    wrong invisibly' that screen_view.gd:120 warns about.

What is actually true, and what P5 does with it:

  * The exporter ALREADY emits the focus record's second element, the 42x46
    ring ptbtneff01, for all five buttons. That gap is in the renderer, not
    the exporter -- nothing to change in crates/sylpheed-export.
  * Base minus focus is (7,7) directly from the declared positions, so P5
    draws each focus element at its own pos and authors no constant.
  * ptbtn04 is 1 px off the 80 px grid ON THE DISC (base rows 162 242 322
    401 482; focus rows a clean 155 235 315 395 475). So its base->focus
    delta is (7,6) while its art aligns at (7,7). Do NOT derive focus
    placement from the base by a constant: it would be wrong on exactly one
    button and right on the other four.

Verified against captures rather than our other renderer: diffing
live-main-menu against live-main-menu-options-focused isolates one cluster at
x 506..702, y 398..445, and ptbtn04's focus record under pos-as-top-left spans
x 500..706, y 395..451. Under pos - pivot it predicts x 433..604, y 367..422,
which matches nothing in the capture and no other button either.

Also records an instrument that FAILED ITS CONTROL and was discarded: a masked
NCC template matcher returned NCC 0.096-0.206 with three of five results pinned
to the search boundary when asked to re-find the base sprites at their known
positions. None of its output is used. Filed so this is not rebuilt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Evuhbt8pxKJEUvwfniwYkU
2026-08-29 10:46:05 +00:00
Sylpheed port agent
cd1b6cfe72 port: reconcile BLOCKED.md against the in-repo HANDOFF at 9ca1eb5
The page cited '/reborn HEAD 9a0ca0d' and that address no longer resolves,
for two reasons it now records:

  * the repositories were merged into one monorepo (65cefa7), so HANDOFF.md
    is docs/port/HANDOFF.md **here** and its provenance is a sha in this
    history -- a sha from the old repo cannot be looked up at all;
  * /reborn is still mounted but is now an EMPTY DIRECTORY, so a test for
    the mount passes and every read under it fails with ENOENT.

Staleness is now checkable in one command instead of by trusting a date:
`git log -1 --format=%h -- docs/port/HANDOFF.md`.

Three rows were listed as blocking that HANDOFF has since answered:

  * Q4 -- and the old row was WRONG, not merely stale. It said A on
    NEW GAME 'hangs the emulator'. Q4 now reads measured for all five
    buttons, NEW GAME -> DIFFICULTY -> SELECT DATA, 'not a hang'. This is
    what unblocks P5.
  * Q9 -- one A skips a movie, measured. P4 had already taken it; the row
    survived only because nobody deleted it.
  * Q6 -- answered as a negative with a stated reach (the driver is code,
    not data, four search spaces closed), which is not the same as unsettled.

The five asks are recorded with what came back: four answered at 9ca1eb5,
and only ask 4 (rotation) still open -- with the human, not the RE agent.

Adds one question the port raised and did not guess: whether the focus ring
spins while focused. Its group ends 0 -> 360 deg on the untimed keyframe, and
'groups hold' cannot decide it, because 0 and 360 are the same pose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Evuhbt8pxKJEUvwfniwYkU
2026-08-29 10:45:47 +00:00
MechaCat02
06676d3dc0 containers: each agent clones the monorepo into its own volume
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 7m48s
CI / WASM — Web (push) Failing after 7m16s
CI / Formatting (push) Failing after 1m15s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
The last structural fix for the collision class that has bitten three times. Both
containers now clone the repository into their OWN named volume instead of
bind-mounting a human's working tree, so an agent's local git config cannot
capture a human's commits, a credential helper cannot leak a container-only path
onto the host, and a `git add -A` cannot sweep another party's in-flight files.

Cloned once at startup and never auto-pulled: pulling under a running agent
moves files out from under whatever it is mid-edit, which is the same bug again.

Accepted knowingly: Claude Code keys per-project memory off the working
directory, so moving off the host path starts that memory empty. The corpus in
docs/ is the memory that matters and it travels with the clone.

Other changes:
* docker/agent -> docker/decoder; the launcher is sylph-decoder. Roles, not
  "the agent", now that there is more than one.
* /reborn is gone -- one repository now, so the port reads HANDOFF from its own
  checkout rather than through a live read-only mount of someone else's tree.
* Canary mounts separately at /canary; it stays a fork tracking upstream.
* A shared `sylpheed-exchange` volume at /exchange, with tools/ on PATH so
  `share` is available in both.
* The decoder's credential file gets the .host-copy treatment the port already
  had -- `credential.helper=store` rewrites by rename-over-target, which is
  EBUSY on a bind mount and reports a fatal that is not one.
* Budget split deliberately: decoder 5 cpu / 6 GB, port 3 / 4, leaving room for
  the planned Referee. "Half the host" was right when there was one agent.

Prompts move to docs/agents/ and are rewritten around the protocol: the oracle
is the running game, dynamic RE stays with the decoder, each iteration must
attempt to refute one claim of the other, and neither may verify its way out of
its own role.
2026-08-29 11:48:30 +02:00
MechaCat02
65cefa74c3 monorepo: one repository for the decoders, the port and the corpus
Some checks failed
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
Merges the Godot port into the reverse-engineering repository, preserving both
histories -- 1019 commits of corpus plus the port's 31, brought in by subtree
merge and then moved into place so git can follow each file across the rename.

The reason is not tidiness. The two-repo split forced the exporter to depend on
the decoders by pinned revision, and that created a whole class of failure that
now disappears: a sha reachable only from a topic branch, orphaned by a
squash-merge, breaking a fresh checkout silently at build time. It also forced a
live read-only mount of one agent's working tree into another's container, which
is why a contract file could move mid-iteration. With a path dependency, a
decoder change and the exporter change it requires land in the same commit or
not at all.

Canary stays separate: it is a fork tracking upstream.

New structure for the long term:

  docs/game/     how the game is NAVIGATED -- menus, modals, prompts, alerts,
                 and in-game flight. Written so nobody rediscovers it. Mostly
                 open questions on purpose; the in-game tutorials are the
                 resource for the flight half.
  docs/port/MODDING.md
                 modding as a constraint on the exporter TODAY, not a later
                 feature: one logical asset in one file (the disc splits nearly
                 everything, and resolving that is the exporter's job), names a
                 person recognises, PNG/OGG/OGV/JSON only, base-and-overrides so
                 re-exporting is always safe, provenance in every file.
  data/base + data/mods
                 generated tree and drop-in overrides, both gitignored
  exchange/      transient inter-agent files, deliberately outside history
  docs/agents/   the team protocol

Both the README and the navigation doc lead with the correction that cost the
most: the oracle is the real game under Xenia Canary. Reborn's renderer is a
hypothesis under test, it has been wrong, and treating it as ground truth
propagated into three documents and both agents before a human caught it.

Scripted modding stays possible without being built: no screen name is hardcoded
in GDScript and there is no native code in port/, which is what Godot Mod Loader
needs to be able to substitute behaviour later.
2026-08-29 11:34:46 +02:00
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
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
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
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
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