dm2-saves: reverse the Sega CD BRAM save format (add SaveVariant::SegaCd) #158

Open
opened 2026-07-03 10:17:21 +02:00 by jqueuniet · 0 comments
jqueuniet commented 2026-07-03 10:17:21 +02:00 (Migrated from codeberg.org)

Reverse the Sega CD BRAM save format and add a SaveVariant::SegaCd to
dm2-saves (currently: DosRetail, MacRetail, DosBetaV09).

Context

A DM2 US Sega CD saved game was captured from BlastEm's Sega CD internal
backup RAM (internal.bram, 8 KB). It holds one real save: payload runs
~0x400x14a0 (~5 KB), with a directory-entry pair above the standard
SEGA_CD_ROM / RAM_CARTRIDGE___ BRAM format footer. The RAM-cart image
(cart.bram, 128 KB) was formatted but empty. These are BRAM (saved-game
data), not emulator save states — so they carry the game's own save format,
nothing else.

Why the existing decoder can't read it

dm2-saves targets SkSave.dat, whose three variants share one on-disk
layout: mostly plaintext (42-byte header, embedded dungeon state, raw
section blocks) with only a trailing suppression bitstream compressed. Two
facts rule that layout out for the Sega CD BRAM:

  1. Size. A retail/Beta SkSave.dat is ~45.9 KB; the compressed bitstream
    alone is ~6.3–6.9 KB. The entire Sega CD internal BRAM is 8 KB. A
    plaintext-layout save cannot fit.
  2. Shape. The BRAM payload is high-entropy from the first byte
    (18fe 0001 … a39b … 9360), with no plaintext header — w_timer_flag
    does not read as 1 at the expected offset under either endian. The
    DOS/Mac format is not high-entropy up front (readable map/record tables).

So the Sega CD save is a genuinely different, tighter encoding built to fit
BRAM — not the existing layout byte-swapped. Nothing in the crate reads it
today.

Goal

  • Reverse the Sega CD save/load routines and document the format under
    docs/formats/.
  • Add SaveVariant::SegaCd + a looks_like_* predicate and parse/write path.
  • Once the layout is understood, wire it into convert.rs so Sega CD ↔
    DOS/Mac conversion becomes possible (the Sega CD is big-endian 68000 like
    the Mac variant, so shared sub-structures — champion records, object tables
    — may be recognizable once decompressed; the Mac decoder is the reference).

Method

  • The save/load routines live in SKUL.FTL on the Sub-CPU — the same
    module and the same BlastEm Sub-CPU tracing workflow built for the palette
    work (tooling/palette-oracle/blastem-debugger.md), so that tooling
    transfers directly.
  • First parse the Sega CD BIOS BRAM directory (entries grow down from the
    footer) to recover the save's true start block and length — the two entries
    in this sample are not clean 11-char filenames at the obvious offsets, so
    the directory model needs care before the blob can be cleanly extracted.

Difficulties

  • Undocumented, compressed format; only one sample so far and no spec — early
    reversing is guess-and-check against a single save. Producing several
    in-game saves at known states → more BRAM captures would give an oracle.
  • Captures are derived from copyrighted game data, so BRAM images stay
    gitignored (kept in scratch/), not committed as fixtures.
Reverse the Sega CD BRAM save format and add a `SaveVariant::SegaCd` to `dm2-saves` (currently: DosRetail, MacRetail, DosBetaV09). ## Context A DM2 US Sega CD saved game was captured from BlastEm's Sega CD internal backup RAM (`internal.bram`, 8 KB). It holds one real save: payload runs ~`0x40`–`0x14a0` (~5 KB), with a directory-entry pair above the standard `SEGA_CD_ROM` / `RAM_CARTRIDGE___` BRAM format footer. The RAM-cart image (`cart.bram`, 128 KB) was formatted but empty. These are BRAM (saved-game data), not emulator save states — so they carry the game's own save format, nothing else. ## Why the existing decoder can't read it `dm2-saves` targets `SkSave.dat`, whose three variants share one on-disk layout: mostly *plaintext* (42-byte header, embedded dungeon state, raw section blocks) with only a trailing suppression bitstream compressed. Two facts rule that layout out for the Sega CD BRAM: 1. **Size.** A retail/Beta `SkSave.dat` is ~45.9 KB; the compressed bitstream alone is ~6.3–6.9 KB. The entire Sega CD internal BRAM is 8 KB. A plaintext-layout save cannot fit. 2. **Shape.** The BRAM payload is high-entropy from the first byte (`18fe 0001 … a39b … 9360`), with no plaintext header — `w_timer_flag` does not read as `1` at the expected offset under either endian. The DOS/Mac format is not high-entropy up front (readable map/record tables). So the Sega CD save is a genuinely different, tighter encoding built to fit BRAM — not the existing layout byte-swapped. Nothing in the crate reads it today. ## Goal - Reverse the Sega CD save/load routines and document the format under `docs/formats/`. - Add `SaveVariant::SegaCd` + a `looks_like_*` predicate and parse/write path. - Once the layout is understood, wire it into `convert.rs` so Sega CD ↔ DOS/Mac conversion becomes possible (the Sega CD is big-endian 68000 like the Mac variant, so shared sub-structures — champion records, object tables — may be recognizable once decompressed; the Mac decoder is the reference). ## Method - The save/load routines live in `SKUL.FTL` on the **Sub-CPU** — the same module and the same BlastEm Sub-CPU tracing workflow built for the palette work (`tooling/palette-oracle/blastem-debugger.md`), so that tooling transfers directly. - First parse the Sega CD BIOS BRAM directory (entries grow down from the footer) to recover the save's true start block and length — the two entries in this sample are not clean 11-char filenames at the obvious offsets, so the directory model needs care before the blob can be cleanly extracted. ## Difficulties - Undocumented, compressed format; only one sample so far and no spec — early reversing is guess-and-check against a single save. Producing several in-game saves at known states → more BRAM captures would give an oracle. - Captures are derived from copyrighted game data, so BRAM images stay gitignored (kept in `scratch/`), not committed as fixtures.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jqueuniet/dm2-tools#158
No description provided.