-
-
Notifications
You must be signed in to change notification settings - Fork 4
DM1 FMTowns Guide
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.
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.
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.
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
-
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.
- 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.
| 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).
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.
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.
- 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 |
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 draws a coloured panel viaSPC_BLOT(0x1ccec), 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[+2]/DYNAMENU[+3]are0xFF-flagged colour overrides for the panel colour (default0x0B, alternates0x4D/0x4F). -
MOUSE_OFF(0xdd38) /MOUSE_ON(0xdd18) arecli-guarded reference-counted wrappers aroundMOS_DISP(0x21a40) usingMSE_STATE(0x25848) as the hide depth.
"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.
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.
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.
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.
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.
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 retained FMTOWNS.BIN at the
mixed-sector-computed byte offset and hands them to
M11_Audio_PlayCdda (16-bit signed LE stereo 44100 Hz).
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.
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.
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").
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.
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.
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.
Wired (real-data only, gated on the receipt):
- Runtime cache materialization from the retained 7z including
FMTOWNS.BIN+FMTOWNS.CUEretention. - 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.
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
DYNAMENUrecords 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.DATis now shipping source-locked C with tests.
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}")- 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_startsand compute the byte offset by hand (seetest_dm1_v1_fmtowns_cd_audio.c).
| 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
|
- 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.