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(®ion) {
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}