If you’re at all familiar with Super Mario Bros. speedrunning, you’ve probably heard the term “frame rule”. Super Mario Bros. updates the game logic and redraws the screen 60 times per second. These redraws are called “frames”. Every 21 frames, the game checks to see if you’ve completed a level,and if you have, it starts loading the next level. For speedrunning, that means that as long as you complete a level anytime within a 21 frame window, you won’t gain or lose any time, so you can afford to take things a little slower and more carefully as long as you still make that window. But why did the programmers do this, and why is it 21 frames? Let’s take a look.
6502 crash course
The NES has a MOS Technology 65021 CPU, which is an 8-bit CPU. That means the largest value it can store at once is 2552. It has 3 general purpose registers: A, X, and Y. Registers are fast, temporary memory locations on the CPU that you need to use for arithmetic. For example, if you wanted to add 5 to a user’s life count, you would first load the value from RAM into A, add 5 to the A register, then write it back:
LDA lives ; Load into A the value in the "lives" RAM address CLC ; Clear the carry flag, in case a previous addition had a carry ADC #5 ; Add with carry (which we just cleared) the value 5 to A STA lives ; Store A back into the "lives" RAM address
Each CPU has special rules about how registers and instructions interact. For example, the 6502 always stores the result of an addition or subtraction in A, and loading values from a list can only be done into A and must use X or Y as the offset into the list.
Super Mario Bros. disassembly
Thanks to the excellent work of doppelganger and others, we have a full disassembly of the Super Mario Bros. game. They reversed the ROM, figured out what each memory address is used for, what each code address is used for, and added descriptive labels and comments. This isn’t the original source code’s labels (which Nintendo has never released), but it still compiles identically to the original ROM, and its logic should match the original source code’s. You can find the Super Mario Bros. disassembly on GitHub.
When Super Mario Bros. wants to do something for a certain amount of time, it sets a timer. There is a chunk of memory that is reserved for timers. At the end of each frame, the game runs some code that subtracts 1 from each timer until they reach 0. If a part of the code wants to do something for 30 frames, it just needs to write the value 30 into one of these memory locations, and then keep checking it and wait until it reaches 0.
Some of the timers are listed on line 80. For example, the AirBubbleTimer controls how long the game waits until it spawns a new bubble when underwater. That bubble spawning code is on line 6384, under the
BubbleCheck: --- cut code --- lda AirBubbleTimer ;if air bubble timer not expired, bne ExitBubl ;branch to leave, otherwise create new air bubble
The game loads the bubble timer into the A register with
LDA. Then if it’s not equal to zero (that is, the timer hasn’t expired), then the code branches to exit the subroutine, using
BNE. But if it is zero, it continues and spawns another bubble.
SetupBubble: --- cut code --- ldy $07 ;get pseudorandom bit, use as offset lda BubbleTimerData,y ;get data for air bubble timer sta AirBubbleTimer ;set air bubble timer
After the bubble is spawned, the game loads a random number, either 0 or 1, into the Y register with
LDY. Then it uses that to look up a value from the list named
LDA. That list has 2 values in it: 64 and 32. Then it stores that value back into the
STA. Thus, a new bubble will appear in either 64 or 32 frames.
The code that adjusts the timers is on line 796, under the
DecTimersLoop: lda Timers,x ;check current timer beq SkipExpTimer ;if current timer expired, branch to skip, dec Timers,x ;otherwise decrement the current timer SkipExpTimer: dex ;move onto next timer bpl DecTimersLoop ;do this until all timers are dealt with
First, the game loads a timer from the list offset by the X register with
LDA. If that timer’s value is 0, it skips decreasing it with
BEQ. Otherwise, it decreases that timer by 1 with
DEC. Then it goes to the next timer by decreasing X with
DEX. If X is still positive (i.e. >= 0) there are more timers
to process, so it will repeat the loop with
Because the NES has an 8-bit CPU, the largest value it can hold is 255. Because the game runs at 60 frames per second, this means the longest a timer can last is 255 / 60 = 4 1/4 seconds. But what if you want a timer to last longer than that?
Nintendo’s solution is to have a second set of timers that only decrease occasionally. They set a second timer control named
IntervalTimerControl and decrease it by 1 every frame. When that value hits -1, the game will decrease that second set of timers. We can see this right above the
DecTimers: ldx #20 ;load end offset for end of frame timers dec IntervalTimerControl ;decrease interval timer control, bpl DecTimersLoop ;if not expired, only frame timers will decrease lda #20 ; sta IntervalTimerControl ;if control for interval timers expired ldx #35 ;interval timers will decrease too DecTimersLoop:
Here, the game starts by loading 20 into X with
LDX. Later, that X value will be used to control how many timers from the list are decreased. Then, it decreases
DEC. If it’s still positive, then it branches to the
DecTimersLoop which I discussed above with
BPL. However, if it’s negative, it resets
IntervalTimerControl back to 20 with
STA , and then it loads 35 into X with
LDX so that more timers will be decreased.
Now imagine what happens if the game sets a long timer when
IntervalTimerControl is set to 0. At the end of that frame, the timer will be immediately decreased instead of waiting for the full 20 frames. This is where the frame rule comes from: depending on what
IntervalTimerControl‘s value is when one of these longer timers is set, the timer may run for up to 20 fewer frames. Additionally, this frame rule will apply to any long timer. The full
list of long timers is here:
ScrollIntervalTimer = $0795 EnemyIntervalTimer = $0796 BrickCoinTimer = $079d InjuryTimer = $079e StarInvincibleTimer = $079f ScreenTimer = $07a0 WorldEndTimer = $07a1 DemoTimer = $07a2
Of note here are
StarInvincibleTimer, indicating how long Mario is invincible for after receiving damage or collecting a star. The length of Mario’s invincibility after getting hit or collecting a star are also framerule dependent.
Beginning of next level
The final piece of the puzzle is the
ScreenTimer. Between levels, the game displays some game information, including the current level and the number of lives the player has. This screen uses a long timer,
ScreenTimer, to stay on the screen for a few seconds, before continuing. And this is where the frame rule behavior comes from. The amount of time the intermediate screen is displayed depends on which frame rule it first appears in.
The routine that draws the status information is on line 1540, under the
DisplayIntermediate label. That routine calls
ResetScreenTimer on line 1784, which sets the
ScreenTimer to 7.
One oddity is that it sets the long timer to 7, meaning it will run at most 6 x 21 = 126 frames, or about 2 seconds. Because it’s less than 255, this could have been done using a regular timer, and the game would not have any frame rules affecting speedruns. Maybe the programmer knew the timer had to run for over 20 frames, and decided to just use the long timer. Maybe the regular timers were all in use, so the programmer decided to just use a long one. This is plausible, because even though the final released game doesn’t use all of the regular timers, there are gaps in the timer memory, which suggests that some timers were removed at some point in development.
Could the interval timer causing frame rules be considered a bug? I don’t think so. Oftentimes in programming, you’ll be given a requirement, and while you could spend hours writing a perfect solution, there’s an opportunity cost where time could be better spent working elsewhere. This is especially true in video games, where any number of things can always be tweaked and improved. The programmer had a requirement: make some timers for things like star
invincibility last longer than 2 1/4 seconds, and the implementation they wrote works. No player at the time would have noticed the difference, and it doesn’t affect casual gameplay anyway.
Super Mario Bros. in particular had a big opportunity cost in terms of time. The way levels are encoded means that saving a few bytes here and there can be used to extend a level by an additional screen. At one point during development, the programmers at Nintendo went back through the code and tried to reduce the size slightly to squeeze in even more level data. You can see evidence of this in several places, such as in the
MoveAllSpritesOffscreen subroutine on line 940, where they combine 2 routines into 1 and insert a
BIT opcode to “trick” the CPU into treating the next instruction
LDY #04 as part of a
BIT instruction, because that’s one byte shorter than just adding a branch to skip over the
LDY. Seriously, that blows my mind.
What about the interval timer lasting 21 frames instead of 20; is that a bug? The programmer in this case set the interval to 20, then decreases it until it expires. Here, the programmer had 2 choices: they could have used
BEQ (branch if equal to zero) to detect when it exactly hits 0, or
BMI (branch if minus/negative) which is what they did use. One disadvantage of
BEQ is that if another part of code incidentally decreases the interval timer and skips over the value 0, then the
BEQ won’t trigger until the timer rolls back over, thus making the longer timers last too long.
BMI avoids this because it will trigger on any negative number. As far as I can tell, this never happens in the code, but it’s a defensive choice. I would think that the developer intended the interval to last 20 frames, because the game runs at 60 frames per second and 60 is divisible by 20. But again, even if they did intend it to last exactly 20 frames, I wouldn’t consider this a bug, because it has no effect on gameplay anyway.
So the frame rule behavior in Super Mario Bros. that affects speedruns so much isn’t due to optimization or any other concerns; it’s just a byproduct of how Nintendo programmed timers, and their incidental decision to use a long timer for the intermediate information screen between levels. Neat!
- Technically it’s a Ricoh 2A03, which is a second-sourced 6502. It has a few minor differences, but it’s substantially similar and still uses the 6502 instruction set.
- You can store larger numbers in RAM by using multiple bytes, but the CPU can only do arithmetic on them 1 byte at a time, and the programmer must write specific code to handle these multiple bytes.