Skip to content

DM1 FMTowns Guide

github-actions[bot] edited this page Aug 7, 2026 · 2 revisions

DM1 FM Towns — technical guide for Firestaff agents

This page is the single entry point any agent should read before touching DM1 FM Towns code. It consolidates the disc image format, the executable structure, symbol coordinates recovered by disassembly, CDDA layout, currently wired features and the concrete extraction workflow. All addresses come from the hash-verified HMA-240 English disc; the Japanese disc uses the same layout but a different executable (JDM.EXP) whose structural map is recorded separately — see the JDM section below.

Cross-game context

Before writing new FM Towns code, read docs/fmtowns/CROSS_GAME_COVERAGE.md for the complete cross-game byte-verified recovery tally. Several payloads are byte-identical across DM1/CSB/DM2 at per-game vaddrs:

  • Menu font raster (768 bytes, 3 games) — one implementation covers all three via dm1_v1_fmtowns_font_rasterise_string_pc34.
  • CHAR + ICON geometry (14+8 bytes, 3 games) — same constants.
  • SPELL_COSTS + SPELL_MULT (40 bytes, 3 games).
  • OICON descriptor (1344 bytes, DM1+CSB).
  • DYNA_BUTTONS pool (500+ bytes English labels, DM1+CSB).
  • PLAYER_COLOR + ICON_PAL (14 bytes, DM1+CSB).
  • Phar Lap 4-slot bridge (fs:[0x20/0x40/0x48/0x80], all 11 binaries).
  • Direct I/O port 0x04E9 (all 8 game/util binaries).

CSB CDATA/GRAPHICS.DAT (ext_v1, sig 0x8001) parses via the DM1 dm1_v1_fmtowns_pic_library_open_pc34 with a 2-byte prefix skip — see csb_v1_fmtowns_pic_library_open_ext_v1_pc34. DM2's ext_v4 format (sig 0x8004) has a different per-record layout and needs its own decoder.

See also docs/fmtowns/CROSS_GAME_INVENTORY.md for the earlier inventory doc that this coverage tally supersedes.

Font raster layout — round-trip verified

The 768-byte menu font raster loaded by dm1_v1_fmtowns_pic_library_load_menu_font_pc34 (asset 557) is laid out as 6 rows × 128 ASCII glyphs, each byte MSB-first with the CHAR_X_SIZE (=5) body pixels right-aligned in bits 4..0. Bits 7..5 are always zero. Verified 2026-08-07 by decoding the shipping Japanese Track 01 GRAPHICS.DAT and rendering ASCII 0x20..0x7f. See dm1_v1_fmtowns_font_rasteriser for the ready consumer + test_dm1_v1_fmtowns_font_rasteriser for the round-trip test (skips cleanly without FIRESTAFF_DM1_FMTOWNS_DATA_DIR).

The 768 bytes are byte-identical in CSB (@ file offset 0x50f1a in CDATA/GRAPHICS.DAT) and DM2 (@ 0x2f5a3 in DATA/GRAPHICS.DAT); one rasteriser covers all three games.

Complete FM Towns module list (as of 2026-08-07)

Byte-verified with real-data round-trip tests:

DM1-specific:

  • dm1_v1_fmtowns_text_geometry — CHAR/ICON macros
  • dm1_v1_fmtowns_icon_geometry — ICON constants
  • dm1_v1_fmtowns_icon_category — LOAD_ICON threshold table
  • dm1_v1_fmtowns_oicon_descriptor — 1344-byte OICON records
  • dm1_v1_fmtowns_dyna_buttons — English action label pool
  • dm1_v1_fmtowns_font_asset — asset 557 identity
  • dm1_v1_fmtowns_font_rasteriser — 6×128 raster decoder
  • dm1_v1_fmtowns_menu_regions — 23-block registry (994 records)
  • dm1_v1_fmtowns_menu_bss — 12 vaddrs + PLAYER schema
  • dm1_v1_fmtowns_egb_rect — region-tree walker
  • dm1_v1_fmtowns_menu_render — end-to-end composer
  • dm1_v1_fmtowns_music_tables — LEVEL_SONGS + SPELL_COSTS + DOOR_PAL etc.
  • dm1_v1_fmtowns_edm_sym1 — 1174 named EDM.EXP symbols
  • dm1_v1_fmtowns_snd_api — 36 SND_* function vaddrs
  • dm1_v1_fmtowns_tmenu_input — TMENU event schema
  • dm1_v1_fmtowns_tbios_id — 8 TBIOS version fingerprints
  • dm1_v1_fmtowns_pharlap_bridge — 4-slot Phar Lap layout
  • dm1_v1_fmtowns_direct_io — 5 direct I/O ports

CSB-specific (aliases for shared DM1 payloads + independent):

  • csb_v1_fmtowns_oicon_descriptor — aliases DM1 OICON via vaddr 0x27f77
  • csb_v1_fmtowns_dyna_buttons — aliases DM1 label pool via vaddr 0x29d50
  • csb_v1_fmtowns_tmenu_sym1 — 1724 CSB launcher symbols
  • csb_v1_fmtowns_pic_library — ext_v1 wrapper over DM1 pic_library

Shared cross-game infrastructure:

  • fmtowns_pharlap_all_games — 11 binary profiles + direct-I/O invariant
  • fmtowns_graphics_dat_format — legacy/ext_v1/ext_v4 classifier
  • fmtowns_geometry_all_games — per-game CHAR/ICON vaddrs
  • fmtowns_shared_tables_all_games — per-game SPELL/PLAYER/ICON vaddrs
  • fmtowns_font_raster_all_games — per-game font raster locations

Companion parity-evidence files (deep-decode references)

1. Retail media

  • Archive: ~/.firestaff/data/dm1/Dungeon Master (Japan) (En,Ja) (Rev 1).7z (or the same file without (Rev 1) for the earlier press). Both English and Japanese runtimes ship on the same disc.
  • Layout: 21-file BIN + one CUE. Track 01 is the data track, Tracks 02..20 are CDDA audio. The CUE names the multi-file layout; firestaff_fmtowns_disc.c parses the CUE and computes byte offsets.
  • Data track: MODE1/2048 ISO 9660 volume DUNGEON (5,056,800 bytes).
  • Audio tracks: raw 44.1 kHz signed-LE stereo, 2352-byte sectors.
  • Do not assume a uniform sector size across the disc — the audio offset computation must add data_track_end * 2048 for the data region and then (audio_start - data_track_end) * 2352 for the audio region. This is why the CSB CDDA parser (uniform 2352) cannot be reused for DM1 and why fmtowns_cue_parse_track_starts() is used instead. See test_dm1_v1_fmtowns_cd_audio.c for the exact math.

Track 01 file listing (ISO9660, /DUNGEON/)

File Purpose
AUTOEXEC.BAT Launches \\CONTROL.EXE → TownsOS shell
CONFIG.SYS DOS/TownsOS config
CONTROL.EXE TownsOS shell (owns TMENU)
RUN386.EXE Phar Lap DOS-extender
TBIOS.SYS / TBIOS.BIN FM Towns TownsOS graphics library
IO.SYS TownsOS I/O
DICUTY.COM TownsOS dictionary utility
OAK2USR.DIC / T_OAK2.EXE Japanese input method
DRIVE_R.IMG RAM disk image
TMENU.EXP / TMENU.ICN / TMENU.INF TownsOS file-browser launcher (owns the language-select menu, not the in-game menu)
EDM.EXP English game executable (Phar Lap P3, 310518 B)
JDM.EXP Japanese game executable
DATA/ English game data (GRAPHICS.DAT, DUNGEON.DAT, …)
JDATA/ Japanese game data

The hash-admitted pair for the English runtime is:

  • GRAPHICS.DAT MD5 same as the retained catalog entry — the fingerprint registry in include/firestaff_game_data_fingerprint.h covers both English and Japanese variants; only that registry admits a runtime.
  • DUNGEON.DAT for FM Towns has its own two fingerprints (EN 3DC0A932…, JP FE098F70…) — indices 113/114 (FINGERPRINT_COUNT = 115 in the header).

Materialization saves FMTOWNS.BIN and FMTOWNS.CUE into the runtime cache directory (m12_materialize_dm1_fmtowns_runtime_cache in src/shared/asset_status_m12.c) so the M11 CDDA dispatcher can read raw audio bytes without re-opening the 7z at runtime.

2. EDM.EXP — Phar Lap 386 (P3) executable

Level-1 P3 image, 310518 bytes. All values below are from the actual header via dm1_v1_fmtowns_startup.c:validate_p3_header.

Field Header offset Value
Magic 0x00 "P3"
Level 0x02 1
Header size 0x04 0x180
File size 0x06 0x46b4f
Runtime params off/sz 0x0c / 0x10 0x180 / 0x80
Relocations off/sz 0x14 / 0x18 0x200 / 0
Load image off/sz 0x26 / 0x2a 0x200 / 0x46941
Symbol table off/sz 0x2e / 0x32 0x46b41 / 0x51b5
Initial EIP 0x68 0x42a48
Memory requirements 0x74 0x77684

File offset of a runtime virtual address = load_image_offset (0x200) + vaddr. Every symbol value in the SYM1 table is a load-image virtual address.

SYM1 symbol table (0x46b41, 0x51b5 bytes, 1174 entries)

  • 4-byte magic "SYM1" at offset 0.
  • Entry count word at file+0x1e (relative to SYM1 base).
  • Records begin at cursor 0x22. Each record: {u8 name_size, char name[name_size], u32 value, u16 flags} — 6 bytes of tail past the name.
  • Names are ASCII, up to 127 bytes, non-empty.

The receipt at include/dm1_v1_fmtowns_startup.h already exposes the following recovered addresses on the English disc:

Symbol Vaddr Kind
DO_TITLE_ANIMATION 0xc3b0 fn
TITLE_PRESENTS 0x28f4a data
TITLE_DUNGEON 0x28f4c data
DRAW_DMENU 0x4620 fn
DYNAMENU 0x2418c data (8 B)
MENU_ICONS 0x2415c data (word)
CD_LEVEL_SONG 0x211d8 fn

3. Menu drawing chain — decoded

Full disassembly with symbol resolution is in parity-evidence/dm1_fmtowns_menu_p3_disassembly.md. Highlights so agents don't need to re-lift them:

  • DRAW_DMENU (0x4620, 240 B) — mode-gates on MENU_ICONS-vs-MENU_OWNER. Icon mode loops PARTY_SIZE times through DRAW_ICN_BUTTON (0x44f0). Dynamic mode draws a coloured panel via SPC_BLOT (0x1ccec), a main label from a stride-319 table at 0x26019 + MENU_OWNER * 319, and three button labels looked up through GET_LABEL (0x43e4).
  • GET_LABEL walks DYNA_BUTTONS (0x24194) as a NUL-separated string table; 0xFF sentinel returns 0x21d9c (blank label).
  • DYNAMENU[+2] / DYNAMENU[+3] are 0xFF-flagged colour overrides for the panel colour (default 0x0B, alternates 0x4D / 0x4F).
  • MOUSE_OFF (0xdd38) / MOUSE_ON (0xdd18) are cli-guarded reference-counted wrappers around MOS_DISP (0x21a40) using MSE_STATE (0x25848) as the hide depth.

DYNA_BUTTONS label pool (first 96 bytes, verified)

"N", "BLOCK", "CHOP", "X", "BLOW HORN", "FLIP", "PUNCH", "KICK",
"WAR CRY", "STAB", "CLIMB DOWN", "FREEZE LIFE", "HIT", "SWING",
"STAB", "THRU", …

These are the FM Towns action-menu button labels. Index 0 is the "N" placeholder glyph; every subsequent index is a real verb.

3a. Region table — locked coordinates for menu geometry

GET_RGN_COORD (0x194fc) is a 1:1-scale tail-call into GET_SCL_COORD (0x1942c), which walks a linked list of region blocks whose head pointer is stored at [0x28f08] (statically initialised to 0x28e78). The lookup routine at 0x18db4 iterates blocks matching first_id ≤ id ≤ last_id and returns the record at block+8 + (id-first_id)*8.

Each record is 8 bytes: type u16 | parent u16 | a i16 | b i16.

  • type == 9: (a, b) are (width, height) in pixels. The record is a size node.
  • Other types: (a, b) are anchor coordinates whose meaning is selected by GET_COORD (0x18df0) jump tables at 0x18f14 / 0x18fe6.
  • parent walks up the tree; GET_SCL_COORD requires the parent to be a type == 9 size node and returns parent_size * scale / 10000 (scale defaults to 10000, i.e. 1:1 when called via GET_RGN_COORD).

Byte-verified regions that back the current menu draw:

Region Record Meaning
10 type=9 parent=2 size=(87, 45) SPC_BLOT dynamic-menu panel size
11 type=2 parent=10 anchor=(319,77) Menu clear-area anchor (inherits 87×45)

The full 23-block registry (994 records) is enumerated byte-exact in parity-evidence/dm1_fmtowns_region_table.md; consult it before adding any new region binding.

4. TownsOS EGB primitives — locked coordinates

FILL_RECT (0x1fccc) and PIX_BLOT (0x1fe7c) route into the FM Towns TownsOS EGB graphics library. All calls take a persistent WORK block at 0x318d8 and stage a 4-word rect through EGBPARA at 0x360d8. The runtime destination page is WRITE_PAGE at 0x36170.

EGB primitive Vaddr Purpose
EGB_RESOLUTIONRAM 0x40739 Retarget EGB at a RAM raster
EGB_VIEWPORT 0x407a0 Clip rectangle
EGB_WRITEPAGE 0x407ec Select destination VRAM page
EGB_COLOR 0x40836 Set foreground/background colour
EGB_WRITEMODE 0x408a5 0 = plain copy, 6 = masked
EGB_PAINTMODE 0x408ed 0x20 = solid fill
EGB_PUTBLOCK 0x40bec Copy source raster to viewport
EGB_RECTANGLE 0x40ee5 Filled/outlined rectangle

Because these are documented TownsOS calls with published semantics, the menu draw does not require reverse-engineering custom pixel code. The bounded implementation task is a software EGB shim over the M11 framebuffer.

4a. Text rasteriser — DO_DRAW_CTEXT subtree

The menu draws all labels through the following call chain:

DO_FDRAW_CTEXT (0x1a8c0)   pad string to column count, NUL-terminate
  → DO_DRAW_CTEXT (0x1a804)  measure + place + rasterise
      → text_measure  (0x1a710)   glyph width sum via CHAR_X_SPC
      → text_place    (0x18df0)   same GET_COORD used by regions
      → DO_DRAW_TEXT  (0x1a664)   per-glyph loop
          → text_colourise (0x1a5a4)  1bpp → 4bpp packed
          → PIX_BLOT       (0x1fe7c)  EGB_PUTBLOCKCOLOR blit

Font base. Symbol TEXT_SIZE at vaddr 0x29344 is a dword pointer to a 768-byte 1bpp source raster, populated at INIT_TEXT. A pre-baked fg/bg-resolved 4bpp copy is kept at [0x293e4] (3072 bytes, EGB source stride 0x400).

Glyph geometry.

Quantity Symbol / value
Source pitch 8 pixels / char
Drawn width CHAR_X_SPC
Drawn height CHAR_Y_SPC - CHAR_X_SIZE + 1
Per-char advance CHAR_Y_HYT
Static default 5×6 cell, 6-pixel advance
Runtime source TEXT_PIC at 0x2934c

ASCII → glyph mapping. Direct: source_x = char_byte * 8. No -0x20 offset, no lookup table. Colour handling is done entirely in the pre-blit text_colourise (1bpp source → 4bpp packed: bit7 → low nibble, bit6 → high nibble, each nibble selects fg or bg); the blit itself is PIX_BLOT with colour = -1, i.e. a plain EGB_PUTBLOCKCOLOR copy.

The only piece not recoverable from the executable is the physical 2-D shape of the raster and the actual glyph bitmaps — those are loaded at INIT_TEXT and remain a bounded next task.

5. CDDA layout — 19 audio tracks (2..20)

Runtime mapping is in include/dm1_v1_fmtowns_cd_audio.h and src/dm1/dm1_v1_fmtowns_cd_audio.c.

Track Symbol / Role
02 DM1_FMTOWNS_TRACK_TITLE (title screen)
03 DM1_FMTOWNS_TRACK_HALL (Hall of Champions)
04 unused
05 DM1_FMTOWNS_TRACK_ENTRANCE_MAP6 (entrance + map 6)
06–17 per-map dungeon tracks (see map_to_track[])
13 DM1_FMTOWNS_TRACK_GAME_OVER
18 DM1_FMTOWNS_TRACK_GAME_WON
19–20 reserved / silence

dm1_v1_fmtowns_cd_track_for_map(map_index) maps dungeon map indices to tracks; dm1_v1_fmtowns_cd_track_for_event(evt) maps title/HoC/ game-over/game-won events to tracks.

Playback wiring (all live in src/engine/main_loop_m11.c and

src/engine/m11_game_view.c, gated on dm1FmtownsStartupReceiptValid)

  • Title (track 2)m11_open_requested_launch calls M11_GameView_PlayFmtownsCdda before the FM Towns title animation.
  • HoC (track 3)m11_publish_dm1_hoc_presented_capture_to_m12 dispatches when the HoC presented-capture receipt first goes ready.
  • Entrance (track 5)m11_play_redmcsb_entrance_transition dispatches before the door-open sequence, guarded against re-trigger via dm1FmtownsCddaCurrentTrack.
  • Map transitions — stair and teleporter transitions invoke dm1_v1_fmtowns_cd_track_for_map(toMapIndex).
  • Per-tick idle — the music loop starts the current map's track when nothing is playing.
  • Game events — game-over and game-won dispatch via dm1_v1_fmtowns_cd_track_for_event(2) / (3).
  • Music togglem11_dm1_stop_fmtowns_cdda when music is disabled.

Playback reads raw PCM bytes from the retained FMTOWNS.BIN at the mixed-sector-computed byte offset and hands them to M11_Audio_PlayCdda (16-bit signed LE stereo 44100 Hz).

6. Startup receipt — what is authenticated today

dm1_v1_fmtowns_startup_receipt validates before any FM Towns runtime opens:

  • Exact MD5s of AUTOEXEC.BAT, selected EDM/JDM, TMENU.EXP, TMENU.ICN, TMENU.INF.
  • Menu-program symbol references to TMENU.INF / TMENU.ICN / TMENU.EXP (menu_program_symbols_verified).
  • Game-program symbol references to DO_TITLE_ANIMATION, TITLE_PRESENTS, TITLE_DUNGEON, DRAW_DMENU, DYNAMENU, MENU_ICONS, CD_LEVEL_SONG (game_program_symbols_verified).
  • Bounded Phar Lap P3 header for both menu and game (menu_p3_header_verified, game_p3_header_verified).
  • English EDM.EXP: full SYM1 parse (1174 entries) with recorded addresses for the seven symbols listed above; the title-animation plan (GRAPHICS.DAT graphic index 1, PRESENTS/MASTER/zoom rects) bound from the P3 load image at 0xc3d1..0xc726.
  • TMENU.INF: two 128-byte records selecting \\JDM.EXP and \\EDM.EXP.
  • CD title/hall/entrance track ownership recorded on the receipt.

Japanese JDM.EXP is admitted at the header level but has no SYM1 table; its title animation and menu remain source-boundary-only. See section 6a for the structural map that unblocks JDM recovery.

6a. JDM.EXP (Japanese runtime) — structural map

JDM.EXP is 290,221 bytes; SHA-256 1db4f049…de0bae; initial EIP 0x00042cb4; load image at 0x200 (size 0x46bad); memory requirement 0x778f0. No SYM1 table: both header fields are 0 and the literal "SYM1" does not appear in the file. No English identifier strings (DRAW_DMENU, DYNAMENU, EGB_, …) appear either.

Same source, Japanese resources. The initial-EIP disassembly matches EDM.EXP byte-for-byte for 62 of 64 bytes — differing only in one call displacement (the Metaware / Phar Lap High-C run-time stub). JDM is therefore the same source rebuilt with Japanese resources rather than an independent build.

Japanese label pool. Located at vaddr 0x243bc (`EDM.DYNA_BUTTONS

  • 0x228`). All 15 verb slots decode from Shift-JIS with a 1:1 mapping to the English pool:
EDM label JDM (Shift-JIS)
BLOCK さえぎる
CHOP 叩き切る
BLOW HORN (recorded)
WAR CRY ときの声
full list in evidence

Larger dialog pool starts at 0x22000; asset paths in the executable use Q:\JDATA\... (versus EDM's \DATA\).

EGB library still linked. The trampoline signature 0f a0 68 10 01 00 appears at multiple JDM call sites; only the EGB_* symbol names are lost. Recovering them is a structural byte-fingerprint pass against EDM.

Recovery plan (recorded). Structural byte-fingerprint match against EDM.EXP → data-segment reference translation via string- anchored deltas → EGB-trampoline pattern enumeration → bind the Japanese label pool and JDATA/ path fixup. Full recipe in parity-evidence/dm1_fmtowns_jdm_structural_map.md.

JDM.EXP recovered symbol map

Two follow-up passes ship JDM-side vaddrs so a JDM-selected runtime can reach the same features EDM has:

  • 19 code + EGB-trampoline vaddrs recovered by masked byte- fingerprint match (evidence: dm1_fmtowns_jdm_symbol_recovery.md). All 8 EGB trampolines share a uniform +0x26c shift; menu / drawing / text code symbols shift per linked .OBJ (+0x78, +0xb4, +0xc0, +0x1f0, +0x2c8 observed). Encoded in include/dm1_v1_fmtowns_jdm_symbols.h; lookup via dm1_v1_fmtowns_jdm_symbol_vaddr_pc34("NAME").
  • 18 BSS-scalar vaddrs recovered by XREF triangulation and contiguous-block neighbor-delta derivation (evidence: dm1_fmtowns_jdm_bss_triangulation.md). Block shifts: menu-owner +0x228, screen/icon +0x264, character-metrics +0x276, party state +0x26c — non-uniform confirms the same-source-different-link finding. Encoded in include/dm1_v1_fmtowns_jdm_bss.h; lookup via dm1_v1_fmtowns_jdm_bss_vaddr_pc34("NAME").

6b. Font asset identity (INIT_TEXT / GET_MY_DECODED)

INIT_TEXT (EDM.EXP 0x1ae54) allocates a 768-byte buffer and calls GET_MY_DECODED(0xffffc22d, buf, 0, 0). Disassembling GET_MY_DECODED (EDM.EXP 0x9f04) reveals the encoded id:

0xc22d  = 0x8000 (direct-to-buffer) | 0x4000 (skip size header)
                | 0x022d (picture-library index 557)

So the FM Towns DM1 menu font is picture-library asset 557, decoded in direct-to-caller-buffer, no-header mode. The GET_MY_DECODED grammar (bit 15 = direct, bit 14 = no-header, bits 13..0 = index) is encoded in include/dm1_v1_fmtowns_font_asset.h. Full evidence in parity-evidence/dm1_fmtowns_font_asset.md.

6c. Picture-library container — DATA/GRAPHICS.DAT

The FM Towns DM1 picture library is DATA/GRAPHICS.DAT on the extracted disc. This is a distinct file format from the DM1 PC 3.4 GRAPHICS.DAT LZW container — different asset count, different compression, different header. Do not reuse the PC34 loader here.

  • Total size: 396,970 bytes
  • Header: u16 asset_count followed by asset_count × u32 size size table. Byte-verified asset_count = 575, so the header is 2 + 575 × 4 = 2,302 bytes.
  • Payload: 394,668 bytes = Σ size_table[i] for i = 0..574 (byte-exact match, no gaps or padding).
  • Asset span for index N begins at 2 + 575*4 + Σ size_table[0..N-1] and runs for size_table[N] bytes.

Encoded in include/dm1_v1_fmtowns_pic_library.h. The API returns zero-copy views into a caller-owned buffer that already contains GRAPHICS.DAT; no I/O is performed by the module itself. Full evidence in parity-evidence/dm1_fmtowns_pic_library_format.md.

The menu font at index 557 is exactly 768 bytes and stored uncompressed — it takes the DIRECT+NO_HDR path that bypasses DECODEGRAPHIC entirely (raw span memcpy'd into the caller buffer). load_raw_asset_pc34 implements this path.

6d. DECODEGRAPHIC RLE decoder — round-trip verified

DECODEGRAPHIC (EDM.EXP 0x1f63c) turns a compressed asset span into a nibble-packed 4bpp pixel matrix. Ported to Firestaff with byte-verified disassembly of all four leaf helpers: 0x1f4c4 (put_pixel), 0x1f518 (raw stream copy), 0x1f578 (nibble fill), 0x1f5d8 (row copy).

Per-asset header (first 4 bytes of every asset span):

+0  u16 width_pixels
+2  u16 height_pixels

Row stride: padded_width = (width + 0x1f) & ~0x1f (32-pixel alignment), row_bytes = padded_width / 2 (2 pixels per byte). When width == padded_width a fast-copy branch at 0x1f85f emits the on-disk bytes verbatim; otherwise the RLE loop runs.

RLE control-byte grammar:

bit 7 bit 6 count encoding
0 (ctrl >> 4) + 1 pixels literal
1 0 next-byte + 1
1 1 next-two-bytes-BE + 1

Bits 5..4 pick the mode: 00 raw stream copy (0x1f734), 01 single-pixel fill using ctrl & 0x0f (0x1f775), 11 row-copy with pixel nibble from stream (0x1f7f3).

Round-trip verification against the real DATA/GRAPHICS.DAT: 347 of 347 RLE-branch assets round-trip byte-exact — decoder consumes exactly size_table[index] source bytes and emits exactly padded_width/2 × height destination bytes for every asset. Two representative vectors in the test:

  • Asset 6: 80 × 14 pixels, consumes 185 source bytes
  • Asset 25: 144 × 73 pixels, consumes 565 source bytes

API: dm1_v1_fmtowns_pic_library_decode_asset_pc34() returns the 4bpp matrix and its (padded_width, height) dimensions. Test test_real_graphics_dat_rle_roundtrip is gated on the FIRESTAFF_DM1_FMTOWNS_GRAPHICS_DAT env var and skips silently when the file is absent — no game bytes are bundled.

7. What is wired, what is open

Wired (real-data only, gated on the receipt):

  • Runtime cache materialization from the retained 7z including FMTOWNS.BIN + FMTOWNS.CUE retention.
  • CDDA end-to-end: title, HoC, entrance, all 16 map transitions, ticks, events, music toggle.
  • FM Towns title animation: 18-frame reverse zoom from GRAPHICS.DAT graphic 1 via dm1_v1_fmtowns_title compositor, driven by the receipt's title-plan geometry.
  • Startup routes selected FM Towns editions around the PC34 SWSH → TITLE → ENTRANCE transaction — no PC34 presentation fallback.

Decoded and shipping as source-locked C (available today — just call the API, no re-lifting from the executable required):

Layer Module
Region geometry dm1_v1_fmtowns_menu_regions
DYNAMENU 8-byte record dm1_v1_fmtowns_dynamenu
English 44-label pool dm1_v1_fmtowns_dyna_buttons
Japanese 44-label pool dm1_v1_fmtowns_dyna_buttons_ja
Text/screen/icon geometry dm1_v1_fmtowns_text_geometry
Software EGB shim dm1_v1_fmtowns_egb_shim (fill / put)
JDM code + EGB trampolines dm1_v1_fmtowns_jdm_symbols (19 syms)
JDM BSS scalars dm1_v1_fmtowns_jdm_bss (18 scalars)
Font asset identity dm1_v1_fmtowns_font_asset
Picture library container dm1_v1_fmtowns_pic_library (575 ids)
DECODEGRAPHIC RLE decoder dm1_v1_fmtowns_pic_library (347/347)

What remains for the visible menu: bind the ten modules above into the M11 main loop. The bounded consumer skeleton is:

/* Look up the region rectangle */
DM1_V1_FmtownsRegionRecord panel, anchor;
dm1_v1_fmtowns_region_menu_panel_pc34(&panel);
dm1_v1_fmtowns_region_menu_clear_area_pc34(&anchor);

/* Compose the panel-colour and slot indices from live state */
uint8_t rec[DM1_V1_FMTOWNS_DYNAMENU_BYTES] = {...};
uint8_t col = dm1_v1_fmtowns_dynamenu_panel_colour_pc34(rec);

/* Fill the panel via the software EGB shim */
dm1_v1_fmtowns_egb_fill_rect_pc34(fb, W, H, W,
    anchor.a - panel.a, anchor.b,
    anchor.a - 1,       anchor.b + panel.b - 1, col);

/* Draw three labels */
for (int i = 0; i < 3; ++i) {
    uint8_t ix = dm1_v1_fmtowns_dynamenu_slot_label_pc34(rec, i);
    const char *s = language == JP
        ? dm1_v1_fmtowns_dyna_button_label_ja_pc34(ix)
        : dm1_v1_fmtowns_dyna_button_label_pc34(ix);
    /* rasterise s at (anchor.a - panel.a + PAD, anchor.b + 20*i)
     * using the font raster loaded from GRAPHICS.DAT index 557 */
}

Remaining bounded next steps (all non-synthetic):

  • Bind the picture-library decoder into the M11 asset pipeline so it opens ~/.firestaff/data/dm1/... (materialised cache) rather than requiring the caller to supply the GRAPHICS.DAT bytes.
  • Load font index 557 at boot into a persistent M11 slot; wire the text rasteriser subtree (section 4a) to sample from it.
  • Compose live DYNAMENU records from the current champion's action-hand state and drive the shim.
  • TMENU (TownsOS shell) interactive icon/layout rendering and mouse routes — separate scope, above the game executable.

Genuinely blocked (require external evidence):

  • No item in the menu-draw chain remains genuinely blocked. Every constant and format reachable via disassembly of the hash-verified EDM.EXP / JDM.EXP / DATA/GRAPHICS.DAT is now shipping source-locked C with tests.

8. Extracting data during development

Firestaff at runtime never unpacks the 7z. Development extraction uses standard host tools. From the repo root:

mkdir -p ~/scratch/fmtowns
cd ~/scratch/fmtowns
7z x -y ~/.firestaff/data/dm1/'Dungeon Master (Japan) (En,Ja) (Rev 1).7z' '*Track 01*'
# The Track 01 BIN is a plain ISO9660 image because it's MODE1/2048.
# Mount or bsdtar it to reach the files:
bsdtar -xf 'Dungeon Master (Japan) (En,Ja) (Rev 1) (Track 01).bin' -C extracted/
ls extracted/         # AUTOEXEC.BAT CONFIG.SYS EDM.EXP JDM.EXP TMENU.* DATA/ JDATA/ …

To parse the P3 header and dump the SYM1 name table:

import struct
p = open("extracted/EDM.EXP", "rb").read()
r32 = lambda o: struct.unpack("<I", p[o:o+4])[0]
sym_off, sym_sz = r32(0x2e), r32(0x32)
load_off = r32(0x26)                    # 0x200 for EDM.EXP
table, cursor, syms = p[sym_off:sym_off+sym_sz], 0x22, {}
while cursor < sym_sz:
    ns = table[cursor]; cursor += 1
    if not (0 < ns <= 127) or cursor + ns + 6 > sym_sz: break
    name = table[cursor:cursor+ns].decode("ascii", errors="replace")
    val  = int.from_bytes(table[cursor+ns:cursor+ns+4], "little")
    syms[name] = val
    cursor += ns + 6
# To pull code for a symbol, read `p[load_off + syms[name] : … ]`.

Disassembling a symbol range with capstone:

import capstone
md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
start_v = syms["DRAW_DMENU"]
end_v   = syms["FIZZLE_SPELL"]           # next symbol above DRAW_DMENU
for i in md.disasm(p[load_off+start_v : load_off+end_v], start_v):
    print(f"{i.address:#8x}: {i.mnemonic:<7} {i.op_str}")

9. Rules that apply to all FM Towns DM1 work

  • Never synthesise pixels when real data exists. The menu is blocked on decoded EGB shim work, not on a placeholder.
  • Never extract data at runtime — Firestaff must open the materialized cache only. All extraction is a development-time step.
  • Never bypass the startup receipt. Every playback and rendering path must gate on dm1FmtownsStartupReceiptValid.
  • Never restore the PC34 startup as a fallback when an FM Towns edition is selected.
  • Never use the CSB uniform-2352 CDDA parser for DM1 — the disc is mixed-sector. Use fmtowns_cue_parse_track_starts and compute the byte offset by hand (see test_dm1_v1_fmtowns_cd_audio.c).

10. Files to know

Purpose Path
Startup receipt include/dm1_v1_fmtowns_startup.h, src/dm1/dm1_v1_fmtowns_startup.c
CDDA track table + lookup include/dm1_v1_fmtowns_cd_audio.h, src/dm1/dm1_v1_fmtowns_cd_audio.c
BIN/CUE + disc I/O include/firestaff_fmtowns_disc.h, src/shared/firestaff_fmtowns_disc.c
ISO9660 walker src/dm1/dm1_v1_fmtowns_iso9660.c
Title compositor include/dm1_v1_fmtowns_title.h, src/dm1/dm1_v1_fmtowns_title.c
Runtime cache materializer src/shared/asset_status_m12.c (m12_materialize_dm1_fmtowns_runtime_cache)
Runtime wiring src/engine/main_loop_m11.c, src/engine/m11_game_view.c
Fingerprints include/firestaff_game_data_fingerprint.h, src/shared/firestaff_game_data_fingerprint.c
Menu disassembly evidence parity-evidence/dm1_fmtowns_menu_p3_disassembly.md
Region table evidence parity-evidence/dm1_fmtowns_region_table.md
Text rasteriser evidence parity-evidence/dm1_fmtowns_text_rasteriser.md
JDM.EXP structural map parity-evidence/dm1_fmtowns_jdm_structural_map.md
JDM.EXP symbol recovery parity-evidence/dm1_fmtowns_jdm_symbol_recovery.md
JDM.EXP BSS triangulation parity-evidence/dm1_fmtowns_jdm_bss_triangulation.md
Font asset identity parity-evidence/dm1_fmtowns_font_asset.md
Picture library + RLE parity-evidence/dm1_fmtowns_pic_library_format.md
Menu region constants include/dm1_v1_fmtowns_menu_regions.h, src/dm1/dm1_v1_fmtowns_menu_regions.c
DYNAMENU record helpers include/dm1_v1_fmtowns_dynamenu.h, src/dm1/dm1_v1_fmtowns_dynamenu.c
English label pool include/dm1_v1_fmtowns_dyna_buttons.h, src/dm1/dm1_v1_fmtowns_dyna_buttons.c
Japanese label pool include/dm1_v1_fmtowns_dyna_buttons_ja.h, src/dm1/dm1_v1_fmtowns_dyna_buttons_ja.c
Text/screen/icon geometry include/dm1_v1_fmtowns_text_geometry.h, src/dm1/dm1_v1_fmtowns_text_geometry.c
Software EGB shim include/dm1_v1_fmtowns_egb_shim.h, src/dm1/dm1_v1_fmtowns_egb_shim.c
JDM symbol vaddr map include/dm1_v1_fmtowns_jdm_symbols.h, src/dm1/dm1_v1_fmtowns_jdm_symbols.c
JDM BSS vaddr map include/dm1_v1_fmtowns_jdm_bss.h, src/dm1/dm1_v1_fmtowns_jdm_bss.c
Font asset identity include/dm1_v1_fmtowns_font_asset.h, src/dm1/dm1_v1_fmtowns_font_asset.c
Picture library + RLE include/dm1_v1_fmtowns_pic_library.h, src/dm1/dm1_v1_fmtowns_pic_library.c
Tests tests/test_dm1_v1_fmtowns_cd_audio.c, tests/test_dm1_v1_fmtowns_title.c, tests/test_firestaff_fmtowns_disc.c

11. Cross-references

  • DM2 FM Towns status and layout (also disc-image based): see the DM2 FM Towns notes tracked by the DM2 lane in TODO.md.
  • CSB FM Towns CDDA runtime: src/csb/csb_v1_fmtowns_cdda_* — do not reuse for DM1 (uniform 2352 vs mixed sectors).
  • ReDMCSB DM1 reference source and DMWeb/Greatstone documentation remain the authoritative outside references; the FM Towns disc's Phar Lap P3 layer is the only piece unique to this port.

Clone this wiki locally