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.
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:
| 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:
graph TB
subgraph "CPS-2 Hardware Limits vs. Largest Shipping Games"
subgraph "Graphics ROM"
GMax["Hardware Max: 128 MB"]
GShip["Largest shipped: 32 MB (MvC1, SFA3)"]
GGap["Unused capacity: 96 MB (75%)"]
end
subgraph "Program ROM"
PMax["Hardware Max: 4 MB"]
PShip["Largest shipped: 4 MB (SFA3)"]
PGap["SFA3 uses full space!"]
end
subgraph "Audio Sample ROM"
AMax["Hardware Max: 16 MB"]
AShip["Largest shipped: 8 MB (most games)"]
AGap["Unused capacity: 8 MB (50%)"]
end
end
style GMax fill:#112d20,stroke:#419865,color:#eaf2ef
style PMax fill:#12303a,stroke:#397b83,color:#eaf2ef
style AMax fill:#33290f,stroke:#b28c36,color:#f4e9c8Why 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)
...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:
packet-beta
title One 64 bit GFX fetch reads 1 byte from each of 8 interleaved chips
0: "mvc 13m"
1: "mvc 14m"
2: "mvc 15m"
3: "mvc 16m"
4: "mvc 17m"
5: "mvc 18m"
6: "mvc 19m"
7: "mvc 20m"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 |
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: ~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).
flowchart TD
gfx["Empty GFX space"] --> ff["Pad with 0xFF"]
ff --> tr["0xFF is a TRANSPARENT pixel<br/>in the CPS-2 tile format"]
tr --> safe["No artifacts from unused regions,<br/>GPU skips transparent tiles,<br/>safe to read from any expanded address"]
gfx -.->|"the tempting mistake"| zero["Pad with 0x00"]
zero -.-> black["0x00 is BLACK, not transparent<br/>-- sprites from the expanded region<br/>render as solid black rectangles"]
aud["Empty audio space"] --> z2["Pad with 0x00"]
z2 --> sil["0x00 is digital silence<br/>in QSound PCM<br/>-- no glitch if the DSP reads it"]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:
flowchart TD
A["Load Original ROM ZIP"] --> B["Expand 8 GFX: 4MB → 16MB"]
B --> C["Expand 2 Audio: 4MB → 8MB"]
C --> D["Plant GOLD Marker"]
D --> E["Apply Code Patches"]
E --> F["Assemble 68K Handler"]
F --> G["Save 148MB ZIP"]
style A fill:#12303a,stroke:#397b83,color:#eaf2ef
style G fill:#112d20,stroke:#419865,color:#eaf2efKey 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 |
| Extra-Large Characters | 4+ MB | up to 24 | Onslaught-class boss sprite sets |
| 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, 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
| 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: Correction -- It Is Already Full
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 MBMarvel 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.
- 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
Why this matters more than it sounds
A native character port needs her data resident in that space. Measured for Dark Sakura in MSHvSF:
- 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
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
- 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.
- Reclaiming in place. Free a contiguous block from data that is no longer needed. Bounded by what is actually spare.
- 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:
- The MAME driver expects specific file names and sizes for program chips, and does not simply warn and continue
- New code has to be position-aware -- 68000 absolute addressing means relocated code does not just work where it lands
- 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
| 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 | 4 MB (already full) | 4 MB | 4 MB | 4 MB — no headroom; expansion needs banking |
| Build Pipeline | Manual | Automated | Python + vasm | Python + vasm |
| Verification | None | ULTRA 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.
- 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
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