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.
This commit is contained in:
@@ -523,7 +523,7 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
|
||||
println!();
|
||||
println!("geometry — decoded sprite size vs the declared pivot, and every keyframe");
|
||||
println!(
|
||||
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=fade-alpha",
|
||||
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=alpha r=rot°",
|
||||
"#", "sprite", "decoded", "pivot*2", "same"
|
||||
);
|
||||
for el in &b.elements {
|
||||
@@ -543,8 +543,13 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let rot = if f.rotation_deg != 0 {
|
||||
format!(" r={}", f.rotation_deg)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!(
|
||||
"{}: {},{} {}%,{}% a={}",
|
||||
"{}: {},{} {}%,{}% a={}{rot}",
|
||||
f.time.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
||||
f.x,
|
||||
f.y,
|
||||
|
||||
@@ -70,22 +70,38 @@ const DESIGN_H: u32 = 720;
|
||||
/// +36 u32 time
|
||||
/// ```
|
||||
///
|
||||
/// ## `+4` / `+8` / `+12` are **angles**, not zeroes (2026-08-28)
|
||||
/// ## `+12` is the screen-plane ROTATION, in degrees (2026-08-28)
|
||||
///
|
||||
/// These were documented as `0` from a sample that happened to contain none.
|
||||
/// Over **72 287** keyframe blocks disc-wide they are non-zero in **4.81 %**,
|
||||
/// **4.56 %** and **15.82 %** of blocks, and read as **signed** values clustering
|
||||
/// on `180`, `−180`, `90`, `−90`, `120`, `22` — degrees. 🟡 Three of them, so
|
||||
/// plausibly rotation about three axes; **not tied to an observed rotation yet**,
|
||||
/// and this crate does not use them.
|
||||
/// ✅ **Measured against the framebuffer, not against our own renderer.** The
|
||||
/// title's two light sweeps are the nested leaf records `ptloop01.rat` /
|
||||
/// `ptloop02.rat`, and their keyframe blocks read `+12` = `30` and `-45`. A
|
||||
/// `log_ui_draws` capture of the live title submits those two quads rotated by
|
||||
/// **+30.26°** and **-45.28°** — magnitude *and* sign, on two different values.
|
||||
/// Positive is clockwise in screen space (Y down).
|
||||
///
|
||||
/// ⚠️ They do **not** explain the title screen: every element of `GP_TITLE`
|
||||
/// build 4 has all three at zero, while the game demonstrably submits rotated
|
||||
/// quads there. See `docs/re/ui-title-build-map.md`.
|
||||
/// `+4` and `+8` are 🟡 still unexplained: signed, non-zero in ~4.7 % / 4.6 %
|
||||
/// of blocks disc-wide, dominated by `±180` and `±90`. Plausibly rotation about
|
||||
/// the other two axes, but nothing observed turns on them.
|
||||
///
|
||||
/// ⚠️ **`rotation_deg` is decoded but NOT rendered.** [`crate::ui_layout`]'s
|
||||
/// blitter draws axis-aligned quads only, so `screen render` still paints a
|
||||
/// rotated element upright. See `docs/re/ui-title-build-map.md`.
|
||||
///
|
||||
/// ⚠️ The earlier note here — *"every element of `GP_TITLE` build 4 has all
|
||||
/// three at zero"* — was **wrong about reach, not about the bytes**: build 4's
|
||||
/// top-level elements do read zero, but the rotated quads come from its two
|
||||
/// **nested** `.rat` leaf records, which the census never opened.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Keyframe {
|
||||
/// The fade colour, ARGB. Its alpha is what ramps an element in.
|
||||
pub fade: u32,
|
||||
/// Screen-plane rotation in **degrees**, clockwise-positive (`+12`).
|
||||
/// Confirmed against a GPU capture; see the type's docs. Not rendered.
|
||||
pub rotation_deg: i32,
|
||||
/// `+4` / `+8` — signed, meaning unexplained. Carried rather than dropped
|
||||
/// so a consumer can see them instead of assuming they are zero.
|
||||
pub unknown_4: i32,
|
||||
pub unknown_8: i32,
|
||||
/// Scale in percent (100 = 1:1).
|
||||
pub scale_x: u32,
|
||||
pub scale_y: u32,
|
||||
@@ -460,6 +476,9 @@ fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
||||
}
|
||||
group.push(Keyframe {
|
||||
fade: be32(bundle, blk),
|
||||
rotation_deg: be32(bundle, blk + 12) as i32,
|
||||
unknown_4: be32(bundle, blk + 4) as i32,
|
||||
unknown_8: be32(bundle, blk + 8) as i32,
|
||||
scale_x: be32(bundle, blk + 16),
|
||||
scale_y: be32(bundle, blk + 20),
|
||||
tint: be32(bundle, blk + 24),
|
||||
@@ -569,6 +588,9 @@ fn fallback_elements(bundle: &[u8], records: &HashMap<String, (usize, usize)>) -
|
||||
pivot_y: be32(rec, 0x54),
|
||||
keyframes: vec![Keyframe {
|
||||
fade: 0xffff_ffff,
|
||||
rotation_deg: 0,
|
||||
unknown_4: 0,
|
||||
unknown_8: 0,
|
||||
scale_x: 100,
|
||||
scale_y: 100,
|
||||
tint,
|
||||
@@ -1118,6 +1140,9 @@ mod tests {
|
||||
fn kf(x: i32, y: i32, time: u32) -> Keyframe {
|
||||
Keyframe {
|
||||
fade: 0xffff_ffff,
|
||||
rotation_deg: 0,
|
||||
unknown_4: 0,
|
||||
unknown_8: 0,
|
||||
scale_x: 100,
|
||||
scale_y: 100,
|
||||
tint: 0xffff_ffff,
|
||||
@@ -1238,6 +1263,9 @@ mod tests {
|
||||
};
|
||||
let k = Keyframe {
|
||||
fade: 0xffff_ffff,
|
||||
rotation_deg: 0,
|
||||
unknown_4: 0,
|
||||
unknown_8: 0,
|
||||
scale_x: 200,
|
||||
scale_y: 200,
|
||||
tint: 0xffff_ffff,
|
||||
|
||||
Reference in New Issue
Block a user