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:
- 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 macrosdm1_v1_fmtowns_icon_geometry— ICON constantsdm1_v1_fmtowns_icon_category— LOAD_ICON threshold tabledm1_v1_fmtowns_oicon_descriptor— 1344-byte OICON recordsdm1_v1_fmtowns_dyna_buttons— English action label pooldm1_v1_fmtowns_font_asset— asset 557 identitydm1_v1_fmtowns_font_rasteriser— 6×128 raster decoderdm1_v1_fmtowns_menu_regions— 23-block registry (994 records)dm1_v1_fmtowns_menu_bss— 12 vaddrs + PLAYER schemadm1_v1_fmtowns_egb_rect— region-tree walkerdm1_v1_fmtowns_menu_render— end-to-end composerdm1_v1_fmtowns_music_tables— LEVEL_SONGS + SPELL_COSTS + DOOR_PAL etc.dm1_v1_fmtowns_edm_sym1— 1174 named EDM.EXP symbolsdm1_v1_fmtowns_snd_api— 36 SND_* function vaddrsdm1_v1_fmtowns_tmenu_input— TMENU event schemadm1_v1_fmtowns_tbios_id— 8 TBIOS version fingerprintsdm1_v1_fmtowns_pharlap_bridge— 4-slot Phar Lap layoutdm1_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 0x27f77csb_v1_fmtowns_dyna_buttons— aliases DM1 label pool via vaddr 0x29d50csb_v1_fmtowns_tmenu_sym1— 1724 CSB launcher symbolscsb_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 invariantfmtowns_graphics_dat_format— legacy/ext_v1/ext_v4 classifierfmtowns_geometry_all_games— per-game CHAR/ICON vaddrsfmtowns_shared_tables_all_games— per-game SPELL/PLAYER/ICON vaddrsfmtowns_font_raster_all_games— per-game font raster locations
Companion parity-evidence files (deep-decode references)
dm1_fmtowns_menu_p3_disassembly.md— DRAW_DMENU, DRAW_ICN_BUTTON, GET_LABEL, MOUSE_OFF/ON, SPC_BLOT, FILL_CSCREEN + TownsOS EGB primitive table.dm1_fmtowns_region_table.md— GET_SCL_COORD / GET_RGN_COORD / GET_COORD, the 23-block region registry at[0x28f08], byte-exact region records including regions 10 and 11.dm1_fmtowns_text_rasteriser.md— DO_DRAW_CTEXT subtree, TEXT_SIZE font base, ASCII→glyph mapping, colourisation.dm1_fmtowns_jdm_structural_map.md— JapaneseJDM.EXPP3 header, Shift-JIS label pool, initial-EIP fingerprint match with EDM, EGB-trampoline recovery plan.dm1_fmtowns_jdm_symbol_recovery.md— 19 JDM.EXP symbols recovered by masked byte-fingerprint (10 code + 8 EGB trampolines + 1 data pool); per-.OBJ non-uniform shifts.dm1_fmtowns_jdm_bss_triangulation.md— 18 BSS scalars recovered by XREF triangulation + neighbor-delta derivation.dm1_fmtowns_font_asset.md— INIT_TEXT + GET_MY_DECODED decode; menu font = picture-library index 557, direct-to-buffer- no-header bits, 768-byte allocation.
dm1_fmtowns_pic_library_format.md—DATA/GRAPHICS.DATcontainer (575 assets, 2 + 575×4-byte header + payload) plus DECODEGRAPHIC RLE decoder with byte-verified leaf-helper decodes and round-trip vectors.
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.cparses 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 * 2048for the data region and then(audio_start - data_track_end) * 2352for the audio region. This is why the CSB CDDA parser (uniform 2352) cannot be reused for DM1 and whyfmtowns_cue_parse_track_starts()is used instead. Seetest_dm1_v1_fmtowns_cd_audio.cfor 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.DATMD5 same as the retained catalog entry — the fingerprint registry ininclude/firestaff_game_data_fingerprint.hcovers both English and Japanese variants; only that registry admits a runtime.DUNGEON.DATfor FM Towns has its own two fingerprints (EN3DC0A932…, JPFE098F70…) — indices 113/114 (FINGERPRINT_COUNT= 115 in the header).
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)
- 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 loopsPARTY_SIZEtimes throughDRAW_ICN_BUTTON(0x44f0). Dynamic mode blits graphic C010 throughSPC_BLOT(0x1ccec) to a destination region selected from DYNAMENU’s sentinels, then draws a main label from a stride-319 table at0x26019 + MENU_OWNER * 319, and three button labels looked up throughGET_LABEL(0x43e4).GET_LABELwalksDYNA_BUTTONS(0x24194) as a NUL-separated string table;0xFFsentinel returns0x21d9c(blank label).DYNAMENU[+3] == 0xFFselects destination region 77; thenDYNAMENU[+2] == 0xFFselects region 79 and overrides it. Otherwise the destination is region 11. These are not colour values.MOUSE_OFF(0xdd38) /MOUSE_ON(0xdd18) arecli-guarded reference-counted wrappers aroundMOS_DISP(0x21a40) usingMSE_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 byGET_COORD(0x18df0) jump tables at0x18f14/0x18fe6. parentwalks up the tree;GET_SCL_COORDrequires the parent to be atype == 9size node and returnsparent_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_launchcallsM11_GameView_PlayFmtownsCddabefore the FM Towns title animation. - HoC (track 3) —
m11_publish_dm1_hoc_presented_capture_to_m12dispatches when the HoC presented-capture receipt first goes ready. - Entrance (track 5) —
m11_play_redmcsb_entrance_transitiondispatches before the door-open sequence, guarded against re-trigger viadm1FmtownsCddaCurrentTrack. - 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 toggle —
m11_dm1_stop_fmtowns_cddawhen music is disabled.
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:
- Exact MD5s of
AUTOEXEC.BAT, selectedEDM/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.EXPand\\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+0x26cshift; menu / drawing / text code symbols shift per linked .OBJ (+0x78,+0xb4,+0xc0,+0x1f0,+0x2c8observed). Encoded ininclude/dm1_v1_fmtowns_jdm_symbols.h; lookup viadm1_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 ininclude/dm1_v1_fmtowns_jdm_bss.h; lookup viadm1_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_countfollowed byasset_count × u32 sizesize table. Byte-verifiedasset_count = 575, so the header is2 + 575 × 4 = 2,302bytes. - 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 forsize_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):
- ZIP BIN/CUE media stays archive-owned. The runtime reads it in memory and creates no loose game-data cache.
- 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.DATgraphic 1 viadm1_v1_fmtowns_titlecompositor, driven by the receipt’s title-plan geometry. - Startup routes selected FM Towns editions around the PC34
SWSH → TITLE → ENTRANCEtransaction — no PC34 presentation fallback.
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
- Never synthesise pixels when real data exists. The active M11 menu path blits authenticated asset 10 (C010); the standalone software EGB shim remains synthetic legacy test code and is not used by that path.
- 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_startsand compute the byte offset by hand (seetest_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 |
| 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
- 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.