Skip to the content.

PC-98 driver families — how each one is re-hosted

This document records, per sound-driver family, how hoot’s PC-98 (pc98dos) sets deliver a song to their driver and how the hootrip harness reproduces that so the driver plays and we can log its OPN/OPNA register writes to S98/VGM.

It focuses on the families brought up in the 2026-07-24 “remaining families” campaign, with enough of the shared model to make the fixes legible.


1. The shared model

A pc98dos set is a real MS-DOS re-host, not a memory blob:

  1. Materialize every <rom> file into a virtual DOS CWD (real .COM/.EXE/ data files live there).
  2. Bind rom → DOS handle: hoot presents each <rom offset="N">FILE</rom> on DOS file handle N. In the harness (bind_rom_handles):
    • type="file" → the file’s content is readable on handle N.
    • type="conin" → the filename text (ASCIIZ basename) is on handle N.
    • offset="-1" → materialized on disk only (open-by-name works), not handle-bound.
  3. Load device drivers (<rom type="device">*.SYS/.DRV/.EXE) CONFIG.SYS-style: run their INIT so they install their API interrupt.
  4. Run the shell chain (<rom type="shell"> in order). The last command is usually a tiny hoot glue stub (xxxx_98.COM) that installs one of hoot’s externalCommand vectors, INT 7Eh or INT 7Fh, and then idles.
  5. Trigger playback: the harness sets virtual ports and invokes the stub’s INT 7Eh/7Fh. The stub loads the selected song and starts the driver, whose timer ISR (OPN chip timer on IRQ3 = INT 0Bh, the PC-98 PIT on INT 08h, a slave IRQ like INT 14h, or the CRT VSYNC on IRQ2 = INT 0Ah) then sequences the music. We record every OPN register write during this phase.

Virtual trigger ports (modelled in pc98/io.rs)

Port Name Meaning
0x7E0 EXT_CMD command byte: 0 = play, 2 = stop
0x7E2 EXT_SONG song word the harness sets before the trigger
0x7E4 EXT_PARAM extra selection parameter
0x7E8 EXT_STATE stub handshake (0x81 = ready)

Song selection (selected_song) and delivery (bind_trigger_song)

selected_song(title_code) picks the song file by matching offset against the title code — a file rom (whole code, then low byte), then a conin rom at the low byte (excluding engine names ending .EXE/.COM/.DRV/.SYS, so a low byte that collides with an engine handle can never select the engine as the song).

bind_trigger_song then presents it in the convention the driver expects. The central discovery of this campaign is that three unrelated drivers share one bug: they open the song file by name themselves, but hoot’s stubs were handing them the file content. They are handled by the opens_by_name branch, which puts the filename text (not content) on handle 0.


2. Families

cplay98 (~87 games) — Madou Monogatari, Puyo Puyo, …

artdi_98 (~52 games) — A-Train, Atlas II, Lunatic Dawn, …

Combined driver+stub ARTDI_98.COM (byte-identical across the family) overlay- loads a per-game engine EXE (named on conin handle 5, AH=4B03) and hooks INT 7Fh. Two song layouts:

mbmusp (~19 games) — Estate, Ce’st la vie, Coming Heart, …

mdrv_98 acidplan family (~10+ games) — Charm, Present Duo, Metal Mover, …

MDR external-voice (7 sets) — Zeta, Ginga Tetsudou, J.League, …

MUSE device family (~10 games) — NMUSE / MUSE2 / MUSE3 / SDD_2

MDRV98 + mlp_hoot family — Markadia, Twins, Camel-Zoo, Bakutotsu, Owl-Zoo


3. General levers (help beyond one family)


4. Measurement caveat — the index-0 stop-track

A smoke sweep that rips title index 0 false-negatives 146 pc98dos games whose first title is a stop/SE pseudo-track ([STOP], 演奏停止, 無音, SND OFF), which are silent by design. Real coverage is materially higher than an index-0 sweep shows. Use famtally.py, which selects the first non-stop title, or rip all titles and treat a designed-silent track as expected. The Markadia “regression” was entirely this artifact.


5. Verification

6. Still open (per-family RE remaining)