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 and currently wired features. 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:

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:

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

Shared cross-game infrastructure:

Companion parity-evidence files (deep-decode references)

1. Retail media

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:

The original archive remains the source owner. M11 reads the CUE and BIN members directly into bounded process memory when CDDA is needed; it does not materialize a FMTOWNS.BIN, FMTOWNS.CUE, or loose game-data cache.

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)

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:

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.

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)

Playback reads raw PCM bytes from the original BIN member 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:

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

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:

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.

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:

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):

Recovered source structures and production path:

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
Legacy software EGB test shim dm1_v1_fmtowns_egb_shim (synthetic)
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)

The active M11 menu loop calls m11_draw_dm1_fmtowns_dmenu_backdrop in src/engine/m11_game_view.c. It loads authentic GRAPHICS.DAT asset 10 (C010) through the asset loader, selects the source crop from the DYNAMENU sentinels, and draws English action labels. Region IDs 11/77/79 are destinations, not colours. The standalone menu-render adapter and EGB shim remain synthetic test-era code; they are not called by this active loop and must not be used to infer production pixels.

Remaining parity work is to compare the full menu against original captures, especially Japanese text and the icon-mode route. Loading the C010 background alone does not establish complete pixel parity. TMENU (TownsOS shell) interactive icon/layout rendering and mouse routes remain separate scope.

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

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
Archive-owned runtime media src/shared/asset_status_m12.c, src/engine/m11_game_view.c
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