Sometimes, you’ll want to stop code execution until the screen is done drawing. For instance, if you need to update more tiles on screen than the vBlank period is capable of, when you want to turn off the screen to do additional updates, or simply because you want a few frames of idle time in your game. There are two ways to do this, which NESmaker both applies. Let’s go into the details.
What is VBlank?
CRT monitors and TV’s project graphics on screen by using a beam which, from left to right and from top to bottom, sends light to the screen. When the beam reaches the bottom right of the screen, the beam shuts off and moves back to the top left to send a new frame of lights to the monitor. This period is called vertical blanking, or VBlank for short. Generally, this period is used by the NES to update graphics before sending them to the screen, because updating these on the fly may result in glitched graphics.
Why wait for VBlank?
Because graphical updates on the fly will most likely glitch out the screen, NESmaker projects don’t send these updates directly to the PPU. Instead, they add updates to buffer variables called scrollUpdateRam
. During VBlank, the scrollUpdateRam values get sent to the PPU, so on the next frame the graphical changes will take effect.
The VBlank period is fairly short though. So if you need to update a lot of graphics, these may take more time than available within VBlank. The result: either the new frame gets drawn while updates are still being sent to the PPU, glitching out the screen, or NMI gets triggered before it ends, causing the screen to not update properly or even the game to lock up entirely. To prevent this from happening, we’ll wait for VBlank to happen during buffer updates. The scrollUpdateRam gets sent to the PPU and gets cleared to gain time before the additional updates get added.
How do we know when VBlank happens?
When VBlank happens, the NES does two things:
- It sets the VBlank flag (the highest bit of the PPU status at address $2002)
- It triggers the nonmaskable interrupt (or NMI for short)
So if we want to wait a frame, we could poll the high bit of $2002 to see if it is set, and when it is, we know we’re in VBlank. Additionally, we can use a RAM variable to check if NMI has occurred. We set this variable (which NESmaker calls waiting) to 1 and keep checking this variable in a loop until it is 0 again. During NMI, we can reset this variable back to 0, so we break out of the original loop and continue the main loop.
How does NESmaker check the VBlank flag?
Open up the default reset script at BASE_4_5\System\Reset.asm
. You’ll come across the following script:
BIT $2002 vBlankWait1: BIT $2002 BPL vBlankWait1
BIT $2002
, among other things, sets the Negative flag if bit 7 of $2002 is 1, and resets it if bit 7 of $2002 is 0. Bit 7 of $2002 is the VBlank flag, which is set when VBlank happens.
Additionally, reading from address $2002 also resets bit 7. This is why BIT $2002
is called once before the loop: to ensure the flag is 0 before looping.
BPL vBlankWait1
branches out if the Negative flag is not set. So as long as that VBlank flag is 0, it loops back to vBlankWait1:
, and it only breaks out of the loop when that flag is 1 again.
How does NESmaker check for NMI?
This is where the doWaitFrame subroutine (BASE_4_5\Game\Subroutines\doWaitFrame.asm
) comes in handy. Here’s what the subroutine looks like, with added comments about what it does exactly.
doWaitFrame: ; the label by which the subroutine is called INC waiting ; add 1 to the waiting variable waitLoop: ; this label is used to be able to loop LDA waiting ; load the value of waiting into the accumulator BNE waitLoop ; if it is not zero, branch back to waitLoop ; if we're here, that means waiting is zero RTS ; return to where the subroutine was called
But wait – if waiting
is set to 1, and never reset to 0 in this routine, how is this not an endless loop? Well, that’s what NMI does. Every time VBlank happens, the NES jumps to NMI (that’s what “interrupt” means), executes all code there, and returns to where the script was initially. And if you check BASE_4_5\System\NMI.asm
, somewhere at the end, you’ll see that the waiting
variable indeed gets reset:
LDA #$00 STA waiting
So after NMI happened, waiting
is 0 and the subroutine will stop looping.
Why not always use the VBlank flag method?
Experience has proven that the VBlank flag is not too reliable. Reading the VBlank resets it, and the NES itself also resets it at a certain time every frame. Therefore, for timing purposes, this method is not recommended. For warmup (i.e. within the Reset routine) it’s fine to rely on this flag though.