36625d7bb3cedecd893fbcf37b23a8040c2e6d32
219 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
36625d7bb3 |
port: voice export carries every qualifying stream; a unity sum was refused by check
#4 answered: ADV.wmv carries ONE audio stream and it is WMA Pro 5.1, not XMA, so the movie's own track is the bed and the three streams are additional. Solving capture = 0.600 x movie + residual gives three residual signals at three positions, with LFE reproducing to -115.73 dBFS -- where nothing is added the decoders agree exactly, so the rest is added content. presentation: all keeps every equal-length non-silent survivor -- ADV 2 of 3, S00A 1 of 3 -- and the warning now keys on kept < present rather than on more than one existing. A unity sum was tried first and check refused it at +2.62 dBFS. The BGM stems precedent did not transfer: those are stems of one signal, these are positions in a field whose downmix weights sum to one whatever the assignment. Dividing by the count preserves the total and claims nothing about placement; ADV lands at -3.1. That is the OPPOSITE of the two divisor bugs already in this file, where a silent input sat in the divisor. Divide-by-N is not right or wrong in itself. Also carries their census correction: the ALSA permutation does not apply, the map is the identity, and the '82% silent' channel was LFE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
77e217807a |
port: FORMAT.md declared the port's own export invalid; assert audio.json's stems
Audits the open format spec against the validator by breaking each documented requirement. Four of five caught. The fifth is the doc's error: FORMAT.md said check refuses any peak >= 0 dBFS, where the implementation is kind-dependent -- a bgm is a sum we produced and is refused at full scale, an se/voice is a disc wave whose lossy decode overshoots and is allowed to +1.0. The doc was wrong about our own export: confirm ships at +0.18 and the ADV voice at +0.31. A consumer implementing a validator from FORMAT.md would have rejected a valid tree -- the file that exists so someone can check our work without trusting us. Corrected, with the +1.0 marked as a judgement. Also closes the last unread authored value: audio.json's was carried as stems_why only, so serde ignored the value. Now deserialised and asserted in the exporter (only sum is implemented); the assertion is proved to fire. Files, not fixes: a failed export leaves a tree with no manifest, and every tool then says 'is that an export tree?' -- which nearly made me conclude the validator was checking nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
c4d7553e59 |
port: check the five MODDING rules, and label the generated files in the asset tree
MODDING.md calls modding a constraint on the exporter TODAY and nothing verified it -- the same shape as the black hold, skipped[], stop_bed and --focus. All five rules pass, so check-modding is a guard rather than a fix, and it is proved able to fail: a stripped .cmd header, a bogus.bmp, and one orphaned PNG each exit 1. It found one thing: the .cmd encode-cache sidecars sat in the modder-facing tree with nothing saying what they were. They now carry a header. The header is excluded from the cache key so rewording it does not re-encode four minutes of video, and the sidecar is refreshed whenever its text differs rather than only on re-encode -- otherwise a header change could never reach an existing export. Also partly answers my own question to the Decoder: there is no general capture-path floor, because the port matches live-title-press-a at 0.00093% full-frame and 0.000% across the band. The 0.301% is specific to that pair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
29a5b4a394 |
port: the PRESS (A) plate could not be drawn at any instant -- four faults, and a misquoted number
1. --time= was silently ignored on any screen with a settle window >= 30 units: pose_at overwrote the requested instant with settle_instant. ScreenView.frozen now marks an explicit instant and skips both clamps. 2. press_start's settle window was [0,214] -- the dead stretch BEFORE the plate exists -- so its settle instant was t=107, where the element is alpha 0. The exporter now rejects intervals in which nothing is visible. title keeps [160,236], the interval the Decoder's draw stream confirmed. 3. My authored looping_focus_records entry for press_start/ptbtn00 drew a dim focus record INSTEAD of the plate's own sprite: max 0 vs max 252.5. Deleted -- an authored guess that overrides a decode with a worse answer is removed. 4. verify-capture passed --time=5.9617 for the title and it was never applied. Every title figure it has printed, including the 0.26% quoted to the Decoder, was measured at the settle instant under a note claiming t=357.7. Both rows now pose by omission and the note matches. title is 0.21% honestly; splashes unchanged at 0.01%. The boot's end artifact now contains the plate (region mean 95.7 vs 33.6). Corrects last iteration's BLOCKED row, which had the entry's effect backwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
06115af659 |
port: test backdrop coverage per instant -- scale animates, and the disc proves it
The Decoder found forced_backdrop judged coverage from declared size alone. pbafc.prm declares 844x600 at alpha ff and draws ~17x18 px at 2%x3% scale -- a rule reading declared size would call it screen-covering. scale_at interpolates on the same ramp as the fade, and coverage is folded into the opaque-instant test: an instant counts only where the element is BOTH alpha 255 and covering. The previous code tested the two halves at different times. The size prefilter deliberately no longer rejects, since an element scaled above 100 could cover from a smaller declared size. No verdict moves: 6 forced before and after, 16 screens validate, oracle figures identical to the digit. Nothing in GP_TITLE has a non-100 scale on any keyframe. It is in because the data that would break it exists, not because it failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
df3ea68366 |
port: P6 gate verified with sound on the bus; tighten the backdrop guard to a positive primitive test
verify-menu-audio records the Master bus over the P5 walk under the Dummy driver. A dead press is bit-identical to the bed alone; all three cues match their exported wave in the recording with margin over a bed-only control; the cue order matches the script order, which the correlator was never told. The first version of this tool counted envelope bursts above a multiple of the bed and gave 4 cues on one run and 0 on the next from the same script. Replaced with template matching, which has no tuned constant. Cue LENGTH is deliberately not asserted -- the bed masks the tail and I nearly filed that as a defect. Also acts on the Decoder's .tbm self-refutation. No port verdict is affected -- all six forced elements are .prm solid black, and GP_TITLE has no full-screen .tbm at all -- but the guard was sprite.is_none(), a symptom test of the same shape as the one they say fixed their symptom not their cause. Now role == primitive. Six verdicts identical, 16 screens validate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
1e06ad2e8e |
port: implement the Decoder's forced-backdrop rule; two screens were black for their whole life
build_12 and build_15 rendered mean 0 at every instant of a PLAYING timeline, and verify-screen scored both OK -- two renderers sharing implied_layer_key, comparing nothing against nothing. Implements the constraint as a post-pass over ui_layout::derived_paint_order, with both of the Decoder's limits copied verbatim: layerless elements only (a sprite's element alpha says nothing about its texture's coverage) and NOT a name heuristic (palogo_eff0 is named like an overlay and paints first). Both controls reproduce: palogo_eff0 forced first, pteff00 still last on all four title screens at exactly 2 opaque instants. Splashes unmoved against the oracle at 0.01%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
fb472e8dfc |
port: pose a settled screen at ONE instant -- title 1.81% -> 0.26%, splashes to 0.01%
The Decoder's finding, applied. `rest()` returns each element's last hold keyframe chosen independently of every other element: right for anything that ends the screen settled, exactly wrong for a transient. The title's ptlogo_back2eff1 is a two-frame flash (0 until t52, 255 at t54-56, 0 by t58), so its last hold IS the flash peak and rest() left it burning -- five of them, drawn at once. The settled instant is the longest interval containing no keyframe time, over a bundle's TOP-LEVEL elements. Reproduced here before adopting: title [160, 236], midpoint 198, the Decoder's number to the unit. The top-level restriction is what makes it match -- including the ptloop leaves gives [269, 540]. AGAINST THE ORACLE: title 20.92 RMSE 1.81% -> 14.61 RMSE 0.26% publisher_logo 9.05 0.75% -> 2.17 0.01% developer_logos 8.86 0.33% -> 3.05 0.01% main_menu 0.08% -> 0.08% window too narrow extras 0.19% -> 0.19% window too narrow Seven times fewer differing pixels on the title, seventy-five times fewer on the publisher splash, whose differing region is now a 13x18 box. The largest correctness gain this port has had, and none of it is mine -- it is a decode computed from the keyframe table with no reference to any capture. APPLIED ONLY WHERE THE WINDOW IS WIDE, and the bar is not invented. This export's widths split with nothing in between: 214, 190, 145, 76, then 12, 12, 8, 4. The bar is 30 units -- the Decoder's disc-wide census puts the knee there (30% of bundles >= 30, 42% under 10, the latter mostly loop* fragments meant to be in motion) and this export's screens sit 4x either side with nothing between 12 and 46. Two independent populations agreeing on where to cut. Checked unbroken: boot pacing unmoved, scripted walk runs end to end with focus restored. Also recorded: my "34 focus-record elements, only 2 varying" is right for GP_TITLE and reads as a fact about the format -- disc-wide it is 210 varying, 202 with rest() at the peak, concentrated in the paks a wider port reaches next. And their sharper framing, which I have adopted: a pulsing element has no resting pose at all, so rest() is MALFORMED rather than mis-answered on one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
ace071dd7b |
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
|
||
|
|
e5be03709a |
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
|
||
|
|
cb616ab9af |
port: implement the decoded leaf composition -- and it does not close the 1.82%
The Decoder decoded the rule I refused to guess: draw the leaf on its own timeline, do NOT multiply the parent's alpha in. Multiplying is refuted rather than unsupported -- at the fitted time the parent has expired, so leaf x parent predicts zero for both quads and the sweeps would be invisible. They are drawn. Implemented: `_draw_leaf` runs the leaf unclamped, like the spinning ring and for the same reason -- held at its own rest.t the leaf sits at x=1521, entirely off the right edge, so `holding` would delete the sweeps rather than settle them. AND IT CHANGES NOTHING MEASURABLE. The title is still 1.82% against the oracle: 1.82 at t=261, 1.81 at t=355, 1.79 at t=420. At t=355 my interpolation puts the leaf's top-left at x ~ -324, off-screen left, where the Decoder's model puts the quad's CENTRE at 981. Those cannot both be right, and it is not something to tune away -- it is a disagreement about how the leaf's keyframes become a placed quad, most likely in the pivot and the rotation about it. Handed back with both numbers. So: the exporter no longer drops the data, the composition rule is implemented as decoded, and the port's largest oracle gap is exactly where it was. Fixing the export was necessary and not sufficient. TWO FLAGGED ELEMENTS DELIBERATELY NOT DRAWN, in authored/rendering.json with reasons. title_jp/ptlogo_eff2 (parent 125%, leaf 100%) is the same shape and is the element DECISIONS has recorded since P1 as the largest render disagreement -- but the Decoder said plainly "I have not tested it", and drawing it would extend a decode past the case it was fitted on. pgloading_loop5's leaf is scale (0,0), and scale-0 is one of the three historical failures this corpus names. Neither can be adjudicated here: title_jp has no oracle capture, and verify-screen compares against a renderer that draws no leaves at all, so ANY leaf drawing increases that divergence whether right or wrong. Its max went 155 -> 232 when they were drawn, and that number is not evidence in either direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF |
||
|
|
9ad8f63234 |
port: the exporter never opened an element's own .rat leaf -- 45 elements, and the title's 1.82%
The Decoder overturned the elimination I was most confident about. I ruled out
the ptloop sweeps because "399x180 at (441,270), keyframes hold position
constant". That is the PARENT's record. The geometry is in the leaf.
ptloop01 parent: scale (100,100) rot 0, fixed at (441,270)
LEAF: scale (100,600) rot +30, x sweeping -639 -> -39 -> 1521
ptloop02 parent: scale (100,100) rot 0, fixed at (441,270)
LEAF: scale (100,800) rot -45, x sweeping 1721 -> 1111 -> -839
Two ~1080 and ~1440 px quads leaning opposite ways and sweeping across the frame,
against two 400 px sprites drawn upright and static in the middle. That is
exactly the signature I measured -- darker centre-left, brighter right, nearly
cancelling -- and the GPU capture puts their centres at x ~ 467 and 992, the two
cells where my signed difference peaked.
`ui_layout`'s own doc comment said it: "the rotated quads come from its two
nested .rat leaf records, which the census never opened". Neither did this
exporter -- it opened a leaf in exactly one place, `highlight_name`, for focus
records.
IT IS NOT TWO ELEMENTS, IT IS 45: every button on every menu (the benign case,
where screen.rs already knew the leaf duplicates the parent and the parent wins),
the four loading screens' pgloading_loop*, and title_jp's ptlogo_eff2 -- which is
the element DECISIONS has recorded since P1 as the largest render disagreement in
the export, and which has a TWO-element leaf. A lead, not a conclusion.
EMITTED, DELIBERATELY NOT DRAWN. One `read_leaf` closure serves both the new path
and the focus path, because a second copy is how this would go missing again.
ScreenView ignores the data: parent and leaf each carry their own alpha ramp over
a different span (parent 0->255 over t=70..238, leaf 255->0x80->255 over
t=150..600), so how they compose is a decoding question, and drawing on a guess
would replace a visible 1.82% gap with an invisible wrong one. verify-screen
confirms nothing moved.
Additive blending is refuted -- the Decoder tested T8aD +0x04 bit 0x02 and "every
measure worsens", and the export carries no blend field because none has been
found (no RB_BLENDCONTROL in the per-draw capture). My hypothesis from last
iteration is dead.
This makes the port's biggest oracle gap the same item as the rotation question
already standing with the human: sylpheed-cli deliberately does not rotate, which
is why both renderers show it, and MISSION's "Needs a human decision -- rotation"
now has a number: 1.82% of the title's pixels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
|
||
|
|
ff04fbce52 |
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
|
||
|
|
8fba7944d4 |
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 |
||
|
|
c43d44f57e |
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 |
||
|
|
2939c21b38 |
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 |
||
|
|
29fd735b96 |
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
|
||
|
|
0e7fcc2dda |
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 |
||
|
|
7465e49c8e |
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
|
||
|
|
3aa74ea029 |
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 |
||
|
|
7757dd4c84 | Merge remote-tracking branch 'origin/main' into auto/port-p6-audio | ||
|
|
002df51173 |
viewer: show where a cutscene's voice actually is, and let you hear it
The Cutscenes window printed the voice token as text and offered no way to play it, which left the most confusing thing on the disc invisible. The movie voices are one continuous XMA stream chunked into VOICE_*.slb entries whose boundaries do NOT match the cutscene cues, so the bank named after a movie need not hold that movie's audio. Measured, on the retail disc: ADV region 433930240..437044592 inside VOICE_ADV.slb name honest S00A region 452798464..455499120 inside VOICE_S00A.slb name honest RT01A region 437044592..437345648 inside VOICE_ADV.slb NAME LIES RT01A's voice sits in bytes belonging to the entry named after the intro movie. A viewer that played the name-matched bank would be confidently wrong for exactly the cutscenes where it matters, and would look right on the two that are easiest to check. So the window now shows BOTH locations -- the named bank with its byte range, and the resolved region -- and states plainly whether the name is honest, highlighting it when it is not. Play routes through the movie form of RequestAudio, which resolves the region rather than reading the bank. Static data only: sound.pak and tables.pak, both on the disc. |
||
|
|
d6252481e1 |
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
|
||
|
|
9fbb352ef0 |
monorepo: one repository for the decoders, the port and the corpus
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.
|
||
|
|
76653ca41b |
re(ui): the focus ring SPINS, the game draws it, and the leaf owns the f record
Three things, all from parsing ptbtn0Nf.rat as a build. 1. THE RING SPINS. Its two keyframes differ in exactly one field: rotation_deg ramps 0 -> 360 with position, scale, alpha and tint all constant. A spin in place, the same shape as the GP_BUNK example already recorded. 2. THE ORACLE CONFIRMS THE GAME RENDERS IT. In the OPTIONS-focused capture the ring's bright head sits in a completely different angular position from the sprite's own -- caught mid-spin. This is a SECOND independent confirmation that rotation_deg is drawn, now on a different screen and a different element from the ptloop sweeps, and it raises rotation's priority: it is not a title-only concern that sits off-screen at rest, it is the main menu's focus marker. NO ANGLE IS QUOTED. A brightest-region centroid says ~250 deg, but the control refuses that precision -- rotating the sprite by a known 30/90/180/270 and re-measuring gives errors up to 19.8 deg. What survives the error bar is that a <=20 deg error cannot manufacture a ~250 deg displacement. 3. WHICH PLACEMENT WINS -- correcting this page's own earlier caveat, which said to use the leaf only for elements the parent does not declare. Right for a BASE record, wrong for an f record: the parent declares NO element for ptbtn0Nf.rat at all (zero of build 5's 16), so the f record's placement comes from its leaf for BOTH elements, label included. The label's (-7,-7) is load-bearing -- the f sprite is 13px larger per axis and -7 keeps them concentric (535+96/2 = 583 vs 542+83/2 = 583.5). Corroborated against the oracle: the focused-minus-unfocused region is x 505..703, and the leaf predicts a right edge near 707 where the parent reading predicts 714. Also exposes UiBuild::records (name -> (offset, size) of a nested .rat leaf). Nested records were parsed into a PRIVATE map, so a consumer holding a UiBuild could not locate a leaf's bytes at all -- which is exactly what blocked the port from reaching the ring. |
||
|
|
6779d9c807 |
media: expose se_wave_riff -- the menu's SE cues, assembled where the format lives
The port is forbidden from reimplementing media assembly and Static.slb is exactly that case: no RIFF, no seek chunk, no XACT container, just a packed run of whole 2048-byte XMA1 packets, so a wave is defined only by (offset, packet count) and the header has to be synthesized. That step now happens once, in the crate that owns the format, instead of in each consumer. `slb::xma1_wave_riff` wraps raw packets; `media::se_wave_riff` looks the bank up and reads just the packets asked for. Both reuse the existing synth_xma1_fmt / build_riff, which are already byte-identical to what tools/re-capture/ slb_extract_wave.py writes -- so this is exposure, not a second implementation. It reads a TARGETED range rather than the whole bank, and that is load-bearing: Static.slb is the ONE entry of sound.pak's 9 519 whose declared extent runs past the end of the extracted segments -- by exactly 616 768 B -- so reading it whole fails outright on this extraction. Every cue we need is in the first few hundred KB. Recorded rather than worked around silently. Verified as an artifact, not a compile: all three cues decode through ffmpeg to mono 48 kHz PCM at 0.533 / 0.344 / 1.016 s, non-silent (rms 2085 / 2985 / 4327, peaks 29813 / 16973 / 32767). The refusal path is exercised in the same run -- an impossible packet count is rejected rather than returning a short stream, because a truncated XMA decodes to plausible-sounding garbage. Also adds docs/re/captures/ORACLE-CAPTURES.md: an index of the nine canary framebuffer captures already in this repo, and a plain statement that THEY are the reference and `screen render` is not. |
||
|
|
0ee0bb8565 |
re(ui): the focus ring's position is decoded -- a .rat leaf parses as a build
The port needed ptbtneff01.t32's placement and was about to author it from an eyeballed PNG measurement. It does not have to: a `.rat` leaf needs no new reader. Its first 32 bytes have a bundle header's shape -- "RATC", 0x3c declaration-entry size at +4, element count at +20, design 1280x720 at +24/+28 -- so ui_layout::parse_build reads it unchanged. The control is the base record, whose position is known independently: the parent screen reports ptbtn01.rat resting at (542,162), and parsing the leaf alone returns ptbtn01.t32 at (542,162). It reproduces all five buttons. Positions are absolute design-space top-left. The ring rests at (500, 156/236/ 316/396/476) for buttons 1-5 -- a uniform (-42,-6) from each button's own rest, identical in the Japanese bundle. The bright label is a uniform (-7,-7). Two things recorded rather than smoothed over: a leaf's placement DUPLICATES the parent's rather than being relative to it, and the two copies are not always byte-equal (ptbtn04's parent says y=401, its leaf says 402) -- the parent is what compose honours, so the leaf is the source only for elements the parent does not declare, which is exactly the ring. And `screen render --focus` is blind to the ring for the same reason the port's exporter was: el.focused is name-based on top-level elements and neither walks into the leaf. |
||
|
|
1a82ade187 |
re(ui): the 60 nameless RATC children are frames, not children -- .tan decoded
Closes the reach caveat the `opt ` name fix left behind: 60 of 18 002 RATC children carry no `opt ` block, and it was not established whether they lack one or sit past our 128-byte window. Neither. They are not children. `examples/ratc_optless_children.rs` re-runs `ratc::parse`'s own guards over the disc and reports which one fired: all 60 are "tag beyond the window", none is rejected by length, gap or charset, none is child #0, and all 60 live in six bundles of one archive. Within a bundle the distances back to the nearest tag are an exact arithmetic progression, step 60 600 -- ten different records finding the SAME tag, because there is only one. Reading a bundle directly: children 1..10 are equal-size T8aD blocks under a single `opt ` name, `pb_f15_eg_anm.tan`. `.tan` is a FRAME SEQUENCE. One block declares the resource; its payload is a run of T8aD frames. Disc-wide, over all 18 718 `opt ` names in all 33 paks: a RATC bundle names exactly six kinds of resource -- `.t32` 14 756, `.rat` 3 311, `.prm` 367, `.tbm` 224, `.sbo` 54, `.tan` 6. Six `.tan`, ten frames each = 60, the entire population with nothing left over. The negative is closed, not narrowed. Consequence recorded but deliberately not fixed: `ratc::parse` over-reports there, listing a `.tan`'s frames as anonymous children. Nothing in the menu milestone reads a `.tan` -- it occurs only in GP_READY_ROOM, which S1 ruled out -- so no screen the port draws changes. Also a METHOD entry for this container OOM-killing `slb_leading_segment_disc` under default test parallelism (SIGKILL, no assertion; 8/8 pass with --test-threads=1). |
||
|
|
0ed33bcd38 |
re(ui): a RATC child's name is stated, not inferred -- and it was hiding every menu background
`ratc::parse` named each child by scanning backwards for the last printable run of bytes before its magic. The format states the name explicitly instead, in an `opt ` block: `"opt " | BE32 len | name | NUL | 3 bytes | magic` -- the same block `ui_layout::opt_link` already read for a button's focus link. The scan agrees with it 17 918 times out of 17 942 and is wrong 24 times, every one the same failure: the 3 trailing payload bytes are themselves printable and beat the real name. For `pteff05.t32` those bytes are `38 41 58` = `8AX`, so the full-resolution background of all five menu screens registered under a name no element declares, resolved to no sprite, and `compose` dropped it through an early `continue` that -- unlike the two arms above it -- records nothing. The screen lost its background and `screen render` still reported "all resolved". `8AX` was never a name. Docs that treated it as one are corrected here. Disc-wide, and the control is the 17 918 the scan already got right: the `opt ` reading reproduces every one of them. Effect on the five screens is the signature of the same art at twice the resolution -- mean brightness unmoved, high-frequency detail x1.15..x1.30 -- which is what the separately-measured `ui-8ax-fullres-background` result said the game draws. Also closes a long-standing dangling reference: `pmbase.t32`, recorded as "on the disc nowhere", is the `GP_STAGE_CLEAR` child the scan called `8AX`. RATC sibling references now resolve 10 148 of 10 148. Verified: 114/114 sylpheed-formats unit tests (including two new ones pinning the `8AX` case byte for byte and the no-block fallback), and every disc-gated integration suite in sylpheed-formats/sylpheed-cli. |
||
|
|
a46a922bc8 |
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. |
||
|
|
e63efabcfb |
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. |
||
|
|
7347db24df |
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.
|
||
|
|
5b218e6887 |
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.
|
||
|
|
3eba09aae0 |
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.
|
||
|
|
af43563806 |
re(ui): measure the paint-order hedge -- exact on 4 of 5, and bound the rest
`compose` claimed the derived paint order "reproduces both measured orders up to ties". That sentence was never measured and was stale by one: there are three measured orders, not two. examples/paint_order_audit.rs checks it. main menu (entries 5, 8) derived == measured 0 inverted pairs developer splash (11, 14) derived == measured 0 inverted pairs title (entry 4) DIFFERS 8, all same-key ties So the claim holds and the exception is entirely ties -- but two of those ties are total occlusions, not near-misses. The tied family is the five ptlogo_back2eff glows (key 32899); back2eff5 is 1133x280 and FULLY CONTAINS back2eff3 (82,824 px^2 = 100% of the smaller) and back2eff4 (152,047 px^2 = 100%). Derived paints it on top of two glows it entirely covers; the game paints it underneath. A tie-break by declaration index can therefore be wrong by a whole layer. The title itself is unaffected -- it has a measured order. The port's actual exposure, per screen: title, main menu and developer splash all use MEASURED orders; the publisher splash is derived but has ZERO ties, so it is fully determined; EXTRAS is derived with 15 tied pairs of which only 2 OVERLAP. Two element pairs on one screen is the whole risk, and that is what HANDOFF now says -- not the raw 15, which would have overstated it 7x. Reach stated: this compares the derived order against orders measured from the game, not an independent derivation, so where no measured order exists only the tie exposure can be checked. Overlap uses pivot*2 as the element size at its resting placement. Stale comment in compose corrected. METHOD: a hedge in a code comment is an unmeasured claim; and count the cases that can bite, not the ones that match the pattern. |
||
|
|
d6a24f40f7 |
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.
|
||
|
|
d18a99a055 |
re(ui): the keyframe-time shift is favoured 26x by timing, rejected by a render
Follow-up on last iteration's unadopted candidate (+36 holds the NEXT pose's time, not its own). Two new results, pointing opposite ways, and both are reported. FOR, and calibration-free: the observed full-alpha hold : fade-out ratio on palogo_gamearts is 83 : 13 frames = 6.38. The shifted reading predicts 8.00. The current reading predicts 0.25 -- off by 26x. With the glow's 2 units/frame fixed and nothing else free, the current reading says the logo holds full alpha for 2.0 frames; the capture holds it for 83. This is no longer the shape argument the candidate rested on. Also for: rest()'s plain max-dwell fallback picks a=0 -- a transparent pose, for a publisher logo -- under the current reading, and the visible a=255 hold under the shift. Only the rest_plateau special case rescues the render today, and that is the case the port agent reported a bug in. AGAINST: rendering every build of six UI paks under both readings, 10 of 11 compared are byte-identical and one changes -- GP_TITLE build 7, the Japanese twin of build 4, by 13.1% of pixels. Build 4, the one verified against a live capture, is unchanged either way, so the single build the shift moves is the one with no capture to adjudicate it. The proxy goes against the shift: language twins are the same artwork, and build 7 reads 70.94 mean luminance as decoded against build 4's 71.41, but 76.32 shifted. Correlation does not separate them (0.6206 vs 0.6201). These constrain different things -- timing versus pose selection -- and rest() is a heuristic layered on the times, so moving the times moves its tie-breaks. Adopting the shift means revisiting that heuristic in the same change, with no build-7 capture to verify against. Default UNCHANGED. Experiment reachable via SYLPHEED_KF_TIME_SHIFT=1. Shifted-mode suite: 122 passed, 0 failed across 3 suites at commit time. |
||
|
|
ffd1a30ba7 |
re(ui): decode keyframe +12 as screen-plane rotation in degrees
The rotated quads on the title screen come from the keyframe block after all. The earlier negative -- "every GP_TITLE build 4 element has all three angle words at zero" -- read the right bytes over too small a region: it walked the top-level declaration table, and the rotated elements are the nested leaf records ptloop01.rat / ptloop02.rat. Confirmed against the framebuffer rather than against our own renderer. The two records declare +12 = 30 and -45; the GPU capture submits their quads at +30.26 and -45.28 degrees -- magnitude and sign, two different values. Corroborated by shape in GP_BUNK 117ca14f, where +12 ramps 0 -> 360 with position, scale and alpha constant: a spin in place. Identifying which draw it was needed edge lengths, not bounding boxes: 400x1076 and 400x1444 against pteff03/pteff03a 399x180 at the elements' two different declared scales, 600% (1080) and 800% (1440). The same test names three known-positives in the capture (ptlogo1, ptcopyright, ptbtn00), so it passes its own control. Keyframe gains rotation_deg plus unknown_4/unknown_8, carried rather than dropped. NOT rendered -- ui_layout::blit is axis-aligned only, so the reference renderer and the port will both draw these upright until a rotating blit exists. The census tool ships with the trap that broke its first version: nested RATC blobs are not 4-byte aligned, so an aligned scan found 0/3 of its own control blocks and missed 16 341 blocks. Disc-wide +12 is non-zero in 14.50 % of 83 862 blocks. sylpheed-formats tests, SYLPHEED_DISC set: 131 passed, 0 failed across the 6 suites finished at commit time; the run had not yet completed. |
||
|
|
e6ad72c7a0 |
re: three angle fields found in the keyframe -- and they are not the
title's rotation Looking for where the rotated quads come from, the obvious candidates were the three keyframe words at +4, +8 and +12 that ui_layout.rs documents as zero. They are not zero. Across 72287 keyframe blocks disc-wide they are non-zero in 4.81, 4.56 and 15.82 percent of blocks, and read as signed values clustering on 180, -180, 90, -90, 120 and 22 -- degrees. Three of them, so plausibly rotation about three axes. I have marked that amber because it is the shape of the numbers and nothing more; no observed rotation has been tied to a value. The doc comment is corrected regardless: "0 on every frame seen" was an artefact of the sample. And they do not explain the screen I was chasing. Every element of GP_TITLE build 4 has all three at zero, element by element, while the game demonstrably submits rotated parallelograms there. So the title's rotation comes from outside the keyframe data and is still unidentified. One correction to my own last write-up, flagged rather than left: I stated that the skewed draw IS the swoosh. It is the only skewed geometry in the capture and the swoosh is the only diagonal element on the screen, so the inference is reasonable -- but I never confirmed it by matching the draw's texture or screen position to that element, and I should have said so the first time. |
||
|
|
4cee06c07f |
re: additive blending refuted, the swoosh is not displaced, and the
residual is smaller than I said Testing the candidate I raised last iteration rather than carrying it. Blending bit-0x02 sprites additively moves every measure the wrong way -- whole-frame mean diff +0.55 to +1.04, swoosh-band mean +1.83 to +3.98, band edge-correlation 0.6971 down to 0.5578. So the bit is real and independent but does not select an additive blend. I reverted the experiment and kept the word as T8adImage::flags, documented and not acted on; the render is byte-identical to before. Second refutation: the swoosh is not displaced. Shifting the band over plus or minus 80 by 8 pixels peaks sharply at zero, 0.7342, falling to 0.22 at 24 px. So the pivot story is dead twice over -- inert at scale 100, and no displacement to explain anyway. And I have restated the residual, because earlier sections overstated it. The +16 to +34 band tiles I quoted were measured WITHOUT --primitives. With the dim drawn the band's average is nearly right at +1.83; what is wrong is its structure, tiles running -38.6 then +33.8 and cancelling. Six candidates eliminated now and none confirmed. One caveat I owe the port agent about the capture I gave them: it is at t=4.0s, roughly 174 keyframe units into a screen whose elements have keyframes out to t=600. I judged "settled" from mean luminance, which cannot see a thin sprite still moving. It is settled for the bulk of the screen and not proven settled for every element -- which is a live alternative explanation for a structural difference in exactly the band the sweeps cross. METHOD: cargo build passing does not mean cargo test compiles. Adding the field built the library in 1.48s and broke two test-only struct literals; cargo test failed with exit 101. |
||
|
|
5e6cf0db92 |
re: rest() fixed -- a trailing keyframe run is the hold when it is
VISIBLE, not when its pose repeats The port agent's report was right about the defect and about which elements it hits, and its proposed condition does not survive the case the exclusion was written for. Their test -- the final untimed keyframe has the same pose as the last timed one -- is also true of pgptitle.rat, whose trailing run is two identical transparent frames. Adopting it would erase the word PAUSE again, which a committed capture disproves. What separates the two is alpha: an exit fades the element out so its last keyframe is transparent, while an element with no exit ends on the pose you can see. So rest_plateau now accepts a trailing run exactly when it is visible. Verified against a capture rather than against another renderer, which is the point their own report made: ptframe1's rest moves from (620,108) t=16 to (440,108) t=62; the changed pixels are 10082 in a bounding box of x 440-839, y 108-577, which is exactly the 400x470 at (440,108) they predicted; and correlation against main-menu-oracle.png over that region improves from 0.9596 to 0.9748. The PAUSE wordmark is unchanged across all three pause builds. This also closes a question ui-paint-order-key.md has carried for a while, that ptframe1 and ptframe2 rest at alpha 0 while the capture shows the menu frame plainly. One trap cost most of this iteration and goes in METHOD: CARGO_TARGET_DIR is redirected in this container, so ./target/debug/sylpheed-cli is hours stale and every render I made against it was old code. Byte-identical before and after reads as "no effect" when it actually means "you ran the old binary". The full disc-gated test suite was still executing when this was committed; the verification above is artifact-based. |
||
|
|
47f423f914 |
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> |
||
|
|
a81da8cfcc |
viewer: a Cutscenes browser -- the manifest was invisible plumbing
`movie_manifest` has been parsed since the movie-voice work and rendered nowhere: it resolved a voice bank and that was all. So the only way to find a cutscene was to hunt `.wmv` files in the ISO tree, where nothing tells you which mission a file belongs to, whether it has subtitles, or what is said in it. View ▸ Cutscenes lists all 104 manifest slots with mission/phase, kind, movie, subtitle track, voice token and telop, and -- the part that needed no new parsing, only a route -- resolves the captions to a readable TRANSCRIPT with a language selector. Subtitles were previously burned into the video during playback and reachable no other way. Three negatives are shown rather than smoothed over: * 5 manifest-bound movies have no `.wmv` (logo1-4 and an encoder test clip). They are marked and get no Play button instead of one that would fail. * 9 of 101 movies resolve no English transcript. * the `.prt` telop overlay is named by the manifest and we have no parser, so the reference is shown labelled "not decoded" rather than omitted. `cutscene_catalog_binds_movies_and_transcripts` pins all of it against the disc -- 104/101/99/99/22, the exact absent-movie list, 92 transcripts -- because a browser that quietly dropped these would look complete and be wrong. The counts independently reproduce docs/re/movie-subtitle-link.md. Play routes through the normal FileSelected path, so the existing video player handles it exactly as it would from the tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
761e91de96 |
viewer: open the whole sound bank, not just the voice half
The library enumerator kept only names containing VOICE or \Briefing\, and read eng\sounds.tbl unconditionally. So the Explorer could reach 4382 of the 9519 banks in sound.pak: no music, no jingles, no sound effects, and no Japanese voice at all -- roughly half the disc's audio had no route to the UI. `slb::list_audio_entries` now returns every named bank with the category its path implies (Music / Jingles / Sound effects / Radio / Dialogue / Movie voice / Briefing). `list_voice_clips` is that, restricted to the spoken categories, so its existing test still guards the old behaviour. The 36 root banks carry no language component and appear whichever table is read; the window gets an English/Japanese switch that re-reads the other sounds.tbl, since the table name IS the selector. Two defects the decode found, both recorded in docs/re/structures/sound-pak-contents.md: * `Static.slb` -- the SFX bank -- declares 616768 bytes more than sound.p04 holds. Not our extraction: p04 matches the ISO's own directory record, and a sweep of every pak on the disc finds this one entry over-running and no other. It is the highest-offset entry, so its comp_size is an allocation size. A short read is now allowed for the tail entry ONLY; any other overrun stays an error, because clamping it would hide real damage behind a half-decoded asset. The bank went from unreadable to 514 s of audio. * the left-channel downmix was applied to everything. Right for voice (mono content however stored), wrong for music (a real stereo mix, half of it discarded). The caller now decides from the category. 35 of the 36 shared banks decode; JNGL_001 does not, and says so in the player instead of the panel silently closing. Its payload is not a whole number of XMA1 packets from any known data offset, so it is likely not a plain headerless stream -- written up rather than papered over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cf1f5763b0 |
viewer: make the two screen presenters agree, and expose the compose options
The UI Screens window enumerated with `is_build` while the PAK browser's inline preview composes anything `parse_build` accepts. So the browser drew screens this window flatly refused to list -- most visibly `palogo`, the publisher splash, which declares its sprites directly and has no `.rat` layout child. `compose_screen` now gates on `is_composable`, the documented superset (every `is_build` bundle passes it), so anything the list offers is drawable and the two presenters share one rule. A "Fragments" toggle widens the enumeration to it as well, off by default: the extra ~1786 bundles are mostly two-element fragments (a button beside its glow) that would bury the real screens. It re-scans, which renumbers the display ordinal -- harmless, because the pak ENTRY index is the locator and the stale catalog is cleared. The toggle sets a `rescan` flag rather than self-sending RequestScreenCatalog: a system that both reads and writes one event type is a B0002 panic at startup, which is how the Save browser broke. Also plumbs the last two ComposeOptions the CLI had and the UI did not -- `black backdrop` (what a framebuffer capture must be compared against) and `primitives` (decoded, but paint order unsolved; hover text says so). Verified: `screen render --all --build 13 GP_TITLE.pak` draws the SQUARE ENIX splash, 2/3 elements, the third being the .prm quad. Workspace builds, viewer reaches steady state, disc-gated suite 20/20 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b239510fd6 |
viewer: stop the UI Screens browser loading forever
Three compounding causes, found by tracing every place a `loading` flag is set
against every place it is cleared.
THE COST. `compose_screen` inflated EVERY entry in the pak and held every build
in memory at once, purely to index into the result -- then did it again on every
checkbox click, re-opening the ISO from scratch each time. But the catalog
already records each build's pak ENTRY INDEX, so the locator was there all
along: compose now reads exactly one entry. `RequestScreenCompose` carries
`entry` (the locator) beside `build` (the display ordinal).
`build_screen_catalog` had no budget at all, while the pak browser has capped
exactly this work since it was written. It now skips oversized entries and stops
at a ceiling.
That ceiling is 384 MB, not the pak browser's 64 MB, and the difference is the
point: GP_HANGAR_ARSENAL inflates past 160 MB and holds ~390 builds, so a 64 MB
cap would have quietly hidden most of them -- trading a hang for a wrong answer.
When the ceiling IS hit the pak is marked `truncated` and the UI says so.
THE LATCH. `poll_loader_channel` treated a disconnected channel exactly like an
empty one, so if a worker died every in-flight spinner stayed up for the life of
the process -- and the `if loading { return }` guard at the top of each handler
then refused every retry. Disconnect now clears the flags and reports it.
Verified: the workspace builds, the viewer reaches steady state, and one
composite still draws 11/11 elements of the tutorial pause menu.
|
||
|
|
16a19219e8 |
slb: guard the wave-boundary identity with a test, and expose data_at
A wave runs to data_at + declared_size, and there is seek magic exactly there whose little-endian packet count at +12 times 2048 equals the declared size -- 7620/7620 disc-wide. That is the decoder-independent boundary and the thing that proves the declared sizes honest, so it should not be able to regress silently. The boundary routinely lies outside the entry's own comp_size window, so reading it needs the flat segment stream rather than the entry slice; PakArchive gains a small data_at(offset, len) accessor for that. Test walks a bounded slice to stay fast -- 792 banks in this run, all holding. 8 disc tests pass. |
||
|
|
c3ff6aa0e8 |
slb: read Channels instead of assuming mono -- and retract the TCAF_608 conclusion
I read the seek chunk's packet count big-endian; it is little-endian at seek+12, with size == 8 + 4*count. And a seek sits immediately AFTER its own data, so an entry's first seek usually belongs to the PREVIOUS bank (implied start -25232 for D_452, -145988 for TCAF_608). I was comparing an entry's first seek against its first data -- different waves by construction, which is why no reading lined up. With that fixed, the declared sizes are honest: every RIFF-bearing entry on the disc has seek magic at exactly data_at + declared_size with count*2048 == declared. 7620/7620, zero failures. VOICE_TCAF_608 is not truncated. Its Channels is 2 and I decoded it as mono; read as stereo it gives 6520176 bytes = 33.96 s, agreeing with both length signals in the bank (33.88 s from cumulative samples, 33.97 s from PsuedoBytesPerSec). 170 of 8021 banks (2.12%) are stereo -- exactly the rate of my 1-in-60 outlier. This is the mono/stereo trap already documented on this very page, met from the other direction: I had written 'at two channels every bank yields one frame' and then spent several passes blaming missing data for a one-frame decode. Code fix: to_xma_riffs built the leading segment with a hard-wired mono fmt. It now reads Channels from the bank's first RIFF. 7 disc tests pass. |
||
|
|
220e250453 |
slb: the seek chunk gives the data offset structurally, and breaks the 28 ties
The ties needed a different signal, not a longer scan. Banks carry one: a seek chunk sitting on a packet boundary, so seek_pos % 2048 IS the data offset. On the 6033 labelled banks with a seek before their first RIFF, 6031 agree (99.97%) -- better than the packet scan and structural rather than statistical, so scan_data_offset now tries it first. On the scan's 28 ties it resolves 26 correctly and 0 wrongly (2 have no usable seek). Combined rule scores 7354/7358 = 99.95%, up from 99.62%. 762 of the 1495 RIFF-less banks carry a seek, so the signal exists where it is needed. Also ruled out, since a wrong offset was this page's whole subject: the header is not audio being discarded. Adding 0 to the candidate set, it wins 6 of 7358. 7 disc tests pass. |
||
|
|
b7b29dcb05 |
slb: the declared data size is an upper bound, not an exact one
Two comments claimed it is 'honest per sub-wave'. Measured: 5296 of 7586 banks declare more than the entry holds and none declares exactly what it holds, so the existing .min(slb.len()) clamp is load-bearing rather than defensive. Comment-only change; 7 disc tests still pass. |