If you look through a game’s routines long enough you’re bound to find something wrong. Maybe a register was clobbered. Maybe something was written twice when it didn’t need to be. Maybe something goes horribly wrong and just happens to work out, disaster narrowly avoided. It’s likely that your average player (and/or your average hacker) might never see these goofs, so I figured it’s time to have a thread for them.
Post whatever bugs, compiler goofs, etc. here. If anything this thread will keep me from pasting random snippets of ASM into the discord server, and hopefully this thread’ll be a good read.
Let’s get into it. I like to play around with FE5, so these examples will be 65816 assembly. I’d like to explain each of these so that someone with little to no ASM experience could hopefully follow along.
#Block Transfer To/From Anywhere
65816 has two block memory transfer opcodes, MVP and MVN, which are similar to THUMB’s ldmia+stmia for transferring data. The opcodes include the bank (the upper 8 bits of a pointer on the 65816) for both the source and the destination, with the number of bytes to transfer and the lower 16 bits of the source and destination in CPU registers. This poses a bit of a problem, as you can only copy data to/from locations known at compile time (you need to know the banks to write the opcode). To overcome this, FE5 has routines that build a block transfer routine in RAM. When you need to copy data, you fill out the banks of the opcode in RAM and hop to the routine. It’s quite clever in my opinion.
The first byte of the MVN/MVP opcodes are written to RAM on startup, along with another opcode to return from the routine:
Routine (written for the 64tass assembler)
blockcopy_copier phb php phk plb sep #$20 ldx #size(mvn_routine) - 1 - lda mvn_routine,x sta $04AE,x dex bpl - ldx #size(mvp_routine) - lda mvp_routine,x sta $04B2,x dex bpl - plp plb rts mvn_routine mvn #$00,#$00 rts mvp_routine mvp #$00,#$00 rts unknown_routine phb
If you don’t know 65816, this might be mumbo jumbo to you, so let’s break it down. We’re copying two routines, mvn_routine and mvp_routine, to RAM addresses $0004AE and $0004B2 respectively. We copy them end first, using a loop counter in the X register. We need this counter to be one byte less than the size of the routine because we’re looping with a BPL opcode (0 is considered positive). After each byte, we decrement the loop counter.
MVN Transfer breakdown
X Byte Part 0003 60 rts 0002 00 destination bank 0001 00 source bank 0000 54 mvn FFFF end loop
Here’s the issue: when copying the MVP routine, the size wasn’t reduced by one, so the first byte of the next routine (a phb opcode) gets copied into RAM at $0004B6, overwriting whatever was there accidentally. Man, that’s a huge explanation for such a tiny thing, right? So, what was originally at $0004B6? $0004B6 is used exactly once when setting up the sound system, and probably didn’t even need to be used. Lucky us, nothing of value was lost. Even better, the only known routines that use this block memory copier look like this:
MVN Routine user (64tass syntax)
phb php ; program bank -> data bank phk plb phx phy ; get the source, dest banks and ; build the mvn opcode sep #$20 lda $04AB ; dest bank sta $04AF lda $04A8 ; source bank sta $04B0 lda #$54 ; mvn opcode sta $04AE lda #$60 ; rts opcode sta $04B1 rep #$20 ; get params ldx $04A6 ; source ldy $04A9 ; dest lda $04AC ; size dec a ; cool trick, can rts ; because $0000-$2000 ; of RAM mirrored to ; every bank jsr $04AE ply plx plp plb rtl
These rewrite the entire MVN/MVP routines anyway! $0004B6 was clobbered needlessly!
There’s some interesting other things to consider: The way the routine user loads the parts of the routine as literals and writes them to fixed points in RAM is faster than the startup routine. The startup routine would probably be faster if it actually used MVN/MVP to copy the MVN/MVP routines. And, finally, none of these seem to be called.
Same thing happens in FE4 in the same place, too.