essayUpdated Jul 21, 202615 min read$02

I Thought Capcom Redrew the Sprites. I Was Comparing the Wrong Bytes.

A near-zero tile match looked like incompatible artwork. Reproducible ROM analysis exposed the bad assumption—and made the correction testable.

reverse-engineeringdocumentationautomationreproducibilityworkflows
CPS-2 Reverse EngineeringPart 2 of 43
Browse all writing
On this page
Evidence Strip

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.

Editor's update (July 2026)

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.

Goal

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

How it works
Drawing the diagram…

Why 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 EXIT

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

How it works
Drawing the diagram…

Decompilation 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:

FunctionROM AddressSize
load_red_venom$022B2E66 bytes
load_orange_hulk$05D04460 bytes
load_gold_war_machine$07EF9012 bytes
load_shadow_lady$0A26C820 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

How it works
Drawing the diagram…

ROM Architecture Comparison

File Structure Overview

How it works
Drawing the diagram…

ROM Size Comparison

ComponentXMvsSFMSHvsSFMvC1 (Original)MvC1 (Phoenix Max)CPS-2 Max
Program1 MB1 MB1 MB1 MB4 MB
Graphics32 MB32 MB32 MB128 MB128 MB
Audio Samples8 MB8 MB8 MB16 MB16 MB
Audio Program256 KB256 KB256 KB256 KB256 KB
Total~41 MB~41 MB~41 MB~148 MB~148 MB

Code Similarity Analysis

Key Finding: Relocated Shared Code

ComparisonIdentical at Same OffsetIdentical at ANY OffsetInterpretation
MSHvsSF ↔ MvC111,320+Code relocated but preserved!
Critical Discovery

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

How it works
Drawing the diagram…

Bank 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:

ComparisonSame-Address MatchesTotal TilesMatch Rate
XMvsSF ↔ MSHvsSF384131,0290.29%
MSHvsSF ↔ MvC1447131,0330.34%
XMvsSF ↔ MvC1455131,0340.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

How it works
Drawing the diagram…

The 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

CharacterXMvsSFMSHvsSFMvC1Bank Address
Wolverine0x130000
Cyclops0x110000
Storm0x120000
Rogue0x130000
Gambit0x140000
Spider-Man0x160000
Hulk0x120000
Captain America0x110000
Ryu0x000000
Chun-Li0x040000
Zangief0x060000
Akuma0x080000
M. Bison0x100000

Import Feasibility Analysis

What Works ✅ (Corrected July 2026)

ComponentStatusNotes
Audio/Music✅ WorksSame QSound format
Bank Table Structure✅ Compatible100% compatible between MSHvsSF & MvC1
Character Graphics✅ WorksSame tile content at different banks -- copy to a free bank and remap
Animation/Frame Tables✅ Byte-identicalMSHvsSF → MvC1 mode-2 framedata: 759/759 blocks verified, pixel-proven
Full Characters✅ Portable (data); behavior is real workTables + 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

ComponentStatusNotes
Stage Backgrounds⚠️ FeasibleLayer/scroll structures port, but art needs free GFX space and remapping
UI Elements⚠️ Game-specificSelect screen, portraits, and HUD layouts differ per game
Select/roster wiring⚠️ ManualCharacter IDs, roster tables, and select-screen hooks are per-game

What Makes a Character

How it works
Drawing the diagram…

Code 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

StructureMSHvsSFMvC1OffsetNotes
Bank Table0x019E180x01D350+0x3538Sprite bank addresses
Char Ptr Table0x019DBC0x01D2F4+0x3538Character 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)

AspectCompatibilityNotes
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 engine1,313 shared blocks, relocated not rewritten (~0% at same offsets)
Graphics Tiles🟢 Same contentRelocated to different banks, NOT redrawn (~0.3% only at same address)
Anim/Frame Tables🟢 Byte-identicalMSHvsSF → MvC1: 759/759 mode-2 framedata blocks, pixel-proven
Import🟢 Works with bank remapSame-offset copy scrambles; remapped copy renders correctly

The Path Forward

How it works
Drawing the diagram…
Bottom Line (revised)

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

Written by Daniel Plas Rivera · 3,227 words · $02

ShareXLinkedIn