referenceUpdated Jul 27, 202619 min read$07

I Expanded MvC to 148MB. Most of It Was Unreachable.

Bigger ROM files fooled the diagnostics. Making the extra graphics visible took a custom emulator mapper—and cost standard Fightcade compatibility.

hardware-limitscps2rom-expansionembedded-systemsarcade-hardware
CPS-2 Reverse EngineeringPart 6 of 43
Browse all writing
On this page
Evidence Strip

Goal: Push every CPS-2 ROM type to its theoretical maximum and document the hardware limits with in-game verification.

Constraints: Hardware address bus widths are fixed (24-bit for 68000, 24-bit per GFX chip). Interleaving scheme requires all 8 GFX chips to be identical size. MAME validates ROM sizes against driver expectations.

Approach: Analyzed MAME CPS-2 driver source for address space mappings. Calculated theoretical maximums from bus width and chip count. Built automated expansion pipeline. Injected in-game verification screen.

Result: 128MB GFX (8x16MB), 16MB Audio (2x8MB), 4MB Program space (24-bit bus). Total: 148MB on disk. 8MB of MSHVSSF QSound samples byte-copied into the expanded region (XMSF+XCOTA config also verified at the byte level). See Editor's update: the padded space turned out not to be addressable under the stock emulator (GFX masked at 32MB, QSound at 8MB); real addressable expansion came later via a custom FBNeo core (64MB GFX + 16MB QSound).

Proof / Validation: ULTRA SETTINGS: GFX TOTAL: 128MB, CPS2 GFX: AT MAXIMUM (computed from file sizes). GOLD marker at mvc.13m:0x3FFFFB. MAME output shows WRONG LENGTH warnings but game runs (it never touches the padded region). Byte-for-byte ROM verification: mvs.11m/mvs.12m placed at intended QSound $800000/$C00000 offsets -- but bank playback of that region under the stock emulator wraps into the original 8MB (see Editor's update).

Artifacts: A single-command build pipeline that expands every GFX and audio chip, pads them format-correctly, plants the verification marker and assembles the diagnostic handler -- plus the patched ROM set it emits, always rebuilt from a pristine base rather than edited in place.

Editor's update (July 2026)

Two claims in this post were overclaims, and honest verification later caught them:

  • "MAME maps the full expanded ROM into the hardware's address space" -- wrong. Padding the ROM files makes the bytes present, not addressable. The mvsc emulator mapper AND-masks GFX addressing past 32MB and QSound addressing past 8MB, so reads of the "expanded" region silently wrap back into the original data. The GOLD marker exists in the file; nothing in the emulated machine can fetch it. The same wrap applies to the imported MSHVSSF sample banks at $800000+ -- what the sound test actually played came from the original 8MB region.
  • "148MB at the CPS-2 ceiling" describes the ZIP, not the machine. No stock emulator build addressed a single byte past the shipped sizes.

The caps were genuinely broken later with a custom FBNeo core: extended GFX chip loading in d_cps2.cpp (appended chips, stock 0-32MB kept byte-identical), a new mapper_mvsc64, and a Cps2ExtScroll OBJ hook (sprite Y-bit12 carried into tile-code bit 18) -- reaching a pixel-proven 64MB GFX region, plus 16MB QSound via a two-line driver change. Stock MAME cannot even load the 64MB zip, and the custom core breaks standard Fightcade netplay -- a real trade-off, documented rather than hidden.

Read the rest of this post as: correct hardware ceiling analysis and a working file-expansion pipeline, paired with an addressability claim that didn't survive verification.

Update (July 21, 2026): the 64MB region now has two tenants. The Dark Sakura port seated her 9,520-tile MSHvSF animation tree in the upper GFX space first; the native-port era added Shuma-Gorath's 11,587 tiles alongside. The expansion is carrying real, rendered character content, not just a GOLD marker.

Every piece of hardware has theoretical limits imposed by its address bus width, chip select lines, and memory controller design. For Capcom's CPS-2 arcade platform (1993-2003), these limits were never fully utilized by any shipping game. This post documents the process of expanding Marvel vs. Capcom 1 to the absolute ceiling of what CPS-2 can address -- and proving it works.

The CPS-2 Platform

The CPS-2 (Capcom Play System 2) powered over 40 arcade games from 1993 to 2003, including the entire Street Fighter Alpha series, the Marvel VS series, and many others. Its architecture:

ComponentSpecification
CPUMotorola 68000 @ 11.8 MHz
GraphicsCustom CPS-2 GPU
Resolution384 x 224 pixels
Colors4,096 per entry (12-bit RGB444)
SpritesUp to 900 on screen
SoundQSound DSP (Z80 @ 8 MHz)
Audio16-bit stereo, 4 channels

What No Shipping Game Ever Did

Every CPS-2 game shipped with ROM sizes well within the hardware's capabilities. Here's what the hardware can actually address vs. what games used:

How it works
Drawing the diagram…

Why These Are the Limits

Graphics: 128MB (8 x 16MB)

CPS-2 uses 8 interleaved graphics ROMs. The hardware reads 64 bits (8 bytes) at a time, one byte from each chip. Each chip position can hold a maximum 128Mbit (16MB) mask ROM -- the largest size manufactured for this package type.

Address Bus Width: 24 bits per chip = 16MB addressable
Number of Chips:   8 (interleaved)
Total:             8 x 16MB = 128MB

The interleaving means pixel data is spread across all 8 chips:

Byte 0  → mvc.13m
Byte 1  → mvc.14m
Byte 2  → mvc.15m
Byte 3  → mvc.16m
Byte 4  → mvc.17m
Byte 5  → mvc.18m
Byte 6  → mvc.19m
Byte 7  → mvc.20m
Byte 8  → mvc.13m (next address)
...

Each 64-bit fetch reads exactly one byte from every chip in parallel -- which is why all 8 GFX ROMs must be the same size, and why a single tile's pixels are physically striped across the whole ROM bank:

How it works
Drawing the diagram…

Program: 4MB (68000 address space)

The Motorola 68000 has a 24-bit address bus, allowing 16MB of total addressable memory. CPS-2 maps program ROM to $000000-$3FFFFF -- exactly 4MB. Some games (Street Fighter Alpha 3) actually use the full 4MB.

CPU Address Bus:   24 bits (16MB total)
ROM Mapping:       $000000 - $3FFFFF
Available:         4MB
MvC1 Uses:         1MB (two 512KB chips)
Expansion Room:    3MB (75% unused)

Audio: 16MB (2 x 8MB) -- QSound Hardware Ceiling

The QSound DSP (DL-1425 / DSP16A) uses an 8-bit bank register + 16-bit address register = 24-bit addressing = 16MB maximum. This is the hard limit imposed by the DSP silicon -- unlike GFX (where the custom GPU has wider addressing), the QSound chip physically cannot address beyond 16MB of sample ROM. This means 8MB free is the absolute ceiling for audio expansion, regardless of how much ROM we attach.

QSound Address Space (24-bit, 16MB max):
  $000000-$3FFFFF  mvc.11m original (4MB) ── MvC1 instruments
  $400000-$7FFFFF  mvc.12m original (4MB) ── MvC1 instruments
  $800000-$BFFFFF  mvc.11m expanded (4MB) ── Imported CPS-2 audio
  $C00000-$FFFFFF  mvc.12m expanded (4MB) ── Imported CPS-2 audio

Current use: All 8MB of free space is filled with MSHVSSF (Marvel Super Heroes vs. Street Fighter) QSound sample data, imported directly from mshvsfu.zip. Configurable to XMSF + XCOTA instead (both verified at the ROM-byte level). Correction: under the stock emulator this region is not reachable -- the QSound mapping for mvsc masks at 8MB, so banks $80+ wrap into the original samples. The imported data only becomes addressable under the custom FBNeo core's 16MB QSound driver (see Editor's update).

The Expansion

Before and After

ROM TypeChip CountOriginal SizeExpanded SizePaddingFree Space
GFX84MB each16MB each0xFF (transparent)96MB
Audio Sample24MB each8MB each0x00 (silence)8MB
Audio Program1256KB256KBN/A0
Program2512KB each512KB eachN/A3MB addressable

This was not a single jump. The first expansion pass doubled each GFX chip to 8MB -- a 64MB file set with 32MB free -- and the 128MB pass doubled it again. Both passes ran through the same pipeline; only the target constant changed.

Total ROM Set Size

Graphics:   8 x 16MB  = 128MB
Audio:      2 x  8MB  =  16MB
Program:    2 x 512KB =   1MB
Data:       6 x 512KB =   3MB
Audio Pgm:  1 x 256KB = 256KB
Key:                   =  20B
───────────────────────────────
Total:                 ~148MB

Why Padding Matters

GFX Padding: 0xFF

In CPS-2's tile format, 0xFF represents a transparent pixel. Filling empty graphics space with 0xFF means:

  • No visual artifacts from unused regions
  • The GPU skips transparent tiles efficiently
  • Safe to read from any expanded address

Audio Padding: 0x00

In QSound's PCM format, 0x00 represents digital silence. Filling empty audio space with 0x00 means:

  • No audio glitches if the DSP reads expanded regions
  • Silent by default, can be filled with new samples later

Current state: The expanded audio space is filled (not merely padded). 8MB of MSHVSSF (Marvel Super Heroes vs. Street Fighter) QSound sample data has been imported directly from the source ROM, with zero conversion needed (same CPS-2 QSound format). A Z80 dispatch table hook routes sound codes $0900-$0901 to a custom handler for QSound channel 15 playback with multi-bank streaming. Alternatively, XMSF + XCOTA can fill the same space ($0900-$0903). The build script supports multiple configurations.

Correction (July 2026): playing those banks under the stock emulator does not reach the imported data -- QSound addressing for mvsc wraps at 8MB, so banks $80+ alias the original samples. The imported region is genuinely playable only under the custom FBNeo core's 16MB QSound driver (see Editor's update).

How it works
Drawing the diagram…

Why NOT 0x00 for Graphics

Using 0x00 for graphics padding would mean black pixels, not transparent. Sprites rendered from the expanded region would show as solid black rectangles instead of being invisible.

The Build Pipeline

A single Python script handles the entire expansion:

How it works
Drawing the diagram…

Key Code: Expansion Logic

# GFX: 4MB → 16MB per chip (0xFF = transparent)
for name in GFX_ROMS:
    data = bytearray(self.clean_files[name])
    expanded = bytearray(16 * 1024 * 1024)  # 16MB
    expanded[:len(data)] = data
    for i in range(len(data), len(expanded)):
        expanded[i] = 0xFF
    self.clean_files[name] = bytes(expanded)
 
# Audio: 4MB → 8MB per chip (0x00 = silence)
for name in AUDIO_SAMPLE_ROMS:
    data = bytearray(self.clean_files[name])
    expanded = bytearray(8 * 1024 * 1024)   # 8MB
    expanded[:len(data)] = data
    # 0x00 is default for bytearray - already silence
    self.clean_files[name] = bytes(expanded)

GOLD Marker

To verify the expansion is real (not just MAME mapping tricks), we plant a 5-byte marker at offset 0x3FFFFB in mvc.13m -- the last 5 bytes of the original 4MB region:

marker = b'GOLD\x01'
expanded[0x3FFFFB:0x400000] = marker

The in-game ULTRA SETTINGS screen can read this marker to confirm the expansion was applied.

MAME Behavior

MAME handles oversized ROMs gracefully. It generates warnings but loads the data:

mvc.13m WRONG LENGTH (expected: 00400000 found: 01000000)
mvc.13m WRONG CHECKSUMS:
    EXPECTED: CRC(fa5f74bc)
    FOUND:    CRC(xxxxxxxx)
mvc.11m WRONG LENGTH (expected: 00400000 found: 00800000)
...
WARNING: the machine might not run correctly.

The game runs despite all warnings -- but not because the expansion is live. MAME loads the oversized files into the region, then the mvsc mapper AND-masks GFX addressing at 32MB and QSound at 8MB, so the game simply never sees the padded space. "Runs perfectly" proved the padding was harmless; it did not prove the space was mapped. (It wasn't -- see Editor's update.)

In-Game Verification

The ULTRA SETTINGS screen (injected into the F2 Test Menu) confirms the expansion in real time:

  GFX TOTAL: 0128MB    <- Computed from actual chip sizes
  GFX USED:   032MB        <- File size > original 4MB
  GFX FREE:  0096MB    <- TOTAL - 32MB original data
  SND TOTAL:   16MB
  SND USED:    08MB
  SND FREE:    08MB
  CPS2 GFX: AT MAXIMUM
  CPS-2 MAX: 0128MB    <- Hardware theoretical limit

When GFX TOTAL equals CPS-2 MAX (both 128MB), you've reached the hardware ceiling. There is nowhere else to expand.

Content Capacity

With 96MB Free Graphics

ContentSize EachCountNotes
Average Characters1.5-2 MB48-64Full sprite set + animations
Large Characters2.5-3 MB32-38Hulk, Sentinel-class
Extra-Large Characters4+ MBup to 24Onslaught-class boss sprite sets
Stages0.5-1 MB96-192Scrolling backgrounds
Helpers/Assists0.2-0.4 MB240-480Short animation sequences

With 8MB Free Audio

ContentSize EachCountNotes
Character Voice Set200-400 KB20-40Attacks, wins, intros
Stage Music500KB-1MB8-16Full BGM loops
Sound Effects100-300 KB27-80Hits, projectiles, UI

Realistic "Dream Expansion" Mix

CategoryCountSpace Used
New characters30~60 MB
New stages15~15 MB
New helpers30~12 MB
Effects + UI--~9 MB
Total75+ items~96 MB

That's enough to add the entire MSHvSF roster, half of XMvSF, plus original characters and stages.

Cross-Game Comparison

How does our expanded MvC1 compare to other CPS-2 games?

GameYearGFX SizeProgramAudioTotal
X-Men COTA199432 MB1 MB8 MB~41 MB
XMvsSF199632 MB1 MB8 MB~41 MB
MSHvsSF199732 MB1 MB8 MB~41 MB
SFA3199832 MB4 MB8 MB~44 MB
MvC1 (original)199832 MB1 MB8 MB~41 MB
MvC1 Phoenix2026128 MB1 MB16 MB~148 MB

Our Phoenix Edition ROM set is 3.6x larger than any shipping CPS-2 game.

Using the Free GFX Space: Sprite Data Copies

The 96MB of free GFX space isn't just theoretical headroom -- we have a concrete use case. To add Armored Spider-Man (character ID 0x2E) with actual modified sprites (not just a palette swap), we need to copy Spider-Man's tile data to the expanded region and modify it.

CPS-2 Sprite Tile Format

PropertyValue
Tile dimensions16x16 pixels
Color depth4bpp planar (4 bitplanes per tile)
Bytes per tile128 bytes
Palette16 colors per tile, indexed into Palette RAM
Color formatRGB444 big-endian (0x0RGB) -- 2 bytes per color, 32 bytes per palette
ROM interleavingTiles split across all 8 GFX ROM chips

The Copy Plan

Spider-Man's complete sprite set (all animation frames) occupies approximately 1.5-2 MB across the 8 interleaved chips. To copy and modify:

  1. Extract -- De-interleave Spider-Man's tile data from the 8-chip layout
  2. Copy -- Place copies at the start of the free region (offset 4MB in each chip)
  3. Modify -- Add armor details: thicker outlines for plating, visor glow, metallic highlight tiles
  4. Update pointer -- Change the sprite bank pointer in the character table entry for ID 0x2E at $065CCA

Since the CPU can't directly read GFX ROMs, the character table at $065CCA contains a sprite bank index that tells the CPS-2 graphics hardware which tile range to use. Pointing ID 0x2E's bank to the copied tiles is a simple table patch.

That architectural split is worth stating precisely, because it governs what any GFX-space plan can and cannot do. The 68000 has no path to graphics ROM at all. It communicates with the drawing hardware through two RAM windows:

  • Object RAM ($700000-$709FFF) -- 8-byte entries per sprite: Y position, tile number, attributes, X position
  • Palette RAM ($900000-$92FFFF) -- 192KB, RGB444

The CPU says "draw tile N with palette P at X,Y"; the CPS-2 custom graphics hardware fetches the tile bytes from GFX ROM itself. So expanded GFX space is only ever reachable through a tile number -- which is exactly why the mapper's address mask (below) is the thing that decides whether the expansion is real.

Space Usage

AssetSize% of Free Space
Spider-Man sprite copy~2 MB2.1%
Remaining free~94 MB97.9%

We could theoretically copy ALL existing characters' sprites and still have 60+ MB for completely new content.

Program ROM: Correction -- It Is Already Full

This section was wrong, and the error mattered

This post previously said MvC1 "uses 1MB" of a 4MB program budget and that expansion would yield "3MB of additional code space." That is wrong. It counted only the two chips named mvce.03a/mvce.04a and missed that mvc.05a through mvc.10 are program-space chips too, not a separate data bus.

The real map:

$000000  mvce.03a  512 KB      $200000  mvc.07    512 KB
$080000  mvce.04a  512 KB      $280000  mvc.08    512 KB
$100000  mvc.05a   512 KB      $300000  mvc.09    512 KB
$180000  mvc.06a   512 KB      $380000  mvc.10    512 KB
                                        --------------------
                                        4.00 MB of 4.00 MB

Marvel vs. Capcom already occupies the entire 68000 address space. There is no 3MB to claim, and no flat expansion is possible. Every byte of "free space" in this game is a gap inside those 4MB, not headroom beyond them.

68000 program space: what is actually available
  • Occupied by MvC4,096KB

    all 8 chips, $000000-$3FFFFF

  • Hardware maximum4,096KB
  • Free gaps inside it41KB

    41,608 bytes total, fragmented -- largest single run 5,076 B

The headroom is 1% of the space, and it is in pieces.

Why this matters more than it sounds

A native character port needs her data resident in that space. Measured for Dark Sakura in MSHvSF:

One imported character vs the space that exists
  • Animation tree + records26,338bytes
  • Special-execution code28,672bytes
  • Character code block2,628bytes
  • Box shapes / box-set (est.)6,144bytes
  • TOTAL she needs63,782bytes
  • Free in the whole 4MB41,608bytes

    and the largest contiguous run is 5,076 B

63,782 needed, 41,608 free, largest block 5,076. The tree alone needs one run five times bigger than anything available.

That single measurement explains the shape of the whole character-port project. A true import does not fit, so the port instead redirects: it patches individual per-character lookups to borrow data that already exists. That works, and it is why the characters run at all -- but anything not explicitly redirected silently uses another character's values, which is why defects surface one at a time through play rather than appearing on a list.

The three real routes

  1. Banking. Map additional ROM through a window, the way the custom core widened the GFX mapper. This is the only route that creates genuinely new program space, and it is the same class of change that already worked for graphics.
  2. Reclaiming in place. Free a contiguous block from data that is no longer needed. Bounded by what is actually spare.
  3. Continue redirecting. Accepts permanent partial fidelity.

The rest of this section describes what program space would buy, and remains accurate as a wish list -- it just requires route 1 or 2 first.

What the space would buy

  • Complete AI opponent logic
  • New character movesets and combo systems (including wiring Armored Spider-Man's load function)
  • Enhanced training mode features
  • Additional game modes
  • Position check patches for new secret character boxes

This requires more careful MAME driver analysis since the program ROM file names and sizes are validated differently than GFX/Audio ROMs. Three specific reasons program ROM is harder than the GFX and audio pads:

  1. The MAME driver expects specific file names and sizes for program chips, and does not simply warn and continue
  2. New code has to be position-aware -- 68000 absolute addressing means relocated code does not just work where it lands
  3. Existing jump tables reference hardcoded addresses, so anything that shifts existing code breaks them

The ULTRA SETTINGS handler is the existence proof that injecting new code into free program space works; the open question is scaling that to the full 4MB window, not whether the technique is sound.

Summary

MetricOriginalFile Size After ExpansionAddressable (stock emulator)Addressable (custom FBNeo core)
GFX ROM32 MB128 MB32 MB (mapper masks past 32 MB)64 MB (pixel-proven)
Audio ROM8 MB16 MB8 MB (QSound masks past 8 MB)16 MB
Program ROM4 MB (already full)4 MB4 MB4 MB — no headroom; expansion needs banking
Build PipelineManualAutomatedPython + vasmPython + vasm
VerificationNoneULTRA SETTINGS (file-size based)22 diagnostics+ pixel-proof from upper GFX region

The GFX story in one axis: padding the files reached the paper ceiling, but the addressable region only moved when a custom core widened the mapper -- and even then to 64MB, not the full 128MB the files claim.

Graphics region size: the honest progression
  • Largest shipped CPS-2 board32MB
  • Addressable on stock hardware32MB

    the ceiling that actually constrains a real cabinet

  • Addressable on the custom core64MB

    pixel-proven, and offline only — a stock board cannot reach it

  • Padded into the file128MB

    bytes on disk, not addressable space; padding a zip proves nothing

  • Theoretical hardware ceiling128MB
Two of these numbers are 128 and neither of them means the graphics fit.

The hardware ceiling analysis in this post stands: 128MB GFX / 16MB audio / 4MB program is what the CPS-2 platform can physically address. What this post's original conclusion got wrong is who was doing the addressing -- padded files alone changed nothing, and the real expansion (64MB GFX + 16MB QSound) required a custom emulator core, at the cost of standard Fightcade netplay compatibility. As of July 2026 that upper GFX region is in production use: it holds the tile sets of both natively ported characters -- Dark Sakura's 9,520 tiles and Shuma-Gorath's 11,587 (donor-chassis history, native port).

References

  • Capcom CPS-2 hardware specifications
  • MAME CPS-2 driver source (cps2.cpp, cps_draw.cpp)
  • 68000 Programmer's Reference Manual (Motorola)
  • CPS-2 ROM interleaving analysis
  • MAME ROM loading and validation code

Written by Daniel Plas Rivera · 4,087 words · $07

ShareXLinkedIn