Skip to content

v0.0.3

Latest

Choose a tag to compare

@MagicalTux MagicalTux released this 15 Jun 05:16
· 8 commits to master since this release
dc1eb58

Other

  • AC-3 sync-frame header decoder (syncinfo + bsi prefix)
  • decode HLI_GI btn_md into typed ButtonMode (PCI highlight button groups)
  • PCI NSML_AGLI non-seamless angle jump table
  • add First-Play PGC reader (DvdDisc::parse_fp_pgc)
  • menu C_ADT + VOBU_ADMAP reader helpers on DvdDisc
  • decode VMGM_PGCI_UT + VTSM_PGCI_UT (menu PGCI Unit Table)
  • decode VMG_VTS_ATRT + VMG_PTL_MAIT on the VMG side
  • typed accessors for the remaining language / sentinel SPRMs
  • drop release-plz.toml — use release-plz defaults across the workspace
  • typed-instruction iterators on PgcCommandTable + decode_instruction bridge
  • typed HighlightStatus enum on PCI_GI hli_ss
  • typed cell-elapsed-time accessor on DsiGi + PgcTime::to_nanoseconds
  • VTSI_MAT / VMGI_MAT stream-attribute extension decoders
  • VOBU_ADMAP + VTS_TMAPTI typed decoders for time-based seek
  • typed UOP-prohibition decoder + three-level OR-merge
  • decode DVD-Video LPCM private_stream_1 audio-pack header
  • SPRM bitfield accessors + named SPRM indices (Phase 3c next-item)
  • execute Type 4..6 compound CMP/SET/LNK families (Phase 3c completion)
  • Phase 3c interpreter — SPRM/GPRM register file + Link/Jump/Call execution

Added

  • AC-3 sync-frame header decode (ac3 module). Ac3Header::parse
    decodes the syncinfo() (sync word 0x0B77, crc1, fscod
    sampling-rate code, frmsizecod frame-size code) and the
    deterministically-positioned prefix of bsi() (bsid, bsmod
    bitstream mode, acmod audio-coding mode, and the cmixlev /
    surmixlev / dsurmod conditional fields whose presence is a pure
    function of acmod, plus lfeon) off the start of an AC-3
    elementary stream routed from DvdSubstream::Ac3. Accessors:
    sample_rate_hz, nominal_bitrate_kbps and frame_size_words /
    frame_size_bytes (driven by the 38-entry frmsizecod table with
    per-sample-rate columns), total_channel_count (nfchans + lfeon),
    and the Ac3AudioCodingMode channel-layout / conditional-field
    classifiers. Reserved fscod / frmsizecod codes are preserved and
    surface as None from the rate/size accessors. Header-only: fields
    past lfeon (variable-length bsi() tail) and the audio blocks stay
    with a downstream AC-3 decoder. Clean-room per
    docs/container/dvd/application/stnsoft-ac3hdr.html +
    mpucoder-dvdmpeg.html.

  • HLI_GI btn_md typed decode. HighlightInfo::button_mode()
    now returns a ButtonMode { group_count, group_types: [u8; 3] }
    decoded from the raw btn_md word per the btn_md word sub-table
    of docs/container/dvd/application/mpucoder-pci_pkt.html:
    btngr_ns (number of button groups, u16 bits 13..12) and the three
    3-bit btngrN_ty group-type codes (bits 10..8 / 6..4 / 2..0), with
    the reserved bits (15..14, 11, 7, 3) masked out. ButtonMode
    also provides from_btn_md / to_btn_md (reserved-bit-dropping
    round-trip). The reference labels the type codes "normal / lb /
    p/s" (normal / letterbox / pan-scan) but gives no numeric
    value-to-name mapping, so the codes are surfaced raw rather than as
    a named enum; the field had previously been kept as an opaque u16.

  • PCI NSML_AGLI non-seamless angle jump table. PciPacket now
    decodes the 36-byte NSML_AGLI block at PCI packet offset
    0x3C..0x60 into a typed NsmlAgli { cells: [NsmlAngleCell; 9] }
    per docs/container/dvd/application/mpucoder-pci_pkt.html. Each
    nsml_agl_cN_dsta cell carries the relative sector offset to the
    current ILVU for that angle, with bit 31 as the direction
    (0 = forward, 1 = backward) and the 0x0000_0000 (angle absent) /
    0x7FFF_FFFF (no more video) sentinels. NsmlAngleCell exposes
    is_absent / is_no_more_video / is_backward / offset_sectors;
    NsmlAgli exposes is_empty, active_angle_count, and a 1-based
    angle(n) accessor that pairs with SPRM 3 (current angle). This is
    the PCI counterpart to the existing DSI SmlAgli seamless-angle
    table, completing the multi-angle navigation surface a player needs
    to switch angles on a non-seamless interleaved block.

  • First-Play PGC reader — DvdDisc::parse_fp_pgc. The VMGI_MAT
    word at 0x0084 is the start byte address of FP_PGC, the
    program chain a player enters at disc insertion before any title or
    menu domain is active — per
    docs/container/dvd/application/mpucoder-ifo.html it is the only
    VMGI structure addressed in bytes rather than sectors (same unit as
    the 0x0080 "end byte address of VMGI_MAT" word), and its body is
    an ordinary PGC per mpucoder-pgc.html (the MAT row links straight
    to the PGC page), so Pgc::parse decodes it unchanged. The new
    helper reads the MAT, follows the byte address, and parses the PGC;
    it returns Ok(None) when fp_pgc_addr is zero (no First-Play PGC
    authored). The read is bounded at the first non-zero sector-aligned
    VMG table so a malformed address can't pull bytes from an unrelated
    table — an address at/past that boundary is rejected with an error
    rather than mis-parsed. This closes the navigation bootstrap gap:
    the Phase 3c VM could already execute startup routing
    (JumpSs(FirstPlay) / JumpTT actions) but nothing could fetch
    the FP_PGC those commands live in. Three new tests: the populated
    path drives the disc-insertion sequence end-to-end (synthetic
    cell-less FP_PGC at byte 0x0400parse_fp_pgc
    commands.preVm::run_listVmAction::JumpTitle { ttn: 1 }),
    plus the zero-pointer None path and the past-first-table
    rejection. 311 lib tests (was 308) under default features;
    321 lib tests (was 318) under --all-features.

  • Menu C_ADT + VOBU_ADMAP reader helpers on DvdDisc. The
    VMGI / VTSI MATs carry sector pointers to the menu-side cell-address
    tables (vmgm_c_adt_sector / vtsm_c_adt_sector) and menu VOBU
    address maps (vmgm_vobu_admap_sector / vtsm_vobu_admap_sector),
    but no high-level reader followed them. The body decoders already
    existed — docs/container/dvd/application/mpucoder-ifo.html documents
    VMGM_C_ADT / VTSM_C_ADT / VTS_C_ADT under one shared #c_adt
    heading (and the three VOBU_ADMAP variants under #vam) because all
    share the wire format, so VtsCAdt::parse / VobuAdmap::parse
    decode the menu copies unchanged. This round wires the four
    high-level reader helpers that read the appropriate MAT, follow the
    sector pointer, and parse the body:

    • DvdDisc::parse_vmgm_c_adt(reader) — VMG menu cell-address table
      (VIDEO_TS.VOB cells).
    • DvdDisc::parse_vmgm_vobu_admap(reader) — VMG menu VOBU sector
      list.
    • DvdDisc::parse_vtsm_c_adt(reader, ts_index) — per-title-set menu
      cell-address table (VTS_xx_0.VOB cells).
    • DvdDisc::parse_vtsm_vobu_admap(reader, ts_index) — per-title-set
      menu VOBU sector list.
      Each returns Ok(None) when the corresponding MAT sector pointer is
      zero (no menu VOB authored). The reads are bounded at the next
      non-zero table sector in the MAT so a malformed end_address length
      field can't pull bytes from an unrelated table — the same
      bounded-read discipline the parse_vmgm_pgci_ut / parse_vtsm_pgci_ut
      helpers use. Five new in-module tests cover the populated happy path
      for all four helpers (synthetic VMGI/VTSI disc image → cell lookup +
      VOBU sector-count/start round-trip) and the four zero-pointer None
      paths. 308 lib tests (was 303) under default features; 318 lib
      tests
      (was 313) under --all-features.
  • VMGM_PGCI_UT + VTSM_PGCI_UT decoders (menu PGCI Unit Table).
    The MAT records the sector pointers vmgm_pgci_ut_sector and
    vtsm_pgci_ut_sector for the menu PGC tables on both the VMG and
    VTS sides, but no body parser existed. This round materialises both
    per docs/container/dvd/application/mpucoder-ifo_vmg.html §VMGM_PGCI_UT
    and mpucoder-ifo_vts.html §VTSM_PGCI_UT — the wire format is
    identical between the two sides:

    • PgciUt — the outer search-pointer list keyed by ISO 639 language
      code (each entry: 16-bit language code + 1-byte language-code
      extension + 1-byte menu_existence flag + 32-bit offset to LU).
      The language_unit(lang_code) lookup round-trips a packed
      b"en"-style code to its parsed Language Unit; the per-entry
      has_root_menu / has_subpicture_menu / has_audio_menu /
      has_angle_menu / has_ptt_menu accessors decode each
      menu-existence flag bit per the table at mpucoder-ifo_vts.html
      (bit 0x80 = root/title, 0x40 = sub-picture, 0x20 = audio,
      0x10 = angle, 0x08 = PTT — the constants live in the public
      menu_existence sub-module).
    • PgciLu — one Language Unit body: a per-PGC search-pointer list
      (PgciLuSrp: 32-bit PGC category dword + 32-bit offset to the
      PGC body) plus the parsed Pgc bodies themselves (via
      Pgc::parse). The PgciLuSrp::is_entry_pgc /
      menu_type / parental_mask accessors decompose the category
      dword per mpucoder-ifo_vts.html (PGC category breakdown).
    • MenuType enum — decodes the low nibble of the PGC category
      byte 0 (2 = title / 3 = root / 4 = sub-picture / 5 =
      audio / 6 = angle / 7 = PTT, plus Unknown(_) for the
      reserved nibble values).
    • DvdDisc::parse_vmgm_pgci_ut(reader) /
      parse_vtsm_pgci_ut(reader, ts_index) — high-level reader helpers
      that read the appropriate MAT, follow the sector pointer, and
      parse the body. Both return Ok(None) when the corresponding
      MAT sector pointer is zero (table absent on this disc / title
      set). The reads are bounded at the next non-zero table sector
      so a malformed length field can't pull bytes from an unrelated
      table.

    Nine new unit tests cover the happy path (two-language walkthrough
    with entry-PGC + menu-type round-trip), the boundary cases (zero
    language units, parental-mask extraction from the category dword),
    and the four malformed-input rejection paths (short header /
    SRP list past buffer / LU offset zero / LU offset past buffer /
    inner PGC offset past buffer).

  • VMG_VTS_ATRT + VMG_PTL_MAIT decoders on the VMG side. The
    VMG IFO's MAT carries two table pointers we'd previously parsed
    (vts_atrt_sector, ptl_mait_sector) without surfacing the
    table bodies. This round materialises both per
    docs/container/dvd/application/mpucoder-ifo_vmg.html:

    • VmgVtsAtrt — per-VTS attribute copies that mirror each VTS
      IFO's attribute block (the buffer at VTS IFO offset 0x0100,
      typically 0x300 bytes long) onto the VMG side. Each
      VmgVtsAtrtEntry exposes the entry's vts_category field
      (0 = unspecified, 1 = Karaoke), a 1-based vts_number,
      and the raw attribute blob. entry(vts_number) looks up an
      entry; bound checks reject malformed EAs that would overlap
      the next entry.
    • VmgPtlMait — the country-keyed parental management table.
      Each PtlMait body carries the eight parental-level mask
      arrays (Nts + 1 16-bit masks per level — index 0 is the
      VMG-side mask, 1..=nts are the title sets). The on-disc
      storage order is descending (level 8 first), but the typed
      masks array is surfaced ascending (masks[0] = level 1) so
      a caller can index with parental_level - 1 directly.
      country(code) looks up a country sub-table; mask(level, title_set) returns the 16-bit allow-mask for the
      (parental_level, title_set) pair.
    • DvdDisc::parse_vmg_vts_atrt(reader) /
      parse_vmg_ptl_mait(reader) — high-level reader helpers that
      read the MAT, follow the sector pointer, and parse the body.
      Both return Ok(None) when the corresponding MAT sector
      pointer is zero (table absent on this disc). The PTL_MAIT
      reader bounds its sector read at the next non-zero table
      pointer in the MAT so a malformed length field can't pull
      bytes from an unrelated table.
      Nine new tests cover the happy path (two-country / two-VTS
      walkthroughs with mask + blob round-trip), boundary cases (zero
      countries, partial header), and the four malformed-input
      rejection paths (short header / offset list past buffer /
      body offset past buffer / per-entry EA overlapping the next
      entry).
  • Typed accessors for the remaining language / sentinel SPRMs.
    Round 3c's first SPRM accessor sweep covered the six bit-packed
    slots (SPRM 2 / 8 / 11 / 14 / 15 / 20); the rest of
    docs/container/dvd/application/mpucoder-sprm.html documents
    nine more SPRMs that aren't plain integers either — the four
    two-byte ASCII slots (SPRM 0 menu language, SPRM 12 parental
    country, SPRM 16 / 18 preferred audio / sub-picture language,
    ISO 639 / ISO 3166 alpha-2) and the sentinel-typed integer
    slots (SPRM 1 audio stream 0..=7 + 15-none, SPRM 3 angle
    1..=9, SPRM 13 parental level 1..=8 + 15-none, SPRM 17
    audio language extension five-value enum, SPRM 19 sub-picture
    language extension eleven-value enum). New surface on
    RegisterFile:

    • menu_language() / parental_country() /
      preferred_audio_language() /
      preferred_subpicture_language() return a LanguageCode
      that exposes the raw word, an is_not_specified() predicate
      (the 0xFFFF SPRM 16 / 18 default), an ascii_bytes()
      Option<[u8; 2]> accessor that only succeeds when both bytes
      are printable ASCII letters, and an as_string() lower-cased
      alpha-2 form for downstream tooling.
    • audio_stream() returns an AudioStreamSelector enum that
      distinguishes the 15-none sentinel from real stream indices
      Stream(0..=7) and preserves out-of-range raws as Invalid.
    • angle_number() collapses the SPRM 3 word to
      Option<u8> with the 1..=9 range enforced.
    • parental_level() returns a ParentalLevel enum with
      Level(1..=8) / None (= 15) / Invalid shapes.
    • preferred_audio_language_ext() /
      preferred_subpicture_language_ext() return
      AudioLanguageExt / SubpictureLanguageExt enums covering
      every spec-table value; unmapped values collapse to
      Reserved(raw) for round-trip.
      Twelve new tests cover the defaults, the in-range values, and
      the out-of-range / sentinel collapse for each accessor.
  • Typed-instruction iterators on PgcCommandTable. The PGC
    command table carries three lists of raw 8-byte
    [NavCommand] words (pre / post / cell) per
    docs/container/dvd/application/mpucoder-pgc.html; the Phase 3c
    disassembler in the nav module turns one word into a typed
    [NavInstruction]. Previously the bridge between the two was
    manual — callers had to walk commands.pre / commands.post /
    commands.cell, then call nav::NavCommand::decode() on each
    entry themselves. New surface:

    • NavCommand::decode_instruction() — convenience that
      delegates to the Phase 3c precursor disassembler so the IFO
      side can reach a typed instruction without re-importing the
      nav module's surface.
    • PgcCommandTable::pre_instructions() /
      post_instructions() / cell_instructions() — borrowing
      iterators of NavInstruction that walk each list in storage
      order.
    • PgcCommandTable::cell_instruction(index_1based: u16)
      1-based indexed lookup matching the on-wire encoding
      CellPlaybackInfo::cell_command carries; passes 0 for
      "no cell command", out-of-range indices return None rather
      than panicking. Per mpucoder-pgc.html the cell-command
      table is 1-based, so 1 → cell[0], 2 → cell[1], etc.

    Round-trip checked: a NavCommand constructed by hand with a
    Type 1 jumpcall + cmd_nibble = 1 payload decodes through both
    decode() and decode_instruction() to the same Exit
    variant. Four new unit tests in src/ifo.rs (synth command
    table → typed walk; 1-based indexing; 0 and out-of-range
    return None; round-trip with explicit nav::decode()).

  • HighlightStatus typed enum on PCI_GI hli_ss. The PCI
    packet's HLI_GI 00 field carries a 16-bit word whose lower two
    bits encode how a player should treat the menu-button overlay
    for the VOBU. Previously the field was surfaced only as the raw
    u16 (PciPacket::hli_ss), forcing every consumer to repeat
    the & 0b11 masking and four-way match documented in
    docs/container/dvd/application/mpucoder-pci_pkt.html.
    New typed surface:

    • HighlightStatus enum with four exhaustive variants —
      None (00), AllNew (01), UsePrevious (10),
      UsePreviousExceptCommands (11).
    • HighlightStatus::from_hli_ss(u16) infallible constructor
      that ignores the 14 reserved upper bits.
    • HighlightStatus::to_bits() round-trip back to the 2-bit
      code.
    • Four classifier predicates — is_none(),
      declares_new_geometry(), reuses_previous_geometry(),
      supplies_own_commands() — that match the four-row spec
      table directly so call sites no longer have to re-derive
      "AllNew + UsePreviousExceptCommands ⇒ commands come from
      this VOBU" from scratch.
    • PciPacket::highlight_status() accessor wrapping the
      constructor; the raw hli_ss word stays exposed so callers
      that need the reserved bits still have them.

    The HighlightInfo geometry struct is still populated only
    when the VOBU actually declares buttons (btn_ns > 0); the
    typed status accessor is now the documented way to detect a
    "re-use previous geometry" VOBU whose own BTN_IT is empty.

  • DsiGi cell-elapsed-time typed accessor. The DSI_GI block
    on every Nav-Pack carries a 4-byte BCD c_eltm field describing
    the elapsed playback time inside the current cell, layered out
    identically to the PGC_GI playback-time field (hh:mm:ss:ff

    • 2-bit frame-rate code per mpucoder-dsi_pkt.html). Previously
      surfaced only as the raw u32. New methods:
    • DsiGi::cell_elapsed_time() -> PgcTime decodes the four BE
      bytes through the existing PgcTime::from_bytes decoder, so
      the same hours / minutes / seconds / frames / frame_rate
      fields the PGC playback-time accessor returns become available
      on the DSI side without the caller re-implementing the BCD
      nibble split.
    • DsiGi::cell_elapsed_ns() -> u64 collapses the typed view to
      absolute nanoseconds via the new PgcTime::to_nanoseconds
      method below.
    • DsiPacket::cell_elapsed_time() / cell_elapsed_ns()
      convenience getters mirror the existing flat vobu_ea() /
      vobu_vob_idn() shape.
  • PgcTime::to_nanoseconds() method. Previously the
    nanosecond conversion lived only inside the mkv-output
    feature gate as a free function on the MKV-writer (because the
    chapter timeline was the only consumer). Promoted to a regular
    method on PgcTime so default-feature builds get the rational
    (frames × 1e9) / fps conversion (30 fps → 33,333,333 ns/frame,
    25 fps → 40,000,000 ns/frame, illegal / reserved rates drop the
    frame fraction and keep only the integer-second portion). The
    mkv_writer::pgc_time_to_ns free function is preserved as a
    thin wrapper for callers that imported it directly.

  • VTSI_MAT / VMGI_MAT stream-attribute extension blocks.
    The two MAT structures previously stopped at sector-pointer
    offset 0x00E4 — the audio / sub-picture / multichannel
    attribute extension that occupies 0x0100..0x015C (menu) and
    0x0200..0x03D8 (VTS title content + karaoke multichannel) was
    ignored. This round adds typed decoders for every field in
    those blocks and surfaces them on VtsiMat::menu_attributes /
    VtsiMat::title_attributes / VmgIfo::menu_attributes.
    Clean-room per docs/container/dvd/application/mpucoder-ifo.html
    (the vidatt, audatt, spatt, and mcext field layouts);
    no external implementation source consulted.

    • VideoAttributes — coding mode (MPEG-1 / MPEG-2),
      NTSC / PAL standard, 4:3 / 16:9 aspect, pan-scan and
      letterbox display-mode disallow flags, line-21 CC-field
      flags, and a VideoResolution::dimensions(standard) helper
      that resolves the 3-bit resolution code to absolute pixel
      dimensions (Full-D1 / ¾-D1 / Half-D1 / SIF).
    • AudioAttributes — coding mode (AC-3 / MPEG-1 / MPEG-2-
      ext / LPCM / DTS), language type + two-letter ISO-639 code +
      code-extension byte (per the SPRM-17 alternate-
      director-comment scheme), application mode (unspecified /
      karaoke / surround), channel count, sample-rate selector
      (only 48 kHz defined), and dual-interpretation
      quantization / DRC field (16/20/24 bps for LPCM versus
      DRC-on/off for MPEG). Helpers: sample_rate_hz(),
      dolby_surround_suitable(), and the four karaoke decoders
      (karaoke_channel_assignment, karaoke_version,
      karaoke_mc_intro_present, karaoke_duet) for the
      application-info byte at offset 7.
    • SubpictureAttributes — 2-bit-RLE coding mode (the only
      one defined), language type, ISO-639 code, and code-extension
      byte (per the SPRM-19 scheme).
    • McExtensionEntry — 24-entry karaoke multichannel
      extension table; each 8-byte entry decodes the 14 ACH
      guide-melody / guide-vocal / sound-effect flag bits across
      channels 0..=4.
    • Backwards-compatible parse. VtsiMat::parse still accepts
      a 0x200-byte buffer; the menu block fits within that range
      and is populated, the title block stays empty and the
      multichannel-extension vec stays empty. Real VTS_xx_0.IFO
      files run to 0x03D8 and now populate fully.
  • VobuAdmap + VtsTmapti / VtsTmap — time-based seek tables.
    The two title-set sector pointers VTSI_MAT::vts_vobu_admap_sector
    and VTSI_MAT::vts_tmapti_sector previously surfaced only as raw
    u32 fields; this round materialises both tables into typed
    parsers and wires them onto VtsIfo so a player can answer
    "where is playback at second N?" without re-walking the IFO byte
    buffer. Clean-room per
    docs/container/dvd/application/mpucoder-ifo.html (VOBU_ADMAP
    layout) and docs/container/dvd/application/mpucoder-ifo_vts.html
    (VTS_TMAPTI / VTS_TMAP layout); no external implementation
    source consulted.

    • VobuAdmap{ end_address, entries: Vec<u32> } decoder
      for the per-VOBU sector list shared by VMGM_VOBU_ADMAP,
      VTSM_VOBU_ADMAP, and VTS_VOBU_ADMAP (all three share the
      same wire format per mpucoder-ifo.html). Entry count is
      implicit in the end_address field; the parser carves
      (end_address + 1 - 4) / 4 four-byte VOB-relative sector
      words. vobu_count, vobu_start_sector(vobu_number) (1-based
      lookup), and vobu_containing(sector) (binary-partition
      inverse lookup that returns the 1-based VOBU number whose
      range covers the requested sector) round out the surface.
    • VtsTmap + TmapEntry — per-PGC time map. The 4-byte
      header is { time_unit: u8, reserved: u8, number_of_entries: u16 }; each entry is a 4-byte big-endian word with bit 31 set
      when the previous entry was time-discontinuous (a VOBU
      boundary that crosses an STC reset) and the low 31 bits
      carrying the VOB-relative sector. sector_at(seconds)
      translates a PGC-relative wall-clock time into the VOBU
      sector whose [(i - 1) * time_unit, i * time_unit) bracket
      contains it; the result clamps to the last entry once
      seconds runs past the map. Empty maps and time_unit == 0
      both yield None rather than panic, per spec language that
      declares an empty map legal but unindexable.
      TmapEntry::DISCONTINUITY_BIT + SECTOR_MASK constants make
      the bit-31 split explicit.
    • VtsTmapti{ number_of_pgcs, end_address, maps: Vec<VtsTmap> }. The spec mandates "each PGC MUST have a time
      map, even if it is empty" so maps.len() == number_of_program_chains is invariant. get(pgcn) returns
      the per-PGC map for a 1-based program-chain number.
    • Wired onto VtsIfo::parse as the two new
      Option<VobuAdmap> + Option<VtsTmapti> fields
      (vobu_admap, time_map). Both stay None when the
      corresponding VTSI_MAT sector pointer is zero — the spec
      lists VTS_VOBU_ADMAP as mandatory but some authoring tools
      elide it on title sets that hold only menu VOBs, and
      VTS_TMAPTI is the explicitly-optional one. The new
      VtsIfo::vobu_sector_at_pgc_time(pgcn, seconds) wrapper
      composes time_map.get(pgcn) with VtsTmap::sector_at, the
      expected entry point a playback engine uses when the user
      requests a wall-clock seek; combine with
      VtsiMat::title_vob_sector for the absolute disc LBA.
    • 15 new in-module tests (round-trip + partition lookup +
      pre-sector / past-end edges + non-multiple-of-4 / truncated /
      empty-map rejections for VobuAdmap; entry decode +
      discontinuity-bit isolation + time-bracket sweep + empty +
      zero-time_unit / truncated rejections for VtsTmap;
      two-PGC walk + empty-PGC invariant + short-offset rejection
      for VtsTmapti; end-to-end VOBU-map + time-map composite
      that walks a six-sector synthetic IFO through VtsIfo::parse
      and asserts vobu_sector_at_pgc_time on three sample
      timestamps). 244 lib tests (was 229) under default
      features; 254 lib tests (was 239) under --all-features.
  • uops module — DVD-Video User Operation flag decoder.
    Three on-disc fields carry a UOP-prohibition bitmask: the
    TT_SRPT entry (bits 0+1 packed into title_type), the PGC
    header (offset 0x0008), and the PCI packet (PCI_GI 08). The
    new uops module surfaces them as typed values, clean-room per
    docs/container/dvd/application/mpucoder-uops.html (25-row bit
    table + per-level applicability columns + the "set bit in any
    mask inhibits the associated control" three-level OR-merge
    rule).

    • UserOp enum — 25 variants (TimePlayOrSearch,
      PttPlayOrSearch, TitlePlay, Stop, GoUp,
      TimeOrPttSearch, TopPgOrPrevPgSearch, NextPgSearch,
      ForwardScan, BackwardScan, the six MenuCall* variants,
      Resume, ButtonSelectOrActivate, StillOff, PauseOn,
      AudioStreamChange, SubpictureStreamChange, AngleChange,
      KaraokeAudioMixChange, VideoPresentationModeChange) with
      bit(), mask(), from_bit(), and ALL accessors.
    • UopMasku32 newtype with contains / is_allowed
      / with / without / set / clear / is_empty / count
      / iter accessors plus merge_or(a, b, c) for the three-
      level OR. defined_bits() masks the raw word to bits 0..=24
      so reserved bits don't pollute the comparison. fits_level
      validates that a mask carries only bits the spec table marks
      present at the given level — useful for an IFO sanity check.
    • UopLevel enum (TitleSearchPointer / ProgramChain
      / Vobu) with a cover() accessor reporting which bits the
      spec table's PGC and VOBU columns mark check-marked. PGC
      cover excludes bit 4 (GoUp) per the spec table's row 4
      PGC-column blank; VOBU cover excludes bits 0/1/2/17 per the
      same table.
    • title_type_uop_mask(title_type) -> UopMask — extracts
      the 2-bit TT_SRPT subset from a DvdTitleEntry::title_type
      byte (low two bits only; remaining bits are jump/link/call
      permission flags per mpucoder-ifo_vmg.html and stay out of
      the UOP surface).
    • Typed accessors wired into existing parsers:
      • Pgc::uop_mask() / Pgc::is_user_op_allowed(UserOp)
        around Pgc::prohibited_user_ops.
      • PciPacket::uop_mask() / PciPacket::is_user_op_allowed
        around PciPacket::vobu_uop_ctl.
      • DvdTitleEntry::uop_mask() /
        DvdTitleEntry::is_user_op_allowed around
        DvdTitleEntry::title_type (low 2 bits).
    • ConstantsUOP_TIME_PLAY_OR_SEARCH through
      UOP_VIDEO_PRESENTATION_MODE_CHANGE (25 named bit-number
      constants), UOP_BIT_COUNT = 25, and UOP_DEFINED_BITS = 0x01FF_FFFF.
    • 21 new in-module tests (bit-number / mask round-trip; spec-
      table column reproduction including the GoUp/PGC-blank row;
      title_type byte sweep; merge-or commutativity / associativity
      / identity; iter ordering; reserved-bit skip; fits_level
      cross-products) plus 7 cross-module integration tests in
      tests/uops_integration.rs validating the typed accessors
      against a hand-built Pgc::parse / PciPacket::parse / raw
      DvdTitleEntry plus the three-level merge end-to-end.
      229 lib tests (was 208) + 7 integration tests.
  • lpcm module — DVD-Video LPCM 7-byte audio-pack header decoder.
    The private_stream_1 LPCM substream (0xA0..=0xA7) carries a
    fixed 7-byte audio-pack header ahead of the raw PCM sample bytes
    that pins the sample format, the seamless-playback frame counter,
    and the X/Y dynamic-range coefficients. The new lpcm module
    decodes that header into a typed LpcmHeader, clean-room per
    docs/container/dvd/application/mpucoder-lpcm.html
    (field layout + linear_gain = 2^(4 - (X + Y/30)) /
    gain_db = 24.082 - 6.0206 X - 0.2007 Y formulas) and
    docs/container/dvd/application/stnsoft-LimPcmAud.html (the
    per-(sample_rate × quantisation × channels) bitrate table and
    the 6144 kbps DVD-Video LPCM ceiling). Clean-room from those two
    spec pages only.

    • LpcmHeader{ sub_stream_id, number_of_frame_headers, first_access_unit_pointer, audio_emphasis_flag, audio_mute_flag, audio_frame_number, quantisation, sample_frequency, channel_count, dynamic_range_x, dynamic_range_y } decoded view.
    • LpcmQuantisation enum — Bits16 / Bits20 / Bits24 /
      Reserved, with bits_per_sample() accessor.
    • LpcmSampleFrequency enum — Hz48000 / Hz96000 /
      Reserved, with hz() accessor.
    • LpcmHeader::bitrate_kbps() computes channels × sample_rate × bits_per_sample / 1000 and returns None when either of the
      two reserved codes is present;
      LpcmHeader::is_within_dvd_video_limit() checks the result
      against the stnsoft-LimPcmAud.html 6144 kbps ceiling (the red-
      highlighted combinations such as 96 kHz × 24-bit × 8-channel
      return false).
    • LpcmHeader::linear_gain() + gain_db() evaluate the
      two parameterisations of the dynamic-range coefficient table.
      X = 0, Y = 0 gives the unity-gain reference (2^4, +24.082 dB);
      X = 7, Y = 30 gives the -24 dB pole. Applying the gain to the
      decoded samples stays with the audio decoder.
    • peel_lpcm_payload(&[u8]) -> Result<(LpcmHeader, &[u8])>
      splits the substream-ID-prefixed PES payload into the typed
      header and the raw PCM tail in one zero-copy call.
    • ConstantsLPCM_HEADER_LEN = 7 and
      DVD_LPCM_MAX_BITRATE_KBPS = 6144.
    • 14 new unit tests including a full reproduction of the
      stnsoft-LimPcmAud.html bitrate table (48 combinations across
      {48k, 96k} × {16, 20, 24} × {1..=8 ch}, every cell pinning both
      the decoded kbps and the green/red is_within_dvd_video_limit
      verdict). Parse-reject cases for the truncated buffer and the
      non-LPCM substream selector; isolated decode of every quantisation,
      sample-rate, and channel-count code; bit-by-bit decoding of the
      emphasis / mute / frame-number byte, the first-access-unit
      pointer, and the X/Y dynamic-range split; the unity-gain identity
      and the -24 dB attenuation pole; and peel_lpcm_payload round-
      trip + short-buffer rejection. 208 lib tests (was 192).
  • mkv_writer strips the LPCM audio-pack header before forwarding
    PCM samples to the MKV muxer, so the pcm_s16be track now receives
    the clean big-endian sample bytes A_PCM/INT/BIG expects (the
    previous comment had punted the stripping to "Phase 3c"; this round
    closes that gap by re-using the new lpcm::LPCM_HEADER_LEN
    constant).

  • SPRM bitfield-aware accessors + named indices for SPRMs 0/12/14..20.
    The vm module now exposes typed views for the six packed SPRMs
    whose contents are documented as bit-packed payloads on
    mpucoder-sprm.html:

    • RegisterFile::subpicture_stream()SubpictureStreamView
      decodes SPRM 2 into the 6-bit stream index, the bit-6
      display flag, plus is_none_sentinel / is_forced_sentinel
      helpers for the spec's 62 / 63 special values.
    • RegisterFile::highlight_button()u8 — decodes SPRM 8's
      1..=36 button number from bits 10..=15; out-of-range fields
      surface as 0 so a malformed disc cannot crash a player.
    • RegisterFile::audio_mix_mode()AudioMixMode — decodes
      SPRM 11's six per-channel mix bits (bits 2/3/4 → front,
      bits 10/11/12 → rear).
    • RegisterFile::video_preference()VideoPreference with
      AspectRatio (4:3 / NotSpecified / Reserved / 16:9) and
      DisplayMode (Normal / PanScan / Letterbox / Reserved) decoded
      from SPRM 14 bits 10..=11 and 8..=9 respectively.
    • RegisterFile::audio_capabilities()AudioCapabilities
      decodes SPRM 15's nine documented capability bits (SDDS / DTS /
      MPEG / Dolby / PCM, each with optional karaoke variant);
      cannot_play() returns true when the register is zero per the
      spec page's "0 = cannot play" semantic.
    • RegisterFile::region_allowed(region) / region_mask()
      decode SPRM 20's 8-bit region mask (bit i ⇒ region i + 1).
      Named index constants added for the missing SPRMs: SPRM_MENU_LANG
      (0), SPRM_CC_PLT (12), SPRM_VIDEO_PREF (14), SPRM_AUDIO_CAPS
      (15), SPRM_PREF_AUDIO_LANG (16), SPRM_PREF_AUDIO_LANG_EXT (17),
      SPRM_PREF_SUBP_LANG (18), SPRM_PREF_SUBP_LANG_EXT (19),
      SPRM_REGION_MASK (20). Default-vector documentation table
      re-rendered with one row per SPRM index, the spec value, and the
      spec-page source. SPRMs 17 and 19 now hold an explicit 0 ("not
      specified") rather than an implicit zero-fill, matching the spec's
      language-extension enumeration. Clean-room per
      docs/container/dvd/application/mpucoder-sprm.html. 14 new tests
      cover each accessor's default value and bit-by-bit decode.
  • Compound CMP/SET/LNK execution (Type 4..6) — Phase 3c completion.
    The nav module's SetCLnk / CSetCLnk / CmpSetLnk variants now
    carry the full operand triple (SET source, CMP RHS, shared selector,
    Type 5's independent CMP-LHS, hl_bn button override, Link subset)
    pulled out of the 8-byte word per the per-row layouts on
    mpucoder-vmi.html (table 2, rows 88..101). The vm interpreter
    executes each compound in spec order per mpucoder-vmi-sum.html:

    • SetCLnk — SET first, then CMP against post-SET selector,
      then Link on true; false collapses to Continue so the
      outer command list keeps walking.
    • CSetCLnk — CMP first; SET and Link only on true.
    • CmpSetLnk — CMP first; SET only on true; Link
      unconditional (the distinguishing semantic from CSetCLnk).
      Compound Link subsets Nop collapse to Continue even when the
      enclosing compound ran; Rsm pops the same RSM stack as a bare
      Type-1 link; Invalid(_) subsets degrade to Continue so a
      malformed disc cannot crash the interpreter. The two "Illegal"
      red rows (SET-dir=1 AND CMP-dir=1 for Types 5 and 6, where the
      operand bytes would overlap) surface as NavInstruction::Invalid
      per the spec page's explicit rejection. 14 new tests (the four
      full-operand decode forms across register / immediate mixes for
      Types 4, 5, 6, the two Invalid-row encodings, plus 10 VM-exec
      cases covering SET-then-LINK truth + false-branch behaviour for
      all three families, the Link-subset Nop / Rsm /
      Invalid collapse paths, and the SetOp::None "skip SET phase"
      short-circuit). 177 lib tests (was 163 after Phase 3c VM
      landed). Clean-room per the spec pages cited above; no external
      implementation source consulted.
  • vm module — DVD-Video VM interpreter (Phase 3c). Wraps the
    nav module's typed NavInstruction disassembler with a stateful
    executor. Clean-room per docs/container/dvd/application/mpucoder-{vmi,vmi-sum,vmi-jmp,sprm,uops}.html;
    no external implementation source consulted.

    • RegisterFile — 16 GPRMs (writable, persists across PGCs)
      • 24 SPRMs initialised to the spec-defined defaults (ASTN = 15,
        SPSTN = 62, AGLN/TTN/VTS_TTN/PTTN = 1, HL_BTNN = 1 << 10,
        preferred-language slots = 0xFFFF) + a per-GPRM counter-mode
        bit-mask the SetGPRMMD mf flag toggles. tick_counters(delta)
        advances every counter-mode GPRM by delta seconds (saturating)
        so a playback engine that owns a wall clock can drive the
        1 Hz semantic without owning the register file. Out-of-range
        index reads return 0 and writes are silently dropped — matches
        the spec's "invalid register reads as 0" fallback observed in
        malformed PGC command tables.
    • Vm — owns a RegisterFile, the call/return stack
      (ResumePoint frames bounded by MAX_RSM_DEPTH = 8 to detect
      runaway nesting without restricting commercial-disc 1–2-deep
      Menu Call → sub-menu use cases), and the per-list program
      counter. Vm::step(NavInstruction) -> VmAction advances one
      decoded instruction. Vm::run_list(&[NavCommand]) walks a
      pre/post/cell command list end-to-end, honours intra-list
      Goto (1-based line numbers per the spec page; out-of-range
      target falls through to the end of the list) + Break + Exit
      control flow, and terminates a pathological Goto self-loop
      via a len * 16 step budget so a malformed disc can never hang
      the interpreter.
    • VmAction — the playback-engine-visible effect of one step:
      Continue / Break / Exit / Link(LinkAction) / JumpTitle
      / JumpVtsTitle / JumpVtsPtt / JumpSs(JumpSSTarget) /
      CallSs(CallSSTarget) / Resume(ResumePoint) / SetNavTimer { seconds, pgcn } / NoOpRaw(NavCommand). The interpreter
      applies any register / counter / SPRM mutations the instruction
      implied before returning, so the engine sees the post-state.
    • LinkAction + ResumePoint — typed Link-family
      descriptors. LinkAction::Subset { subset, hl_bn } covers the
      13 enum-style forms (LinkTopCellLinkTailPGC); the four
      numbered forms (Pgcn, Pttn, Pgn, Cn) each get a dedicated
      variant. ResumePoint { resume_cell, hl_btn } carries the
      CallSS rsm_cell byte through to a matching RSM so a player
      can resume to a different cell than the one active at call time.
    • Vm::evaluate + Vm::apply_set — pure helpers exposing
      the CMP and SET sub-op tables. evaluate covers all 7 named
      comparison predicates plus the None "unconditional" sentinel;
      apply_set covers all 12 named SET ops (mov, swp, add,
      sub, mul, div, mod, rnd, and, or, xor) using
      wrapping arithmetic for overflow, checked_div / checked_rem
      for the zero-divisor case (returns the destination unchanged
      rather than panic), and a deterministic 0 placeholder for
      rnd until a caller wraps the VM with an entropy source.
    • Vm::push_resume / Vm::pop_resume / Vm::resume_depth
      — public RSM stack manipulators for tests + tooling. Push is
      capacity-bounded at MAX_RSM_DEPTH (drops the new frame and
      returns false rather than overflow).
    • 12 SPRM index constants re-exported at crate root
      (SPRM_AUDIO_STREAM, SPRM_SUBPICTURE_STREAM, SPRM_ANGLE,
      SPRM_TITLE, SPRM_VTS_TITLE, SPRM_PGCN, SPRM_PTT,
      SPRM_HL_BTNN, SPRM_NV_TIMER, SPRM_NV_PGCN, SPRM_AMXMD,
      SPRM_PARENTAL_LEVEL) so callers don't carry magic numbers.
    • 37 new unit tests covering register-file defaults + out-of-range
      indexing + counter-mode toggle + tick saturation, the full CMP
      sub-op truth table, every named SET op including overflow-wrap
      • zero-divisor + Invalid no-op, step() dispatch for every
        NavInstruction family (Set arithmetic / Swap exchange /
        SetStn per-flag application / SetNvtmr action + SPRM 9-10 load /
        SetGprmMd counter-mode toggle / SetHlBtnn / SetTmpPml / every
        Link/Jump/Call surface / CallSs push + RSM pop with hl_btn
        propagation / RSM-with-empty-stack falls through to Continue /
        push bounded to MAX_RSM_DEPTH / Unknown + Invalid → NoOpRaw
        without mutation), and run_list() PC handling (clean
        Nop chain / Break-mid-list / Exit-mid-list / Goto 1-based
        addressing / out-of-range Goto falls off the end / runaway
        Goto-self-loop terminates under budget / PC resets between
        invocations / default NavCommand runs as single NOP).
        163 lib tests (was 126 after the SPU compositor landed).

Changed

  • Phase 3c precursor → Phase 3c proper: the nav module's
    NavInstruction disassembler is now consumed by the new vm
    module's interpreter, and Type 4..6 compounds carry their full
    operand triple instead of just the classifier sub-ops. Existing
    NavInstruction decode + the disc / IFO / VOB / SPU / MKV
    surfaces are unchanged.
  • BreakingNavInstruction::{SetCLnk, CSetCLnk, CmpSetLnk}
    field layout extended with the per-row operand fields documented
    in mpucoder-vmi.html table 2; the previous classifier-only
    shape (set_op, cmp_op, and Type 4's scr only) no longer
    compiles. Pre-0.0.3 release — no published consumer to break.
  • Scrubbed an attributive external-implementation mention in
    disc.rs's DvdFileKind doc comment and an enumerated-denial
    paragraph at the bottom of README.md; both are now spec-only
    wording per the project's clean-room provenance discipline.