formats: +0x08 is the animation length - checked against the keyframe times
The rate/duration reading of the bundle header came from the values alone, so it was checked against something the file states independently: the keyframe `time` fields in the placement region. Over the 2313 bundles that have both, the largest keyframe time is <= +0x08 in EVERY one, none exceeds it, and 444 reach it exactly. The refutation attempt is in the same data: a large unrelated constant would bound every time too, but the max_time/+0x08 ratio would then pile up near zero - instead it peaks at exactly 1.0 with 520 bundles. Asserted, including the shape of that histogram, so the bound cannot later be waved through as vacuous. Stated precisely because the units are a separate claim: what is proven is that +0x08 is the animation length in the SAME UNIT as a keyframe's time. That the unit is frames still rests on the values (30/60/120/1200) and on the 16.16 reading of +0x04, which stays amber - the only new evidence for it is that the twelve 30.0 bundles cap at +0x08 = 30 while the 2843 60.0 ones reach 1440, and twelve bundles is not a demonstration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
143
crates/sylpheed-formats/tests/ui_header_time_disc.rs
Normal file
143
crates/sylpheed-formats/tests/ui_header_time_disc.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! Is the bundle header's `+0x08` really an animation **duration in frames**?
|
||||
//!
|
||||
//! The header sweep found `+0x04` takes only `0x3C0000` and `0x1E0000` — exactly
|
||||
//! `60.0` and `30.0` in 16.16 — and `+0x08` takes 30 / 1200 / 120 / 60, which
|
||||
//! *looks* like a frame rate and a length. That reading came from the values
|
||||
//! alone, and this checks it against something the file states independently:
|
||||
//! the **keyframe times** in the placement region.
|
||||
//!
|
||||
//! If `+0x08` is the length of the bundle's animation, the largest keyframe time
|
||||
//! in the bundle should never exceed it, and should reach it on bundles that
|
||||
//! animate all the way through. If instead the times run past it, the reading is
|
||||
//! wrong and the amber has to come off the other way.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
|
||||
|
||||
fn disc_root() -> Option<PathBuf> {
|
||||
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
|
||||
let p = PathBuf::from(p);
|
||||
if p.join("dat").is_dir() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
let default = Path::new(
|
||||
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
|
||||
);
|
||||
if default.join("dat").is_dir() {
|
||||
return Some(default.to_path_buf());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) {
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
for p in &paks {
|
||||
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
for e in arc.entries() {
|
||||
let Ok(bytes) = arc.read(e) else { continue };
|
||||
if ratc::is_ratc(&bytes) {
|
||||
f(&name, &bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn be32(b: &[u8], at: usize) -> u32 {
|
||||
u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_0x08_against_the_keyframe_times() {
|
||||
let Some(root) = disc_root() else {
|
||||
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
||||
return;
|
||||
};
|
||||
|
||||
let (mut animated, mut within, mut exact, mut over) = (0usize, 0, 0, 0);
|
||||
let mut worst: Vec<String> = Vec::new();
|
||||
// How the ratio max_time / header_0x08 is distributed, in tenths.
|
||||
let mut ratio: HashMap<u32, usize> = HashMap::new();
|
||||
// Does the rate word co-vary with anything?
|
||||
let mut by_rate: HashMap<u32, (usize, u32)> = HashMap::new(); // rate -> (bundles, max seen 0x08)
|
||||
|
||||
for_each_build(&root, |pak, bytes| {
|
||||
if bytes.len() < 0x20 {
|
||||
return;
|
||||
}
|
||||
let Some(build) = ui_layout::parse_build(bytes) else {
|
||||
return;
|
||||
};
|
||||
if build.from_fallback {
|
||||
return;
|
||||
}
|
||||
let dur = be32(bytes, 0x08);
|
||||
let rate = be32(bytes, 0x04);
|
||||
let e = by_rate.entry(rate).or_default();
|
||||
e.0 += 1;
|
||||
e.1 = e.1.max(dur);
|
||||
|
||||
let max_time = build
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter())
|
||||
.filter_map(|k| k.time)
|
||||
.max();
|
||||
let Some(max_time) = max_time else { return };
|
||||
if dur == 0 {
|
||||
return;
|
||||
}
|
||||
animated += 1;
|
||||
if max_time <= dur {
|
||||
within += 1;
|
||||
if max_time == dur {
|
||||
exact += 1;
|
||||
}
|
||||
} else {
|
||||
over += 1;
|
||||
if worst.len() < 10 {
|
||||
worst.push(format!("{pak}: max keyframe {max_time} > header {dur}"));
|
||||
}
|
||||
}
|
||||
let r = ((max_time as f64 / dur as f64) * 10.0).round() as u32;
|
||||
*ratio.entry(r.min(30)).or_default() += 1;
|
||||
});
|
||||
|
||||
eprintln!("bundles with keyframe times and a non-zero +0x08: {animated}");
|
||||
eprintln!(" max keyframe time <= +0x08: {within} of which EXACTLY equal: {exact}");
|
||||
eprintln!(" max keyframe time > +0x08: {over}");
|
||||
let mut r: Vec<_> = ratio.iter().collect();
|
||||
r.sort();
|
||||
eprintln!(" ratio max_time/+0x08 (tenths -> bundles): {r:?}");
|
||||
let mut br: Vec<_> = by_rate.iter().collect();
|
||||
br.sort();
|
||||
eprintln!(" +0x04 rate word -> (bundles, largest +0x08 seen): {br:?}");
|
||||
for w in &worst {
|
||||
eprintln!(" over: {w}");
|
||||
}
|
||||
|
||||
assert!(animated > 0, "no animated bundles — the sweep is broken");
|
||||
|
||||
// MEASURED 2026-08-24. +0x08 bounds the keyframe times in EVERY one of the
|
||||
// 2313 bundles that have both, and 444 of them reach it exactly. The
|
||||
// spread-out ratio histogram is what rules out the boring explanation: a
|
||||
// large unrelated constant would bound everything too, but then the ratios
|
||||
// would pile up near zero instead of peaking at 1.0.
|
||||
assert_eq!(over, 0, "a keyframe time runs past the header's +0x08");
|
||||
assert!(exact > 400, "the bound is never attained — it may be unrelated");
|
||||
let near_one = ratio.get(&10).copied().unwrap_or(0);
|
||||
let near_zero = ratio.get(&0).copied().unwrap_or(0);
|
||||
assert!(
|
||||
near_one > near_zero * 4,
|
||||
"the max_time/+0x08 ratio does not peak at 1.0 — the bound may be vacuous"
|
||||
);
|
||||
}
|
||||
@@ -508,7 +508,7 @@ composable bundles with a real declaration table
|
||||
| offset | distinct values | reading |
|
||||
|---|---|---|
|
||||
| `+0x04` | **3** — `0x3C0000` ×2 843, `0x1E0000` ×12, `0x3C0001` ×4 | 🟡 **frame rate in 16.16**: `0x3C0000` is exactly `60.0`, `0x1E0000` exactly `30.0` |
|
||||
| `+0x08` | 22 — 30, 1200, 120, 60, … | 🟡 a **duration in frames** (0.5 s, 20 s, 2 s, 1 s at 60) |
|
||||
| `+0x08` | 22 — 30, 1200, 120, 60, … | ✅ the **animation length**, in the same unit as a keyframe's `time` — see below |
|
||||
| `+0x0c` | 170 | ❔ |
|
||||
| `+0x10` | 83 — `0x9400`, `0x9200`, `0x8212`, … | ❔ flags; bit 15 set on **91 %** of all bundles |
|
||||
| `+0x18` | **2** — `1280` ×2 829 | ✅ **design width** |
|
||||
@@ -542,3 +542,40 @@ Element counts over those 2 859 bundles: **min 1, p25 1, median 2, p75 5, p95 23
|
||||
max 56**, and only **365** carry a full-screen element. The population really is
|
||||
mostly fragments, and the separation is **shape** — or which bundle references
|
||||
which, which the PAK cannot answer directly because its entries are name-hashed.
|
||||
|
||||
|
||||
### ✅ `+0x08` is the animation length — checked against the keyframe times
|
||||
|
||||
The rate/duration reading came from the values alone, so it was checked against
|
||||
something the file states independently: the `time` field of the keyframes in the
|
||||
placement region (`tests/ui_header_time_disc.rs`).
|
||||
|
||||
Over the **2 313** bundles that have both keyframe times and a non-zero `+0x08`:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| largest keyframe time **≤** `+0x08` | **2 313 — every one** |
|
||||
| largest keyframe time **>** `+0x08` | **0** |
|
||||
| reaches it **exactly** | **444** |
|
||||
|
||||
and the ratio `max_time / +0x08` peaks at **1.0** (520 bundles) rather than near
|
||||
zero:
|
||||
|
||||
```
|
||||
tenths: 0.0→92 0.1→128 0.2→201 0.3→198 0.4→92 0.5→110
|
||||
0.6→194 0.7→159 0.8→293 0.9→326 1.0→520
|
||||
```
|
||||
|
||||
That histogram is the refutation attempt, and it is why the bound is not vacuous:
|
||||
a large unrelated constant would also bound every time, but the ratios would then
|
||||
pile up near zero. They peak at exactly 1.
|
||||
|
||||
**Stated precisely, because the units are a separate claim:** what is proven is
|
||||
that `+0x08` is the animation's **length in the same unit as the keyframe `time`
|
||||
field**. That the unit is *frames* still rests on the values themselves
|
||||
(30 / 60 / 120 / 1200) and on the 16.16 reading of `+0x04`.
|
||||
|
||||
🟡 **`+0x04` stays amber.** The one new piece of evidence is weak but points the
|
||||
right way: the twelve bundles whose rate word is `0x1E0000` (30.0) have a largest
|
||||
`+0x08` of **30**, while the 2 843 at `0x3C0000` (60.0) go up to **1 440**. Twelve
|
||||
bundles is not a demonstration, and nothing here measures a wall-clock duration.
|
||||
|
||||
Reference in New Issue
Block a user