//! DVD-Video VM command decoder. //! //! An 8-byte navigation command as found in PGC command tables (pre/post/cell) //! and PCI button info. Decoded per the DVD-Video VM instruction set and //! verified against real discs. //! //! Bit model: the 8 bytes are a big-endian 64-bit word. `byte0` bits 7-5 are the //! command **type**; for type 1, `byte0` bit 4 selects Link (0) vs Jump (1), and //! `byte1` bits 3-0 are the sub-command. Compare predicates live in `byte1` //! bits 6-4 with the operands in bytes 2-5. //! //! This module is pure decode + a register model — no I/O, no English (numeric //! semantics only), matching libfreemkv conventions. The navigation *executor* //! and IFO/PCI parsing build on top of this. /// A decoded navigation instruction. Only the variants freemkv's start-point /// resolver needs are modelled explicitly; everything else is [`Instr::Other`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Instr { Nop, /// Stop executing the current command list (resume cell playback). Break, /// Goto command line within the same list (1-based). Goto { line: u8, }, /// Leave the current domain. Exit, /// Jump to a VMG title (1-based TT_SRPT index). JumpTt { ttn: u8, }, /// Jump to a title within the current VTS (1-based VTS title index). JumpVtsTt { ttn: u8, }, /// Jump to a part-of-title (chapter) within a VTS title. JumpVtsPtt { ttn: u8, pttn: u16, }, /// Jump to the First-Play PGC. JumpSsFp, /// Jump to a Video-Manager menu (`menu` = menu id). JumpSsVmgm { menu: u8, }, /// Jump to a Video-Title-Set menu. JumpSsVtsm { vts: u8, ttn: u8, menu: u8, }, /// Jump to a specific VMGM menu PGC. JumpSsVmgmPgc { pgcn: u16, }, /// Call a sub-domain (raw retained; resume handled by the executor). CallSs { sub: u8, }, /// Link to a PGC number within the current domain. LinkPgcn { pgcn: u16, }, /// Link to a part-of-title within the current PGC's title. LinkPttn { pttn: u16, }, /// Link to a program number within the current PGC (1-based). LinkPgn { pgn: u8, }, /// Link to a cell number within the current PGC (1-based). LinkCn { cn: u8, }, /// A link "subset" op (LinkTopCell/NextPG/RSM/…); `sub` is the raw code. LinkSub { sub: u8, }, /// Set a GPRM. `op` is the set-op code (1=mov, 3=add, …); value is immediate /// (`imm`) when `immediate`, else the contents of register `src`. SetGprm { reg: u8, op: u8, immediate: bool, imm: u16, src: u8, }, /// Set a system parameter / unmodelled set — executor may ignore. SetSystem, /// Anything not individually modelled (kept as raw bytes). Other([u8; 8]), } /// A compare predicate carried by a command (`byte1` bits 6-4). `None` = always. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Compare { /// Compare op: 1=&,2===,3=!=,4=>=,5=>,6=<=,7=<. pub op: u8, /// Left register index (GPRM 0-15, SPRM 128+). pub lhs_reg: u8, /// Right side: immediate when `immediate`, else register `rhs_reg`. pub immediate: bool, pub imm: u16, pub rhs_reg: u8, } /// A fully decoded command: its predicate (if any) and the instruction. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Command { pub compare: Option, pub instr: Instr, } // Command types — `byte0` bits 7-5. const TYPE_SPECIAL: u8 = 0; const TYPE_LINK_JUMP: u8 = 1; const TYPE_SET_SYSTEM: u8 = 2; const TYPE_SET_GPRM: u8 = 3; // Special (type 0) sub-commands — `byte1` bits 3-0. const SP_GOTO: u8 = 1; const SP_BREAK: u8 = 2; // Jump/Call (type 1, direct=1) sub-commands. const JP_EXIT: u8 = 1; const JP_JUMP_TT: u8 = 2; const JP_JUMP_VTS_TT: u8 = 3; const JP_JUMP_VTS_PTT: u8 = 5; const JP_JUMP_SS: u8 = 6; const JP_CALL_SS: u8 = 8; // Link (type 1, direct=0) sub-commands. NOTE: sub-op 0 is NOP/no-link and 1 is // the LinkSub form (the DVD-Video VM link instruction). const LK_SUB: u8 = 1; const LK_PGCN: u8 = 4; const LK_PTTN: u8 = 5; const LK_PGN: u8 = 6; const LK_CN: u8 = 7; // JumpSS sub-domain selector — `byte5` bits 7-6. const SS_FP: u8 = 0; const SS_VMGM_MENU: u8 = 1; const SS_VTSM: u8 = 2; // Operand field widths (spec-defined bit counts). const MASK_TTN: u8 = 0x7F; // 7-bit title number const MASK_PGN: u8 = 0x7F; // 7-bit program number const MASK_LINKOP: u8 = 0x1F; // 5-bit link sub-op const MASK_REG: u8 = 0x0F; // 4-bit GPRM index const MASK_MENU: u8 = 0x0F; // 4-bit menu id const MASK_PTTN: u16 = 0x03FF; // 10-bit part-of-title const MASK_PGCN: u16 = 0x7FFF; // 15-bit PGC number #[inline] fn be16(b: &[u8; 8], o: usize) -> u16 { ((b[o] as u16) << 8) | b[o + 1] as u16 } // Compare-operand layouts ("if_version"s) per the DVD-Video VM. The op // nibble is always `byte1` bits 6-4; the immediate flag is `byte1` bit 7. The // operand *offsets* differ by command family. // // v1 (special + link): lhs reg = b[3]; rhs imm = bytes4-5 / rhs reg = b[4]. // v2 (jump + system-set): lhs reg = b[6]; rhs reg = b[7] (registers only). // v3 (set-GPRM): lhs reg = b[2]; rhs imm = bytes6-7 / rhs reg = b[6]. fn if_v1(b: &[u8; 8]) -> Option { let op = (b[1] >> 4) & 7; (op != 0).then(|| Compare { op, lhs_reg: b[3], immediate: b[1] >> 7 != 0, imm: be16(b, 4), rhs_reg: b[4], }) } fn if_v2(b: &[u8; 8]) -> Option { let op = (b[1] >> 4) & 7; (op != 0).then(|| Compare { op, lhs_reg: b[6], immediate: false, imm: 0, rhs_reg: b[7], }) } fn if_v3(b: &[u8; 8]) -> Option { let op = (b[1] >> 4) & 7; (op != 0).then(|| Compare { op, lhs_reg: b[2], immediate: b[1] >> 7 != 0, imm: be16(b, 6), rhs_reg: b[6], }) } /// Decode an 8-byte VM command. pub fn decode(b: &[u8; 8]) -> Command { let typ = b[0] >> 5; let direct = (b[0] >> 4) & 1; let setop = b[0] & 0x0F; let cmd = b[1] & 0x0F; // Compare predicate, with the operand layout for this command family // (the DVD-Video VM command type dispatch). let compare = match (typ, direct) { (TYPE_SPECIAL, _) => if_v1(b), (TYPE_LINK_JUMP, 1) => if_v2(b), // jump (TYPE_LINK_JUMP, 0) => if_v1(b), // link (TYPE_SET_SYSTEM, _) => if_v2(b), (TYPE_SET_GPRM, _) => if_v3(b), _ => None, // 4/5/6 compound — not needed by the resolver }; // JumpSS sub-domain selector lives in byte5 bits 7-6. let ss_sel = b[5] >> 6; let instr = match typ { TYPE_LINK_JUMP if direct == 1 => match cmd { JP_EXIT => Instr::Exit, JP_JUMP_TT => Instr::JumpTt { ttn: b[5] & MASK_TTN, }, JP_JUMP_VTS_TT => Instr::JumpVtsTt { ttn: b[5] & MASK_TTN, }, JP_JUMP_VTS_PTT => Instr::JumpVtsPtt { ttn: b[5] & MASK_TTN, pttn: be16(b, 2) & MASK_PTTN, }, JP_JUMP_SS => match ss_sel { SS_FP => Instr::JumpSsFp, SS_VMGM_MENU => Instr::JumpSsVmgm { menu: b[5] & MASK_MENU, }, SS_VTSM => Instr::JumpSsVtsm { vts: b[4], ttn: b[3], menu: b[5] & MASK_MENU, }, _ => Instr::JumpSsVmgmPgc { pgcn: be16(b, 2) & MASK_PGCN, }, }, JP_CALL_SS => Instr::CallSs { sub: ss_sel }, _ => Instr::Nop, }, TYPE_LINK_JUMP => match cmd { // direct == 0 (link). sub-op 0 = NOP/no-link. LK_SUB => Instr::LinkSub { sub: b[7] & MASK_LINKOP, }, LK_PGCN => Instr::LinkPgcn { pgcn: be16(b, 6) & MASK_PGCN, }, LK_PTTN => Instr::LinkPttn { pttn: be16(b, 6) & MASK_PTTN, }, LK_PGN => Instr::LinkPgn { pgn: b[7] & MASK_PGN, }, LK_CN => Instr::LinkCn { cn: b[7] }, _ => Instr::Nop, }, TYPE_SPECIAL => match cmd { SP_GOTO => Instr::Goto { line: b[7] }, SP_BREAK => Instr::Break, _ => Instr::Nop, }, TYPE_SET_GPRM => Instr::SetGprm { reg: b[3] & MASK_REG, op: setop, immediate: direct != 0, imm: be16(b, 4), src: b[5], }, TYPE_SET_SYSTEM => Instr::SetSystem, _ => Instr::Other(*b), }; Command { compare, instr } } #[cfg(test)] mod tests { use super::*; fn h(s: &str) -> [u8; 8] { let v: Vec = (0..8) .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap()) .collect(); v.try_into().unwrap() } // KATs taken from the real SOTL / Greenland discs (decoded in the PoC). #[test] fn greenland_first_play_is_jumptt_1() { let c = decode(&h("3002000000010000")); assert_eq!(c.instr, Instr::JumpTt { ttn: 1 }); assert!(c.compare.is_none()); } #[test] fn sotl_first_play_is_jumpss_vtsm_root() { // 30 06 ... byte5=0x83 -> sub 2 (VTSM), vts=byte4=1, menu=byte5&0xF=3 (root) let c = decode(&h("3006000101830000")); assert_eq!( c.instr, Instr::JumpSsVtsm { vts: 1, ttn: 1, menu: 3 } ); } #[test] fn sotl_title_dispatch_is_conditional_linkpgn_2() { // 20 a6 ... CmpLink: if GPRM0 == 2 -> LinkPGN 2 (cell 2 = the 5:02 start) let c = decode(&h("20a6000000020002")); assert_eq!(c.instr, Instr::LinkPgn { pgn: 2 }); let cmp = c.compare.expect("conditional"); assert_eq!(cmp.op, 2); // == assert_eq!(cmp.lhs_reg, 0); // GPRM0 assert!(cmp.immediate); assert_eq!(cmp.imm, 2); } #[test] fn sotl_root_button_is_linkpgcn_37() { assert_eq!( decode(&h("2004000000000025")).instr, Instr::LinkPgcn { pgcn: 37 } ); } #[test] fn greenland_scene_button_is_linkpgn() { assert_eq!( decode(&h("2006000000001401")).instr, Instr::LinkPgn { pgn: 1 } ); } #[test] fn jumpvts_ptt_decodes_ttn_and_pttn() { // synthetic: 30 05 | ptt(bytes2-3)=0x0002 | ttn(byte5)=1 let c = decode(&h("3005000200010000")); assert_eq!(c.instr, Instr::JumpVtsPtt { ttn: 1, pttn: 2 }); } #[test] fn setgprm_immediate_mov() { // SOTL First-Play pre[0]: 71 00 | reg=byte3=6 | imm(bytes4-5)=0x03e8 -> g6 = 1000 match decode(&h("7100000603e80000")).instr { Instr::SetGprm { reg, op, immediate, imm, .. } => { assert_eq!(reg, 6); assert_eq!(op, 1); // mov assert!(immediate); assert_eq!(imm, 1000); } other => panic!("expected SetGprm, got {other:?}"), } } // Regression for the link sub-op decode: 0 = NOP, 1 = LinkSub. #[test] fn link_subop_zero_is_nop_one_is_linksub() { assert_eq!(decode(&h("2000000000000000")).instr, Instr::Nop); assert_eq!( decode(&h("2001000000000010")).instr, Instr::LinkSub { sub: 0x10 } ); } // if_version_1 register compare: rhs register is byte4 (not byte5). #[test] fn link_register_compare_rhs_is_byte4() { // 20 26: link, cmp=EQ(2), dircmp=0(register) ; cmd=6 LinkPGN let c = decode(&h("2026000304000002")); assert_eq!(c.instr, Instr::LinkPgn { pgn: 2 }); let cmp = c.compare.expect("conditional"); assert!(!cmp.immediate); assert_eq!(cmp.lhs_reg, 3); assert_eq!(cmp.rhs_reg, 4); } // if_version_2 jump compare: both operands are registers in byte6 / byte7. #[test] fn jump_compare_uses_bytes6_and_7() { // 30 22: jump, cmp=EQ(2) ; cmd=2 JumpTT ttn=byte5=5 let c = decode(&h("3022000000050607")); assert_eq!(c.instr, Instr::JumpTt { ttn: 5 }); let cmp = c.compare.expect("conditional"); assert!(!cmp.immediate); assert_eq!(cmp.lhs_reg, 6); assert_eq!(cmp.rhs_reg, 7); } }