Introduction
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.
Timers
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
label.
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 BubbleTimerData
with LDA
. That list has 2 values in it: 64 and 32. Then it stores that value back into the AirBubbleTimer
with STA
. Thus, a new bubble will appear in either 64 or 32 frames.
Timer code
The code that adjusts the timers is on line 796, under the DecTimersLoop
label.
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 BPL
.
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 DecTimersLoop
:
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 IntervalTimerControl
with 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 LDA
and 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 InjuryTimer
and 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.
Other thoughts
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.
Conclusion
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!