52a2eae4bfdc41343cd143ad1e9dda25adc06796
461 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0e03e7496e |
re: the three streams of a voice cue are decoded CONCURRENTLY, not alternatives
Refutes a framing of mine that two documents and the port s exporter were built on, so it is a new page rather than an edit. voice-region-leading-chunk.md read a long cue s three streams as three presentations of one take, and from that came the instruction "take one stream, do not sum", which the port implemented. The open question was which presentation the game plays. It has no answer. Booted with --xma_param_probe=true, the cvar whose own comment says it is keyed to reveal which sub-wave of a movie s .slb the game actually decodes. The guest opens three XMA contexts and decodes all three concurrently: ctx=0 packets=632 byte_size=1294336 ch=2 48000 ADV stream 1 ctx=1 packets=546 byte_size=1118208 ch=2 48000 ADV stream 2 ctx=2 packets=572 byte_size=1171456 ch=2 48000 ADV stream 3 Byte-exact against the three streams payloads taken independently off the disc (RIFF size minus 60). Only these three contexts appear in the run. So a consumer that picks one discards two thirds of what the game mixes. Both "three presentations of one take" and "take one stream" are withdrawn -- and the previous behaviour is not thereby right either, because an equal-gain 1/n sum of channel pairs is not a downmix and the port s measured 6.02 dB loss was real. Neither rule is established; a consumer is authoring. Three concurrent stereo streams is six channels and N stereo streams is how XMA carries multichannel on the 360, which would also explain the 1-or-3-never-2 span census. Recorded as a hypothesis, not a result: all three fmt chunks declare ChannelMask 0x0002 identically, which is not what distinct channel roles should look like. Everything byte-level survives: the leading chunk being stream 1 clipped by our own guard, the 70 + 8 + 17 decomposition, the bank-header discriminator. Reach: one cue, one boot. That 28 cues are 3-stream is decoded; that all three decode concurrently is measured on ADV alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd |
||
|
|
aad050cb4a |
formats: XMA1 is not a WAVEFORMATEX -- audio info was reading three wrong fields
parse_riff_wave read every fmt chunk as a WAVEFORMATEX. XMA1 (tag 0x0165) is not one, so audio info reported the disc s movie voices as 16 channels, 4310 Hz, 2-bit: 16 is wBitsPerSample read as a channel count and 4310 is wEncodeOptions (0x10d6) read as a sample rate. This misled me earlier in the session and I recorded it as a limitation before finding the cause. XMA1 carries XMAWAVEFORMAT followed by one XMASTREAMFORMAT per stream. The reader now branches on the tag and reads bits at +2, PsuedoBytesPerSec at +12, SampleRate at +16 and Channels at +29. The same three files now report 2 channels, 48000 Hz, 16-bit. The consequence worth having: this crate has no XMA decoder, and data_bytes / PsuedoBytesPerSec is the only route to a duration. Checked against durations decoded independently by the port: ADV presentation 1 137.34 s declared 137.324 s decoded +0.012 percent ADV presentation 2 137.33 s declared 137.324 s decoded +0.004 percent S00A presentation 1 93.71 s declared 93.694 s decoded +0.017 percent So the corpus can now get XMA1 durations off the disc without a decoder, which is a capability I had written down as absent. It is a declared rate rather than a measurement of the samples, and the CLI labels it as such. Regression test pins the real on-disc header bytes and asserts the duration against the independently decoded 137.324 s. 115 lib tests and 3 media disc tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd |
||
|
|
7a0e18889f |
re: the dual-mono explanation for the extra stream bytes does not generalise
The port chose a voice presentation on the argument that ADV chunk 1 is mono-in-stereo and chunk 2 is dual-mono, so chunk 2 s extra bytes encode a duplicated channel rather than fidelity -- which would explain its higher declared PsuedoBytesPerSec without appealing to encode quality. Their ADV channel measurement stands. The generalisation does not. If stream 3 were systematically the same take with its channel duplicated, its size ratio to stream 2 would be tight across the 28 three-stream cues. Measured: min 0.0778 (S00A, the silent one) median 1.2565 max 2.9163 (S06A) sd 0.5057 within 15 percent of 1.0: 12 of 28 A 37x spread is not a duplicated channel, and the declared rates scatter with them -- S06A is 5661 against 16513 B/s. Whatever distinguishes the three streams varies per cue rather than being a fixed channel-configuration triple. This does not touch the port s decision, which is to take the loudest presentation: that is a per-asset content measurement, not a structural rule, so a scattering ratio cannot undermine it. It touches the explanation, which should not harden into a fact about the format. Two curiosities recorded: S12B s three streams are byte-size identical at 14396 each, and BIRD_224 is 3-stream while being a non-movie cue, so the shape is not exclusive to cutscenes. Also narrows the settle-time page s own generalisation. The port measured its boot the way this corpus measured the game and found the sequencer NOT late -- its 0.6 s discrepancy was arrival-to-arrival timestamps compared against visible spans, the plate-delay trap in a second place. So what is supported is that rest.t is the wrong landmark for the TITLE, not that everything paced off it is late. And the offered re-take of the one-run menu figures is recorded as declined, with the reason, rather than left looking unfinished. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd |
||
|
|
ff715938f5 |
re: withdraw a stream-selection recommendation that contradicted itself
The port implemented "take the highest-rate stream" and reported that on ADV it selects the QUIETER presentation -- chunk 2, 1171516 B at -8.3 dBFS, over chunk 1, 1118268 B at 0.0. They were right to flag it rather than accept it. My sentence was "the highest-rate, highest-gain one is chunk 1". Those two criteria do not select the same stream and the sentence should never have joined them; the parenthetical named chunk 1 while the rule named chunk 2. Withdrawn. What the header does decode, read off the bytes: the fmt chunk is a 32-byte XMAWAVEFORMAT, little-endian, and +0x20 is a declared PsuedoBytesPerSec -- 8142 and 8530 on ADV s two presentations, agreeing with the computed rates to 0.02 percent, with 48000 Hz at +0x24. So the rate is decoded rather than inferred. What it does not decode: wEncodeOptions (0x10d6), channel count and channel mask are byte-identical across the presentations. Nothing in the header ranks them, so stream selection stays an authored choice and the port must know it is authoring. Settleable in one emulator run -- a capture of the intro with the dialogue audible says which level the game plays -- and not yet done. Also records that sylpheed-cli audio info is misaligned for XMA1: its "16 channels / 4310 Hz / 2-bit" is wBitsPerSample, wEncodeOptions and the channel fields read at the wrong offsets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd |
||
|
|
801062c933 |
re: a long voice cue is three presentations of one take, and our guard clips the first
Closes the last open question on the voice regions: why one cue s byte span decodes to ~2.6x the movie s length. The port measured, with controls including a cross-movie negative, that a region s leading chunk is the TAIL of the full-length chunk that follows it -- r = 0.998 at a lag that puts it flush against that chunk s end, residual 16.7 dB down over 84.5 s. They withdrew their own earlier 0.768, which came from a search that scored best on the boundary of its own lag range. Checked it here by an independent route that needs no decoder. If the leading chunk is the tail of a full-length first stream, the whole leading stream should be one complete take of chunk 1 s duration. For ADV: 504464 + 808304 = 1312768 B at chunk 0 s byte rate of 9559.7 B/s is 137.323 s, against chunk 1 s measured 137.324 s. One millisecond over 137 seconds, from byte rates rather than from envelope correlation. And the byte structure settles the shape disc-wide. Counting stream starts inside every inter-descriptor span: 258 hold exactly 1 stream, 28 hold exactly 3, and nothing holds 2 or any other number. All 20 spans over 1.5 MB are 3-stream. The 95 movie regions decompose 70 + 8 + 17, and the 8 are independently the same 8 the first census found as bank-header-with-3-chunks. So 359 s = 84.55 + 137.32 + 137.32: three presentations of one take, the first clipped by resolve_movie_voice_region s own 1.5 MB guard. Consequences recorded for the port: dropping the leading chunk is removing a duplicate rather than truncating, so the hedge is lifted; but summing chunk 1 and chunk 2 is wrong, because they are the same take at different gain, not two stems. Take one stream. Also flags a coincidence I nearly built on: the 504464 B constant is structural, not proportional -- ADV s proportional prediction lands within 8 bytes of it and S00A s is 4305 B out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd |
||
|
|
06a1779b19 |
re: the voice region s leading chunk is the movie s OWN dialogue, and a guard puts it there
My own leading hypothesis -- that the leading chunk is an in-mission VOICE_D_* line -- is refuted, on the route the port suggested: widen the enumeration past the 95 manifest-bound movies and the byte-span test settles it without anyone listening. Scanning the stream for every trailer descriptor (the (id, 0x11) pair whose id repeats at +0x800) gives the complete cue partition, mission lines included: 287 descriptors in a 116.2 MB window, all 287 carrying an id the 4280-name registry names. Every one of the 17 leading spans is bracketed by desc(N-1)..desc(N) where desc(N) is that movie s OWN cue id. Zero mission lines. The mechanism is a guard in our own resolver. resolve_movie_voice_region takes the predecessor trailer as the region start, guards it with end - start < 1_500_000, and falls back to the .slb TOC anchor when that fails. Cues with a true span over the guard: 17, of which 17 are stream-opening. Cues under it: 78, of which 0. Perfect discrimination both ways. The anchor sits a constant 504464 B after the true predecessor trailer on all 17, which is unexplained. Not established, and stated as such: this does NOT mean the export truncates N seconds. The port s decode already has ADV s region at 359 s against a 137 s movie, so it over-covers and the byte-to-time mapping is not linear. No XMA1 decoder in this container to check. Also withdraws a claim this page had adopted from the port -- that chunks 1 and 2 are two stems of one performance. The port refuted its own claim by decoding: S00A chunk 2 is digital silence, ADV chunk 2 is 0.60x chunk 1 with the residual 26.8 dB down. Equal duration was a shape match and Q10 s music census should not have been carried across to voice on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd |
||
|
|
7e12a3b1f9 |
re: the voice-region third chunk is a different structure from the BGM one
The port hit a 2+1 chunk signature on a resolved movie-voice region and asked
whether the bank-header explanation that closed HANDOFF Q10 also covers it,
rather than assuming it. It does not, and the discriminator is mechanical.
Disc-wide over the 95 English movie-voice regions the manifest binds:
78 open with a bank header -- bank_header_len fires, 10240 B = 5 packets
exactly, every time. That is the BGM case.
17 open with a leading headerless stream -- bank_header_len is None, and all
17 have length congruent to 1392 mod 2048, the disc s own derived data
offset. No other residue occurs.
0 begin at a RIFF.
Counting chunks does not discriminate: 8 bank-header regions also yield three
chunks. slb.rs already predicted this in its own doc comment -- the header
signature has "zero false positives on the 7993 mid-bank windows, where the
leading region IS real" -- and a voice region is a mid-bank window by
construction.
Also tested the obvious defence of dropping the leading chunk, that it is the
predecessor cue s audio: 0 of 17 leading spans lie inside any other resolved
region, 0.0 percent on every one. The test finds overlaps where they exist (16
overlapping pairs among the regions, 60 exactly-adjacent boundaries, 73 of 78
bank-header regions starting where another ends), so the zero is not the
instrument.
Left open, with reach: the census covers movie-voice regions only, and the same
stream carries the in-mission VOICE_D_* cues, which are not enumerated -- the
leading bytes plausibly belong to one of those. Could not be settled by
listening: no XMA1 decoder in this container, and sylpheed-cli audio info
reports these chunks as 16 channels / 4310 Hz / 2-bit, which is visibly wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
|
||
|
|
ae42f7f850 |
re: the paint-order tie-break costs the port zero pixels, not 24 pairs
The census bounded WHERE a wrong tie-break could show and said outright that nobody had measured how many of those pairs change a pixel. Measured. `compose_with_order` renders a bundle in a caller-supplied order; the new example renders each screen twice, once derived and once with one tied pair swapped, and diffs. Same-key elements are contiguous under a stable sort on (key, i), so a swap paints nothing else in between. Controlled per entry: swapping an OVERLAPPING pair with DIFFERENT keys moves 36 305 to 771 479 px (max delta 254). Where no such pair is drawn the output says so rather than reporting an uninterpretable zero. * EXTRAS (entries 6/9) and the main menu (5/8): 0 px. The tied ptframe pairs ink ~3 600 px each and share NONE of them -- the 102x132 rect overlap was an artefact of approximating an element as pivot x 2. Blend-independent. * Across all 31 drawable overlapping tied pairs in GP_TITLE, the largest change any of them makes to any channel is 3/255. * Withdrawn: "a wrong tie-break can be wrong by a whole layer". That rested on ptlogo_back2eff5 geometrically containing two other glows. Rendered, the swap moves 6 390 px by max delta 2. Containment is not occlusion when the container is a near-transparent glow, and nobody had rendered it before asserting it. Reach: this measures our compositor's sensitivity to order, not the game's. The zero-shared-ink results hold under any per-pixel blend; the delta<=3 figures assume ours. 13 disc-gated ui_paint_order_disc tests and 114 lib tests pass unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd |
||
|
|
c5b815c5aa |
re: the paint-order tie-break costs 24 overlapping pairs, not one element
The port challenged HANDOFF's "costs one element's blend on one screen" with a census of 105 elements sharing a layer key across 12 of 16 screens. The two numbers count different things -- elements vs overlapping pairs -- so it is not the contradiction it looked like, but the objection stands and the line was wrong. paint_order_audit already reports overlapping ties per entry, and over all 16 GP_TITLE entries: 5 use a measured order and carry no tie risk; of the 11 that fall back to the derived order, 7 have overlapping ties, 24 pairs in total. The Japanese title (entry 7) alone has 16, because it is the twin of the one build whose measured order exists and has none of its own. Overlap bounds where a wrong tie-break COULD show, not what it costs; nobody has measured how many of the 24 change a pixel, and the port is right about that too. The rule itself is unaffected: the layer key is still decoded and the derived order still reproduces every measured order exactly except the title's eight tied pairs. Census committed at docs/re/data/paint-order-ties-gp_title.txt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nsxw1A9JseUw99Yw1ZRQzY |
||
|
|
c1f3608b07 |
formats: a music bank's third sub-wave was its own header
The port hit `sound_bank_riffs("BGM_103.slb")` returning three against a census
that says two, and refused to guess which to drop. It was our reader.
`to_xma_riffs`'s hybrid branch derives a leading packet stream's start as
`first_riff % XMA1_PACKET`. That is right only when the bank header is smaller
than one 2048-byte packet -- true of the voice banks the branch was written for
(1392/1468/1600/1728), false of a music bank, whose header is exactly five
packets. The modulus returned 0 and the whole 10 240-byte header was emitted as
sub-wave 0.
The header states its own length, so the guard needs no threshold: BE u32 0x800
at +0x18 with the bank id repeated at +0x00 and +0x20, header length in blocks at
+0x24. Disc-wide over sound.pak's 9 519 entries, 28 match at offset 0 -- every
music bank, ids 1001-1023 and 1101-1105 -- and on 28/28 the declared header ends
EXACTLY at the first RIFF. Zero have a gap, so a header and a leading packet
stream never coexist here; zero false positives among the other 9 491.
Controlled rather than argued: decoding the emitted region through the same
chain, on the same bank, in the same run gives 0.009 s of PCM where the bank's
real wave 0 gives 87.744 s against a declared 87.75. The region is also 99.1%
zero bytes. And the oracle had already said two -- the XMA probe at the main menu
saw exactly two streams, at BGM_103's two declared wave sizes.
BGM_106-109 are deliberately NOT in the 28: their entries start mid-bank, so they
have no header at offset 0 and their leading region is real audio. The
VOICE_D_453 recovery is untouched and its tests still pass, 10/10 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
|
||
|
|
3491d30066 |
re(media): the disc ships movies in TWO audio profiles, and 28 of them are 5.1
Probed all 97 movies. 28 are wmapro 48 kHz 6-channel 5.1 -- ADV.wmv and every S*.wmv story cutscene; the other 69 are wmav2 48 kHz stereo, every RT*.wmv and hokyu_*.wmv. The split is cinematics vs in-mission radio chatter. Both movies the menu milestone needs, ADV.wmv (the boot/attract intro) and S00A.wmv (the new-game intro), are in the SURROUND group. Why it matters: one ffmpeg command over dat/movie/ produces two different kinds of result and records neither. The 69 stereo files pass through unchanged; the 28 surround files get downmixed 5.1 -> stereo by ffmpeg's DEFAULT matrix, folding centre-channel dialogue into L/R at a weighting nobody chose and which is not stable across ffmpeg versions. That is a content decision inherited by accident, so it should be stated explicitly and recorded beside the command. Credit where due: found by the human while checking a transcode, verified independently here and widened from one file to the whole disc. Two METHOD entries from the same episode, both about measurement rather than format: don't probe a file another process is still writing (a half-written transcode reported 33 s against a 137 s source, no error, nearly a filed bug), and a difference-signal RMS is meaningless before cross-correlation alignment (-34.2 dB against a -25.3 dB source looks like failure and is inconclusive). |
||
|
|
7eeae3006a |
re(ui): the focus ring SPINS, the game draws it, and the leaf owns the f record
Three things, all from parsing ptbtn0Nf.rat as a build. 1. THE RING SPINS. Its two keyframes differ in exactly one field: rotation_deg ramps 0 -> 360 with position, scale, alpha and tint all constant. A spin in place, the same shape as the GP_BUNK example already recorded. 2. THE ORACLE CONFIRMS THE GAME RENDERS IT. In the OPTIONS-focused capture the ring's bright head sits in a completely different angular position from the sprite's own -- caught mid-spin. This is a SECOND independent confirmation that rotation_deg is drawn, now on a different screen and a different element from the ptloop sweeps, and it raises rotation's priority: it is not a title-only concern that sits off-screen at rest, it is the main menu's focus marker. NO ANGLE IS QUOTED. A brightest-region centroid says ~250 deg, but the control refuses that precision -- rotating the sprite by a known 30/90/180/270 and re-measuring gives errors up to 19.8 deg. What survives the error bar is that a <=20 deg error cannot manufacture a ~250 deg displacement. 3. WHICH PLACEMENT WINS -- correcting this page's own earlier caveat, which said to use the leaf only for elements the parent does not declare. Right for a BASE record, wrong for an f record: the parent declares NO element for ptbtn0Nf.rat at all (zero of build 5's 16), so the f record's placement comes from its leaf for BOTH elements, label included. The label's (-7,-7) is load-bearing -- the f sprite is 13px larger per axis and -7 keeps them concentric (535+96/2 = 583 vs 542+83/2 = 583.5). Corroborated against the oracle: the focused-minus-unfocused region is x 505..703, and the leaf predicts a right edge near 707 where the parent reading predicts 714. Also exposes UiBuild::records (name -> (offset, size) of a nested .rat leaf). Nested records were parsed into a PRIVATE map, so a consumer holding a UiBuild could not locate a leaf's bytes at all -- which is exactly what blocked the port from reaching the ring. |
||
|
|
b21c8e4118 |
re(ui): the focus ring's position is decoded -- a .rat leaf parses as a build
The port needed ptbtneff01.t32's placement and was about to author it from an eyeballed PNG measurement. It does not have to: a `.rat` leaf needs no new reader. Its first 32 bytes have a bundle header's shape -- "RATC", 0x3c declaration-entry size at +4, element count at +20, design 1280x720 at +24/+28 -- so ui_layout::parse_build reads it unchanged. The control is the base record, whose position is known independently: the parent screen reports ptbtn01.rat resting at (542,162), and parsing the leaf alone returns ptbtn01.t32 at (542,162). It reproduces all five buttons. Positions are absolute design-space top-left. The ring rests at (500, 156/236/ 316/396/476) for buttons 1-5 -- a uniform (-42,-6) from each button's own rest, identical in the Japanese bundle. The bright label is a uniform (-7,-7). Two things recorded rather than smoothed over: a leaf's placement DUPLICATES the parent's rather than being relative to it, and the two copies are not always byte-equal (ptbtn04's parent says y=401, its leaf says 402) -- the parent is what compose honours, so the leaf is the source only for elements the parent does not declare, which is exactly the ring. And `screen render --focus` is blind to the ring for the same reason the port's exporter was: el.focused is name-based on top-level elements and neither walks into the leaf. |
||
|
|
9ca1eb50fd |
re(ui): answer four of the port's five asks -- splash, fade-out, focus, gamma
1 SPLASH ADDRESSING (was blocking P3). No content predicate exists: design size fails (every extra composable bundle sampled is 1280x720, like every screen) and element count fails (fragments run 2..15, the splash halves are 3 and 7). But GP_TITLE needs none -- `--all` adds exactly four bundles there and all four are real screens, with the --all index equal to the pak entry index 1:1. And there are TWO splash screens: 11/14 are the developer logos, 10/13 are the SQUARE ENIX publisher wordmark, which the port did not have and which the boot shows first. 2 FADE-OUT (was blocking P3). It is (a), and it is bigger than the fade quad. Every element ends on exactly ONE untimed keyframe, which rules out (b); that block is where the screen plays out -- quad to a=255, buttons/labels/glows to a=0, frames hold. (c) is refuted by a null test that discriminates: a black quad alone holds the button/background brightness ratio constant, and through the fade it falls 6.50 -> 1.94, 3.4x monotonic. 3 FOCUS (saves P5 rework). Over-vs-instead is unobservable -- the focused sprite covers the base at 100.0% of base-visible pixels on three pairs once aligned (true offset (7,7); the centre alignment reads a misleading 78-84%), and compositing both ways differs by RMSE 1.1 inside the button rect. The real defect is the focus record's SECOND element: ptbtn0Nf.rat declares ptbtneff01.t32 (a 42x46 glowing ring, focus only) plus the bright label, where the base declares one sprite. That ring is the marker the port draws nowhere. 5 GAMMA. The capture is not neutral: capture ~ 255*(render/255)^g, g ~ 1.34-1.49, and the chain says it is a ramp the GAME installed, not a capture artefact. So RMSE against captures has a floor. Reach stated: the flat patches are all dark (render ~0-60), so midtones and highlights are unconstrained. 4 ROTATION is a human's call and is recorded in MISSION, not acted on -- the port rotating while the reference renderer does not would make verify-screen report a large diff meaning "the port is right". The RE half is answered: rotation is about the declared pivot, measured against a GPU capture. The focus record's +20 element-count word is marked 🟡 not ✅ -- read on GP_TITLE's ten button records only; the disc-wide check is written and still running. |
||
|
|
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). |
||
|
|
56cc7acfc3 |
re(ui): a RATC child's name is stated, not inferred -- and it was hiding every menu background
`ratc::parse` named each child by scanning backwards for the last printable run of bytes before its magic. The format states the name explicitly instead, in an `opt ` block: `"opt " | BE32 len | name | NUL | 3 bytes | magic` -- the same block `ui_layout::opt_link` already read for a button's focus link. The scan agrees with it 17 918 times out of 17 942 and is wrong 24 times, every one the same failure: the 3 trailing payload bytes are themselves printable and beat the real name. For `pteff05.t32` those bytes are `38 41 58` = `8AX`, so the full-resolution background of all five menu screens registered under a name no element declares, resolved to no sprite, and `compose` dropped it through an early `continue` that -- unlike the two arms above it -- records nothing. The screen lost its background and `screen render` still reported "all resolved". `8AX` was never a name. Docs that treated it as one are corrected here. Disc-wide, and the control is the 17 918 the scan already got right: the `opt ` reading reproduces every one of them. Effect on the five screens is the signature of the same art at twice the resolution -- mean brightness unmoved, high-frequency detail x1.15..x1.30 -- which is what the separately-measured `ui-8ax-fullres-background` result said the game draws. Also closes a long-standing dangling reference: `pmbase.t32`, recorded as "on the disc nowhere", is the `GP_STAGE_CLEAR` child the scan called `8AX`. RATC sibling references now resolve 10 148 of 10 148. Verified: 114/114 sylpheed-formats unit tests (including two new ones pinning the `8AX` case byte for byte and the no-block fallback), and every disc-gated integration suite in sylpheed-formats/sylpheed-cli. |
||
|
|
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.
|
||
|
|
2532c056be |
docs: the splash .prm measured -- and its paint order is a sequence, not depth
ui-prm-primitives recorded that where a .prm paints on a screen without a measured order is unsolved. For the developer splash it is now measured. Every frame opens with two full-screen draws before any sprite. The second is untextured in all 212 frames with a constant vertex colour of FF000000 -- opaque black -- matching palogo_eff0.prm's declaration exactly: kind 0x10, pivot (640,360) -> 1280x720, one keyframe, a = 255. So the splash backdrop is an opaque black full-screen quad from the bundle itself, painted behind every sprite, which is why a splash render needs --black rather than the default backdrop. Not a general rule, and said so: the measured main-menu order puts pteff02.prm at position 4 and pteff00.prm LAST, the latter being the screen-transition fade. And a correction to an existing row. measured_paint_order returns [0, 2, 4, 6, 1, 3, 5] for the splash, described as "the .prm, then all three glows, then the three logos". But the glows and the logos never appear in the same frame -- 0 overlapping frames in 235 -- and two elements that never co-occur have no observable relative depth. Between those halves the vector records the order they were SEEN IN, not a front-to-back relationship. That does not make the render wrong, and element 0 is a real depth observation since the .prm co-occurs with everything. But the type of the claim matters: reading the vector as depth invites compositing all seven elements at once, which is exactly what does not reproduce the screen. METHOD: two things that never co-occur have no observable relative order; when recording an order, note which pairs actually appeared together. |
||
|
|
7988f52ce7 |
re(ui): deliver the measured splash sequence the port has to author
The activation decision is code, and MISSION already says the port authors the sequence -- so the useful move is to hand over the sequence measured rather than chase the code. From the 235-frame draw capture, at 1 frame = 1/30 s (2 units/frame, 1 unit = 1/60 s, both settled in Q1): publisher: SQUARE ENIX logo f1-90 90 frames 3.00 s+ at 0.00 (gap, nothing drawn) f91-93 3 frames 0.10 s at 3.00 developer: both glows f94-115 22 frames 0.73 s at 3.10 developer: both logos f116-211 96 frames 3.20 s at 3.83 Three limits, stated with the numbers rather than after them. The capture opens with palogo_sqex already at a=255, so the publisher phase began before the window and 3.00 s is a FLOOR -- every "starts at" is relative to the capture, not to boot. palogo_anima and palogo_anima_eff get 0 draws in all 214 frames, so a third pair's phase is not in this measurement. And it is one capture, one run: the glow->logo switch being a single frame boundary with no overlap is a strong shape, but each duration is one sample. What is solid is the part that matters: the 0.73 s and 3.20 s phases are each within 2% of their element's declared span, so the durations are the bundle's own and only the ordering is authored. That is the difference between a port transcribing timing and inventing it. |
||
|
|
7156591654 |
re(ui): re-establish selective activation by killing the alternative statically
Last iteration I withdrew "a bundle is a palette" because the evidence did not choose between selective activation within one bundle and two compositions shown in sequence. The alternative can be killed from the disc, which I had not tried. Hypothesis 2 needs a bundle declaring the GLOWS WITHOUT THE LOGOS. There is none. Every GP_TITLE entry carrying palogo elements: 10, 13 (publisher twins) palogo_eff0.prm, palogo_sqex, palogo_sqex_eff 11, 14 (developer twins) palogo_eff0.prm + all three logos + all three glows Four entries, and each developer entry declares the complete set of six. So whichever bundle was active across frames 94-211 -- entry 11, entry 14, or both in turn -- it declared the logos and the glows, while the game drew two sprites at a time in disjoint phases. Therefore only a subset of a bundle's elements is drawn at any moment, whatever the bundle-loading story is. The conclusion no longer depends on resolving how many bundles are involved, which is why the texture-base test's failure stopped mattering. So the claim is reinstated -- this time by eliminating the alternative rather than by assuming it away. What worked was not a better capture but asking what the competing hypothesis would REQUIRE on the disc and finding it absent. METHOD: a hypothesis that predicts an artefact can be killed by looking for the artefact, which is often far cheaper than measuring the behaviour. |
||
|
|
2a086051b3 |
re(ui): withdraw the mechanism -- "palette" was an explanation, not a finding
Last iteration I wrote that a bundle is a palette whose elements are
selectively activated. The disjoint glow/logo phases have two
explanations and I asserted one:
1. one bundle, some elements run then others;
2. two bundle-loads shown in sequence (entries 11 and 14 are twins
declaring identical sprites).
The draw log's tex[base=...] looked like it would separate them. It does
not, and the control is in the same table:
publisher splash f1-90 0x11C30000, 0x10000000
glows f94-115 0x11C30000, 0x10000000
logos f116-211 0x11C30000, 0x10000000
The publisher splash is certainly a DIFFERENT bundle from the developer
splash, and it uses the same base. So 0x11C30000 is a reused upload slot,
not a bundle identity, and the test cannot choose between the two
hypotheses.
Survives: a bundle's declared elements are not what gets drawn.
palogo_anima and palogo_gamearts carry byte-identical keyframe times and
in the same run one is drawn 95 frames and the other none -- and
whichever twin was active declares both. The phases are strictly disjoint
(0 overlapping frames in 235).
Withdrawn: the mechanism. The practical consequence is unchanged --
compositing every element of a bundle does not reproduce what the game
shows over time -- but the why is not established and I stated it as
though it were.
What would separate them: a per-draw capture recording the bundle each
draw came from, or a file-IO log showing whether a second RATC entry is
read between frames 115 and 116.
METHOD: a shared resource address does not identify the resource's owner;
and state the mechanism as a separate claim from the observation, or the
weaker one inherits the stronger one's evidence.
|
||
|
|
baed44a9ea |
re(ui): the sequencing survives refutation -- and a bundle is a palette
Two checks on last iteration's "sequential, not simultaneous" reading. First, the phases really are disjoint. If glows and logos ever shared a frame the claim would be wrong. Across all 235 captured frames the count of frames containing both is ZERO, and the switch is a single clean boundary -- f110-115 draw 1280x720 + 262x108 + 525x90, f116 onward 1280x720 + 243x86 + 499x72. Two sprites either side, no transition frame. Second, and larger: a third of the bundle is never drawn. Entry 11 declares three logo/glow pairs and only two appear. palogo_gamearts / _eff 95 / 22 frames palogo_seta / _eff 95 / 22 frames palogo_anima / _eff never palogo_anima declares the SAME keyframe times as palogo_gamearts. Two elements with byte-identical data, 95 frames and 0 frames in one run. Reach: the capture covers frames 1-214, so this is "never in the window". So a bundle is a palette, not a script. Its elements say what to draw and for how long; which of them run, and when each starts, is decided outside the placement data. That is the same conclusion the boot-order work reached from the other end -- the driver is code, not data -- now with a per-element measurement behind it. For the port, concretely: compositing every element of a bundle does not reproduce what the game shows over time. It is the right thing for a static screen that settles, and it is not a timeline. METHOD: two elements with identical data and different outcomes is the strongest possible evidence that the decision is elsewhere. |
||
|
|
7fb6bdfad8 |
re(ui): a group's duration is in the data, its start time is not
Tested whether the splash timeline, played, reproduces the capture -- the last gap in the animation model. Half of it does. Durations match. At 2 units/frame under the shifted reading, from the 235-frame draw capture of the developer splash: glows drawn f94-115 (22 frames = 44 units) declared ~0..45 = 45 97.8% logos drawn f116-211 (96 frames = 192 units) declared 15..210 = 195 98.5% Each element is on screen for its declared span to within 2%. Starts do not. Every glow declares the same times 15,30,45 and every logo the same 15,30,190,194,206,210, so on one clock they would overlap almost entirely -- and they do not overlap at all. The glows run 94-115 and the logos 116-211, strictly sequential, the logos starting the frame after the glows end. Fitting one origin needs f0 ~ 93.5 for gamearts_eff and ~103 for gamearts, about 19 units apart, and aligning one throws the other off by ~9 frames at both ends. The obvious candidate is refuted. parse_placements reads each group header as (element index, keyframe count) plus one undecoded LEAD-IN WORD -- exactly where a per-group start offset would live. It is 0x00000000 for all seven elements, glows and logos alike. Reach: not the keyframe times (identical within each family), not that word (zero), not declaration order (which interleaves logos and glows where the observed sequencing groups them), not the RATC child order. What remains is that the sequencing is code-driven, which agrees with what the boot-order work concluded independently. For the port: a group says how long an element animates and what it does, not when it starts relative to its neighbours. The observed order on the developer splash -- both glows, then both logos -- is measured for one screen, not a decoded rule, so the sequencing has to be authored. METHOD: when a model reproduces durations but not positions, the missing piece is an origin, not a rate. |
||
|
|
1f2b469f7e |
re(ui): a static composite is only meaningful for a screen that settles
The model's sharpest prediction, tested with its control. The draw log says that on the developer splash the _eff glows are drawn on frames 94-115 and the logos on 116-211, so at the moment the reference capture was taken EVERY glow is already finished -- including the two that have plateaus and which rest_plateau therefore renders visible. Suppressing them should help on the splashes and hurt where a screen genuinely settles. publisher splash +0.9604 -> +0.9982 +0.0377 developer splash +0.9659 -> +0.9980 +0.0321 title (control) +0.9500 -> +0.9480 -0.0020 main menu(control) +0.9460 -> +0.8544 -0.0916 EXTRAS (control) +0.9440 -> +0.8370 -0.1070 Both splashes jump to about 0.998; all three persistent screens get worse. The control is what makes this a finding rather than a coincidence: the same edit helps exactly where the model says it should and hurts exactly where it says it should not. So rest_plateau is not over-drawing in general -- it over-draws on TRANSIENT screens. A plateau mid-animation means the element is held at that point in the timeline, not that it is on screen once the screen has settled. Where a screen settles, the held pose IS the settled pose and the rule is measurably right. And that answers the question left open several iterations ago -- what "rest" means for a transient element. It does not mean anything: the splashes never rest. A static composite of them can match a chosen frame, and about 0.998 is what these captures' frame is worth, but the format does not answer a question the screen never poses. For the port: play the timeline for the two splashes, which the settled keyframe timing now supports, and composite statically for title, main menu and EXTRAS. METHOD: an edit that improves one set of cases is only interesting once you have shown it damages the cases where it should. |
||
|
|
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. |
||
|
|
4012d5b555 |
re(ui): why rest_plateau is right -- and last is right only for a transient
The shifted keyframe-time reading looked like it implied something simple: the final pose is reached at a definite time and nothing follows, so rest should just be the last keyframe and the plateau heuristic could go. Tested by applying it to EVERY element: title +0.9500 -> +0.6819 -0.2681 main menu +0.9460 -> +0.6416 -0.3044 EXTRAS +0.9440 -> +0.5745 -0.3695 publisher splash +0.9600 -> blank (zero variance, corr undefined) developer splash +0.9643 -> blank Refuted, and the failure supplies the model. A group is entry -> hold -> exit, and the exit is the screen's DISMISSAL. While a screen is displayed it has not reached its last keyframe; it is sitting at the hold. So rest_plateau is the correct primary rule, and the last keyframe is the post-exit state -- correct only once the screen is gone, which is why applying it everywhere blanks the splashes. This does not contradict the shifted reading. That reading says when each pose is reached; it says nothing about the group being played to completion while the screen is still up. The step between them was mine. And it explains why last wins for the two plateau-less elements: an element with no hold is a transient, it flashes and is over, and at any settled moment it is gone -- which is its last keyframe. The draw capture says the same independently: on the developer splash the _eff glows draw on frames 94-115 and the logos on 116-211, so the glows are already finished when the logos are up. Three independent observables -- animation timing, static composites, and the per-frame draw log -- now agree on one rule: plateau where there is one, last keyframe where there is not. METHOD: a blank render is a NaN correlation, not a low score, and that NaN was the strongest form of the result; and when a model predicts something the measurement refuses, suspect the step you supplied between them. |
||
|
|
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.
|
||
|
|
93e9b185ea |
re(ui): the rest fallback fires on 2 elements, and "last keyframe" wins there
Scored candidate rest-pose rules by rendering and correlating instead of
arguing, and both results correct something I had published.
First, the exposure. The guessing fallback is reached only by an element
that is plateau-less AND multi-keyframe -- a single-keyframe element
short-circuits at `match len { 1 => first }`. Per screen:
title (4) 24 elements 2 plateau-less 0 reach the fallback
main menu (5) 16 5 0
EXTRAS (6) 18 5 0
publisher splash (10) 3 2 1
developer splash (11) 7 2 1
So on the three screens the port cares most about, rest() never guesses.
That is why three different rules render builds 4/5/6 to identical
correlations -- the code is unreachable there, which I nearly read as
"the choice does not matter".
Second, where it does fire, the last keyframe is markedly better:
publisher splash dwell +0.9600 last +0.9982 maxalpha +0.9600
developer splash dwell +0.9643 last +0.9758 maxalpha +0.9643
That refutes my own earlier refutation. I had killed the last-keyframe
rule by arguing it makes palogo_anima_eff invisible while its two
siblings stay lit, which looked like an artefact. The capture says
otherwise: making it invisible is what improves the match. The sibling
symmetry was my expectation, not evidence.
Caveat kept in front: both captures are single frames of a transient
animation, so this fixes which pose matches THOSE frames, not which is
canonically at rest. Default unchanged -- better on both screens where it
fires and identical on the other three, but it would move 2 305 elements
disc-wide on two measurements. Reachable via SYLPHEED_REST_RULE=last.
Also confirmed: all 195 zero-scale rest poses are inside the corrected
2 305 ambiguous population; none is a single-keyframe element.
METHOD: score a rule where it can differ, or you measure nothing; and an
argument from symmetry is a prediction, not a refutation.
|
||
|
|
0265da31a1 |
re(ui): refute my own fix for rest(), and correct the defect rate by 65%
Two corrections from one experiment.
A keyframe group is entry -> hold -> exit, and the exit ends invisible:
on the five port screens the final keyframe is invisible for 21/24
(title), 8/16 (main menu), 12/18 (EXTRAS), 2/3 and 6/7 (splashes). So the
screen as seen is the HOLD, which is why rest_plateau is the right
primary rule and why "rest = last keyframe" would empty every screen.
That suggested a fix: an element with no hold has no representative pose,
so draw nothing rather than guess an endpoint. Tested through compose's
visible mask and correlated against the live captures:
title +0.9500 -> +0.6839 -0.2661
main menu +0.9460 -> +0.9037 -0.0423
EXTRAS +0.9440 -> +0.9094 -0.0346
Refuted on all three, and the reason invalidates a number I published. An
element with a SINGLE keyframe has no adjacent pair, so the plateau test
marks it plateau-less -- but its one pose is unambiguously its rest.
Suppressing those removes backgrounds and full-screen layers, which is
the title's -0.27.
no plateau (as published) 3 807 (24.57 %)
... single-keyframe 1 502 trivially at rest, not a guess
genuinely ambiguous 2 305 (14.88 %)
So rest() guesses for 2 305 elements, not 3 807 -- the figure I gave the
port overstated the defect by 65%. Corrected in HANDOFF and the page.
METHOD: a predicate over adjacent PAIRS silently misclassifies a
one-element list; and acting on a claim is a better test of it than
re-reading it -- this flaw survived a census, a write-up and a handoff
row, and died the moment the rule was used to change a rendering.
|
||
|
|
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.
|
||
|
|
5f701db588 |
re(ui): kill two candidate meanings for the T8aD 0x02 bit, and fix attribution
The bit at +0x04 was recorded as a real field with its meaning "not
diagnosed", noting ptlogo_back2eff is 0x8830 "despite its name". That note
rested on a size match -- and its size is ambiguous, which is the trap
this corpus already records.
First, a sound attribution. T8aD headers appear in the bundle in RATC
CHILD ORDER, verified on GP_TITLE build 4 against an independent property
-- each header's decoded dimensions versus the dimensions the named child
should have: 18 of 18 match, 0 mismatches. Two of those eighteen share a
size (ptlogo_back2eff and ptlogo_back2eff5, both 1133x280), so a size-keyed
lookup cannot separate them; ordering can. Index 12 is back2eff5 (0x8832,
bit set), index 14 is back2eff (0x8830, bit clear). The documented
counterexample is real and correctly attributed -- now on evidence.
Two candidate meanings tested and refuted:
bit <=> name contains "eff" REFUTED: ptlogo_back2eff is an eff
name with the bit clear. All 10
bit-set sprites are eff names, so
the implication holds one way only.
bit <=> the element is transient REFUTED: pteff03/pteff03a carry the
bit and run to t=250, ramping to
a=255 and holding.
Up close, the exception pair differs in two header words: +0x04
0x8832/0x8830 and +0x08 0x8083/0x8081 -- layer keys 32899 and 32897. They
are NOT duplicates: their alpha summaries agree to one decimal (4.5%
opaque, 86.7% clear, mean 19.3) but a pixel compare gives max abs diff 21.
Two renditions of one image at one size, which is why the summaries were
not trusted.
Still not diagnosed, and said so -- but the search space is two smaller
and the attribution beneath it is now sound.
METHOD: T8aD headers sit in child order, use that not the size; and
identical summary statistics are not identical data.
|
||
|
|
421c61a517 |
re(ui): close the gamma chain from canary's defaults -- the game writes a ramp
Continues the previous iteration, where the game was measured calling VdGetCurrentDisplayGamma at video init. The remaining link -- does it then WRITE the ramp -- is a GPU register operation (XE_GPU_REG_DC_LUT_RW_INDEX in CommandProcessor::WriteRegister), unlogged and invisible to kernel logging. Two facts from the source close it without instrumenting. 1. The swap-path gamma stage is a PURE LUT. apply_gamma_table.xesli is the whole transform: index by input*255, fetch from a 256-entry ramp buffer, output. No sRGB encode, no second transfer function. 2. The table DEFAULTS TO IDENTITY. CommandProcessor::Initialize fills it with value = i * 0x3FF / 0xFF, and its own comment says the linear default is "what games set when starting with the sRGB (return value 1) VdGetCurrentDisplayGamma". An unwritten ramp is a no-op. So the only transform is a LUT, the LUT is identity unless written, the game queries the display gamma at init, and the capture differs from our composite by gamma 1.34-1.49 -- which identity cannot produce. The guest wrote a non-identity ramp. Labelled an inference, with its weak joint named: it assumes our composite reproduces the PRE-RAMP framebuffer, which it does not exactly. What carries it is the shape -- a systematic ~1.4 fitted on flat patches across three screens is not a compositor bug. The obvious alternative, a fixed sRGB stage in the presenter, fits neither direction: an encode (^0.45) brightens and we measured darkening; a decode (^2.2) darkens far more than 1.4. Direct observation remains available and cheap, and needs the emulator only to boot: a GPU trace records gamma ramps as their own command type, or one log line at the DC_LUT register write would settle it outright. Not done. METHOD: a default value is evidence; and name the weak joint of an inference in the same breath as the conclusion. |
||
|
|
c6ce000002 |
re(ui): the game does query the display gamma -- measured, with its control
Last iteration's corrected experiment, run. Boot with --log_mask=12 --log_level=3 (Kernel logging on, Cpu/Gpu off), which changes nothing about the output and so cannot perturb the capture harness the way the gamma-cvar experiment would have. VdGetCurrentDisplayGamma is called once, at video init: d> VdGetSystemCommandBuffer(701CF830, 701CF804) d> VdGetCurrentDisplayGamma(701CE1F8(00000000), 701CE1F0(0)) d> VdSetDisplayMode(40000000) d> VdGetCurrentDisplayInformation(701CF110) The control is in the same log: 359 VdRetrainEDRAM and 358 VdGetSystemCommandBuffer lines, so an absent call would have been visible. Per the export's own comment the returned type is "used in D3D SetGammaRamp/SetPWLGamma" -- the game asks the question a ramp-builder asks, at the moment one would ask it. Still open, and stated: whether it then WRITES the ramp, and whether the measured gamma 1.34-1.49 is that ramp. The write is a GPU register operation (DC_LUT), invisible to kernel logging; a GPU trace records gamma ramps as a command type (TraceWriter::WriteGammaRamp), which is where to look next. Worth its own METHOD line: this had been parked behind "needs the emulator to reach a menu" for several iterations, and it needed the emulator only to BOOT -- video init happens in the first seconds. A blocker that stops one experiment does not stop every experiment in the same area. |
||
|
|
93b0a6b20f |
re(ui): the gamma confound is refuted from source -- canary applies none of its own
I had parked the tone-curve finding behind "this may be the emulator, not the game: canary applies kernel_display_gamma_type = 2 (BT.709) on output", with a planned run setting it to 0 and re-fitting. Reading the source kills both the confound and the experiment. VdGetCurrentDisplayGamma_entry is a kStub GETTER the guest calls (xboxkrnl_video.cc). Its own comment: "Used in D3D SetGammaRamp/ SetPWLGamma to adjust the ramp for the display." The cvar is a value REPORTED TO THE GAME, which then builds its own ramp. Canary's role is downstream: the guest writes DC_LUT, command_processor.cc reads it into gamma_ramp_256_entry_table_, and the swap path applies it via swap_apply_gamma_pipeline_layout with apply_gamma_table.ps / apply_gamma_pwl.ps compiled in. So there is no emulator-side BT.709 post-process to subtract, and any gamma in a captured frame is a ramp the game installed. What is NOT established, and the reach is stated: that this game installs a ramp at all, or that the measured 1.34-1.49 is it. The run logs cannot say -- kernel exports log at Debug and this harness masks Kernel logging (log_mask = 13, per boot_menu.sh's own comment), so their silence is guaranteed regardless of what the game did. The planned experiment was wrong in design: changing the cvar changes what the GUEST is told and therefore which ramp the GAME builds, so it could never isolate a stage that does not exist -- and it perturbs the capture harness, since skip_intro classifies movie-vs-static on an absolute rmse threshold that a brighter frame biases. The right run changes nothing about the output: LOG_MASK=12 LOG_LEVEL=3 and look for the call and the DC_LUT writes. Also for the port: the ramp depends on the display type the game is told, and canary hard-codes TV/BT.709 where hardware uses a console setting. So this is a display profile, not a fixed property of the game. METHOD: read what a cvar does before building an experiment around it; and an absence in a log is only evidence if the log would have shown it. |
||
|
|
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.
|
||
|
|
f7f9b555f6 |
re(ui): the paint-order tie-break is undecodable from the bundle
Q3 was delivered as "decoded: a u16 layer key at +0x0A". The audit last
iteration showed the key does not fully order a screen -- elements
sharing a key are tied, and on the title that tie-break decides two total
occlusions. This searches for what breaks the tie, and closes it as a
negative with reach.
The game paints the five tied ptlogo_back2eff glows in the order
eff1, eff2, eff5, eff3, eff4. Three static structures were searched:
1. The declaration table. Entries 14-18 are byte-identical apart from the
pivot, which is only half the sprite's own size.
2. The T8aD headers. All five carry identical +0x04 (0x8832) and
identical +0x08/+0x0A (32899, the key itself), differing only in
position and tile count. Searched exhaustively -- every offset
0x00-0x7f, u8/u16/u32, ascending and descending:
fields sorting to the MEASURED order: 0
fields sorting to the DECLARATION order (control): 64
The control is the point: 64 fields can be found that reproduce a
known ordering, so the scan finds ordering fields when they exist. It
finds none for the order the game uses.
3. The RATC child order -- a genuinely different permutation on other
screens -- gives eff1..eff5 here, declaration order again.
All three static orderings give eff1..eff5; the game gives eff1,2,5,3,4.
That agrees with ui-screen-runtime's conclusion from the other direction:
the game builds a reordered child list at load time and paints that.
Q3 now reads honestly: the layer key is decoded and orders 4 of the 5
measured bundles exactly; the tie-break within a key is undecodable, and
a consumer must use a measured order or accept declaration index as an
arbitrary stand-in. The port's exposure remains 2 overlapping tied pairs
on EXTRAS.
METHOD: an exhaustive field search needs a positive control, or "found
nothing" is worthless.
|
||
|
|
ba47bdebe8 |
re(ui): measure the paint-order hedge -- exact on 4 of 5, and bound the rest
`compose` claimed the derived paint order "reproduces both measured orders up to ties". That sentence was never measured and was stale by one: there are three measured orders, not two. examples/paint_order_audit.rs checks it. main menu (entries 5, 8) derived == measured 0 inverted pairs developer splash (11, 14) derived == measured 0 inverted pairs title (entry 4) DIFFERS 8, all same-key ties So the claim holds and the exception is entirely ties -- but two of those ties are total occlusions, not near-misses. The tied family is the five ptlogo_back2eff glows (key 32899); back2eff5 is 1133x280 and FULLY CONTAINS back2eff3 (82,824 px^2 = 100% of the smaller) and back2eff4 (152,047 px^2 = 100%). Derived paints it on top of two glows it entirely covers; the game paints it underneath. A tie-break by declaration index can therefore be wrong by a whole layer. The title itself is unaffected -- it has a measured order. The port's actual exposure, per screen: title, main menu and developer splash all use MEASURED orders; the publisher splash is derived but has ZERO ties, so it is fully determined; EXTRAS is derived with 15 tied pairs of which only 2 OVERLAP. Two element pairs on one screen is the whole risk, and that is what HANDOFF now says -- not the raw 15, which would have overstated it 7x. Reach stated: this compares the derived order against orders measured from the game, not an independent derivation, so where no measured order exists only the tie exposure can be checked. Overlap uses pivot*2 as the element size at its resting placement. Stale comment in compose corrected. METHOD: a hedge in a code comment is an unmeasured claim; and count the cases that can bite, not the ones that match the pattern. |
||
|
|
ee73de50be |
re(ui): put a number on the render-vs-capture tone difference
Closes an observation I left dangling last iteration ("the capture is ~4x
darker than the render") and puts a figure on the ❔ INDEX's texture row
already carried: exact gamma/sRGB fidelity untested because a hue
comparison cannot see it.
Geometry first: cross-correlating the main-menu capture against our
render over +/-6 px puts the best alignment at exactly dy=0 dx=0,
correlation 0.9466. Only the tone differs.
Two methods failed before one worked, and both failures are recorded.
Three dark patches gave "4x darker" -- the whole-frame best linear scale
is 0.914, so three patches from one region are not a transfer curve. A
pixel-wise fit over 854,685 pixels then produced a NON-MONOTONIC transfer
(render 96-127 mapping brighter than render 128-159) with mean abs error
10-14 for every candidate model. That is edge misalignment, not a tone
curve: at correlation 0.947 a bright pixel routinely lands on a dark one.
Flat patches fix it -- 16x16 blocks with std < 8 in BOTH images, a
threshold chosen from the counts (0/83/404/1055/1788 at std<3/5/8/12/20):
main menu 404 patches gamma 1.491 err 0.28 (best linear 0.276, 0.34)
EXTRAS 382 patches gamma 1.493 err 0.22 (best linear 0.273, 0.28)
title 506 patches gamma 1.338 err 1.08 (best linear 0.842, 9.02)
Reach, stated because it is narrow: those patches span only render values
~0-60, where gamma and a plain scale are nearly indistinguishable -- the
two menus decide nothing (0.28 vs 0.34, 0.22 vs 0.28) and only the title
separates them. Nothing constrains midtones or highlights.
The held-out control FAILED TO DISCRIMINATE and is reported as such: the
splash's 2918 flat patches are pure black (render 0-4), so every model
scores ~0.00. That is a test with no power, not corroboration.
Confound left open: this compares our composite to what canary DISPLAYS,
and canary applies kernel_display_gamma_type = 2 (BT.709). The exponent
may be its output stage. The discriminating run -- set it to 0, recapture,
refit -- needs one emulator session reaching the main menu and was not
done.
Classified measured, not decoded; HANDOFF says plainly that a port
applying it is authoring.
|
||
|
|
2b4ec20f00 |
re(ui): a full-screen element is dropped on three port screens -- do not "fix" it
Audited what `screen render` silently omits on the port's five screens,
since an element the game draws but we skip is the one defect class the
port agent has actually hit. Everything is accounted for -- kind & 0x4
ghost instances, .prm primitives, loop* animations -- except pteff04.t32
on the title and pteff05.t32 on both menus. Those are kind 0x0, one
keyframe, rest a=255, pivot (640,360): full-screen and opaque.
Cause: the element declares pteff05.t32, but the T8aD behind its `opt `
link is registered under the name 8AX, so build.sprites.get() misses and
compose hits a silent continue. Bytes at 0x0e2035 of GP_TITLE entry 5:
opt ... 70 74 65 66 66 30 35 2e 74 33 32 00 38 41 58 54 38 61 44
p t e f f 0 5 . t 3 2 \0 8 A X T 8 a D
8AX is 1280x720 and present in all six title-family bundles; it is a
sprite in builds 4/5/6 and never an element.
It does not currently show, and that is the useful half. ptbase.t32 is
640x360 drawn at 200% and carries THE SAME ARTWORK: its 2x upscale
differs from 8AX by mean abs diff 2.05 (max 80), and our rendered
background is pixel-identical to 8AX in every patch sampled. So resolving
the name and drawing it in addition would double-draw an opaque
full-screen layer -- invisible as a doubling, which is worse than a
visible bug. Written into HANDOFF as a do-not-do.
The free win, offered and not taken: use 8AX at 1:1 and drop ptbase
instead of upscaling a half-res copy. That is a rendering choice and
ptbase's element carries the keyframes, so it is the port's call.
Not established: which of the two the game actually draws. Both carry the
same art, so pixels cannot separate them; it needs a per-draw capture
recording texture base addresses, since the two differ in size.
|
||
|
|
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.
|
||
|
|
b947fa8e8e |
re(ui): withdraw the render evidence -- rest()'s dwell fallback is unsound
Last iteration I reported a 13.1% render change in GP_TITLE build 7 as
evidence AGAINST the keyframe-time shift, arguing that language twins
should match in brightness. Withdrawn. Localising the diff to an element
shows it is not about the time association at all.
The element is ptlogo_eff3.t32, a transient bloom:
46: (98,42) 100%,100% a=0
61: (108,72) 0%,0% a=0
103: (108,72) 200%,200% a=255 r=80
-: (108,72) 0%,0% a=0 r=150
No two adjacent poses are equal, so there is no plateau, and rest() falls
through to its longest-dwell fallback. The longest gap is 61->103, during
which the sprite grows from nothing to 200% at full alpha and then
collapses. The rule returns whichever end of that movement the indexing
lands on: the invisible frame as decoded, the 200% peak shifted. An
896x389 sprite at 200% is larger than the screen, which accounts for the
entire 13.1% and the entire 4.9-unit luminance gap. I was comparing a
heuristic against itself.
The defect generalises, and structurally. A dwell gap is time spent
interpolating FROM pose k TO pose k+1; neither pose is held during it
unless the two are equal -- which is a plateau, and the plateau path has
already returned by then. So the fallback is guaranteed to be guessing
every time it is reached, under any reading of the times. Every element
with no two adjacent identical poses has a guessed rest pose, in our
renderer and in anything built from it.
Consequences: the case FOR the shift (26x on the hold:fade-out ratio) is
no longer opposed by render evidence -- 10 of 11 builds byte-identical,
the 11th differing only through this heuristic. It is still not adopted,
now because it flips this element to the visibly wrong answer, so the
shift and a decision about plateau-less elements must land together, and
neither half has a capture to verify against.
Default-mode suite green (122 passed, 0 failed across 3 suites so far).
|
||
|
|
f11fde51e5 |
re(ui): rotation is not nested-only, and the pivot-anchored scale is measured
Two corrections and one new confirmation, all from finishing the sweep I left running last iteration. Refuted, by my own sweep, within the hour: "rotation appears to live only in nested .rat leaf records". That held for GP_TITLE, GP_BUNK and GP_CHALLENGE -- the three archives the sweep had reached when I wrote it -- and fails on GP_DIALOG and GP_DEBRIEFING_PILOTLOG, which rotate top-level elements. Scoping the claim made it cheap to withdraw, but the sentence should have waited for the sweep. Those top-level cases are the best evidence on the disc, and they show up in `screen info --geometry` without a hex dump. GP_DIALOG build 0: pceff03/pceff04 ramp r = 90 -> 30 -> 10 -> 3 -> 0 while alpha ramps 0 -> 255 and they slide into place -- a swing-in settling upright. Build 6: pzeff02 ramps 43 -> 61 -> 75 -> 90 while scaling 112% -> 200% and fading to 0 -- a spin-out. Rows committed as reference data. New: the pivot-anchored scale term in blit, kf.x - pivot*(scale-100)/100, was implemented and reasoned about but never measured, because every element previously examined sits at 100% scale where the term is exactly zero. The ptloop pair scale 600% and 800%, where it is worth 450 and 630 px. Formula predicts centre y = 360.0 for both; capture measures 359.1 and 360.0. Top-left anchoring predicts 810 and 990. Horizontally it makes t-from-position agree with t-from-alpha to 0.33 / 0.65 units against ~8 without it. That agreement does NOT prove linear interpolation -- both fields were inverted through the same linear map, so a shared easing curve cancels. It shows position and alpha ride one shared parameter. Recorded as such. The disc-wide sweep is still running; the count is incomplete, the existence is settled. |
||
|
|
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. |
||
|
|
5d02d5cd60 |
re: additive blending refuted, the swoosh is not displaced, and the
residual is smaller than I said Testing the candidate I raised last iteration rather than carrying it. Blending bit-0x02 sprites additively moves every measure the wrong way -- whole-frame mean diff +0.55 to +1.04, swoosh-band mean +1.83 to +3.98, band edge-correlation 0.6971 down to 0.5578. So the bit is real and independent but does not select an additive blend. I reverted the experiment and kept the word as T8adImage::flags, documented and not acted on; the render is byte-identical to before. Second refutation: the swoosh is not displaced. Shifting the band over plus or minus 80 by 8 pixels peaks sharply at zero, 0.7342, falling to 0.22 at 24 px. So the pivot story is dead twice over -- inert at scale 100, and no displacement to explain anyway. And I have restated the residual, because earlier sections overstated it. The +16 to +34 band tiles I quoted were measured WITHOUT --primitives. With the dim drawn the band's average is nearly right at +1.83; what is wrong is its structure, tiles running -38.6 then +33.8 and cancelling. Six candidates eliminated now and none confirmed. One caveat I owe the port agent about the capture I gave them: it is at t=4.0s, roughly 174 keyframe units into a screen whose elements have keyframes out to t=600. I judged "settled" from mean luminance, which cannot see a thin sprite still moving. It is settled for the bulk of the screen and not proven settled for every element -- which is a live alternative explanation for a structural difference in exactly the band the sweeps cross. METHOD: cargo build passing does not mean cargo test compiles. Adding the field built the library in 1.48s and broke two test-only struct literals; cargo test failed with exit 101. |
||
|
|
e0d02dce71 |
re: a candidate blend flag -- T8aD header +0x04, bit 0x02
Continuing the swoosh. Last iteration ended with "the next step is finding where a blend mode would be encoded, and I do not know the format carries one". It does carry a candidate. The kind field is not it -- the swoosh sprites are kind 0x0, the same as ordinary ones. But the T8aD header word at +0x04 splits the title's sprites exactly along effect versus normal: pteff01, pteff03a, ptlogo_back2eff1..5 and both ptlogoall_eff are 0x8832, while ptlogo1, ptlogo2, ptlogo_tm, ptbase2, ptlogo_back2 and ptcopyright are 0x8830. One bit, 0x02. Disc-wide it behaves like a real flag rather than an artefact: 19216 sprites, 18 distinct values, bit 0x02 set in 27.1 percent, and it toggles against otherwise identical words -- 0x8830 against 0x8832, 0x0830 against 0x0832, 0x0810 against 0x0812, 0x0030 against 0x0032. Marked as correlation and not decode, because nothing here shows the bit MEANS additive. The one thing that makes it more than a guess is ptlogo_back2eff, which carries 0x8830 despite having eff in its name -- so the split is the field's and not my pattern-matching on names. The test is to blend bit-0x02 sprites additively and re-correlate the title against the plate-free capture. METHOD gets the trap that cost the first attempt: searching for a sprite's name and taking the next T8aD returns the SAME header for every sprite, because the names all live together in the declaration table. It failed its own control at once -- different sprites reporting identical dimensions -- and the fix was to match on width and height instead of on proximity. |