Marvel vs. Capcom · inside CPS-2

Six buttons.
Keep every choice.

The cabinet already had six attack buttons. The game saved only “punch color” or “kick color.” I gave each button its own index, then followed that choice through the readers that draw the fighter.

Press a button, break a reader, fill the available space. These small experiments show how the system works—and how to build on it.

Native palette notes and technical review: N41.

01

One button. One choice.

Compare the selector rules, then press a physical button. The same sprite can keep its shape while its palette changes.

Unmodified native v9.1 War Machine capture for the LP color
Native capture v9.1 diagnostic · LP

Physical buttons

LP →Color 1 / 9

The six singles remain separate. LP+LK, MP+MK and HP+HK select the three extra choices.

Selector simulation; no emulator runs here. Screenshot pixels and game timing are unchanged.

One choice. Several readers.

A correct body color can still become the wrong color during selection. Switch the readers on one at a time to see why.

02

Follow every reader.

A correct body does not prove the next screen or animation uses the same source. Give each reader the selected palette.

Reader model
Button confirmationStores index 5

Fighter body

012345

Selected index survives

Select figure

01

Falls back to index 1

Selected animation

01

Falls back to index 1

1 / 3 readers agreeThe remaining path still treats every nonzero index as the kick choice.

This models a two-choice Boolean reader. A real animation can also have a hardcoded base or temporary palette source; changing the stride alone does not establish its ownership.

The first body patch looked finished until the large select figure disagreed. Fixing that exposed a third reader: the selected-character animation collapsed every nonzero choice to bank 1. Each consumer needed to retain the full index.

The useful lesson: trace the choice from input to stored byte to source pointer to palette RAM. A screenshot at the end cannot tell you which earlier reader lost it.

Room that was already there.

0x184000 was existing padding. No sprites were removed, no sound was sacrificed, and no compression was needed for this bank.

03

Make the bank fit.

One body choice takes 96 bytes. Test the group counts against the historical 16 KiB slot; an allocation that crosses the boundary is refused.

Choices per group
Group count
6 choices×25 groups×96 bytes=14,400 B
0x184000Historical slot · 16,384 bytes0x188000
Requested end0x187840
Space left1,984 B
Calculator modelChoose a layout, then test the complete range.

The standalone six-color arena used 23 groups (13,248 bytes). The combined arena uses 25 (14,400 bytes), adding two Ryu forms. The current nine-choice body bank is separately allocated at 0x239E00–0x23F260 (21,600 bytes). These are CPU addresses; the historical start is file offset 0x4000 in mvc.06a. This calculator does not allocate ROM memory or change a build.

The clean Euro game has a continuous FF run from 0x183E7A to 0x188000. Starting at the aligned address 0x184000 leaves 16,384 bytes in mvc.06a. That CPU address is file offset 0x4000 in this member.

0x3F4000 held the standalone selector code, rather than the body colors. Combined DHC moved that code to 0x3F8000, added its v2 helper at 0x3F8300, and used 0x3F5000 for large select-art palettes. Nine choices need a larger body bank at 0x239E00.

Keep the drawing. Change the colors.

A sprite pixel stores a pen number. The palette gives that number its color. Try a pen and change its RGB channels.

04

The pixel stores an index.

Choose a pen, then change its RGB nibbles. Every model pixel pointing at that pen keeps its index and receives the new color.

Illustrated index plane

The diagram is an index model, not game sprite art.

Preview pen D0x02BD
Index 13 → RGB 0x2BD4 bits per channel · 4096 RGB combinations. Brightness bits are separate from RGB.

The initial 15 visible RGB values are War Machine’s native P body row. Indexed previews reserve pen 0, so preview pens 1–15 map to native visible pens 0–14. Web swatches show RGB only; this does not emulate CPS2 brightness or edit palette files.

A body block is 0x60 bytes: 48 words arranged as three 16-color rows. Keep the indexed artwork and transparent entries intact. Treat brightness separately from the low 12 RGB bits; the value a loader writes can include its own brightness handling.

Forms, portraits and attack effects can choose their sources independently. Ryu’s Ken/Akuma forms need their own complete banks. A second row in a palette file does not establish which attack owns it.

From experiment to patch.

The repeatable method, the address map and the checks that make room usable.

Start with the choice the game throws away

Stock MvC has six attack buttons, but its palette selector collapses them into two values: punches select 0 and kicks select 1. The restriction is in the software. More buttons do not create more palette choices while that stored value remains a Boolean.

N41's notes identify the native palette directory and point out that portraits and intro palettes have separate consumers. I used that distinction while following the native selector into the select object's color byte at +0x61, through the confirmed-slot latches at 0xFF40C4..0xFF40C7, and into the fighter's color byte at +0x277. The patch keeps a full index instead: 0–5 for six choices. DHC v9.1 adds three paired-button choices, 6–8.

Every reader must agree about that index. The large select figure, selected animation and live body were three separate paths in the standalone patch. A correct match body did not prove the other two. The animation at 0x07AF6C still chose bank 1 for any nonzero index until its reader was widened too.

Measure the room before using it

On the clean Euro 980123 set, mvc.06a contains an existing FF padding run from CPU 0x183E7A to 0x188000, exclusive. Aligning the start to 0x184000 leaves exactly 16,384 bytes. It is file offset 0x4000 in that 512 KiB member. The patch uses that existing room; it does not remove sprites, sound or old palettes to create it.

A complete body palette block occupies 0x60 bytes. The bank sizes explain both what fits and why the nine-color version moved:

LayoutGroups × choices × bytesBody bank, end exclusive
Standalone v123 × 6 × 96 = 13,2480x184000..0x1873C0
Combined six-color v225 × 6 × 96 = 14,4000x184000..0x187840
DHC v9.1 nine-color release25 × 9 × 96 = 21,6000x239E00..0x23F260

There are 21 stock-selectable identities. The original table also reserved body rows for Dark Sakura and Shuma-Gorath; data rows alone do not make them playable. Combined v2 adds separate Ryu Ken/Akuma form groups, bringing the body directory to 25. Its six-color bank leaves 1,984 bytes in the old slot. Nine choices exceed that slot by 5,216 bytes, so the new bank uses a separately qualified range. It does not spill into the following stock data. Repointing a directory changes where a reader looks; it does not erase the original palettes. ARCADE keeps its native sources, and the older six-color arena remains retained in this nine-color diagnostic.

0x3F4000 was the standalone code location: the 744-byte six-color payload. The body bank was at 0x184000. In combined DHC, the core moved to 0x3F8000 and the v2 helper uses 0x3F8300; large select-art palettes use 0x3F5000. The older code slot serves the combined magnetic extension. A code address, body bank and presentation bank have different jobs even when all occupy existing program-ROM space.

Check the padding in your own clean input

This read-only check verifies the exact member before counting the available space. It produces no patched ROM and does not need a decryption key.

import hashlib
import zipfile
 
with zipfile.ZipFile("mvsc.zip") as archive:
    chip = archive.read("mvc.06a")
 
expected = "132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b"
assert len(chip) == 0x80000
assert hashlib.sha256(chip).hexdigest() == expected
slot = chip[0x4000:0x8000]
assert slot == bytes([0xFF]) * 0x4000
print("16,384 existing bytes at CPU 0x184000..0x188000")

For another game, an FF run is a candidate, not permission to overwrite it. Check the actual address map, code and data references, computed pointers, and every component already using that range. Reserve an aligned interval, assert the complete clean preimage, and reject overlaps before writing. Keep every original member's length exact. Retest the paths that could touch that region, including alternate modes and forms.

Build a directory, then change its readers

A color bank needs a pointer per fighter/form, a fixed stride, and an index that stays within the number of installed blocks. The body directory is at 0x1C7268, with longword pointers indexed by characterId / 2. The native reader families revealed where the stored choice became a Boolean offset. Nine body-reader families were widened; the similar-looking assist/effect reader at 0x00A920 retained its native source behavior. For the combined six-color layout:

fighter base = 0x184000 + compact_group × 0x240
color source = fighter base + color_index × 0x60

The nine-color layout changes the group stride to 0x360 and base to 0x239E00. Changing the table alone is insufficient: a consumer that tests “zero or nonzero” will still collapse choices, and a transformed fighter that keeps a two-block table can read unrelated data for index 2.

Keep the code and data views separate. 0x184000 is a CPU address; mvc.06a + 0x4000 is its raw member location. CPS-2 instruction bytes also have an encrypted/decrypted distinction, while palette data must retain the loader's word layout. Use the correct view for each operation and compare exact preimages instead of copying an address from another revision.

MvC2's donor records are little-endian ARGB4444. The original extractor checks alpha, keeps the lower 12 RGB bits, and maps visible donor pens into the native row layout; it does not turn RGB565 into RGB444. Keep that exact nibble-copy process separate from the RGB888-to-RGB444 quantization used for later indexed PNG imports. Mapping colors into a native row requires proving the pen layout, not just making the swatches look right.

Make every surface prove its own result

The original standalone release physically selected all 21 vanilla identities with each of the six buttons: 126 selections. Each accepted run compared 32 words for the selected animation, 48 for the large figure and 48 for the body. That scope does not include every portrait, helper or attack effect.

The latest DHC palette revision has different checks: complete source-bound palette reconstruction, 1,800 retained-code artwork upload cases, and 45 secret choices compared against 216 regular/form references. Those fixture checks do not replace natural rendered WIN scenes, fresh all-secret selection captures, normal-speed audible sound or Windows/online acceptance.

When extending another game, test the full path rather than just the new lookup: native buttons, duplicate fighters, both sides, tag/ownership changes, transformations, interrupted actions and return to the selected color. Compare all words the reader uploads. Keep effect and portrait ownership explicit.

N41's contribution

Thanks to N41 for documenting the native palette directory and separate consumers, and for asking for decoded instructions, breakpoint/register checks and boundary tests. His questions pushed the write-up to explain what a changed hex pattern actually does, rather than treating a range of modified bytes as an explanation. His selection-transition report also identified a specific follow-up: whether a palette write can happen one frame before the sprite's character identity changes. That requires recording both writes and the intervening rendered frame; it is not established as fixed by the six-color selection census.

The ROM-space question is another useful review prompt: identify what already occupied a range, show the preimage, and explain where the code and color data went. These checks make the technique usable by someone building their own patch. N41 is credited for that technical review, the documented native structure and the reported timing issue.

Continue from the measured sources

The original selector assembly and standalone hashes preserve the v1 implementation. The full historical notes contain the Ghidra setup, encrypted preimages, donor mapping and native traces. Their historical build status remains labeled separately from today's release.

The DHC page covers the combined project's current v9.2.1 release and remaining checks. The workshop models are explanations you can manipulate; the original screenshots and receipts remain the evidence. Capcom's artwork and native palettes retain their credit; the standalone body's donor banks came from MvC2. This article is about carrying a color choice through the engine.

The current v9.2.1 update corrects Orange Hulk’s seven added large SELECT face palettes. His two original faces, all nine body colors and the other secret artwork stay unchanged. Focused notes separate machine palette checks from native rendering still awaiting playtesting.

The original six-color result.

The standalone v1 reel: 21 selectable fighters × six physical buttons. This is the original evidence, with its original scope.

All 126 standalone v1 color selectionsYouTube ↗

The accepted v1 runs compare the selected animation, large figure and match body. The workshop’s nine-color images come from the later DHC diagnostic; they do not add new validation to this historical reel.

Play it. Study it. Extend it.

Choose the standalone palette patch or the combined DHC project. Their releases and checks stay separate.

STANDALONE · V1

Six colors, without the DHC changes.

The original patch-only release for your own clean Euro mvsc.zip.

Download standalone v1 ↓

COMBINED · DHC

Nine colors in DHC v11.

DHC includes the palette system alongside its gameplay changes. The latest secret-color revision and remaining tests are documented there.

DHC downloads and checks ↗

Original selector assembly · Standalone hashes · Full historical engineering notes

Community input · Next release

Help improve the MvC six-color patch

Choose up to 3 priorities. No account needed.