use crate::ifo::NavCommand;
use crate::nav::{
CallSSTarget, CmpOp, JumpSSTarget, LinkSubset, NavInstruction, Operand, Register, SetOp,
};
pub const GPRM_COUNT: usize = 16;
pub const SPRM_COUNT: usize = 24;
pub const SPRM_MENU_LANG: u8 = 0;
pub const SPRM_AUDIO_STREAM: u8 = 1;
pub const SPRM_SUBPICTURE_STREAM: u8 = 2;
pub const SPRM_ANGLE: u8 = 3;
pub const SPRM_TITLE: u8 = 4;
pub const SPRM_VTS_TITLE: u8 = 5;
pub const SPRM_PGCN: u8 = 6;
pub const SPRM_PTT: u8 = 7;
pub const SPRM_HL_BTNN: u8 = 8;
pub const SPRM_NV_TIMER: u8 = 9;
pub const SPRM_NV_PGCN: u8 = 10;
pub const SPRM_AMXMD: u8 = 11;
pub const SPRM_CC_PLT: u8 = 12;
pub const SPRM_PARENTAL_LEVEL: u8 = 13;
pub const SPRM_VIDEO_PREF: u8 = 14;
pub const SPRM_AUDIO_CAPS: u8 = 15;
pub const SPRM_PREF_AUDIO_LANG: u8 = 16;
pub const SPRM_PREF_AUDIO_LANG_EXT: u8 = 17;
pub const SPRM_PREF_SUBP_LANG: u8 = 18;
pub const SPRM_PREF_SUBP_LANG_EXT: u8 = 19;
pub const SPRM_REGION_MASK: u8 = 20;
const SPRM_DEFAULTS: [u16; SPRM_COUNT] = {
let mut v = [0u16; SPRM_COUNT];
v[1] = 15; v[2] = 62; v[3] = 1; v[4] = 1; v[5] = 1; v[7] = 1; v[8] = 1 << 10; v[16] = 0xFFFF; v[17] = 0; v[18] = 0xFFFF; v[19] = 0; v
};
#[derive(Debug, Clone)]
pub struct RegisterFile {
gprm: [u16; GPRM_COUNT],
sprm: [u16; SPRM_COUNT],
counter_mask: u16,
}
impl Default for RegisterFile {
fn default() -> Self {
Self {
gprm: [0; GPRM_COUNT],
sprm: SPRM_DEFAULTS,
counter_mask: 0,
}
}
}
impl RegisterFile {
pub fn new() -> Self {
Self::default()
}
pub fn gprm(&self, index: u8) -> u16 {
if (index as usize) < GPRM_COUNT {
self.gprm[index as usize]
} else {
0
}
}
pub fn set_gprm(&mut self, index: u8, value: u16) {
if (index as usize) < GPRM_COUNT {
self.gprm[index as usize] = value;
}
}
pub fn sprm(&self, index: u8) -> u16 {
if (index as usize) < SPRM_COUNT {
self.sprm[index as usize]
} else {
0
}
}
pub fn set_sprm(&mut self, index: u8, value: u16) {
if (index as usize) < SPRM_COUNT {
self.sprm[index as usize] = value;
}
}
pub fn read(&self, reg: Register) -> u16 {
match reg {
Register::Gprm(i) => self.gprm(i),
Register::Sprm(i) => self.sprm(i),
Register::Invalid(_) => 0,
}
}
pub fn read_operand(&self, op: Operand) -> u16 {
match op {
Operand::Register(r) => self.read(r),
Operand::Immediate(v) => v,
}
}
pub fn set_counter_mode(&mut self, gprm_index: u8, on: bool) {
if (gprm_index as usize) < GPRM_COUNT {
let bit = 1u16 << gprm_index;
if on {
self.counter_mask |= bit;
} else {
self.counter_mask &= !bit;
}
}
}
pub fn counter_mode(&self, gprm_index: u8) -> bool {
if (gprm_index as usize) < GPRM_COUNT {
(self.counter_mask >> gprm_index) & 1 == 1
} else {
false
}
}
pub fn tick_counters(&mut self, delta: u16) {
let mut mask = self.counter_mask;
while mask != 0 {
let bit = mask.trailing_zeros() as usize;
self.gprm[bit] = self.gprm[bit].saturating_add(delta);
mask &= !(1u16 << bit);
}
}
pub fn subpicture_stream(&self) -> SubpictureStreamView {
let raw = self.sprm(SPRM_SUBPICTURE_STREAM);
SubpictureStreamView {
stream: (raw & 0x3F) as u8,
display: (raw >> 6) & 1 == 1,
raw,
}
}
pub fn highlight_button(&self) -> u8 {
let raw = self.sprm(SPRM_HL_BTNN);
let n = (raw >> 10) as u8;
if (1..=36).contains(&n) {
n
} else {
0
}
}
pub fn audio_mix_mode(&self) -> AudioMixMode {
let raw = self.sprm(SPRM_AMXMD);
AudioMixMode {
mix_2_to_front: (raw >> 2) & 1 == 1,
mix_3_to_front: (raw >> 3) & 1 == 1,
mix_4_to_front: (raw >> 4) & 1 == 1,
mix_2_to_rear: (raw >> 10) & 1 == 1,
mix_3_to_rear: (raw >> 11) & 1 == 1,
mix_4_to_rear: (raw >> 12) & 1 == 1,
raw,
}
}
pub fn video_preference(&self) -> VideoPreference {
let raw = self.sprm(SPRM_VIDEO_PREF);
VideoPreference {
aspect: AspectRatio::from_bits(((raw >> 10) & 0b11) as u8),
mode: DisplayMode::from_bits(((raw >> 8) & 0b11) as u8),
raw,
}
}
pub fn audio_capabilities(&self) -> AudioCapabilities {
let raw = self.sprm(SPRM_AUDIO_CAPS);
AudioCapabilities {
sdds_karaoke: (raw >> 2) & 1 == 1,
dts_karaoke: (raw >> 3) & 1 == 1,
mpeg_karaoke: (raw >> 4) & 1 == 1,
dolby_karaoke: (raw >> 6) & 1 == 1,
pcm_karaoke: (raw >> 7) & 1 == 1,
sdds: (raw >> 10) & 1 == 1,
dts: (raw >> 11) & 1 == 1,
mpeg: (raw >> 12) & 1 == 1,
dolby: (raw >> 14) & 1 == 1,
raw,
}
}
pub fn region_allowed(&self, region: u8) -> bool {
if !(1..=8).contains(®ion) {
return false;
}
let raw = self.sprm(SPRM_REGION_MASK);
(raw >> (region - 1)) & 1 == 1
}
pub fn region_mask(&self) -> u8 {
self.sprm(SPRM_REGION_MASK) as u8
}
pub fn menu_language(&self) -> LanguageCode {
LanguageCode::from_raw(self.sprm(SPRM_MENU_LANG))
}
pub fn audio_stream(&self) -> AudioStreamSelector {
AudioStreamSelector::from_raw(self.sprm(SPRM_AUDIO_STREAM))
}
pub fn angle_number(&self) -> Option<u8> {
let v = self.sprm(SPRM_ANGLE) as u8;
if (1..=9).contains(&v) {
Some(v)
} else {
None
}
}
pub fn parental_country(&self) -> LanguageCode {
LanguageCode::from_raw(self.sprm(SPRM_CC_PLT))
}
pub fn parental_level(&self) -> ParentalLevel {
ParentalLevel::from_raw(self.sprm(SPRM_PARENTAL_LEVEL))
}
pub fn preferred_audio_language(&self) -> LanguageCode {
LanguageCode::from_raw(self.sprm(SPRM_PREF_AUDIO_LANG))
}
pub fn preferred_audio_language_ext(&self) -> AudioLanguageExt {
AudioLanguageExt::from_raw(self.sprm(SPRM_PREF_AUDIO_LANG_EXT) as u8)
}
pub fn preferred_subpicture_language(&self) -> LanguageCode {
LanguageCode::from_raw(self.sprm(SPRM_PREF_SUBP_LANG))
}
pub fn preferred_subpicture_language_ext(&self) -> SubpictureLanguageExt {
SubpictureLanguageExt::from_raw(self.sprm(SPRM_PREF_SUBP_LANG_EXT) as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubpictureStreamView {
pub stream: u8,
pub display: bool,
pub raw: u16,
}
impl SubpictureStreamView {
pub fn is_none_sentinel(self) -> bool {
self.stream == 62
}
pub fn is_forced_sentinel(self) -> bool {
self.stream == 63
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AudioMixMode {
pub mix_2_to_front: bool,
pub mix_3_to_front: bool,
pub mix_4_to_front: bool,
pub mix_2_to_rear: bool,
pub mix_3_to_rear: bool,
pub mix_4_to_rear: bool,
pub raw: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VideoPreference {
pub aspect: AspectRatio,
pub mode: DisplayMode,
pub raw: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AspectRatio {
Ar4x3,
NotSpecified,
Reserved,
Ar16x9,
}
impl AspectRatio {
pub fn from_bits(bits: u8) -> Self {
match bits & 0b11 {
0 => Self::Ar4x3,
1 => Self::NotSpecified,
2 => Self::Reserved,
_ => Self::Ar16x9,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayMode {
Normal,
PanScan,
Letterbox,
Reserved,
}
impl DisplayMode {
pub fn from_bits(bits: u8) -> Self {
match bits & 0b11 {
0 => Self::Normal,
1 => Self::PanScan,
2 => Self::Letterbox,
_ => Self::Reserved,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AudioCapabilities {
pub sdds_karaoke: bool,
pub dts_karaoke: bool,
pub mpeg_karaoke: bool,
pub dolby_karaoke: bool,
pub pcm_karaoke: bool,
pub sdds: bool,
pub dts: bool,
pub mpeg: bool,
pub dolby: bool,
pub raw: u16,
}
impl AudioCapabilities {
pub fn cannot_play(self) -> bool {
self.raw == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LanguageCode {
pub raw: u16,
}
impl LanguageCode {
pub const NOT_SPECIFIED: u16 = 0xFFFF;
pub fn from_raw(raw: u16) -> Self {
Self { raw }
}
pub fn is_not_specified(self) -> bool {
self.raw == Self::NOT_SPECIFIED
}
pub fn ascii_bytes(self) -> Option<[u8; 2]> {
if self.is_not_specified() {
return None;
}
let hi = (self.raw >> 8) as u8;
let lo = self.raw as u8;
if hi.is_ascii_alphabetic() && lo.is_ascii_alphabetic() {
Some([hi, lo])
} else {
None
}
}
pub fn as_string(self) -> Option<String> {
let [a, b] = self.ascii_bytes()?;
Some(format!(
"{}{}",
(a.to_ascii_lowercase()) as char,
(b.to_ascii_lowercase()) as char,
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioStreamSelector {
Stream(u8),
None,
Invalid(u16),
}
impl AudioStreamSelector {
pub fn from_raw(raw: u16) -> Self {
match raw {
0..=7 => Self::Stream(raw as u8),
15 => Self::None,
other => Self::Invalid(other),
}
}
pub fn is_stream(self) -> bool {
matches!(self, Self::Stream(_))
}
pub fn is_none_sentinel(self) -> bool {
matches!(self, Self::None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParentalLevel {
Level(u8),
None,
Invalid(u16),
}
impl ParentalLevel {
pub fn from_raw(raw: u16) -> Self {
match raw {
1..=8 => Self::Level(raw as u8),
15 => Self::None,
other => Self::Invalid(other),
}
}
pub fn is_level(self) -> bool {
matches!(self, Self::Level(_))
}
pub fn is_none_sentinel(self) -> bool {
matches!(self, Self::None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioLanguageExt {
NotSpecified,
Normal,
VisuallyImpaired,
DirectorComments,
AlternateDirectorComments,
Reserved(u8),
}
impl AudioLanguageExt {
pub fn from_raw(raw: u8) -> Self {
match raw {
0 => Self::NotSpecified,
1 => Self::Normal,
2 => Self::VisuallyImpaired,
3 => Self::DirectorComments,
4 => Self::AlternateDirectorComments,
other => Self::Reserved(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubpictureLanguageExt {
NotSpecified,
Normal,
Large,
Children,
NormalCaptions,
LargeCaptions,
ChildrensCaptions,
Forced,
DirectorComments,
LargeDirectorComments,
DirectorCommentsForChildren,
Reserved(u8),
}
impl SubpictureLanguageExt {
pub fn from_raw(raw: u8) -> Self {
match raw {
0 => Self::NotSpecified,
1 => Self::Normal,
2 => Self::Large,
3 => Self::Children,
5 => Self::NormalCaptions,
6 => Self::LargeCaptions,
7 => Self::ChildrensCaptions,
9 => Self::Forced,
13 => Self::DirectorComments,
14 => Self::LargeDirectorComments,
15 => Self::DirectorCommentsForChildren,
other => Self::Reserved(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VmAction {
Continue,
Break,
Exit,
Link(LinkAction),
JumpTitle { ttn: u8 },
JumpVtsTitle { ttn: u8 },
JumpVtsPtt { ttn: u8, pttn: u16 },
JumpSs(JumpSSTarget),
CallSs(CallSSTarget),
Resume(ResumePoint),
SetNavTimer { seconds: u16, pgcn: u16 },
NoOpRaw(NavCommand),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkAction {
Subset { subset: LinkSubset, hl_bn: u8 },
Pgcn { pgcn: u16 },
Pttn { pttn: u16, hl_bn: u8 },
Pgn { pgn: u8, hl_bn: u8 },
Cn { cn: u8, hl_bn: u8 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResumePoint {
pub resume_cell: u8,
pub hl_btn: u8,
}
pub const MAX_RSM_DEPTH: usize = 8;
#[derive(Debug, Clone, Default)]
pub struct Vm {
pub regs: RegisterFile,
rsm_stack: Vec<ResumePoint>,
pc: usize,
}
impl Vm {
pub fn new() -> Self {
Self::default()
}
pub fn pc(&self) -> usize {
self.pc
}
pub fn reset_pc(&mut self) {
self.pc = 0;
}
pub fn push_resume(&mut self, frame: ResumePoint) -> bool {
if self.rsm_stack.len() >= MAX_RSM_DEPTH {
false
} else {
self.rsm_stack.push(frame);
true
}
}
pub fn pop_resume(&mut self) -> Option<ResumePoint> {
self.rsm_stack.pop()
}
pub fn resume_depth(&self) -> usize {
self.rsm_stack.len()
}
pub fn evaluate(cmp: CmpOp, lhs: u16, rhs: u16) -> bool {
match cmp {
CmpOp::None => true,
CmpOp::Bc => (lhs & rhs) == 0,
CmpOp::Eq => lhs == rhs,
CmpOp::Ne => lhs != rhs,
CmpOp::Ge => lhs >= rhs,
CmpOp::Gt => lhs > rhs,
CmpOp::Le => lhs <= rhs,
CmpOp::Lt => lhs < rhs,
}
}
pub fn apply_set(op: SetOp, dst: u16, src: u16) -> u16 {
match op {
SetOp::None => dst,
SetOp::Mov => src,
SetOp::Swp => src, SetOp::Add => dst.wrapping_add(src),
SetOp::Sub => dst.wrapping_sub(src),
SetOp::Mul => dst.wrapping_mul(src),
SetOp::Div => dst.checked_div(src).unwrap_or(dst),
SetOp::Mod => dst.checked_rem(src).unwrap_or(dst),
SetOp::Rnd => {
if src == 0 {
dst
} else {
0
}
}
SetOp::And => dst & src,
SetOp::Or => dst | src,
SetOp::Xor => dst ^ src,
SetOp::Invalid(_) => dst,
}
}
pub fn step(&mut self, ins: NavInstruction) -> VmAction {
match ins {
NavInstruction::Nop => VmAction::Continue,
NavInstruction::Goto { line: _ } => {
VmAction::Continue
}
NavInstruction::Break => VmAction::Break,
NavInstruction::SetTmpPml { level, line: _ } => {
self.regs.set_sprm(SPRM_PARENTAL_LEVEL, u16::from(level));
VmAction::Continue
}
NavInstruction::LinkSub { subset, hl_bn } => match subset {
LinkSubset::Rsm => match self.pop_resume() {
Some(mut rp) => {
rp.hl_btn = hl_bn;
VmAction::Resume(rp)
}
None => VmAction::Continue,
},
_ => VmAction::Link(LinkAction::Subset { subset, hl_bn }),
},
NavInstruction::LinkPgcn { pgcn } => VmAction::Link(LinkAction::Pgcn { pgcn }),
NavInstruction::LinkPttn { pttn, hl_bn } => {
VmAction::Link(LinkAction::Pttn { pttn, hl_bn })
}
NavInstruction::LinkPgn { pgn, hl_bn } => {
VmAction::Link(LinkAction::Pgn { pgn, hl_bn })
}
NavInstruction::LinkCn { cn, hl_bn } => VmAction::Link(LinkAction::Cn { cn, hl_bn }),
NavInstruction::Exit => VmAction::Exit,
NavInstruction::JumpTT { ttn } => VmAction::JumpTitle { ttn },
NavInstruction::JumpVtsTt { ttn } => VmAction::JumpVtsTitle { ttn },
NavInstruction::JumpVtsPtt { ttn, pttn } => VmAction::JumpVtsPtt { ttn, pttn },
NavInstruction::JumpSs(t) => VmAction::JumpSs(t),
NavInstruction::CallSs(t) => {
let rsm_cell = match t {
CallSSTarget::FirstPlay { rsm_cell } => rsm_cell,
CallSSTarget::VmgmMenu { rsm_cell, .. } => rsm_cell,
CallSSTarget::VtsmMenu { rsm_cell, .. } => rsm_cell,
CallSSTarget::VmgmPgcn { rsm_cell, .. } => rsm_cell,
};
let _pushed = self.push_resume(ResumePoint {
resume_cell: rsm_cell,
hl_btn: 0,
});
VmAction::CallSs(t)
}
NavInstruction::SetStn {
direct,
af,
audio_src,
sf,
subpic_src,
nf,
angle_src,
} => {
if af {
let v = if direct {
u16::from(audio_src)
} else {
self.regs.gprm(audio_src)
};
self.regs.set_sprm(SPRM_AUDIO_STREAM, v);
}
if sf {
let v = if direct {
u16::from(subpic_src)
} else {
self.regs.gprm(subpic_src)
};
self.regs.set_sprm(SPRM_SUBPICTURE_STREAM, v);
}
if nf {
let v = if direct {
u16::from(angle_src)
} else {
self.regs.gprm(angle_src)
};
self.regs.set_sprm(SPRM_ANGLE, v);
}
VmAction::Continue
}
NavInstruction::SetNvtmr { src, pgcn } => {
let seconds = self.regs.read_operand(src);
self.regs.set_sprm(SPRM_NV_TIMER, seconds);
self.regs.set_sprm(SPRM_NV_PGCN, pgcn);
VmAction::SetNavTimer { seconds, pgcn }
}
NavInstruction::SetGprmMd { src, dst, counter } => {
let v = self.regs.read_operand(src);
if let Register::Gprm(i) = dst {
self.regs.set_gprm(i, v);
self.regs.set_counter_mode(i, counter);
}
VmAction::Continue
}
NavInstruction::SetAmxMd { src } => {
let v = self.regs.read_operand(src);
self.regs.set_sprm(SPRM_AMXMD, v);
VmAction::Continue
}
NavInstruction::SetHlBtnn { src } => {
let v = self.regs.read_operand(src);
self.regs.set_sprm(SPRM_HL_BTNN, v);
VmAction::Continue
}
NavInstruction::Set { op, dst, src } => {
if let Register::Gprm(i) = dst {
let cur = self.regs.gprm(i);
let rhs = self.regs.read_operand(src);
let new = Self::apply_set(op, cur, rhs);
self.regs.set_gprm(i, new);
if matches!(op, SetOp::Swp) {
if let Operand::Register(Register::Gprm(j)) = src {
self.regs.set_gprm(j, cur);
}
}
}
VmAction::Continue
}
NavInstruction::SetCLnk {
set_op,
cmp_op,
scr,
set_src,
cmp_rhs,
hl_bn,
link,
} => self.exec_set_clnk(set_op, cmp_op, scr, set_src, cmp_rhs, hl_bn, link),
NavInstruction::CSetCLnk {
set_op,
cmp_op,
sr1,
set_src,
cmp_lhs,
cmp_rhs,
hl_bn,
link,
} => self.exec_cset_clnk(set_op, cmp_op, sr1, set_src, cmp_lhs, cmp_rhs, hl_bn, link),
NavInstruction::CmpSetLnk {
set_op,
cmp_op,
sr1,
set_src,
cmp_rhs,
hl_bn,
link,
} => self.exec_cmp_set_lnk(set_op, cmp_op, sr1, set_src, cmp_rhs, hl_bn, link),
NavInstruction::Unknown | NavInstruction::Invalid => {
VmAction::NoOpRaw(NavCommand::default())
}
}
}
fn fire_link(&mut self, link: LinkSubset, hl_bn: u8) -> VmAction {
match link {
LinkSubset::Nop => VmAction::Continue,
LinkSubset::Rsm => match self.pop_resume() {
Some(mut rp) => {
rp.hl_btn = hl_bn;
VmAction::Resume(rp)
}
None => VmAction::Continue,
},
LinkSubset::Invalid(_) => VmAction::Continue,
_ => VmAction::Link(LinkAction::Subset {
subset: link,
hl_bn,
}),
}
}
fn apply_set_to_register(&mut self, op: SetOp, dst: Register, src: Operand) {
let Register::Gprm(i) = dst else {
return;
};
let cur = self.regs.gprm(i);
let rhs = self.regs.read_operand(src);
let new = Self::apply_set(op, cur, rhs);
self.regs.set_gprm(i, new);
if matches!(op, SetOp::Swp) {
if let Operand::Register(Register::Gprm(j)) = src {
self.regs.set_gprm(j, cur);
}
}
}
#[allow(clippy::too_many_arguments)]
fn exec_set_clnk(
&mut self,
set_op: SetOp,
cmp_op: CmpOp,
scr: Register,
set_src: Operand,
cmp_rhs: Operand,
hl_bn: u8,
link: LinkSubset,
) -> VmAction {
if !matches!(set_op, SetOp::None | SetOp::Invalid(_)) {
self.apply_set_to_register(set_op, scr, set_src);
}
let lhs = self.regs.read(scr);
let rhs = self.regs.read_operand(cmp_rhs);
if Self::evaluate(cmp_op, lhs, rhs) {
self.fire_link(link, hl_bn)
} else {
VmAction::Continue
}
}
#[allow(clippy::too_many_arguments)]
fn exec_cset_clnk(
&mut self,
set_op: SetOp,
cmp_op: CmpOp,
sr1: Register,
set_src: Operand,
cmp_lhs: Register,
cmp_rhs: Operand,
hl_bn: u8,
link: LinkSubset,
) -> VmAction {
let lhs = self.regs.read(cmp_lhs);
let rhs = self.regs.read_operand(cmp_rhs);
if !Self::evaluate(cmp_op, lhs, rhs) {
return VmAction::Continue;
}
if !matches!(set_op, SetOp::None | SetOp::Invalid(_)) {
self.apply_set_to_register(set_op, sr1, set_src);
}
self.fire_link(link, hl_bn)
}
#[allow(clippy::too_many_arguments)]
fn exec_cmp_set_lnk(
&mut self,
set_op: SetOp,
cmp_op: CmpOp,
sr1: Register,
set_src: Operand,
cmp_rhs: Operand,
hl_bn: u8,
link: LinkSubset,
) -> VmAction {
let lhs = self.regs.read(sr1);
let rhs = self.regs.read_operand(cmp_rhs);
if Self::evaluate(cmp_op, lhs, rhs) {
if !matches!(set_op, SetOp::None | SetOp::Invalid(_)) {
self.apply_set_to_register(set_op, sr1, set_src);
}
}
self.fire_link(link, hl_bn)
}
pub fn run_list(&mut self, list: &[NavCommand]) -> (VmAction, usize) {
self.pc = 0;
let budget = list.len().saturating_mul(16).max(256);
let mut spent = 0usize;
while self.pc < list.len() && spent < budget {
spent += 1;
let ins = list[self.pc].decode();
match ins {
NavInstruction::Goto { line } => {
let idx = (line as usize).saturating_sub(1);
if idx >= list.len() {
self.pc = list.len();
} else {
self.pc = idx;
}
continue;
}
other => {
let action = self.step(other);
match action {
VmAction::Continue => {
self.pc += 1;
}
_ => return (action, self.pc),
}
}
}
}
(VmAction::Continue, self.pc)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ifo::NavCommand;
use crate::nav::{CmpOp, JumpSSTarget, LinkSubset, NavInstruction, Operand, Register, SetOp};
#[test]
fn register_file_default_matches_spec_defaults() {
let r = RegisterFile::new();
for i in 0..GPRM_COUNT {
assert_eq!(r.gprm(i as u8), 0);
}
assert_eq!(r.sprm(SPRM_AUDIO_STREAM), 15);
assert_eq!(r.sprm(SPRM_SUBPICTURE_STREAM), 62);
assert_eq!(r.sprm(SPRM_ANGLE), 1);
assert_eq!(r.sprm(SPRM_TITLE), 1);
assert_eq!(r.sprm(SPRM_VTS_TITLE), 1);
assert_eq!(r.sprm(SPRM_PTT), 1);
assert_eq!(r.sprm(SPRM_HL_BTNN), 1 << 10);
assert_eq!(r.sprm(SPRM_NV_TIMER), 0);
assert_eq!(r.sprm(16), 0xFFFF);
assert_eq!(r.sprm(18), 0xFFFF);
}
#[test]
fn register_out_of_range_indexing_returns_zero() {
let r = RegisterFile::new();
assert_eq!(r.gprm(99), 0);
assert_eq!(r.sprm(99), 0);
assert_eq!(r.read(Register::Invalid(0x7F)), 0);
}
#[test]
fn register_read_operand_dispatch() {
let mut r = RegisterFile::new();
r.set_gprm(3, 0xCAFE);
r.set_sprm(SPRM_HL_BTNN, 0x0800);
assert_eq!(r.read_operand(Operand::Register(Register::Gprm(3))), 0xCAFE);
assert_eq!(
r.read_operand(Operand::Register(Register::Sprm(SPRM_HL_BTNN))),
0x0800
);
assert_eq!(r.read_operand(Operand::Immediate(0xBEEF)), 0xBEEF);
}
#[test]
fn counter_mode_flag_round_trips() {
let mut r = RegisterFile::new();
assert!(!r.counter_mode(5));
r.set_counter_mode(5, true);
assert!(r.counter_mode(5));
assert!(!r.counter_mode(6));
r.set_counter_mode(5, false);
assert!(!r.counter_mode(5));
}
#[test]
fn tick_counters_advances_only_counter_mode_gprms() {
let mut r = RegisterFile::new();
r.set_gprm(0, 100);
r.set_gprm(1, 100);
r.set_counter_mode(1, true);
r.tick_counters(5);
assert_eq!(r.gprm(0), 100);
assert_eq!(r.gprm(1), 105);
}
#[test]
fn tick_counters_saturates_at_u16_max() {
let mut r = RegisterFile::new();
r.set_gprm(2, u16::MAX - 3);
r.set_counter_mode(2, true);
r.tick_counters(10);
assert_eq!(r.gprm(2), u16::MAX);
}
#[test]
fn evaluate_covers_every_cmp_op() {
assert!(Vm::evaluate(CmpOp::None, 0, 0));
assert!(Vm::evaluate(CmpOp::Bc, 0xF0, 0x0F)); assert!(!Vm::evaluate(CmpOp::Bc, 0xF0, 0x10)); assert!(Vm::evaluate(CmpOp::Eq, 5, 5));
assert!(!Vm::evaluate(CmpOp::Eq, 5, 4));
assert!(Vm::evaluate(CmpOp::Ne, 5, 4));
assert!(!Vm::evaluate(CmpOp::Ne, 5, 5));
assert!(Vm::evaluate(CmpOp::Ge, 5, 5));
assert!(Vm::evaluate(CmpOp::Ge, 6, 5));
assert!(!Vm::evaluate(CmpOp::Ge, 4, 5));
assert!(Vm::evaluate(CmpOp::Gt, 6, 5));
assert!(!Vm::evaluate(CmpOp::Gt, 5, 5));
assert!(Vm::evaluate(CmpOp::Le, 5, 5));
assert!(Vm::evaluate(CmpOp::Le, 4, 5));
assert!(!Vm::evaluate(CmpOp::Le, 6, 5));
assert!(Vm::evaluate(CmpOp::Lt, 4, 5));
assert!(!Vm::evaluate(CmpOp::Lt, 5, 5));
}
#[test]
fn apply_set_covers_named_arithmetic_ops() {
assert_eq!(Vm::apply_set(SetOp::None, 5, 99), 5);
assert_eq!(Vm::apply_set(SetOp::Mov, 5, 99), 99);
assert_eq!(Vm::apply_set(SetOp::Add, 5, 3), 8);
assert_eq!(Vm::apply_set(SetOp::Sub, 5, 3), 2);
assert_eq!(Vm::apply_set(SetOp::Mul, 5, 3), 15);
assert_eq!(Vm::apply_set(SetOp::Div, 14, 3), 4);
assert_eq!(Vm::apply_set(SetOp::Mod, 14, 3), 2);
assert_eq!(Vm::apply_set(SetOp::And, 0xF0F0, 0x0FF0), 0x00F0);
assert_eq!(Vm::apply_set(SetOp::Or, 0xF000, 0x000F), 0xF00F);
assert_eq!(Vm::apply_set(SetOp::Xor, 0xFF00, 0x0FF0), 0xF0F0);
}
#[test]
fn apply_set_handles_zero_divisor_safely() {
assert_eq!(Vm::apply_set(SetOp::Div, 5, 0), 5);
assert_eq!(Vm::apply_set(SetOp::Mod, 5, 0), 5);
assert_eq!(Vm::apply_set(SetOp::Rnd, 5, 0), 5);
}
#[test]
fn apply_set_swp_returns_src_caller_writes_dst_back() {
assert_eq!(Vm::apply_set(SetOp::Swp, 5, 99), 99);
}
#[test]
fn apply_set_arithmetic_overflow_wraps() {
assert_eq!(Vm::apply_set(SetOp::Add, u16::MAX, 1), 0);
assert_eq!(Vm::apply_set(SetOp::Sub, 0, 1), u16::MAX);
assert_eq!(Vm::apply_set(SetOp::Mul, 0x0100, 0x0100), 0); }
#[test]
fn apply_set_invalid_sub_op_is_noop() {
assert_eq!(Vm::apply_set(SetOp::Invalid(0x0C), 5, 99), 5);
}
#[test]
fn step_nop_continues() {
let mut vm = Vm::new();
assert_eq!(vm.step(NavInstruction::Nop), VmAction::Continue);
}
#[test]
fn step_break_and_exit_terminate() {
let mut vm = Vm::new();
assert_eq!(vm.step(NavInstruction::Break), VmAction::Break);
assert_eq!(vm.step(NavInstruction::Exit), VmAction::Exit);
}
#[test]
fn step_set_writes_gprm_via_mov() {
let mut vm = Vm::new();
vm.step(NavInstruction::Set {
op: SetOp::Mov,
dst: Register::Gprm(4),
src: Operand::Immediate(0x1234),
});
assert_eq!(vm.regs.gprm(4), 0x1234);
}
#[test]
fn step_set_arithmetic_chain_through_gprm() {
let mut vm = Vm::new();
vm.regs.set_gprm(0, 10);
vm.step(NavInstruction::Set {
op: SetOp::Add,
dst: Register::Gprm(0),
src: Operand::Immediate(5),
});
assert_eq!(vm.regs.gprm(0), 15);
vm.step(NavInstruction::Set {
op: SetOp::Mul,
dst: Register::Gprm(0),
src: Operand::Register(Register::Gprm(0)),
});
assert_eq!(vm.regs.gprm(0), 225);
}
#[test]
fn step_set_swp_exchanges_two_gprms() {
let mut vm = Vm::new();
vm.regs.set_gprm(1, 0xAAAA);
vm.regs.set_gprm(2, 0x5555);
vm.step(NavInstruction::Set {
op: SetOp::Swp,
dst: Register::Gprm(1),
src: Operand::Register(Register::Gprm(2)),
});
assert_eq!(vm.regs.gprm(1), 0x5555);
assert_eq!(vm.regs.gprm(2), 0xAAAA);
}
#[test]
fn step_setstn_honours_per_flag_application() {
let mut vm = Vm::new();
vm.step(NavInstruction::SetStn {
direct: true,
af: true,
audio_src: 4,
sf: false,
subpic_src: 7,
nf: true,
angle_src: 3,
});
assert_eq!(vm.regs.sprm(SPRM_AUDIO_STREAM), 4);
assert_eq!(vm.regs.sprm(SPRM_SUBPICTURE_STREAM), 62); assert_eq!(vm.regs.sprm(SPRM_ANGLE), 3);
}
#[test]
fn step_setnvtmr_loads_timer_pair_and_surfaces_action() {
let mut vm = Vm::new();
let act = vm.step(NavInstruction::SetNvtmr {
src: Operand::Immediate(120),
pgcn: 42,
});
assert_eq!(
act,
VmAction::SetNavTimer {
seconds: 120,
pgcn: 42,
}
);
assert_eq!(vm.regs.sprm(SPRM_NV_TIMER), 120);
assert_eq!(vm.regs.sprm(SPRM_NV_PGCN), 42);
}
#[test]
fn step_setgprmmd_with_counter_flag_toggles_mode_bit() {
let mut vm = Vm::new();
vm.step(NavInstruction::SetGprmMd {
src: Operand::Immediate(99),
dst: Register::Gprm(7),
counter: true,
});
assert_eq!(vm.regs.gprm(7), 99);
assert!(vm.regs.counter_mode(7));
vm.step(NavInstruction::SetGprmMd {
src: Operand::Immediate(0),
dst: Register::Gprm(7),
counter: false,
});
assert!(!vm.regs.counter_mode(7));
}
#[test]
fn step_sethlbtnn_writes_sprm8() {
let mut vm = Vm::new();
vm.step(NavInstruction::SetHlBtnn {
src: Operand::Immediate(0x0C00),
});
assert_eq!(vm.regs.sprm(SPRM_HL_BTNN), 0x0C00);
}
#[test]
fn step_set_tmp_pml_writes_sprm13() {
let mut vm = Vm::new();
vm.step(NavInstruction::SetTmpPml { level: 7, line: 0 });
assert_eq!(vm.regs.sprm(SPRM_PARENTAL_LEVEL), 7);
}
#[test]
fn step_link_subset_surfaces_link_action() {
let mut vm = Vm::new();
let a = vm.step(NavInstruction::LinkSub {
subset: LinkSubset::LinkNextPG,
hl_bn: 3,
});
assert_eq!(
a,
VmAction::Link(LinkAction::Subset {
subset: LinkSubset::LinkNextPG,
hl_bn: 3,
})
);
}
#[test]
fn step_link_pgcn_pttn_pgn_cn_surface_named_targets() {
let mut vm = Vm::new();
assert_eq!(
vm.step(NavInstruction::LinkPgcn { pgcn: 0x1234 }),
VmAction::Link(LinkAction::Pgcn { pgcn: 0x1234 })
);
assert_eq!(
vm.step(NavInstruction::LinkPttn { pttn: 5, hl_bn: 1 }),
VmAction::Link(LinkAction::Pttn { pttn: 5, hl_bn: 1 })
);
assert_eq!(
vm.step(NavInstruction::LinkPgn { pgn: 9, hl_bn: 2 }),
VmAction::Link(LinkAction::Pgn { pgn: 9, hl_bn: 2 })
);
assert_eq!(
vm.step(NavInstruction::LinkCn { cn: 11, hl_bn: 4 }),
VmAction::Link(LinkAction::Cn { cn: 11, hl_bn: 4 })
);
}
#[test]
fn step_jump_family_surfaces_typed_actions() {
let mut vm = Vm::new();
assert_eq!(
vm.step(NavInstruction::JumpTT { ttn: 7 }),
VmAction::JumpTitle { ttn: 7 }
);
assert_eq!(
vm.step(NavInstruction::JumpVtsTt { ttn: 8 }),
VmAction::JumpVtsTitle { ttn: 8 }
);
assert_eq!(
vm.step(NavInstruction::JumpVtsPtt { ttn: 9, pttn: 4 }),
VmAction::JumpVtsPtt { ttn: 9, pttn: 4 }
);
assert_eq!(
vm.step(NavInstruction::JumpSs(JumpSSTarget::FirstPlay)),
VmAction::JumpSs(JumpSSTarget::FirstPlay)
);
}
#[test]
fn step_callss_pushes_resume_then_rsm_pops_it() {
let mut vm = Vm::new();
assert_eq!(vm.resume_depth(), 0);
let _action = vm.step(NavInstruction::CallSs(CallSSTarget::FirstPlay {
rsm_cell: 7,
}));
assert_eq!(vm.resume_depth(), 1);
let act = vm.step(NavInstruction::LinkSub {
subset: LinkSubset::Rsm,
hl_bn: 5,
});
assert_eq!(
act,
VmAction::Resume(ResumePoint {
resume_cell: 7,
hl_btn: 5,
})
);
assert_eq!(vm.resume_depth(), 0);
}
#[test]
fn step_rsm_with_empty_stack_is_continue() {
let mut vm = Vm::new();
let act = vm.step(NavInstruction::LinkSub {
subset: LinkSubset::Rsm,
hl_bn: 0,
});
assert_eq!(act, VmAction::Continue);
assert_eq!(vm.resume_depth(), 0);
}
#[test]
fn step_callss_stack_depth_bounded_to_max_rsm_depth() {
let mut vm = Vm::new();
for _ in 0..(MAX_RSM_DEPTH + 4) {
let _action = vm.step(NavInstruction::CallSs(CallSSTarget::FirstPlay {
rsm_cell: 1,
}));
}
assert_eq!(vm.resume_depth(), MAX_RSM_DEPTH);
}
#[test]
fn step_unknown_and_invalid_yield_noopraw() {
let mut vm = Vm::new();
let pre = vm.regs.clone();
let a = vm.step(NavInstruction::Unknown);
assert!(matches!(a, VmAction::NoOpRaw(_)));
let b = vm.step(NavInstruction::Invalid);
assert!(matches!(b, VmAction::NoOpRaw(_)));
assert_eq!(vm.regs.gprm(0), pre.gprm(0));
assert_eq!(vm.regs.sprm(0), pre.sprm(0));
}
#[test]
fn step_set_clnk_runs_set_then_compare_links_on_true() {
let mut vm = Vm::new();
vm.regs.set_gprm(3, 5);
let action = vm.step(NavInstruction::SetCLnk {
set_op: SetOp::Add,
cmp_op: CmpOp::Eq,
scr: Register::Gprm(3),
set_src: Operand::Immediate(5),
cmp_rhs: Operand::Immediate(10),
hl_bn: 2,
link: LinkSubset::LinkNextPG,
});
assert_eq!(vm.regs.gprm(3), 10);
assert_eq!(
action,
VmAction::Link(LinkAction::Subset {
subset: LinkSubset::LinkNextPG,
hl_bn: 2,
})
);
}
#[test]
fn step_set_clnk_runs_set_but_skips_link_on_false() {
let mut vm = Vm::new();
vm.regs.set_gprm(3, 1);
let action = vm.step(NavInstruction::SetCLnk {
set_op: SetOp::Add,
cmp_op: CmpOp::Eq,
scr: Register::Gprm(3),
set_src: Operand::Immediate(1),
cmp_rhs: Operand::Immediate(10),
hl_bn: 0,
link: LinkSubset::LinkNextPG,
});
assert_eq!(vm.regs.gprm(3), 2);
assert_eq!(action, VmAction::Continue);
}
#[test]
fn step_cset_clnk_runs_set_only_on_true() {
let mut vm = Vm::new();
vm.regs.set_gprm(7, 9);
let action = vm.step(NavInstruction::CSetCLnk {
set_op: SetOp::Mov,
cmp_op: CmpOp::Eq,
sr1: Register::Gprm(3),
set_src: Operand::Immediate(99),
cmp_lhs: Register::Gprm(7),
cmp_rhs: Operand::Immediate(9),
hl_bn: 0,
link: LinkSubset::LinkTopPGC,
});
assert_eq!(vm.regs.gprm(3), 99);
assert_eq!(
action,
VmAction::Link(LinkAction::Subset {
subset: LinkSubset::LinkTopPGC,
hl_bn: 0,
})
);
}
#[test]
fn step_cset_clnk_skips_set_and_link_on_false() {
let mut vm = Vm::new();
vm.regs.set_gprm(7, 1);
vm.regs.set_gprm(3, 42);
let action = vm.step(NavInstruction::CSetCLnk {
set_op: SetOp::Mov,
cmp_op: CmpOp::Eq,
sr1: Register::Gprm(3),
set_src: Operand::Immediate(99),
cmp_lhs: Register::Gprm(7),
cmp_rhs: Operand::Immediate(9),
hl_bn: 0,
link: LinkSubset::LinkTopPGC,
});
assert_eq!(vm.regs.gprm(3), 42);
assert_eq!(action, VmAction::Continue);
}
#[test]
fn step_cmp_set_lnk_links_unconditionally_even_on_false_cmp() {
let mut vm = Vm::new();
vm.regs.set_gprm(1, 1);
let action = vm.step(NavInstruction::CmpSetLnk {
set_op: SetOp::Mov,
cmp_op: CmpOp::Eq,
sr1: Register::Gprm(1),
set_src: Operand::Immediate(99),
cmp_rhs: Operand::Immediate(9),
hl_bn: 5,
link: LinkSubset::LinkNextPGC,
});
assert_eq!(vm.regs.gprm(1), 1);
assert_eq!(
action,
VmAction::Link(LinkAction::Subset {
subset: LinkSubset::LinkNextPGC,
hl_bn: 5,
})
);
}
#[test]
fn step_cmp_set_lnk_runs_set_on_true_then_links() {
let mut vm = Vm::new();
vm.regs.set_gprm(2, 7);
let action = vm.step(NavInstruction::CmpSetLnk {
set_op: SetOp::Add,
cmp_op: CmpOp::Eq,
sr1: Register::Gprm(2),
set_src: Operand::Immediate(3),
cmp_rhs: Operand::Immediate(7),
hl_bn: 0,
link: LinkSubset::LinkPrevPG,
});
assert_eq!(vm.regs.gprm(2), 10);
assert_eq!(
action,
VmAction::Link(LinkAction::Subset {
subset: LinkSubset::LinkPrevPG,
hl_bn: 0,
})
);
}
#[test]
fn step_compound_with_link_nop_returns_continue() {
let mut vm = Vm::new();
let action = vm.step(NavInstruction::CmpSetLnk {
set_op: SetOp::None,
cmp_op: CmpOp::None,
sr1: Register::Gprm(0),
set_src: Operand::Immediate(0),
cmp_rhs: Operand::Immediate(0),
hl_bn: 0,
link: LinkSubset::Nop,
});
assert_eq!(action, VmAction::Continue);
}
#[test]
fn step_compound_with_link_rsm_pops_resume_stack() {
let mut vm = Vm::new();
assert!(vm.push_resume(ResumePoint {
resume_cell: 4,
hl_btn: 0,
}));
let action = vm.step(NavInstruction::CmpSetLnk {
set_op: SetOp::None,
cmp_op: CmpOp::None,
sr1: Register::Gprm(0),
set_src: Operand::Immediate(0),
cmp_rhs: Operand::Immediate(0),
hl_bn: 9,
link: LinkSubset::Rsm,
});
assert_eq!(
action,
VmAction::Resume(ResumePoint {
resume_cell: 4,
hl_btn: 9,
})
);
assert_eq!(vm.resume_depth(), 0);
}
#[test]
fn step_compound_with_invalid_link_subset_is_continue() {
let mut vm = Vm::new();
let action = vm.step(NavInstruction::SetCLnk {
set_op: SetOp::None,
cmp_op: CmpOp::None,
scr: Register::Gprm(0),
set_src: Operand::Immediate(0),
cmp_rhs: Operand::Immediate(0),
hl_bn: 0,
link: LinkSubset::Invalid(0x04),
});
assert_eq!(action, VmAction::Continue);
}
#[test]
fn step_compound_setop_none_skips_set_phase() {
let mut vm = Vm::new();
vm.regs.set_gprm(5, 42);
let action = vm.step(NavInstruction::CSetCLnk {
set_op: SetOp::None,
cmp_op: CmpOp::Eq,
sr1: Register::Gprm(5),
set_src: Operand::Immediate(99), cmp_lhs: Register::Gprm(5),
cmp_rhs: Operand::Immediate(42),
hl_bn: 1,
link: LinkSubset::LinkTopCell,
});
assert_eq!(vm.regs.gprm(5), 42);
assert_eq!(
action,
VmAction::Link(LinkAction::Subset {
subset: LinkSubset::LinkTopCell,
hl_bn: 1,
})
);
}
fn encode_nop() -> NavCommand {
NavCommand {
bytes: [0x00, 0x00, 0, 0, 0, 0, 0, 0],
}
}
fn encode_break() -> NavCommand {
NavCommand {
bytes: [0x00, 0x02, 0, 0, 0, 0, 0, 0],
}
}
fn encode_exit() -> NavCommand {
NavCommand {
bytes: [0x30, 0x01, 0, 0, 0, 0, 0, 0],
}
}
fn encode_goto(line: u8) -> NavCommand {
NavCommand {
bytes: [0x00, 0x01, 0, 0, 0, 0, 0, line],
}
}
#[test]
fn run_list_completes_cleanly_through_nops() {
let mut vm = Vm::new();
let list = vec![encode_nop(), encode_nop(), encode_nop()];
let (action, pc) = vm.run_list(&list);
assert_eq!(action, VmAction::Continue);
assert_eq!(pc, 3);
}
#[test]
fn run_list_break_returns_at_break_pc() {
let mut vm = Vm::new();
let list = vec![encode_nop(), encode_break(), encode_nop()];
let (action, pc) = vm.run_list(&list);
assert_eq!(action, VmAction::Break);
assert_eq!(pc, 1);
}
#[test]
fn run_list_exit_returns_at_exit_pc() {
let mut vm = Vm::new();
let list = vec![encode_nop(), encode_exit(), encode_nop()];
let (action, pc) = vm.run_list(&list);
assert_eq!(action, VmAction::Exit);
assert_eq!(pc, 1);
}
#[test]
fn run_list_goto_jumps_to_one_based_line() {
let mut vm = Vm::new();
let list = vec![encode_goto(3), encode_nop(), encode_break()];
let (action, pc) = vm.run_list(&list);
assert_eq!(action, VmAction::Break);
assert_eq!(pc, 2);
}
#[test]
fn run_list_goto_out_of_range_runs_to_end() {
let mut vm = Vm::new();
let list = vec![encode_goto(99), encode_nop()];
let (action, pc) = vm.run_list(&list);
assert_eq!(action, VmAction::Continue);
assert_eq!(pc, list.len());
}
#[test]
fn run_list_runaway_goto_loop_terminates_under_budget() {
let mut vm = Vm::new();
let list = vec![encode_goto(1)];
let (action, _) = vm.run_list(&list);
assert_eq!(action, VmAction::Continue);
}
#[test]
fn run_list_pc_resets_to_zero_between_invocations() {
let mut vm = Vm::new();
vm.pc = 17;
let _ = vm.run_list(&[encode_nop()]);
assert_eq!(vm.pc(), 1);
}
#[test]
fn default_navcommand_runs_as_single_nop() {
let mut vm = Vm::new();
let (action, pc) = vm.run_list(&[NavCommand::default()]);
assert_eq!(action, VmAction::Continue);
assert_eq!(pc, 1);
}
#[test]
fn sprm_defaults_cover_language_extension_slots() {
let r = RegisterFile::new();
assert_eq!(r.sprm(SPRM_PREF_AUDIO_LANG_EXT), 0);
assert_eq!(r.sprm(SPRM_PREF_SUBP_LANG_EXT), 0);
}
#[test]
fn sprm_named_constants_match_spec_indices() {
assert_eq!(SPRM_MENU_LANG, 0);
assert_eq!(SPRM_CC_PLT, 12);
assert_eq!(SPRM_VIDEO_PREF, 14);
assert_eq!(SPRM_AUDIO_CAPS, 15);
assert_eq!(SPRM_PREF_AUDIO_LANG, 16);
assert_eq!(SPRM_PREF_AUDIO_LANG_EXT, 17);
assert_eq!(SPRM_PREF_SUBP_LANG, 18);
assert_eq!(SPRM_PREF_SUBP_LANG_EXT, 19);
assert_eq!(SPRM_REGION_MASK, 20);
}
#[test]
fn subpicture_stream_default_is_none_with_display_off() {
let r = RegisterFile::new();
let view = r.subpicture_stream();
assert_eq!(view.stream, 62);
assert!(!view.display);
assert!(view.is_none_sentinel());
assert!(!view.is_forced_sentinel());
}
#[test]
fn subpicture_stream_forced_sentinel_with_display_on() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_SUBPICTURE_STREAM, 63 | (1 << 6));
let view = r.subpicture_stream();
assert_eq!(view.stream, 63);
assert!(view.display);
assert!(view.is_forced_sentinel());
assert!(!view.is_none_sentinel());
}
#[test]
fn highlight_button_decodes_default_as_one() {
let r = RegisterFile::new();
assert_eq!(r.highlight_button(), 1);
}
#[test]
fn highlight_button_decodes_arbitrary_value() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_HL_BTNN, 17 << 10);
assert_eq!(r.highlight_button(), 17);
}
#[test]
fn highlight_button_rejects_out_of_range_field() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_HL_BTNN, 0);
assert_eq!(r.highlight_button(), 0);
r.set_sprm(SPRM_HL_BTNN, 37 << 10);
assert_eq!(r.highlight_button(), 0);
}
#[test]
fn audio_mix_mode_decodes_all_six_documented_bits() {
let mut r = RegisterFile::new();
let bits = (1 << 2) | (1 << 3) | (1 << 4) | (1 << 10) | (1 << 11) | (1 << 12);
r.set_sprm(SPRM_AMXMD, bits);
let m = r.audio_mix_mode();
assert!(m.mix_2_to_front);
assert!(m.mix_3_to_front);
assert!(m.mix_4_to_front);
assert!(m.mix_2_to_rear);
assert!(m.mix_3_to_rear);
assert!(m.mix_4_to_rear);
assert_eq!(m.raw, bits);
}
#[test]
fn audio_mix_mode_default_is_no_routing() {
let r = RegisterFile::new();
let m = r.audio_mix_mode();
assert!(!m.mix_2_to_front);
assert!(!m.mix_3_to_front);
assert!(!m.mix_4_to_front);
assert!(!m.mix_2_to_rear);
assert!(!m.mix_3_to_rear);
assert!(!m.mix_4_to_rear);
}
#[test]
fn video_preference_decodes_aspect_and_mode() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_VIDEO_PREF, (0b11 << 10) | (0b10 << 8));
let p = r.video_preference();
assert_eq!(p.aspect, AspectRatio::Ar16x9);
assert_eq!(p.mode, DisplayMode::Letterbox);
}
#[test]
fn video_preference_covers_every_named_code() {
assert_eq!(AspectRatio::from_bits(0), AspectRatio::Ar4x3);
assert_eq!(AspectRatio::from_bits(1), AspectRatio::NotSpecified);
assert_eq!(AspectRatio::from_bits(2), AspectRatio::Reserved);
assert_eq!(AspectRatio::from_bits(3), AspectRatio::Ar16x9);
assert_eq!(DisplayMode::from_bits(0), DisplayMode::Normal);
assert_eq!(DisplayMode::from_bits(1), DisplayMode::PanScan);
assert_eq!(DisplayMode::from_bits(2), DisplayMode::Letterbox);
assert_eq!(DisplayMode::from_bits(3), DisplayMode::Reserved);
}
#[test]
fn audio_capabilities_cannot_play_when_zero() {
let r = RegisterFile::new();
let c = r.audio_capabilities();
assert!(c.cannot_play());
assert!(!c.dolby);
assert!(!c.dts);
assert!(!c.mpeg);
}
#[test]
fn audio_capabilities_decodes_each_documented_bit() {
let mut r = RegisterFile::new();
let bits = (1 << 2) | (1 << 3) | (1 << 4) | (1 << 6) | (1 << 7) | (1 << 10) | (1 << 11) | (1 << 12) | (1 << 14); r.set_sprm(SPRM_AUDIO_CAPS, bits);
let c = r.audio_capabilities();
assert!(c.sdds_karaoke);
assert!(c.dts_karaoke);
assert!(c.mpeg_karaoke);
assert!(c.dolby_karaoke);
assert!(c.pcm_karaoke);
assert!(c.sdds);
assert!(c.dts);
assert!(c.mpeg);
assert!(c.dolby);
assert!(!c.cannot_play());
}
#[test]
fn region_mask_default_is_all_disallowed() {
let r = RegisterFile::new();
for region in 1..=8 {
assert!(!r.region_allowed(region));
}
assert!(!r.region_allowed(0));
assert!(!r.region_allowed(9));
}
#[test]
fn region_mask_per_region_decode() {
let mut r = RegisterFile::new();
let mask = (1u16 << 0) | (1 << 1) | (1 << 3) | (1 << 7);
r.set_sprm(SPRM_REGION_MASK, mask);
assert!(r.region_allowed(1));
assert!(r.region_allowed(2));
assert!(!r.region_allowed(3));
assert!(r.region_allowed(4));
assert!(!r.region_allowed(5));
assert!(!r.region_allowed(6));
assert!(!r.region_allowed(7));
assert!(r.region_allowed(8));
assert_eq!(r.region_mask(), mask as u8);
}
#[test]
fn menu_language_default_is_uninitialised() {
let r = RegisterFile::new();
let lc = r.menu_language();
assert!(!lc.is_not_specified());
assert_eq!(lc.ascii_bytes(), None);
}
#[test]
fn menu_language_encodes_ascii_pair() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_MENU_LANG, 0x656E);
let lc = r.menu_language();
assert_eq!(lc.ascii_bytes(), Some([b'e', b'n']));
assert_eq!(lc.as_string().as_deref(), Some("en"));
assert_eq!(lc.raw, 0x656E);
}
#[test]
fn preferred_audio_language_default_is_not_specified() {
let r = RegisterFile::new();
let lc = r.preferred_audio_language();
assert!(lc.is_not_specified());
assert_eq!(lc.ascii_bytes(), None);
assert_eq!(lc.as_string(), None);
assert_eq!(lc.raw, LanguageCode::NOT_SPECIFIED);
}
#[test]
fn preferred_subpicture_language_round_trips_uppercase_ascii() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_PREF_SUBP_LANG, 0x4A41);
let lc = r.preferred_subpicture_language();
assert_eq!(lc.ascii_bytes(), Some([b'J', b'A']));
assert_eq!(lc.as_string().as_deref(), Some("ja"));
}
#[test]
fn parental_country_garbage_collapses_to_none() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_CC_PLT, 0x3030); let lc = r.parental_country();
assert_eq!(lc.ascii_bytes(), None);
assert_eq!(lc.as_string(), None);
assert_eq!(lc.raw, 0x3030);
}
#[test]
fn audio_stream_default_is_none_sentinel() {
let r = RegisterFile::new();
let s = r.audio_stream();
assert_eq!(s, AudioStreamSelector::None);
assert!(s.is_none_sentinel());
assert!(!s.is_stream());
}
#[test]
fn audio_stream_concrete_index() {
let mut r = RegisterFile::new();
for i in 0..=7u16 {
r.set_sprm(SPRM_AUDIO_STREAM, i);
let s = r.audio_stream();
assert_eq!(s, AudioStreamSelector::Stream(i as u8));
assert!(s.is_stream());
assert!(!s.is_none_sentinel());
}
}
#[test]
fn audio_stream_invalid_preserves_raw() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_AUDIO_STREAM, 8);
assert_eq!(r.audio_stream(), AudioStreamSelector::Invalid(8));
r.set_sprm(SPRM_AUDIO_STREAM, 99);
assert_eq!(r.audio_stream(), AudioStreamSelector::Invalid(99));
}
#[test]
fn angle_default_is_one() {
let r = RegisterFile::new();
assert_eq!(r.angle_number(), Some(1));
}
#[test]
fn angle_in_range_round_trip() {
let mut r = RegisterFile::new();
for i in 1..=9u16 {
r.set_sprm(SPRM_ANGLE, i);
assert_eq!(r.angle_number(), Some(i as u8));
}
}
#[test]
fn angle_out_of_range_is_none() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_ANGLE, 0);
assert_eq!(r.angle_number(), None);
r.set_sprm(SPRM_ANGLE, 10);
assert_eq!(r.angle_number(), None);
r.set_sprm(SPRM_ANGLE, 0xFFFF);
assert_eq!(r.angle_number(), None);
}
#[test]
fn parental_level_default_is_uninitialised_zero() {
let r = RegisterFile::new();
assert_eq!(r.parental_level(), ParentalLevel::Invalid(0));
assert!(!r.parental_level().is_level());
assert!(!r.parental_level().is_none_sentinel());
}
#[test]
fn parental_level_in_range() {
let mut r = RegisterFile::new();
for i in 1..=8u16 {
r.set_sprm(SPRM_PARENTAL_LEVEL, i);
assert_eq!(r.parental_level(), ParentalLevel::Level(i as u8));
assert!(r.parental_level().is_level());
}
}
#[test]
fn parental_level_none_sentinel() {
let mut r = RegisterFile::new();
r.set_sprm(SPRM_PARENTAL_LEVEL, 15);
assert_eq!(r.parental_level(), ParentalLevel::None);
assert!(r.parental_level().is_none_sentinel());
}
#[test]
fn audio_language_ext_decode_table() {
let mut r = RegisterFile::new();
assert_eq!(
r.preferred_audio_language_ext(),
AudioLanguageExt::NotSpecified
);
for (raw, expected) in [
(1, AudioLanguageExt::Normal),
(2, AudioLanguageExt::VisuallyImpaired),
(3, AudioLanguageExt::DirectorComments),
(4, AudioLanguageExt::AlternateDirectorComments),
(5, AudioLanguageExt::Reserved(5)),
(250, AudioLanguageExt::Reserved(250)),
] {
r.set_sprm(SPRM_PREF_AUDIO_LANG_EXT, raw);
assert_eq!(r.preferred_audio_language_ext(), expected);
}
}
#[test]
fn subpicture_language_ext_decode_table() {
let mut r = RegisterFile::new();
assert_eq!(
r.preferred_subpicture_language_ext(),
SubpictureLanguageExt::NotSpecified
);
for (raw, expected) in [
(1, SubpictureLanguageExt::Normal),
(2, SubpictureLanguageExt::Large),
(3, SubpictureLanguageExt::Children),
(5, SubpictureLanguageExt::NormalCaptions),
(6, SubpictureLanguageExt::LargeCaptions),
(7, SubpictureLanguageExt::ChildrensCaptions),
(9, SubpictureLanguageExt::Forced),
(13, SubpictureLanguageExt::DirectorComments),
(14, SubpictureLanguageExt::LargeDirectorComments),
(15, SubpictureLanguageExt::DirectorCommentsForChildren),
(4, SubpictureLanguageExt::Reserved(4)),
(8, SubpictureLanguageExt::Reserved(8)),
(10, SubpictureLanguageExt::Reserved(10)),
(11, SubpictureLanguageExt::Reserved(11)),
(12, SubpictureLanguageExt::Reserved(12)),
(200, SubpictureLanguageExt::Reserved(200)),
] {
r.set_sprm(SPRM_PREF_SUBP_LANG_EXT, raw);
assert_eq!(r.preferred_subpicture_language_ext(), expected);
}
}
}