Drawing ahead when scrolling up

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

Post Reply
charlielaw1
Posts: 7
Joined: Sat Jun 13, 2020 12:57 pm
Location: Indiana

Drawing ahead when scrolling up

Post by charlielaw1 » Sat Jun 20, 2020 8:14 am

I'm working on drawing a row of background ahead as I scroll so that I can expand my background beyond 2 nametables worth of data. I understand that when scrolling right or down, you want to draw to the same number row in the opposite nametable. When scrolling up or left, you should draw to the next (lower) row in the same nametable (except when the scroll register is at 0), correct? I have tried to adapt the code from the Nerdy Nights scrolling tutorial to do this, but I can't get it to work properly. When anything is drawn, it is several rows ahead of the screen. It is only drawing to row 0 or row 1 of the nametables, and it is only drawing tiles from one specific row of my background data. Sometimes it draws an entire row, and other times it draws to two disconnected tiles in the row.

Code: Select all

DrawNewRow:
  LDA scrollv
  LSR A
  LSR A
  LSR A
  SEC
  SBC #$01
  STA RowLow
  LDA nametable
  ASL A
  ASL A
  ASL A
  CLC
  ADC #$20
  STA RowHigh
  LDA RowNumber
  ASL A
  ASL A
  ASL A
  ASL A
  ASL A
  STA SourceLow
  LDA RowNumber
  LSR A
  LSR A
  LSR A
  STA SourceHigh
  LDA SourceLow
  CLC
  ADC #LOW(background)
  STA SourceLow
  LDA SourceHigh
  ADC #hIGH(background)
  STA SourceHigh
DrawRow:
  LDA $2002
  LDA RowHigh
  STA $2006
  LDA RowLow
  STA $2006
  LDX #$20
  LDY #$00
DrawRowLoop:
  LDA [SourceLow],y
  STA $2007
  INY
  DEX
  BNE DrawRowLoop
  
  RTS

charlielaw1
Posts: 7
Joined: Sat Jun 13, 2020 12:57 pm
Location: Indiana

Re: Drawing ahead when scrolling up

Post by charlielaw1 » Sun Jun 28, 2020 5:39 am

I updated my map so that I have fewer tiles that are all a single color. I figured this would give me a better idea of what is going on, since I'm not updating the attributes along with drawing the rows yet. It is still only drawing to the top 2 rows of of the background, and well ahead of the top of the visible screen. It is also only drawing the 2 tiles from the right hand side of the map, and drawing them repeatedly across rows 0 and 1 until row 1 is filled with what should the 2nd tile from the right. In row 0 the tile that should be at the far right keeps moving across the screen as the game draws.
This code should fetch a different tile for each draw, shouldn't it? I'm lost on what I'm doing wrong for both the source and the placement of the tiles to be off.

Code: Select all

LDA [SourceLow],y
  STA $2007
  INY
  DEX
  BNE DrawRowLoop

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

Re: Drawing ahead when scrolling up

Post by tokumaru » Sun Jun 28, 2020 8:40 am

When scrolling in both axes you really have to take the name table mirroring into consideration. If you're using only the two built-in babe name tables, you're left with no off-screen space to hide the scroll seam in one of the axes, so you have to figure out how you're handling that.

Most games go with vertical mirroring (name tables arranged horizontally), which results in clean horizontal scrolling, and try to hide the vertical scroll seam in the overscan area (with varying degrees of success, since different TVs have different amounts of overscan).

Anyway, the name table layout you pick will affect the calculation of the target addresses for rows and columns of tiles when scrolling. And don't forget the tile attributes, those can be a pain to handle if you're not careful with how you design your scrolling engine.

Another thing to consider is that your VRAM update loop is very slow: it has an indirect memory access, 2 counter/index updates and a branch. The amount of data that can be updated with code like that is pretty small, so make sure that your updates are not taking longer than vblank. You may want to consider unrolling that loop, partially or completely, in order to considerably increase the amount of data you can transfer to VRAM each frame.

EDIT: stupid phone keyboards...
Last edited by tokumaru on Mon Jun 29, 2020 4:53 am, edited 1 time in total.

calima
Posts: 1130
Joined: Tue Oct 06, 2015 10:16 am

Re: Drawing ahead when scrolling up

Post by calima » Sun Jun 28, 2020 11:29 pm

built-in babe tables <3

charlielaw1
Posts: 7
Joined: Sat Jun 13, 2020 12:57 pm
Location: Indiana

Re: Drawing ahead when scrolling up

Post by charlielaw1 » Mon Jun 29, 2020 10:55 am

Thanks tokumaru. Yeah, I'm only scrolling vertically and using horizontal nametables. Sorry, "expanding my background beyond 2 nametables," I meant creating a level that is more than 2 screens high. I'm really making a mess of these posts, both too long and not clear. I appreciate the help.

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

Re: Drawing ahead when scrolling up

Post by tokumaru » Mon Jun 29, 2020 1:04 pm

Oh, so you're using horizontal mirroring (name tables arranged vertically) and only scrolling vertically? That makes things easier, but still more complicated than pure horizontal scrolling due to the height of the name tables not being a power of two.

Ok, let's think about this. The screen is 30 tiles tall, but since the scroll can be between tiles, you need to have a clean image that's 31 tiles tall at all times. That's not a problem since you're working with an area that's 60 tiles high (due to the horizontal mirroring).

The first thing you need to do is to detect WHEN to draw a new row of tiles. That would be when the camera crosses a tile boundary (i.e. every 8 scrolled pixels). In order to detect that, you need to backup the scroll position before changing it, and then compare the new value with the old one. You can do this by EOR'ing both values:

Code: Select all

lda ScroolY
eor OldScrollY
and #%11111000
beq SkipUpdate
EOR outputs 1 if the two input bits are different, and we need to know whether the tile row changed when the scroll changed, so we use that. Then we isolate the bits that matter (only the tile row matters - the fine scroll within the tile can change without triggering an update) and they will tell us whether a name table update is needed: if the result is 0, no bits changed, so no update is necessary, otherwise we DO need to draw a new row.

The next step is to determine the SOURCE address of the new tiles in the level map, and the TARGET address for them in the name tables. These can be calculated from the camera's coordinates and from the scroll value, respectively. If your game scrolls, it should have a virtual camera indicating which part of the map is currently visible. This is normally a 16-bit variable, since 8 bits can only count the pixels of a single screen. And you also need a corresponding value to tie the camera's position to the name table position where the image "captured" by the camera will be displayed. This is the scroll value that you write to PPU registers $2005/$2000. Do note that on the NES, the scroll is actually 9 bits long, where the lower 8 bits go to $2005 and the 9th bit goes to $2000 (i.e. the "name table" bits).

Anyway, horizontal scrolling is simpler because the camera's position can be the same variable as the scroll position, because both wrap around at 256. But for vertical scrolling, the camera's position will usually wrap around at 256, as normal, but the scroll position will wrap around at 240, since that's how tall name tables are. Different programmers deal with this differently, some will even weirdly pretend that entire rows of their level maps don't exist, since skipping those rows allows them to perfectly sync the camera with the scroll, but that comes with it's own set of problems (collision detection for example becomes HELL). Others will divide the camera's position by 240 in order to calculate the scroll position on the fly every frame, but divisions are troublesome in 6502 assembly. I personally prefer to maintain two separate variables, one for the camera and one for the scroll, and always update them by the same amounts, so that they're keep in sync but one moves in label map space (where coordinates wrap around at 256) and the other in name table space (where coordinates wrap around at 240).

The camera's position is the one you use to calculate the SOURCE address for the tiles in ROM or RAM. The actual calculation depends on HOW your levels are stored, and what kind of compression they use, but assuming you're using raw uncompressed name table data, the formula would be Source = CameraY / 8 * 32, since each row is 8 pixels high and 32 tiles wide. CameraX is always 0 (no horizontal scroll whatsoever), so it doesn't affect this calculation at all. Anyway, that's when scrolling UP, and can be optimized to Source = (CameraY & %1111111111111000) << 2:

Code: Select all

lda CameraY+0
and #%11111000
sta Source+0
lda CameraY+1
asl Source+0
rol a
asl Source+0
rol a
sta Source+1
When scrolling down, you need to use the coordinate that's 240 pixels down, because that's how tall the screen (i.e. the area covered by the camera) is: Source = ((CameraY + 240) & %11111111 11111000) << 2:

Code: Select all

clc
lda CameraY+0
adc #240
and #%11111000
sta Source+0
lda CameraY+1
adc #$00
asl Source+0
rol a
asl Source+0
rol a
sta Source+1
Now you have to calculate the TARGET address for the tiles in the name tables. Name table addresses have the following layout:

Code: Select all

0010BAYY YYYXXXXX
B: name table Y;
A: name table X;
YYYYY: tile Y;
XXXXX: tile X;
So you simply have to take your ScrollY variable and put the bits in the right places:

Code: Select all

lda ScrollY+0
and #%11111000
sta Target+0
lda ScrollY+1
lsr a
lda #%10
rol a
asl a
asl Target+0
rol a
asl Target+0
rol a
sta Target+1
This is for scrolling up. When scrolling down you have to add 240 again, but since ScrollY wraps at 240, adding 240 consists simply in flipping the NT bit:

Code: Select all

lda ScrollY+0
and #%11111000
sta Target+0
lda ScrollY+1
eor #%1 ;<- this flips the bit
lsr a
lda #%10
rol a
asl a
asl Target+0
rol a
asl Target+0
rol a
sta Target+1
Now that you have both "Source" and "Target" calculated, you can do the data transfer during vblank as usual:

Code: Select all

  lda Target+1
  sta $2006
  lda Target+0
  sta $2006
  ldx #32
  ldy #$00
TransferByte:
  lda (Source), y
  sta $2007
  iny
  dex
  bne TransferByte
Keep in mind that this is all very simplified, since we're talking about uncompressed data and we're avoiding the subject of attribute tables altogether. Due to how the attribute tables work (each pair of attribute bits affect a 16x16-pixel area), some people like to work in increments of 2 tiles (or even 4, like many Capcom games), instead of 1 tile like we're doing here.
Last edited by tokumaru on Tue Jun 30, 2020 10:29 am, edited 1 time in total.

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

Re: Drawing ahead when scrolling up

Post by tokumaru » Tue Jun 30, 2020 7:44 am

A couple of things I forgot to mention:

The code I posted for calculating the source address is handling just the offset from the beginning of the map. To get the actual pointer you have to add the base address of the map. The full calculation could look something like this (when scrolling UP):

Code: Select all

lda CameraY+1
sta Source+1
lda CameraY+0
and #%11111000
asl a
rol Source+1
asl a
rol Source+1
adc Background+0
sta Source+0
lda Source+1
adc Background+1
sta Source+1
Do note that the base address for the map is being loaded from a 16-bit variable, rather than directly from a label via #LOW() and #HIGH(), so that this code can be used with multiple maps. Just put the address of the map you want to use in the variable "Background".

And this would be for scrolling DOWN:

Code: Select all

clc
lda CameraY+0
adc #240
sta Source+0
lda CameraY+1
adc #$00
sta Source+1
lda Source+0
and #%11111000
asl a
rol Source+1
asl a
rol Source+1
adc Background+0
sta Source+0
lda Source+1
adc Background+1
sta Source+1
The other thing I forgot to mention is that all the preparation (moving the camera, detecting the need for an update and calculating the pointers) should be done in advance, and only the data transfer itself should happen during vblank. The vertical blank period is very short and you really don't want to waste any of it with code that's not directly manipulating the PPU.

One last thing is that I didn't use NESASM syntax when using indirection (NESASM uses square brackets for indirection, while all other 6502 assemblers I'm aware of use parenthesis), so watch out for that.

Post Reply