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 ($2005
):
lda xScroll
sta $2005
lda yScroll
sta $2005
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:
Sprite0Wait:
lda $2002
and #$40
beq Sprite0Wait
I'd use the following loop, as it's slightly smaller and more efficient, but it does the same thing:
Sprite0Wait:
bit $2002
bvc Sprite0Wait
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:
Sprite0ClearWait:
bit $2002
bvs Sprite0ClearWait
Sprite0Wait:
bit $2002
bvc Sprite0Wait
Mapper interrupts
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.
Vertical scroll
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 PPUSCROLL
and PPUADDR
(and the nametable select bits of PPUCTRL
). These registers are usually referred to as v
, t
, x
, and w
.
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 PPUADDR
or PPUSCROLL
. A read from PPUSTATUS
will reset w
to 0.
PPUADDR
directly sets t
and v
, while PPUSCROLL
only sets t
. When writing a byte to PPUADDR
, if w
is 0 (the first write), the byte will be written to the upper 7 bits of t
and 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
, t
will be copied to v
, and w
will be reset to 0.
t
and v
and scrolling
The PPU also uses t
and 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
where NN
is the nametable select from PPUCTRL
, 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 x
.
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 t
to v
, even in the middle of a picture.
To fully set the scroll, we'll have to write to both PPUSCROLL
and PPUADDR
. The most common pattern is to write PPUADDR
, PPUSCROLL
, PPUSCROLL
, and then PPUADDR
. PPUSCROLL
and PPUADDR
share 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:
- Write
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 t
.
- Write the X scroll to
PPUSCROLL
. This will write the coarse X to the horizontal scroll bits of t
and the fine X to x
.
- Write
((y & 0xF8) << 2) | (x >> 3)
to PPUADDR. This will write to the bottom half of t
and copy t
to v
.
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.
Example code
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.
lda splitNT
asl
asl
sta PPUADDR
;Write y bits to t.
lda splitY
sta PPUSCROLL
;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.
and #$F8
asl
asl
sta tmp
lda splitX
;Write the X bits to t and x.
sta PPUSCROLL
;Finish calculating the fourth write.
lsr
lsr
lsr
ora tmp
;Wait for HBlank.
ldx #06 ;How long to wait. Play around with this value
;until you don't have a visual glitch.
loop:
dex
bne loop
;Write to t and copy t to v.
sta PPUADDR