d0f6dfa86131b8a24f9be3db4e10caf3703538d0
308 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
39209eab05 |
port: the title's sweeps loop, the black hold is 9 units, and one claim refuted
THREE THINGS FROM THE DECODER, one of which I am not taking. REFUTED: "the developer splash is one composited quad, the bounding box of the three logos". The observed quad is 525x259 at (378,155). The three logos' bounding box is 500x421 at (390,164) -- a 259-tall quad CANNOT contain them, and palogo_anima alone starts at y=449, thirty-five pixels below that quad's bottom edge. The observed quad matches the union of gamearts_eff and seta_eff, 521x261 at (379,154), to about four pixels in every dimension -- and both of those are TRANSIENTS my own census flagged, dark by t=45, so a frame containing that quad is a build-in frame rather than the settled screen. I cannot see their draw stream, so I sent the arithmetic rather than a verdict, and the port keeps drawing three: I will not stop drawing an element on a claim whose stated identification excludes that element from its own bounding box. THE BLACK HOLD IS 9 UNITS, NOT 12. I authored 12 from Q7's luminance plateau of 0.17-0.23 s, supported by the menus' transition quad. The Decoder counted SUBMITTED QUADS instead -- luminance cannot separate the outgoing fade's tail from true black. Four frames with no sprite quad at all, at 2.284 units/frame derived from the disc as its own clock, gives 9.1 units = 0.152 s (6.9-11.4). That overlaps the luminance figure only at the top, and the true black is SHORTER still since both boundary frames carry picture. My 12 was supported by analogy -- a different screen's quad on a different path -- and a number that fits by analogy loses to one measured in place. verify-dwell's bound moved with it; both screens still agree. THE TITLE'S SWEEPS LOOP. The oracle shows the quad oscillating over its whole x range and resetting hard, one reset in the first title dwell and two in the second. The loop-length field could NOT have settled it, correcting a hope I had stated: both records declare exactly their last keyframe time, slack zero, and "loops at 600" and "runs once for 600 and stops" write the identical header. Verified on the two sweeps' LCM, since their periods differ: 600 and 720 realign at 3600 units, mean diff 0, against 0.438 at half that. Scoped to the title. The menus declare the same lengths but the oracle measurement is of the title, and my own weak evidence points the other way there -- best match with the sweeps off-screen, three times worse mid-screen, against a 73% on-screen duty cycle if they looped. Two weak signals in opposite directions is a reason to scope, not to pick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
5dbc9aeac0 |
port: delete exit_ramp_units, invert the format's own rule, and guard a scale-0 leaf
FOUR THINGS, and the first is what MISSION section 3 calls the measure of
progress.
DELETED `exit_ramp_units` AND `exit_ramp_seconds`. They were authored because the
disc had no time slot on a group's final keyframe, so the ramp into it was the
one unknown duration per screen. Under the corrected record layout that keyframe
does not exist -- a group is an 8-byte header then frames x {u32 time; 36-byte
pose} and every pose is timed. VERIFIED DEAD BEFORE DELETING: setting it to 9999
(166 s) moved the boot's transitions by 0.04 s, which is wall-clock jitter, and
both uses in ScreenView are gated on a condition that no longer fires on any of
the export's 866 keyframes.
INVERTED THE FORMAT'S OWN RULE. `check.rs` enforced "the final keyframe has no
`t`; the disc has no time slot there" and FORMAT.md stated it. Both are now
backwards, and the validator fired 150 times on a re-export. I had not run
`check` between pinning the tag and measuring against the oracle -- the pixel
harness was green while the format validator was failing on every screen with a
multi-keyframe group. A correctness harness does not replace a format one; they
fail at different layers.
GUARDED A SCALE-0 LEAF, which the Decoder hit in its own renderer: its leaf
branch marked the element drawn unconditionally while the blit returned early on
zero scale, so a scale-0 leaf suppressed its parent and blanked the element --
live on all four loading screens. This port did not have the bug only because
authored/rendering.json happens not to list pgloading_loop5. That is an accident
of a gate written for another reason, not a defence, so `_draw_leaf` now reports
whether it drew and `_draw` falls back to the parent.
ISOLATED THE PACING QUESTION rather than leaving it as a suspected regression.
Legacy association: publisher 4.70 agrees, developer 3.92 DIFFERS. Corrected:
publisher 4.26 DIFFERS, developer 3.62 agrees. Both misses are ~0.03 s outside a
composite bound. The association traded which screen is marginally out; it did
not regress the pacing.
Bumped the pin c -> d for the parser and audio changes. Its headline renderer
change does not reach this port: sylpheed-cli builds from the workspace crate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
8994ca7c59 |
port: the 11.5 px was the fit's resolution -- and the lesson inverts
The Decoder closed it by ADDING OBSERVABLES, not by tuning. The vertex buffer carries positions and colours at the same instant, so all four quantities must agree on one t: quad A x solves to 357.88 and quad B x to 357.58, both +/-0.12 units, against 355.75 +/-1.54 and 354.09 +/-1.89 from the alphas. Alpha moves only 0.27-0.33 levels per unit, so one byte of quantisation is worth 1.5-1.9 units -- 6-8 px of sweep at 4 px/unit. That is the whole of the 11.5 px. At t=357.7 the centres land within 0.70 px and both alphas inside one level. THE LESSON IS THE EARLIER ONE INVERTED AND IT IS THE HALF WORTH KEEPING. Checking a wrong rule against alpha made it look confirmed; here the same insensitivity MANUFACTURED a residual that did not exist. An insensitive quantity does not merely fail to falsify -- it invents error. Solve on the fastest-moving field, check the slow one, never the reverse. I was already looking for a pivot rule to explain 11.5 px when they wrote; there was nothing to find. REFUTATION ATTEMPT, survived with a nuance: they state the leaf pivot is (200,90) on a 399x180 sprite, "the pivot is the centre, so rotation displaces it by nothing". Checked against my export -- pivot [200,90], sprite 399x180, true centre 199.5,90. It survives, but the sprite is ODD-WIDTH so the pivot is the centre to within half a pixel rather than exactly. No consequence against their 0.70 px agreement; worth stating because "displaces it by nothing" is the kind of sentence that later gets leaned on for a sub-pixel claim. verify-capture now poses the title at t=357.7 rather than 355: RMSE 21.07 -> 20.92, differing 1.82% -> 1.81%. Marginal, and it is the right pose for a stated reason rather than a better number. AND ptlogo_eff2 IS WITHHELD FOR A BETTER REASON THAN MINE. I had it on caution about untested generalisation; the Decoder points out it is on title_jp and MISSION section 7 scopes out "localisation beyond English", so it is not a question this port has to answer and the parked Japanese capture does not need reviving for it. authored/rendering.json now gives scope first and undecidability second. Widening scope to close a residual would have been the wrong trade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
42474642e0 |
port: pin formats-pin-2026-08-29c -- the knob I tested last iteration was retired
I tested the wrong switch. SYLPHEED_KF_TIME_SHIFT is a superseded partial fix: it
got the association right but LEFT POSE 0 UNTIMED, which is exactly why the
untimed keyframe appeared to move from last to first. The real correction is the
DEFAULT in the tagged crate, with the old reading behind SYLPHEED_KF_TIME_LEGACY.
So last iteration's five rows measured a mismatch against a knob nobody should
use -- I suspected they were not decisive, I did not suspect the knob was retired.
THE CONSEQUENCE IS MUCH SMALLER THAN I BUDGETED. A placement group is an 8-byte
header then frames x {u32 time; 36-byte pose}, so pose 0's time is the group's
lead-in word and every pose is timed. Measured on the re-export: 866 keyframes,
0 untimed. `pose_at`'s "the final keyframe carries no t, so give it a synthetic
time" premise does not invert, it DISAPPEARS -- dead code rather than wrong code,
which is why nothing needed re-deriving. And the leaf now reads t=0 x=-639,
t=150 x=-39, t=540 x=1521, giving x=781 at t=355: the Decoder's predicted
top-left, and the 1300 px discrepancy is gone.
Pinned by tag, which is what MISSION section 2's tagging rule is for. BLOCKED was
wrong in both directions -- "cannot be taken yet" AND "only when that branch lands
on main". It arrives when the tag is pinned.
COST STATED: sylpheed-cli builds from the workspace crate, so until this reaches
main the exporter and the reference renderer read different decoders and
verify-screen compares two eras. verify-capture is unaffected -- it compares
against oracle captures and never touches the CLI. Revert to the path dependency
when the tag is an ancestor of main.
Oracle: publisher_logo 1.00% -> 0.75%, developer_logos 0.39% -> 0.33%, and
extras' differing region COLLAPSING from 736x525 to 398x295 at the sweep position
-- the residual localised onto the one element still in question. title unchanged
at 1.82%, now posed at t=355, the Decoder's FITTED sweep time. t=390 measures
1.65% and picking it would be fitting the pose to the score.
REFUTED, MINE: "ptlogo_eff2 is the single drawn element at a scale that is not a
whole multiple of 100%". That census was parents-only; the 45 leaves hold
thirteen distinct non-whole-multiple scales and 125% is among the rarest at two.
The claim's real content was "the only one the port draws" -- about my element
set, not the disc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
ffb54429fb |
port: S00A is closed as not obtainable, and Q5's shape is tighter than "unstable"
Three things folded in from the Decoder, none of which change the export. S00A IS NOT OBTAINABLE HERE and it is the game that stops it, not the drive. The drive works end to end -- main menu +0.999, newgame-difficulty +0.999, newgame-selectdata-crash +0.997, with the focus detector validated live against a known transition rather than trusted -- and then the guest throws at PC: 0x82307128 x349 with no S00A stream ever decoding. Recorded in BLOCKED as a route finding, with the consequence for me stated: THE CENTRE-CHANNEL RESULT RESTS ON ADV ALONE. S00A was wanted precisely because its second stream is digital silence where ADV's is a 0.60x copy. Q5 GAINED TWO POINTS and the shape is sharper than "it varies": TUTORIAL x3, NEW GAME x3 over six boots, and no other item ever observed. That does not change the port's authored initial focus -- NEW GAME is still one of exactly two observed states and is the state of the committed capture -- but it changes what would REFUTE it, so the authored `why` now says so: a boot opening on LOAD GAME, OPTIONS or EXTRAS breaks the two-way shape, and a rule predicting the split deletes the entry outright. `which-focus` NEEDS GODOT AND THE EXPORT TREE, so it cannot run in the RE container. Recorded in its own header rather than left for the next agent to discover, along with the pointer to the capture-only alternative and that tool's own caveat: its offline controls are its calibration inputs, which is self-consistency and not validation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
14ec1c1df8 |
port: which-focus -- a focus detector for the Decoder, with the control wired in
S00A is blocked on knowing which button a screenshot has focused. `newgame_path.sh` assumed NEW GAME at boot, drove on it, and landed in a tutorial mission -- HANDOFF Q5 measured focus as UNSTABLE across boots. Counting presses cannot substitute: up from the first item wraps to the last, so no fixed number of presses lands on a known item from an unknown start. The Decoder's own attempt, a per-row brightness statistic, FAILED the control -- it picked NEW GAME on the capture whose filename says OPTIONS. The render-difference method passes it, so this packages it as a script. IT RUNS THE CONTROL ON EVERY INVOCATION, not once when it was written, and refuses to report anything if the control fails. live-main-menu-options-focused KNOWN ANSWER OPTIONS 4.7x live-main-menu the question NEW GAME 11.4x live-extras KNOWN from corpus MISSION SELECT 4.2x live-title-press-a no menu at all refuses 1.0x The extras row is a second known answer I did not plant -- authored/flow.json already records "MEASURED: EXTRAS opens focused on MISSION SELECT (live-extras.png)" -- and the tool reaches it independently. The title row is the negative control. AND THE REFUSAL NOW CARRIES A NON-ZERO EXIT CODE. The first version printed "do not act on this" and exited 0, so a caller scripting it -- which is the entire point -- would have read a refusal as an answer. Same defect as a checker claiming a check it skipped, and the fifth instance of that shape this session. What it is not: it identifies focus in ONE FRAME and says nothing about what selects focus. Q5's instability stands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
3d3d620913 |
port: the capture has NEW GAME focused, and my harness had been rendering no focus
The Decoder attached an honest caveat to its reproduction of the tone-curve
refutation -- its bins included the focus-state mismatch, so it was "not a clean
second opinion". Removing that caveat found a defect of mine.
IDENTIFYING THE FOCUSED BUTTON, with a known-answer control. Walking focus
through all five buttons and comparing each against a capture:
vs -options-focused vs live-main-menu
ptbtn01 NEW GAME 6351 531
ptbtn02 LOAD GAME 7087 7094
ptbtn03 TUTORIAL 6230 6237
ptbtn04 OPTIONS 1292 6364
ptbtn05 EXTRAS 6073 6080
The control picks OPTIONS by 4.7x -- the answer its own filename gives -- so the
test is trustworthy, and live-main-menu.png has NEW GAME focused by 11.5x. That
is what the port already focuses. Corroboration, NOT a decode: Q5's instability
stands (four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME) and this
identifies one frame rather than a rule. Recorded in the authored entry.
THE DEFECT: `verify-capture` rendered menus with `--screen=`, which draws no
focus record, so it had been comparing main_menu to the oracle in a state the
oracle was never in.
main_menu 0.25% differing, RMSE 14.79 -> 0.06%, RMSE 13.21
extras 0.46%, RMSE 15.29 -> 0.20%, RMSE 13.38
A 4x improvement that was entirely my harness posing the port wrong rather than
the port drawing wrong. The first run of a new correctness harness reported a
discrepancy and three quarters of it was the harness.
AND THE DECODER'S CAVEAT RESOLVES TO NOTHING, measured rather than assumed:
re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 /
1.18 / 1.10 / 1.03 / 0.94 at render 8..48, unchanged from the mismatched run. Its
reproduction stands as a second opinion after all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
899c5da066 |
port: build the correctness harness verify-screen has pointed at since P1
`tools/port/verify-screen` line 20 has said "use `tools/verify-capture` for the
correctness question" since P1, and there was no such file. The port has had a
harness comparing itself to sylpheed-cli -- two renderers sharing its assumptions
-- and none comparing it to the game, while its own docs said otherwise. That gap
is what ORACLE-CAPTURES.md warns about: this corpus has been bitten three times
by two renderers agreeing, and every one was obvious against a capture.
Five screens against framebuffer captures of the real game:
main_menu RMSE 14.79 0.25% differing focus state may differ
extras RMSE 15.29 0.46% focus state may differ
title RMSE 21.07 1.82% ptloop sweeps never stop
publisher_logo RMSE 10.77 1.00%
developer_logos RMSE 9.37 0.39%
NO SCREEN SHOWS A LARGE CONNECTED BLOB -- the shape a missing element makes, and
the shape all three historical failures made.
And 74.1% of main_menu's differing pixels fall inside the ORACLE'S OWN focus
signature (live-main-menu vs live-main-menu-options-focused, the same screen with
a different button lit). So the bulk of that disagreement is a state mismatch,
not a defect.
REFUTATION ATTEMPT, on ui-render-tone-curve.md's `capture = 255*(render/255)^g`.
It survives where it was measured and not past it. Binning every structurally
matched pixel by render level gives the relationship directly, and the implied
exponent is NOT constant: 1.26 at render 16, 1.10 at 32, crossing 1.0 near 44,
down to 0.69 at 96. Above ~44 the capture is BRIGHTER than the render, which one
exponent cannot express -- and that is exactly why my whole-frame fits kept
returning 1.00, the two halves cancelling. The page's own stated reach ("nothing
constrains midtones or highlights") was not a hedge, it was the finding. Its 1.49
for this screen measures 1.18-1.26 in my darks; recorded as a disagreement rather
than resolved, since they fit selected flat patches and I binned everything.
Two earlier versions of this tool reported a best-fit gamma and were wrong both
times -- once fitting across a 74% structural mismatch, once extrapolating past
the measurement's stated reach. The fix was not a better fit but a different
instrument: it prints the curve, which somebody can argue with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
b8f939cc4c |
port: stripping driver padding is exact -- the gate on S00A is cleared
The Decoder made this the gate on S00A and that was the right order: ADV plays itself on boot and can be captured with --gpu=null at 0.96x real time, but S00A starts ~4.5 s after (A) on a save slot, which needs a driven run, which needs screens, which rules out --gpu=null. So S00A is necessarily the 0.70x rendered route with ~10% additive padding, and is only worth a boot if stripping is exact. It is. A real music+SFX bed -- 137.37 s, carrying 454 genuine zero runs of its own -- had 1149 holes inserted at 8.37/s to +9.9% length, matching the observed ALSA profile, then was stripped and correlated in the low band: original vs itself r 1.000 lag 0.0 s margin +0.141 ceiling PADDED vs original r 0.436 lag -12.2 s margin +0.006 destroyed STRIPPED vs original r 1.000 lag 0.0 s margin +0.142 recovered stripped vs orig-also-stripped r 1.000 margin +0.143 Two things worth reading off that. Padding at that profile destroys correlation completely -- r 0.436 in the known-absent regime -- which independently confirms on a file whose contents I control that the earlier captures were unusable for the reason claimed and not for some other one. And recovery does NOT require stripping both sides: the stripped capture matches the UNSTRIPPED source at the ceiling, so the port's reference assets never need touching. `tools/port/strip-padding` implements it, and its header leads with when the operation is vandalism rather than with what it does: PulseAudio's monitor SUBSTITUTES silence and deleting those holes repairs nothing, while Xenia's ALSA writer PADS and removing that is exact. Running it on the wrong artefact would look like it worked. Its output is byte-identical to the control's own stripping, so the tool and the experiment are one operation rather than two implementations that agree. Not licensed by this: stripping removes genuine silence too and cannot tell them apart. Here the genuine runs total 0.71 s in 137 s and cost nothing measurable; on material that is mostly silence they would. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
421d6df1b0 |
port: check-capture refuses a format it cannot read, and accepts the one it nearly rejected
The Decoder is moving to an ALSA `type file` tee, which writes float32. Read as s16 that yields a plausible-looking file whose only tell is per-channel peaks alternating exactly -- the two halves of each float landing in alternate channels. My parser assumed s16 throughout and would have mis-read it confidently. An unreadable format now ends the run at PARTIAL (exit 2) rather than PASS: channels were checked, starvation was not, and the tool says which. A checker that claims a check it skipped is the shape of every failure this file documents. AND THE FIRST VERSION OF THE GUARD WAS TOO STRICT -- it rejected one of this tool's own controls, a six-tone file `ffprobe` correctly calls pcm_s16le, because the file is WAVE_FORMAT_EXTENSIBLE (tag 0xFFFE) rather than plain PCM. A format guard that refuses a legitimate capture is the same defect as one that mis-reads an illegitimate one, pointing the other way. The check turns on wBitsPerSample, which is what decides the layout; a float tee is 32-bit and still caught. Control sweep, now the tool's real specification and all of it runnable here: real music+SFX bed PASS voice track, mono, 53% real pauses PASS six distinct tones, PCM and extensible PASS bed with 350 ms holes punched in FAIL the starved capture FAIL the same tones as float32 PARTIAL Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
f2e08ae31d |
port: check-capture needed two numbers -- the rate alone passed a 50%-silent file
The Decoder found a blind spot in the bar I shipped last iteration. Raising the PulseAudio client buffer keeps cutting the gap RATE while total silence bottoms out and then doubles -- an over-large buffer starves in a few enormous holes instead of many small ones. Its 500 ms capture scores 1.3 gaps/s, better than a genuine music bed at 3.3, while being 50% silence. My 20/s bar passed it. Same shape as the level table that cannot see a duplicated channel: one number, blind to the failure next door. I did not set a bar on their numbers, because I do not hold those files and the last two bars in this tool were wrong precisely from being invented. Instead I built a control in that regime -- `bigholes`, a real bed with 350 ms holes punched in -- and set the rule from four controls I can run: real music+SFX bed 1.1% silence, 3.3 gaps/s PASS voice track, mono, pauses 53.2% silence, 0.3 gaps/s PASS bed with 350 ms holes 46.3% silence, 3.2 gaps/s FAIL the starved capture 35.6% silence, 30.9 gaps/s FAIL Rate alone cannot separate rows 2 and 3; silence alone cannot separate 1 and 3. The pair does: fail when >=10% is silent on every channel AND there is at least one gap per second. Real audio is either mostly not silent, or silent in a few long stretches -- not both at once. AND THE REGIME IT STILL CANNOT JUDGE IS PRINTED RATHER THAN PASSED. High silence with very few gaps is what a real voice track looks like and what an over-buffered capture looks like; nothing here separates them, so the tool says UNJUDGED and tells the reader to check against a known source. Inventing a bar for a regime with no control in it is how the previous two bars came to be wrong. A CONTROL THAT DOES NOT EXECUTE IS NOT A CONTROL: the tool returned immediately for single-channel input, so the mono voice track -- one of the four controls -- was never run through the check it was meant to control. Mono now skips only the duplicate test. Also recorded: the Decoder has withdrawn "the monitor-sink route cannot be fixed by configuration". A ~200 ms client buffer is worth a retry BEFORE anyone spends a session on a Canary rebuild. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
528043ef88 |
port: make the dwell comparison repeatable, and record that the settle run is unanchored
TWO THINGS, and the first is that nothing needed changing. The Decoder withdrew one of the two legs under its settle-time run: the plate pulse period it had offered as proof the run was not slowed rests on one interval at a 125 ms sample rate, and re-picking the troughs gives 2.628 s rather than 2.369 -- an adjacent local minimum counted as a separate trough. It cannot resolve a real-time factor below ~7%. Nothing in the port moves, because the numbers that correction touches were already unauthored. Checked rather than remembered: grep over authored/ and port/scripts/ finds no 0.531 and no 0.482. The only build-in reference in the tree is the plate arithmetic t=118 -> t=238, 120 units, which is the anchored leg -- it agrees with three prior readings and with the disc's own declaration. I had declined those two as one-run figures the Decoder itself flagged, with the port already within ~0.1 s from the disc's keyframes. That reasoning now has a second, independent justification I did not have at the time: a few per cent of slowdown sits inside them undetected. SECOND: `tools/port/verify-dwell`. Last iteration's hand comparison refuted a red flag I had filed myself -- `rest.t` is the wrong settle landmark, but "everything the sequencer paces off it is therefore late" was false and I nearly re-paced screens that already matched the game to 0.05 s. That check existed once, in a transcript. Now it runs. Its header carries the trap it exists to prevent, because that is the whole point: a port's TRANSITION TIMESTAMPS and the oracle's VISIBLE SPANS are not the same quantity, and differ by the exit ramp plus the black hold -- about 0.6 s, the entire discrepancy. The same confusion cost this corpus 0.48 s on the plate delay. The bar is the oracle's own run-to-run spread plus one film interval. Three cold boots of the real game differ by 0.3 s, so agreeing more tightly than the oracle agrees with itself would mean nothing. The developer-logo span reads 3.50 s on the hand-run and 3.75 s here, one interval apart and both inside the bar -- the tool reporting its resolution rather than hiding it. The oracle's numbers are in the script as a labelled test fixture citing their RE document; nothing in the port derives them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
a8d2491366 |
audio: actually install the capture path I kept deferring
Some checks failed
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. |
||
|
|
a8815f2826 |
agents: the team protocol, the share tool, and a player's-eye navigation doc
Some checks failed
**navigation.md rewritten from the player's chair.** It was written from the inside out -- GamePart ids, pak names, sprite names -- which is how WE find things, not what the game shows anyone. Now it describes what is on screen, what you press and what happens, with internals as footnotes. Most rows are open on purpose: it exists to be filled in by playing, and the in-game tutorials are the resource for the flight half. **tools/share** gives transient files provenance without giving them history. Three kinds of thing were travelling down one channel with opposite needs: code and decoded knowledge want permanence, cited evidence wants permanence, and "look at this PNG" wants no history at all. The third kind bloats a repository forever; passing it by message is worse, because the receiver gets bytes with no idea which build produced them. `share put` records who, when, what, the sender's commit, and whether their tree was dirty -- because a capture taken from a modified tree cannot be reproduced from the sha, and the receiver deserves to know that before building an argument on it. **docs/agents/PROTOCOL.md** is the contract. The parts that matter: Dynamic RE stays with the Decoder -- most of what is open is behavioural and cannot be answered from the file. What the planned Referee adds is different: bias enters at what you CHOOSE to capture, so a corpus captured to a fixed protocol by someone with no hypothesis is worth more than one captured to settle an argument. A message may point, ask, prioritise and challenge. It may not change scope, redefine ground truth, or carry a finding instead of writing it down -- including a message claiming to relay the human, because a relayed instruction has no evidence attached and this project has watched a wrong belief travel further and faster than its correction. Adversarial duty is explicit: every iteration, try to refute one claim of another agent and record the attempt either way. Run your own instrument through a control first. Disagreements go to the human with both positions, not to whoever is more certain. And no agent may verify its way out of its own role: the Port has no oracle, the Decoder builds nothing, the Referee interprets nothing. |
||
|
|
65cefa74c3 |
monorepo: one repository for the decoders, the port and the corpus
Some checks failed
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.
|
||
|
|
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). |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
6d9214fd07 |
docs: check that the docs' headline figures match their committed data
Nothing had ever verified that a number written in prose matches the reference data file committed beside it. The figure is written once from a run; the prose is edited around it afterwards and the data file is regenerated independently, so drift is silent. All 19 headline figures across four censuses -- the eff-bit census, the plateau census, the top-level rotation census and the eff-bit alpha test -- currently agree with their data files. The checker had to be numeric, and the first attempt is the reason it is a script rather than a grep: comparing strings reported almost every figure as a mismatch, because the data files write 14709 where the docs write "14 709" with a thin space, and the docs round 33.66 to 33.7. A consistency check that fails on formatting trains you to ignore it, so the tolerance is explicit: exact against the data, within 0.05 against the doc to allow rounding. Also ran the full disc-gated workspace suite (build-reborn test, which wires SYLPHEED_DISC -- without it the disc tests self-skip and green means almost nothing), covering this session's three decoder changes: rotation_deg on Keyframe, the scale-0 fix in blit/fill_quad, and the flags field on T8adImage. 122 passed / 0 failed across the four suites that had completed; the long disc-gated integration tests (records_roundtrip_disc, first_header_word_is_record0_hash) were still running and are not counted here. |
||
|
|
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. |
||
|
|
489ea12759 |
re(ui): the eff-name implication for bit 0x02 is refuted disc-wide
Last iteration I killed the biconditional and reported that the one-way
reading survived: all 10 bit-set sprites on GP_TITLE build 4 are eff
names, so "bit set => eff name". Checked over the disc, that is false.
sprites with a resolvable preceding name 14 709
bit SET & name has 'eff' 2 338
bit SET & name lacks 'eff' 2 657 <-- counterexamples
bit clear & name has 'eff' 1 399
bit clear & name lacks 'eff' 8 315
P(eff | set) = 0.468
P(eff | clear) = 0.144
The implication fails more often than it holds. What survives is an
association -- 3.3x enrichment -- and build 4's 10/10 was a local naming
habit in an 18-element bundle, not a format rule.
The counterexamples are the useful part: pv_loading_ring0,
pv_loading_light0-3, pv_loading_line, px_bunk_line, px_top_extra. Rings,
glows, lights, thin lines -- effect-like artwork that does not carry the
eff naming convention. Consistent with the bit marking effect sprites by
authoring intent rather than by name, which is a description and not a
decode, and is labelled as such.
Names here come from the string immediately preceding each T8aD,
validated 17/18 on build 4 against the RATC child order; the single
mismatch is the known pteff04.t32 -> registered as 8AX case, so this is
the element (opt) name rather than the sprite's registered name. That
mismatch is itself an independent confirmation of the 8AX finding,
reached from the opposite direction.
METHOD: a pattern perfect on one screen can be near-chance on the disc;
and when an association survives a refuted implication, the
counterexamples are the finding.
|
||
|
|
3490fba9e3 |
re(ui): settle 8AX vs ptbase statically -- the game draws the full-res one
I had parked this as "needs a per-draw capture recording texture base addresses". It did not. 8AX (1280x720) and ptbase (640x360 at 200%) are the SAME artwork at two resolutions, which is exactly why comparing either against a capture is inconclusive -- and why comparing their DIFFERENCE is not. Compute 8AX - upscale(ptbase), the detail only 8AX has, and ask whether the capture contains it. Both candidates are first mapped into the capture's tone domain with the measured gamma; without that the residual is dominated by the tone difference and the test is blind. main menu corr +0.0475 controls +0.0032 shift, -0.0075 flip 68% of ceiling title corr +0.0634 controls +0.0095 shift, +0.0086 flip 68% of ceiling Two independent screens, both at 68% of the theoretical ceiling (sd of the 8AX-only detail over sd of the capture residual), 7-15x their matched controls. The controls preserve spatial correlation and destroy only alignment, so they are what "no signal" looks like. So the recommendation changes: resolve the name and draw 8AX at 1:1. Upscaling ptbase 2x is wrong, not merely softer. Still do not draw both -- an opaque layer over an identical one costs fill and hides later changes, and ptbase's element is the one carrying the keyframes, so a consumer needs its timing with 8AX's pixels. Also recorded and withdrawn: a cruder pixel-pair test gave 0.00-0.72 for upscales, 0.98 native and 1.01 for the capture -- apparently decisive. Additive noise raises both terms of that ratio equally and drives any value toward 1; fitting a noise term, both "native + noise" and "bilinear + noise" reproduce the observed numbers. The conclusion is right, that test does not establish it, and it is in REFUTED because the number looks conclusive and is not. Not shown: whether ptbase is also drawn underneath. 8AX is ~86% opaque and carries the same art, so it would hide it either way. |
||
|
|
aaaa08b164 |
docs: the UI decode's own evidence images were unreachable -- 11 links repaired
The brief's rule is to commit reference data beside the finding so the port can be built without a disc. Nothing had ever checked that the docs' cited artifacts actually exist. doc_link_check.py walks every markdown file under docs/, resolves each relative link, and reports targets that are missing -- and separately targets that resolve to a ZERO-BYTE file, which looks fine in any listing. links resolving 1038 -> 1049 missing targets 16 -> 5 empty targets 0 -> 0 +11 resolving and -11 missing against 11 edits: the counts pair, which is the confirmation the pass did what it claimed and touched nothing else. Two of the sixteen were the evidence for the UI layout decode itself. structures/ui-rat-layout.md is what the port is built on, and its two figures -- backing "the tutorial PAUSE menu rebuilds pixel-accurately from its sprites" and "the same method reproduces the main menu" -- were written as captures/ui-layout/... from a file in structures/, one directory too shallow. The headline evidence for the decode could not be opened from its own document. Eleven links had the wrong relative depth with the target present. Each was rewritten only where exactly one candidate path resolved, so nothing was guessed; the first pass left three alone because equivalent spellings (captures/../captures/x) failed to collapse, and a second pass normalised them. Five remain genuinely absent and are left rather than invented: two point at MEMORY.md outside the repo, one at a header in the separate xenia-canary-native tree, and two name documents that were never written (weapon-datasheet-runtime.md, canary-build-verified-env-confound.md). None is port-relevant. A missing document is a different problem from a bad path and is not something a link fix should paper over. |
||
|
|
7373035868 |
re: the port was still being told SE audio is undecodable -- it is not
A resolve-check on HANDOFF's own rows. Q8 read "SE audio is undecodable
from the disc -- no XACT container exists anywhere". menu-audio-cues.md
retracted exactly that ("### Retracting 'cannot be extracted'") and
locates three cues in Static.slb that decode to PCM: d-pad move 0x1ec0
(4 packets), (B) back 0x0ec0 (2), (A) confirm 0x5d6c0 (6), all mono
48 kHz. The retraction landed in docs/re/ and the page the port reads
kept the superseded text -- the fourth time in this corpus.
Writing the rule down has not worked, so there is a tool now.
handoff_lint.py flags every HANDOFF line making a strong negative claim
that links a doc containing retraction language. First run: found the Q8
row, plus one benign false positive (Q3 links a doc whose retraction is
about a sprite count, not about the tie-break -- checked, and HANDOFF
repeats none of the retracted figures). The lint also caught its own bug
first: it reported existing docs as missing because it joined a guessed
repo root, so it now resolves links relative to the file as markdown does.
Separately, EXTRAS's paint-order risk narrows twice more. Of its 15 tied
pairs only 2 overlap, and of those, ptloop01 x ptloop02 are loop*
animations compose skips by default -- so exactly ONE tie can be drawn:
ptframe3 x ptframe4, overlapping 102x132 px. Against live-extras.png that
contested region correlates +0.9622, better than the whole frame (+0.9440)
and inside the range of regions where order cannot matter (+0.8502 /
+0.9903). Consistent with our order, not proof: correlation cannot see a
swap between locally similar art.
15 -> 2 -> 1 -> consistent is now the whole paint-order risk on the five
screens, and HANDOFF says so.
|
||
|
|
c3cf3c2e81 |
re: a title negative that survives its own cross-check
Three earlier "the title never appears" claims came from instruments later found broken -- a stale pixel oracle, a 41 s sampling interval, a freezing stream. This one carries its own evidence. title_probe_xchecked.py restarts its capture stream every 30 s AND prints its reading beside an independent `import` grab every 60 s: 1851 frames in 560.2 s = 3.30 fps cross-checks 9, disagreements 1 max glyph 0 t= 62s stream 6.05 | import 0.07 disagree (a fade, logos mid-transition) t=123s stream 7.40 | import 7.49 agree t=183s stream 8.18 | import 8.29 agree t=243s stream 0.23 | import 0.10 agree t=311s stream 89.68 | import 89.51 agree t=371s stream 80.97 | import 81.58 agree t=426s stream 117.43 | import 117.72 agree t=487s stream 77.71 | import 76.25 agree t=546s stream 70.43 | import 70.55 agree Eight of nine agree within 2%, fps held at 3.30 with no collapse to 1.60, and the surface moved through dark and bright phases. So the frames were live: over 560 continuous seconds from launch, sampled 3.3 times a second, the interactive title's green (A) plate never appears while the game renders throughout. The final frame correlates 0.0145 / -0.0047 / 0.0102 with our title / main menu / EXTRAS renders -- attract-movie content, not a UI screen. Why remains unknown. live-title-press-a.png with its 753 glyph pixels proves the title was reachable from this container on 2026-08-28, and clearing the shader cache fixed the black surface but not this. The two emulator-side questions (gamma control, 8AX vs ptbase) are therefore blocked on a characterised failure rather than a suspicion. Neither blocks the five menu screens, so I am returning to static work; the probe is committed for whoever picks it up. METHOD: a probe that cross-checks itself turns "no result" into a result. |
||
|
|
46006406a4 |
re: the fast probe stalls -- its own dense negatives are withdrawn
Cross-checked the instrument built last iteration against an independent grabber while both watched the same screen, and it fails. A single long-lived ffmpeg x11grab stream degrades and then freezes: 862 frames in 540.1 s = 1.60 fps (it starts at 3.98) t=450/480/510/540 s: surface mean 5.21, identical every time At that same moment `import` read surface mean 125.65, and a freshly started ffmpeg stream read 122.43 -- agreeing with import to 3%. So the acquisition was broken, not the analysis: the stream replayed a stale frame while the screen was 24x brighter. That withdraws last iteration's headline. "2391 frames over 600 s from t=0, max glyph 0" cannot distinguish "the title never appeared" from "the stream froze early and repeated one frame 2391 times". Its 3.98 fps was measured over the first 20 s, before the degradation. Sample count is not coverage unless the samples are known independent. Fixed: the stream is now torn down and restarted every 30 s. Startup is ~0.3 s, cheap against the title's window, and it guarantees live frames. Separately, the cache hypothesis was tested and is SUPPORTED. cache, cache0, cache1, cache_host moved aside (to /tmp/xenia-cache-aside, not deleted) and the surface renders again: import reads mean 54.8 and 68.6 with 100% non-black warm content, against 0.07 and 0.08% non-black in the black run; 773 of 862 probe frames had >2% non-black. One run each side and many kill -9s before the black one, so it is supported, not proven -- the old caches are kept for reproduction. Still no title, but that number now comes from a stalling probe and establishes nothing either way. METHOD: validating a probe on static images tests its analysis, not its acquisition -- cross-check against an independent grabber during a run. |
||
|
|
e9924ff9e8 |
re: build the fast probe -- and it refutes the diagnosis that motivated it
Last iteration I blamed four failed runs on the probe sampling every ~41 s, slower than the title screen lasts, and withdrew three earlier conclusions on that basis. Building the fix tested the claim and killed it. The speedup is real and control-verified. One long-lived ffmpeg x11grab stream, raw RGB, glyph counted in numpy -- no per-sample process startup, no PNG encode, no convert -crop: wrapper `screenshot` 3.98 s per sample (emulator running) import -window root -> PPM 1.20 s long-lived x11grab stream 0.29 s 13.7x The counter is byte-identical to is_title.py: 753 on the committed title capture, 327 on the main menu. Pointed at a running game it says the opposite of what I expected: 332 frames in 85.3 s = 3.89 fps; max glyph 0 1674 frames in 420.0 s = 3.99 fps; max glyph 0 1674 consecutive samples over seven unbroken minutes, four per second, zero green-(A) pixels. Sampling rate was a real defect that happened not to be the cause. So "neither locale reaches the interactive title without a pad press" -- withdrawn last iteration for want of evidence -- is reinstated, now as a dense measurement, with its reach stated: a MID-RUN window only, silent about the boot title. Leading hypothesis, unconfirmed: the PRESS (A) plate appears only in the boot title window and the attract loop's title carries none, which is exactly what title_states_capture.sh was written to test. The experiment is to start the fast probe from t=0 rather than attach to a run already in progress. METHOD: fixing the instrument is how you test the explanation that blamed it -- a plausible mechanism is a hypothesis, and the fix is its experiment, not its proof. |
||
|
|
f10edf1e79 |
re: fix wait_title.sh's stale oracle; the JP title still is not reached
Last iteration's "never reached the title in 787 s" was a broken tool reporting on the world. wait_title.sh was still sampling the single pixel (625,618) that is_title.py had already been written to replace -- its docstring says why: a 1280x720 coordinate sampled against the 1279x675 game surface, so it always reads the copyright line. The replacement sat in the same directory. wait_title.sh now delegates to it. is_title.py passes its own controls before being trusted here: 753 green-glyph pixels on the committed English title capture, 327 on the main menu, threshold 400. Re-ran with the working oracle and the profile flag the English captures use. The game STILL did not present the interactive title -- but that is now a measurement rather than an artefact: not one frame showed a single green-(A) glyph pixel, and content correlation against either build-7 render never exceeded 0.22. Canary was alive and polling XamInputGetKeystrokeEx (601 calls), sitting in the attract movie. So the open question narrowed again, and is written into MISSION.md: whether the attract loop returns to the INTERACTIVE title without a pad press. title_states_capture.sh claims it does on the English boot with no pad input; if that holds, the difference is the locale. Nothing decided about the keyframe-time association or the rest() rule. Emulator stopped, lock cleared, locale restored to English. |
||
|
|
0f1c0f4e14 |
re: the JP-locale capture is not blocked -- I stopped one grep too early
Last iteration I wrote into MISSION.md that a Japanese-locale capture is impossible here, because user_language is DECLARE_int32 at four call sites with no DEFINE and no entry in xenia-canary.config.toml. That is true, and it was not the question. The language is PERSISTED: kernel_state.cc builds XConfig over <storage_root>/xconfig.settings, SetDefaults() only supplies a value when the file has none, and the file is writable. Checking where a setting is stored rather than where it is configured turned "blocked, needs a human decision" into a two-line edit. Withdrawn from MISSION.md; METHOD and REFUTED lines added. The field is located from struct landmarks rather than a hard-coded offset, and the check re-runs on every invocation so it fails loudly if the layout moves: music_volume 0.7f at User+449 -> BE float at 2727 -> User base 0x8e6 language at User+44 -> reads 1 (kEnglish) at 0x912 country at User+64 -> reads 103 (US) at 0x926 XLanguage::kJapanese = 2 (xbox.h:307). set_console_language.py wraps it with a backup and a --restore. The capture itself is still NOT taken, for a smaller reason than I claimed. A run with the locale set to Japanese booted fine but never reached the title in 787 s: wait_title.sh's green-(A) oracle never fired and burst-sampling found no frame correlating above 0.18 with either build-7 render -- the run sat in the attract loop. So it needs a longer or pad-driven run, not a rebuilt emulator. Emulator stopped, lock cleared, locale restored to English. Nothing is decided about the keyframe-time association or the rest() rule; this only changes what standing between us and deciding them. |
||
|
|
6d246c7a97 |
re(ui): finish the top-level rotation census; record the JP-capture blocker
Two threads had converged on needing one capture this container cannot take, so this iteration records that and finishes something reachable. BLOCKED, written into MISSION.md rather than worked around: Q1's keyframe time association and the rest() rule for plateau-less elements both now hinge on a running capture of GP_TITLE build 7, the Japanese title. The console language is not settable here -- user_language appears only as DECLARE_int32 at four call sites with no DEFINE anywhere in the tree, and it is absent from the registered cvars in xenia-canary.config.toml. There is no flag to pass, and guessing one is specifically unsafe: run-canary's own header records that xenia calls ShowSimpleMessageBox from ParseLaunchArguments before logging starts, so a bad flag blocks forever with an empty log. Rebuilding canary to add the cvar would be improvising around the blocker; it needs a human decision. Neither question blocks the five menu screens. FINISHED: the disc-wide top-level rotation count, left running four iterations ago as a shell loop over `screen info --geometry` that never completed (it decodes every texture per build). Walking the placement region directly takes seconds. top-level elements with a keyframe group 15 493 carrying a non-zero rotation 2 152 (13.89 %) Both controls pass: GP_TITLE build 4 reports 0 (its rotations are the nested ptloop records) and GP_DIALOG build 0 reports the expected two. The control earned its place -- the first version indexed the pak with a `screen list` BUILD number and got 0 for a screen that has two, because GP_DIALOG build 0 is entry 2. GP_TITLE maps 1:1, which is how the assumption survived. METHOD line added. Two free corroborations of the rotation decode. The rotated population is dominated by tactical-map ship icons -- pbb_destroyer 444, pbr_destroyer 402, pbr_fighter 276 -- i.e. markers rotated to heading, the single largest use of the field on the disc. And GP_TITLE entry 7's Japanese wordmark pieces settle from ALTERNATING tilts: ptlogo3a r = 0, -14, -4, -1, 0, ... ptlogo3b r = 0, +14, +4, +1, 0, ... ptlogo3c r = 0, -14, -4, -1, 0, ... Same magnitudes, opposite signs, all decaying to upright. A misread field does not produce that. |
||
|
|
b973d8cde3 |
re(ui): scale 0 means collapsed, not "unset" -- stop drawing it full size
blit() and fill_quad() both opened with
let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };
so an element whose pose is collapsed to nothing rendered at FULL SIZE.
I first described this as "a 1-pixel sliver", reading the .max(1) in the
size arithmetic two lines below -- the guard above it meant .max(1) never
saw a zero. Read the whole function, not the lines you went looking for.
Control run before the change: if 0 meant "unset", something on the disc
would use it throughout. Nothing does.
elements with a keyframe group 15 493
at least one keyframe at scale 0 2 166
EVERY keyframe at scale 0 0
zero is a transient the element grows out of 1 762
ptlogo_eff3.t32 runs 0% -> 200%. An "unset" marker no element ever uses
throughout is not a marker.
Both functions now return without drawing when either scale is 0.
Reach: 24 of 24 renders byte-identical across GP_TITLE (all 16 builds),
GP_PAUSE_MENU and GP_OPTIONS -- additive on every screen the port needs.
195 elements have a guessed rest pose at scale 0 and 126 of those have a
non-zero alpha, so the old code painted them; all 126 are in
GP_READY_ROOM.pak, which S1 already declared a no-go. They are
tactical-map sprites at sx=0/sy=100, mid-horizontal-wipe, that the
coercion drew full-width.
So: a real correctness fix with no visible effect on the menu port.
Suite green, 122 passed / 0 failed across 3 suites at commit time.
|
||
|
|
c2c69b02be |
re(ui): size the rest() guess disc-wide, and refute my own proposed fix
Follows up the defect found last iteration: rest()'s dwell fallback is
guessing whenever it runs. Two things were open -- how big it is, and
whether "rest = the last keyframe" is the fix. Both are now answered, and
the second is answered no.
plateau_census.py walks the placement regions directly instead of going
through `screen info --geometry`, which decodes every texture and cannot
do a disc-wide pass in reasonable time. Its control reproduces GP_TITLE
build 7's three fallback elements and names ptlogo_eff3.t32 among them
before counting anything.
elements with a keyframe group 15 493
no plateau -> rest pose is guessed 3 807 (24.57 %)
... current rule returns invisible 1 711 (44.9 %)
... current rule returns scale=0 195 ( 5.1 %)
the two candidate rules agree 1 911 (50.2 %)
195 elements get a rest pose with scale 0%, which is not a pose. And
disc-wide the choice of rule is not cosmetic: the candidates agree half
the time.
But the port's exposure is one element. Across main menu, EXTRAS, title
and the developer splash, 14 elements are plateau-less and the two rules
agree on 13. The single disagreement is palogo_anima_eff.t32.
And "last keyframe" loses there, on a control that needed no new capture:
the splash carries three sibling glows with identical structure and
identical times --
palogo_gamearts_eff 15:a=0 30:a=255 45:a=255 -:a=0 plateau -> visible
palogo_seta_eff 15:a=0 30:a=255 45:a=255 -:a=0 plateau -> visible
palogo_anima_eff 15:a=0 30:a=255 45:a=212 -:a=0 no plateau
-- differing in one byte. "Last keyframe" makes anima alone invisible
while its two siblings stay lit. The capture agrees weakly: box-mean
ratios capture/render are gamearts 0.717, seta 0.723, anima 0.772, and a
glow we drew that the game does not would put anima below its siblings,
not above.
So the defect is measured and the fix is still undecided. Nothing in the
decoder changed.
|
||
|
|
531840b417 |
re(ui): Q1 -- the interpolation law holds, the group timeline does not
Q1's gate asks whether the ramp is linear. It is, and that result stands: it rests on the splash's _eff glows, which reproduce exactly. This adds the part that does not. The test is a calibration, not a fit. Fix the clock on palogo_gamearts_eff -- declared 15-unit fade-in 0@15 -> 255@30 against captured alphas 34,68,102,136,170,204,238, a constant step of 34, giving t = 2f - 171 -- then check that against the glow's own next landmark: its declared hold ends t=45, predicted frame 108.0, observed last full-alpha frame 107. Then apply it to palogo_gamearts in the same bundle and the same frames, with no free parameter left: declared a=232 at t=206 -> frame 188.5, observed alpha 255 declared a= 32 at t=210 -> frame 190.5, observed alpha 255 The logo is still at full alpha nine frames after it should read 32; its fade-out runs ~17 frames late; its declared 80-frame fade-in is never drawn. Not culling -- the same element is submitted down to a=7 on the way out. Calibration-free version: the declared fade-out spends 12 of 16 units dropping 23/255 of the alpha, and the capture has no such plateau. Candidate, offered and NOT adopted: if +36 held the NEXT keyframe's time, the fade-out shape fits (RMS 4.05 vs 12.13, two elements) and the decoder's "last block's time is unreadable" special case disappears -- the last block would simply have no successor. Rejected for now because it explains neither the missing fade-in nor the lateness, and because the _eff elements cannot discriminate between the readings at all (with four blocks the shift only relabels the phases). Decoder unchanged. Also withdrawn, mine, within the iteration: "the _eff glows hold a constant alpha 33". They ramp 34 -> 255 in steps of 34. I printed the series minimum and read it as its range, with a "14 distinct colours" column sitting next to it saying otherwise. |
||
|
|
67fa1a1b0b |
re(ui): decode keyframe +12 as screen-plane rotation in degrees
The rotated quads on the title screen come from the keyframe block after all. The earlier negative -- "every GP_TITLE build 4 element has all three angle words at zero" -- read the right bytes over too small a region: it walked the top-level declaration table, and the rotated elements are the nested leaf records ptloop01.rat / ptloop02.rat. Confirmed against the framebuffer rather than against our own renderer. The two records declare +12 = 30 and -45; the GPU capture submits their quads at +30.26 and -45.28 degrees -- magnitude and sign, two different values. Corroborated by shape in GP_BUNK 117ca14f, where +12 ramps 0 -> 360 with position, scale and alpha constant: a spin in place. Identifying which draw it was needed edge lengths, not bounding boxes: 400x1076 and 400x1444 against pteff03/pteff03a 399x180 at the elements' two different declared scales, 600% (1080) and 800% (1440). The same test names three known-positives in the capture (ptlogo1, ptcopyright, ptbtn00), so it passes its own control. Keyframe gains rotation_deg plus unknown_4/unknown_8, carried rather than dropped. NOT rendered -- ui_layout::blit is axis-aligned only, so the reference renderer and the port will both draw these upright until a rotating blit exists. The census tool ships with the trap that broke its first version: nested RATC blobs are not 4-byte aligned, so an aligned scan found 0/3 of its own control blocks and missed 16 341 blocks. Disc-wide +12 is non-zero in 14.50 % of 83 862 blocks. sylpheed-formats tests, SYLPHEED_DISC set: 131 passed, 0 failed across the 6 suites finished at commit time; the run had not yet completed. |
||
|
|
b1b2576769 |
re: the UI cues decode -- 0.53 s, 0.34 s and 1.02 s of real audio
Finishing the step I left open last iteration rather than starting something new. Offsets and packet counts were in hand; what was missing was proof they are actually waves. slb_extract_wave.py wraps a (bank, offset, packets, channels, rate) slice in a synthesized XMA1 RIFF, following the layout the Rust decoder already uses. The three located cues decode to 0.533 s, 0.344 s and 1.016 s of mono 48 kHz audio, audible from sample 0, each with the percussive attack-and-decay envelope of a UI blip. Bitrates come out at 12-15 kB/s, about half the stereo BGM rate, which is what mono should be. The control matters more than the results. The SAME wrapper applied to BGM_001's first wave decodes to 173.808875 s -- identical to the duration that bank's own on-disc RIFF header produced back when Q10 was answered. So the header I synthesized is not approximately right, it reproduces a known-good decode exactly, and the cue durations are trustworthy for the same reason. I did not commit the decoded audio. Three commands regenerate it from the disc, and the corpus's job is measurements and tooling rather than extracted game assets. The offsets, the packet counts and the tool are the deliverable. |
||
|
|
e81dcad632 |
re: the boot intro and the attract loop are the same video, and the
new-game intro has a name Q9, answered twice over -- once from the disc and once from frames I had already captured and not used. The movie manifest's first eight slots are the whole boot-side flow, and the slot key is the role: LOGO1-4 -> logo1-4.wmv (not on the disc, which is why the splash is a screen), ADVERTISE_MOVIE -> ADV.wmv, STAFF_ROLL -> the credits reel, MS00A -> S00A.wmv, MS01A -> S01A.wmv. So the new-game intro is S00A.wmv, decoded -- which is the half I could not test at runtime, because A on NEW GAME hangs the emulator. And there is no separate boot-intro slot: ADV.wmv IS the advertise movie, the boot just plays it first. Confirmed independently by matching 19 attract frames against five candidates -- 15 hit ADV with a playhead that advances monotonically at the sampling rate and ends at 137 s, its full length, with the title back on the next sample. So the attract movie plays to its end; nothing cuts it short. That corrects me. Two iterations ago I recorded the attract movie as "~85 s, so probably not ADV.wmv" -- arithmetic on a start I never observed, since sampling began 39 s in. REFUTED and METHOD both take it, along with the matcher's real failure mode: a near-black frame has no signature, and its runner-up is not evidence. Skippability I did NOT settle, and the corpus contradicts itself: one page says A skips a movie every time, while the boot harness deliberately never taps during one because it breaks the title. Named the one-boot test rather than picking a side. |
||
|
|
b3c8632006 |
re: a music bank is two stems that play together -- not intro+loop, and
not something to concatenate Q10, and it starts by withdrawing the question's own premise. BGM_001 is not three sub-waves of 10 KB / 4.47 MB / 4.67 MB: the 10 KB is the bank header. A bank is exactly TWO waves, and across all 32 BGM banks on the disc the two always have the SAME duration -- equal to 0.01 s over lengths from 37 s to 277 s. That alone kills intro+loop and kills two halves of one piece, both of which require unequal lengths. Four banks appear to break the rule and do not: BGM_106-109 are the known leading-region straddle, and the giveaway is that the entry named BGM_107.slb contains BANK id=1108. The seek packet counts pin each join exactly, so they realign to the same two-equal-waves shape rather than being dropped as noise. That trap goes in METHOD. Then the roles, by decoding both waves to PCM. They are sample-synchronous: transient-envelope correlation searched over +/-5 s peaks at lag +0.00 s, and both waves stop at the same millisecond, 167.663 s. Two stems of one performance, meant to sound at once. Wave 1 is quieter, has almost no bass and is far more L/R-decorrelated, which reads as a surround-rear pair or a second intensity layer -- I cannot separate those two from the file, and say so: ChannelMask is 0x0002 on both, and this game's channel metadata is already documented as meaningless. Two things the port needs that are NOT on the disc, both marked as authored: the track is not a seamless loop (BGM_001 fades out and is followed by 6.15 s of silence, no loop-point field found), and nothing names which bank the menu plays -- all 32 BGM cues are numeric. |
||
|
|
7fef19b3a0 |
re: the transition between screens is a fade through black, and most of
its timing is on the disc Q7. Every title-side screen carries a full-screen black .prm quad that paints last, and its keyframe group IS the transition: black at T0, clear by T1, clear until T2, then back to black on exit. Read with the corpus's start-of-a-ramp rule and Q1's time unit that gives 0.87s for EXTRAS, 0.97s for the main menu, 4.08s for the title -- from the file, not from a stopwatch. The disc-wide check is per-pak all-or-nothing rather than the 41% the headline count suggests, and GP_TITLE's 6 of 12 is the useful row: the six builds carrying a fade quad are exactly the six SCREENS, and the six without are exactly the six overlays. GP_DIALOG is 0 of 133. That is independent corroboration of the overlay finding from two iterations ago. One piece is NOT on the disc and says so: the fade-OUT length. The fourth keyframe has no time slot, because a group's last block stops four bytes short. Measured instead, at 30fps, ~0.4s and the same both directions. And a warning I earned: the luminance rise after a transition is NOT the quad's ramp. The incoming screen's own elements animate in after the quad has cleared -- 1.47s observed against a declared 0.97s. Time the fade from where the frame is pure black. Rig: screenshot samples at 0.5 Hz and cannot see a 0.4s fade at all, which is why an earlier burst called this an instant cut. ffmpeg x11grab at 30fps instead; both go in METHOD. |
||
|
|
4e745c8177 |
re: the title menu wraps -- Q5 measured, Q4 driven, and one of my own
method lines withdrawn Q5, measured off two boots: up/down move one item and WRAP at both ends on the 5-item main menu and the 3-item EXTRAS alike; left/right do nothing; B goes up one level and restores focus to the item you came from (4/4); B on the main menu returns to the title; B on the title does nothing. The menu opens on TUTORIAL -- the middle item -- 2/2, though a third recorded run implies NEW GAME, so that one is reproducible rather than invariant and says so. Q4 by driving: LOAD GAME opens the save-slot list, TUTORIAL the lesson list, OPTIONS the settings menu, EXTRAS build 6, MISSION SELECT the stage list. NEW GAME is not tested -- A on it hangs the emulator and this iteration needed the session. The GamePart ID behind each is NOT measured: it is the entry of the decoded id table whose name matches the screen I saw, and the page says so rather than wearing the badge. And the withdrawal. Last iteration I wrote that these menus drop d-pad presses shorter than ~0.3s. They do not. Once wrap is measured, every press count I had is exactly right -- four presses moved four steps THROUGH the bottom, which lands one above where a non-wrapping menu would. I invented hardware flakiness instead of testing the ends of the list. METHOD keeps the withdrawal rather than deleting the line. Also: label brightness is not a cursor oracle here -- the background art outshines the highlight on some rows. menu_focus.py reads the focus ring instead, 254 vs <82, no tuning. |
||
|
|
8b6dbcfead |
formats: move media assembly out of the viewer, where it could not be reused
The trickiest reading on the disc lived in the Bevy viewer: resolving a cutscene's voice to a continuous byte REGION of the sound stream, because the movie voices are one XMA stream chunked into VOICE_*.slb entries whose boundaries do not match the cues -- a cue routinely spans two chunks, so a .slb need not hold the track its name claims. That put the logic most likely to be re-derived incorrectly in the crate least likely to be reused. The Godot port's exporter needs the same answers, and there must be one implementation of them. New `sylpheed_formats::media` owns every case where the bytes of one playable thing are not one archive entry: segment-spanning reads, multi-sub-wave banks, and the voice-region resolution. Callers supply bytes through a `DiscSource` trait, so the viewer keeps its ISO/directory abstraction and a headless consumer gets `DirectorySource` for free. The seam is deliberate: this module returns XMA RIFFs, not PCM. Decoding means shelling out to FFmpeg, which is native-only and a policy decision for the consumer -- everything up to "here are the bytes that belong together" is disc knowledge, everything after it is a codec choice. The four moved functions were previously untested; `tests/media_disc.rs` now pins them, including the negative the corpus paid for -- an unbound movie must stay unvoiced rather than borrow a neighbour's clip, which was tried and played the WRONG recording. The algorithm is unchanged, moved verbatim (same window sizes, same fallbacks). The new disc tests pass; the broader audio suite was not re-run in this pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
69291a315f |
port: add Q10 -- what a music bank's sub-waves actually are
BGM_001.slb is three sub-waves (10 KB, 4.47 MB, 4.67 MB) and the decoder concatenates them into one 347 s track. That is a default nobody chose, not a decision: two near-equal halves could be intro + loop, two variations, or two halves of one piece, and a menu that loops its music needs to know which. Found while wiring the Audio Library up to the shared banks. Recorded in HANDOFF.md as a trap too, so the port does not build looping on top of the concatenated track before the question is answered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
03ece95c06 |
Merge branch 'auto/idxd-unnamed-keys'
# Conflicts: # docs/re/INDEX.md |
||
|
|
1fce3f9c71 |
Merge branch 'auto/slb-loader'
# Conflicts: # docs/re/INDEX.md # docs/re/structures/slb-data-offset.md |
||
|
|
8124b85fc7 |
Merge branch 'auto/regn-reader'
# Conflicts: # docs/re/BACKLOG.md # docs/re/INDEX.md # docs/re/structures/regn-map-grid.md # tools/re-capture/regn_decode.py |
||
|
|
245b73243b |
Merge branch 'auto/isl-builtins-26-28-29'
# Conflicts: # docs/re/INDEX.md # tools/re-capture/isl.py |