Goal: Transform scattered reverse engineering notes into reproducible, machine-executable workflows that produce verifiable results.
Constraints: Analysis targets a binary with no source code. Multiple contributors (human + AI). Findings must be reproducible across sessions and tools. Must validate claims against the actual ROM.
Approach: Established "observe → instrument → validate → automate → publish" loop. Built Python scripts that produce tables/patches from ROM data (not screenshots). Embedded verification into the artifact itself via ULTRA SETTINGS in-game diagnostic screen.
Result: 22 diagnostic values verified in real-time in the running game. Cross-game code similarity analysis: 1,313 shared code blocks between MSHvsSF and MvC1 (relocated, not rewritten). Bank table compatibility confirmed (identical structure at different offsets). Graphics tile analysis initially reported <0.35% shared tiles and concluded characters were redrawn per game -- that conclusion was later disproven (see Editor's update below): the art is the same content stored at different GFX banks, and the animation/frame tables are byte-identical between games.
Proof / Validation: ULTRA SETTINGS screen displays computed values from the ROM. If ROM is corrupted, displayed values change. Decompiled functions checked against ROM bytes. Build pipeline is deterministic.
Artifacts: Three classes, each machine-checkable. A decompilation corpus (one C translation per recovered function, plus memory-map and character-ID headers). An analysis-script set that reads the ROM and emits tables and patches rather than screenshots. And structured documentation generated from those outputs, so the prose and the binary cannot drift apart silently.
This post turned out to be a case study in its own thesis: reproducible workflows exist so that wrong conclusions can be overturned by better instrumentation. Several graphics conclusions in the original version have since been disproven and are corrected in place below:
- "Graphics are NOT compatible / characters redrawn per game" — wrong. MSHvSF → MvC1 animation and frame-data tables are byte-identical (mode-2 framedata, 759/759 blocks verified, pixel-proven in the running game with an unchanged MvC decoder). The sprite art is the same content stored at different GFX bank addresses, not redrawn art.
- The "<0.35% shared tiles" figure came from comparing tile content at matching addresses. Once bank relocation is accounted for, the tiles match -- the comparison methodology, not the art, produced the near-zero number.
- The "Direct Import Failed - Scrambled" result came from a same-offset copy that skipped the bank remapping step. Copying tiles to a free GFX bank and pointing the (structurally compatible) bank tables at them renders correctly.
- The corrected porting model: character data ports by porting the tables and remapping tiles to a free GFX bank -- far less work than the "2-6 months per character" estimate originally given here. A later correction to the correction (July 21): that covers the data half only. Making a ported character fight required first a donor-chassis architecture and ultimately native code relocation with a cumulative chain of live-gated fixes -- the behavior half is real engineering, not remapping.
The core code findings stand: 1,313 shared code blocks between MSHvsSF and MvC1, same engine, code relocated rather than rewritten.
When working on complex technical analysis, you accumulate memory maps, byte offsets, pattern hypotheses, and architectural diagrams. But when you ask an AI coding assistant to help, it often fails because your documentation isn't structured for execution. This guide transforms scattered knowledge into reproducible workflows -- culminating in an in-game verification screen that proves every claim in the documentation.
A good blog post is a snapshot. A good engineering repo is a machine.
Build operational documentation that enables reproducible results: observe → instrument → validate → automate → publish.
What a CPS2 RE Repo Should Contain
- Source of truth: scripts that produce tables/patches, not just screenshots
- Verification: deterministic replay checks, hashes, and sanity asserts
- Index: what's known, what's suspected, what's disproven
- Artifacts: generated JSON maps, diffs, patch files, and diagrams
The RE Loop
flowchart TD
A["ROM set / driver"] --> B[Observation]
B --> C[Instrumentation]
C --> D[Hypothesis test]
D --> E["Codify (script/patch)"]
E --> F[Determinism + validation]
F --> G[Write-up + diagrams]
G --> BWhy This Matters
In our MvC1 project, the strongest findings are always the ones that are actionable:
- a table address,
- a structure hypothesis,
- and a way to prove it.
The blog should read like a story, but the repo should run like a pipeline.
The Ultimate Validation: In-Game Verification
The most powerful form of reproducible analysis is embedding the verification into the artifact itself. Our ULTRA SETTINGS screen is injected directly into MvC1's F2 Test Menu, displaying 22 real-time diagnostic lines that verify every claim from the project documentation:
9.ULTRA SETTINGS
PRG TOTAL: 4096KB <- Full 68000 address space ($000000-$3FFFFF)
PRG USED: 0964KB <- Scanned backward from end of ROM
PRG FREE: 3132KB <- TOTAL - USED
GFX TOTAL: 128MB <- From actual chip file sizes
GFX USED: 032MB <- Original MvC data (always 32MB)
GFX FREE: 096MB <- TOTAL - USED
SND TOTAL: 16MB <- From actual audio chip sizes
SND USED: 08MB <- Original QSound data (always 8MB)
SND FREE: 08MB <- TOTAL - USED
CPS2 GFX: AT MAXIMUM <- 128MB = CPS-2 hardware ceiling
NORMAL IDS: 16 <- Counted from roster table at $AC7A
SECRET IDS: 05 <- Counted from secret table
ID 2E: NO <- Scanned roster for byte $2E
P PALETTE: OK <- Verified palette at $48642
K PALETTE: OK <- Verified palette at $486A2
FN REDVENOM: OK <- Verified code at $022B2E
FN ORNGHULK: OK <- Verified code at $05D044
FN GOLDW.M.: OK <- Verified code at $07EF90
FN SHDWLADY: OK <- Verified code at $0A26C8
PRESS BUTTON TO EXITThis is documentation that runs inside the game itself. If the ROM is corrupted, the values change. If a function is missing, "OK" becomes "NO". The verification is inseparable from the artifact.
flowchart TD
A["Documentation claims"] --> B["Python script reads ROM data"]
B --> C["Embeds values into 68000 assembly"]
C --> D["Compiles and injects into ROM"]
D --> E["F2 Test Menu displays results"]
E --> F["Visual proof on screen"]
style A fill:#33290f,stroke:#b28c36,color:#f4e9c8
style F fill:#112d20,stroke:#419865,color:#eaf2efDecompilation as Reproducible Documentation
Beyond the ULTRA SETTINGS screen, we decompiled all 4 secret character load functions into C, creating a permanent reference that anyone can verify against the ROM:
| Function | ROM Address | Size |
|---|---|---|
load_red_venom | $022B2E | 66 bytes |
load_orange_hulk | $05D044 | 60 bytes |
load_gold_war_machine | $07EF90 | 12 bytes |
load_shadow_lady | $0A26C8 | 20 bytes |
Each one is a standalone C translation named for the function it recovers, checkable byte-for-byte against the address it came from.
The character data table at $065CCA (32 bytes per entry) and unlock flag system (base $FF8000, offsets $3C-$4C) were mapped into C headers -- one for the memory map, one for the character-ID enum -- creating machine-readable documentation that can be consumed by both humans and build tools.
Capcom VS Series - Technical Deep Dive
A comprehensive technical analysis of CPS2 VS Series ROM architecture:
XMvsSF (1996) → MSHvsSF (1997) → MvC1 (1998)
Game Evolution Timeline
timeline
title Capcom VS Series Evolution (CPS2 Era)
section 1994-1995
X-Men COTA : First Marvel CPS2 game
: Established X-Men sprites
Marvel Super Heroes : Introduced Infinity Gems
: Spider-Man, Hulk debut
Street Fighter Alpha 2 : New SF engine
: Variable combo system
section 1996
X-Men vs Street Fighter : First crossover
: Tag team system
: Combined X-Men + SF rosters
: Base engine for future games
section 1997
Marvel Super Heroes vs SF : MSH characters join
: Spider-Man, Hulk, Blackheart
: Refined tag mechanics
: Secret characters added
section 1998
Marvel vs Capcom : Expanded roster
: New Capcom characters
: Duo Team Attack
: Final CPS2 VS gameROM Architecture Comparison
File Structure Overview
graph TB
subgraph XMvsSF["🎮 X-Men vs Street Fighter (1996)"]
direction TB
X_PROG["📦 Program ROMs xvse.03f, xvse.04f 1 MB Total"]
X_GFX["🎨 Graphics ROMs xvs.13m - xvs.20m 32 MB (8 × 4MB)"]
X_SND["🔊 Audio ROMs xvs.01, xvs.02 xvs.11m, xvs.12m"]
end
subgraph MSHvsSF["🦸 MSH vs Street Fighter (1997)"]
direction TB
M_PROG["📦 Program ROMs mvse.03f, mvse.04f 1 MB Total"]
M_GFX["🎨 Graphics ROMs mvs.13m - mvs.20m 32 MB (8 × 4MB)"]
M_SND["🔊 Audio ROMs mvs.01, mvs.02 mvs.11m, mvs.12m"]
end
subgraph MvC1["⚔️ Marvel vs Capcom (1998)"]
direction TB
C_PROG["📦 Program ROMs mvce.03, mvce.04 1 MB Total"]
C_GFX["🎨 Graphics ROMs mvc.13m - mvc.20m 32 MB (8 × 4MB)"]
C_SND["🔊 Audio ROMs mvc.01, mvc.02 mvc.11m, mvc.12m"]
end
XMvsSF -->|"Engine Evolution"| MSHvsSF
MSHvsSF -->|"Engine Evolution"| MvC1
style XMvsSF fill:#12303a,stroke:#397b83,color:#eaf2ef,stroke-width:3px
style MSHvsSF fill:#33290f,stroke:#b28c36,color:#f4e9c8,stroke-width:3px
style MvC1 fill:#112d20,stroke:#419865,color:#eaf2ef,stroke-width:3pxROM Size Comparison
| Component | XMvsSF | MSHvsSF | MvC1 (Original) | MvC1 (Phoenix Max) | CPS-2 Max |
|---|---|---|---|---|---|
| Program | 1 MB | 1 MB | 1 MB | 1 MB | 4 MB |
| Graphics | 32 MB | 32 MB | 32 MB | 128 MB | 128 MB |
| Audio Samples | 8 MB | 8 MB | 8 MB | 16 MB | 16 MB |
| Audio Program | 256 KB | 256 KB | 256 KB | 256 KB | 256 KB |
| Total | ~41 MB | ~41 MB | ~41 MB | ~148 MB | ~148 MB |
Code Similarity Analysis
Key Finding: Relocated Shared Code
| Comparison | Identical at Same Offset | Identical at ANY Offset | Interpretation |
|---|---|---|---|
| MSHvsSF ↔ MvC1 | 1 | 1,320+ | Code relocated but preserved! |
While only 1 block matches at the same offset, over 1,300 code blocks are shared between games at DIFFERENT offsets! This confirms the same engine with code relocation, not a rewrite.
Sprite Bank System
Bank Table Architecture
graph LR
subgraph TABLES["📍 Bank Table Locations"]
MSH_TBL["MSHvsSF 0x019E18"]
MVC_TBL["MvC1 0x01D350"]
end
subgraph BANKS["🎨 Sprite Banks (Shared Values!)"]
B0["0x100000"]
B1["0x110000"]
B2["0x120000"]
B3["0x130000"]
B4["0x140000"]
B5["0x150000"]
B6["0x160000"]
B7["0x000000"]
end
MSH_TBL --> BANKS
MVC_TBL --> BANKS
style TABLES fill:#12303a,stroke:#397b83,color:#eaf2ef,stroke-width:2px
style BANKS fill:#33290f,stroke:#b28c36,color:#f4e9c8,stroke-width:2pxBank Tables at Different Offsets, Similar Structure
╔══════════════════════════════════════════════════════════════════════════════╗
║ MSHvsSF Bank Table @ 0x019E18 │ MvC1 Bank Table @ 0x01D350 ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ [00] 0x040000 (Chun-Li) │ [00] 0x020000 ║
║ [01] 0x050000 │ [01] 0x030000 ║
║ [02] 0x060000 (Zangief) │ [02] 0x040000 (Chun-Li) ║
║ [03] 0x070000 │ [03] 0x050000 ║
║ [04] 0x080000 │ [04] 0x060000 (Zangief) ║
║ [05] 0x090000 │ [05] 0x070000 ║
║ [06] 0x100000 (Bison/War Machine)│ [06] 0x080000 ║
║ [07] 0x110000 │ [07] 0x090000 ║
║ [08] 0x120000 (Hulk) │ [08] 0x100000 (War Machine) ║
║ [09] 0x130000 (Wolverine) │ [09] 0x110000 (Cap America) ║
║ [10] 0x140000 │ [10] 0x120000 (Hulk) ║
║ [11] 0x150000 │ [11] 0x130000 (Wolverine) ║
║ [12] 0x160000 (Spider-Man) │ [12] 0x140000 (Gambit) ║
║ [13] 0x170000 │ [13] 0x150000 (Venom) ║
║ [14] 0x000000 │ [14] 0x160000 (Spider-Man) ║
║ [15] 0x000000 │ [15] 0x000000 ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ Tables at DIFFERENT offsets (0x019E18 vs 0x01D350) = CODE RELOCATED ║
║ Bank VALUES follow same pattern = COMPATIBLE STRUCTURE ║
╚══════════════════════════════════════════════════════════════════════════════╝Graphics Compatibility Matrix
Tile-Level Comparison (Same-Address Methodology -- Superseded)
The original analysis compared tile content at matching ROM addresses across games:
| Comparison | Same-Address Matches | Total Tiles | Match Rate |
|---|---|---|---|
| XMvsSF ↔ MSHvsSF | 384 | 131,029 | 0.29% |
| MSHvsSF ↔ MvC1 | 447 | 131,033 | 0.34% |
| XMvsSF ↔ MvC1 | 455 | 131,034 | 0.35% |
We originally read these numbers as "characters were redrawn for each game." That interpretation was wrong. The near-zero match rate is an artifact of comparing at the same address: each game relocates its tile data to different GFX banks, so a same-address comparison misses content that is present in both games at different offsets -- exactly the mistake the code-similarity analysis above avoided by matching blocks at ANY offset.
Corrected Finding: Graphics Are Relocated, Not Redrawn
flowchart TD
subgraph WRONG["❌ Original (disproven) reading"]
A["Same Bank Addresses (0x160000 = Spider-Man)"] --> B["Different Tile Content at Same Address"]
B --> C["Conclusion: characters REDRAWN per game"]
end
subgraph RIGHT["✅ Corrected finding"]
E["Same-offset copy of MSHvsSF tiles → SCRAMBLED"] --> F["Root cause: bank remapping was skipped"]
F --> G["Anim/frame tables are BYTE-IDENTICAL (759/759 blocks, pixel-proven)"]
G --> H["Port the tables + remap tiles to a free GFX bank → renders correctly"]
end
WRONG --> RIGHT
style WRONG fill:#351b1b,stroke:#b56565,color:#f6dada,stroke-width:3px
style RIGHT fill:#112d20,stroke:#419865,color:#eaf2ef,stroke-width:3pxThe infamous "scrambled tiles" screenshot from the first import attempt was a same-offset copy that never remapped the sprite banks. The art is the same content living at different bank addresses -- the pixels' shadows moved, not the pixels.
Character Roster Comparison
Cross-Game Character Presence
| Character | XMvsSF | MSHvsSF | MvC1 | Bank Address |
|---|---|---|---|---|
| Wolverine | ✅ | ✅ | ✅ | 0x130000 |
| Cyclops | ✅ | ✅ | ❌ | 0x110000 |
| Storm | ✅ | ❌ | ❌ | 0x120000 |
| Rogue | ✅ | ❌ | ❌ | 0x130000 |
| Gambit | ✅ | ❌ | ✅ | 0x140000 |
| Spider-Man | ❌ | ✅ | ✅ | 0x160000 |
| Hulk | ❌ | ✅ | ✅ | 0x120000 |
| Captain America | ❌ | ✅ | ✅ | 0x110000 |
| Ryu | ✅ | ✅ | ✅ | 0x000000 |
| Chun-Li | ✅ | ✅ | ✅ | 0x040000 |
| Zangief | ✅ | ✅ | ✅ | 0x060000 |
| Akuma | ✅ | ✅ | ❌ | 0x080000 |
| M. Bison | ✅ | ✅ | ❌ | 0x100000 |
Import Feasibility Analysis
What Works ✅ (Corrected July 2026)
| Component | Status | Notes |
|---|---|---|
| Audio/Music | ✅ Works | Same QSound format |
| Bank Table Structure | ✅ Compatible | 100% compatible between MSHvsSF & MvC1 |
| Character Graphics | ✅ Works | Same tile content at different banks -- copy to a free bank and remap |
| Animation/Frame Tables | ✅ Byte-identical | MSHvsSF → MvC1 mode-2 framedata: 759/759 blocks verified, pixel-proven |
| Full Characters | ✅ Portable (data); behavior is real work | Tables + tile bank remap move the data; making her fight took a donor chassis, then native code relocation (see Editor's update) |
What Still Requires Real Work
| Component | Status | Notes |
|---|---|---|
| Stage Backgrounds | ⚠️ Feasible | Layer/scroll structures port, but art needs free GFX space and remapping |
| UI Elements | ⚠️ Game-specific | Select screen, portraits, and HUD layouts differ per game |
| Select/roster wiring | ⚠️ Manual | Character IDs, roster tables, and select-screen hooks are per-game |
What Makes a Character
graph TD
subgraph CHARACTER["🎮 What Makes a Character"]
CODE["💻 Character Code (Move logic, AI)"]
DATA["📋 Data Tables (Hitboxes, damage)"]
ANIM["🎬 Animation Data (Frame sequences)"]
GFX["🎨 Graphics Tiles (Sprite art)"]
SND["🔊 Sound Effects (Voice, SFX)"]
end
CODE --> PROBLEM
DATA --> PROBLEM
ANIM --> PROBLEM
GFX --> PROBLEM
SND --> PROBLEM
PROBLEM["✅ For same-generation games (MSHvsSF → MvC1) the data/anim tables are BYTE-IDENTICAL"]
PROBLEM --> SOLUTION["🔧 Port the tables, remap the tiles to a free GFX bank"]
style CHARACTER fill:#33290f,stroke:#b28c36,color:#f4e9c8,stroke-width:2px
style PROBLEM fill:#12303a,stroke:#397b83,color:#eaf2ef,stroke-width:3px
style SOLUTION fill:#112d20,stroke:#419865,color:#eaf2ef,stroke-width:2pxCode Relocation Map
Discovered Offset Patterns
Through binary analysis, we've identified 1,313 code blocks that are shared between MSHvsSF and MvC1, relocated to different addresses:
╔══════════════════════════════════════════════════════════════════════════════╗
║ MSHvsSF → MvC1 RELOCATION MAP ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ MSHvsSF Range │ Offset to MvC1 │ Blocks │ Content ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ 0x000000 - 0x010000 │ +0x0656 (1,622) │ 195 │ Interrupt vectors ║
║ 0x010000 - 0x020000 │ +0x330D (13,069) │ 432 │ Core engine code ║
║ 0x020000 - 0x030000 │ +0x421F (16,927) │ 656 │ Character code ║
║ 0x060000 - 0x070000 │ +0x10FC9 (69,577) │ 25 │ Extended data ║
╚══════════════════════════════════════════════════════════════════════════════╝Key Structure Locations
| Structure | MSHvsSF | MvC1 | Offset | Notes |
|---|---|---|---|---|
| Bank Table | 0x019E18 | 0x01D350 | +0x3538 | Sprite bank addresses |
| Char Ptr Table | 0x019DBC | 0x01D2F4 | +0x3538 | Character code pointers |
Technical Specifications
CPS2 Hardware Platform
╔══════════════════════════════════════════════════════════════════╗
║ CPS2 HARDWARE SPECIFICATIONS ║
╠══════════════════════════════════════════════════════════════════╣
║ CPU: Motorola 68000 @ 11.8 MHz ║
║ Graphics: Custom CPS2 GPU ║
║ Resolution: 384 × 224 pixels ║
║ Colors: 32,768 (15-bit RGB555) ║
║ Sprites: Up to 900 on screen ║
║ Tile Size: 8×8 or 16×16 pixels ║
║ Sound: QSound DSP (Z80 @ 8 MHz) ║
║ Audio: 16-bit stereo, 4 channels ║
╚══════════════════════════════════════════════════════════════════╝Sprite Bank Memory Layout
Graphics ROM Address Space (32 MB total, 8 ROMs interleaved)
═══════════════════════════════════════════════════════════
0x000000 ┌─────────────────────────────────────────────────┐
│ Ryu, Roll, Ken (SF Characters) │
0x040000 ├─────────────────────────────────────────────────┤
│ Chun-Li │
0x050000 ├─────────────────────────────────────────────────┤
│ Jin / Dhalsim │
0x060000 ├─────────────────────────────────────────────────┤
│ Zangief │
0x070000 ├─────────────────────────────────────────────────┤
│ Strider / M.Bison │
0x080000 ├─────────────────────────────────────────────────┤
│ Morrigan / Akuma │
0x100000 ├─────────────────────────────────────────────────┤
│ War Machine / Bison │
0x110000 ├─────────────────────────────────────────────────┤
│ Captain America / Cyclops │
0x120000 ├─────────────────────────────────────────────────┤
│ Hulk │
0x130000 ├─────────────────────────────────────────────────┤
│ Wolverine │
0x140000 ├─────────────────────────────────────────────────┤
│ Gambit │
0x150000 ├─────────────────────────────────────────────────┤
│ Venom / Sabretooth │
0x160000 ├─────────────────────────────────────────────────┤
│ Spider-Man │
0x180000 ├─────────────────────────────────────────────────┤
│ Secret Characters / Effects │
└─────────────────────────────────────────────────┘Final Verdict (Corrected July 2026)
| Aspect | Compatibility | Notes |
|---|---|---|
| ROM Structure | 🟢 100% | Identical sizes and layout |
| Bank Tables | 🟢 100% | Same structure; per-game offsets |
| Audio Format | 🟢 100% | QSound compatible, songs play |
| Code Logic | 🟢 Same engine | 1,313 shared blocks, relocated not rewritten (~0% at same offsets) |
| Graphics Tiles | 🟢 Same content | Relocated to different banks, NOT redrawn (~0.3% only at same address) |
| Anim/Frame Tables | 🟢 Byte-identical | MSHvsSF → MvC1: 759/759 mode-2 framedata blocks, pixel-proven |
| Import | 🟢 Works with bank remap | Same-offset copy scrambles; remapped copy renders correctly |
The Path Forward
flowchart TD
A["1. Map Code Relocations Find MSHvsSF → MvC1 offset table"] --> B["2. Identify Character Boundaries Where does each character's code start/end?"]
B --> C["3. Extract Character Block Code + Data + Animation tables"]
C --> D["4. Remap Internal References Update pointers to new locations"]
D --> E["5. Copy Graphics Bring sprite tiles from source game"]
E --> F["6. Inject into Target Insert at available ROM space"]
F --> G["7. Update Character Table Register new character ID"]
style A fill:#12303a,stroke:#397b83,color:#eaf2ef,stroke-width:2px
style G fill:#112d20,stroke:#419865,color:#eaf2ef,stroke-width:2pxCharacter import IS possible because the games share the same engine -- and it's easier than this post originally estimated. For a same-generation pair (MSHvsSF → MvC1), the animation/frame tables carry over byte-identical, so the work is porting the tables and remapping tiles into a free GFX bank, not re-authoring a character.
The original estimate here ("2-6 months per character") assumed art had to be redrawn and every table re-derived. With byte-identical tables, a same-generation port is a dramatically smaller job dominated by wiring (roster/select hooks, bank remap, sound), not asset work.