case study7 min read$1B

The Debug Menu Was In The ROM The Whole Time

For months I've been inferring Marvel vs. Capcom's animation structures from bytes -- which group index means what, which pointer is a spritelist. It turns out Capcom shipped a SECRET TEST MENU in the retail ROM that names them out loud. The master debug flag is bit 15 of a single word at $C0, my static search for it failed for an instructive reason, and a MAME read tap found it in one 59-second headless run. The HIT EDITER now shows animation names, group indices and live CG pointers -- the labelled ground truth this whole series has been reconstructing the hard way.

cps2reverse-engineeringmarvel-vs-capcommshvsfdebug-mode
CPS-2 Reverse EngineeringPart 16 of 17
Browse all writing
On this page
Evidence Strip

Goal: Find out whether Marvel vs. Capcom's documented-but-unverified debug mode is real in our ROM, and if so, get into it -- because the character-port work has been inferring animation-table semantics statically for months.

Constraints: The published flag address ($C0) was documented for a different game in the family, and the same source notes another title uses a different address entirely. So it had to be verified, not assumed.

Approach: A static search first (which failed, instructively), then a MAME Lua read tap over low ROM logging every (address, PC) pair -- letting the game show me where its own flag lives.

Result: $C0 is real for both MvC and MSHvSF. Only bit 15 matters. The SECRET TEST MENU boots, and HIT EDITER runs -- displaying animation names, group indices, live CG pointers and all eight hitbox slots.

Proof / Validation: Screenshots below, captured headless from a clean verified mvsc romset in MAME 0.287.

The Setup

A question came in from a different direction entirely: could Street Fighter Zero (CPS Changer) help this project? It reportedly has a debug mode that unlocks a lot of the game.

The answer was no, twice over -- it's CPS-1 hardware, and a structural test I ran across six Capcom fighters found the per-character animation directory in every CPS-2 fighter and nothing in the CPS Changer ROM at any threshold. Wrong hardware, wrong data.

But chasing that down surfaced the thing that mattered: a survey of CPS-2 debug switches listing the games that have them. Marvel Super Heroes vs Street Fighter and Marvel vs Capcom are both on it. The games I've been reverse-engineering all along ship with a debug mode.

The Search That Failed, And Why That's The Interesting Part

The write-up gives the master debug flag as a word at ROM $C0, set to $FFFF -- for X-Men vs Street Fighter "and most Marvel games". It also notes X-Men: Children of the Atom uses $D24 instead. So the address varies, and I couldn't just assume it.

First pass, static. In the data view, $C0 is a zeroed word in both games -- consistent with a defaulted-off flag, but weak evidence; plenty of words are zero. So: find the code that reads it. I searched every absolute-addressing form -- absolute short (4A78 00C0, 3038 00C0, ...), absolute long (... 000000C0).

Nothing. In either game.

I wrote that up as "unconfirmed, resolve it live." Which was the right call, but for a slightly wrong reason -- I'd framed it as maybe these games use a different address. The real problem was my search. Here's what the game actually does:

000EAA  movea.l #$c0, a0            ; debug block base into a register
000EB0  tst.b   $4(a0)              ; sub-flag at $C4
...     move.b  $5(a0)..$f(a0), $79xx(a5)
000F2C  move.w  (a0), -$3b74(a5)    ; the MASTER FLAG word at $C0
000F30  tst.w   -$3b74(a5)
000F34  bpl.b   $f94                ; bit 15 clear -> skip the entire debug path

The flag is read register-indirect through a0. The literal $C0 appears once, as an immediate operand to movea.l, and every actual access is (a0) or $4(a0). No absolute-addressing pattern could ever have found it.

That's the generalisable bit: a negative result from a pattern search is only as strong as the addressing modes the pattern covers. I'd have kept believing "no reference exists" indefinitely.

Letting The Game Answer

The fix is to stop asking the ROM and ask the running machine. MAME's Lua API can install a read tap over an address range and fire a callback on every access:

local space = cpu.spaces['program']
local tap = space:install_read_tap(0x0, 0x1FFF, 'lowrom', function(offset, data, mask)
  local e = seen[offset]
  if not e then e = { n = 0, pc = pc() }; seen[offset] = e; order[#order+1] = offset end
  e.n = e.n + 1
  return data
end)

Tap the whole low-ROM region, log which addresses get read and from which PC, run the boot and attract sequence headless. One 59-second run:

$0000C0  reads=1        first_PC=$000F2E
$0000C2  reads=1        first_PC=$000EC4
$0000C4  reads=4        first_PC=$000EB2
$0000C6  reads=3        first_PC=$000ED6
$0000C8  reads=114      first_PC=$0006E2
$0000CA  reads=2        first_PC=$000EE8

There it is. $C0 is read exactly once, from $000F2E -- and it isn't alone. $C0 through $CF form a whole debug flag block, read by a cluster of code in $000EB2-$000F2E. Disassemble there and the structure above falls out immediately: $C0 is the master word, $C4-$CF are per-feature sub-flags that get copied into work RAM.

Only bit 15 of $C0 matters. The test is tst.w followed by bpl -- branch if plus. $FFFF works because it happens to set the top bit, not because the value itself is special.

And both games have it, in near-identical code:

What's Behind The Door

Set the flag, toggle Service Mode with P1 Button 1 held, and:

Marvel vs Capcom SECRET TEST MENU listing HIT EDITER, CATCH EDITER, ENEMY SENSEI, ENDING TEST, MESSAGE TEST, SCRL MOVE TEST, three SCRL BLOCK entries, KAO TEST, HIT TEST and EXIT
SECRET TEST MENU // twelve entries Capcom left in the retail ROM
The HIT EDITER screen in CHAR SELECT MODE, showing per-side readouts for animation name, Char Gp, PosX/PosY, CG_Cnt, Now_Cnt and CG_Ptr, with HIT 0 through HIT 7 hitbox slot bars and a character sprite centred on a debug grid
HIT EDITER // the animation, named and indexed by the game itself

Twelve entries: HIT EDITER, CATCH EDITER, ENEMY SENSEI, ENDING TEST, MESSAGE TEST, SCRL MOVE TEST, three scroll-layer viewers, KAO TEST, HIT TEST, EXIT.

HIT EDITER is the one I care about, and it is almost comically on-the-nose for this project. Per side it displays:

  • The animation's name, in plain text. 00 Dachi / STAND FOOTWORK. (Dachi = 立ち, "standing".) The game names its own animations.
  • Char Gp -- the animation group index. That is the L1 index of the two-level animation tree I've spent this series walking byte by byte.
  • CG_Ptr. -- the live graphics pointer. That is the spritelist pointer each 16-byte animation record carries at +4.
  • CG_Cnt. / Now_Cnt. -- frame counters, with the total in parentheses.
  • HIT 0-HIT 7 -- all eight hitbox slots, as bars.

Everything this series has been inferring from static bytes, the game will simply tell you, with names attached.

What This Changes

Some honest scoping, because the project rule is no false success claims.

Verified: MvC, live, on a clean checksum-verified romset. The screenshots above are from a headless run I can reproduce on demand.

Not verified live: MSHvSF. The debug block is confirmed statically -- same code, same address, $000F0E -- but I don't currently have an MSHvSF romset MAME will boot. My decrypted set is revision G under encrypted chip names; MAME's decrypted clone wants revision 1. Static-only, and I'm labelling it that way.

Correction (same day)

This turned out to be wrong, and the reason is worth recording. I did have a perfect mshvsfj set -- zero bad CRCs -- sitting in the same ROM folder. It was missing exactly one file: its 20-byte decryption key, which was also already on disk in a different directory. Adding it gives romset mshvsfj is good, and MSHvSF's secret test menu, HIT EDITER and ENEMY SENSEI all run.

I had tested two other MSHvSF sets, both phoenix-decrypted, which produce garbage tiles because MAME decrypts already-plain data -- and generalised from that to "no bootable set." When a romset won't boot, check -verifyroms for a missing file before concluding the data is bad. See the follow-up.

Not done yet, and it's the actual prize: reading the HIT EDITER's state out of RAM each frame and correlating Char Gp and CG_Ptr against the tree my static tools walk. That turns an inferred group/sub numbering into a named, game-confirmed map, and validates the record schema against the engine instead of against my own parser. That's the next session.

There's also a quieter benefit for the Saturn work. The Saturn port carries the arcade spritelists but re-authored the sequencing -- so knowing precisely what the arcade animation tree means, with Capcom's own labels, is a prerequisite for recognising what the Saturn replaced it with.

Two MAME-Lua gotchas, so you don't lose the hour I did

Drive ioport fields with field:set_value(v). Assigning field.field = 1 doesn't error -- it silently kills the frame notifier, so your script appears to run and then does nothing.

Snapshots need a real video target. -video none is great for tracing but produces no images; use -video soft -window when you want screenshots.

Where this connects

The animation tree this menu labels is the same structure the Shuma-Gorath native port relocates into MvC, and the same one the Saturn port turns out not to carry at all. The detour that started this -- "can the CPS Changer version help?" -- was a dead end on its own terms, and still worth taking.

Written by Daniel Plas Rivera · 1,567 words · $1B

ShareXLinkedIn