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).
Repo / Artifacts: CHARACTER_SELECT/scripts/patch_rom_check_complete.py, patched ROM set in CHARACTER_SELECT/roms/mvscud.zip.
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.
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:
| Component | Specification |
|---|---|
| CPU | Motorola 68000 @ 11.8 MHz |
| Graphics | Custom CPS-2 GPU |
| Resolution | 384 x 224 pixels |
| Colors | 4,096 per entry (12-bit RGB444) |
| Sprites | Up to 900 on screen |
| Sound | QSound DSP (Z80 @ 8 MHz) |
| Audio | 16-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:
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 = 128MBThe 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)
...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 audioCurrent 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 Type | Chip Count | Original Size | Expanded Size | Padding | Free Space |
|---|---|---|---|---|---|
| GFX | 8 | 4MB each | 16MB each | 0xFF (transparent) | 96MB |
| Audio Sample | 2 | 4MB each | 8MB each | 0x00 (silence) | 8MB |
| Audio Program | 1 | 256KB | 256KB | N/A | 0 |
| Program | 2 | 512KB each | 512KB each | N/A | 3MB addressable |
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: ~148MBWhy 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).
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:
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] = markerThe 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 limitWhen 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
| Content | Size Each | Count | Notes |
|---|---|---|---|
| Average Characters | 1.5-2 MB | 48-64 | Full sprite set + animations |
| Large Characters | 2.5-3 MB | 32-38 | Hulk, Sentinel-class |
| Stages | 0.5-1 MB | 96-192 | Scrolling backgrounds |
| Helpers/Assists | 0.2-0.4 MB | 240-480 | Short animation sequences |
With 8MB Free Audio
| Content | Size Each | Count | Notes |
|---|---|---|---|
| Character Voice Set | 200-400 KB | 20-40 | Attacks, wins, intros |
| Stage Music | 500KB-1MB | 8-16 | Full BGM loops |
| Sound Effects | 100-300 KB | 27-80 | Hits, projectiles, UI |
Realistic "Dream Expansion" Mix
| Category | Count | Space Used |
|---|---|---|
| New characters | 30 | ~60 MB |
| New stages | 15 | ~15 MB |
| New helpers | 30 | ~12 MB |
| Effects + UI | -- | ~9 MB |
| Total | 75+ 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?
| Game | Year | GFX Size | Program | Audio | Total |
|---|---|---|---|---|---|
| X-Men COTA | 1994 | 32 MB | 1 MB | 8 MB | ~41 MB |
| XMvsSF | 1996 | 32 MB | 1 MB | 8 MB | ~41 MB |
| MSHvsSF | 1997 | 32 MB | 1 MB | 8 MB | ~41 MB |
| SFA3 | 1998 | 32 MB | 4 MB | 8 MB | ~44 MB |
| MvC1 (original) | 1998 | 32 MB | 1 MB | 8 MB | ~41 MB |
| MvC1 Phoenix | 2026 | 128 MB | 1 MB | 16 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
| Property | Value |
|---|---|
| Tile dimensions | 16x16 pixels |
| Color depth | 4bpp planar (4 bitplanes per tile) |
| Bytes per tile | 128 bytes |
| Palette | 16 colors per tile, indexed into Palette RAM |
| Color format | RGB444 big-endian (0x0RGB) -- 2 bytes per color, 32 bytes per palette |
| ROM interleaving | Tiles 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:
- Extract -- De-interleave Spider-Man's tile data from the 8-chip layout
- Copy -- Place copies at the start of the free region (offset 4MB in each chip)
- Modify -- Add armor details: thicker outlines for plating, visor glow, metallic highlight tiles
- 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 (it only writes tile numbers to Object RAM), 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.
Space Usage
| Asset | Size | % of Free Space |
|---|---|---|
| Spider-Man sprite copy | ~2 MB | 2.1% |
| Remaining free | ~94 MB | 97.9% |
We could theoretically copy ALL existing characters' sprites and still have 60+ MB for completely new content.
Program ROM: The Next Frontier
The one area we haven't expanded yet is program ROM. MvC1 uses 1MB but the hardware supports 4MB. Street Fighter Alpha 3 proves 4MB program ROMs work on CPS-2.
Expanding program ROM would give us 3MB of additional code space -- enough for:
- 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.
Summary
| Metric | Original | File Size After Expansion | Addressable (stock emulator) | Addressable (custom FBNeo core) |
|---|---|---|---|---|
| GFX ROM | 32 MB | 128 MB | 32 MB (mapper masks past 32 MB) | 64 MB (pixel-proven) |
| Audio ROM | 8 MB | 16 MB | 8 MB (QSound masks past 8 MB) | 16 MB |
| Program ROM | 1 MB | 1 MB | 1 MB | 1 MB (4 MB hardware max) |
| Build Pipeline | Manual | Automated | Python + vasm | Python + vasm |
| Verification | None | ULTRA SETTINGS (file-size based) | 22 diagnostics | + pixel-proof from upper GFX region |
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.
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