Skinny on NES scrolling

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

User avatar
koitsu
Posts: 4218
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Post by koitsu » Fri Jun 01, 2012 12:37 pm

tokumaru wrote:
koitsu wrote:Then can you verify that my example here is correct? http://wiki.nesdev.com/w/index.php/The_ ... 06_example
Looks correct. I see no reason to write anything other than the NT index on the first write though, since everything else gets overwritten. Is your purpose to show that bits do get overwritten?
The purpose is to document things that cause confusion. The existing documentation referred to Drag's forum post which confused myself and another person to no end. The existing documentation and the forum posts are, simply put, not clear enough to someone who has little familiarity with the nature of how this works; it's good to have reference source material, but it's even better to thoroughly document things.

Seeing which bits get overwritten is equally as important too, though I'm not trying to cover every example case (e.g. 2005/2005/2006/2006, 2005/2006/2005/2006, 2006/2005/2005/2006, 2006/2005/2006/2005, etc...), just ones which are known to be commonly seen in people's code or commercial games.

This is the kind of stuff that really needs thorough and concise documentation, since I think PPU behaviour on writes to $2005/2006 has been a long-standing sore spot for anyone coding for the NES who isn't ancient ( :-) ), wasn't involved in the discovery, or is working on an emulator.

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru » Fri Jun 01, 2012 1:06 pm

By the way, I didn't manage to solve the problem with vertical scroll after screensplit in Lawn Mower (works in emulators, does not work on the hardware), and gave up. So I think that more clear explaination with working code snippets would be helpful.

3gengames
Formerly 65024U
Posts: 2281
Joined: Sat Mar 27, 2010 12:57 pm

Post by 3gengames » Fri Jun 01, 2012 1:41 pm

I dunno what is confusing people. The only thing I think should be added is a basic "T register bit breakdown" to see what bits do what to make it a tiny bit more clear. Loopys old doc was TERRIBLE. I read it many times, and didn't know what it meant at any time ever. I never understood it. But just last week in chat, showed me the new one, and seeing it as it is in the new doc made it brain-dead easy to understand. You guys have seen the short, simple, and easy document, right?

User avatar
koitsu
Posts: 4218
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Post by koitsu » Fri Jun 01, 2012 9:38 pm

I've only seen what's on the wiki and what used to be distributed as loopy.txt back in the day. *sigh* You people! ;P

User avatar
Kasumi
Posts: 1292
Joined: Wed Apr 02, 2008 2:09 pm

Post by Kasumi » Fri Jun 01, 2012 10:12 pm

3gengames wrote:You guys have seen the short, simple, and easy document, right?
Nope. Care to share it?

3gengames
Formerly 65024U
Posts: 2281
Joined: Sat Mar 27, 2010 12:57 pm

Post by 3gengames » Fri Jun 01, 2012 10:15 pm

http://home.comcast.net/~olimar/NES/skinny.txt

Pretty straight forward if you just take a loot at that. :)

Drag
Posts: 1327
Joined: Mon Sep 27, 2004 2:57 pm
Contact:

Post by Drag » Fri Jun 01, 2012 11:29 pm

Back in the day, the main thing that confused me was $2006. It looks like you can just write the address of some tile within the nametable, and it'll start drawing from there. However, that's not true, because it won't start from the top of the tile unless you subtract #$2000 from what you write, and this confused me for the longest time, until I had something explained to me

The PPU doesn't have an "address" register. Instead, it has various counters: X, Y, y, and N. X and Y are 5-bit counters, and hold the (X, Y) coordinates for the current tile being drawn. N is a 2-bit counter that determines which of the 4 nametables that tile is coming from. "y" is a 3-bit counter that determines which scanline of the tile's pattern we're drawing (Don't forget, the PPU draws the same row of tiles for 8 scanlines, the only thing that changes is the specific byte of the pattern data it draws)

When you line those counters up like this: NNYYYYYXXXXX, you conveniently get the byte address for the nametable tile that those counters are pointing to. This is what's happening when you write to $2006, you're giving an "address", and the PPU is translating it to an (X, Y) coordinate within one of the nametables (N) by stuffing the bits of your "address" into those counters as appropriate.

The tricky part is the fact that the PPU needs 2 additional bits, so it knows whether you're trying to access the pattern tables, the nametables, or the palette. Those 2 additional bits come from "y".

So, when you write to $2006, this is what's happening:

Code: Select all

      15      bit      0
$2006 [........ ........]
         |||||| ||||||||
         |||||| |||+++++--- X
         |||||| |||
         ||||++-+++-------- Y
         ||||
         ||++-------------- N
         ||
         ++---------------- y
(Note: The highest two bits of $2006 are ignored)
This is why you can't simply write the address for a nametable tile when you want the screen to start drawing from there; the nametables are at $2xxx, so you're setting "y" to 2, which means you'll start 2 pixels down from the top of the tile you want to draw, instead of at the top. If you subtract #$2000 from the tile address you want, then even though the "address" you write points to the pattern tables, the actual counters are set up properly (especially "y", which is now set to 0), and the tile will correctly be drawn from its top scanline.

The PPU can line these counters up in a variety of different ways. For example, $2005 does it like this:
YYYYYyyy XXXXXxxx
which translates to a specific pixel within a nametable, but you cannot select which nametable you want by using $2005.

While the PPU is rendering, it uses something like this:
NYYYYYyyy NXXXXX
Every 8 pixels, 1 is added to X, which overflows to the low bit of N (so it'll cross to the next nametable, horizontally). On the next scanline, NXXXXX is reset, and 1 is added to y (moving us to the next scanline), which overflows to Y (moving us to the next row of tiles), which overflows to the high bit of N (crossing us over into the next nametable, vertically).

Additionally, there's some extra logic in place to make it so Y overflows (and wraps) after 29, instead of after 31. However, Y only overflows into the top bit of N when overflowing from 29. Otherwise, Y will just wrap from 31 to 0. This is what creates the "negative scrolling" quirk when you set the Y scrolling between E0-FF; Y is being set to 30 or 31, and after 31, it wraps to 0 without touching N.

This was awfully wordy to explain, but hopefully it wasn't too confusing. :P When you write to $2006, you're setting counters, even though it looks like you're writing an address. That's why the $2006/2005/2005/2006 trick requires you to write a bunch of bullshit to $2006; even though they're not addresses, they're still setting the counters the way you want them.

UncleSporky
Posts: 385
Joined: Sat Nov 17, 2007 8:44 pm

Post by UncleSporky » Sat Jun 02, 2012 8:06 am

3gengames wrote:I dunno what is confusing people. The only thing I think should be added is a basic "T register bit breakdown" to see what bits do what to make it a tiny bit more clear. Loopys old doc was TERRIBLE. I read it many times, and didn't know what it meant at any time ever. I never understood it. But just last week in chat, showed me the new one, and seeing it as it is in the new doc made it brain-dead easy to understand. You guys have seen the short, simple, and easy document, right?
You realize the two documents are almost completely identical, right?

Here they are side by side. I stripped off the email headers and put SKINNY.TXT and SKINNYNT.TXT in the same doc:

Image

So I don't see how you consider his old doc terrible and the new one brain-dead easy. It's more likely that your own knowledge grew and made it easier for you to understand over time.

User avatar
tokumaru
Posts: 11907
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru » Sat Jun 02, 2012 8:22 am

Everyone who gets into NESDEV appears to have trouble understanding this... I know I did! It's not really complicated though, it's just that the original docs explained it very poorly, so it's good that you guys are trying to do a better job on the wiki page.

tepples
Posts: 22143
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples » Sat Jun 02, 2012 8:43 am

After a short vdiff on the pictures above, I'm pretty sure the difference has something to do with the use of letters like ABCDExxx in the "stuff that affects register contents" section of loopy's new doc as opposed to the 11111000 "shorthand logic" in the old version. The wiki version follows a convention very close to that of the new version. Other polish changes made in the wiki version include splitting t and v at byte boundaries, the identification of what bits get copied at "scanline start" as all horizontal bits, use of $ on all hexadecimal addresses, and capital letters at the start of sentences.

Drag
Posts: 1327
Joined: Mon Sep 27, 2004 2:57 pm
Contact:

Post by Drag » Sat Jun 02, 2012 9:52 am

For a much simpler 2006/2005/2005/2006 trick explanation (since my previous wall-of-text may be intimidating):
  • Calculate a temporary value TEMP:
    • Take your Y scroll value, ASL twice, AND #$E0, and store it to TEMP.
    • Take your X scroll value, LSR three times, ORA with TEMP, and store it again.
  • Select the nametable you want to display from (ASL'd twice), and write it to $2006.
  • Write your Y scroll value to $2005
  • <wait for h-blank to start...>
  • Write your X scroll value to $2005
  • Take your TEMP value and write it to $2006.
Those last two writes need to occur in h-blank, which is why it's important to calculate that TEMP value beforehand, and not in between the last two writes. (Calculating this value may be a good way to wait for h-blank though!)

If you don't care about why this method works, you can simply use this method as-is, and it should work. :P

If $2006 confuses you, keep in mind, you only write addresses to $2006 when you want to read/write $2007 afterwards. If you're just playing with the scrolling, the value you write to $2006 is not going to be an address.

If the backwards $2005 writes confuse you:
$2005 and $2006 are 16-bit registers; you need to write to them twice. However, they share the same latch that determines whether you're writing to the upper 8 bits, or the lower 8 bits. So, you're writing a high byte to $2006, but rather than writing a low byte to $2006, you're writing one to $2005 instead. And then the reverse happens, you write a high byte to $2005, and then you write a low byte to $2006.

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru » Sat Jun 02, 2012 10:12 am

Code explaination in this way (do this one time, that two times etc) gives more opportunity to make a mistake.

It is always said that the last two writes should be done in the h-blank time, but never explained why, what could happen if they aren't, and how to get them into the h-blank properly (not obvious, since the hblank time is short).
Last edited by Shiru on Sat Jun 02, 2012 10:55 am, edited 1 time in total.

User avatar
Dwedit
Posts: 4365
Joined: Fri Nov 19, 2004 7:35 pm
Contact:

Post by Dwedit » Sat Jun 02, 2012 10:54 am

I think the letters are absolutely confusing. I made my summary using x and . notation, where X is the bit affected, and . is the bit not affected.
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!

tepples
Posts: 22143
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples » Sat Jun 02, 2012 12:00 pm

Shiru: If the writes don't land in hblank, you get glitches like those in Super Mario Bros. 3: the fine X scroll might be out of sync with the rest of the scroll values. Or you might get shaking when the writes are applied before the critical time in one line and after in the next.

The hblank time is short, but not too short. The portion of the scanline when the NES isn't rendering or fetching the background is dots 256 to 319. On NTSC, this is (319-256)/3 = 21 cycles long, and a typical sprite 0 spin wait has an uncertainty of 7 cycles. A DMC IRQ wait might have a few more cycles of uncertainty. Drag's pseudocode translates to the following code, which works with up to 15 cycles of uncertainty:

Code: Select all

  lda last_PPUCTRL
  asl a
  asl a
  sta PPUADDR
  lda camera_y_lo
  sta PPUSCROLL
  asl a
  asl a
  and #$E0
  sta temp
  ldx camera_x_lo
  txa
  lsr a
  lsr a
  lsr a
  ora temp
  ; hblank needs to start before the LAST cycle of the next instruction
  stx PPUSCROLL
  sta PPUADDR
It isn't too different from tokumaru's code, except tokumaru cleverly overlaps camera_x_lo with temp to save one byte of RAM at the cost of three cycles and two bytes of ROM.

Dwedit: What do you mean? The version on the wiki uses EDCBA for affected bits and . for unaffected bits.

User avatar
Bregalad
Posts: 7988
Joined: Fri Nov 12, 2004 2:49 pm
Location: Chexbres, VD, Switzerland

Post by Bregalad » Sat Jun 02, 2012 1:26 pm

I'm sorry but what you write to $2006 IS an adress - if you forget about the high 2 bits that is.
At least for me things became MUCH easier to understand that way.

As long as you keep writing coherent things to $2005/6 (that is the name table adress you write to $2006 correspond exactly to the scroll position you write with $2005) then there should be no glitches, and the order in which you write the registers does only matter for fine scrolling. Any order should do as long as you end by a final $2006/2 write.
Useless, lumbering half-wits don't scare us.

Post Reply