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 pathThe 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=$000EE8There 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:


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.
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.
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.
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.