28

Games like Super Mario Bros. and Super Mario Bros. 3 were able to scroll the playfield but not the status bar:

Super Mario Bros. -- the status bar is on the top

Super Mario Bros. 3 -- the status bar is on the bottom

The status bars are clearly too big to be sprites, yet they don't scroll with the background. How did they do this? How can I do this in my own NES games?

36

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:

enter image description here

The horizontal scroll is 0 (you can barely see the white line on the left side).

Here is the same image on scanline 31:

enter image description here

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):

Mario, a Goomba, and something at the bottom of the status bar.

See that, above and slightly to the left of Mario's head?

enter image description here

That's Sprite 0, and it overlaps the bottom of the coin icon in the status bar:

The coin counter

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:

  1. 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).
  2. Write the Y scroll to PPUSCROLL. This will write to the vertical scroll bits of t.
  3. 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.
  4. 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
  • 1
    That's quite simple, yet impressive. – JAB Dec 9 '16 at 4:42
  • 1
    This is a fantastic post! Well done! – Wilson Dec 9 '16 at 18:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.