The skinny on NES scrolling: Difference between revisions

From NESdev Wiki
Jump to navigationJump to search
(→‎Stuff that affects register contents: Vramaddr.jpg by Quietust)
m (→‎H position reset: someone said at dot 256... so I changed it.)
Line 39: Line 39:
  t:........ HGFEDCBA = d:HGFEDCBA
  t:........ HGFEDCBA = d:HGFEDCBA
  v                  = t
  v                  = t
At dot 258 of each scanline, if rendering is enabled, the PPU copies all bits related to horizontal position from ''t'' to ''v'':
At dot 256 of each scanline, if rendering is enabled, the PPU copies all bits related to horizontal position from ''t'' to ''v'':
  v:.....H.. ...EDCBA = t:.....H.. ...EDCBA
  v:.....H.. ...EDCBA = t:.....H.. ...EDCBA
And at dot 304 of the pre-render scanline, if rendering is enabled, the PPU copies all bits from ''t'' to ''v'':
And at dot 304 of the pre-render scanline, if rendering is enabled, the PPU copies all bits from ''t'' to ''v'':

Revision as of 22:10, 2 June 2012

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 a 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 dot 256 of each scanline, if rendering is enabled, the PPU copies all bits related to horizontal position from t to v:

v:.....H.. ...EDCBA = t:.....H.. ...EDCBA

And at dot 304 of the pre-render scanline, if rendering is enabled, 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 so that the next write is seen as a first write.

All of this info agrees with the tests Loopy has run on an NES console and Quietust's analysis of a micrograph of the PPU die. 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.

Assume all 6502 code is run sequentially in the order shown, one instruction after the next.

Finally, note that the number of bits for v and t is 15, while further up the page the documentation shows them as 16-bit. The 16th bit is unused (always 0).

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
....... ........ ....... ........ ... LDA #$27 (%00100111)
STA $2006
0100111 ........ ....... ........ ... Bit 15 of t set to zero
0100111 ........ ....... ........ ... LDA #$3E (%00111110)
STA $2005
1100100 111..... ....... ........ ... Behaviour of 2nd $2005 write
1100100 111..... ....... ........ ... LDA #$7D (%01111101)
STA $2005
1100100 11101111 ....... ........ 101 Behaviour of 1st $2005 write
1100100 11101111 ....... ........ 101 LDA #$99 (%10011001)
STA $2006
1100100 10011001 1100100 10011001 101 After t is updated, contents of t copied into v