Subpar Mario Bros.
Introduction

My favorite thing about the NES is how little RAM it has, only 2kB. That's 2048 bytes! You can fit that many numbers on a screen at the same time! And, because all the code is stored safely in ROM, it turns out that you can randomly corrupt those RAM values and continue playing the game, at least for a little while until something breaks.

Many years ago I modified an NES emulator to do exactly this. Although most games don't corrupt in a way that is fun to play, it turns out that Super Mario Bros., the quintessential NES game, is pretty funny when it glitches. I think it's partially because the game is so iconic, especially the early levels. It's very obvious when a goomba starts throwing hammers, or when the music is suddenly five times faster, or when Mario starts swimming through the air in World 1-1.


I wanted to make a physical version of this, something I could exhibit at shows, but the idea of stuffing a Raspberry Pi into a broken NES wasn't very exciting. If only there was some way to do this with a real NES! The hardware version of this was beyond me, but what if I could do it in software? Could Super Mario Bros. be made to corrupt itself as you play?


Plan of attack

In the years after the NES was released in 1985, game cartridges got much more sophisticated; apparently the biggest NES game released in the USA was Kirby's Adventure, which had 512KB of code/data and 256KB of sprites. Super Mario Bros. was released much earlier than that, and uses the default cartridge layout: 32KB for code/data and 8KB for sprites. (Famously, the clouds and bushes in the game use the same sprite.) Unfortunately for us, this means that there isn't any room for us to put our code, as they've already used it all!

I spent some time reading through a disassembled version of the game's source code searching for anything that could be jettisoned, and ended up settling on a small demo routine that makes the game play itself if you don't push any buttons on the title screen:

DemoActionData:
              .db $01, $80, $02, $81, $41, $80, $01
              .db $42, $c2, $02, $80, $41, $c1, $41, $c1
              .db $01, $c1, $01, $02, $80, $00

DemoTimingData:
              .db $9b, $10, $18, $05, $2c, $20, $24
              .db $15, $5a, $10, $20, $28, $30, $20, $10
              .db $80, $20, $30, $30, $01, $ff, $00

DemoEngine:   ldx DemoAction            ; load current demo action
              lda DemoActionTimer       ; load current action timer
              bne DoAction              ; if timer still counting down, skip
              inx
              inc DemoAction            ; if expired, increment action, X, and
              sec                       ; set carry by default for demo over
              lda DemoTimingData-1,x    ; get next timer
              sta DemoActionTimer       ; store as current timer
              beq DemoOver              ; if timer already at zero, skip
DoAction:     lda DemoActionData-1,x    ; get and perform action (current or next)
              sta SavedJoypad1Bits
              dec DemoActionTimer       ; decrement action timer
              clc                       ; clear carry if demo still going
DemoOver:     rts

I then replaced a jump into the main game loop ( GameEngine ) with a jump to my patch code, which runs the game loop and then the new corruption routine.


Corruption routine

I hope you're comfortable reading 6502 assembly...

; Make sure that OperMode_Task == 3, which indicates the game is running
define OperMode_Task $0772
lda OperMode_Task
cmp #$03
bcc Exit

; Call the GameEngine subroutine
define GameEngine $AEFE
jsr GameEngine

; Only corrupt memory once per N frames
define Corrupt_Timer $07F0
dec Corrupt_Timer
bne Exit

; As we read from the RNG we will save it into RAM that is not cleared on a warm boot
; so that you can hit the reset switch without resetting the glitch sequence
define Random_Backup_0 $07F1
define Random_Backup_1 $07F2

; Make room on the zero-page so that we can do an indirect memory write later
; This code boldly assumes that the game will never use these locations, and that
; they are initialized to zero (either of which might not actually be true)
define Corrupt_AddrLow $40
define Corrupt_AddrHigh $41

; Generate the low address byte (0 to 255) in Y
define Random_0 $07A7
ldy Random_0
sty Random_Backup_0

; Generate the high address byte (0 to 7)
define Random_1 $07A8
lda Random_1
sta Random_Backup_1
and #$07
sta Corrupt_AddrHigh

; Avoid writing to the stack, which is almost guaranteed to do nothing but crash
cmp #$01
beq TryAgainSoon

; Avoid corrupting some memory locations that will likely crash the game, or even damage the NES
; (http://wiki.nesdev.com/w/index.php/PPU_registers#Master.2Fslave_mode_and_the_EXT_pins)
cmp #$07
bne AddrValid
cpy #$70 ; OperMode = $0770
beq SwimInstead
cpy #$72 ; OperMode_Task = $0772
beq SwimInstead
cpy #$78 ; Mirror_PPU_CTRL_REG1 = $0778
beq SwimInstead
cpy #$79 ; Mirror_PPU_CTRL_REG2 = $0779
beq SwimInstead
bne AddrValid

; Instead of corrupting the values above, turn on swimming ($0704)
SwimInstead:
ldy #$04
AddrValid:

; Actually corrupt the target memory
define Random_2 $07A9
lda Random_2
sta (Corrupt_AddrLow),Y

; Reset the corruption delay timer
lda #29
sta Corrupt_Timer

; If we picked a bad address the timer will still be at $00, so we can reset it to $01
; to force the corruption routine to run again next frame and not introduce a long delay
TryAgainSoon:
inc Corrupt_Timer

Exit: 
rts

What is any of that?

The main purpose of the corruption routine is to corrupt memory, duh.

Every 30 frames it will pick a random memory address between 0x0000 and 0x07FF (i.e. all 2048 bytes of RAM) and write a random byte to it. Some memory addresses are off-limits, like the stack page (0x0100-0x01FF) and high-level state machine (OperMode and OperMode_Task), which are guaranteed to crash the game. It also blocks writes to the PPU CTRL registers (Mirror_PPU_CTRL_REG1/2), which could (supposedly) damage a real NES! For some of these, it instead redirects to memory address 0x0704, which turns on swimming.

You might be wondering where those random numbers are coming from. In order to... do things (?)... Super Mario Bros. has its own random number generator (RNG), which stores the current frame's random data starting at 0x07A7. The corruption routine uses some of those values to generate the address to be written to and the data to be written.


Who randomizes the randomizer?

Every time that Super Mario Bros. starts, it initializes the memory of the NES, which includes initializing the RNG to a specific seed. The game's random values change each frame, but they'll be the same sequence every time you play. Normally this isn't a big deal, but these random values are used to generate our glitches, which means you'll get the same glitches every time you start the game. This is definitely not what we want!

To fix this I modified the game's initialization code to persist some of the RNG's state when the NES reset button is pressed, which resets the CPU but does not clear the RAM. The code for this is below, although I couldn't really explain it to you. It's a lot like the original initialization code, except that some memory locations end up with different, better values.

define WarmBootValidation $07FF
define WarmBootOffset $D6
define ColdBootOffset $FE
define InitializeMemory $90CC
define SND_DELTA_REG_1 $4011
define OperMode $0770
define Random_0 $07A7
define Random_1 $07A8
define Random_Backup_0 $07F1
define Random_Backup_1 $07F2

ldy #ColdBootOffset
lda WarmBootValidation
cmp #$a5
bne ColdBoot
ldy #WarmBootOffset
ColdBoot:
jsr InitializeMemory
sta SND_DELTA_REG_1
sta OperMode
lda #$a5
sta WarmBootValidation
lda Random_Backup_0
ora #$01
sta Random_0
lda Random_Backup_1
sta Random_1

Can I play it?

Uhhh yeah, sure.

D-Pad: W/A/S/D   Buttons: J/K   Start: U   |   Emulation by JSNES