Skinny on NES scrolling

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Skinny on NES scrolling
by on (#94887)
So I took what must be my hundredth attempt at understanding this mess:

http://wiki.nesdev.com/w/index.php/The_ ... _scrolling

But found that the wiki suffers from the exact same problem that Loopy's original document does: lack of definition of what each variable represents. The irony is in the fact that at the top of the Wiki it references that exact problem, but (from what I can see) does very little to address that problem.

I would like to see the following variables explained/defined:

d
A
B
C
D
E
F
G
H
X

I'm left with the impression that A through H are just letter placeholders for specific VRAM address bits, but without an explanation it's literally impossible to tell. I can't even begin to fathom what X is.

I'm not the only one who after 10+ years still finds this document completely and entirely indecipherable. :-)

I'd love if someone could really write this thing up into a coherent step-by-step (read: verbose) guide, since there are a multitude of games that (obviously) rely on this behaviour, so emulating it wrong results in hard-to-discern breakage (e.g. in some games but not others).

P.S. -- And yeah, I'm aware of this, which is much more helpful but at a different level: http://wiki.nesdev.com/w/index.php/PPU_scrolling

EDIT: After reading the 2nd URL (PPU_scrolling), it's a little more clear, but not entirely. It seems to me these two pages should probably be re-written and merged into one well-written page. Given my history with documentation I'd be happy to do this except I do not understand how it works, hence my post here. :-)

by on (#94888)
The "d" is loopy's; the "A-H" is mine. I added the following:
"In the following, d refers to the data written to the port, and A through H to individual bits of this value."

by on (#94890)
Thanks Tepples. Is that edit correct though?

http://wiki.nesdev.com/w/index.php?titl ... oldid=3595

It turns "X" into "H", but then within the PPU registers section of the document, "x" (note that it's lowercase, yet in the main part of the document something was referred to as "X" (capital)) is still labelled as "fine X scroll (3 bits)" even though I don't see any visual indication of something controlling 3 bits.

Yep, still confusing. Hehe :D

EDIT: Oh, I see. For the 1st $2005 write:

Code:
t:........ ...HGFED = d:HGFED...
x                   = d:.....CBA

Okay, so that explains what "x" is, and its 3 bits. However, now "x" isn't used anywhere further below that.

I think the formatting of all of these variables could really be improved in some way, probably if the entire thing was turned into a table and then each variable (t, v, x, and d) were given their own columns, along with a description column, thus every row would indicate what was written and which of the 8-bits of d went into what. I'll edit a sandbox page and see how it looks.

by on (#94891)
koitsu wrote:
It turns "X" into "H"

I chose this letter for two reasons: 1. "horizontal", and 2. we've already designated the letter to mean something that's copied.

Quote:
within the PPU registers section of the document, "x" (note that it's lowercase, yet in the main part of the document something was referred to as "X" (capital)) is still labelled as "fine X scroll (3 bits)" even though I don't see any visual indication of something controlling 3 bits.

I changed that: x:CBA = d:.....CBA.

Quote:
However, now "x" isn't used anywhere further below that.

Background scrolling in units smaller than one tile is accomplished through 8-bit-long shift registers: a parallel-to-serial input stage (2 bits wide, receiving data from pattern tables) and a serial-to-parallel output stage (4 bits wide, with 2 of the bits fed by the input stage and 2 fed by the attribute register). Fine X scroll is the select value of an 8-way mux on the output stage. The only thing that changes fine X scroll is the first write to $2005. When you write to $2005, the change to fine X scroll takes effect immediately, as of the next pixel. The rest of the write doesn't take effect until the end of the scanline.

Quote:
I think the formatting of all of these variables could really be improved in some way, probably if the entire thing was turned into a table and then each variable (t, v, x, and d) were given their own columns

Perhaps the reason that wasn't done in the first place might have had something to do with the 80 column limitation of common PC displays at the time.

by on (#94892)
The absolute easiest thing would be short example code. It really wouldn't have to be that long. There's a link to some of tokumaru's code but we don't get to see what's in each variable.

An example like, here is a game with a status bar at the top, the player has just started playing and has triggered the first pixel of horizontal movement. Timed code has gotten us to this point, here are several variables and exactly what they contain, and here is a routine showing how to scroll 1 pixel over.

I realize it's important to understand the underpinnings, but some people learn better this way.

EDIT: On further review, tokumaru's code and the rest of the links (Shiru's vertical scroll question) are already very educational, but again, literally showing values being written and when to write them can also be informative.

by on (#94894)
UncleSporky wrote:
The absolute easiest thing would be short example code. It really wouldn't have to be that long. There's a link to some of tokumaru's code but we don't get to see what's in each variable.


Tepples and UncleSporky, please review (no URL is not a typo):

http://wiki.nesdev.com/w/index.php/User:Koitsu

I provided a table-ised version (which IMHO is easier to read, especially given the formatting I did on it), and an example showing what v/t/x bits get changed. I'm not sure if Wiki syntax allows it, but I could probably colour-code the individual bits from the opcode which gets "transferred" into the t variable are a certain colour thus easier to read when looking at the "Instructions" vs. "After" columns.

by on (#94895)
koitsu wrote:
Tepples and UncleSporky, please review (no URL is not a typo):

http://wiki.nesdev.com/w/index.php/User:Koitsu

The common practice for making drafts in your userspace on a wiki running MediaWiki is to name it something like "User:Koitsu/The skinny on NES scrolling".

Quote:
I'm not sure if Wiki syntax allows it, but I could probably colour-code the individual bits

<span style="background: #FC9">A</span>

by on (#94896)
Thanks Tepples -- updated. I also found a mistake in my bit values (I know how/when I made this mistake), so I fixed that up. Things should be colourful and easier to note what goes where, though admittedly not friendly to those who are colour-blind.

Bare minimum, I think the Examples section would be worthwhile keeping. Though the preceding table-ised version of the ASCII diagram took me a while to do, I'm fine with giving it up.

I would also strongly advise that someone make an Example for this insanity (taken from PPU Scrolling) because that's one I STILL do not get/understand.

EDIT: I've integrated the Examples section into the skinny page.

by on (#94897)
koitsu wrote:
I would also strongly advise that someone make an Example for this insanity (taken from PPU Scrolling) because that's one I STILL do not get/understand.

That is just the order in which you have to write to the registers (and how the bits you write are supposed to be arranged) to fully reset the scroll mid-screen, considering all the rules loopy has detailed in his document.

by on (#94900)
Loopys new doc makes it pretty plain to figure out, and makes it clearer because it's basically a "Temp" and "V" ($2007 write) pointer, and when it shows what rights do what in the "T" register, it takes all of 3 seconds to understand.

by on (#94901)
tokumaru wrote:
koitsu wrote:
I would also strongly advise that someone make an Example for this insanity (taken from PPU Scrolling) because that's one I STILL do not get/understand.

That is just the order in which you have to write to the registers (and how the bits you write are supposed to be arranged) to fully reset the scroll mid-screen, considering all the rules loopy has detailed in his document.

What makes no sense to me is the "XXXX/1" vs. "XXXX/2" nomenclature used in that post, which is also in the PPU scrolling wiki. Specifically:

Code:
2006/1 --vv NNVV
2005/2 VVVV Vvvv
2005/1 HHHH Hhhh
2006/2 VVVH HHHH

2006/1 ---- NN-- (nametable select)
2005/2 VV-- -vvv (upper two bits of coarse V scroll, all bits of fine V scroll)
2005/1 ---- -hhh (fine horizontal scrolling) (takes effect immediately)
2006/2 VVVH HHHH (lower three bits of coarse V scroll, all bits of coarse H scroll)

Why is this not 2006/1, 2005/1, 2005/2, 2006/2? The behaviour of the "2005/2" write seems to be that of the 1st $2005 write per Loopy's doc, so I don't know why it's called "2005/2".

Code:
x
    Fine X scroll (3 bits)
Code:
$2005 second write:

t:......HG FED..... = d:HGFED...
t:.CBA.... ........ = d:.....CBA
Code:
$2005 first write:

t:........ ...HGFED = d:HGFED...
x:              CBA = d:.....CBA

by on (#94903)
koitsu wrote:
Why is this not 2006/1, 2005/1, 2005/2, 2006/2?


$2005 and $2006 share the high/low byte latch for the 1st/2nd write.

by on (#94904)
For me things became much, MUCH easier when I completely forgot about loopy's approach and went for my own approach.

Writing an address via $2006, simply, makes the adressed tile visible on the left of the next scanline.
The only tricky part is about the highest 4 bits - they are ignored (or you could say "hardwired to $2xxx", and instead the highest bits are used for fine Y scroll.
To adress fine X scroll you'll have to ressort to using $2005, as usual.

I think that's all there is to say about this topic.

by on (#94905)
3gengames wrote:
koitsu wrote:
Why is this not 2006/1, 2005/1, 2005/2, 2006/2?


$2005 and $2006 share the high/low byte latch for the 1st/2nd write.


Then can you verify that my example here is correct? http://wiki.nesdev.com/w/index.php/The_ ... 06_example

by on (#94914)
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?

by on (#94916)
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.

by on (#94917)
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.

by on (#94918)
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?

by on (#94924)
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

by on (#94925)
3gengames wrote:
You guys have seen the short, simple, and easy document, right?

Nope. Care to share it?

by on (#94926)
http://home.comcast.net/~olimar/NES/skinny.txt

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

by on (#94927)
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:
      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.

by on (#94929)
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.

by on (#94930)
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.

by on (#94931)
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.

by on (#94934)
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.

by on (#94935)
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).

by on (#94937)
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.

by on (#94938)
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:
  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.

by on (#94939)
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.

by on (#94942)
Maybe I saw somebody elses breakdowns on loopys site, but the "old one" was always 3 or so pages long, and had no charts like that. Maybe I'm thinking of another scrolling doc.

by on (#94943)
Everything above "Examples" is straight out of loopy; everything from "Examples" on down is brand new.

by on (#94953)
Shiru wrote:
Code explaination in this way (do this one time, that two times etc) gives more opportunity to make a mistake.

Any kind of code explanation leaves opportunities for mistakes.

Shiru wrote:
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).

If the last two writes are outside of h-blank, then there'll be ugly glitches where you split the screen. There may also be some "shaking" if it's not properly inside h-blank.

The way you delay your code depends on how you're splitting the screen; are you using MMC3? Are you using sprite 0? The absolute worst case scenario is that the programmer guesses how long to wait, and just adjusts until there isn't an ugly glitch in the middle of the screen anymore. :P

by on (#94954)
What shaking? I had something that is difficult to call 'shaking' in Lawn Mower. It was not wobbling a scanline up-down, more like two screens with different X offsets alternating every frame (i.e. ghostly doubled picture) - for the whole bottom part of the screen, where the scrolling is applied after split. Unfortunately it seems I don't have the recording of the problem.

I'm personally interested to see code for sprite 0 hit. The problem with the 'guess' way is that it requires to have the hardware, as emulators don't show glitches in this case, at least not the one that was in Lawn Mower.

by on (#94956)
Bregalad wrote:
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.

What you write to $2006 is only an address when you're writing to $2006 in order to use $2007 to access PPU memory. If you're writing to $2006 to change the scrolling, then what you write isn't necessarily going to be an address.

For example, to scroll the screen to the tile at $22A0, you need to write $02A0 to $2006. If you write $22A0 instead, then the screen does start at tile $22A0, but it'll start two pixels down, instead of at the top.

$02A0 scrolls you to the tile at $22A0, but the actual address $02A0 points to the pattern table. Thus, what you write to $2006 may not always correspond exactly to what you think it logically should.

Similarly, writing $12A0 will start 1 row into tile $22A0, and $32A0 will start 3 rows into $22A0. So again, when you're writing to $2006 to set the scrolling, and not to access PPU memory with $2007, then as far as you're concerned, you're not writing an address, you're setting a bunch of counters.

Edit: The only reason I'm so vocal about seeing it this way is because everyone needs to know that the highest nybble of what you write to $2006 has an effect; you cannot just use the address for a nametable tile without subtracting $2000 first.

by on (#94957)
No need to answer to my problem. It was an year and many projects ago, so I have no idea what I was writing - something that I was told to in a forum thread, and that worked in all emulators. I guess if I had the hardware, I would find how to do it properly, by trial and error. But I didn't have the hardware, still don't have, and probably won't have - so it would be nice to have proper working code with all explainations how to use it without such problems, and/or emulators that show the same glitches as the HW.

by on (#94958)
Shiru wrote:
What shaking? I had something that is difficult to call 'shaking' in Lawn Mower. It was not wobbling a scanline up-down, more like two screens with different X offsets alternating every frame (i.e. ghostly doubled picture) - for the whole bottom part of the screen, where the scrolling is applied after split. Unfortunately it seems I don't have the recording of the problem.

The shaking would be a scanline up-down wobbling.

What were you splitting the screen for? You weren't scrolling the top half, were you? I saw your code snippet in the other thread, it looked like it should work, so I'm thinking it could be a problem somewhere else in the code. Without seeing the rest of your code, the rom, or a video or anything, I can't do much to help. :P

Finally, I know the 2006/5/5/6 trick works, I've done it myself in a failed homebrew attempt, and I saw it work (scrolling and all) on a PowerPak.

by on (#94959)
Top part was fixed (stats), bottom part was scrolled left-right and up-down (rarely).

I doubt that the problem was somewhere else, because vertical scroll worked in all emulators. It didn't work on the hardware only, and was removed.

The ROM and source code are available for an year now. It is not hidden. I lost the video, as I said two posts ago.

Still, no need to solve this particular problem. I'm not going to change a game that was released long ago. I would like to have a working solution in case I'd need it in the future.

by on (#94968)
Quote:
For example, to scroll the screen to the tile at $22A0, you need to write $02A0 to $2006. If you write $22A0 instead, then the screen does start at tile $22A0, but it'll start two pixels down, instead of at the top.

Yes, and the adress of the corresponding tile is still $22A0.
As I said, it's an address exept for the higher 2 bits. And basically you're explaining me it's not an adress because of those 2 bits... but I already know that.
You're right that in hardware you're setting a bunch of counters, but it's actually totally equivalent, and I think it's much easier to think in therms of adresses. You just take the adress, AND it with $fff, and OR it with the Y fine scroll, and there you are. Much easier than understanding all this crappy counters stuff.


Quote:
Edit: The only reason I'm so vocal about seeing it this way is because everyone needs to know that the highest nybble of what you write to $2006 has an effect; you cannot just use the address for a nametable tile without subtracting $2000 first.

Of course you can !
It's just it will affect the fine scroll.

by on (#94971)
Two different people interpreting things two different ways, I suppose.