formats: composite in the MEASURED paint order, and drop the ghost instances
The compositor painted in declaration order, which the draw capture proved wrong — the title's background is declared ninth and paints first. The order the game uses is its runtime child list, and no decoded field reproduces it, so rather than leave the screen composited wrongly this lands the orders that HAVE been read off the running game and falls back to declaration order everywhere else. Two builds are covered: GP_TITLE's title build and the GAME ARTS / SETA / studio anima splash. Keyed by element names, which identify a build across paks and language variants. Rendering it exposed a second defect, and the same capture settles it: the kind = 0x4 elements are motion-trail ghosts, not resting content. The bundle declares three instances of each wordmark; the capture shows exactly ONE quad at each wordmark's position. Drawing them at their resting keyframe put three oversized PROJECT SYLPHEED copies across the composite. They are now skipped. Verified with an artifact, not a green build: the composite is committed (captures/title-composited-measured-order.png) and now reads as the title screen — background, planet, ship, wordmark, TM, copyright, correctly layered. The test is disc-gated and was checked BOTH ways: it passes as landed, and disabling the order table makes it fail. It reads one pak rather than every build on the disc — the first version used the all-builds helper and got the test process OOM-killed running alongside the other three.
This commit is contained in:
@@ -493,6 +493,51 @@ pub fn compose_build(bundle: &[u8], include_focus: bool) -> Option<ComposedScree
|
||||
/// `visible`, when given, selects elements by index — the viewer uses it for
|
||||
/// per-element toggles. Elements are drawn in declaration order, which is the
|
||||
/// screen's own back-to-front order.
|
||||
/// Paint orders **measured from the running game**, not derived from the file.
|
||||
///
|
||||
/// The bundle does not say what order its elements paint in — the game builds a
|
||||
/// second, reordered child list at load time and paints that, and no decoded
|
||||
/// field reproduces it (`docs/re/structures/ui-screen-runtime.md` records the
|
||||
/// search, including everything refuted). Until the ordering is derived, the
|
||||
/// honest thing is to use the orders that HAVE been read off the running game
|
||||
/// and to fall back to declaration order everywhere else — which is what this
|
||||
/// table does. Keying is by element names, because that identifies a build
|
||||
/// across paks and language variants without a pak hash.
|
||||
///
|
||||
/// Each entry is the paint order as declaration indices, first painted first.
|
||||
fn measured_paint_order(build: &UiBuild) -> Option<Vec<usize>> {
|
||||
let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect();
|
||||
// GP_TITLE.pak entry 4 (a60fcb85) — the title screen the game actually runs.
|
||||
const TITLE: [&str; 24] = [
|
||||
"ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32",
|
||||
"ptlogo2.t32", "pteff01.t32", "ptlogo_tm.t32", "pteff00.prm", "ptbase2.t32",
|
||||
"pteff04.t32", "ptloop01.rat", "ptloop02.rat", "pteff02.prm",
|
||||
"ptlogo_back2eff1.t32", "ptlogo_back2eff2.t32", "ptlogo_back2eff3.t32",
|
||||
"ptlogo_back2eff4.t32", "ptlogo_back2eff5.t32", "ptlogo_back2.t32",
|
||||
"ptlogo_back2eff.t32", "ptcopyright.t32", "ptlogoall_eff.t32",
|
||||
"ptlogoall_eff2.t32",
|
||||
];
|
||||
// GP_TITLE.pak entries 11/14 — the GAME ARTS / SETA / studio anima splash.
|
||||
const SPLASH: [&str; 7] = [
|
||||
"palogo_eff0.prm", "palogo_gamearts.t32", "palogo_gamearts_eff.t32",
|
||||
"palogo_seta.t32", "palogo_seta_eff.t32", "palogo_anima.t32",
|
||||
"palogo_anima_eff.t32",
|
||||
];
|
||||
if names == TITLE {
|
||||
// background, the rotating pair, the other full-screen layers, the
|
||||
// back2 glow group, the wordmarks, the copyright, the fade.
|
||||
return Some(vec![
|
||||
9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5,
|
||||
22, 23, 21, 8,
|
||||
]);
|
||||
}
|
||||
if names == SPLASH {
|
||||
// the full-screen .prm, then all three glows, then the three logos.
|
||||
return Some(vec![0, 2, 4, 6, 1, 3, 5]);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn compose(
|
||||
build: &UiBuild,
|
||||
bundle: &[u8],
|
||||
@@ -508,7 +553,14 @@ pub fn compose(
|
||||
}
|
||||
let mut drawn = Vec::new();
|
||||
let mut missing = Vec::new();
|
||||
for el in &build.elements {
|
||||
// Measured paint order when one exists for this build, declaration order
|
||||
// otherwise — see `measured_paint_order`.
|
||||
let order: Vec<usize> =
|
||||
measured_paint_order(build).unwrap_or_else(|| (0..build.elements.len()).collect());
|
||||
for &ei in &order {
|
||||
let Some(el) = build.elements.get(ei) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(v) = visible {
|
||||
if !v.get(el.index).copied().unwrap_or(true) {
|
||||
continue;
|
||||
@@ -517,6 +569,16 @@ pub fn compose(
|
||||
if (el.animated && !opts.include_animated) || (el.focused && !opts.include_focus) {
|
||||
continue;
|
||||
}
|
||||
// `kind = 0x4` elements are the repeated instances of a template — the
|
||||
// motion-trail ghosts a wordmark leaves while it flies in. They are NOT
|
||||
// on screen at rest: the title's draw capture shows exactly ONE quad at
|
||||
// each wordmark's position, though the bundle declares three instances
|
||||
// of each, and their keyframe groups end at alpha 0. Drawing them at
|
||||
// their resting keyframe is what put three oversized copies of
|
||||
// PROJECT SYLPHEED across the composite.
|
||||
if el.kind & 0x4 != 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(kf) = el.rest() else { continue };
|
||||
let Some(sprite) = el.sprite.as_ref() else {
|
||||
continue;
|
||||
|
||||
@@ -216,3 +216,62 @@ fn scaled_elements_are_a_small_and_mostly_undiscriminating_minority() {
|
||||
"expected thousands of placements, got {total}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The measured paint order is applied, and the ghost instances are not drawn.
|
||||
///
|
||||
/// Both facts come from the running game, not from the file:
|
||||
///
|
||||
/// * the paint order is the screen object's reordered child list, read out of
|
||||
/// live guest memory and checked against the draw capture
|
||||
/// (`docs/re/structures/ui-screen-runtime.md`). For the title build that puts
|
||||
/// `ptbase2` (declaration index 9) **first**, which declaration order cannot;
|
||||
/// * the `kind = 0x4` repeat instances are motion-trail ghosts and are absent at
|
||||
/// rest — the capture shows exactly one quad per wordmark though the bundle
|
||||
/// declares three instances of each.
|
||||
#[test]
|
||||
fn title_composites_in_the_measured_order_without_ghosts() {
|
||||
skip_without_disc!(root);
|
||||
// Read ONE pak, not every build on the disc: `builds()` holds them all in
|
||||
// memory at once, and a fourth test doing that in parallel with the other
|
||||
// three got the process OOM-killed.
|
||||
let mut found = false;
|
||||
for bundle in pak_builds(&root, "GP_TITLE.pak") {
|
||||
let Some(build) = ui_layout::parse_build(&bundle) else {
|
||||
continue;
|
||||
};
|
||||
// the title build: 24 elements, and it declares ptbase2 at index 9
|
||||
if build.elements.len() != 24
|
||||
|| build.elements[9].name != "ptbase2.t32"
|
||||
|| build.elements[0].name != "ptlogo1.t32"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
let out = ui_layout::compose(&build, &bundle, ComposeOptions::default(), None);
|
||||
|
||||
// the background is painted FIRST — the whole point of the measured order
|
||||
assert_eq!(
|
||||
out.drawn.first().copied(),
|
||||
Some(9),
|
||||
"expected ptbase2 (element 9) painted first, got {:?}",
|
||||
out.drawn.first()
|
||||
);
|
||||
// ... and before both wordmarks, which declaration order would put first
|
||||
let pos = |i: usize| out.drawn.iter().position(|&d| d == i);
|
||||
assert!(pos(9) < pos(0), "background must precede ptlogo1");
|
||||
assert!(pos(9) < pos(1), "background must precede ptlogo2");
|
||||
// the copyright is late, as captured
|
||||
assert!(pos(21) > pos(0), "copyright must follow the wordmarks");
|
||||
|
||||
// no kind = 0x4 ghost instance is drawn
|
||||
for &d in &out.drawn {
|
||||
assert_eq!(
|
||||
build.elements[d].kind & 0x4,
|
||||
0,
|
||||
"element {d} ({}) is a kind=0x4 ghost and must not be drawn at rest",
|
||||
build.elements[d].name
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(found, "the 24-element GP_TITLE build was not found");
|
||||
}
|
||||
|
||||
BIN
docs/re/captures/title-composited-measured-order.png
Normal file
BIN
docs/re/captures/title-composited-measured-order.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 672 KiB |
Reference in New Issue
Block a user