The skinny on NES scrolling
Preface
"The skinny on NES scrolling" was posted by loopy on 1999-04-13 to what eventually became the NESdev Yahoo! Group. It was the first to publicly tell how exactly how the PPU uses addresses written to its ports. After over a decade, it is still believed accurate. Some people get turned off by the fact that it's provided as monospaced text inside a zipfile, that addresses have nothing to distinguish them from years, and that the diagrams of what bits get copied where are allegedly difficult to read. What follows is this document, reformatted to web standards, with a few minor things made slightly clearer.
PPU registers
Games using complex raster effects require a complete understanding of how the various address registers inside the PPU work. Here are the related registers:
- v
- Current VRAM address (15 bits)
- t
- Temporary VRAM address (15 bits)
- x
- Fine X scroll (3 bits)
Registers v and t are 15 bits, but because emulators commonly store them in 16-bit machine words, they are shown with an extra bit that's never used.
The PPU uses the current VRAM address for both reading and writing PPU memory thru $2007, and for fetching nametable data to draw the background. As it's drawing the background, it updates the address to point to the nametable data currently being drawn. Bits 10-11 hold the nametable address minus $2000. Bits 12-14 are the Y offset of a scanline within a tile.
Stuff that affects register contents
In the following, d refers to the data written to the port, and A through H to individual bits of this value.
$2000 write:
t:....BA.. ........ = d:......BA
$2005 first write:
t:........ ...HGFED = d:HGFED... x: CBA = d:.....CBA
$2005 second write:
t:......HG FED..... = d:HGFED... t:.CBA.... ........ = d:.....CBA
$2006 first write:
t:..FEDCBA ........ = d:..FEDCBA t:.G...... ........ = 0
$2006 second write:
t:........ HGFEDCBA = d:HGFEDCBA v = t
At the end of each scanline, if rendering is turned on, the PPU copies all bits related to horizontal position from t to v:
v:.....H.. ...EDCBA = t:.....H.. ...EDCBA
And at the end of the pre-render scanline, if rendering is turned on, the PPU copies all bits from t to v:
v = t
Note: $2005 and $2006 share the toggle that selects between first/second writes. Reading $2002 will clear it.
All of this info agrees with the tests Loopy has run on an NES console. If there's something you don't agree with, please let the BBS know so that a member can verify it.
Wrapping around
You can think of bits 4-0 of the VRAM address as the "coarse x scroll"(*8) that the PPU increments as it draws. As it wraps from 31 to 0, bit 10 is switched. You should see how this causes horizontal wrapping between nametables (0,1) and (2,3).
You can think of bits 9-5 as the "coarse y scroll"(*8). This functions slightly different from the X. It wraps to 0 and bit 11 is switched when it's incremented from 29 instead of 31. There are some odd side effects from this. If you manually set the value above 29 (from either $2005 or $2006), the wrapping from 29 obviously won't happen, and attribute data will be used as nametable data. The "y scroll" still wraps to 0 from 31, but without switching bit 11. This explains why writing 240+ to 'Y' in $2005 appeared as a negative scroll value.
Examples
Below are examples using actual 6502 code, indicating (tracking) what happens to all relevant variables described above, both before and after the 6502 instructions are executed.
Individual bits written to a PPU register are colour-coded to reflect where they end up in t.
Finally, assume all 6502 code is run sequentially in the order shown, one instruction after the next.
Original skinny example
This is based completely off of the above Stuff that affects register contents section.
Before | Instructions | After | Notes | ||||
---|---|---|---|---|---|---|---|
t | v | x | t | v | x | ||
....... ........ | ....... ........ | ... | LDA #$00 (%00000000) STA $2000 |
...00.. ........ | ....... ........ | ... | |
...00.. ........ | ....... ........ | ... | LDA #$7D (%01111101) STA $2005 |
...00.. ...01111 | ....... ........ | 101 | |
...00.. ...01111 | ....... ........ | 101 | LDA #$5E (%01011110) STA $2005 |
1100001 01101111 | ....... ........ | 101 | |
1100001 01101111 | ....... ........ | 101 | LDA #$3D (%00111101) STA $2006 |
0111101 01101111 | ....... ........ | 101 | Bit 15 of t gets set to zero |
0111101 01101111 | ....... ........ | 101 | LDA #$F0 (%11110000) STA $2006 |
0111101 11110000 | 0111101 11110000 | 101 | After t is updated, contents of t copied into v |
2006-2005-2005-2006 example
This is based on Drag's example on the nesdev forum where writes to PPU registers are done in the order of $2006, $2005, $2005, $2006, and what the effect is.
Before | Instructions | After | Notes | ||||
---|---|---|---|---|---|---|---|
t | v | x | t | v | x | ||
To be written... |