disc: payload-plausibility floor on nav/authoring feature promotion

A nav- or authoring-resolved playlist could promote a video-bearing but
negligible-payload branch above the substantial feature it sits beside:
Spotlight (2015) UHD navigates to a Dolby-Vision seamless-branch playlist
(00001, 0.4 GB across 102 tiny clips) instead of the 91.5 GB single-clip
feature (00002). A player plays the branch; a rip wants the feature.

Require a nav/authoring-promoted title to also carry at least 10% of the
largest video-bearing title's bytes; otherwise selection falls through to
the composite/size ranking, which recovers the real feature. Adds the
Spotlight-shaped regression test.

Verified against the 88-disc hoard: title selection is identical to the
prior release on every disc except the one it fixes (Spider-Man 3 UHD,
00245 wrapper decoy -> 00001 feature).

Also clears three 1.97 clippy lints in the HDMV VM and drops two
reference-implementation filename citations from the bdnav module docs.
This commit is contained in:
Matthew Jackson
2026-08-25 13:44:04 -07:00
parent 6c00090308
commit 8a630c8839
4 changed files with 81 additions and 38 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
//! Parse `/BDMV/index.bdmv` — the BD navigation index (First Play, Top Menu,
//! and the title table), mirroring libbluray `index_parse.c`. This is read as a
//! and the title table), per its documented binary layout. This is read as a
//! documented binary format, never executed: every field is bounds-checked and
//! any malformed input yields `None` (the nav resolver then abstains).
//!
+2 -2
View File
@@ -1,5 +1,5 @@
//! Parse `/BDMV/MovieObject.bdmv` — the HDMV navigation programs, mirroring
//! libbluray `mobj_parse.c`. Read as a documented format, never executed here:
//! Parse `/BDMV/MovieObject.bdmv` — the HDMV navigation programs, per their
//! documented binary layout. Read as a documented format, never executed here:
//! bounds-checked, never panics.
//!
//! Layout: `"MOBJ"` + version(4) + reserved… ; `MovieObjects()` at byte 40 is
+28 -32
View File
@@ -137,10 +137,8 @@ fn run(vm: &mut Vm, mut obj_id: usize, is_feature: &dyn Fn(u16) -> bool) -> Opti
return None;
}
let obj = vm.mobjs.get(obj_id)?;
let c = match obj.cmds.get(pc) {
Some(c) => *c,
None => return None, // ran off the end without a feature PlayPL
};
// Ran off the end without a feature PlayPL → abstain.
let c = *obj.cmds.get(pc)?;
let mut npc = pc + 1;
let dst = if c.op_cnt > 0 {
vm.fetch(c.imm_op1, c.dst)
@@ -223,35 +221,33 @@ fn run(vm: &mut Vm, mut obj_id: usize, is_feature: &dyn Fn(u16) -> bool) -> Opti
}
// SET (sub_grp 0). SETSYSTEM (sub_grp 1) only mutates system PSRs
// whose values the feature path does not branch on — skip it.
2 => {
if c.sub_grp == 0 {
let r: Option<u32> = match c.set_opt {
0x01 => Some(src), // MOVE
0x02 => {
vm.wr(c.dst, src); // SWAP
vm.wr(c.src, dst);
None
}
0x03 => Some(dst.wrapping_add(src)),
0x04 => Some(dst.saturating_sub(src)),
0x05 => Some(dst.wrapping_mul(src)),
0x06 => Some(if src > 0 { dst / src } else { 0xffff_ffff }),
0x07 => Some(if src > 0 { dst % src } else { 0xffff_ffff }),
0x08 => Some(dst), // RND — deterministic stand-in
0x09 => Some(dst & src),
0x0a => Some(dst | src),
0x0b => Some(dst ^ src),
0x0c => Some(dst | (1u32 << (src & 31))),
0x0d => Some(dst & !(1u32 << (src & 31))),
0x0e => Some(dst.wrapping_shl(src & 31)),
0x0f => Some(dst.wrapping_shr(src & 31)),
_ => None,
};
if let Some(r) = r
&& !c.imm_op1
{
vm.wr(c.dst, r);
2 if c.sub_grp == 0 => {
let r: Option<u32> = match c.set_opt {
0x01 => Some(src), // MOVE
0x02 => {
vm.wr(c.dst, src); // SWAP
vm.wr(c.src, dst);
None
}
0x03 => Some(dst.wrapping_add(src)),
0x04 => Some(dst.saturating_sub(src)),
0x05 => Some(dst.wrapping_mul(src)),
0x06 => Some(dst.checked_div(src).unwrap_or(0xffff_ffff)),
0x07 => Some(dst.checked_rem(src).unwrap_or(0xffff_ffff)),
0x08 => Some(dst), // RND — deterministic stand-in
0x09 => Some(dst & src),
0x0a => Some(dst | src),
0x0b => Some(dst ^ src),
0x0c => Some(dst | (1u32 << (src & 31))),
0x0d => Some(dst & !(1u32 << (src & 31))),
0x0e => Some(dst.wrapping_shl(src & 31)),
0x0f => Some(dst.wrapping_shr(src & 31)),
_ => None,
};
if let Some(r) = r
&& !c.imm_op1
{
vm.wr(c.dst, r);
}
}
_ => {}
+50 -3
View File
@@ -2453,6 +2453,23 @@ impl Disc {
.filter(|t| t.has_probable_video())
.map(|t| t.duration_secs)
.fold(0.0_f64, f64::max);
// Largest video-bearing title's byte size, used as a plausibility floor
// below. A nav/authoring signal can legitimately resolve a seamless-branch
// or Dolby-Vision presentation playlist that a player navigates to but
// that carries almost no unique payload (e.g. a 0.4 GB / 102-clip branch
// sitting beside the 91.5 GB single-clip feature). Those are correct for
// playback but wrong as a RIP target, so a promoted title must also hold a
// meaningful fraction of this size; otherwise it falls through to the
// composite/size ranking, which recovers the substantial feature.
let max_video_size = titles
.iter()
.filter(|t| t.has_probable_video())
.map(|t| t.size_bytes)
.max()
.unwrap_or(0);
let carries_feature_payload = |t: &DiscTitle| {
max_video_size == 0 || t.size_bytes as f64 >= 0.10 * max_video_size as f64
};
titles
.iter()
.enumerate()
@@ -2480,11 +2497,16 @@ impl Disc {
let authoring = !composite
&& hint.is_some_and(|h| h.matches(p.playlist_id, &p.playlist))
&& p.has_video()
&& (longest_video_dur <= 0.0 || p.duration_secs >= 0.5 * longest_video_dur);
&& (longest_video_dur <= 0.0 || p.duration_secs >= 0.5 * longest_video_dur)
&& carries_feature_payload(p);
// The nav result is video-gated exactly like the authoring hint,
// so a mis-resolved playlist that happens to be streamless can
// never win (and thus cannot reopen the decoy hole).
let nav = nav_feature == Some(p.playlist_id) && p.has_video();
// never win (and thus cannot reopen the decoy hole). It is also
// payload-gated: a player may navigate to a low-byte branch/DV
// presentation, but that is never the rip target.
let nav = nav_feature == Some(p.playlist_id)
&& p.has_video()
&& carries_feature_payload(p);
TitleRank {
nav,
authoring,
@@ -5167,6 +5189,31 @@ mod tests {
);
}
/// Regression (Spotlight 2015 UHD): the disc navigates to a Dolby-Vision
/// seamless-branch playlist (`00001` = 102 tiny clips, 0.4 GB) that has real
/// video streams but carries almost no payload, sitting beside the 91.5 GB
/// single-clip feature (`00002`). A player plays the branch; a RIP wants the
/// feature. The nav pick is video-gated but must ALSO clear the payload floor,
/// so it falls through to size ranking and the substantial feature wins.
#[test]
fn nav_feature_ignored_when_target_is_a_low_payload_branch() {
let branch = bd_title("00001.mpls", 1, 7500.0, 400_000_000, &["b01", "b02", "b03"]);
let feature = bd_title("00002.mpls", 2, 7680.0, 91_500_000_000, &["00002"]);
let capacity = 100_000_000_000u64;
assert!(
branch.has_probable_video(),
"the DV branch has video streams (only its payload is tiny)"
);
let mut titles = vec![branch, feature];
// Nav resolves the branch (1) like a real player — must be payload-gated.
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, Some(1));
assert_eq!(
titles[0].playlist_id, 2,
"the 91.5 GB feature wins; the 0.4 GB nav-picked branch is not a rip target"
);
}
/// A hint pointing at a STREAMLESS entry must not resurrect the decoy bug:
/// the authoring preference requires the designated title to have real video
/// streams, and a clip-less streamless title fails the `has-video` floor too.