Not just code, but data too

Originally the project included a lot of data structures dumped directly from the ROMs, such as sprite animation, palettes, hitboxes, AI bytecode and more. The only data not ported were the actual tiles; the game depended on four tile ROMs from the original game to be present.

Since all this data from the code ROMs is a considerable part of the original code, I decided to pull it all out and require the entire ROM set to be present, basically as much as you’d need to run the game in Mame.

This has its own difficulties, for a start we need to do endian-swapping each time we read anything larger than a byte. I wondered how much this would impact performance, but it turns out not very much. Some CPUs even have special instructions just to do this.

It also has some benefits. Getting the data out of the ROMs and into C-style structs and byte arrays was error prone, and the utility I wrote to do this sometimes messed up and produced incomplete / corrupt data. Many arrays are jagged. Sometimes sentinel elements are used. Sometimes I had to guess lengths of arrays and got it wrong. Going to the ROMs for all this ensures nothing is missing.

I wrote a compatibility layer to take care of locating these structures and doing the necessary byte-swapping.

As an example, here’s how an 2D jagged array of words would be stored.

00001ff0 lea 0x2000(%pc), %a6    ;get address of row table
00001ff4 move.w (%pc, %d0), %d0  ;get offset of row pointed by %d0
00001ff8 lea (%d0, %a6), %a6     ;add offset to base address
00001ffc jmp some_routine_that_uses_this_data

00002000 dc.w 0x0008   ;first row starts at 0x2000 + 0x8 = 0x2008
00002002 dc.w 0x0018   ;next row starts at 0x2000 + 0x18 = 0x2018
00002004 dc.w 0x001a   ;and so on
00002006 dc.w 0x0020

00002008 dc.w 0x1234   ;first element of first row
0000200a dc.w 0x5678   ;second element of first row
....
00002016 dc.w 0x8888   ;last element of first row
00002018 dc.w 0x1235   ;first (and only) element of second row
....
0000201a dc.w 0x1236   ;first element of third row

I guess they did it this way to save a little space at the cost of a couple of extra instructions, as opposed to just using 32-bit pointers, although there was plenty of free space in the ROMs.

Switch-style statements are very similar (and very frequently used throughout the code). Case variables are often kept to even numbers to avoid having to do pointer arithmetic. An example from the state-machine:

000077dc  302d 000c   move.w (game_mode_3, %a5), %d0
000077e0  323b 0006   move.w (#6, %pc, %d0.w), %d1
000077e4  4efb 1002   jmp    (#2, %pc, %d1.w)
;the jump-offset table follows
000077e8  000a        dc.w 0x000a ;jump to 0x77f2
000077ea  0016        dc.w 0x0016 ;jump to 0x77fe
;... several more ...
; here is case 0:
000077f2  546d 000c   addq.w #2, (game_mode_3, %a5) ;next time do the next case
000077f6  4eb8 2794   jsr 0x2794   ; reset display state
000077fa  4ef8 2c8a   jmp 0x2c8a   ; initialize both players (tail call)

000077fe  546d 000c   addq.w #2, (game_mode_3, %a5) ;again, next time to the next case
00007802  4eb8 1698   jsr 0x1698   ; set up palettes
00007806  4ef8 1ae2   jmp 0x1ae2   ; set up scrolls (tail call again)

This is more or less equivalent to

switch (g_game_mode_3) {
    case 0:
        g_game_mode_3 += 2;
        reset_display_state();
        init_players();
        break;
    case 2:
        g_game_mode_3 += 2;
        set_up_palettes();
        set_up_scrolls();
        break;
    /* more cases */
}

If the case is odd, the machine will crash with a bus error exception. This is why 2 is added to g_game_mode_3. If the case is greater than the number of elements in the jump-offset table, the behaviour is undefined. I’ve put default clauses in all my switch statements to land in the debugger to let me know something’s up.

Advertisements
Not just code, but data too