diff --git a/authored/audio.json b/authored/audio.json index 47918e52..2dafab21 100644 --- a/authored/audio.json +++ b/authored/audio.json @@ -110,54 +110,66 @@ }, "voice": { "_": [ - "WHICH PRESENTATION of a cutscene's voice to export. CHOSEN, not measured and", - "not decoded -- delete this the day a capture says which one the game plays.", + "πŸ”΄ KNOWN WRONG, HELD DELIBERATELY. Which of a voice region's streams to", + "export. The premise this entry was built on has been REFUTED BY THE RUNNING", + "GAME and the entry is kept, escalated, rather than swapped for another guess.", "", - "A voice region carries THREE PRESENTATIONS OF ONE TAKE (decoded disc-wide by", - "the Decoder: 258 inter-descriptor spans hold one stream, 28 hold three, none", - "holds two). The exporter drops the leading one, which this port measured to", - "be the tail of the others, and must then pick between the two full-length", - "ones. Nothing on the disc ranks them: wEncodeOptions, channel count and", - "channel mask are byte-identical across them.", + "The premise was: a region carries THREE PRESENTATIONS OF ONE TAKE, so the", + "exporter picks one. The Decoder booted with `--xma_param_probe=true` -- the", + "cvar that reports which sub-wave the game decodes -- and the game decodes", + "ALL THREE, CONCURRENTLY, in three separate XMA contexts, with byte sizes", + "matching the three disc payloads exactly (1294336 / 1118208 / 1171456", + "against RIFF size - 60 of 1294396 / 1118268 / 1171516).", "", - "This lives here rather than in the exporter because MISSION section 3 puts a", - "value we chose in authored/ with a why. A selection rule compiled into Rust", - "reads like something the exporter derived, and nobody deletes what nobody", - "can see." + "SO THERE IS NO 'WHICH ONE' TO ANSWER. `presentation` below discards two of", + "three streams the game plays. It is not a preference between rules any more;", + "it is a known-incomplete export.", + "", + "WHY IT IS NOT CHANGED TODAY. Reverting to the 1/n sum is not obviously less", + "wrong: an equal-gain sum of channel pairs is not a downmix -- MISSION", + "section 6 makes exactly that point when it pins an explicit matrix for the", + "movies' 5.1 fold rather than letting ffmpeg default -- and the 6.02 dB the", + "sum cost S00A was a real defect. Swapping one guess for another on a message", + "is what produced this entry twice already.", + "", + "🟑 HYPOTHESIS, NOT A RESULT, and it is the Decoder's: three concurrent stereo", + "streams is six channels, and N stereo streams is how XMA carries", + "multichannel on the 360, so 5.1 would explain the differing byte rates, the", + "near-silent stream and why cues are 1-stream or 3-stream and never 2. AGAINST", + "IT: all three declare ChannelMask = 0x0002 identically, which is odd for", + "distinct channel roles. Do not build on it.", + "", + "WHAT SETTLES IT: a recording of the game's own output over the intro,", + "through the PulseAudio null sink (AUDIO-VERIFICATION section 3). Candidate", + "combinations of the three decoded streams can then be correlated against", + "what the game actually played. Asked 2026-08-29." ], "presentation": "loudest", "presentation_why": [ "`loudest` = the full-length stream whose peak is nearest full scale.", "", - "IT USED TO BE `highest_rate`, on the Decoder's recommendation, and that", - "recommendation was WITHDRAWN as self-contradictory -- it read 'the", - "highest-rate, highest-gain one is chunk 1', and those two criteria select", - "different streams: ADV chunk 1 is 1118268 B at 0.0 dBFS, chunk 2 is", - "1171516 B at -8.3 dBFS.", + "πŸ”΄ READ THE BLOCK ABOVE FIRST. This selects one of three streams the game", + "decodes concurrently, so whatever it selects, two are missing. The", + "paragraphs below are the history of how the value was arrived at, kept", + "because the reasoning is what makes the error checkable -- NOT because the", + "choice is defensible on its own terms any more.", "", - "WHY `loudest` AND NOT `highest_rate`: this is a PER-ASSET CONTENT choice,", - "and that is the whole of its justification. The disc masters its other audio", - "near full scale -- the SE cues decode to +0.18 dBFS -- and S00A's only", - "surviving full-length stream is its louder one at -4.2 dBFS, so `loudest`", - "makes the two cutscenes' dialogue sit at comparable levels instead of 4.4 dB", - "apart. Neither half is strong alone; together they are what there is.", + "It was `highest_rate`, on a recommendation withdrawn as self-contradictory:", + "'the highest-rate, highest-gain one is chunk 1' selects different streams --", + "ADV stream 2 is 1118268 B at 0.0 dBFS, stream 3 is 1171516 B at -8.3.", "", - "⚠️ A STRUCTURAL ARGUMENT WAS OFFERED HERE AND IS WITHDRAWN. It said ADV", - "chunk 1 is mono-in-stereo (channel 2 digitally silent) while chunk 2 is", - "dual-mono (channels identical at -8.318574), therefore the extra bytes", - "encode a duplicated channel rather than fidelity, therefore the rate", - "argument collapses. The CHANNEL MEASUREMENT stands -- it is ADV's, and it is", - "this port's own. THE GENERALISATION DOES NOT. The Decoder tested it", - "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 (S06A: 5661 against", - "16513 B/s). A 37x spread is not a duplicated channel.", + "A structural argument for `loudest` was offered and withdrawn too: ADV", + "stream 2 is mono-in-stereo and stream 3 is dual-mono, so the extra bytes", + "looked like a duplicated channel rather than fidelity. The CHANNEL", + "MEASUREMENT stands and now reads differently -- these are channel pairs, and", + "0.60x with the residual 26.8 dB down is what a correlated pair at a lower", + "level looks like. The GENERALISATION was refuted by census: the stream-3 /", + "stream-2 size ratio over the 28 three-stream cues runs 0.0778 to 2.9163.", "", - "So `highest_rate` was not refuted as a rule; it was simply never argued for,", - "and neither was this. That is why the entry is CHOSEN and why it says so.", - "", - "STILL A CHOICE. One capture of the intro with dialogue audible settles it,", - "and it is the last unforced decision in the voice pipeline." + "⚠️ THE FAILURE MODE HERE IS THAT IT SOUNDS FINE. A single stream decodes to", + "clean audible dialogue, so nothing in the output reveals that two streams", + "are missing. That is why the manifest says it in words on every voice entry", + "rather than leaving it to this file." ] } } diff --git a/crates/sylpheed-export/src/audio.rs b/crates/sylpheed-export/src/audio.rs index 92c0604e..88792ffd 100644 --- a/crates/sylpheed-export/src/audio.rs +++ b/crates/sylpheed-export/src/audio.rs @@ -741,22 +741,21 @@ pub fn export_voice( movie -> token -> sound id -> byte region [{start}, {end}) of the continuous \ voice stream. NOT matched by filename: RT01A's voice lives inside \ VOICE_ADV.slb, so the name is right for this movie by luck and wrong for \ - others. Of {} region chunk(s), exactly ONE is kept -- not summed. A region \ - carries THREE PRESENTATIONS OF ONE TAKE, decoded disc-wide by counting stream \ - starts inside every inter-descriptor span: 258 spans hold one stream and 28 hold \ - three, and nothing holds two. This exporter read these chunks wrongly twice \ - before that landed -- first concatenating them (359 s for a 137 s movie), then \ - summing them as HANDOFF Q10's two stems, which its own measurements refuted: \ - S00A's second full-length chunk is DIGITAL SILENCE and ADV's is 0.60x the first \ - with 26.8 dB of residual. Summing a take with a scaled copy of itself adds ~4 dB \ - and colours it. 🟑 WHICH of the equal-duration survivors is kept is a CHOICE, \ - not a decoded field -- authored/audio.json voice.presentation = {:?}, with its \ - why. Nothing on the disc ranks the presentations: wEncodeOptions, channel count \ - and channel mask are byte-identical across them. One capture of the movie with \ - dialogue audible deletes that entry.{} Folded to mono from the {} of {channels} declared \ - channel(s) that carry signal -- channel 2 of both voice streams is digitally \ - silent, and averaging it in cost 5.94 dB until this was measured rather than \ - read off the declared count.{against}", + others. πŸ”΄ KNOWN INCOMPLETE: of {} region chunk(s) exactly ONE is exported, \ + and the RUNNING GAME DECODES ALL THREE CONCURRENTLY -- measured with Canary's \ + --xma_param_probe, three separate XMA contexts whose byte sizes match the three \ + disc payloads exactly. So this file is MISSING TWO STREAMS, and because a single \ + stream decodes to clean audible dialogue, nothing in the audio reveals that. The \ + earlier reading -- three presentations of one take, pick one -- is REFUTED; the \ + streams are believed to be channels, though 5.1 is a hypothesis and all three \ + declare ChannelMask 0x0002 identically, which argues against distinct roles. \ + Held rather than changed: an equal-gain 1/n sum of channel pairs is not a \ + downmix either (MISSION section 6 pins an explicit matrix for exactly this \ + reason), and summing cost S00A 6.02 dB when one stream was silence. Which stream \ + is kept: authored/audio.json voice.presentation = {:?}. What settles it: a \ + recording of the game's own output over the movie. See docs/port/BLOCKED.md.{} \ + Folded to mono from the {} of {channels} declared channel(s) that carry signal.\ + {against}", riffs.len(), match presentation { Presentation::Loudest => "loudest", @@ -766,15 +765,10 @@ pub fn export_voice( String::new() } else { format!( - " DROPPED, and NOT as junk -- the leading chunk is DECODED to be this \ - movie's OWN dialogue, 17 of 17 regions (docs/re/structures/\ - voice-region-leading-chunk.md; an earlier note here wrongly equated it \ - with BGM_103's third sub-wave, which a disc-wide census showed is a \ - different structure). This port then measured it to be the TAIL of the \ - kept stream -- sliding envelope correlation r=0.998 (ADV) and 0.932 \ - (S00A), the lag placing it flush against that stream's end, against \ - controls of 1.000 self and 0.289 for a different movie -- so dropping it \ - removes a DUPLICATE, not dialogue. Dropped: {}.", + " Also dropped, and this one IS understood: the leading chunk, which this \ + port measured to be the TAIL of the kept stream (sliding envelope \ + correlation r=0.998 ADV / 0.932 S00A, lag flush against that stream's \ + end, controls 1.000 self and 0.289 for a different movie): {}.", dropped.join(", ") ) }, diff --git a/crates/sylpheed-export/src/main.rs b/crates/sylpheed-export/src/main.rs index 746a0d08..5355644b 100644 --- a/crates/sylpheed-export/src/main.rs +++ b/crates/sylpheed-export/src/main.rs @@ -441,12 +441,30 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> { let want = audio_cfg.as_ref().map(|c| c.voice).unwrap_or_default(); match audio::export_voice(&source, out, stem, *len, want)? { Some(a) => { + // πŸ”΄ A TOP-LEVEL WARNING, not just a `why` on the entry. The + // export is known to be missing audio the game plays, and + // the failure sounds like success: one stream decodes to + // clean dialogue, so nobody listening finds out. + if a.sub_waves > 1 { + warnings.push(format!( + "{}: KNOWN INCOMPLETE. This region holds {} streams and the RUNNING \ + GAME DECODES ALL OF THEM CONCURRENTLY (Canary --xma_param_probe: \ + three XMA contexts, byte sizes matching the disc payloads exactly). \ + The export carries ONE. Nothing in the audio reveals this -- a \ + single stream is clean audible dialogue. Held rather than summed \ + because an equal-gain sum of channel pairs is not a downmix and \ + would be a second guess, not a fix. See authored/audio.json voice \ + and docs/port/BLOCKED.md.", + a.file, a.sub_waves + )); + } println!( - " voice {:<8} -> {} ({}, {} region chunk(s))", + " voice {:<8} -> {} ({}, 1 of {} streams{})", a.name, a.file, describe(&a), - a.sub_waves + a.sub_waves, + if a.sub_waves > 1 { " -- KNOWN INCOMPLETE, see warnings" } else { "" } ); audio.push(ManifestAudio::from(a)); } diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 5b26e627..d5d502df 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -96,7 +96,7 @@ HANDOFF. | Milestone | Needs | HANDOFF | State | |---|---|---|---| -| P4/P7 β€” the intro's dialogue | ~~why the intro has no voices~~ | Q9 | βœ… **answered and TAKEN 2026-08-29, and the obvious diagnosis was wrong.** Not a transcode fault: `ADV.wmv` carries music and effects only, and a cutscene's voice is a *separate* continuous XMA stream in `sound.pak` bound by the movie manifest. `audio::export_voice` now resolves it with `media::resolve_movie_voice_region` β€” never by filename, because `RT01A`'s voice lives inside `VOICE_ADV.slb` and a name match is right on exactly the two movies this port would have spot-checked. Region chunks are **concatenated** (one continuous stream), not summed. This is **decoded, nothing authored**. | +| P4/P7 β€” the intro's dialogue | ~~why the intro has no voices~~ | Q9 | βœ… **answered and TAKEN 2026-08-29, and the obvious diagnosis was wrong.** Not a transcode fault: `ADV.wmv` carries music and effects only, and a cutscene's voice is a *separate* continuous XMA stream in `sound.pak` bound by the movie manifest. `audio::export_voice` now resolves it with `media::resolve_movie_voice_region` β€” never by filename, because `RT01A`'s voice lives inside `VOICE_ADV.slb` and a name match is right on exactly the two movies this port would have spot-checked. This is **decoded, nothing authored**. ⚠️ **This row's original text said the region's chunks are "concatenated (one continuous stream), not summed" β€” that was the first of three wrong readings and it is superseded**; see the incomplete-export row below for where it ended up. Left visible rather than silently rewritten, because the sequence of wrong readings is what makes the final one checkable. | | P4/P7 β€” the movie downmix | **is the exporter allowed to ship a matrix MISSION Β§6 did not pin?** | β€” | πŸ”΄ **with the HUMAN, not the Decoder, and now visible for the first time.** Β§6 pins the 5.1 fold as a human decision of 2026-08-29; `video.rs` has shipped that matrix scaled by **0.4142** since P4 β€” same weighting, **7.65 dB quieter** β€” and said so nowhere. Re-measured this iteration with the right instrument (float decode, whole file, count the samples that would clamp, not a peak reading): under the **pinned** matrix `ADV` peaks at **+4.26 dBFS** with **4 406** samples at or over full scale and 1 874 more than 1 dB over, while `S00A` peaks at βˆ’1.34 dBFS and **never clips**. So the pin overloads one movie and the exporter's constant is over-broad for the other. Smallest single scalar under which neither clamps: **0.612**, +3.39 dB on today. **Not changed** β€” the level of a mix is what Β§6 reserves. The export now carries a manifest warning with these numbers. | | ~~P4/P7 β€” a voice region's chunks~~ | ~~what is the leading chunk, and is the second one played?~~ | β€” | βœ… **CLOSED 2026-08-29, decoded disc-wide, and it cost this exporter three wrong readings in one session.** A region carries **three presentations of one take** β€” the Decoder counted stream starts inside every inter-descriptor span: 258 spans hold one, 28 hold three, nothing holds two (`auto/no-disc-and-menu-captures` at `801062c`). So `359 = 84.55 + 137.32 + 137.32`. My concatenation was wrong, my "two stems" reading was wrong (and had already been adopted into the Decoder's page before I tested it β€” withdrawn in both), and summing was wrong a third time because a take plus a 0.60Γ— copy of itself is ~4 dB louder and coloured. The exporter now keeps **one stream** and does no arithmetic on it. The leading chunk is this movie's own dialogue, and I measured it to be the **tail** of the kept stream (r=0.998 / 0.932, controls 1.000 and 0.289), so dropping it removes a duplicate. 🟑 **What is left open is which presentation to keep**: the selector is highest byte rate on the Decoder's recommendation, nothing on the disc says which the game plays, and on `ADV` it picks the quieter of two. Settled by a capture of the movie's dialogue level. ❔ Why the disc stores three at all is unanswered by either agent. | | P4 β€” is an attract movie skippable at all? | **does the real game let β’Ά end `ADV`, or does it play through?** | Q9 | πŸ”΄ **a human play-test reports β’Ά does not skip the port's intro, and the port could not tell which bug that is.** It is *implemented*, not assumed: `authored/flow.json` carries `skippable: true` with a `why` citing Q9 as measured (title at 57 s against a 193 s baseline), and `boot.gd` `_unhandled_input` acts on it. What did not exist was any way to **test** it: `--script` structurally cannot press during a movie, because `_script_settled` waits while `_player != null`. `--skip-at=SECONDS` was added this iteration to close that hole. ⚠️ Two different questions sit behind the one symptom, and only the first is mine: (a) does the synthetic press reach `_unhandled_input` β€” measurable here; (b) does the **game** permit skipping an attract movie β€” `INDEX.md` still marks skippability 🟑 and only a capture settles it. If (b) is no, the port's skip path is deleted rather than debugged. Asked 2026-08-29. | @@ -114,7 +114,7 @@ HANDOFF. | P6 BGM β€” the sub-wave count | **is a music bank's LEADING REGION a stem, or a decoder artefact?** | Q10 | πŸ”΄ **HANDOFF and the decoders disagree, and P6 ships the disagreement.** `media::sound_bank_riffs("BGM_103.slb")` returns **three** sub-waves; HANDOFF Q10's census says a music bank is *"exactly two waves of identical duration (32/32 banks on the disc)"*. The third comes from `slb.rs:380` `to_xma_riffs`, whose hybrid branch emits a leading headerless packet region ahead of the `RIFF` waves β€” and `docs/re/REFUTED.md` already records that region as what makes `BGM_106`–`BGM_109` *"break the two-wave rule"*. Derived at HANDOFF `9ca1eb5`. **The exporter sums all three and writes a manifest warning**, because choosing which sub-wave to drop is a decoding question and MISSION Β§2 forbids this exporter answering one. So the menu currently plays a sum of three things where the census predicts two. What settles it: whether that leading region carries music. Raised with the Decoder 2026-08-29. | | ~~P3 β€” the plate's ONSET~~ | ~~visible 2.13 s after settle, or group starts then?~~ | Q2 | βœ… **resolved 2026-08-29, and the answer is AUTHOR NOTHING.** The port's refutation held and produced a better answer than either option it offered. Correction at `5b0a6e6` on `auto/no-disc-and-menu-captures`: **both builds run on one clock, started together**, and the plate arrives at its own declared `t=238`. Checked against this export rather than taken on trust β€” build 4's visible build-in ends at `t=118` (`pteff01`, `pteff02`, `ptlogoall_eff` finish together), `ptbtn00` reaches alpha 255 at `t=238`, difference **120 units = 2.000 s**, against a measured 2.138 / 2.132 s at an emulator presenting 28.1 fps rather than 30. The 2.13 s constant is **deleted**. | | ~~P3/P5 β€” `settle_time()`~~ | ~~`rest.t` is not when a screen settles, and the port's sequencer uses it~~ | β€” | βœ… **MEASURED 2026-08-29 and the row was HALF WRONG β€” mine.** The Decoder took it on a cold profile with no shader cache (`auto/no-disc-and-menu-captures` at `4bd4779`, `docs/re/boot-settle-times-measured.md`). 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 β€” visible span, `--film` at 4 fps β€” the publisher wordmark runs **4.25 s** against the game's 4.297/4.604/4.370 and the developer logos **3.50 s** against 3.508/3.503/3.366. Dead on. My earlier reading compared the port's *arrival-to-arrival* 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. `rest.t` is still the wrong landmark; its blast radius is `_script_settled` waiting longer than it needs to, which is a slow test and not a wrong frame. `dwell_seconds` stays `null`, now for a measured reason. πŸ”΄ **Do not author an β’Άβ†’menu dwell**: it measures 3.763 s and contains a 1.53 s guest load stall, third independent reproduction. 🟑 Menu build-in 0.531 s and β’·β†’title 0.482 s rest on one run and are not authored; the port is within ~0.1 s of both from the disc. | -| P4/P7 β€” which voice presentation | **which of a region's two full-length streams does the game play?** | β€” | 🟑 **the last unforced decision in the voice pipeline, and it is now unambiguously the port's.** The Decoder's "highest byte rate" was **withdrawn as self-contradictory** β€” its sentence read *"the highest-rate, highest-gain one is chunk 1"*, and those select different streams (`ADV` chunk 1: 1 118 268 B at 0.0 dBFS; chunk 2: 1 171 516 B at βˆ’8.3). Nothing on the disc ranks them: `wEncodeOptions`, channel count and channel mask are byte-identical. Moved to `authored/audio.json` `voice.presentation` per MISSION Β§3, set to **`loudest`** β€” a **per-asset content** choice and nothing more: the disc masters its other audio near full scale (the SE cues decode to +0.18 dBFS), and it puts the two cutscenes' dialogue at comparable levels instead of 4.4 dB apart. ⚠️ **A structural argument for it was offered and is withdrawn.** I said `ADV`'s higher-rate stream is dual-mono where the louder is mono-in-stereo, so its extra bytes encode a duplicated channel rather than fidelity. The **channel measurement stands** β€” it is `ADV`'s and it is mine β€” but the Decoder tested the *inference* disc-wide over the 28 three-stream cues and the stream-3/stream-2 size ratio runs **min 0.0778, median 1.2565, max 2.9163, sd 0.5057**, only 12 of 28 within 15 % of 1.0. A 37Γ— spread is not a duplicated channel. So `highest_rate` was never *refuted*, it was merely never argued for β€” and neither is `loudest`. That is precisely why the entry is marked CHOSEN. What settles it: **one capture of the intro with dialogue audible.** That did not ride the settle-time boot, which drove the title path and never played the movie with audio. | +| P4/P7 β€” the voice export is INCOMPLETE | **what are the three concurrent streams, and how do they combine?** | β€” | πŸ”΄ **the premise of every earlier row here was refuted by the RUNNING GAME, 2026-08-29.** Canary's `--xma_param_probe` shows the game decoding **all three streams concurrently** in three XMA contexts, byte sizes matching the disc payloads exactly (1 294 336 / 1 118 208 / 1 171 456 against 1 294 396 / 1 118 268 / 1 171 516). So they are **not** three presentations of one take, there is no "which one" to answer, and the export β€” which ships one β€” is **missing two streams the game plays**. ⚠️ **The failure sounds like success**: one stream is clean audible dialogue. Stated as a top-level manifest warning per movie, on the console, and in `authored/audio.json`. **Behaviour deliberately unchanged**: an equal-gain `1/n` sum of channel pairs is not a downmix either (MISSION Β§6 pins an explicit matrix for exactly this reason) and summing cost `S00A` 6.02 dB when one stream was silence β€” swapping one guess for another is what produced this row twice. 🟑 "They are 5.1" is the Decoder's **hypothesis**: three stereo streams is six channels and N stereo streams is how XMA carries multichannel on the 360, but all three declare `ChannelMask = 0x0002` identically, which argues against distinct roles. **What settles it: a recording of the game's own output over `ADV` through the PulseAudio null sink** β€” candidate combinations can then be correlated against what the game played. Asked 2026-08-29, emulator was up. | | ~~P3/P5 β€” the title plate~~ | ~~does the idle title show `PRESS β’Ά`~~ | Q2 | βœ… **answered and TAKEN at this iteration.** `auto/no-disc-and-menu-captures` at `fb536df`, `docs/re/title-plate-delay-measured.md`, traces in `docs/re/data/plate-timing-run{1,2}.tsv`. It is the third case: build 4 alone, then the plate composited over it. ⚠️ The delay is timed from where build 4 **stops animating**, not from where it first appears β€” measured the other way the two runs differ by 0.48 s against 6 ms. `ScreenView` now draws two builds at once, as a second `ScreenView` in the same `SubViewport` rather than a subordinate screen inside one. The onset question above is what is left. | | P3 β€” the plate's PULSE | **does the plate's focus record loop, and with what period?** | Q2 | ❔ **open, and the port's earlier reading of it was wrong.** The port had looked for the pulse in `ptbtn00`'s own group; `5b0a6e6` identifies it as the plate's **focus record** `ptbtn00f` β€” a glow ramping alpha `0x00`β†’`0x50` and back, t=6…105. Measured on the running game at 2.12 / 2.19 / 2.34 / 2.31 s, mean **2.24 s**. 🟑 **The port has not taken it.** Looping that record needs a period, and its group is 105 timed units plus the **authored** 24-unit exit ramp = 129 units = 2.15 s β€” composing an authored constant with a loop assumption to land on a measured number is tuning, not measuring. Separately: the port draws no focus record on `press_start` at all, because the screen has no `buttons` and nothing is focused, so *whether the game always draws it* is its own question. | diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index e961e864..ff6c0bb9 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -2971,3 +2971,162 @@ the same breath. The corpus catches it because someone else runs the census. * **`BIRD_224` is three-stream and is not a movie cue** β€” so the three-stream shape is not exclusive to cutscenes, which narrows how it was described to this port earlier. Neither affects `export_voice`, which only resolves movies. + +## Two rows of the P1 baseline were comparing blank frames and reporting OK + +`docs/port/BLOCKED.md` has carried a πŸ”΄ since P3: *"the loading screen's fade quad +rests OPAQUE BLACK … it will bite whoever first draws a loading screen."* It had +already bitten, in the one place nobody looks β€” the regression harness. + +`build_12` and `build_15` render as **pure black in both renderers**: mean 0, +max 0, on the Godot side and on `sylpheed-cli`'s. The difference between two +blank frames is zero, so `verify-screen` scored them `max 0 over3 0 OK` β€” the +strongest verdict it has. **Two of sixteen rows were comparing nothing against +nothing**, and the committed baseline reads as sixteen passes. + +That is worse than a missing test. A missing test is visible in the count. + +### The cause, isolated by a control rather than by reading + +`build_00` and `build_01` are the *plain* loading variant β€” the same screen minus +three elements. They render: **mean 1.913, max 214.5**. `build_12`/`build_15` add +`pgloading_baseeff`, `pgloading_loop5` and **`pgloading_eff00`**, a 1280Γ—720 +primitive whose `rest` is `0xff000000` β€” opaque black β€” at `t=38`, inside its own +opening black hold (`0xff000000` at 38, `0xff000000` at 48, clear on the untimed +final). It carries `layer_source: "none"`, so paint order puts it **last**, over +everything. + +Same screen, one element different, one renders and one does not. That is the +diagnosis, and it did not require an opinion about `rest`. + +### The rule I was about to write, and the census that killed it + +The obvious reading is that `rest.t = 38` is wrong because it precedes the +element's last timed keyframe at 48 β€” so "`rest.t` before the last timed +keyframe" would flag the pathology. **I ran the census before writing the rule, +and it does not survive: 152 of 212 elements in this export have `rest.t` earlier +than their last timed keyframe.** It is the norm. + +What actually distinguishes this element is its *content*, and the reach of that +is one: + +| screen | full-frame primitive | rest.t | last | rest fade | +|---|---|---|---|---| +| `build_12` / `build_15` | `pgloading_eff00` | 38 | 48 | **`0xff000000`** | +| `extras` / `extras_jp` | `pteff00` | 64 | 74 | `0x00000000` | +| `main_menu` / `main_menu_jp` | `pteff00` | 70 | 80 | `0x00000000` | +| `title` / `title_jp` | `pteff00` | 261 | 269 | `0x00000000` | +| `title` / `title_jp` | `pteff02` | 46 | 236 | `0x40000000` | + +**`pgloading_eff00` is the only element in the whole export whose resting pose is +a fully opaque full-frame quad β€” 1 of 212.** Every other full-frame primitive +rests clear or at 25 %. One instance is not a rule about `rest`, and keying the +renderer on "an opaque full-frame quad at rest is probably wrong" would be a +content heuristic of exactly the kind this port refuses elsewhere. + +### So nothing in the renderer changed, and the harness did + +The screens stay black. Nothing draws a loading screen, and the honest position +is that either `rest` is mis-identified for this one element β€” a decoding +question, asked β€” or the screen really does begin fully black and `--pose=rest` +is simply the wrong thing to photograph it at. + +What changed is that **a blank pair can no longer score.** `verify-screen` now +checks both frames for ink first and reports +`BLANK -- both renderers drew nothing; this row proves nothing`. It is not a +failure β€” the port may legitimately have nothing to draw β€” and `status` is +untouched, so an unrelated `DIFFERS` still fails the run. The corrected baseline: + +``` +build_00/01 max 3 OK press_start(_jp) max 1 OK +title max 6 DIFFERS main_menu(_jp) max 4 DIFFERS +extras(_jp) max 3 OK title_jp max 155 DIFFERS +publisher_logo(_r) max 1 OK developer_logos(_r) max 2 OK +build_12 / build_15 BLANK ← previously OK +``` + +Fourteen rows, not sixteen. No new drift among the fourteen. + +## Refutation attempt β€” the loading-screen variants, and it survived + +The Decoder's `ui-title-build-map.md` says entries 0/1 are the plain loading +variant at 7 elements and 12/15 the dressed one at 10, the three additions being +`pgloading_eff00`, `pgloading_loop5` and `pgloading_baseeff`. Checked against this +export, which addresses by pak entry: + +| entry | elements | +|---|---| +| 0, 1 | 7 β€” `eff01 eff02 line loop1 loop3 loop4 str` | +| 12, 15 | 10 β€” the same seven **plus** `baseeff`, `eff00`, `loop5` | + +**Exact, in both the count and the identity of the three.** The claim survives, +and it paid for itself immediately: the two variants differing by exactly the +black quad is what made `build_00` a control for `build_12` and turned "the +loading screen is black" into "this one element blacks it out". + +## πŸ”΄ The voice export is known incomplete β€” the game decodes all three streams at once + +The Decoder booted with Canary's `--xma_param_probe=true` β€” the cvar whose own +comment exists to say which sub-wave a movie's `.slb` the game decodes β€” and the +answer is that **it does not pick one. It decodes all three, concurrently, in +three separate XMA contexts.** + +| ctx | packets | byte_size | disc payload (RIFF βˆ’ 60) | +|---|---|---|---| +| 0 | 632 | 1 294 336 | 1 294 396 | +| 1 | 546 | 1 118 208 | 1 118 268 | +| 2 | 572 | 1 171 456 | 1 171 516 | + +Three-way, byte-exact. **So "three presentations of one take, pick one" is +refuted by the running game**, and the question I had been arguing about β€” +*which* presentation β€” has no answer, because its premise was wrong. + +### This one was not caught by a census, and could not have been + +The last three claims of mine that overreached were all killed by counting +something. This one survived every count available: the streams really are +equal-duration, one really is silence, one really is 0.60 Γ— another with the +residual 26.8 dB down. Every measurement was right and the frame around them was +wrong, and **no amount of looking harder at the file would have moved it** β€” the +file says `ChannelMask = 0x0002` on all three. It took the running game. + +That is the mission's own sentence arriving in practice: *the Port has no oracle +β€” if it needs to know what the game does, it asks.* I did ask, repeatedly, and +each time for the wrong thing: which stream, rather than whether the premise held. + +### What changed, and what deliberately did not + +**The behaviour is held.** Reverting to the `1/n` sum is not obviously less +wrong: an equal-gain sum of channel pairs is **not** a downmix β€” MISSION Β§6 makes +exactly that point when it pins an explicit matrix for the movies' 5.1 fold +rather than letting ffmpeg default β€” and the sum cost `S00A` 6.02 dB when one +stream was silence. Swapping one guess for another on the strength of a message +is what produced this entry twice already. + +**What changed is that the wrongness is now loud.** ⚠️ *This failure sounds like +success*: a single stream decodes to clean, audible dialogue, so nothing a +listener hears reveals that two streams are missing. So it is stated in three +places a reader cannot miss β€” a top-level `manifest.json` warning per movie, the +console line (`1 of 3 streams -- KNOWN INCOMPLETE`), and the entry's own `why` β€” +and 🟑 became πŸ”΄ in `authored/audio.json` and `BLOCKED.md`. + +🟑 **"They are 5.1" is the Decoder's hypothesis and is not established.** Three +concurrent stereo streams is six channels and N stereo streams is how XMA carries +multichannel on the 360, which would explain the differing byte rates, the +near-silent stream, and why cues are 1-stream or 3-stream and never 2. Against +it: all three declare `ChannelMask = 0x0002` identically, which is odd for +distinct channel roles. Nothing here builds on it. + +**What settles it, and it is asked:** a recording of the game's own output over +`ADV`, through the PulseAudio null sink (`AUDIO-VERIFICATION` Β§3). Candidate +combinations of the three decoded streams can then be correlated against what the +game actually played β€” which turns the channel-role question from a decode into a +fit against an oracle. Twenty seconds over dialogue is enough. + +### The measurements survive; only their meaning moved + +`S00A`'s silent stream and `ADV`'s 0.60 Γ— relationship are untouched and now read +as facts about **channels**: 0.60 Γ— with the residual 26.8 dB down is what a +correlated channel pair at a lower level looks like, and a silent channel is an +unused one. Nothing measured here is retracted. What is retracted is every +sentence that called them *presentations*. diff --git a/tools/port/verify-screen b/tools/port/verify-screen index 03be9d38..5ba9ebfe 100755 --- a/tools/port/verify-screen +++ b/tools/port/verify-screen @@ -133,11 +133,33 @@ print(json.load(open("export/"+f))["source"]["build"])' "$name") -compose difference -composite -colorspace Gray -threshold $((3*65535/255)) \ -format "%[fx:int(mean*w*h)]" info:) - # 3/255 is what integer-truncating compositing in the CLI and float rounding - # in a GPU differ by. Anything above that is a placement, order or colour - # disagreement and needs a reason, not a threshold. + # BOTH FRAMES BLANK IS NOT AGREEMENT, AND THIS SCRIPT USED TO SAY IT WAS. + # + # `build_12` and `build_15` -- the two dressed loading screens -- render as + # pure black in BOTH renderers, mean 0 and max 0, so the difference is 0 and + # the row read `max 0 over3 0 OK`. Two of the sixteen rows in the committed + # baseline were comparing nothing against nothing and reporting the strongest + # verdict this script has. + # + # That is worse than a missing test: it is a test that reports a pass. The + # screens are black because `pgloading_eff00` is a full-frame opaque black + # quad whose `rest.t` (38) sits inside its own opening black hold, and + # `--pose=rest` freezes it there -- see docs/port/DECISIONS.md. Whether that + # is the port's bug or the decoders' reading of `rest` is open; what is not + # open is that a blank pair may not be scored. + # + # So blankness is checked FIRST and reported as its own verdict. It is not a + # failure -- the port may legitimately have nothing to draw -- but it is not a + # pass either, and `status` is left alone so an unrelated screen's DIFFERS is + # still what fails the run. + ink=$(convert "$OUT/$name.godot.png" "$OUT/$name.ref.png" \ + -evaluate-sequence max -colorspace Gray -format "%[fx:maxima*255]" info:) verdict=OK - awk "BEGIN{exit !($max > 3)}" && { verdict=DIFFERS; status=1; } + if awk "BEGIN{exit !($ink <= 0)}"; then + verdict="BLANK -- both renderers drew nothing; this row proves nothing" + else + awk "BEGIN{exit !($max > 3)}" && { verdict=DIFFERS; status=1; } + fi printf '%-17s build %-3s max %-5s mean %-8s over3 %-7s %s\n' \ "$name" "$build" "$max" "${mean:0:6}" "$over" "$verdict" done