Mega Man Anniversary Collection is a Mega Man collection for PS2, Gamecube, and Xbox that was released in 2004. I guess this means that it was meant to celebrate the 17th anniversary of Mega Man which seems a bit weird but whatever. It includes ports of Mega Mans 1-8, plus the two arcade Mega Man fighting games.
The ports of the first 6 games descend from the Rockman Complete Works series on Playstation. A lot of Mega Man fans consider the Complete Works/Anniversary Collection ports to be the best way to play the NES Mega Man games due to the reduced slowdown, lack of sprite flicker, native save functionality, and quality-of-life fixes like being able to switch weapons with the shoulder buttons. It makes the games feel more modern, as opposed to something like Legacy Collection or Virtual Console where they feel like NES games running in an emulator.
I had always heard that these were native ports, and I was interested in how Capcom ported the games, so I decided to look into how they work. I used the PS2 version of Anniversary Collection, as a large number of PS2 games have debug symbols included in the retail build. Fortunately, Anniversary Collection was no exception. As it turns out, the Complete Works/Anniversary Collection ports aren't done from scratch, but are instead statically recompiled from the original NES games' assembly source code. Because the PS2 version of the game has debug symbols, this means that you basically get the label and variable names from all of the NES Mega Man games. This may be useful for anyone who's looking at the code for the original NES games. For example, here are the RAM variable names for Mega Man 2.
I figured it might be interesting if I wrote a bit about how the static recompilation is implemented. As you'd expect, the CPU registers and flags are variables in memory, and the code uses a combination of C macros and functions to implement the 6502 instruction set. The exact way the processor state is stored is a bit strange. Everything depends on a function called SPAD_ADDRESS, which looks something like this:
extern uint32_t *SPAD_ADDR;
uint32_t *SPAD_ADDRESS(uint32_t slot) {
return SPAD_ADDR + slot;
}
All the processor state is then accessed through calls to SPAD_ADDRESS. I imagine they had macros like this for readability:
#define CPU_BYTE(slot, byte) (((uint8_t *)SPAD_ADDRESS(slot))[byte])
#define CPU_WORD(slot) (*(uint16_t *)SPAD_ADDRESS(slot))
#define CPU_PC (*SPAD_ADDRESS(0x8a))
#define CPU_A CPU_BYTE(0xfd, 0)
#define CPU_X CPU_BYTE(0xfd, 2)
#define CPU_Y CPU_BYTE(0xfd, 3)
#define CPU_SP CPU_BYTE(0xfe, 0)
#define CPU_C CPU_WORD(0xf6)
#define CPU_Z CPU_WORD(0xf7)
#define CPU_N CPU_WORD(0xfb)
In practice, this means that a significant portion of the recompiled game code is just calls to SPAD_ADDRESS, which is both inefficient and makes the decompiler output from a tool like Ghidra difficult to read. I have no idea why they implemented it like this. Maybe they just assumed that the function would be inlined? It doesn't really matter since the game doesn't have any slowdown.
RAM/ROM access for data uses the same memory addresses as the original game, and goes through helper functions that account for ROM bankswitching and simulate some of the NES hardware registers. The memory map is a bit different, as everything between $2000-$5FFF that's not a hardware register is now RAM, as opposed to mirrors of the hardware registers. This is used to allow for more onscreen sprites, which helps reduce flicker in some cases. There's now 200 sprites instead of 64, and the OAM RAM buffer is moved from $200 to $3000 to make room for the additional OAM entries.
The recompiled game code is made re-entrant because it needs to be able to keep its place while the game runtime code executes. For example, when the recompiled code is done processing a frame of gameplay logic, the game runtime code needs to draw the frame's graphics, play any music/sound effects, read the controllers, etc. The recompiled code then needs to pick up where it left off and start running the code for the next frame. All of this is handled by simulating the program counter and stack. This happens inline, so I imagine there were macros like this to handle things:
static inline void push8(uint8_t value) {
uint8_t old_sp = CPU_SP;
CPU_SP = (uint8_t)(old_sp - 1);
fcl_STACK_area[old_sp] = value;
}
static inline uint8_t pop8(void) {
CPU_SP = (uint8_t)(CPU_SP + 1);
return fcl_STACK_area[CPU_SP];
}
#define JSR(target, resume) do { \
push8((uint8_t)((resume) >> 8)); \
push8((uint8_t)(resume)); \
CPU_PC = (target); \
return; \
} while (0)
#define JMP(target) do { CPU_PC = (target); return; } while (0)
#define RTS() do { \
CPU_PC = pop8(); \
CPU_PC |= (uint32_t)pop8() << 8; \
return; \
} while (0)
Then assembly code like this:
G1_EMYR0E:
LDA CHRWK0,x
BEQ check_state
DEC CHRWK0,x
BNE hold_animation
check_state:
LDA CHRSTT,x
AND #$0F
BNE run_begin
JSR DIRSET
JSR DIRCHR
...
gets turned into C code like this:
void G1_EMYR0E(void)
{
switch (CPU_PC) {
case 0x427:
case 0x428:
LDA_iaa(CHRWK0, CPU_X);
if (CPU_Z) goto check_state;
DEC_iaa(CHRWK0, CPU_X);
if (!CPU_Z) goto hold_animation;
/* fall through */
case 0x429:
check_state:
LDA_iaa(CHRSTT_MM3, CPU_X);
AND_ia(0x0f);
if (!CPU_Z) goto run_begin;
JSR(0xc8e, 0x431); /* DIRSET */
case 0x431:
JSR(0xc8f, 0x432); /* DIRCHR */
case 0x432:
...
As you can see, a switch statement is added, and new "program counter" values are generated for each label and each instruction after a subroutine call. Then the code to run a frame's worth of recompiled code looks something like this:
void Rockman3Main(void) {
RCK3_task_out = 0;
// ...
while (!RCK3_task_out) {
// various stuff to set flags based on program counter goes here
uint32_t curr_pc = CPU_PC;
if (curr_pc < MAX_PCS) {
switch (curr_pc) {
// called when the recompiled nmi routine is finished
case 0x000:
RTS();
RCK3_task_out = 1;
break;
// recompiled code is modified to call this when it waits for vblank
case 0x001:
if (fcl_VSyncProc() != 0) {
// if vblank is enabled, set pc to recompiled vblank code
MM3_CPU_PC = 0xaf6;
INTNMI();
} else {
// if vblank isn't enabled, just end the frame and yield
MM3_CPU_PC = 0;
PLP();
}
// ...
case 0x427: case 0x428: case 0x429: case 0x42a: case 0x42b: case 0x42c:
case 0x42d: case 0x42e: case 0x42f: case 0x430: case 0x431: case 0x432:
case 0x433: case 0x434: case 0x435: case 0x436: case 0x437:
G1_EMYR0E();
break;
// ...
}
}
// various stuff after game has yielded here
}
Whenever a subroutine is called, returned from, or a label is jumped to, the processor will return from the recompiled code, find the new code to jump to in the dispatcher loop, and resume execution from there. This also means that it's possible to "call" the middle of a C function, and there is code that does this. I'm not sure how the C function boundaries were determined. Maybe they were manually defined, or maybe they were generated from the symbols that each source file exported, or something else. Some of the translated code also uses regular C function calls and returns instead of the simulated stack. I can't come up with a good rule for why this is either. Maybe someone manually went through the code looking for frequently called subroutines?
Mega Man 3-6 use the MMC3 mapper, which allows for interrupting the CPU at a given scanline (this is typically used for mid-frame graphical effects). The game runtime supports this functionality. When the PPU emulation code goes to draw each scanline, it will check to see if an IRQ should fire. If it does, it will run the recompiled IRQ handler code prior to drawing the scanline.
The idea of statically recompiling a game to make porting easier is compelling, and it's interesting to see an example of how this was used in a commercial game. However, Anniversary Collection's execution leaves a lot to be desired. The whole point of static recompilation is that it lets you get away from needing a jump table or whatever that goes to a function call for each instruction. Here, every time you access anything related to processor state, it's a function call, then the actual instruction implementation is a function call too a lot of the time. I haven't done any formal benchmarking, but combined with how the code is made re-entrant by using a virtual PC and SP instead of stack switching, it seems like Anniversary Collection's approach would be worse than a decently optimized 6502 interpreter.
Even though the actual static recompilation is most likely pointless from a functional perspective, it's still pretty neat that the NES games' function and variable names are viewable as a result of it. If anybody is looking at the game code from the NES Mega Man games and would like to see the symbols, I had Chatgpt Codex write a Python script to extract and format a .elf file for analysis. You just put the script in the same directory as the files from the PS2 version of Anniversary Collection, and then run it with Python. The .elf file it outputs can be opened with something like Ghidra, but you'll need the Emotion Engine plugin because the PS2 uses a nonstandard processor.