guideUpdated Aug 13, 20266 min read$2C

A Marvel vs. Capcom 3 Stage Has to Fit in 1,632 KB. Now Port It.

That is Dreamcast MvC2's measured texture budget. The decoded stage format and conversion plan show what must be cut; a converted stage has not booted yet.

mvc2mvc3dreamcastnaomistage-modding
On this page

One request comes up whenever stage mods do: take a Marvel vs. Capcom 3 stage — Kattelox Island, the Daily Bugle — and put it in Dreamcast MvC2. Not a texture swap. The stage.

Short answer: it's real work but it's not research anymore — every link in the chain already exists. The honest version of the answer is a budget problem, and the budget is brutal. This post walks the actual numbers from my own dump, because "just port it" changes meaning when you see what an MvC2 stage physically is.

What an MvC2 stage actually is

Crack open the US Dreamcast disc (redump 3-track bin/cue; high-density data track is 1,185,760,800 bytes like every GD-ROM) and the stage inventory is plain in the root directory: 17 stages, exactly two files each.

STG00POL.BIN    173,976     STG00TEX.BIN  1,671,168
STG01POL.BIN    116,384     STG01TEX.BIN  1,671,168
STG02POL.BIN    168,592     STG02TEX.BIN  1,654,784
...
STG10POL.BIN    170,720     STG10TEX.BIN  1,441,792

POL is geometry, TEX is textures. That's the whole stage. No shader packages, no lighting rigs, no streaming — two files.

The observed ceilings across all 17 stages: geometry tops out at 194,008 bytes (STG03POL.BIN) and textures at 1,671,168 bytes = 1,632 KB (several stages sit exactly there — it's the cap the community's stage tutorial documents, and the dump agrees). Whatever MvC3 stage you love, it has to become ≤190-ish KB of geometry and ≤1,632 KB of 16-bit textures.

POL: pointer tables aimed at a fixed address

First bytes of STG00POL.BIN:

0000  10 00 ea 0c 0f 00 00 00 50 00 ea 0c 20 01 ea 0c
0010  20 01 ea 0c 68 89 eb 0c e0 f0 eb 0c e8 2a ec 0c

Read those as little-endian words: 0CEA0010, count 0000000F, then a run of pointers — 0CEA0050, 0CEA0120, 0CEB8968… Every one is an absolute SH-4 RAM address. The file is built to be loaded at 0x0CEA0000 and used in place: a section-count header and a table of absolute pointers into itself (15 sections for STG00, 25 for STG05 — it varies per stage). There is no relocation. That matches how the community builds custom stages: geometry is assembled, literally, with an SH-4 assembler, so the addresses are baked at build time.

TEX: raw twiddled PVR banks

STGxxTEX.BIN has no header at all — it's concatenated PowerVR texture data, 16 bits per pixel, in the Dreamcast's Morton-order "twiddled" layout. Read it linearly and you get colored noise. Untwiddle it and the stage art falls out:

The same 256x256 texture from STG00TEX.BIN read linearly (scrambled bars) and after Morton-order untwiddling (clean wooden door artwork)
THE SAME BYTES TWICE // left: STG00TEX.BIN offset 0 read as linear RGB565; right: the identical data untwiddled (Morton order). No header, no compression — just twiddled 16bpp

Here's the untwiddler core, small enough to gut-check — this is the whole trick, interleaving x/y bits:

def expand(v):                      # spread bits: abc -> a0b0c
    v = (v | (v << 8)) & 0x00FF00FF
    v = (v | (v << 4)) & 0x0F0F0F0F
    v = (v | (v << 2)) & 0x33333333
    v = (v | (v << 1)) & 0x55555555
    return v
 
idx = (expand(x) << 1) | expand(y)  # Morton index into the texture
px  = u16le(tex[2*idx:])            # RGB565 (opaque) / ARGB1555 (cutout)

Formats are the stock PVR pair the tutorial names: RGB565 for opaque, ARGB1555 for anything with cutout transparency, power-of-two sizes from 2 up to 1024.

Four untwiddled lead textures from four different MvC2 stages arranged in a grid
RECEIPTS // the first texture of four different stages (STG00/03/07/0C), decoded straight from my dump with the code above

What an MvC3 stage is, by contrast

MvC3/UMvC3 is MT Framework: stages are .arc archives of .mod meshes with .tex textures, real-time lighting, shader materials, animated set pieces, and texture budgets per object that dwarf an entire MvC2 stage. The extraction half is solved by the community: ARC unpackers and MT Framework model tools get stage geometry and textures into Blender today.

So the port is never a translation. It's a demake with a fixed exchange rate:

MvC3 hasMvC2 acceptsconversion
shader materials, real-time lightsvertex color + baked texture lightbake in Blender; flatten every material to one textured quad-mesh
meshes in the millions of tris~190 KB of assembled geometrydecimate hard; rebuild as quads (the format wants 4-vertex faces); split into floor / midground / background layers like the stock stages
hundreds of MB of textures1,632 KB total, 16bpp, pow2 ≤1024bake+atlas to a handful of sheets; RGB565/ARGB1555; twiddled PVR
animated set pieces, crowd systemsstatic geometry (plus whatever palette tricks you earn)accept the loss, or fake motion with UV/palette cycling later
its own cameraMvC2's fixed stage camera, floor plane, and boundsmodel to MvC2's world scale; the stock stages are the reference

The working pipeline, end to end

Nothing here is hypothetical — each step is an existing tool:

  1. Extract the MvC3 stage: ARC unpack → MT Framework model import → Blender (the UMVC3 importer docs cover the model side).
  2. Demake in Blender: decimate, quad-ify, split into layers, bake all lighting to textures/vertex color, atlas UVs onto pow2 sheets.
  3. Textures out: PVR Tool (RGB565/ARGB1555, vertical flip), then the community's buildTEX.py to pack the bank — stay under 1,632 KB.
  4. Geometry out: Blender-to-.asm export, headers describing mesh/texture assignment, then sh4_asm to a POL.BIN — same path the from-scratch custom stages use.
  5. Inject + rebuild: drop the pair over a stage slot (STG05* etc.), rebuild with GDROM Explorer + mkisofs/cdi4dc, boot it in Flycast before ever burning anything.
  6. Verify like we verify CPS-2 work: hash the two files, screenshot the stage from fixed camera states, and keep the stock pair for A/B.

Where the bodies are buried

  • The texture wall is the fight. 1,632 KB at 16bpp is about 800K pixels — one 1024×512 sheet plus change, for an entire stage. MvC3's look survives on baked lighting quality, so the atlas/bake step decides whether the port reads as "MvC3 in MvC2" or as a blurry postcard.
  • Absolute pointers mean assembled output, not patched output. POL files carry baked 0x0CEAxxxx addresses; you generate a fresh file with the assembler toolchain rather than hex-editing geometry in place.
  • Slot discipline: replacing a stage in place keeps the filesystem simple; going bigger than the stock extent means a full disc rebuild — which the CDI pipeline does anyway, so the real limit is the Dreamcast's texture memory, not the ISO layout.
  • NAOMI is a separate release target. Everything above is the Dreamcast disc. The arcade/NAOMI packaging is different containers around close cousins of these formats — worth doing after a DC build proves the art.

The next post in this thread should be receipts, not maps: one MvC3 stage actually crushed through this pipeline into a booting Flycast capture. The decoder above already proves the read side; the write side is the same trip in reverse.

Written by Daniel Plas Rivera · 1,376 words · $2C

ShareXLinkedIn