Skip to main content

oxideav_dvd/
vm.rs

1//! DVD-Video VM **interpreter** — Phase 3c.
2//!
3//! [`Vm`] owns the register file (16 GPRMs + 24 SPRMs, the per-GPRM
4//! counter-mode bit) and the navigation-resume stack, and exposes
5//! [`Vm::step`] which advances one [`NavInstruction`] and returns a
6//! [`VmAction`] describing the playback-engine-visible effect. The
7//! companion [`Vm::run_list`] walks a pre / post / cell command list
8//! end-to-end and honours the intra-list `Goto` / `Break` control
9//! flow.
10//!
11//! The interpreter is **transport-agnostic** — it never reads from
12//! disc, never decodes a VOBU, never touches MKV. It mutates its own
13//! register state and returns the playback-engine-visible
14//! `JumpSS` / `CallSS` / `Link*` / `JumpTT` / `Exit` / `RSM`
15//! destinations as typed [`VmAction`] values so a downstream player
16//! can decide what to load next.
17//!
18//! Clean-room per:
19//!
20//! - `docs/container/dvd/application/mpucoder-vmi.html` — opcode
21//!   semantics + SET/CMP sub-op tables + link-subset table.
22//! - `docs/container/dvd/application/mpucoder-vmi-sum.html` — the
23//!   plain-English instruction-family summary (compound Type 4..6
24//!   forms' "Set then Compare & Link" ordering rules).
25//! - `docs/container/dvd/application/mpucoder-vmi-jmp.html` — the
26//!   per-domain Jump / Call permission tables (used here only as a
27//!   sanity-check for the destination kinds the interpreter
28//!   surfaces; domain enforcement is the player's job).
29//! - `docs/container/dvd/application/mpucoder-sprm.html` — the SPRM
30//!   numbering + default values.
31//! - `docs/container/dvd/application/mpucoder-uops.html` — User
32//!   Operation flag bit numbers.
33//!
34//! Semantics derive from the `docs/container/dvd/` references
35//! listed above.
36
37use crate::ifo::NavCommand;
38use crate::nav::{
39    CallSSTarget, CmpOp, JumpSSTarget, LinkSubset, NavInstruction, Operand, Register, SetOp,
40};
41
42// =====================================================================
43// Register file.
44// =====================================================================
45
46/// Number of general-purpose registers (`GPRM0..=GPRM15`) per
47/// `mpucoder-sprm.html`. The 16-register file is *persistent* — it
48/// survives across PGCs and across `JumpSS` / `CallSS` boundaries
49/// per the spec page's "GPRM" description.
50pub const GPRM_COUNT: usize = 16;
51
52/// Number of system-parameter registers exposed at runtime
53/// (`SPRM0..=SPRM23`). The remaining `SPRM24..=SPRM31` slots are
54/// reserved on the spec page — they have no defined behaviour so we
55/// don't allocate them.
56pub const SPRM_COUNT: usize = 24;
57
58/// SPRM 0 — Preferred Menu Language (ISO 639 code, "player specific").
59pub const SPRM_MENU_LANG: u8 = 0;
60/// SPRM 1 — Audio Stream Number (`ASTN`).
61pub const SPRM_AUDIO_STREAM: u8 = 1;
62/// SPRM 2 — Sub-picture Stream Number (`SPSTN`).
63pub const SPRM_SUBPICTURE_STREAM: u8 = 2;
64/// SPRM 3 — Angle Number (`AGLN`).
65pub const SPRM_ANGLE: u8 = 3;
66/// SPRM 4 — Title Number in volume (`TTN`).
67pub const SPRM_TITLE: u8 = 4;
68/// SPRM 5 — Title Number in VTS (`VTS_TTN`).
69pub const SPRM_VTS_TITLE: u8 = 5;
70/// SPRM 6 — PGC Number (`TT_PGCN`).
71pub const SPRM_PGCN: u8 = 6;
72/// SPRM 7 — PTT Number (`PTTN`).
73pub const SPRM_PTT: u8 = 7;
74/// SPRM 8 — Highlighted Button Number (`HL_BTNN`).
75pub const SPRM_HL_BTNN: u8 = 8;
76/// SPRM 9 — Navigation Timer (`NVTMR`) in seconds (0..=65535).
77pub const SPRM_NV_TIMER: u8 = 9;
78/// SPRM 10 — PGC jump target when the nav timer expires (`NV_PGCN`).
79pub const SPRM_NV_PGCN: u8 = 10;
80/// SPRM 11 — Karaoke Audio Mixing Mode (`AMXMD`).
81pub const SPRM_AMXMD: u8 = 11;
82/// SPRM 12 — Parental-management Country Code (`CC_PLT`, ISO 3166).
83pub const SPRM_CC_PLT: u8 = 12;
84/// SPRM 13 — Parental Level (`PLT`).
85pub const SPRM_PARENTAL_LEVEL: u8 = 13;
86/// SPRM 14 — Video Preference + current display mode bitfield.
87pub const SPRM_VIDEO_PREF: u8 = 14;
88/// SPRM 15 — Player audio capability bitmap.
89pub const SPRM_AUDIO_CAPS: u8 = 15;
90/// SPRM 16 — Preferred audio language (ISO 639 code, default `0xFFFF`).
91pub const SPRM_PREF_AUDIO_LANG: u8 = 16;
92/// SPRM 17 — Preferred audio language extension code.
93pub const SPRM_PREF_AUDIO_LANG_EXT: u8 = 17;
94/// SPRM 18 — Preferred sub-picture language (ISO 639 code, default `0xFFFF`).
95pub const SPRM_PREF_SUBP_LANG: u8 = 18;
96/// SPRM 19 — Preferred sub-picture language extension code.
97pub const SPRM_PREF_SUBP_LANG_EXT: u8 = 19;
98/// SPRM 20 — Player Region-code mask (bit `i` ⇒ region `i + 1`).
99pub const SPRM_REGION_MASK: u8 = 20;
100
101/// The full SPRM-indexed default vector per `mpucoder-sprm.html`.
102///
103/// "Player specific" cells (SPRM 0 menu-language, SPRM 6 TT_PGCN, SPRM 10
104/// NV_PGCN, SPRM 12 country code, SPRM 13 parental level, SPRM 14 video
105/// preference, SPRM 15 audio caps, SPRM 20 region mask) are left at `0`
106/// — the spec page assigns those to the host player's environment, not to
107/// a fixed numeric default. The numeric defaults follow the spec page's
108/// `default` column:
109///
110/// | SPRM | default            | source             |
111/// | ---- | ------------------ | ------------------ |
112/// |    1 | `15` (none)        | ASTN row           |
113/// |    2 | `62` (none)        | SPSTN row          |
114/// |    3 | `1`                | AGLN row           |
115/// |    4 | `1`                | TTN row            |
116/// |    5 | `1`                | VTS_TTN row        |
117/// |    7 | `1`                | PTTN row           |
118/// |    8 | `1024` (`1<<10`)   | HL_BTNN row, "button 1" in bits 10..15 |
119/// |    9 | `0`                | NVTMR row          |
120/// |   11 | `0`                | AMXMD row          |
121/// |   16 | `0xFFFF` (none)    | preferred audio language row |
122/// |   17 | `0` (not spec'd)   | language extension row |
123/// |   18 | `0xFFFF` (none)    | preferred sub-picture language row |
124/// |   19 | `0` (not spec'd)   | language extension row |
125///
126/// SPRMs 17 and 19 share the same documented enumeration: `0 = "not
127/// specified"`, then a small fixed code set per the spec's `language
128/// extension` row. The default `0` therefore matches "no preference
129/// expressed", consistent with the umbrella `0xFFFF` on the corresponding
130/// language slot. SPRMs 21..=23 are "reserved" on the spec page and we
131/// leave them at `0`.
132const SPRM_DEFAULTS: [u16; SPRM_COUNT] = {
133    let mut v = [0u16; SPRM_COUNT];
134    v[1] = 15; // ASTN
135    v[2] = 62; // SPSTN
136    v[3] = 1; // AGLN
137    v[4] = 1; // TTN
138    v[5] = 1; // VTS_TTN
139    v[7] = 1; // PTTN
140    v[8] = 1 << 10; // HL_BTNN — button 1 in bits 10..15
141    v[16] = 0xFFFF; // preferred audio language
142    v[17] = 0; // preferred audio language extension — "not specified"
143    v[18] = 0xFFFF; // preferred sub-picture language
144    v[19] = 0; // preferred sub-picture language extension — "not specified"
145    v
146};
147
148/// 16 × GPRM + 24 × SPRM register file plus the per-GPRM
149/// "counter mode" bit that `SetGPRMMD` toggles.
150///
151/// Per `mpucoder-vmi.html` the `SetGPRMMD` `mf` flag selects whether
152/// the GPRM behaves as a plain integer register or as a 1 Hz
153/// counter; the spec page reserves that behavioural state but
154/// doesn't give it its own register address — so we carry it on the
155/// side. The interpreter never *ticks* the counters itself (it has
156/// no notion of wall time); the public [`RegisterFile::tick_counters`]
157/// helper exists for a playback engine that owns a wall clock.
158#[derive(Debug, Clone)]
159pub struct RegisterFile {
160    gprm: [u16; GPRM_COUNT],
161    sprm: [u16; SPRM_COUNT],
162    /// Bit `i` set ⇒ `GPRM[i]` is in counter mode (auto-increments
163    /// once per second when ticked).
164    counter_mask: u16,
165}
166
167impl Default for RegisterFile {
168    fn default() -> Self {
169        Self {
170            gprm: [0; GPRM_COUNT],
171            sprm: SPRM_DEFAULTS,
172            counter_mask: 0,
173        }
174    }
175}
176
177impl RegisterFile {
178    /// Construct a fresh register file with the spec-defined SPRM
179    /// defaults and all GPRMs cleared.
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    /// Read a GPRM by index (`0..=15`). Out-of-range index returns
185    /// `0` — matches the spec's "invalid register reads as 0"
186    /// fallback used by malformed PGC command tables in the wild.
187    pub fn gprm(&self, index: u8) -> u16 {
188        if (index as usize) < GPRM_COUNT {
189            self.gprm[index as usize]
190        } else {
191            0
192        }
193    }
194
195    /// Write a GPRM by index. Out-of-range index is silently dropped.
196    pub fn set_gprm(&mut self, index: u8, value: u16) {
197        if (index as usize) < GPRM_COUNT {
198            self.gprm[index as usize] = value;
199        }
200    }
201
202    /// Read an SPRM by index (`0..=23`). Out-of-range returns `0`.
203    pub fn sprm(&self, index: u8) -> u16 {
204        if (index as usize) < SPRM_COUNT {
205            self.sprm[index as usize]
206        } else {
207            0
208        }
209    }
210
211    /// Write an SPRM by index. Out-of-range index is silently dropped.
212    ///
213    /// SPRMs are largely read-only at the bit-stream level (the
214    /// `SetSystem` opcodes are the only legal entry points), but
215    /// nothing in the spec page forbids a runtime / debugger from
216    /// pre-loading the file; we surface the write so tests + tooling
217    /// can.
218    pub fn set_sprm(&mut self, index: u8, value: u16) {
219        if (index as usize) < SPRM_COUNT {
220            self.sprm[index as usize] = value;
221        }
222    }
223
224    /// Read whichever register the [`Register`] enum names. SPRMs
225    /// out of the supported range and the catch-all `Invalid`
226    /// variant both return `0`.
227    pub fn read(&self, reg: Register) -> u16 {
228        match reg {
229            Register::Gprm(i) => self.gprm(i),
230            Register::Sprm(i) => self.sprm(i),
231            Register::Invalid(_) => 0,
232        }
233    }
234
235    /// Resolve an [`Operand`] to its 16-bit value.
236    pub fn read_operand(&self, op: Operand) -> u16 {
237        match op {
238            Operand::Register(r) => self.read(r),
239            Operand::Immediate(v) => v,
240        }
241    }
242
243    /// Flip the per-GPRM counter-mode flag.
244    pub fn set_counter_mode(&mut self, gprm_index: u8, on: bool) {
245        if (gprm_index as usize) < GPRM_COUNT {
246            let bit = 1u16 << gprm_index;
247            if on {
248                self.counter_mask |= bit;
249            } else {
250                self.counter_mask &= !bit;
251            }
252        }
253    }
254
255    /// `true` ⇒ the named GPRM is acting as a 1 Hz counter.
256    pub fn counter_mode(&self, gprm_index: u8) -> bool {
257        if (gprm_index as usize) < GPRM_COUNT {
258            (self.counter_mask >> gprm_index) & 1 == 1
259        } else {
260            false
261        }
262    }
263
264    /// Advance every counter-mode GPRM by `delta` seconds (saturating
265    /// at `u16::MAX`). The interpreter never invokes this — it's a
266    /// hook for a playback engine that owns a wall clock.
267    pub fn tick_counters(&mut self, delta: u16) {
268        let mut mask = self.counter_mask;
269        while mask != 0 {
270            let bit = mask.trailing_zeros() as usize;
271            self.gprm[bit] = self.gprm[bit].saturating_add(delta);
272            mask &= !(1u16 << bit);
273        }
274    }
275
276    // -----------------------------------------------------------------
277    // Bitfield-aware accessors for the 6 SPRMs whose contents are not a
278    // single integer but a documented bit-packed payload. Each accessor
279    // decomposes the register against the layout published on
280    // `docs/container/dvd/application/mpucoder-sprm.html`.
281    //
282    // The accessors **read** from the raw `u16` slot — they don't
283    // shadow the storage. A `SetSystem` opcode that writes the packed
284    // register still goes through `set_sprm`; these helpers exist so a
285    // player engine can ask "which subpicture stream is active and is
286    // it currently being displayed?" without re-implementing the
287    // bit-packing on every callsite.
288    // -----------------------------------------------------------------
289
290    /// SPRM 2 sub-picture stream — bits 0..=5 carry the stream
291    /// number (`0..=31`, `62` = none, `63` = forced); bit 6 = `0`
292    /// means "do not display". Returns the pair as a typed view.
293    pub fn subpicture_stream(&self) -> SubpictureStreamView {
294        let raw = self.sprm(SPRM_SUBPICTURE_STREAM);
295        SubpictureStreamView {
296            stream: (raw & 0x3F) as u8,
297            display: (raw >> 6) & 1 == 1,
298            raw,
299        }
300    }
301
302    /// SPRM 8 highlighted button — bits 10..=15 carry the button
303    /// number (`1..=36`); bits 0..=9 are documented as `0`.
304    /// Returns the decoded button number, or `0` when the field is
305    /// outside `1..=36` (which a malformed disc could encode).
306    pub fn highlight_button(&self) -> u8 {
307        let raw = self.sprm(SPRM_HL_BTNN);
308        let n = (raw >> 10) as u8;
309        if (1..=36).contains(&n) {
310            n
311        } else {
312            0
313        }
314    }
315
316    /// SPRM 11 karaoke audio mixing mode — returns the channel-routing
317    /// matrix. Bits 2/3/4 enable mixing channel 2/3/4 into the front
318    /// (front-left) channel; bits 10/11/12 enable mixing the same
319    /// channels into the rear (back) channel. Other bits are reserved.
320    pub fn audio_mix_mode(&self) -> AudioMixMode {
321        let raw = self.sprm(SPRM_AMXMD);
322        AudioMixMode {
323            mix_2_to_front: (raw >> 2) & 1 == 1,
324            mix_3_to_front: (raw >> 3) & 1 == 1,
325            mix_4_to_front: (raw >> 4) & 1 == 1,
326            mix_2_to_rear: (raw >> 10) & 1 == 1,
327            mix_3_to_rear: (raw >> 11) & 1 == 1,
328            mix_4_to_rear: (raw >> 12) & 1 == 1,
329            raw,
330        }
331    }
332
333    /// SPRM 14 video preference and current mode — bits 10..=11 carry
334    /// the preferred display aspect ratio; bits 8..=9 carry the
335    /// current display mode.
336    pub fn video_preference(&self) -> VideoPreference {
337        let raw = self.sprm(SPRM_VIDEO_PREF);
338        VideoPreference {
339            aspect: AspectRatio::from_bits(((raw >> 10) & 0b11) as u8),
340            mode: DisplayMode::from_bits(((raw >> 8) & 0b11) as u8),
341            raw,
342        }
343    }
344
345    /// SPRM 15 player audio capabilities — decodes the per-codec
346    /// capability bitmap into a typed view. `0` (no bits set) means
347    /// "cannot play anything" per the spec page.
348    pub fn audio_capabilities(&self) -> AudioCapabilities {
349        let raw = self.sprm(SPRM_AUDIO_CAPS);
350        AudioCapabilities {
351            sdds_karaoke: (raw >> 2) & 1 == 1,
352            dts_karaoke: (raw >> 3) & 1 == 1,
353            mpeg_karaoke: (raw >> 4) & 1 == 1,
354            dolby_karaoke: (raw >> 6) & 1 == 1,
355            pcm_karaoke: (raw >> 7) & 1 == 1,
356            sdds: (raw >> 10) & 1 == 1,
357            dts: (raw >> 11) & 1 == 1,
358            mpeg: (raw >> 12) & 1 == 1,
359            dolby: (raw >> 14) & 1 == 1,
360            raw,
361        }
362    }
363
364    /// SPRM 20 player region-code mask — bit `i` set ⇒ this player is
365    /// authorised to play region `i + 1` discs. Returns the bool
366    /// for the supplied region number (`1..=8`). Region `0` and
367    /// regions `9..` always return `false`.
368    pub fn region_allowed(&self, region: u8) -> bool {
369        if !(1..=8).contains(&region) {
370            return false;
371        }
372        let raw = self.sprm(SPRM_REGION_MASK);
373        (raw >> (region - 1)) & 1 == 1
374    }
375
376    /// SPRM 20 — full region-mask byte for callers that want to push
377    /// the bitmap downstream verbatim.
378    pub fn region_mask(&self) -> u8 {
379        self.sprm(SPRM_REGION_MASK) as u8
380    }
381
382    // -----------------------------------------------------------------
383    // Language / region / parental / audio-angle accessors for the
384    // remaining SPRMs whose 16-bit slot carries either two ASCII
385    // characters (ISO 639 language / ISO 3166 country) or a sentinel-
386    // typed integer. Layout per
387    // `docs/container/dvd/application/mpucoder-sprm.html`.
388    // -----------------------------------------------------------------
389
390    /// SPRM 0 preferred menu language — two-byte ISO 639 alpha-2 code
391    /// stored as high byte first character, low byte second character.
392    pub fn menu_language(&self) -> LanguageCode {
393        LanguageCode::from_raw(self.sprm(SPRM_MENU_LANG))
394    }
395
396    /// SPRM 1 audio stream number (`0..=7`, `15` = none) — returns
397    /// the typed view that distinguishes the `15` sentinel from a
398    /// real stream index.
399    pub fn audio_stream(&self) -> AudioStreamSelector {
400        AudioStreamSelector::from_raw(self.sprm(SPRM_AUDIO_STREAM))
401    }
402
403    /// SPRM 3 angle number (`1..=9`) — returns the raw lower byte.
404    /// Values outside `1..=9` collapse to `None`.
405    pub fn angle_number(&self) -> Option<u8> {
406        let v = self.sprm(SPRM_ANGLE) as u8;
407        if (1..=9).contains(&v) {
408            Some(v)
409        } else {
410            None
411        }
412    }
413
414    /// SPRM 12 parental management country code — ISO 3166 alpha-2
415    /// code packed identically to SPRM 0.
416    pub fn parental_country(&self) -> LanguageCode {
417        LanguageCode::from_raw(self.sprm(SPRM_CC_PLT))
418    }
419
420    /// SPRM 13 parental level — typed view that distinguishes the
421    /// `15` sentinel ("none / parental control off") from a real
422    /// level (`1..=8`).
423    pub fn parental_level(&self) -> ParentalLevel {
424        ParentalLevel::from_raw(self.sprm(SPRM_PARENTAL_LEVEL))
425    }
426
427    /// SPRM 16 preferred audio language — ISO 639 alpha-2 code, or
428    /// the `0xFFFF` "not specified" sentinel.
429    pub fn preferred_audio_language(&self) -> LanguageCode {
430        LanguageCode::from_raw(self.sprm(SPRM_PREF_AUDIO_LANG))
431    }
432
433    /// SPRM 17 preferred audio language extension.
434    pub fn preferred_audio_language_ext(&self) -> AudioLanguageExt {
435        AudioLanguageExt::from_raw(self.sprm(SPRM_PREF_AUDIO_LANG_EXT) as u8)
436    }
437
438    /// SPRM 18 preferred sub-picture language — ISO 639 alpha-2 code,
439    /// or the `0xFFFF` "not specified" sentinel.
440    pub fn preferred_subpicture_language(&self) -> LanguageCode {
441        LanguageCode::from_raw(self.sprm(SPRM_PREF_SUBP_LANG))
442    }
443
444    /// SPRM 19 preferred sub-picture language extension.
445    pub fn preferred_subpicture_language_ext(&self) -> SubpictureLanguageExt {
446        SubpictureLanguageExt::from_raw(self.sprm(SPRM_PREF_SUBP_LANG_EXT) as u8)
447    }
448}
449
450/// Decoded view of SPRM 2 (`SPSTN`).
451///
452/// `stream` is the raw 6-bit field (0..=31 = stream index, 62 = none,
453/// 63 = forced); `display` is bit 6 — when `false`, the sub-picture
454/// must not be displayed even if a stream is selected.
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub struct SubpictureStreamView {
457    /// Bits 0..=5 of SPRM 2 — the raw stream identifier.
458    pub stream: u8,
459    /// Bit 6 of SPRM 2 — `true` ⇒ display the chosen sub-picture
460    /// stream; `false` ⇒ suppress display regardless of `stream`.
461    pub display: bool,
462    /// The original SPRM 2 word for callers that want to round-trip it.
463    pub raw: u16,
464}
465
466impl SubpictureStreamView {
467    /// `true` when the stream field encodes the "none" sentinel (`62`).
468    pub fn is_none_sentinel(self) -> bool {
469        self.stream == 62
470    }
471    /// `true` when the stream field encodes the "forced" sentinel (`63`).
472    pub fn is_forced_sentinel(self) -> bool {
473        self.stream == 63
474    }
475}
476
477/// Decoded view of SPRM 11 (`AMXMD`).
478///
479/// Each `mix_<N>_to_<front|rear>` flag follows the spec page's bit
480/// allocation: bit 2 = "mix ch 2 into front", bit 3 = "mix ch 3 into
481/// front", bit 4 = "mix ch 4 into front", bits 10/11/12 do the same
482/// for the rear destination.
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub struct AudioMixMode {
485    /// Mix channel 2 into the front-left channel.
486    pub mix_2_to_front: bool,
487    /// Mix channel 3 into the front-left channel.
488    pub mix_3_to_front: bool,
489    /// Mix channel 4 into the front-left channel.
490    pub mix_4_to_front: bool,
491    /// Mix channel 2 into the rear (back) channel.
492    pub mix_2_to_rear: bool,
493    /// Mix channel 3 into the rear (back) channel.
494    pub mix_3_to_rear: bool,
495    /// Mix channel 4 into the rear (back) channel.
496    pub mix_4_to_rear: bool,
497    /// Raw SPRM 11 word.
498    pub raw: u16,
499}
500
501/// Decoded view of SPRM 14 (video preference + current mode).
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub struct VideoPreference {
504    /// Bits 10..=11 — preferred display aspect ratio.
505    pub aspect: AspectRatio,
506    /// Bits 8..=9 — current display mode.
507    pub mode: DisplayMode,
508    /// Raw SPRM 14 word.
509    pub raw: u16,
510}
511
512/// Preferred display aspect-ratio code per SPRM 14 bits 10..=11.
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514pub enum AspectRatio {
515    /// `0` — 4:3.
516    Ar4x3,
517    /// `1` — not specified.
518    NotSpecified,
519    /// `2` — reserved.
520    Reserved,
521    /// `3` — 16:9.
522    Ar16x9,
523}
524
525impl AspectRatio {
526    /// Decode from the 2-bit field.
527    pub fn from_bits(bits: u8) -> Self {
528        match bits & 0b11 {
529            0 => Self::Ar4x3,
530            1 => Self::NotSpecified,
531            2 => Self::Reserved,
532            _ => Self::Ar16x9,
533        }
534    }
535}
536
537/// Current display mode per SPRM 14 bits 8..=9.
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum DisplayMode {
540    /// `0` — normal.
541    Normal,
542    /// `1` — pan/scan.
543    PanScan,
544    /// `2` — letterbox.
545    Letterbox,
546    /// `3` — reserved.
547    Reserved,
548}
549
550impl DisplayMode {
551    /// Decode from the 2-bit field.
552    pub fn from_bits(bits: u8) -> Self {
553        match bits & 0b11 {
554            0 => Self::Normal,
555            1 => Self::PanScan,
556            2 => Self::Letterbox,
557            _ => Self::Reserved,
558        }
559    }
560}
561
562/// Decoded view of SPRM 15 (player audio capability bitmap).
563///
564/// `false` in every slot ⇒ the spec page's "cannot play" interpretation.
565#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566pub struct AudioCapabilities {
567    /// Bit 2 — SDDS karaoke.
568    pub sdds_karaoke: bool,
569    /// Bit 3 — DTS karaoke.
570    pub dts_karaoke: bool,
571    /// Bit 4 — MPEG karaoke.
572    pub mpeg_karaoke: bool,
573    /// Bit 6 — Dolby karaoke.
574    pub dolby_karaoke: bool,
575    /// Bit 7 — PCM karaoke.
576    pub pcm_karaoke: bool,
577    /// Bit 10 — SDDS.
578    pub sdds: bool,
579    /// Bit 11 — DTS.
580    pub dts: bool,
581    /// Bit 12 — MPEG.
582    pub mpeg: bool,
583    /// Bit 14 — Dolby.
584    pub dolby: bool,
585    /// Raw SPRM 15 word.
586    pub raw: u16,
587}
588
589impl AudioCapabilities {
590    /// `true` ⇒ no capability bits are set; the player cannot play
591    /// any of the documented audio types per the spec page.
592    pub fn cannot_play(self) -> bool {
593        self.raw == 0
594    }
595}
596
597/// Two-byte ASCII language / country code packed into a single `u16`
598/// register slot — the high byte is the first character, the low byte
599/// the second character, per the SPRM 0 / 12 / 16 / 18 layout on
600/// `docs/container/dvd/application/mpucoder-sprm.html`. The
601/// `0xFFFF` value spelled out in the SPRM 16 / 18 default column is
602/// the spec page's "not specified" sentinel.
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub struct LanguageCode {
605    /// The original SPRM word — exposed so a caller can round-trip
606    /// the slot bit-for-bit including the `0xFFFF` sentinel.
607    pub raw: u16,
608}
609
610impl LanguageCode {
611    /// `0xFFFF` — the SPRM 16 / 18 default "preferred language not
612    /// specified" sentinel.
613    pub const NOT_SPECIFIED: u16 = 0xFFFF;
614
615    /// Wrap a raw 16-bit SPRM slot.
616    pub fn from_raw(raw: u16) -> Self {
617        Self { raw }
618    }
619
620    /// `true` when the slot encodes the `0xFFFF` "not specified"
621    /// sentinel used by the SPRM 16 / 18 defaults.
622    pub fn is_not_specified(self) -> bool {
623        self.raw == Self::NOT_SPECIFIED
624    }
625
626    /// Return the two-byte ASCII code as a `[u8; 2]` when the slot
627    /// carries one (i.e. is not the `0xFFFF` sentinel and both bytes
628    /// are printable ASCII letters per ISO 639 / ISO 3166). Returns
629    /// `None` otherwise — including the "player specific" default
630    /// for SPRM 0 / 12 (which has no on-wire representation; the
631    /// spec leaves the slot uninitialised).
632    pub fn ascii_bytes(self) -> Option<[u8; 2]> {
633        if self.is_not_specified() {
634            return None;
635        }
636        let hi = (self.raw >> 8) as u8;
637        let lo = self.raw as u8;
638        if hi.is_ascii_alphabetic() && lo.is_ascii_alphabetic() {
639            Some([hi, lo])
640        } else {
641            None
642        }
643    }
644
645    /// Return the code as a 2-character `String`, lowercased per
646    /// ISO 639 / ISO 3166 alpha-2 convention. `None` when no valid
647    /// ASCII code is present.
648    pub fn as_string(self) -> Option<String> {
649        let [a, b] = self.ascii_bytes()?;
650        Some(format!(
651            "{}{}",
652            (a.to_ascii_lowercase()) as char,
653            (b.to_ascii_lowercase()) as char,
654        ))
655    }
656}
657
658/// Decoded view of SPRM 1 (`ASTN`).
659///
660/// The spec table allows `0..=7` (a real stream index) and `15` (the
661/// "no audio selected" sentinel). Any other value is malformed.
662#[derive(Debug, Clone, Copy, PartialEq, Eq)]
663pub enum AudioStreamSelector {
664    /// `0..=7` — concrete audio stream index.
665    Stream(u8),
666    /// `15` — "no audio" sentinel.
667    None,
668    /// Out-of-range raw value preserved verbatim for round-tripping.
669    Invalid(u16),
670}
671
672impl AudioStreamSelector {
673    /// Decode the SPRM 1 slot.
674    pub fn from_raw(raw: u16) -> Self {
675        match raw {
676            0..=7 => Self::Stream(raw as u8),
677            15 => Self::None,
678            other => Self::Invalid(other),
679        }
680    }
681
682    /// `true` ⇒ a concrete stream index was selected (not `15`, not
683    /// out-of-range).
684    pub fn is_stream(self) -> bool {
685        matches!(self, Self::Stream(_))
686    }
687
688    /// `true` ⇒ the slot encodes the `15` "no audio" sentinel.
689    pub fn is_none_sentinel(self) -> bool {
690        matches!(self, Self::None)
691    }
692}
693
694/// Decoded view of SPRM 13 (`PLT`).
695///
696/// The spec table allows `1..=8` (a real level) and `15` (the
697/// "no parental control" sentinel). The "player specific" default
698/// is whatever the implementing player writes into the slot at boot;
699/// values outside `1..=8` / `15` are surfaced as `Invalid` so the
700/// caller can choose how to handle them.
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
702pub enum ParentalLevel {
703    /// `1..=8` — concrete parental level.
704    Level(u8),
705    /// `15` — "parental control off / no level set" sentinel.
706    None,
707    /// Out-of-range raw value preserved verbatim.
708    Invalid(u16),
709}
710
711impl ParentalLevel {
712    /// Decode the SPRM 13 slot.
713    pub fn from_raw(raw: u16) -> Self {
714        match raw {
715            1..=8 => Self::Level(raw as u8),
716            15 => Self::None,
717            other => Self::Invalid(other),
718        }
719    }
720
721    /// `true` ⇒ a concrete level (`1..=8`) is set.
722    pub fn is_level(self) -> bool {
723        matches!(self, Self::Level(_))
724    }
725
726    /// `true` ⇒ the slot encodes the `15` "off" sentinel.
727    pub fn is_none_sentinel(self) -> bool {
728        matches!(self, Self::None)
729    }
730}
731
732/// Decoded view of SPRM 17 (preferred audio language extension).
733///
734/// Values per `mpucoder-sprm.html`: `0` = not specified, `1` = normal,
735/// `2` = for visually impaired, `3` = director comments,
736/// `4` = alternate director comments. Any other value collapses to
737/// `Reserved`.
738#[derive(Debug, Clone, Copy, PartialEq, Eq)]
739pub enum AudioLanguageExt {
740    /// `0` — not specified.
741    NotSpecified,
742    /// `1` — normal.
743    Normal,
744    /// `2` — for visually impaired.
745    VisuallyImpaired,
746    /// `3` — director comments.
747    DirectorComments,
748    /// `4` — alternate director comments.
749    AlternateDirectorComments,
750    /// Any other value — preserved verbatim for round-trip.
751    Reserved(u8),
752}
753
754impl AudioLanguageExt {
755    /// Decode the SPRM 17 low byte.
756    pub fn from_raw(raw: u8) -> Self {
757        match raw {
758            0 => Self::NotSpecified,
759            1 => Self::Normal,
760            2 => Self::VisuallyImpaired,
761            3 => Self::DirectorComments,
762            4 => Self::AlternateDirectorComments,
763            other => Self::Reserved(other),
764        }
765    }
766}
767
768/// Decoded view of SPRM 19 (preferred sub-picture language extension).
769///
770/// Values per `mpucoder-sprm.html`: `0` = not specified,
771/// `1` = normal, `2` = large, `3` = children, `5` = normal captions,
772/// `6` = large captions, `7` = children's captions, `9` = forced,
773/// `13` = director comments, `14` = large director comments,
774/// `15` = director comments for children. Other values collapse to
775/// `Reserved`.
776#[derive(Debug, Clone, Copy, PartialEq, Eq)]
777pub enum SubpictureLanguageExt {
778    /// `0` — not specified.
779    NotSpecified,
780    /// `1` — normal.
781    Normal,
782    /// `2` — large.
783    Large,
784    /// `3` — children.
785    Children,
786    /// `5` — normal captions.
787    NormalCaptions,
788    /// `6` — large captions.
789    LargeCaptions,
790    /// `7` — children's captions.
791    ChildrensCaptions,
792    /// `9` — forced.
793    Forced,
794    /// `13` — director comments.
795    DirectorComments,
796    /// `14` — large director comments.
797    LargeDirectorComments,
798    /// `15` — director comments for children.
799    DirectorCommentsForChildren,
800    /// Any other value — preserved verbatim for round-trip.
801    Reserved(u8),
802}
803
804impl SubpictureLanguageExt {
805    /// Decode the SPRM 19 low byte.
806    pub fn from_raw(raw: u8) -> Self {
807        match raw {
808            0 => Self::NotSpecified,
809            1 => Self::Normal,
810            2 => Self::Large,
811            3 => Self::Children,
812            5 => Self::NormalCaptions,
813            6 => Self::LargeCaptions,
814            7 => Self::ChildrensCaptions,
815            9 => Self::Forced,
816            13 => Self::DirectorComments,
817            14 => Self::LargeDirectorComments,
818            15 => Self::DirectorCommentsForChildren,
819            other => Self::Reserved(other),
820        }
821    }
822}
823
824// =====================================================================
825// VmAction — the playback-engine-visible effect of one step.
826// =====================================================================
827
828/// Effect of a single executed [`NavInstruction`] visible to the
829/// playback engine.
830///
831/// "Visible" here means "the interpreter has finished applying any
832/// register / counter mutations the instruction implied, and the
833/// engine must now translate this action into a disc-layer
834/// operation": load a different PGC, start a different cell,
835/// resume from a saved CallSS state, etc.
836#[derive(Debug, Clone, Copy, PartialEq, Eq)]
837pub enum VmAction {
838    /// Instruction completed; continue to the next word in the same
839    /// command list. The interpreter's `pc` advances by one.
840    Continue,
841    /// `Break` was executed inside a command list; the playback
842    /// engine should leave the list and proceed to whatever follows
843    /// (pre → cell, cell → post, post → "next PGC").
844    Break,
845    /// `Exit` was executed; playback should stop entirely.
846    Exit,
847    /// One of the Type-1 Link family fired; the playback engine
848    /// should re-enter the same PGC (or restart the current cell /
849    /// program) per the [`LinkAction`] descriptor.
850    Link(LinkAction),
851    /// `JumpTT` — jump to a different title in the volume.
852    JumpTitle { ttn: u8 },
853    /// `JumpVTS_TT` — jump to a different title inside the same VTS.
854    JumpVtsTitle { ttn: u8 },
855    /// `JumpVTS_PTT` — jump to a specific chapter of a VTS-internal
856    /// title.
857    JumpVtsPtt { ttn: u8, pttn: u16 },
858    /// `JumpSS` — cross-domain jump (no resume registered).
859    JumpSs(JumpSSTarget),
860    /// `CallSS` — cross-domain call (resume point pushed onto the
861    /// RSM stack before transferring control).
862    CallSs(CallSSTarget),
863    /// A Type-1 link subset selected `RSM` — pop the RSM stack and
864    /// return to whichever cell + PC was saved when the matching
865    /// CallSS fired. The `target` carries the saved location the
866    /// engine should resume to.
867    Resume(ResumePoint),
868    /// `SetNVTMR` — the navigation timer was loaded; the playback
869    /// engine owns the wall clock and must arrange to fire a
870    /// `LinkPGCN(pgcn)` once the timer expires.
871    SetNavTimer { seconds: u16, pgcn: u16 },
872    /// The instruction was structurally `Unknown` (Type-7) or
873    /// `Invalid` (red row in the opcode table). The interpreter
874    /// applied no mutation and advanced the PC; surfacing the raw
875    /// command lets a downstream debugger inspect what was on disc.
876    NoOpRaw(NavCommand),
877}
878
879/// Detailed form of a Type-1 link-family transfer.
880///
881/// The Type-1 family covers two related-but-distinct destination
882/// styles: the *coarse* `Link*` enum-style subset (restart current
883/// cell, advance to next PG, etc. — destination is "wherever the
884/// PGC's pre/post/cell layout says") and the *numbered* family
885/// (`LinkPGCN(pgcn)`, `LinkPTTN(pttn)`, `LinkPGN(pgn)`, `LinkCN(cn)`
886/// — destination is an explicit numeric index). We surface both
887/// flavours via dedicated variants so a player can dispatch with
888/// `match` and avoid re-decoding the originating instruction.
889#[derive(Debug, Clone, Copy, PartialEq, Eq)]
890pub enum LinkAction {
891    /// One of the 13 [`LinkSubset`] forms — `LinkTopCell` /
892    /// `LinkNextCell` / `LinkPrevCell` / `LinkTopPG` / … /
893    /// `LinkTailPGC` / `Nop` plus the spec's `Invalid` bag.
894    Subset { subset: LinkSubset, hl_bn: u8 },
895    /// `LinkPGCN pgcn` — switch to a different PGC by number.
896    Pgcn { pgcn: u16 },
897    /// `LinkPTTN pttn` — switch to a specific PTT (chapter) by
898    /// number.
899    Pttn { pttn: u16, hl_bn: u8 },
900    /// `LinkPGN pgn` — switch to a specific PG (program) by number
901    /// inside the current PGC.
902    Pgn { pgn: u8, hl_bn: u8 },
903    /// `LinkCN cn` — switch to a specific Cell by number inside the
904    /// current PGC.
905    Cn { cn: u8, hl_bn: u8 },
906}
907
908/// Saved playback location pushed by `CallSS` and popped by `RSM`.
909///
910/// The spec page lets `CallSS` optionally name a *different* resume
911/// cell than the one that was active at call time — when the field
912/// is non-zero the engine resumes to that cell index instead of the
913/// caller's. We preserve it verbatim so the engine can decide.
914#[derive(Debug, Clone, Copy, PartialEq, Eq)]
915pub struct ResumePoint {
916    /// The cell index to resume to (0 = "the cell that was active
917    /// when CallSS fired" — the engine consults its own bookkeeping).
918    pub resume_cell: u8,
919    /// The highlight-button override carried by the matching `RSM`
920    /// subset word (byte 6 bits 5..0 in the link-subset encoding).
921    pub hl_btn: u8,
922}
923
924// =====================================================================
925// Vm — the interpreter.
926// =====================================================================
927
928/// Maximum depth of the call/resume stack. The spec page doesn't
929/// publish a hard bound; commercial discs are routinely seen with
930/// 1–2 simultaneous CallSS frames (Menu Call into a PGC that itself
931/// CallSS's a sub-menu). 8 is a comfortable bound that detects
932/// runaway nesting without restricting real content.
933pub const MAX_RSM_DEPTH: usize = 8;
934
935/// DVD-Video VM interpreter — owns the register file + RSM stack
936/// + the per-list program counter.
937///
938/// The interpreter is intentionally **single-list-scoped**: one
939/// instance covers one pre / post / cell command list at a time, with
940/// the PC indexing into the originating [`Vec<NavCommand>`]. A
941/// playback engine instantiates a new [`Vm`] (or rewinds an existing
942/// one) for each transition between lists. The persistent state that
943/// outlives a list — GPRMs, SPRMs, counter modes, RSM stack — lives
944/// on the [`Vm`] and survives `run_list` calls.
945#[derive(Debug, Clone, Default)]
946pub struct Vm {
947    /// Mutable register file (GPRMs + SPRMs + counter-mode bits).
948    pub regs: RegisterFile,
949    /// CallSS resume stack. Top of stack is the most-recently-pushed
950    /// frame.
951    rsm_stack: Vec<ResumePoint>,
952    /// Program counter inside the currently-running list. Bumped by
953    /// `Continue` / explicit `Goto`. Cleared between `run_list`
954    /// invocations.
955    pc: usize,
956}
957
958impl Vm {
959    /// Construct a fresh VM with the spec-defined SPRM defaults.
960    pub fn new() -> Self {
961        Self::default()
962    }
963
964    /// Borrow the current PC. Inside [`Vm::run_list`] this advances
965    /// instruction by instruction; outside, it's the PC at which the
966    /// last `run_list` either completed (`pc == list.len()`) or
967    /// terminated via `Break` / `Exit` / a transfer action.
968    pub fn pc(&self) -> usize {
969        self.pc
970    }
971
972    /// Reset the PC to `0`. Use when switching to a fresh command
973    /// list.
974    pub fn reset_pc(&mut self) {
975        self.pc = 0;
976    }
977
978    /// Push a CallSS resume frame. Returns `false` if the stack is
979    /// already at [`MAX_RSM_DEPTH`] — the call is dropped rather than
980    /// silently overflowing.
981    pub fn push_resume(&mut self, frame: ResumePoint) -> bool {
982        if self.rsm_stack.len() >= MAX_RSM_DEPTH {
983            false
984        } else {
985            self.rsm_stack.push(frame);
986            true
987        }
988    }
989
990    /// Pop the most-recent CallSS resume frame, or `None` when the
991    /// stack is empty (spec's "RSM with no matching CallSS" is a
992    /// no-op on real players).
993    pub fn pop_resume(&mut self) -> Option<ResumePoint> {
994        self.rsm_stack.pop()
995    }
996
997    /// Inspect the current resume-stack depth (testing convenience).
998    pub fn resume_depth(&self) -> usize {
999        self.rsm_stack.len()
1000    }
1001
1002    /// Evaluate a comparison predicate against two values.
1003    ///
1004    /// Per `mpucoder-vmi.html`'s "SET and CMP operations" table:
1005    /// `BC` is the bit-clear test `(lhs & rhs) == 0`; the named
1006    /// arithmetic predicates are unsigned 16-bit comparisons. The
1007    /// `None` predicate yields `true` so a "compare + something"
1008    /// encoding with no comparator runs the inner action
1009    /// unconditionally — that's how the spec page's compound rows
1010    /// describe their unconditional sub-cases.
1011    pub fn evaluate(cmp: CmpOp, lhs: u16, rhs: u16) -> bool {
1012        match cmp {
1013            CmpOp::None => true,
1014            CmpOp::Bc => (lhs & rhs) == 0,
1015            CmpOp::Eq => lhs == rhs,
1016            CmpOp::Ne => lhs != rhs,
1017            CmpOp::Ge => lhs >= rhs,
1018            CmpOp::Gt => lhs > rhs,
1019            CmpOp::Le => lhs <= rhs,
1020            CmpOp::Lt => lhs < rhs,
1021        }
1022    }
1023
1024    /// Apply a SET sub-op to `(dst, src)` and return the post-op
1025    /// destination value.
1026    ///
1027    /// Per `mpucoder-vmi.html`: the named arithmetic ops are unsigned
1028    /// 16-bit modular arithmetic; `Div` / `Mod` with a zero divisor
1029    /// leave the destination unchanged (the spec page doesn't define
1030    /// the result, and crashing the VM on a malformed disc is a
1031    /// worse outcome than no-op'ing the divide). `Swp` returns the
1032    /// new destination value; the caller is responsible for writing
1033    /// the swapped source back via [`Vm::set_swap_source`] when
1034    /// `Swp` was the op.
1035    pub fn apply_set(op: SetOp, dst: u16, src: u16) -> u16 {
1036        match op {
1037            SetOp::None => dst,
1038            SetOp::Mov => src,
1039            SetOp::Swp => src, // caller writes dst's old value into the source slot
1040            SetOp::Add => dst.wrapping_add(src),
1041            SetOp::Sub => dst.wrapping_sub(src),
1042            SetOp::Mul => dst.wrapping_mul(src),
1043            SetOp::Div => dst.checked_div(src).unwrap_or(dst),
1044            SetOp::Mod => dst.checked_rem(src).unwrap_or(dst),
1045            SetOp::Rnd => {
1046                // The spec page leaves the operand column blank for
1047                // `rnd`; treat the source as the upper bound of a
1048                // `[0, src)` half-open range. With `src == 0` we
1049                // leave the destination unchanged (same as div/mod
1050                // zero-divisor fallback). The interpreter has no
1051                // entropy source — a `0` placeholder is deterministic
1052                // and traceable; callers that need true randomness
1053                // wrap the VM and post-process this slot.
1054                if src == 0 {
1055                    dst
1056                } else {
1057                    0
1058                }
1059            }
1060            SetOp::And => dst & src,
1061            SetOp::Or => dst | src,
1062            SetOp::Xor => dst ^ src,
1063            SetOp::Invalid(_) => dst,
1064        }
1065    }
1066
1067    /// Execute one decoded [`NavInstruction`] and return its
1068    /// playback-engine-visible effect. The PC is **not** mutated by
1069    /// this method — [`Vm::run_list`] owns that bookkeeping so a
1070    /// caller that wants single-step debugging can re-use `step`
1071    /// directly.
1072    pub fn step(&mut self, ins: NavInstruction) -> VmAction {
1073        match ins {
1074            // -------- Type 0 ----------------------------------------
1075            NavInstruction::Nop => VmAction::Continue,
1076            NavInstruction::Goto { line: _ } => {
1077                // Goto is resolved by run_list (the PC index is the
1078                // 1-based line number); a bare step() call doesn't
1079                // own the PC and treats Goto as a Continue. run_list
1080                // intercepts before this point.
1081                VmAction::Continue
1082            }
1083            NavInstruction::Break => VmAction::Break,
1084            NavInstruction::SetTmpPml { level, line: _ } => {
1085                // SetTmpPML asks the player to set a *temporary*
1086                // parental level — distinct from SPRM 13 (the
1087                // persistent level). We don't model the password
1088                // workflow at this layer; record the request on
1089                // SPRM 13 as a best-effort and continue. (The spec
1090                // page describes the password / approval flow as
1091                // player-policy; the VM word itself only carries the
1092                // proposed level.)
1093                self.regs.set_sprm(SPRM_PARENTAL_LEVEL, u16::from(level));
1094                VmAction::Continue
1095            }
1096
1097            // -------- Type 1 — link family --------------------------
1098            NavInstruction::LinkSub { subset, hl_bn } => match subset {
1099                LinkSubset::Rsm => match self.pop_resume() {
1100                    Some(mut rp) => {
1101                        rp.hl_btn = hl_bn;
1102                        VmAction::Resume(rp)
1103                    }
1104                    None => VmAction::Continue,
1105                },
1106                _ => VmAction::Link(LinkAction::Subset { subset, hl_bn }),
1107            },
1108            NavInstruction::LinkPgcn { pgcn } => VmAction::Link(LinkAction::Pgcn { pgcn }),
1109            NavInstruction::LinkPttn { pttn, hl_bn } => {
1110                VmAction::Link(LinkAction::Pttn { pttn, hl_bn })
1111            }
1112            NavInstruction::LinkPgn { pgn, hl_bn } => {
1113                VmAction::Link(LinkAction::Pgn { pgn, hl_bn })
1114            }
1115            NavInstruction::LinkCn { cn, hl_bn } => VmAction::Link(LinkAction::Cn { cn, hl_bn }),
1116
1117            // -------- Type 1 — jump / call family -------------------
1118            NavInstruction::Exit => VmAction::Exit,
1119            NavInstruction::JumpTT { ttn } => VmAction::JumpTitle { ttn },
1120            NavInstruction::JumpVtsTt { ttn } => VmAction::JumpVtsTitle { ttn },
1121            NavInstruction::JumpVtsPtt { ttn, pttn } => VmAction::JumpVtsPtt { ttn, pttn },
1122            NavInstruction::JumpSs(t) => VmAction::JumpSs(t),
1123            NavInstruction::CallSs(t) => {
1124                let rsm_cell = match t {
1125                    CallSSTarget::FirstPlay { rsm_cell } => rsm_cell,
1126                    CallSSTarget::VmgmMenu { rsm_cell, .. } => rsm_cell,
1127                    CallSSTarget::VtsmMenu { rsm_cell, .. } => rsm_cell,
1128                    CallSSTarget::VmgmPgcn { rsm_cell, .. } => rsm_cell,
1129                };
1130                let _pushed = self.push_resume(ResumePoint {
1131                    resume_cell: rsm_cell,
1132                    hl_btn: 0,
1133                });
1134                VmAction::CallSs(t)
1135            }
1136
1137            // -------- Type 2 — SetSystem family ---------------------
1138            NavInstruction::SetStn {
1139                direct,
1140                af,
1141                audio_src,
1142                sf,
1143                subpic_src,
1144                nf,
1145                angle_src,
1146            } => {
1147                // Register form reads from G<src>; immediate form
1148                // uses the 7-bit literal directly. The spec page
1149                // makes the per-flag application order indifferent
1150                // (flags are independent).
1151                if af {
1152                    let v = if direct {
1153                        u16::from(audio_src)
1154                    } else {
1155                        self.regs.gprm(audio_src)
1156                    };
1157                    self.regs.set_sprm(SPRM_AUDIO_STREAM, v);
1158                }
1159                if sf {
1160                    let v = if direct {
1161                        u16::from(subpic_src)
1162                    } else {
1163                        self.regs.gprm(subpic_src)
1164                    };
1165                    self.regs.set_sprm(SPRM_SUBPICTURE_STREAM, v);
1166                }
1167                if nf {
1168                    let v = if direct {
1169                        u16::from(angle_src)
1170                    } else {
1171                        self.regs.gprm(angle_src)
1172                    };
1173                    self.regs.set_sprm(SPRM_ANGLE, v);
1174                }
1175                VmAction::Continue
1176            }
1177            NavInstruction::SetNvtmr { src, pgcn } => {
1178                let seconds = self.regs.read_operand(src);
1179                self.regs.set_sprm(SPRM_NV_TIMER, seconds);
1180                self.regs.set_sprm(SPRM_NV_PGCN, pgcn);
1181                VmAction::SetNavTimer { seconds, pgcn }
1182            }
1183            NavInstruction::SetGprmMd { src, dst, counter } => {
1184                let v = self.regs.read_operand(src);
1185                if let Register::Gprm(i) = dst {
1186                    self.regs.set_gprm(i, v);
1187                    self.regs.set_counter_mode(i, counter);
1188                }
1189                VmAction::Continue
1190            }
1191            NavInstruction::SetAmxMd { src } => {
1192                let v = self.regs.read_operand(src);
1193                self.regs.set_sprm(SPRM_AMXMD, v);
1194                VmAction::Continue
1195            }
1196            NavInstruction::SetHlBtnn { src } => {
1197                let v = self.regs.read_operand(src);
1198                self.regs.set_sprm(SPRM_HL_BTNN, v);
1199                VmAction::Continue
1200            }
1201
1202            // -------- Type 3 — Set arithmetic -----------------------
1203            NavInstruction::Set { op, dst, src } => {
1204                if let Register::Gprm(i) = dst {
1205                    let cur = self.regs.gprm(i);
1206                    let rhs = self.regs.read_operand(src);
1207                    let new = Self::apply_set(op, cur, rhs);
1208                    self.regs.set_gprm(i, new);
1209                    // Swp also writes the swapped value back into
1210                    // the source slot when the source was a register.
1211                    if matches!(op, SetOp::Swp) {
1212                        if let Operand::Register(Register::Gprm(j)) = src {
1213                            self.regs.set_gprm(j, cur);
1214                        }
1215                    }
1216                }
1217                VmAction::Continue
1218            }
1219
1220            // -------- Type 4..6 — compound CMP/SET/LNK families -----
1221            //
1222            // The decoder now carries the full operand fields for
1223            // the compound forms, so the executor performs the
1224            // implied SET + CMP + LINK sequence in spec order per
1225            // `mpucoder-vmi-sum.html`:
1226            //
1227            //   Type 4 SetCLnk  : (1) SET; (2) CMP; (3) Link on true.
1228            //   Type 5 CSetCLnk : (1) CMP; on true → (2) SET, (3) Link.
1229            //   Type 6 CmpSetLnk: (1) CMP; on true → (2) SET; (3) Link
1230            //                     unconditionally.
1231            //
1232            // A failing compare in Type 4 / 5 returns `Continue` so
1233            // the outer command list keeps walking; Type 6 always
1234            // surfaces the Link target (because its Link runs
1235            // regardless of the CMP outcome — that's what
1236            // distinguishes it from Type 5).
1237            NavInstruction::SetCLnk {
1238                set_op,
1239                cmp_op,
1240                scr,
1241                set_src,
1242                cmp_rhs,
1243                hl_bn,
1244                link,
1245            } => self.exec_set_clnk(set_op, cmp_op, scr, set_src, cmp_rhs, hl_bn, link),
1246
1247            NavInstruction::CSetCLnk {
1248                set_op,
1249                cmp_op,
1250                sr1,
1251                set_src,
1252                cmp_lhs,
1253                cmp_rhs,
1254                hl_bn,
1255                link,
1256            } => self.exec_cset_clnk(set_op, cmp_op, sr1, set_src, cmp_lhs, cmp_rhs, hl_bn, link),
1257
1258            NavInstruction::CmpSetLnk {
1259                set_op,
1260                cmp_op,
1261                sr1,
1262                set_src,
1263                cmp_rhs,
1264                hl_bn,
1265                link,
1266            } => self.exec_cmp_set_lnk(set_op, cmp_op, sr1, set_src, cmp_rhs, hl_bn, link),
1267
1268            // -------- Type 7 / red rows -----------------------------
1269            NavInstruction::Unknown | NavInstruction::Invalid => {
1270                VmAction::NoOpRaw(NavCommand::default())
1271            }
1272        }
1273    }
1274
1275    // ---- Type 4..6 compound execution helpers --------------------------
1276    //
1277    // All three helpers funnel into [`Vm::fire_link`] for the final
1278    // step: turn the Link subset into the corresponding `VmAction`
1279    // (Link / Continue / Resume), honouring the spec's RSM-pops-stack
1280    // semantics inside compound bodies as well.
1281
1282    /// Resolve a Link-subset code into the appropriate VM action.
1283    ///
1284    /// `LinkSubset::Nop` collapses to `Continue` (the compound body
1285    /// ran but its tail Link was a no-op); `LinkSubset::Rsm` pops the
1286    /// RSM stack just as a bare Type-1 `LinkSub` would; the 11
1287    /// remaining named subsets become `VmAction::Link(Subset { … })`.
1288    /// `LinkSubset::Invalid(_)` falls through to `Continue` so a
1289    /// malformed disc cannot crash the player.
1290    fn fire_link(&mut self, link: LinkSubset, hl_bn: u8) -> VmAction {
1291        match link {
1292            LinkSubset::Nop => VmAction::Continue,
1293            LinkSubset::Rsm => match self.pop_resume() {
1294                Some(mut rp) => {
1295                    rp.hl_btn = hl_bn;
1296                    VmAction::Resume(rp)
1297                }
1298                None => VmAction::Continue,
1299            },
1300            LinkSubset::Invalid(_) => VmAction::Continue,
1301            _ => VmAction::Link(LinkAction::Subset {
1302                subset: link,
1303                hl_bn,
1304            }),
1305        }
1306    }
1307
1308    /// Apply a SET sub-op against `dst`, writing the result back and
1309    /// handling the `Swp` cooperative write-back. `dst` must be a
1310    /// GPRM — non-GPRM destinations (SPRM / Invalid) silently no-op
1311    /// per the spec page's "compound SET writes a GPRM" wording.
1312    fn apply_set_to_register(&mut self, op: SetOp, dst: Register, src: Operand) {
1313        let Register::Gprm(i) = dst else {
1314            return;
1315        };
1316        let cur = self.regs.gprm(i);
1317        let rhs = self.regs.read_operand(src);
1318        let new = Self::apply_set(op, cur, rhs);
1319        self.regs.set_gprm(i, new);
1320        if matches!(op, SetOp::Swp) {
1321            if let Operand::Register(Register::Gprm(j)) = src {
1322                self.regs.set_gprm(j, cur);
1323            }
1324        }
1325    }
1326
1327    /// Type 4 — `SetCLnk`: SET first, then CMP, then Link if the
1328    /// compare succeeded. The CMP uses the post-SET value of `scr`.
1329    #[allow(clippy::too_many_arguments)]
1330    fn exec_set_clnk(
1331        &mut self,
1332        set_op: SetOp,
1333        cmp_op: CmpOp,
1334        scr: Register,
1335        set_src: Operand,
1336        cmp_rhs: Operand,
1337        hl_bn: u8,
1338        link: LinkSubset,
1339    ) -> VmAction {
1340        // (1) SET — only fires if `set_op` is a real op; `None` makes
1341        // the family collapse into a plain compare-link.
1342        if !matches!(set_op, SetOp::None | SetOp::Invalid(_)) {
1343            self.apply_set_to_register(set_op, scr, set_src);
1344        }
1345        // (2) CMP against the post-SET value of `scr`.
1346        let lhs = self.regs.read(scr);
1347        let rhs = self.regs.read_operand(cmp_rhs);
1348        if Self::evaluate(cmp_op, lhs, rhs) {
1349            // (3) Link on true.
1350            self.fire_link(link, hl_bn)
1351        } else {
1352            VmAction::Continue
1353        }
1354    }
1355
1356    /// Type 5 — `CSetCLnk`: CMP first; on true, SET then Link.
1357    #[allow(clippy::too_many_arguments)]
1358    fn exec_cset_clnk(
1359        &mut self,
1360        set_op: SetOp,
1361        cmp_op: CmpOp,
1362        sr1: Register,
1363        set_src: Operand,
1364        cmp_lhs: Register,
1365        cmp_rhs: Operand,
1366        hl_bn: u8,
1367        link: LinkSubset,
1368    ) -> VmAction {
1369        // (1) CMP.
1370        let lhs = self.regs.read(cmp_lhs);
1371        let rhs = self.regs.read_operand(cmp_rhs);
1372        if !Self::evaluate(cmp_op, lhs, rhs) {
1373            return VmAction::Continue;
1374        }
1375        // (2) SET on the true branch.
1376        if !matches!(set_op, SetOp::None | SetOp::Invalid(_)) {
1377            self.apply_set_to_register(set_op, sr1, set_src);
1378        }
1379        // (3) Link on the true branch.
1380        self.fire_link(link, hl_bn)
1381    }
1382
1383    /// Type 6 — `CmpSetLnk`: CMP first; on true, SET; then Link
1384    /// **unconditionally**. The CMP outcome only gates the SET, not
1385    /// the Link — that's how Type 6 differs from Type 5 per
1386    /// `mpucoder-vmi-sum.html`.
1387    #[allow(clippy::too_many_arguments)]
1388    fn exec_cmp_set_lnk(
1389        &mut self,
1390        set_op: SetOp,
1391        cmp_op: CmpOp,
1392        sr1: Register,
1393        set_src: Operand,
1394        cmp_rhs: Operand,
1395        hl_bn: u8,
1396        link: LinkSubset,
1397    ) -> VmAction {
1398        // (1) CMP — uses the pre-SET value of `sr1`.
1399        let lhs = self.regs.read(sr1);
1400        let rhs = self.regs.read_operand(cmp_rhs);
1401        if Self::evaluate(cmp_op, lhs, rhs) {
1402            // (2) SET on the true branch.
1403            if !matches!(set_op, SetOp::None | SetOp::Invalid(_)) {
1404                self.apply_set_to_register(set_op, sr1, set_src);
1405            }
1406        }
1407        // (3) Link — always.
1408        self.fire_link(link, hl_bn)
1409    }
1410
1411    /// Walk a pre / post / cell command list end-to-end.
1412    ///
1413    /// The PC starts at `0` (callers can pre-set via [`Vm::reset_pc`]
1414    /// or by mutating the field) and advances by 1 after each
1415    /// [`VmAction::Continue`]. Intra-list `Goto` lands the PC on
1416    /// the spec-defined 1-based line number; `Break` / `Exit` /
1417    /// every transfer action returns immediately.
1418    ///
1419    /// Returns `(VmAction, pc)` — the action that terminated the
1420    /// walk plus the PC at termination. A walk that runs off the
1421    /// end of the list returns `(VmAction::Continue, list.len())` so
1422    /// the caller can tell "completed cleanly" from "transferred
1423    /// early".
1424    pub fn run_list(&mut self, list: &[NavCommand]) -> (VmAction, usize) {
1425        self.pc = 0;
1426        // A bounded step budget defends against pathological encoded
1427        // loops (`Goto 1` from line 1, etc.). 128 × 16 = 2048 steps
1428        // is comfortably above the spec's "≤ 128 commands per list"
1429        // bound while still terminating in O(disc-size).
1430        let budget = list.len().saturating_mul(16).max(256);
1431        let mut spent = 0usize;
1432        while self.pc < list.len() && spent < budget {
1433            spent += 1;
1434            let ins = list[self.pc].decode();
1435            match ins {
1436                NavInstruction::Goto { line } => {
1437                    // Spec page: `line` is 1-based; line 1 = first
1438                    // command in the list. A `0` or out-of-range
1439                    // target falls through to the end of the list
1440                    // (treated as a clean completion).
1441                    let idx = (line as usize).saturating_sub(1);
1442                    if idx >= list.len() {
1443                        self.pc = list.len();
1444                    } else {
1445                        self.pc = idx;
1446                    }
1447                    continue;
1448                }
1449                other => {
1450                    let action = self.step(other);
1451                    match action {
1452                        VmAction::Continue => {
1453                            self.pc += 1;
1454                        }
1455                        _ => return (action, self.pc),
1456                    }
1457                }
1458            }
1459        }
1460        // Either we ran off the end (clean completion) or we hit the
1461        // step budget (pathological loop). Either way the caller
1462        // sees a clean Continue back at list.len() — they can detect
1463        // the loop case via the budget if they care.
1464        (VmAction::Continue, self.pc)
1465    }
1466}
1467
1468// =====================================================================
1469// Tests.
1470// =====================================================================
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475    use crate::ifo::NavCommand;
1476    use crate::nav::{CmpOp, JumpSSTarget, LinkSubset, NavInstruction, Operand, Register, SetOp};
1477
1478    // -----------------------------------------------------------------
1479    // Register file.
1480    // -----------------------------------------------------------------
1481
1482    #[test]
1483    fn register_file_default_matches_spec_defaults() {
1484        let r = RegisterFile::new();
1485        // All GPRMs cleared.
1486        for i in 0..GPRM_COUNT {
1487            assert_eq!(r.gprm(i as u8), 0);
1488        }
1489        // Spec-defaulted SPRMs.
1490        assert_eq!(r.sprm(SPRM_AUDIO_STREAM), 15);
1491        assert_eq!(r.sprm(SPRM_SUBPICTURE_STREAM), 62);
1492        assert_eq!(r.sprm(SPRM_ANGLE), 1);
1493        assert_eq!(r.sprm(SPRM_TITLE), 1);
1494        assert_eq!(r.sprm(SPRM_VTS_TITLE), 1);
1495        assert_eq!(r.sprm(SPRM_PTT), 1);
1496        assert_eq!(r.sprm(SPRM_HL_BTNN), 1 << 10);
1497        assert_eq!(r.sprm(SPRM_NV_TIMER), 0);
1498        assert_eq!(r.sprm(16), 0xFFFF);
1499        assert_eq!(r.sprm(18), 0xFFFF);
1500    }
1501
1502    #[test]
1503    fn register_out_of_range_indexing_returns_zero() {
1504        let r = RegisterFile::new();
1505        assert_eq!(r.gprm(99), 0);
1506        assert_eq!(r.sprm(99), 0);
1507        assert_eq!(r.read(Register::Invalid(0x7F)), 0);
1508    }
1509
1510    #[test]
1511    fn register_read_operand_dispatch() {
1512        let mut r = RegisterFile::new();
1513        r.set_gprm(3, 0xCAFE);
1514        r.set_sprm(SPRM_HL_BTNN, 0x0800);
1515        assert_eq!(r.read_operand(Operand::Register(Register::Gprm(3))), 0xCAFE);
1516        assert_eq!(
1517            r.read_operand(Operand::Register(Register::Sprm(SPRM_HL_BTNN))),
1518            0x0800
1519        );
1520        assert_eq!(r.read_operand(Operand::Immediate(0xBEEF)), 0xBEEF);
1521    }
1522
1523    #[test]
1524    fn counter_mode_flag_round_trips() {
1525        let mut r = RegisterFile::new();
1526        assert!(!r.counter_mode(5));
1527        r.set_counter_mode(5, true);
1528        assert!(r.counter_mode(5));
1529        assert!(!r.counter_mode(6));
1530        r.set_counter_mode(5, false);
1531        assert!(!r.counter_mode(5));
1532    }
1533
1534    #[test]
1535    fn tick_counters_advances_only_counter_mode_gprms() {
1536        let mut r = RegisterFile::new();
1537        r.set_gprm(0, 100);
1538        r.set_gprm(1, 100);
1539        r.set_counter_mode(1, true);
1540        r.tick_counters(5);
1541        assert_eq!(r.gprm(0), 100);
1542        assert_eq!(r.gprm(1), 105);
1543    }
1544
1545    #[test]
1546    fn tick_counters_saturates_at_u16_max() {
1547        let mut r = RegisterFile::new();
1548        r.set_gprm(2, u16::MAX - 3);
1549        r.set_counter_mode(2, true);
1550        r.tick_counters(10);
1551        assert_eq!(r.gprm(2), u16::MAX);
1552    }
1553
1554    // -----------------------------------------------------------------
1555    // Comparison evaluator (covers every CmpOp variant).
1556    // -----------------------------------------------------------------
1557
1558    #[test]
1559    fn evaluate_covers_every_cmp_op() {
1560        assert!(Vm::evaluate(CmpOp::None, 0, 0));
1561        assert!(Vm::evaluate(CmpOp::Bc, 0xF0, 0x0F)); // disjoint bit-sets
1562        assert!(!Vm::evaluate(CmpOp::Bc, 0xF0, 0x10)); // overlapping bit
1563        assert!(Vm::evaluate(CmpOp::Eq, 5, 5));
1564        assert!(!Vm::evaluate(CmpOp::Eq, 5, 4));
1565        assert!(Vm::evaluate(CmpOp::Ne, 5, 4));
1566        assert!(!Vm::evaluate(CmpOp::Ne, 5, 5));
1567        assert!(Vm::evaluate(CmpOp::Ge, 5, 5));
1568        assert!(Vm::evaluate(CmpOp::Ge, 6, 5));
1569        assert!(!Vm::evaluate(CmpOp::Ge, 4, 5));
1570        assert!(Vm::evaluate(CmpOp::Gt, 6, 5));
1571        assert!(!Vm::evaluate(CmpOp::Gt, 5, 5));
1572        assert!(Vm::evaluate(CmpOp::Le, 5, 5));
1573        assert!(Vm::evaluate(CmpOp::Le, 4, 5));
1574        assert!(!Vm::evaluate(CmpOp::Le, 6, 5));
1575        assert!(Vm::evaluate(CmpOp::Lt, 4, 5));
1576        assert!(!Vm::evaluate(CmpOp::Lt, 5, 5));
1577    }
1578
1579    // -----------------------------------------------------------------
1580    // SET sub-op application.
1581    // -----------------------------------------------------------------
1582
1583    #[test]
1584    fn apply_set_covers_named_arithmetic_ops() {
1585        assert_eq!(Vm::apply_set(SetOp::None, 5, 99), 5);
1586        assert_eq!(Vm::apply_set(SetOp::Mov, 5, 99), 99);
1587        assert_eq!(Vm::apply_set(SetOp::Add, 5, 3), 8);
1588        assert_eq!(Vm::apply_set(SetOp::Sub, 5, 3), 2);
1589        assert_eq!(Vm::apply_set(SetOp::Mul, 5, 3), 15);
1590        assert_eq!(Vm::apply_set(SetOp::Div, 14, 3), 4);
1591        assert_eq!(Vm::apply_set(SetOp::Mod, 14, 3), 2);
1592        assert_eq!(Vm::apply_set(SetOp::And, 0xF0F0, 0x0FF0), 0x00F0);
1593        assert_eq!(Vm::apply_set(SetOp::Or, 0xF000, 0x000F), 0xF00F);
1594        assert_eq!(Vm::apply_set(SetOp::Xor, 0xFF00, 0x0FF0), 0xF0F0);
1595    }
1596
1597    #[test]
1598    fn apply_set_handles_zero_divisor_safely() {
1599        assert_eq!(Vm::apply_set(SetOp::Div, 5, 0), 5);
1600        assert_eq!(Vm::apply_set(SetOp::Mod, 5, 0), 5);
1601        assert_eq!(Vm::apply_set(SetOp::Rnd, 5, 0), 5);
1602    }
1603
1604    #[test]
1605    fn apply_set_swp_returns_src_caller_writes_dst_back() {
1606        // `Swp` is intentionally cooperative; the helper returns the
1607        // value the destination should take, the executor stamps the
1608        // source-side register with the old dst value.
1609        assert_eq!(Vm::apply_set(SetOp::Swp, 5, 99), 99);
1610    }
1611
1612    #[test]
1613    fn apply_set_arithmetic_overflow_wraps() {
1614        assert_eq!(Vm::apply_set(SetOp::Add, u16::MAX, 1), 0);
1615        assert_eq!(Vm::apply_set(SetOp::Sub, 0, 1), u16::MAX);
1616        assert_eq!(Vm::apply_set(SetOp::Mul, 0x0100, 0x0100), 0); // 1<<16 wraps
1617    }
1618
1619    #[test]
1620    fn apply_set_invalid_sub_op_is_noop() {
1621        assert_eq!(Vm::apply_set(SetOp::Invalid(0x0C), 5, 99), 5);
1622    }
1623
1624    // -----------------------------------------------------------------
1625    // step() — per-instruction dispatch.
1626    // -----------------------------------------------------------------
1627
1628    #[test]
1629    fn step_nop_continues() {
1630        let mut vm = Vm::new();
1631        assert_eq!(vm.step(NavInstruction::Nop), VmAction::Continue);
1632    }
1633
1634    #[test]
1635    fn step_break_and_exit_terminate() {
1636        let mut vm = Vm::new();
1637        assert_eq!(vm.step(NavInstruction::Break), VmAction::Break);
1638        assert_eq!(vm.step(NavInstruction::Exit), VmAction::Exit);
1639    }
1640
1641    #[test]
1642    fn step_set_writes_gprm_via_mov() {
1643        let mut vm = Vm::new();
1644        vm.step(NavInstruction::Set {
1645            op: SetOp::Mov,
1646            dst: Register::Gprm(4),
1647            src: Operand::Immediate(0x1234),
1648        });
1649        assert_eq!(vm.regs.gprm(4), 0x1234);
1650    }
1651
1652    #[test]
1653    fn step_set_arithmetic_chain_through_gprm() {
1654        let mut vm = Vm::new();
1655        vm.regs.set_gprm(0, 10);
1656        vm.step(NavInstruction::Set {
1657            op: SetOp::Add,
1658            dst: Register::Gprm(0),
1659            src: Operand::Immediate(5),
1660        });
1661        assert_eq!(vm.regs.gprm(0), 15);
1662        vm.step(NavInstruction::Set {
1663            op: SetOp::Mul,
1664            dst: Register::Gprm(0),
1665            src: Operand::Register(Register::Gprm(0)),
1666        });
1667        assert_eq!(vm.regs.gprm(0), 225);
1668    }
1669
1670    #[test]
1671    fn step_set_swp_exchanges_two_gprms() {
1672        let mut vm = Vm::new();
1673        vm.regs.set_gprm(1, 0xAAAA);
1674        vm.regs.set_gprm(2, 0x5555);
1675        vm.step(NavInstruction::Set {
1676            op: SetOp::Swp,
1677            dst: Register::Gprm(1),
1678            src: Operand::Register(Register::Gprm(2)),
1679        });
1680        assert_eq!(vm.regs.gprm(1), 0x5555);
1681        assert_eq!(vm.regs.gprm(2), 0xAAAA);
1682    }
1683
1684    #[test]
1685    fn step_setstn_honours_per_flag_application() {
1686        let mut vm = Vm::new();
1687        // Direct form: af set, sf cleared, nf set. SPRM 2 (subpic)
1688        // must remain at default.
1689        vm.step(NavInstruction::SetStn {
1690            direct: true,
1691            af: true,
1692            audio_src: 4,
1693            sf: false,
1694            subpic_src: 7,
1695            nf: true,
1696            angle_src: 3,
1697        });
1698        assert_eq!(vm.regs.sprm(SPRM_AUDIO_STREAM), 4);
1699        assert_eq!(vm.regs.sprm(SPRM_SUBPICTURE_STREAM), 62); // default
1700        assert_eq!(vm.regs.sprm(SPRM_ANGLE), 3);
1701    }
1702
1703    #[test]
1704    fn step_setnvtmr_loads_timer_pair_and_surfaces_action() {
1705        let mut vm = Vm::new();
1706        let act = vm.step(NavInstruction::SetNvtmr {
1707            src: Operand::Immediate(120),
1708            pgcn: 42,
1709        });
1710        assert_eq!(
1711            act,
1712            VmAction::SetNavTimer {
1713                seconds: 120,
1714                pgcn: 42,
1715            }
1716        );
1717        assert_eq!(vm.regs.sprm(SPRM_NV_TIMER), 120);
1718        assert_eq!(vm.regs.sprm(SPRM_NV_PGCN), 42);
1719    }
1720
1721    #[test]
1722    fn step_setgprmmd_with_counter_flag_toggles_mode_bit() {
1723        let mut vm = Vm::new();
1724        vm.step(NavInstruction::SetGprmMd {
1725            src: Operand::Immediate(99),
1726            dst: Register::Gprm(7),
1727            counter: true,
1728        });
1729        assert_eq!(vm.regs.gprm(7), 99);
1730        assert!(vm.regs.counter_mode(7));
1731        // Toggling off via a counter=false update.
1732        vm.step(NavInstruction::SetGprmMd {
1733            src: Operand::Immediate(0),
1734            dst: Register::Gprm(7),
1735            counter: false,
1736        });
1737        assert!(!vm.regs.counter_mode(7));
1738    }
1739
1740    #[test]
1741    fn step_sethlbtnn_writes_sprm8() {
1742        let mut vm = Vm::new();
1743        vm.step(NavInstruction::SetHlBtnn {
1744            src: Operand::Immediate(0x0C00),
1745        });
1746        assert_eq!(vm.regs.sprm(SPRM_HL_BTNN), 0x0C00);
1747    }
1748
1749    #[test]
1750    fn step_set_tmp_pml_writes_sprm13() {
1751        let mut vm = Vm::new();
1752        vm.step(NavInstruction::SetTmpPml { level: 7, line: 0 });
1753        assert_eq!(vm.regs.sprm(SPRM_PARENTAL_LEVEL), 7);
1754    }
1755
1756    // -----------------------------------------------------------------
1757    // step() — Link / Jump / Call surfaces actions.
1758    // -----------------------------------------------------------------
1759
1760    #[test]
1761    fn step_link_subset_surfaces_link_action() {
1762        let mut vm = Vm::new();
1763        let a = vm.step(NavInstruction::LinkSub {
1764            subset: LinkSubset::LinkNextPG,
1765            hl_bn: 3,
1766        });
1767        assert_eq!(
1768            a,
1769            VmAction::Link(LinkAction::Subset {
1770                subset: LinkSubset::LinkNextPG,
1771                hl_bn: 3,
1772            })
1773        );
1774    }
1775
1776    #[test]
1777    fn step_link_pgcn_pttn_pgn_cn_surface_named_targets() {
1778        let mut vm = Vm::new();
1779        assert_eq!(
1780            vm.step(NavInstruction::LinkPgcn { pgcn: 0x1234 }),
1781            VmAction::Link(LinkAction::Pgcn { pgcn: 0x1234 })
1782        );
1783        assert_eq!(
1784            vm.step(NavInstruction::LinkPttn { pttn: 5, hl_bn: 1 }),
1785            VmAction::Link(LinkAction::Pttn { pttn: 5, hl_bn: 1 })
1786        );
1787        assert_eq!(
1788            vm.step(NavInstruction::LinkPgn { pgn: 9, hl_bn: 2 }),
1789            VmAction::Link(LinkAction::Pgn { pgn: 9, hl_bn: 2 })
1790        );
1791        assert_eq!(
1792            vm.step(NavInstruction::LinkCn { cn: 11, hl_bn: 4 }),
1793            VmAction::Link(LinkAction::Cn { cn: 11, hl_bn: 4 })
1794        );
1795    }
1796
1797    #[test]
1798    fn step_jump_family_surfaces_typed_actions() {
1799        let mut vm = Vm::new();
1800        assert_eq!(
1801            vm.step(NavInstruction::JumpTT { ttn: 7 }),
1802            VmAction::JumpTitle { ttn: 7 }
1803        );
1804        assert_eq!(
1805            vm.step(NavInstruction::JumpVtsTt { ttn: 8 }),
1806            VmAction::JumpVtsTitle { ttn: 8 }
1807        );
1808        assert_eq!(
1809            vm.step(NavInstruction::JumpVtsPtt { ttn: 9, pttn: 4 }),
1810            VmAction::JumpVtsPtt { ttn: 9, pttn: 4 }
1811        );
1812        assert_eq!(
1813            vm.step(NavInstruction::JumpSs(JumpSSTarget::FirstPlay)),
1814            VmAction::JumpSs(JumpSSTarget::FirstPlay)
1815        );
1816    }
1817
1818    #[test]
1819    fn step_callss_pushes_resume_then_rsm_pops_it() {
1820        let mut vm = Vm::new();
1821        assert_eq!(vm.resume_depth(), 0);
1822        let _action = vm.step(NavInstruction::CallSs(CallSSTarget::FirstPlay {
1823            rsm_cell: 7,
1824        }));
1825        assert_eq!(vm.resume_depth(), 1);
1826        let act = vm.step(NavInstruction::LinkSub {
1827            subset: LinkSubset::Rsm,
1828            hl_bn: 5,
1829        });
1830        assert_eq!(
1831            act,
1832            VmAction::Resume(ResumePoint {
1833                resume_cell: 7,
1834                hl_btn: 5,
1835            })
1836        );
1837        assert_eq!(vm.resume_depth(), 0);
1838    }
1839
1840    #[test]
1841    fn step_rsm_with_empty_stack_is_continue() {
1842        let mut vm = Vm::new();
1843        let act = vm.step(NavInstruction::LinkSub {
1844            subset: LinkSubset::Rsm,
1845            hl_bn: 0,
1846        });
1847        assert_eq!(act, VmAction::Continue);
1848        assert_eq!(vm.resume_depth(), 0);
1849    }
1850
1851    #[test]
1852    fn step_callss_stack_depth_bounded_to_max_rsm_depth() {
1853        let mut vm = Vm::new();
1854        for _ in 0..(MAX_RSM_DEPTH + 4) {
1855            let _action = vm.step(NavInstruction::CallSs(CallSSTarget::FirstPlay {
1856                rsm_cell: 1,
1857            }));
1858        }
1859        assert_eq!(vm.resume_depth(), MAX_RSM_DEPTH);
1860    }
1861
1862    #[test]
1863    fn step_unknown_and_invalid_yield_noopraw() {
1864        let mut vm = Vm::new();
1865        let pre = vm.regs.clone();
1866        let a = vm.step(NavInstruction::Unknown);
1867        assert!(matches!(a, VmAction::NoOpRaw(_)));
1868        let b = vm.step(NavInstruction::Invalid);
1869        assert!(matches!(b, VmAction::NoOpRaw(_)));
1870        // No mutation on either path.
1871        assert_eq!(vm.regs.gprm(0), pre.gprm(0));
1872        assert_eq!(vm.regs.sprm(0), pre.sprm(0));
1873    }
1874
1875    // -----------------------------------------------------------------
1876    // Type 4..6 compound execution — SET / CMP / LINK sequencing.
1877    // -----------------------------------------------------------------
1878
1879    #[test]
1880    fn step_set_clnk_runs_set_then_compare_links_on_true() {
1881        // SetCLnk: G3 += 5; if (G3 == 10) Link LinkNextPG.
1882        // Set G3 = 5 first so post-SET G3 == 10.
1883        let mut vm = Vm::new();
1884        vm.regs.set_gprm(3, 5);
1885        let action = vm.step(NavInstruction::SetCLnk {
1886            set_op: SetOp::Add,
1887            cmp_op: CmpOp::Eq,
1888            scr: Register::Gprm(3),
1889            set_src: Operand::Immediate(5),
1890            cmp_rhs: Operand::Immediate(10),
1891            hl_bn: 2,
1892            link: LinkSubset::LinkNextPG,
1893        });
1894        assert_eq!(vm.regs.gprm(3), 10);
1895        assert_eq!(
1896            action,
1897            VmAction::Link(LinkAction::Subset {
1898                subset: LinkSubset::LinkNextPG,
1899                hl_bn: 2,
1900            })
1901        );
1902    }
1903
1904    #[test]
1905    fn step_set_clnk_runs_set_but_skips_link_on_false() {
1906        // SetCLnk: G3 += 1 (1 -> 2); if (G3 == 10) Link. Compare
1907        // fails; SET still ran, but no Link surfaces.
1908        let mut vm = Vm::new();
1909        vm.regs.set_gprm(3, 1);
1910        let action = vm.step(NavInstruction::SetCLnk {
1911            set_op: SetOp::Add,
1912            cmp_op: CmpOp::Eq,
1913            scr: Register::Gprm(3),
1914            set_src: Operand::Immediate(1),
1915            cmp_rhs: Operand::Immediate(10),
1916            hl_bn: 0,
1917            link: LinkSubset::LinkNextPG,
1918        });
1919        assert_eq!(vm.regs.gprm(3), 2);
1920        assert_eq!(action, VmAction::Continue);
1921    }
1922
1923    #[test]
1924    fn step_cset_clnk_runs_set_only_on_true() {
1925        // CSetCLnk: if (G7 == 9) { G3 = 99; Link LinkTopPGC }.
1926        // CMP true → SET runs + Link surfaces.
1927        let mut vm = Vm::new();
1928        vm.regs.set_gprm(7, 9);
1929        let action = vm.step(NavInstruction::CSetCLnk {
1930            set_op: SetOp::Mov,
1931            cmp_op: CmpOp::Eq,
1932            sr1: Register::Gprm(3),
1933            set_src: Operand::Immediate(99),
1934            cmp_lhs: Register::Gprm(7),
1935            cmp_rhs: Operand::Immediate(9),
1936            hl_bn: 0,
1937            link: LinkSubset::LinkTopPGC,
1938        });
1939        assert_eq!(vm.regs.gprm(3), 99);
1940        assert_eq!(
1941            action,
1942            VmAction::Link(LinkAction::Subset {
1943                subset: LinkSubset::LinkTopPGC,
1944                hl_bn: 0,
1945            })
1946        );
1947    }
1948
1949    #[test]
1950    fn step_cset_clnk_skips_set_and_link_on_false() {
1951        // CSetCLnk on false: neither SET nor LINK runs.
1952        let mut vm = Vm::new();
1953        vm.regs.set_gprm(7, 1);
1954        vm.regs.set_gprm(3, 42);
1955        let action = vm.step(NavInstruction::CSetCLnk {
1956            set_op: SetOp::Mov,
1957            cmp_op: CmpOp::Eq,
1958            sr1: Register::Gprm(3),
1959            set_src: Operand::Immediate(99),
1960            cmp_lhs: Register::Gprm(7),
1961            cmp_rhs: Operand::Immediate(9),
1962            hl_bn: 0,
1963            link: LinkSubset::LinkTopPGC,
1964        });
1965        // SET did not run — G3 still 42.
1966        assert_eq!(vm.regs.gprm(3), 42);
1967        assert_eq!(action, VmAction::Continue);
1968    }
1969
1970    #[test]
1971    fn step_cmp_set_lnk_links_unconditionally_even_on_false_cmp() {
1972        // CmpSetLnk on false: SET skipped, but LINK still fires (the
1973        // distinguishing semantic from Type 5).
1974        let mut vm = Vm::new();
1975        vm.regs.set_gprm(1, 1);
1976        let action = vm.step(NavInstruction::CmpSetLnk {
1977            set_op: SetOp::Mov,
1978            cmp_op: CmpOp::Eq,
1979            sr1: Register::Gprm(1),
1980            set_src: Operand::Immediate(99),
1981            cmp_rhs: Operand::Immediate(9),
1982            hl_bn: 5,
1983            link: LinkSubset::LinkNextPGC,
1984        });
1985        // SET skipped.
1986        assert_eq!(vm.regs.gprm(1), 1);
1987        // Link still fires.
1988        assert_eq!(
1989            action,
1990            VmAction::Link(LinkAction::Subset {
1991                subset: LinkSubset::LinkNextPGC,
1992                hl_bn: 5,
1993            })
1994        );
1995    }
1996
1997    #[test]
1998    fn step_cmp_set_lnk_runs_set_on_true_then_links() {
1999        // CmpSetLnk on true: SET runs then LINK fires.
2000        let mut vm = Vm::new();
2001        vm.regs.set_gprm(2, 7);
2002        let action = vm.step(NavInstruction::CmpSetLnk {
2003            set_op: SetOp::Add,
2004            cmp_op: CmpOp::Eq,
2005            sr1: Register::Gprm(2),
2006            set_src: Operand::Immediate(3),
2007            cmp_rhs: Operand::Immediate(7),
2008            hl_bn: 0,
2009            link: LinkSubset::LinkPrevPG,
2010        });
2011        assert_eq!(vm.regs.gprm(2), 10);
2012        assert_eq!(
2013            action,
2014            VmAction::Link(LinkAction::Subset {
2015                subset: LinkSubset::LinkPrevPG,
2016                hl_bn: 0,
2017            })
2018        );
2019    }
2020
2021    #[test]
2022    fn step_compound_with_link_nop_returns_continue() {
2023        // A compound whose Link subset is NOP collapses to Continue
2024        // even when the CMP succeeds (the compound body ran but its
2025        // tail Link is a literal NOP per the link-subset table).
2026        let mut vm = Vm::new();
2027        let action = vm.step(NavInstruction::CmpSetLnk {
2028            set_op: SetOp::None,
2029            cmp_op: CmpOp::None,
2030            sr1: Register::Gprm(0),
2031            set_src: Operand::Immediate(0),
2032            cmp_rhs: Operand::Immediate(0),
2033            hl_bn: 0,
2034            link: LinkSubset::Nop,
2035        });
2036        assert_eq!(action, VmAction::Continue);
2037    }
2038
2039    #[test]
2040    fn step_compound_with_link_rsm_pops_resume_stack() {
2041        // A compound's RSM Link variant pops the same RSM stack as a
2042        // bare Type-1 LinkSub Rsm. Push a frame, fire a Type-6
2043        // compound whose Link is Rsm, observe the Resume action.
2044        let mut vm = Vm::new();
2045        assert!(vm.push_resume(ResumePoint {
2046            resume_cell: 4,
2047            hl_btn: 0,
2048        }));
2049        let action = vm.step(NavInstruction::CmpSetLnk {
2050            set_op: SetOp::None,
2051            cmp_op: CmpOp::None,
2052            sr1: Register::Gprm(0),
2053            set_src: Operand::Immediate(0),
2054            cmp_rhs: Operand::Immediate(0),
2055            hl_bn: 9,
2056            link: LinkSubset::Rsm,
2057        });
2058        assert_eq!(
2059            action,
2060            VmAction::Resume(ResumePoint {
2061                resume_cell: 4,
2062                hl_btn: 9,
2063            })
2064        );
2065        assert_eq!(vm.resume_depth(), 0);
2066    }
2067
2068    #[test]
2069    fn step_compound_with_invalid_link_subset_is_continue() {
2070        // An `Invalid` link-subset bag (e.g. 0x04, 0x08) collapses to
2071        // Continue rather than panicking — malformed discs survive.
2072        let mut vm = Vm::new();
2073        let action = vm.step(NavInstruction::SetCLnk {
2074            set_op: SetOp::None,
2075            cmp_op: CmpOp::None,
2076            scr: Register::Gprm(0),
2077            set_src: Operand::Immediate(0),
2078            cmp_rhs: Operand::Immediate(0),
2079            hl_bn: 0,
2080            link: LinkSubset::Invalid(0x04),
2081        });
2082        assert_eq!(action, VmAction::Continue);
2083    }
2084
2085    #[test]
2086    fn step_compound_setop_none_skips_set_phase() {
2087        // SET-op = None means the compound's SET phase is a no-op
2088        // even on the true branch — the destination keeps its
2089        // pre-existing value while CMP + LINK still fire normally.
2090        let mut vm = Vm::new();
2091        vm.regs.set_gprm(5, 42);
2092        let action = vm.step(NavInstruction::CSetCLnk {
2093            set_op: SetOp::None,
2094            cmp_op: CmpOp::Eq,
2095            sr1: Register::Gprm(5),
2096            set_src: Operand::Immediate(99), // would clobber 42 if SET ran
2097            cmp_lhs: Register::Gprm(5),
2098            cmp_rhs: Operand::Immediate(42),
2099            hl_bn: 1,
2100            link: LinkSubset::LinkTopCell,
2101        });
2102        // SET phase skipped.
2103        assert_eq!(vm.regs.gprm(5), 42);
2104        // CMP true; Link surfaces.
2105        assert_eq!(
2106            action,
2107            VmAction::Link(LinkAction::Subset {
2108                subset: LinkSubset::LinkTopCell,
2109                hl_bn: 1,
2110            })
2111        );
2112    }
2113
2114    // -----------------------------------------------------------------
2115    // run_list() — PC / Goto / Break / Exit.
2116    // -----------------------------------------------------------------
2117
2118    fn encode_nop() -> NavCommand {
2119        NavCommand {
2120            bytes: [0x00, 0x00, 0, 0, 0, 0, 0, 0],
2121        }
2122    }
2123
2124    fn encode_break() -> NavCommand {
2125        NavCommand {
2126            bytes: [0x00, 0x02, 0, 0, 0, 0, 0, 0],
2127        }
2128    }
2129
2130    fn encode_exit() -> NavCommand {
2131        // Type 1 jump/call, cmd nibble 1 = Exit.
2132        NavCommand {
2133            bytes: [0x30, 0x01, 0, 0, 0, 0, 0, 0],
2134        }
2135    }
2136
2137    fn encode_goto(line: u8) -> NavCommand {
2138        // Type 0 cmd nibble 1, line in byte 7.
2139        NavCommand {
2140            bytes: [0x00, 0x01, 0, 0, 0, 0, 0, line],
2141        }
2142    }
2143
2144    #[test]
2145    fn run_list_completes_cleanly_through_nops() {
2146        let mut vm = Vm::new();
2147        let list = vec![encode_nop(), encode_nop(), encode_nop()];
2148        let (action, pc) = vm.run_list(&list);
2149        assert_eq!(action, VmAction::Continue);
2150        assert_eq!(pc, 3);
2151    }
2152
2153    #[test]
2154    fn run_list_break_returns_at_break_pc() {
2155        let mut vm = Vm::new();
2156        let list = vec![encode_nop(), encode_break(), encode_nop()];
2157        let (action, pc) = vm.run_list(&list);
2158        assert_eq!(action, VmAction::Break);
2159        assert_eq!(pc, 1);
2160    }
2161
2162    #[test]
2163    fn run_list_exit_returns_at_exit_pc() {
2164        let mut vm = Vm::new();
2165        let list = vec![encode_nop(), encode_exit(), encode_nop()];
2166        let (action, pc) = vm.run_list(&list);
2167        assert_eq!(action, VmAction::Exit);
2168        assert_eq!(pc, 1);
2169    }
2170
2171    #[test]
2172    fn run_list_goto_jumps_to_one_based_line() {
2173        let mut vm = Vm::new();
2174        // line 1 = idx 0; goto(3) at idx 0 → run idx 2 → break.
2175        let list = vec![encode_goto(3), encode_nop(), encode_break()];
2176        let (action, pc) = vm.run_list(&list);
2177        assert_eq!(action, VmAction::Break);
2178        assert_eq!(pc, 2);
2179    }
2180
2181    #[test]
2182    fn run_list_goto_out_of_range_runs_to_end() {
2183        let mut vm = Vm::new();
2184        let list = vec![encode_goto(99), encode_nop()];
2185        let (action, pc) = vm.run_list(&list);
2186        assert_eq!(action, VmAction::Continue);
2187        assert_eq!(pc, list.len());
2188    }
2189
2190    #[test]
2191    fn run_list_runaway_goto_loop_terminates_under_budget() {
2192        let mut vm = Vm::new();
2193        // goto(1) at idx 0 jumps back to itself — infinite loop. The
2194        // bounded step budget guarantees termination.
2195        let list = vec![encode_goto(1)];
2196        let (action, _) = vm.run_list(&list);
2197        // We don't care which final action surfaces; only that the
2198        // call returns at all. Continue is the budget-exhausted
2199        // sentinel.
2200        assert_eq!(action, VmAction::Continue);
2201    }
2202
2203    #[test]
2204    fn run_list_pc_resets_to_zero_between_invocations() {
2205        let mut vm = Vm::new();
2206        vm.pc = 17;
2207        let _ = vm.run_list(&[encode_nop()]);
2208        // run_list started at 0 (reset), advanced past the single
2209        // nop, and finished at 1.
2210        assert_eq!(vm.pc(), 1);
2211    }
2212
2213    // -----------------------------------------------------------------
2214    // Default round-trip — a default NavCommand executes as NOP.
2215    // -----------------------------------------------------------------
2216
2217    #[test]
2218    fn default_navcommand_runs_as_single_nop() {
2219        let mut vm = Vm::new();
2220        let (action, pc) = vm.run_list(&[NavCommand::default()]);
2221        assert_eq!(action, VmAction::Continue);
2222        assert_eq!(pc, 1);
2223    }
2224
2225    // -----------------------------------------------------------------
2226    // SPRM bitfield accessors — cross-check decode against the spec
2227    // page's documented bit layout for the 6 packed registers.
2228    // -----------------------------------------------------------------
2229
2230    #[test]
2231    fn sprm_defaults_cover_language_extension_slots() {
2232        // SPRMs 17 + 19 are language-extension codes whose "not
2233        // specified" default is `0`; the test fixes that we honour
2234        // the spec's explicit default rather than leaving a vague
2235        // zero-fill.
2236        let r = RegisterFile::new();
2237        assert_eq!(r.sprm(SPRM_PREF_AUDIO_LANG_EXT), 0);
2238        assert_eq!(r.sprm(SPRM_PREF_SUBP_LANG_EXT), 0);
2239    }
2240
2241    #[test]
2242    fn sprm_named_constants_match_spec_indices() {
2243        // Pin the spec's SPRM numbering against the named constants
2244        // so a renumbering accident is caught at compile-time-equivalent.
2245        assert_eq!(SPRM_MENU_LANG, 0);
2246        assert_eq!(SPRM_CC_PLT, 12);
2247        assert_eq!(SPRM_VIDEO_PREF, 14);
2248        assert_eq!(SPRM_AUDIO_CAPS, 15);
2249        assert_eq!(SPRM_PREF_AUDIO_LANG, 16);
2250        assert_eq!(SPRM_PREF_AUDIO_LANG_EXT, 17);
2251        assert_eq!(SPRM_PREF_SUBP_LANG, 18);
2252        assert_eq!(SPRM_PREF_SUBP_LANG_EXT, 19);
2253        assert_eq!(SPRM_REGION_MASK, 20);
2254    }
2255
2256    #[test]
2257    fn subpicture_stream_default_is_none_with_display_off() {
2258        let r = RegisterFile::new();
2259        let view = r.subpicture_stream();
2260        // SPRM 2 default = 62 — bits 0..=5 = 62 ("none"), bit 6 = 0
2261        // ("do not display"). Cross-check both decoded fields.
2262        assert_eq!(view.stream, 62);
2263        assert!(!view.display);
2264        assert!(view.is_none_sentinel());
2265        assert!(!view.is_forced_sentinel());
2266    }
2267
2268    #[test]
2269    fn subpicture_stream_forced_sentinel_with_display_on() {
2270        let mut r = RegisterFile::new();
2271        // stream = 63 (forced) with display bit set.
2272        r.set_sprm(SPRM_SUBPICTURE_STREAM, 63 | (1 << 6));
2273        let view = r.subpicture_stream();
2274        assert_eq!(view.stream, 63);
2275        assert!(view.display);
2276        assert!(view.is_forced_sentinel());
2277        assert!(!view.is_none_sentinel());
2278    }
2279
2280    #[test]
2281    fn highlight_button_decodes_default_as_one() {
2282        let r = RegisterFile::new();
2283        assert_eq!(r.highlight_button(), 1);
2284    }
2285
2286    #[test]
2287    fn highlight_button_decodes_arbitrary_value() {
2288        let mut r = RegisterFile::new();
2289        // Button 17 → bits 10..=15 = 17 → value = 17 << 10 = 17408.
2290        r.set_sprm(SPRM_HL_BTNN, 17 << 10);
2291        assert_eq!(r.highlight_button(), 17);
2292    }
2293
2294    #[test]
2295    fn highlight_button_rejects_out_of_range_field() {
2296        let mut r = RegisterFile::new();
2297        // Button 0 (below the 1..=36 spec range) → decode returns 0.
2298        r.set_sprm(SPRM_HL_BTNN, 0);
2299        assert_eq!(r.highlight_button(), 0);
2300        // Button 37 (above the 1..=36 spec range) → decode returns 0.
2301        r.set_sprm(SPRM_HL_BTNN, 37 << 10);
2302        assert_eq!(r.highlight_button(), 0);
2303    }
2304
2305    #[test]
2306    fn audio_mix_mode_decodes_all_six_documented_bits() {
2307        let mut r = RegisterFile::new();
2308        // Set all six documented bits.
2309        let bits = (1 << 2) | (1 << 3) | (1 << 4) | (1 << 10) | (1 << 11) | (1 << 12);
2310        r.set_sprm(SPRM_AMXMD, bits);
2311        let m = r.audio_mix_mode();
2312        assert!(m.mix_2_to_front);
2313        assert!(m.mix_3_to_front);
2314        assert!(m.mix_4_to_front);
2315        assert!(m.mix_2_to_rear);
2316        assert!(m.mix_3_to_rear);
2317        assert!(m.mix_4_to_rear);
2318        assert_eq!(m.raw, bits);
2319    }
2320
2321    #[test]
2322    fn audio_mix_mode_default_is_no_routing() {
2323        let r = RegisterFile::new();
2324        let m = r.audio_mix_mode();
2325        assert!(!m.mix_2_to_front);
2326        assert!(!m.mix_3_to_front);
2327        assert!(!m.mix_4_to_front);
2328        assert!(!m.mix_2_to_rear);
2329        assert!(!m.mix_3_to_rear);
2330        assert!(!m.mix_4_to_rear);
2331    }
2332
2333    #[test]
2334    fn video_preference_decodes_aspect_and_mode() {
2335        let mut r = RegisterFile::new();
2336        // aspect = 3 (16:9) in bits 10..=11, mode = 2 (letterbox) in bits 8..=9.
2337        r.set_sprm(SPRM_VIDEO_PREF, (0b11 << 10) | (0b10 << 8));
2338        let p = r.video_preference();
2339        assert_eq!(p.aspect, AspectRatio::Ar16x9);
2340        assert_eq!(p.mode, DisplayMode::Letterbox);
2341    }
2342
2343    #[test]
2344    fn video_preference_covers_every_named_code() {
2345        // Round-trip each aspect/mode pair so a bit-position regression
2346        // surfaces here rather than as a misdecoded run-time field.
2347        assert_eq!(AspectRatio::from_bits(0), AspectRatio::Ar4x3);
2348        assert_eq!(AspectRatio::from_bits(1), AspectRatio::NotSpecified);
2349        assert_eq!(AspectRatio::from_bits(2), AspectRatio::Reserved);
2350        assert_eq!(AspectRatio::from_bits(3), AspectRatio::Ar16x9);
2351        assert_eq!(DisplayMode::from_bits(0), DisplayMode::Normal);
2352        assert_eq!(DisplayMode::from_bits(1), DisplayMode::PanScan);
2353        assert_eq!(DisplayMode::from_bits(2), DisplayMode::Letterbox);
2354        assert_eq!(DisplayMode::from_bits(3), DisplayMode::Reserved);
2355    }
2356
2357    #[test]
2358    fn audio_capabilities_cannot_play_when_zero() {
2359        let r = RegisterFile::new();
2360        let c = r.audio_capabilities();
2361        assert!(c.cannot_play());
2362        assert!(!c.dolby);
2363        assert!(!c.dts);
2364        assert!(!c.mpeg);
2365    }
2366
2367    #[test]
2368    fn audio_capabilities_decodes_each_documented_bit() {
2369        let mut r = RegisterFile::new();
2370        let bits = (1 << 2)   // sdds_karaoke
2371            | (1 << 3)        // dts_karaoke
2372            | (1 << 4)        // mpeg_karaoke
2373            | (1 << 6)        // dolby_karaoke
2374            | (1 << 7)        // pcm_karaoke
2375            | (1 << 10)       // sdds
2376            | (1 << 11)       // dts
2377            | (1 << 12)       // mpeg
2378            | (1 << 14); // dolby
2379        r.set_sprm(SPRM_AUDIO_CAPS, bits);
2380        let c = r.audio_capabilities();
2381        assert!(c.sdds_karaoke);
2382        assert!(c.dts_karaoke);
2383        assert!(c.mpeg_karaoke);
2384        assert!(c.dolby_karaoke);
2385        assert!(c.pcm_karaoke);
2386        assert!(c.sdds);
2387        assert!(c.dts);
2388        assert!(c.mpeg);
2389        assert!(c.dolby);
2390        assert!(!c.cannot_play());
2391    }
2392
2393    #[test]
2394    fn region_mask_default_is_all_disallowed() {
2395        let r = RegisterFile::new();
2396        for region in 1..=8 {
2397            assert!(!r.region_allowed(region));
2398        }
2399        // Out-of-range queries always return false.
2400        assert!(!r.region_allowed(0));
2401        assert!(!r.region_allowed(9));
2402    }
2403
2404    #[test]
2405    fn region_mask_per_region_decode() {
2406        let mut r = RegisterFile::new();
2407        // Allow regions 1, 2, 4, 8 (bit positions 0, 1, 3, 7).
2408        let mask = (1u16 << 0) | (1 << 1) | (1 << 3) | (1 << 7);
2409        r.set_sprm(SPRM_REGION_MASK, mask);
2410        assert!(r.region_allowed(1));
2411        assert!(r.region_allowed(2));
2412        assert!(!r.region_allowed(3));
2413        assert!(r.region_allowed(4));
2414        assert!(!r.region_allowed(5));
2415        assert!(!r.region_allowed(6));
2416        assert!(!r.region_allowed(7));
2417        assert!(r.region_allowed(8));
2418        assert_eq!(r.region_mask(), mask as u8);
2419    }
2420
2421    // -----------------------------------------------------------------
2422    // SPRM 0 / 12 / 16 / 18 — ISO 639 / ISO 3166 language codes.
2423    // -----------------------------------------------------------------
2424
2425    #[test]
2426    fn menu_language_default_is_uninitialised() {
2427        let r = RegisterFile::new();
2428        // SPRM 0 default ("player specific") = 0 in our table.
2429        let lc = r.menu_language();
2430        assert!(!lc.is_not_specified());
2431        assert_eq!(lc.ascii_bytes(), None);
2432    }
2433
2434    #[test]
2435    fn menu_language_encodes_ascii_pair() {
2436        let mut r = RegisterFile::new();
2437        // "en" — high byte 'e' = 0x65, low byte 'n' = 0x6E.
2438        r.set_sprm(SPRM_MENU_LANG, 0x656E);
2439        let lc = r.menu_language();
2440        assert_eq!(lc.ascii_bytes(), Some([b'e', b'n']));
2441        assert_eq!(lc.as_string().as_deref(), Some("en"));
2442        assert_eq!(lc.raw, 0x656E);
2443    }
2444
2445    #[test]
2446    fn preferred_audio_language_default_is_not_specified() {
2447        let r = RegisterFile::new();
2448        let lc = r.preferred_audio_language();
2449        assert!(lc.is_not_specified());
2450        assert_eq!(lc.ascii_bytes(), None);
2451        assert_eq!(lc.as_string(), None);
2452        assert_eq!(lc.raw, LanguageCode::NOT_SPECIFIED);
2453    }
2454
2455    #[test]
2456    fn preferred_subpicture_language_round_trips_uppercase_ascii() {
2457        let mut r = RegisterFile::new();
2458        // "JA" — uppercase Japanese, as a DVD might emit. The accessor
2459        // lowercases on `as_string` but preserves the bytes in
2460        // `ascii_bytes`.
2461        r.set_sprm(SPRM_PREF_SUBP_LANG, 0x4A41);
2462        let lc = r.preferred_subpicture_language();
2463        assert_eq!(lc.ascii_bytes(), Some([b'J', b'A']));
2464        assert_eq!(lc.as_string().as_deref(), Some("ja"));
2465    }
2466
2467    #[test]
2468    fn parental_country_garbage_collapses_to_none() {
2469        let mut r = RegisterFile::new();
2470        // Non-letter bytes — `ascii_bytes` rejects.
2471        r.set_sprm(SPRM_CC_PLT, 0x3030); // "00"
2472        let lc = r.parental_country();
2473        assert_eq!(lc.ascii_bytes(), None);
2474        assert_eq!(lc.as_string(), None);
2475        // But the raw is preserved for round-trip.
2476        assert_eq!(lc.raw, 0x3030);
2477    }
2478
2479    // -----------------------------------------------------------------
2480    // SPRM 1 (audio stream selector).
2481    // -----------------------------------------------------------------
2482
2483    #[test]
2484    fn audio_stream_default_is_none_sentinel() {
2485        let r = RegisterFile::new();
2486        let s = r.audio_stream();
2487        assert_eq!(s, AudioStreamSelector::None);
2488        assert!(s.is_none_sentinel());
2489        assert!(!s.is_stream());
2490    }
2491
2492    #[test]
2493    fn audio_stream_concrete_index() {
2494        let mut r = RegisterFile::new();
2495        for i in 0..=7u16 {
2496            r.set_sprm(SPRM_AUDIO_STREAM, i);
2497            let s = r.audio_stream();
2498            assert_eq!(s, AudioStreamSelector::Stream(i as u8));
2499            assert!(s.is_stream());
2500            assert!(!s.is_none_sentinel());
2501        }
2502    }
2503
2504    #[test]
2505    fn audio_stream_invalid_preserves_raw() {
2506        let mut r = RegisterFile::new();
2507        r.set_sprm(SPRM_AUDIO_STREAM, 8);
2508        assert_eq!(r.audio_stream(), AudioStreamSelector::Invalid(8));
2509        r.set_sprm(SPRM_AUDIO_STREAM, 99);
2510        assert_eq!(r.audio_stream(), AudioStreamSelector::Invalid(99));
2511    }
2512
2513    // -----------------------------------------------------------------
2514    // SPRM 3 angle.
2515    // -----------------------------------------------------------------
2516
2517    #[test]
2518    fn angle_default_is_one() {
2519        let r = RegisterFile::new();
2520        assert_eq!(r.angle_number(), Some(1));
2521    }
2522
2523    #[test]
2524    fn angle_in_range_round_trip() {
2525        let mut r = RegisterFile::new();
2526        for i in 1..=9u16 {
2527            r.set_sprm(SPRM_ANGLE, i);
2528            assert_eq!(r.angle_number(), Some(i as u8));
2529        }
2530    }
2531
2532    #[test]
2533    fn angle_out_of_range_is_none() {
2534        let mut r = RegisterFile::new();
2535        r.set_sprm(SPRM_ANGLE, 0);
2536        assert_eq!(r.angle_number(), None);
2537        r.set_sprm(SPRM_ANGLE, 10);
2538        assert_eq!(r.angle_number(), None);
2539        r.set_sprm(SPRM_ANGLE, 0xFFFF);
2540        assert_eq!(r.angle_number(), None);
2541    }
2542
2543    // -----------------------------------------------------------------
2544    // SPRM 13 parental level.
2545    // -----------------------------------------------------------------
2546
2547    #[test]
2548    fn parental_level_default_is_uninitialised_zero() {
2549        let r = RegisterFile::new();
2550        // SPRM 13 default ("player specific") = 0 → Invalid(0).
2551        assert_eq!(r.parental_level(), ParentalLevel::Invalid(0));
2552        assert!(!r.parental_level().is_level());
2553        assert!(!r.parental_level().is_none_sentinel());
2554    }
2555
2556    #[test]
2557    fn parental_level_in_range() {
2558        let mut r = RegisterFile::new();
2559        for i in 1..=8u16 {
2560            r.set_sprm(SPRM_PARENTAL_LEVEL, i);
2561            assert_eq!(r.parental_level(), ParentalLevel::Level(i as u8));
2562            assert!(r.parental_level().is_level());
2563        }
2564    }
2565
2566    #[test]
2567    fn parental_level_none_sentinel() {
2568        let mut r = RegisterFile::new();
2569        r.set_sprm(SPRM_PARENTAL_LEVEL, 15);
2570        assert_eq!(r.parental_level(), ParentalLevel::None);
2571        assert!(r.parental_level().is_none_sentinel());
2572    }
2573
2574    // -----------------------------------------------------------------
2575    // SPRM 17 / 19 — audio + subpicture language extensions.
2576    // -----------------------------------------------------------------
2577
2578    #[test]
2579    fn audio_language_ext_decode_table() {
2580        let mut r = RegisterFile::new();
2581        assert_eq!(
2582            r.preferred_audio_language_ext(),
2583            AudioLanguageExt::NotSpecified
2584        );
2585        for (raw, expected) in [
2586            (1, AudioLanguageExt::Normal),
2587            (2, AudioLanguageExt::VisuallyImpaired),
2588            (3, AudioLanguageExt::DirectorComments),
2589            (4, AudioLanguageExt::AlternateDirectorComments),
2590            (5, AudioLanguageExt::Reserved(5)),
2591            (250, AudioLanguageExt::Reserved(250)),
2592        ] {
2593            r.set_sprm(SPRM_PREF_AUDIO_LANG_EXT, raw);
2594            assert_eq!(r.preferred_audio_language_ext(), expected);
2595        }
2596    }
2597
2598    #[test]
2599    fn subpicture_language_ext_decode_table() {
2600        let mut r = RegisterFile::new();
2601        assert_eq!(
2602            r.preferred_subpicture_language_ext(),
2603            SubpictureLanguageExt::NotSpecified
2604        );
2605        for (raw, expected) in [
2606            (1, SubpictureLanguageExt::Normal),
2607            (2, SubpictureLanguageExt::Large),
2608            (3, SubpictureLanguageExt::Children),
2609            (5, SubpictureLanguageExt::NormalCaptions),
2610            (6, SubpictureLanguageExt::LargeCaptions),
2611            (7, SubpictureLanguageExt::ChildrensCaptions),
2612            (9, SubpictureLanguageExt::Forced),
2613            (13, SubpictureLanguageExt::DirectorComments),
2614            (14, SubpictureLanguageExt::LargeDirectorComments),
2615            (15, SubpictureLanguageExt::DirectorCommentsForChildren),
2616            // Gaps in the spec table — 4 / 8 / 10 / 11 / 12 — collapse
2617            // to `Reserved`.
2618            (4, SubpictureLanguageExt::Reserved(4)),
2619            (8, SubpictureLanguageExt::Reserved(8)),
2620            (10, SubpictureLanguageExt::Reserved(10)),
2621            (11, SubpictureLanguageExt::Reserved(11)),
2622            (12, SubpictureLanguageExt::Reserved(12)),
2623            (200, SubpictureLanguageExt::Reserved(200)),
2624        ] {
2625            r.set_sprm(SPRM_PREF_SUBP_LANG_EXT, raw);
2626            assert_eq!(r.preferred_subpicture_language_ext(), expected);
2627        }
2628    }
2629}