This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

Drawing row regarding future scrolling

Drawing row regarding future scrolling
by on (#70755)
Hello everyone. I'm just a new member, and I read lost of posts.

I need your help about method, I know how to do, but I don't know what is the most optimized method.

It's just about drawing one row per NMI event, that's for begin able to do scrolling.
For the moment, I use a Buffer of 30 bytes in RAM which represents 30 tiles of a PAL row.
This is part of my code :
Code:
   DrawPPURow:
      lda PPU_DoDrawBuffer   ; Need to Draw a Row ?
      beq Return_DrawPPURow   ; If PPU_DoDrawBuffer=0 dont draw a row and goto Return_DrawPPURow
      
      lda #$20
      sta PPUADDR
      lda #$00
      sta PPUADDR
      
      sta $FF         ; DEBUG = just for read write at $FF to know when execute this
      
      ldx #00
      -
      lda Buffer_Data,x
      sta $2007
      inx
      cpx #30
      bne -
      
      dec PPU_DoDrawBuffer   ; Drawing is made

      Return_DrawPPURow:



I have seen in the SBM sources at ROOMHACKING, and I expected it use vectors for drawing ? (I'm not sure of this, because I think I saw CHR data in RAM)
Also I see our great post : Scrollin'

My problem is regarding using vector or PPUADDR addressing like you do. (I think it will be useless, because it keep free lots of RAM spaces)

But how clear a row? Or how clear parts where there is nothing ?
With buffer it is easy to do it (just clear the buffer).
But with addressing, for an already written row, how you do it ?

Maybe I can use 1 NMI event for clear the entire row, and 1 NMI event to draw my CHRs datas (using addressing).

Thanks for you Helps, I will continue my search.
Re: Drawing row regarding future scrolling
by on (#70758)
Raccoon wrote:
I know how to do, but I don't know what is the most optimized method.

Since you don't have a lot of data to update, don't worry about the "optimal" way yet, just get it working however you can.

Quote:
For the moment, I use a Buffer of 30 bytes in RAM which represents 30 tiles of a PAL row.

Don't you mean "column" instead of "row"? A row is horizontal, and there are 32 tiles in a name table row, columns are vertical, and there are 30 tiles in each one. The numbers are the same for PAL and NTSC by the way, there is no difference regarding screen sizes.

Quote:
This is part of my code :

The problem with your code is that it always writes to the address $2000, which is the first column/row of the screen. As the screen scrolls you have to draw your column/row to other addresses, the one that is near the edge of the screen.

Quote:
Code:
      lda Buffer_Data,x
      sta $2007
      inx
      cpx #30
      bne -

A simple optimization that saves a good amount of time is counting down instead of up. You'd load X with #$29 instead of #$00 and DEX instead of INX. This way, after you have copied the byte at index 0, the 0 will decrement to $FF (255) and set the N flag (because 255 is the same as -1), so you can use BPL instead of BNE and get rid of the CPX, which is why you save time. You just have to fill your buffer backwards as well.

In 6502 assembly, whenever possible we count down instead of up, because you can detect when indexes reach 0 or -1 without having to compare anything.

Quote:
I expected it use vectors for drawing ? (I'm not sure of this, because I think I saw CHR data in RAM)

I have no idea why you are talking about vectors and CHR-RAM, and I don't see how that has anything to do with scrolling.

Quote:
But how clear a row? Or how clear parts where there is nothing ?

You can just overwrite name table data, there is no need to clear anything first.

What you have to do is calculate the correct PPU address for the row/column you want to update, based on the scrolling position. If you are drawing columns, the first one begins at $2000, the second at $2001, the third at $2002, and so on, until $201F, the 32nd column. For rows, the first one begins at $2000, the second at $2020, the third at $2040, and so on, until $23A0, the 30th row.

Also note that depending on the mirroring, you will also have to draw columns/rows to the other name table, the one at $2400.

by on (#70760)
Quote:
Since you don't have a lot of data to update


That's right for the moment. I was thinking that I can do one column drawing during VblankTime. This must happen when scroll !

Quote:
Don't you mean "column" instead of "row"?


Ho sorry for my understanding, it's a bad french translation from my part. It's ok, I mean "column". Thanks to point this :)

Quote:
The problem with your code is that it always writes to the address $2000


Yes, I know, it will be just for test redraw at the same column. I want to implement probably an indirect addressing.

Quote:
A simple optimization that saves a good amount of time is counting down instead of up.


I know but this reverse my buffer no ?:

Code:
ldx #29
-
lda Buffer_Data,x          ; first time read the 29° byte of the buffer
sta $2007
dex
bne -


Maybe you thinking I will reverse my buffer during it's build function.

Ho sorry I don't see you say this :
Quote:
You just have to fill your buffer backwards as well.



Quote:
I have no idea why you are talking about vectors


It's because , i was thinking a vector is for example:

Code:
Block : .byte $AB, $CH


$AB: the n line position of my block ( 1 -> 15 )
$CH: the CHR byte to write

It's not clear (and not optimized like this), but you can imagine, it's like a command line I will send in my NMI PPU draw function.


Quote:
You can just overwrite name table data, there is no need to clear anything first.


Yest I know, but I was thinking without an buffer. But you said me : "just get it working however you can."
I realize that Dwedit use buffer in his thread : Working on a new game for that compo


Thanks for your time.

by on (#70761)
Quote:
Maybe you thinking I will reverse my buffer during it's build function.

Either that, or you can offset your counter so that it ends at zero when counting upward (i.e. start at 256-30), and offset your base address accordingly. I don't remember whether the 6502 has a penalty for crossing a page boundary with an absolute-indexed address, so this may or may not be a good idea.

by on (#70768)
For sure, it's a good idea. I will simply use a backwards buffer.

Thanks for your helps

by on (#70771)
mic_ wrote:
Either that, or you can offset your counter so that it ends at zero when counting upward (i.e. start at 256-30), and offset your base address accordingly.

Yes, you can do that too, but there is a cycle penalty if you cross a page while reading the data. Instead of the Z flag you can use the N flag to detect the end of the buffer, so you start at 128-30 instead, reducing the chances of crossing a page. The code would look like this:

Code:
   ldx #(128-30)
-
   lda Buffer-(128-30), x
   sta $2007
   inx
   bpl -

As long as the buffer is smaller than 129 bytes and starts after 128-30 in a memory page, that will not cause a cycle penalty. This way the buffer doesn't need to be reversed.

Raccoon wrote:
I was thinking that I can do one column drawing during VblankTime. This must happen when scroll !

Yes, all updates must be done during VBlank, and by using this type of basic loop you can transfer about 150 bytes to the PPU per VBlank. Since a column is only 30 bytes, there's still a lot of time left. Remember that you also have to use that time to update the sprites (a sprite DMA takes 513 cycles plus the time needed to trigger it) and the attribute tables (so that your tiles use the correct palettes).

Quote:
I want to implement probably an indirect addressing.

If by "indirect addressing" you mean something like "STA $2007, X", that's not going work. What you have to do is calculate where in the PPU your new column will start, and when the time comes write that to $2006 before copying the column. Like I said in my last message, the first column starts at $2000, the second at $2001, the third at $2002, and so on.

Quote:
Code:
ldx #29
-
lda Buffer_Data,x          ; first time read the 29° byte of the buffer
sta $2007
dex
bne -

This is almost correct... the only problem is that it will not copy the byte at position 0, because of the BNE you used at the end. Change that to BPL and it will work, because the loop will only finish when the index reaches 255, after you've copied the byte at position 0.

Quote:
It's because , i was thinking a vector is for example:

Code:
Block : .byte $AB, $CH


$AB: the n line position of my block ( 1 -> 15 )
$CH: the CHR byte to write

Sorry, I still can't see the purpose of this. What is this position that ranges between 1 and 15?

Quote:
Yest I know, but I was thinking without an buffer.

Without a buffer? Do you mean copying data directly from the level map to the screen? That's not a good idea, because decoding data from the map usually takes time, and you can't spend your precious VBlank time doing that kind of stuff. The rule is that you do all the reading/decoding/converting outside of VBlank and store all the results into buffers, so that when the time comes you can just copy that data to the PPU as fast as you can.

by on (#70776)
Sure it will help me.
It's true reverse my buffer need just more time like this (it isn't in my NMI loop):
Code:
      ; Buffer_Shift = Line position

      ; Fix for backwards write
      lda #30      ; load one column
      sec               ; set the carry
      sbc Buffer_Shift      ; subtract Buffer_Shift
      sta Buffer_Shift   ; save the low byte



I try it to offset base address, but don't know how do this.
Thanks for the code, and good explains.

Quote:
This is almost correct... the only problem is that it will not copy the byte at position 0


Yes I realize it, i have try this, there just one minute last.
I read my txt doc 6502guid about BPL.

Quote:
If by "indirect addressing" you mean something like "STA $2007, X"


No I mean exactly "Indirect Indexed Addressing", like this :
lda (PPU_Address),y
But it doesn't work or it will be complicated for nothing.
I just need to calculate PPU_Address_low and PPU_Address_high (you answer me thanks)

by on (#70780)
Raccoon wrote:
I just need to calculate PPU_Address_low and PPU_Address_high (you answer me thanks)

Yes, that's what you have to do.

I can only help you if you tell me what kind of mirroring you are using, vertical (the two name tables are side by side) or horizontal (one name table on top of the other). Vertical mirroring is better when scrolling horizontally, since there are no visible glitches. If you use horizontal mirroring, there will be color glitches at the sides of the screen (like in Kirby's adventure or SMB3, for example).

I also need to know how you are updating the scroll during VBlank (I need to see the code that writes to $2000 and $2005).

by on (#70790)
For the moment, I use just MMC1 mapper with vertical mirroring.
I will do some basics test before doing all the great stuff.

Tomorrow, i show you my code regarding $2000 and $2005. (it is very late at night in France !)

Thanks for your help again :)

by on (#70831)
I continue, I am just in reflexion about reading my level table, and build my buffer. ( I think it's the hard part)


Just for show my RAM part :

Code:
;---------------------------------------------------------------------------------------
; RAM                                  
;---------------------------------------------------------------------------------------

;   ; BUFFER                           
   Buffer_Data   EQU $00         ; 60 bytes = 2 Columns of 15*2 tiles
   Buffer_Shift   EQU $3F         ; shift write data

;   ; JOYPAD                           
   Joypad_State   EQU $50      ; Hold states of each buttons

;   ; SCROLL                           
   Scroll_x    EQU $51
   Scroll_y    EQU $52
   acc       EQU $53

;   ; NAMETABLE                           
   TableData_Shift   EQU $60         ; Shift bytes in NameTableScreen
   
;   ; PPU DRAW                           
   PPU_ShiftColumn      EQU $61         ; PPU Column Position from 0 to 32
   PPU_Address      EQU $62   ; 2 BYTES   ; PPU Column Position Address
   PPU_DoDrawBuffer   EQU $64         ; To know when Buffer need to be draw

   VBlank_nCycles      EQU $6F         ; Count n Cycles for one VblankTime


My main, and my NIM:

Code:
;---------------------------------------------------------------------------------------
;MAIN LOOP                              
;---------------------------------------------------------------------------------------



MainLoop:         ; Loop Forever         

   include "Joypad.asm"
   

   WaitNextVblankTime:
      lda VBlank_nCycles
      -
      cmp VBlank_nCycles
      beq -

   jmp MainLoop      ; jump to Main_Loop

;---------------------------------------------------------------------------------------
; NMI                                 
;---------------------------------------------------------------------------------------


NMI:   ; Signal generated by PPU for a VBlank Time
   
   SaveRegisters:
      pha   ; Push A on Stack
      txa   
      pha   ; Push X on Stack
      tya
      pha   ; Push Y on Stack
      
   DrawPPURow:
      lda PPU_DoDrawBuffer   ; Need to Draw a Row ?
      beq Return_DrawPPURow   ; PPU_DoDrawBuffer=0 goto Return_DrawPPURow
      
      lda PPU_Address+1
      sta PPUADDR      ; PPUADDR = $2000
      lda PPU_Address
      sta PPUADDR
      
      sta $FF         ; DEBUG = just for read write at $FF to know when execute this
   
      ldx #(128-30)
      -
      lda Buffer_Data-(128-30), x
      sta $2007
      inx
      bpl -
      
      dec PPU_DoDrawBuffer   ; Drawing is made

      Return_DrawPPURow:
   
   
   Scrooling:      
      ldx Scroll_x
      stx $2005
      ldx #00
      stx $2005
   
   CountVBlankCycles:
      inc VBlank_nCycles   ; from 0 to 255 one VblankTime ?

   RetrieveRegisters:
      pla   
      tay   ; Pull Stack on Y
      pla   
      tax   ; Pull Stack on X
      pla   ; Pull Stack on A
      
      
   rti      ; Return from Interrupt



And just before Main Loop, my some tests wich draw only 2 colums (I use MetaTile 2x2 tiles) :


Code:
;---------------------------------------------------------------------------------------
; TEST RENDER BUFFER                              
;---------------------------------------------------------------------------------------

   jsr Render         ;draw one column x1 tile
   
      WaitNextVblankTime2:
      lda VBlank_nCycles
      -
      cmp VBlank_nCycles
      beq -
   
   jsr IncrementColumnAddress
   jsr WriteBuffer

;---------------------------------------------------------------------------------------
; RENDER                              
;---------------------------------------------------------------------------------------
   Render:

   jsr Initializes      
   jsr ReadObject
   jsr ClearBuffer
   jsr WriteBuffer

   Return_Render:
      rts


Maybe you need to see my Level Data, and my Metatiles (I name this tileSet)

Code:
; OBJECT DEFINITION
;Byte n°1    $RL = nRow, nLine      POSITION   
;Byte n°2    $TS = nTileSet of Bank      CHR      
;nColumn  Position : #0 -> 16 (32 tiles)
;nLine      Position : #0 -> 15 (30 tiles), #16 = $F = New column increment

TableData:.byte $02,$00


   ;BANK of TILESET (64 max X 4 bytes = 256 = $FF)
   BankOfTileSet:
   Plateform_TopRight   : .byte $00,$10,$01,$11      ; $00
   Plateform_TopMiddle   : .byte $02,$12,$01,$11      ; $01
   Plateform_TopLeft   : .byte $02,$12,$03,$13      ; $02
   
   Plateform_BottomRight   : .byte $10,$10,$11,$11      ; $03
   Plateform_BottomMiddle   : .byte $12,$12,$11,$11      ; $04
   Plateform_BottomLeft   : .byte $13,$13,$12,$12      ; $05


It's not really clear like this, if you want see more, I can do an entire ZIP.

I will post screenshot of my 2 columns in fews minutes:

Image

You can see the RAM getting in FCEUX.

by on (#70848)
There are a few inconsistencies in your code that are bothering be a bit. For example, in the part where yoy set the PPU address before writing tiles, there's a comment that says "PPUADDR = $2000 ". That got me confused because the PPU address register is actually $2006, and in the rest of the code you are not using registers' names you are using their addresses ($2007, $2005, etc), so you should either use only their addresses, or give them all names, making sure that they point to the correct addresses.

Another problem is that I don't see any writes to register $2000 (PPUCTRL). There should be a write to it next to the $2005 writes, because the lower 2 bits of PPUCTRL have an important part in the scrolling too. Since you are using vertical mirroring, and your name tables are arranged side by side, there will be times when you will be showing the one on the left and there will be times when you'll be showing the one on the right, and you will need to use PPUCTRL to indicate that.

About calculating the target address for your columns: you will have to use the "Scroll_x" variable for that. One important thing to change is that since your level is longer than 256 pixels, that variable must be 16-bits large. I assume you know how to handle 16-bit values.

Anyway, the tiles are 8 pixels wide, so you must detect when the camera crosses an 8 pixel barrier in order to know when to draw a new column. Look at the binary numbers and you will see that 0 is %00000000, 7 is %00000111, and 8 is %00001000. If you pay attention, you'll see that numbers 1 through 7 have the 4th bit as 0, and only when the number becomes 8 that bit changes to 1. Because of that, that is the bit that will tells us that the scroll crosses an 8 pixel barrier. The code could look something like this:

Code:
   lda Scroll_x+0 ;load the lower byte of Scroll_x
   pha ;save it to the stack

   ;UPDATE Scroll_x HERE

   pla ;get the old Scroll_x
   eor Scroll_x ;XOR it with the new Scroll_x
   and #%00001000
   beq +Skip

   ;DECODE A NEW COLUMN HERE

+Skip:


Once you have decided that a new column must be rendered, it's time to find out where in the level map this column is and where in the name tables it should be rendered to. Scroll_x will also help you with this.

Scroll_x tells you which part of the name tables should be displayed at the leftmost side of the screen, so if you you scrolled left, that's the column that has to be updated. If you scrolled right (to detect whether you scrolled right or left you have to compare the old Scroll_x to the new one and see which one is larger), you have to add 256 to Scroll_x to find which column needs updating, because the right side of the screen is 256 pixels to the right of the left side.

I don't know how your level map is stored, so it's up to you to convert Scroll_x to a format that allows you to read from it, but I can tell you about calculating the destination address in the name tables. Once you have defined the X coordinate of the column (it's either Scroll_x or Scroll_x + 256), you have to get rid of the lower 3 bits to convert it from pixel units to tile units. Then, the next 5 bits (a number between 0 and 31) will indicate what name table column to use. The next bit will tell you if you should write the column to the first name table ($2000) or to the second ($2400).

When setting the scroll, you have to write the lower byte of Scroll_x to $2005, and the first bit of the high byte to bit 0 of PPUCTRL, in order to select the name table where scrolling should start.

I know that's a lot of information, and I don't think I can make it any simpler. This stuff isn't so trivial, so if you are having a hard time with this maybe you should consider not using scrolling for your first project.

by on (#70861)
Quote:
That got me confused because the PPU address register is actually $2006
Yes, you all right it's effectively $2006. It's just an error from my part.

My code is confusing I know, that's why I reorganize it.
I remake the buffer function, for being able to draw an amount of columns. Like this I could pass for initial 32 columns for the first nametable.



Quote:
Another problem is that I don't see any writes to register $2000 (PPUCTRL)


Yes that's because, I do just initialize in the head of the program (before main loop). This refers to an separate file for subroutines:

Code:
ScreenDisplayOFF EQU #%11110111   ; With AND force ScreenDisplay    bit to 0 = DisplayOFF
DisableNMI    EQU #%01111111 ; With AND force Execute NMI    bit to 0 = DisableNMI

Register2000     EQU  #%10001100
;         76543210
;         ||||||||
;         ||||||++----$10   Name Table Select               
;         ||||||      00=$2000  10=$2800               
;         ||||||      01=$2400  11=$2C00               
;         ||||||
;         |||||+-------$2 PPU Address Read/Write Increment         
;         |||||      = 1 Increment by 1               
;         |||||      = 0 Increment by 32               
;         |||||
;         ||||+--------$3 Sprite Pattern Table Address            
;         ||||      0=$0000  1=$1000               
;         ||||
;         |||+---------$4 Screen Pattern Table Address            
;         |||      0=$0000 1=$1000                  
;         |||
;         ||+----------$5 Sprite Size                  
;         ||      0=8x8  1=8x16                  
;         ||
;         |+-----------$6 Execute NMI on Sprite Hit            
;         |      0=Disable  1=Enable               
;         |
;         +------------$7 Execute NMI on VBlank               
;               0=Disable 1=Enable               

Register2001     EQU  #%00001110
;         76543210
;         ||||||||
;         |||||||+-----$0   Color Display                  
;         |||||||      0=All colors                  
;         |||||||      1=Mono Color                  
;         |||||||
;         ||||||+------$1 Image Clip                  
;         ||||||      0 = Don't show the left 8 pixels of the screen      
;         ||||||      1 = Show the left 8 pixels            
;         ||||||
;         |||||+-------$2 Sprite Clip                  
;         |||||      0 = Don't Show sprites in the left 8-pixel column    
;         |||||      1 = Show Everywhere                
;         |||||
;         ||||+--------$3 Screen Display                  
;         ||||      0=OFF 1=ON                  
;         ||||
;         |||+---------$4 Sprite Display                   
;         |||      0=Hide 1=Show                  
;         |||
;         +++--------$765 Full Background Color               
;               000 = NONE   010 = Green            
;               001 = Red   100 = Blue            

;   PPU_CTRL   = $2000
;   PPU_MASK   = $2001
;   PPU_STATUS   = $2002
;   PPU_SCROLL   = $2005
;   PPU_ADDR   = $2006
;   PPU_DATA   = $2007



;---------------------------------------------------------------------------------------
;VBlankWait                              
;---------------------------------------------------------------------------------------
VBlankWait:            ; wait for Vblank, PPU is ready after this      
   bit PPUSTATUS      ; if %2002 = LastPaletteByte then N = 1         
   bpl VBlankWait      ; if  N = 0 goto VblankWait            
   rts         ; return form subroutine            

;---------------------------------------------------------------------------------------
;SetNameTableAt2000                           
;---------------------------------------------------------------------------------------

SetNameTable$2000:
   
   ; write Address NameTable1 > $2000
   lda #$20
   sta PPUADDR
   lda #$00
   sta PPUADDR
   
   ; fix first line place
   lda #$00
   sta PPUADDR
   rts

;---------------------------------------------------------------------------------------
;Screen Turn On                              
;---------------------------------------------------------------------------------------
DisplayScreen:
   jsr VBlankWait      ; Wait VBlank Routine   
   lda Register2000   ; execute NMI on Vblank & Sprite Pattern Table Address = $1000      
   sta PPUCTRL      ; set Control Register #1                  
        lda Register2001   ; screen display & Show sprites everywhere & Show the left 8 pixels   
   sta PPUMASK      ; set Control Register #2                  
   rts

;---------------------------------------------------------------------------------------
;DisableScreen                              
;---------------------------------------------------------------------------------------
DisableScreen:         ;DisableDisplay needed before writing NameTable

   lda DisableNMI      ; Bit Force to DisableNMI
   and Register2000
   sta PPUCTRL

   lda ScreenDisplayOFF   ; Bit Force to ScreenDisplayOFF   
   and Register2001
   sta PPUMASK
   rts

;---------------------------------------------------------------------------------------
;WaitNextVblankTime                           
;---------------------------------------------------------------------------------------

WaitNextVblankTime:

   lda VBlank_nCycles
   -
   cmp VBlank_nCycles
   beq -
      
   rts



Quote:
since your level is longer than 256 pixels, that variable must be 16-bits large. I assume you know how to handle 16-bit values.

Yes I will use it !

Quote:
Anyway, the tiles are 8 pixels wide, so you must detect when the camera crosses an 8 pixel barrier in order to know when to draw a new column.

Hum I think of that a little. Yes, good tips to use the 3th bit detect to see when the number becomes 8. I will use same thing for 31 to swap nametables, yes it's ok, I understand.

Thanks for the code

Code:
so if you are having a hard time with this maybe you should consider not using scrolling for your first project.


No, even if that take some times to do, I want do that.
And I will thanks you again to help, for sure I will share my entire scrolling system with other when it will work!

It's not my real first project, I have made some stuff before.
I have made an drawing system which build large objects: sorts of Meta-tiles where I can chose predefined parts sizes for start, "repeated" middle, end parts. With a "level" table of different objects positions, and sizes, it will calculate all tiles and render. It's difficult to explain, but this is not optimized for scrolling and work for only one nametable.