This architecture was retired in July 2026. It is kept as a record, not as a description of anything currently running.
Superseded by: the native port.
This post documents the donor chassis, the first architecture that made Dark Sakura playable on Marvel vs. Capcom's secret character id $2E. It works by running Ryu's brain inside her body. It shipped, it fought, and it was later retired in favor of a fully native port where she runs her own relocated code with no Ryu underneath (Marvel vs Capcom Had No Room for a 24th Character). Everything below is the historical record of the donor build: the architecture, why it worked, and exactly where it hit a wall. The critical-path items still marked planned or in progress here (authentic tatsu cells, Shououken, Haru Ichiban, the Hunter chain system) were overtaken by the native pivot and never shipped on this donor lineage. They are not claimed to have. You do not need to have read any other post to follow this one.
Goal: Make Dark Sakura (MSHvSF char-id $2A) an actually playable fighter on MvC1's secret character id $2E, an id the engine's 23-entry tables stop short of, with normals, specials, super jump, and her own animation on every frame. Not just a loaded sprite set.
Constraints: No source code. The base ROM is never modified; every change lands in a rebuilt output ROM set. Her animation tree, framedata, and 9,000+ tiles were imported, but her recognizer code was not and cannot be (it lives in a different program ROM). Her tiles overflow the stock 32MB sprite region, so she requires a custom 64MB FBNeo core, which breaks Fightcade netplay. Offline only.
Approach: The "donor chassis": force-remap her in-fight char-id from $2E/$00 to $12 (real Ryu) at the state-machine dispatch layer, so Ryu's input detector, animator, and handler spine run her, while a gated composer hook forces HER imported animation tree (DS_TREE at $3D0700) so every frame draws her cells. A 19-row translation table bridges the two move namespaces in the specials band. A hand-assembled m68k driver replaces Ryu's special-move phase machines with hers.
Result: Attack commits (b0=$28) went from 0 in 8,400 probe frames to 117; all six normals compose her native records (live-verified). Tatsu arc rises 80px against a 78 to 83px native spec. v19: the super-jump flag found at $BA(a6), a one-word fix; the probe shows a 368px rise on her own records and a launcher super-jump-cancel; 81 missing crouch-HK tiles recovered (9,439 -> 9,520 verified).
Proof / Validation: Headless FBNeo Lua probes with scripted inputs, plus live hands-on passes as the final oracle. Every hand-assembled thunk is capstone branch-asserted at build time. Honest caveat: the v19 super jump is probe-verified only. It was deployed, but the native pivot arrived before a hands-on pass confirmed it.
Artifacts: Four classes. Analysis notes: the port plan, the dead-recognizer diagnosis that adjudicated the chassis choice, and a period-accurate movelist reconstructed from 1998 arcade sources as the design ground truth. A standalone m68k assembler for the special-move driver, plus the sha-asserted blob it emits. A build chain that composes remap thunks, composer hook, translation table, and driver into an output ROM set, failing loudly on any expected-bytes or branch-assert mismatch. And three headless probe harnesses: one for attack commit and normal composition, one for specials-band translation, one for jump physics.
The Setup: An ID the Roster Was Never Built to Hold
Marvel vs. Capcom's program ROM does not carry an unfinished character slot waiting for a tenant. It carries 23 characters and stops. Every per-character table in the engine has exactly 23 entries, ids $00 through $2C; $2E is one step past the last row of all of them, with no entry of its own. It resolves only by borrowing $0E's, Spider-Man's.
- 123 rows, ids $00 to $2C$000000–$00002E · 46 BTwo bytes per row. This is the whole table.
- 2id $2E: no row of its own$00002E–$000030 · 2 BOne step past the last row. Nothing in the engine stops the read.
- 3bytes of the next table$000030–$000038 · 8 BZero slack: it begins on the very next byte.
- A$0E, Spider-Man: the row $2E resolves onto$00000E · marker
- Blast valid row, id $2C$00002C · marker
Earlier in this project that borrowed identity hosted an Armored Spider-Man / Iron Spider variant, which is exactly why it worked as far as it did: it was a palette-and-flag recolor of the character whose table row it was already stealing. Real injection work, but a recolor. It was retired in early July when two facts made something far more ambitious possible:
- MSHvSF and MvC1 are same-generation engines. Dark Sakura's framedata is byte-identical between the two games, all 759 mode-2 animation blocks, pixel-proven. Her data doesn't need conversion; it needs relocation.
- A custom 64MB core broke the 32MB sprite wall. Her tile set (9,439 tiles at first, 9,520 after the fix below) seats into the upper GFX region with zero remapping.
So $2E became Dark Sakura, ported whole from MSHvSF where she is char-id $2A. The secret-code handler, committed-slot guards, and select-screen wiring were all reusable from the retired occupant.
By the start of that week she loaded, animated, walked, jumped, and took hits, all on her own imported records.
She couldn't attack. Picture that for a second: a fighting game character, fully drawn, fully animated, who cannot throw a single punch.
The Diagnosis: A Recognizer That Never Fires
So why couldn't she attack? An 8,400-frame instrumented capture with scripted six-button, QCF, DP, crouch, and jump inputs settled it:
| Signal | Expected | Observed |
|---|---|---|
b0 state byte reaches $28 (attack) | On every button press | Never (grep -c ',28,' = 0) |
b7 (commit priority) | Nonzero on attack | $00 every frame |
a7 (move id) | Nonzero on attack | $00 every frame |
b0 values seen | Full state set | {00 02 04 06 08 0A 1E 24 36}: movement, hit, jump, crouch only |
Her attack recognizer was 100% dead, and the root cause is an architecture lesson I'll never need to relearn: the port had imported her data (tree, framedata, tiles) but not her recognizer code. Her in-fight char-id field $52 was $00, and the engine's $52-keyed detector table at $18712 resolves entry [$00] to $02848A:
02848A: moveq #0,d0
rtsflowchart TB
btn["Button press"] --> det["Engine's detector table<br/>at $18712, keyed by $52"]
id["Her in-fight char-id $52 = $00<br/>(the dummy slot)"] --> det
det --> stub["Entry [$00] resolves to $02848A<br/>moveq 0,d0 / rts"]
stub --> nil["Two instructions, return.<br/>b0 never reaches $28<br/>in 8,400 probe frames"]
imported["What the port imported:<br/>tree, framedata, 9,520 tiles"] -.->|"data, not code"| gap["Her recognizer lives in a<br/>different program ROM<br/>and cannot come with her"]
gap --> idA stub. Char $00 is the dummy slot. Every button press in that 8,400-frame log walked into two instructions and returned. This is the same class of finding as the original $2E forensics (not broken code, absent code), except this time I was the one who left it absent.
The Failed Approach: Reanimate In Place
My first fix attempt kept $52=$00 and tried to hook the animator dispatch to give her attack animations directly, to reanimate her in place. It failed twice, and both failures were worth the tuition:
- The first hook pointed the dispatch at
$068E5A. That is Shadow Lady's animator (not a shoto, wrong move structure), and it crashed. - The deeper refutation came from static analysis:
b0=$28(the attack state) is committed only by the shared input processor ($0181C8->$018542), which is reached from theb0state-handler spine, never from the animator dispatch. Ryu's animator at$044290contains zeromove.b #$28,$b0instructions. No animator hook, however clever, can make a character attack. The approach was structurally dead, not merely buggy.
flowchart TD
inp["Button press"] --> proc["Shared input processor<br/>$0181C8 to $018542"]
proc --> only["The ONLY place that commits<br/>b0 = $28, the attack state"]
only --> spine["Reached from the b0<br/>state-handler spine"]
hook["Attempted fix:<br/>hook the animator dispatch"] --> anim["Ryu's animator at $044290"]
anim --> zero["Contains zero<br/>move.b to $b0 instructions"]
zero --> dead["No animator hook, however clever,<br/>can make a character attack"]
hook2["First hook pointed at $068E5A"] --> sl["That is Shadow Lady's animator<br/>(not a shoto, wrong move structure)"]
sl --> crash["Crashed"]The conclusion: attacking is not an animation. It's a decision, and the decision-maker is selected by $52. If her decision-maker doesn't exist in this ROM, borrow one that does.
The Donor Chassis: One Force-Remap, One Composer Hook
The architecture that shipped, and instantly fixed "she can't attack," is a force-remap at the state-machine dispatch layer. Two existing thunks (the remap thunk and the crash-guard thunk) each gained one instruction:
move.w #$12,$52(a6) ; in-fight char-id := REAL RYUHer committed select-id stays $2E (that's her identity, palette, portrait, tile bank). But every frame, the state machine dispatches her as $12 (Ryu), so Ryu's entire behavioral spine runs her, consistently:
| Layer | Table | Entry for $12 |
|---|---|---|
| Input detector | $18712[$52] | $0490C0 (Ryu's button/motion decoder) |
| Animator dispatch | $18DF0[$52] | $044290 |
b0=$28 attack handler | $E6B8[$52] | $0463C0 |
And one hook keeps her hers: the composer at $135E6, the single point where every (group, sub) animation request resolves to a record pointer, is intercepted by a gated thunk (keyed on her $276 identity flag) that forces the tree base to DS_TREE at $3D0700: her imported animation tree, feeding her 9,520 tiles in the 64MB core's upper GFX region.
The whole trick is exactly two seams, the char-id word and the tree-base pointer, stitched every frame:
sequenceDiagram
participant SEL as Committed slot $2E
participant THUNK as remap and crash thunks
participant RYU as Ryu machine $12
participant COMP as Composer $135E6
participant TREE as DS_TREE $3D0700
Note over SEL: identity, palette, tile bank stay $2E
SEL->>THUNK: dispatch this fighter
THUNK->>RYU: move.w #$12 into $52(a6)
Note over RYU: detector $0490C0<br/>animator $044290<br/>b0=$28 handler $0463C0
RYU->>COMP: emit a (group, sub) request
COMP->>COMP: $276-gated hook overrides tree base
COMP->>TREE: resolve request against HER tree
TREE-->>RYU: HER cells, HER 9520 tilesOne character's brain, another character's body, stitched at exactly two points.
The first probe on the donor build told the story in one number: b0=$28 fired 117 times where the previous build managed zero. All six normals composed native groups into her tree, records resolved and advanced, she recovered to idle, and there were zero crashes across the full run. The live pass confirmed it: normals come out, crouch is stable, hit recovery works. Here is the donor build running live:
Two long-standing user-facing bugs (a down-input that randomly triggered super jumps, and an infinite roll loop after eating a combo) vanished without a dedicated fix: they were both artifacts of the dead char-$00 recovery path, and Ryu's native recovery spine ($16FBA -> $1797A) simply doesn't have them.
Where Her Tiles Actually Live: The 64MB GFX Routing
The donor chassis only fixes behavior. Making every frame draw her art, 9,520 tiles that don't fit in stock MvC's 32MB sprite region, is a separate routing problem, and it's the reason the donor build is offline-only.
Stock MvC addresses sprites through a fighter's GFX-bank byte ($47), which the OBJ composer turns into bank bits on the sprite's Y-word. Her tiles live in the custom core's upper 32MB, reached only when Y bit12 is set. Her committed-slot identity carries the bank value that the patched bank table maps to that upper region; the composer override then resolves her requests against DS_TREE, whose record pointers index the upper-region tiles:
flowchart TB
A["Committed slot $2E<br/>GFX-bank byte $47"] --> B["patched bank table<br/>sets Y bit12"]
B --> C["upper 32MB region<br/>(custom 64MB core)"]
D["Composer $135E6<br/>$276-gated override"] --> E["DS_TREE $3D0700<br/>record pointers"]
E --> C
C --> F["her 9520 tiles<br/>rendered on OBJ"]
G["stock MvC tiles<br/>lower 32MB, bit12 = 0"] -.->|untouched| F
The tradeoff is baked into that diagram: the moment her art lives above 32MB, she needs the custom core, and the custom core is not the stock Fightcade core. This is an offline character by construction.
DS_MOVEMAP: 19 Rows Between Two Namespaces
The donor chassis raised an obvious question: when Ryu's handler emits an animation request, does it mean the same thing in her tree?
For normals, the answer turned out to be yes. Completely. Her tree uses Ryu's exact namespace (group = stance + punch/kick class, sub = strength x2). An earlier analysis claiming an 18-cell normals mismatch was a logging artifact; there is no normals translation, ever, and building one would have broken working attacks. This is worth stating plainly because I nearly built it.
The real mismatch is confined to the specials band (groups $2A/$2C/$2E), where the two characters' directories genuinely differ in size and order. Ryu emits up to 14 subs into a directory where she has only 4, which is not just wrong animation but a read-past-the-directory crash surface:
packet-beta
title Specials band directory overrun group 2A
0-7: "her 4 valid subs"
8-15: "Ryu emits subs 4 to 13 here"
16-23: "past directory into next records"DS_MOVEMAP at $23C820 is the bridge: a 19-row (group,sub) -> (group,sub) table plus 0x74 bytes of code, hooked at the composer $135E6 ahead of the tree-override thunk (the two hooks chain; the displaced instruction pair replays first with raw registers so the engine's compose-once guards still match). It translates registers only, gated on the fighter bounds check, the $276 identity flag, and group membership in the specials band. Highlights:
- F+MK -> her flower kick record (
$3D185C) - Ryu's fireball animation emissions -> her fireball entry chain (
$3D4B8C, all three strengths) - Air kicks -> her air records (j.MK
$3D1ECC, j.HK$3D1F70; the latter was previously a garbage pose read 8 bytes past her directory) - Overrun clamps close the crash surface:
(2A, sub >= $06)->(2A, 0)and(2E, sub >= $24)->(2E, 0)
Build-time tree-sanity asserts (her directory words vs. the native dump) stand guard against the table drifting from reality.
DS_SPECDRV: A Hand-Assembled Special-Move Driver
Translation makes the specials look right. It doesn't make them move right. Ryu's tatsu physics on her animation is still Ryu's tatsu. Authentic special moves need her phase machines, and those don't exist in this ROM either.
DS_SPECDRV at $23E000 is a 708-byte hand-assembled m68k driver, emitted by a standalone assembler written for it, its blob sha-asserted by the build and carrying zero absolute self-references so it stays relocatable. It mounts on the b0=$2A special-move handler entry at $47008:
- A
$276-gated dispatcher indexes a pc-relative table by the detector's move index, routing owned moves to owned handlers; everything else falls through to the stock path, byte-preserved. - Each handler is a phase machine driving pure MvC primitives: compose a record, set Y-velocity, gravity, and X-velocity directly, count ticks, advance phase, exit through the engine's own clean-recovery routine.
- Her records carry events as parameter words (
$FFxx-class latches read into$3C(a6)), the same mechanism the engine uses natively, which the driver and stock code consume for phase termination, projectile spawn, and voice triggers. Her DP hit-sparks are driven by per-record attack-ids$33through$38(per strength), Ryu-faithful.
The first shipped handler is her tatsu (Shunpuu Kyaku): launch Yvel -$62000, gravity $B000, per-strength kick, hold, and re-kick duration rows. The headless gate test measured an 80px rise against a 78 to 83px native capture spec, with a mid-move hit-yank resolving to clean hitstun and stock characters' tatsus byte-normal through the mounted dispatcher. The driver is live; the cells it composes are not yet her authentic tatsu cells. The v19 and v20 iterations were still chasing the real (2C,$20/$22/$24) band when the native pivot landed. See the catalog below.
The Wrong-Premise Catalog
Every port this deep accumulates confidently-held wrong beliefs. Yours will too, I promise. Here are the four that cost me the most, written down because the debugging shape (premise, symptom, disproof) is the part you can take with you. A fifth, listed last, outlived the donor era entirely: it wasn't a bug premise but a framing premise, and it was wrong for as long as this post existed.
| # | Premise | Reality | Symptom it caused |
|---|---|---|---|
| 1 | "The super-jump guard hook is inert under the donor" | Its hook point $27B10 is the shared super-jump phase machine, spine-reached for every character | She could not jump vertically at all; only dash-then-jump worked |
| 2 | "$EB is the super-jump flag" | Ryu's $E8/$EB callbacks are plain rts; that flag can never be set. The real flag is $BA(a6) | Her super-jump physics data sat parked and dormant |
| 3 | "The (1C,xx) band is her tatsu" | (1C,xx) is her hit-reaction/juggle band; real tatsu lives at (2C,$20/$22/$24) with per-strength physics | The tatsu driver composed her getting-hit poses, in a spin |
| 4 | "Walk cells are at tree+0x42/+0x46" | Those are sub[8]/sub[C], cells the engine never reads for walking. The composer's actual pointer math reads sub[4] (+0x3E fwd) and sub[6] (+0x40 back) | Forward walk played the guard-walk animation; back walk slid on an idle pose |
| 5 | "$2E is an unfinished slot Capcom left behind" | Every per-character table has exactly 23 entries, ids $00..$2C. $2E was never provisioned at all; it borrows $0E's row to resolve | Framed the whole project as finishing someone else's work, when it was really handing the engine an ID it was never built to hold, and made the past-the-end reads look like oversights instead of the expected consequence |
Premise 5 is the one I'd most want back. It's a comfortable story (the developers ran out of time, the slots sat there, someone eventually filled them), and the ROM never supported it. There is no 24th row in any table to fill. What there is, is a hard stop at $2C and two IDs that walk off the end of it, which is why nearly every crash in the native era turned out to be the same crash wearing a different fingerprint. That's a harder claim than the one it replaces, and it's the true one.
Premise 1 deserves the full story. The guard hook was written months ago to heal a crash: under the old char-$00 donor, a super-jump command entered a dispatch path with no handler and died, so the hook rewrote b0=$0A back to a neutral jump. Under the $12 donor that crash is impossible (Ryu has a real super-jump handler), but the hook was still installed, and a review had labeled it "inert." It was not. Every real-world vertical jump (up after crouching or blocking = down-then-up = a correctly parsed super-jump command) was being healed into a grounded, zero-velocity state that landed instantly. The physics probe showed clean-tap jumps launching perfectly while the user's jumps died, which localized it in one session. The fix was deletion.
The lesson generalized: every guard written for the old donor is a suspect until breakpoint-proven live or dead under the new one. "Inert" claims need a breakpoint hit count per site, not an argument.
v19: A One-Word Fix and an 81-Tile Hunt
Once premise 2 fell (the super-jump flag is $BA(a6), not $EB), the fix was a single word in the jump thunk: test the byte that actually gets set. That's the whole patch. Deployed as v19.
The headless probe on v19 shows the full super-jump pipeline alive on her own records:
- Down-up input fires the state chain
b0$0A->$0E->$1A(prep -> launch -> airborne) $BAreads 1 at launch; launch Y-velocity is -17.0 px/frame- Measured rise: 368px, genuine super-jump height, on her records
- The launcher (c.HP) super-jump-cancels on hit, which unlocks her air combos, the launcher-to-air-chain flow that her design docs specify and that had been dead end-to-end
v19 was deployed and every number above comes from the headless probe. It never got a hands-on pass on the donor lineage; the native pivot arrived first. Under this project's rules that means it was promising, not "working." The probe cannot feel input timing under real play, and premise 1 above is a standing reminder that jump bugs specifically have survived probes that only tested clean scripted taps.
v19 also closed a data gap: her crouching HK had been rendering with holes because 81 tiles were missing from the imported set. The hunt traced the gap to branches of her tree that were unreachable under the old donor (nothing ever composed crouch-HK, so nothing ever exposed the missing art) and became reachable the moment Ryu's machine started emitting the full move set. The tiles were extracted, decoded, and merged: the verified inventory went from 9,439 to 9,520 tiles.

Verification Discipline
How did I keep myself honest through all of this? Five rules, and everything above depends on them:
- Headless probes first. FBNeo Lua harnesses navigate the secret code, script inputs, and log state bytes, record pointers, and physics per frame. Probes answer "does the mechanism fire" with numbers.
- A human as the live oracle. Probes cannot see "looks like her move" or feel input reliability. Nothing is called working until a hands-on pass says so, and this post marks exactly which claims have had one.
- Capstone branch-asserts on every thunk. Each hand-assembled patch is disassembled at build time and its branch targets verified before it ships. Hook sites carry expected-bytes asserts so an engine byte that moved fails the build instead of corrupting a fight.
- The base ROM is never modified. Every build composes its patches onto a pristine source and writes a separate output ROM set; each patch is expect-asserted and reversible by rebuild.
- The trade-off stays stated: her tiles live above 32MB, so she requires the custom 64MB core, which breaks Fightcade netplay. This is an offline character.
Implementation Status (donor lineage, as it stood at the pivot)
| Component | Status | Evidence |
|---|---|---|
| Data port (tree, framedata, tiles) | Complete | 759/759 mode-2 blocks byte-identical; 9,520 tiles verified in upper GFX |
| Donor chassis (force-remap + composer hook) | Complete, live-verified | b0=$28 0 -> 117; six normals on her records; user pass confirmed |
| DS_MOVEMAP (19 rows) | Complete, live-verified | F+MK flower kick, fireball chain, air kicks; crash surface clamped |
| DS_SPECDRV driver + mount | Live | Mount transparent for stock characters; tatsu arc 80px vs 78 to 83px spec |
| Authentic tatsu cells | In progress at pivot | Driver composed provisional cells; real band is (2C,$20/$22/$24) |
v19 super jump ($BA flag) | Probe-verified, never live-confirmed | $0A->$0E->$1A, 368px rise, launcher SJ-cancel |
| Shououken (running DP) run-phase | Planned, never shipped | Phase spec drafted; native capture in hand |
| Haru Ichiban (QCB+KK super) | Planned, never shipped | Phase table captured from native game |
| Midare Zakura (QCF+PP super) | Planned, never shipped | Needed its own native capture |
| Six-button "Hunter" chain system | Planned, never shipped | Target = her chain-permission row in the move-id matrix family |
Why the Donor Chassis Was Ultimately Retired
The donor chassis is a genuinely good architecture: it converted "she can't attack" into a fully playable fighter in two seams, and it kept its promises about normals. But it carries one structural ceiling that no amount of driver work removes. The engine's hit-test and property lookups re-derive attack data from $52-keyed tables, and under the donor, $52 says "Ryu" on every frame. Her real frame properties can be composed onto the screen but never fully authoritative: as long as the engine believes she is Ryu, some of her identity is negotiated through his tables.


(Those two shots are from the native era, captured as the "before" state of the cosmetics work. The condition they show — her art in slot $2E still wearing the retired occupant's name — is exactly the donor-era condition, but they are not donor-build captures, and I would rather say so than let a picture imply a provenance it doesn't have.)
That ceiling is exactly what the native port set out to remove: keep $52 = $2E end to end and pay whatever that costs. It cost four distinct root causes and an emulator-core instrument to find them, but it produced a Dark Sakura who is herself to the engine, name and palette and dispatch entries included. The native lineage has since fully superseded this donor build and now carries a second native character (Shuma-Gorath) as well.
The donor chassis remains the honest answer to the question in the title. Yes, you can run one fighter's brain in another fighter's body, and it will fight. It just won't ever quite be itself.
How to Validate (donor build)
If you want to check any of this yourself, there are three gates, in order.
1. Build. The build chain composes every remap thunk, the composer hook, the translation table and the driver blob onto a pristine base and writes a separate output ROM set. It fails loudly if any hook site's expected bytes don't match, or if any thunk's capstone branch-assert disagrees with where the branch actually lands. That's the point.
2. Headless probes. Three FBNeo Lua harnesses script the secret-code navigation and the inputs, then log state per frame:
| Probe | Question it answers |
|---|---|
| Attack-commit probe | Does b0 reach $28? Do the six normals compose records inside DS_TREE ($3D0xxx)? |
| Move-map verifier | Does the specials band translate? F+MK → $3D185C, QCF+P → $3D4B8C |
| Jump-physics probe | Does down-up walk b0 through $0A/$0E/$1A, set $BA=1, and rise the expected pixels? |
3. Live pass. Select her via the $2E secret code and play. The probes prove mechanisms; only hands prove the character.
References
- Marvel vs Capcom Had No Room for a 24th Character. I Added Two.: the native port that superseded this, keeping
$52 = $2Eand paying the cost - Binary Forensics: Recovering an Unreleased Character: the
$2Eslot discovery this work inherits - State-Machine Injection: the dispatch-layer techniques the remap thunks build on
- CPS-2 Maximum ROM Expansion: the 64MB core whose upper GFX region holds her 9,520 tiles
- Analysis note: the dead-recognizer diagnosis and the chassis adjudication that followed from it
- Analysis note: period-accurate movelist and chain-system spec, reconstructed from 1998 arcade FAQs
- The standalone DS_SPECDRV assembler and the sha-asserted blob it emits
- Detector, animator, and handler dispatch tables:
$18712,$18DF0,$E6B8(all keyed by in-fight char-id$52) - Composer at
$135E6; DS_TREE at$3D0700; DS_MOVEMAP at$23C820; DS_SPECDRV at$23E000 - 68000 Programmer's Reference Manual (Motorola)