The received wisdom in the FGC is that Street Fighter III could never have run on CPS2. Capcom built CPS3 for it, the sprites are enormous, the animation counts are absurd, end of discussion.
I want to know whether that is true, or whether it is just something everyone repeats.
So I am testing it the only way that settles anything. Take a complete 3rd Strike character, put him on real CPS2 sprite rendering inside Marvel vs. Capcom, and measure what breaks first. Not a mockup. Not a mod that swaps a few frames. Every frame Ken has, drawn by the CPS2 sprite path.

The question, stated so it can be answered
"Could CPS2 have run 3rd Strike" is too vague to test. Broken into things that have numbers:
Three questions. Two now have measured answers.
Answer one, the art fits
I extracted 133 Ken poses from live CPS3 char RAM and counted unique tiles after deduplication.
That last number is the one that matters. Thirty seven megabytes of sprite tiles for the whole cast. For a 1999 arcade board that is enormous, but it is a quantity of ROM, not a property of the silicon.
Answer two, the sprite path reaches further than anyone says
Here is where I was wrong for an entire post, and the mistake is the useful part.
The custom FBNeo core this project uses already extends CPS2 graphics to 64 MB. When I seated Ken's tiles up there the character rendered as garbage, so I went looking for why, and found this sitting in the driver:
// SPRITES cannot be extended (raw 18-bit code | (y&0x6000)<<3,
// no mapper in Cps2ObjDraw, hard 32MB cap).I believed it, backed off, and reseated everything into the donor's cramped stock bank.
That comment is out of date. The sprite path does have a promotion, a few files away:
if (Cps2Turbo || Cps2ExtScroll) {
if (yw & 0x1000) yw |= 0x8000;
n |= (yw & 0xe000) << 3; // Y bit12 becomes code bit18
}And the driver's own startup log confirms it is live in this build:
CpsInit: nCpsGfxLen=0x4000000 Cps2ExtScroll=1A comment is not a measurement. I treated a stale code comment as evidence and let it override an experiment I had already run. When the character rendered as garbage the correct move was to instrument the sprite path and find out why, not to read the nearest available explanation and accept it.
Thirty seven and a half megabytes of art against a sixty four megabyte ceiling. So I built a ROM
that seats Ken above the 32 MB line, flipped the donor's bank word from $0006 to $000C, and
ran it.
He was invisible on the select screen and garbled in the match.
An earlier revision of this post said the art fits inside addressing this core already has. That was wrong, and the experiment above is what proved it. The promotion exists in the sprite code, but nothing triggers it: the game never sets Y bit12 for these sprites, and flipping the bank word does not make it. I had found a mechanism and assumed it was wired up.
My first explanation for this was that sprites never call the graphics bank mapper the way background layers do. That is true, and it is also not the reason. I checked it before building on it, and it does not survive.
Sprites do not need the mapper. They index graphics memory directly:
nCpstTile <<= 7; // tile index becomes a byte address
nCpstTile &= nCpsGfxMask; // and the mask is (1<<26)-1 at 64 MBNothing there caps a sprite at 32 MB. A sprite whose code is 0x40000 lands at exactly 32 MB and
would draw correctly. The mask is already wide enough.
So the real gate is narrower and more interesting. A CPS2 sprite cannot express a code that large in the first place. The object entry gives 16 bits of code word, and the only extra bits come from the Y coordinate word:
| Y bit | use |
|---|---|
| 0 to 9 | screen coordinate, masked to 0x3FF |
| 10, 11 | unused |
| 12 | promoted to code bit 18 when the extension is on |
| 13, 14 | code bits 16 and 17 |
| 15 | end of sprite list marker, not available |
Two bits of banking natively, giving 18 bits and exactly 32 MB. Reaching further needs bit 18, which can only come from Y bit 12, which the game has to set when it writes the object entry. It does not. Flipping a graphics bank word changes which tiles the game asks for, not which extra coordinate bits it writes.
Once you see it this way the theoretical limit is countable. Y bits 10 and 11 are unused. So are
X bits 10, 11 and 12, since X masks to 0x3FF and only bits 13 to 15 carry the priority level.
Against a roster that needs 37.5 MB, nineteen bits is already enough and twenty is comfortable. The work is not finding address space. It is making the game emit the bit, which means patching the code that writes object entries rather than patching the emulator.
Before any of that, the art had to come out of CPS3 correctly
Everything above rests on the extraction being right, and the version of this project I inherited had it badly wrong in a way that is worth showing, because every mistake in it looks like progress.
The inherited build shipped a document titled "Ken piece to pixel gate, Status PASSED". It claimed
a sprite format with a 12 byte header and a tag word that was always $0200. That format does not
exist anywhere in CPS3 hardware. It had been pattern matched out of ROM bytes by staring until
structure appeared, which is a thing bytes will always eventually do for you.
The evidence filed under PASSED was a mean normalised cross correlation of 0.563, with no control, and this contact strip:

Worse, none of it reached the ROM anyway. The builder that seated Ken's tiles read from a PNG:

So I threw it out and read the hardware instead. The real format is in MAME's driver: a four
dword object list pointing at three dword sub entries, with sizes indexed through
tilestable = {8,1,2,4}. The single most useful line in it is that when xsize == 0 the entry is
not a sprite at all, it is a command to draw a background tilemap. Skip those and you get every
character on screen with no background, for free.
Four bugs, three of them invisible
Tiles are 8bpp at 256 bytes, not 6bpp. Colour RAM is 0x40000 bytes, not 0x20000. The scroll
registers are write only, so a capture reads back zeros and you have to recover offsets from the
picture instead.
And then the one that took longest. MAME holds char RAM as 32 bit words but decodes it through a byte pointer, so on a little endian host the bytes inside every word are reversed relative to what a Lua read hands you:
Look at the left one. Blonde hair, white gi, black belt, brown gloves. It is obviously Ken. If you were eyeballing your way to success you would call that a win and build everything downstream on a decoder that gets every pixel wrong.
The gate

Exact equality against a control two orders of magnitude lower. That is a gate. A similarity score with no control, like the 0.563 above, is not.
Getting it onto CPS2 at all
Conversion is where the format tax lands. CPS3 gives 8bpp art. CPS2 sprites get fifteen colours plus transparency, and sprites treat pen 15 as transparent rather than pen 0, which will turn your character into a silhouette if you assume otherwise.
Two palette bugs are worth carrying if you ever do this.
The first deletes a colour channel silently. The packing is three shifts, and the values arrive
from the quantiser as numpy uint8, where (r >> 4) << 8 overflows to zero. Every red nibble was
being discarded by a function with no error and a perfectly reasonable looking implementation.
The second is subtler and cost longer. CPS2 sprite hardware addresses the palette row with the 4 bit reversed pen index:
0 8 4 12 2 10 6 14 1 9 5 13 3 11 7 15That permutation is an involution and 15 maps to itself, so transparency looked perfect the whole time while every visible colour was scrambled.
Quantisation loss, an overlapping sprite, or a palette write that never landed all fit that symptom. Flattening Ken's entire palette row to one impossible colour and counting pixels distinguishes all three in a single run, because it converts a question about colour into a question about pixel count. In game: 4,551 magenta. Offline render: 4,551 ink. Exact match, so nothing was overlapping him and the row was live, which leaves only the pen mapping.
Answer three, this is what actually stops you
Not memory. Not addressing. The animation format.
CPS2 characters are driven by chains of 16 byte records. The engine walks a chain by advancing 16 bytes at a time until it reaches a record whose control word has bit 15 set, then jumps to the longword that follows. One record, one displayed frame.
Gambit, the donor slot Ken rides, has an entire animation tree of 21,630 bytes. That is 1,351 record slots for everything he does. Standing, walking, every attack, every reaction, every win pose.
Ken in 3rd Strike has far more animation than that per action. His idle alone is smoother than anything in Marvel vs. Capcom, because CPS3 was built to afford it.
My current build seats 89 records. Everywhere it does not reach, the donor shows through. That is why the select screen animates as Gambit, why win poses are his, why air attacks are his, and why a ground normal flashes one Ken frame then reverts.
None of that is a separate bug. It is one fact reported nine ways. The donor's chains are too short, so they have to be grown rather than filled.
What is running right now
Twelve chains, 89 records, 1,630 unique tiles, one shared palette row.

The lifebar still reads GAMBIT, deliberately. MvC1 draws lifebar names as tile graphics rather than strings, and an earlier version of this project scrambled the game by writing guessed pointers into that table.
Known wrong, stated plainly
I would rather list these than have you find them.
| symptom | cause |
|---|---|
| Select screen animates as Gambit | select and win pose chains not seated |
| Attack frames face the wrong way | mirror flag wrong, extracted art faces opposite |
| Idle looks glitchy and short | 24 donor records holding a much longer CPS3 idle |
| Ground normals flash Ken then revert | only the first records of each chain seated |
| Air and jump attacks all Gambit | those chains not traced or seated |
| Shoryuken shows Ken knocked down | frames labelled by the input I sent, not by what Ken did |
That last one deserves calling out. In the extraction sweep I saved every frame under the name of the button I was pressing. When a motion failed or Ken got hit, the file still went into the shoryuken pile. I seated one of those and got a sprite lying on its back. The chain trace had the opponent neutralised and reached zero contamination. The frame extraction never did, and I carried its labels forward without rechecking them.
What comes next, in dependency order
- Make the game emit Y bit 12 when it writes object entries for the imported character, so sprite codes can carry bit 18 and reach past 32 MB. The emulator side already accepts it.
- Re-extract every Ken frame with the opponent neutralised, so labels mean what they say and the frame count is measured rather than estimated.
- Author new longer chains in the 1 MB program window, one record per Ken frame, and repoint the donor's chain starts at them. This is how a CPS2 character gets CPS3 frame counts.
- Seat everything in the expansion rather than the donor's shared bank.
Step 3 is the thesis in one sentence. If a CPS2 character can be given CPS3 frame counts and still render correctly, the format was never the barrier either.
Where this actually stands
Numbers rather than adjectives. Updated every time the build changes.
| milestone | measured | state |
|---|---|---|
| Sprite decode matches MAME | 98.35 percent exact RGB, 1.81 percent control | done |
| Ken poses extracted | 133 across 17 moves | partial, sweep labels unreliable for specials |
| Converted to CPS2 tiles | 1,630 unique, 0.20 MB | done for what is extracted |
| Records seated in the donor | 89 of 1,351, 6.6 percent | in progress |
| Chains covered | 12 | in progress |
| Colour accuracy after conversion | 74 to 88 percent exact | done, limited by one shared palette row |
| Sprites addressing past 32 MB | not reached, cause identified | blocked on Y bit 12 |
| Chains grown to CPS3 frame counts | none | not started |
| Lifebar name, select art, win poses | none | not started |
| Sound | none | not started |
Against the thesis specifically:
| question | answer |
|---|---|
| Does the art fit in addressable ROM? | yes, 37.5 MB for the roster against a 64 MB reachable ceiling |
| Can a sprite code reach that far? | not yet, needs one spare coordinate bit the game does not set |
| Can the animation format hold CPS3 frame counts? | unknown, this is the next real work |
Six point six percent of one character, in one donor slot, in one game. That is the honest position. What has been settled is that the two limits everyone assumes are fatal, ROM size and sprite addressing, are both quantities rather than walls.
Why this matters beyond one character
If the art fits, and the sprite path reaches it, and the animation format can be grown, then the honest answer to the original question changes. Not "CPS2 could never have run 3rd Strike" but "CPS2 could have run 3rd Strike given enough ROM, and CPS3 existed for reasons that were partly about cost and cartridge economics rather than pure capability."
I do not get to claim that yet. One of three answers is measured and favourable. The second came back negative on the first honest test, with a known cause and a known fix. The third has not started.
That is a worse position than this post originally claimed, and a much better one to actually build from.




