This is done by changing the scroll mid-screen.
This is what the nametables look like on scanline 30 in Super Mario Bros, with the scroll shown:
The horizontal scroll is 0 (you can barely see the white line on the left side).
Here is the same image on scanline 31:
Now the horizontal scroll is at the left edge of the visible area.
The PPU draws the screen in the same order you read text on a page -- left-to-right and top-to-bottom. Each horizontal line of the picture is called a scanline, and the picture is 240 scanlines tall. In between each frame is a short period of time called vertical blanking (or VBlank) where the PPU is idle. The PPU cannot be written to outside of VBlank because it is very busy and trying to talk to it will mess it up.
Normally, to scroll the background, you write the scroll position to the
PPUSCROLL register (
The first write sets the X scroll, and the second write sets the Y scroll.
In order to create a split scroll, set the scroll value for the top section of the picture during VBlank, wait until the PPU is drawing the last scanline of the top section, and then change set scroll for the bottom section.
Waiting for the PPU to reach the split point
There are two commonly used ways to detect once the PPU has reached a specific part of the picture:
Sprite 0 Hit
The PPU sets bit 6 of
PPUSTATUS if an opaque pixel of the sprite at OAM index 0 intersects an opaque background pixel. It sets this flag as soon as it draws the intersecting pixel, so by putting sprite 0 at a clever location, a game can detect when the PPU has drawn a part of the picture.
Here's the image of Super Mario Bros., but with background rendering disabled (so only sprites are visible):
See that, above and slightly to the left of Mario's head?
That's Sprite 0, and it overlaps the bottom of the coin icon in the status bar:
Super Mario Bros detects once the PPU has drawn the coin with this loop:
I'd use the following loop, as it's slightly smaller and more efficient, but it does the same thing:
Note: The Sprite 0 Hit flag isn't cleared until the pre-render line, so if that loop runs during VBlank, it will immediately detect the previous frame's sprite 0 hit. To fix this, wait until the sprite 0 hit flag to be cleared, then wait for it to be set:
Sprite 0 Hit works well for a status bar on the top, but what about Super Mario Bros. 3, which has the status bar on the bottom? The game can't waste 80% of the frame waiting for the PPU to reach the status bar -- it needs to time to run the game logic!
Some mappers, such as the MMC3, can generate an interrupt once the PPU reaches a certain scanline. This is the easiest way to tell when the PPU reaches a certain point of the screen, and it's also the most efficient because the CPU can do other things instead of polling the PPU.
Here's how to set up a scanline IRQ with the MMC3. Other mappers are similar but not the same; check wiki.nesdev.com if you're using a different mapper.
;A is the number of scanlines from now you want the interrupt.
;If this is run during VBlank, A is just the Y-coordinate
;you would like the interrupt at.
sta $C000 ;Set the counter reload value.
sta $C001 ;Trigger a reload at the end of this scanline.
sta $E001 ;Enable the interrupt.
cli ;Enable IRQs.
Your IRQ handler will then run once the PPU draws the target scanline.
Setting the scroll
Horizontal scroll only
Setting the horizontal scroll is easy -- just write to
PPUSCROLL like normal.
Setting the vertical scroll during rendering is far more difficult and requires knowledge of the implementation details of scrolling.
The PPU contains four internal registers to store address/scroll-related data. These registers are modified by
PPUADDR (and the nametable select bits of
PPUCTRL). These registers are usually referred to as
v is a 15-bit register which stores the current VRAM address.
t is the temporary VRAM address, also 15 bits.
x is a 3-bit register which stores the fine X scroll, and
w, a 1-bit register, stores whether this is the first or second write to
PPUSCROLL. A read from
PPUSTATUS will reset
w to 0.
PPUADDR directly sets
PPUSCROLL only sets
t. When writing a byte to
w is 0 (the first write), the byte will be written to the upper 7 bits of
w will be set to 1. If
w is 1 (the second write), the byte will be written to the lower 8 bits of
t will be copied to
w will be reset to 0.
v and scrolling
The PPU also uses
v to store scrolling information. Due to the nametable layout, 15-bit addresses of tiles in nametable are structured like this:
010 NN YYYYY XXXXX
NN is the nametable select from
YYYYY is the coarse Y scroll (the upper 5 bits), and
XXXXX is the coarse X scroll. Because the upper 3 bits are constant, they're used to store the fine Y scroll. The fine X scroll is stored in
The PPU stores in
v the address/scroll of the tile currently being fetched, and
t stores the address/scroll of the upper-left corner. When the PPU finishes fetching a tile, it increments
v to move to the next tile, and at the end of each scanline, it increments the vertical scrolling bits and reloads the horizontal scrolling bits from
t to jump to the beginning of the next scanline. It only loads the vertical bits from
t at the top of the picture, which is why we can't easily modify vertical scroll in the middle of the picture. However, the second write to
PPUADDR will fully copy
v, even in the middle of a picture.
To fully set the scroll, we'll have to write to both
PPUADDR. The most common pattern is to write
PPUSCROLL, and then
w, the latch that keeps track of whether it's the first or the second write. This means we can do the following to change the horizontal and vertical scroll mid-frame:
nametable << 2 to
PPUADDR. This will write to the top half of
t (We don't need to write the Y scroll bits here because they will be overwritten by the next write).
- Write the Y scroll to
PPUSCROLL. This will write to the vertical scroll bits of
- Write the X scroll to
PPUSCROLL. This will write the coarse X to the horizontal scroll bits of
t and the fine X to
((y & 0xF8) << 2) | (x >> 3) to PPUADDR. This will write to the bottom half of
t and copy
The last write should occur during horizontal blanking (but after the PPU increments the vertical position) to avoid the split happening mid-scanline and looking glitchy.
Here is some code to perform a horizontal and vertical scroll split. If you're using sprite 0 hit, run this code after the hit is detected; if you're using a scanline IRQ, run this in your IRQ handler.
;Write nametable bits to t.
;Write y bits to t.
;The last write needs to occur during horizontal blanking
;to avoid visual glitches.
;HBlank is very short, so calculate the value to write now, before HBlank.
;Write the X bits to t and x.
;Finish calculating the fourth write.
;Wait for HBlank.
ldx #06 ;How long to wait. Play around with this value
;until you don't have a visual glitch.
;Write to t and copy t to v.