Metatile troubles

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Metatile troubles
by on (#118132)
I've tried several attempts at getting a working metatile loader thrown together with very little success. Anyway, I have this routine here that should be taking the contents of a map data string and copying what should go into the nametable into RAM. I've tried many methods for this and the current is what I like best because it's sleek and nice looking. Anyway, I've tried debugging it and I'm still unsure where it's happening, but it seems like something is getting overwritten because when I ran the program it's showing the signs of going into an infinite loop.

Here is the routine:
Code:
Scratch .rs 2         ;reserve 2 bytes for scratch
Buffer  .rs 64         ;reserve 64 bytes for level decompression
MetaTileData_Ptr  .rs 2       ;2 bytes for the pointer currently proccessed metatile
ColumnNumX_Ptr     .rs 2      ;2 bytes for column number
ColumnNumY_Ptr     .rs 2      ;2 bytes for column number
[...]
;$00
InitMap:
 LDY #$00
 LDA #$03      ;Pos of buffer
 STA ColumnNumX_Ptr
 LDA ColumnNumX_Ptr+32
 STA ColumnNumY_Ptr
LoadMap:
 LDA LevelData,y
 STY Scratch
 JSR WriteTileToBuffer   ;a = metatilenumber to process
 LDY Scratch
 INY
 CPY #$15
 BNE LoadMap
 RTS

WriteTileToBuffer:
 LDX MT_Table,y
 STX MetaTileData_Ptr
 LDA #HIGH(MT_Table)
 STA MetaTileData_Ptr+1
 LDY #$00         ;start at 0
 LDX #$00
 LDA [MetaTileData_Ptr],y   ;do 1
 STA [ColumnNumX_Ptr,x]
 INY
 INX
 LDA [MetaTileData_Ptr],y   ;do 2
 STA [ColumnNumX_Ptr,x]
 INY
 INX
 LDA [MetaTileData_Ptr],y   ;do 3
 STA [ColumnNumY_Ptr,x]
 INY
 INX
 LDA [MetaTileData_Ptr],y   ;and finally the 4th byte
 STA [ColumnNumY_Ptr,x]
 LDA ColumnNumX_Ptr
 INC A            ;\ We wrote 4 bytes so increase the ptrs accordingly
 INC A            ; |
 STA ColumnNumX_Ptr      ; |
 LDA ColumnNumY_Ptr      ; |
 INC A            ; |
 INC A            ;/
 STA ColumnNumY_Ptr
 RTS



Here are my tables:
Code:
MT_Table:
 .db LOW(MetaTileBlock00),LOW(MetaTileBlock01),LOW(MetaTileBlock02),LOW(MetaTileBlock03)

MetaTileBlock00:
  .db $23,$23,$23,$23
MetaTileBlock01:
  .db $20,$21,$30,$31
MetaTileBlock02:
  .db $22,$23,$32,$33
MetaTileBlock03:
  .db $01,$01,$01,$01
LevelData:
  .db $02,$02,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
Re: Metatile troubles
by on (#118133)
Quote:
Code:
 LDA ColumnNumX_Ptr
 INC A            ;\ We wrote 4 bytes so increase the ptrs accordingly
 INC A            ; |
 STA ColumnNumX_Ptr      ; |
 LDA ColumnNumY_Ptr      ; |
 INC A            ; |
 INC A            ;/
 STA ColumnNumY_Ptr

I can tell you one thing: there is no INC A on the 6502 ("A" here is obviously a reference to the accumulator, not a variable or memory location that you've labelled "A") The assembler you're using is letting you use 65c02 or newer opcodes; the 6502 does not have a way to increment the accumulator aside from clc ; adc #1 (or in this case, you'd want to use clc ; adc #2 since you're wanting to add 2 to something).

Your INC A is probably being assembled into opcode $1a, which is an undefined opcode on the 6502, and is probably causing you chaos.

Get yourself an assembler that adheres strictly to 6502. :-)
Re: Metatile troubles
by on (#118136)
Okay, that makes sense. However, it is producing different results but still seems to go into an infinite loop. Anyone know why? :shock:
Re: Metatile troubles
by on (#118138)
koitsu wrote:
INC A is probably being assembled into opcode $1a, which is an undefined opcode on the 6502, and is probably causing you chaos.


Yes, executing undefined instructions is really bad... probably the cause of your infinite loop.
Re: Metatile troubles
by on (#118141)
You appear to be doing unnecessary complex pointer operations, and making lots of mistakes along the way. For example, I don't see you ever setting the high bytes of ColumnNumX_Ptr or ColumnNumY_Ptr, so when you use those pointers you'll hardly get the addresses you expect. Also, you have the index of the metatile you want to draw in A when you call WriteTileToBuffer, but then you trash it with the LDA #HIGH(MT_Table) command.

Still, errors aside, the solution you're trying to come up with is way more complicated than it should be. Is there any reason why you can't copy your tiles like this:
Code:
WriteTileToBuffer:
   tay
   lda MetaTileBlock00, y
   sta Buffer+0, x
   lda MetaTileBlock01, y
   sta Buffer+1, x
   lda MetaTileBlock02, y
   sta Buffer+32, x
   lda MetaTileBlock03, y
   sta Buffer+33, x
   inx
   inx
   rts

You don't need pointers unless you have more than 256 types of metatiles throughout the game, but even if you do you can use this exact same code, except you'd use indirect addressing instead of absolute addressing for accessing the 4 corners of the metatile (you have to setup the 4 pointers whenever the tileset changes, which is usually between levels):
Code:
WriteTileToBuffer:
   tay
   lda (MetaTileTopLeft), y
   sta Buffer+0, x
   lda (MetaTileTopRight), y
   sta Buffer+1, x
   lda (MetaTileBottomLeft), y
   sta Buffer+32, x
   lda (MetaTileBottomRight), y
   sta Buffer+33, x
   inx
   inx
   rts
Re: Metatile troubles
by on (#118143)
Quote:
,there any reason why you can't copy your tiles like this:

Yes. I'm not sure if I understand your code there. It's using the labels I've made just for readability. wouldn't that be hardcoding them? What I'm trying to say here is how would I use your code but instead of the "block 01" "block 02" ect loading make it load from the beginning of a long set of metatile data?

edit: also, using your method is only viable for one single metatile right? because there's nothing to stop the next jsr to that routine to keep overwriting itself
Re: Metatile troubles
by on (#118145)
Code:
 
STA [ColumnNumY_Ptr,x]


I doubt this is doing what you want, this is not the same as [pointer],y. There is nowhere in this code that needs pointers. Maybe if your metatile definitions were various sizes, you would need a pointer table to find the beginning of the metatile data.

You can leave your data as is and multiply the metatile(0 to 3) to by 4 to find the start index of the relevant data or arrange it so all top left, top right etc are together: (all $23 cause I'm lazy)

Code:
MetaTileBlock00: .db $23,$23,$23,$23 ; all top left
MetaTileBlock01: .db $23,$23,$23,$23 ; all top right
MetaTileBlock02: .db $23,$23,$23,$23 ; all bottom left
MetaTileBlock03: .db $23,$23,$23,$23 ; all bottom right


Then look at tokumaru's code again.
Re: Metatile troubles
by on (#118146)
zkip wrote:
Quote:
,there any reason why you can't copy your tiles like this:

What I'm trying to say here is how would I use your code but instead of the "block 01" "block 02" ect loading make it load from the beginning of a long set of metatile data?

Edited because Kasumi is very silly... :oops:
By changing the value in A.
This writes tile $00.
Code:
     lda #$00
     jsr WriteTileToBuffer

Code:
     lda #$0F
     jsr WriteTileToBuffer

This writes tile $0F.

I'm not sure what you mean that that will only write a single metatile. It will, but you put it in a loop to load the whole map. Something like:
Code:
levelloop:
     sty scratch
     lda leveldata,y
     jsr WriteTileToBuffer
     ldy scratch
     iny
     dec tilesleft
     bpl levelloop


I'm not sure what you mean by "there's nothing to stop the next jsr to that routine to keep overwriting itself".

edit2: Also, I probably misunderstood your first question:

Quote:
What I'm trying to say here is how would I use your code but instead of the "block 01" "block 02" ect loading make it load from the beginning of a long set of metatile data?


Currently, Tokumaru suggests this:
Code:
WriteTileToBuffer:
   tay
   lda MetaTileBlock00, y
   sta Buffer+0, x
   lda MetaTileBlock01, y
   sta Buffer+1, x
   lda MetaTileBlock02, y
   sta Buffer+32, x
   lda MetaTileBlock03, y
   sta Buffer+33, x
   inx
   inx
   rts

For the table you have:
Code:
MetaTileBlock00:
  .db $23,$23,$23,$23
MetaTileBlock01:
  .db $20,$21,$30,$31
MetaTileBlock02:
  .db $22,$23,$32,$33
MetaTileBlock03:
  .db $01,$01,$01,$01


If you needed more sets, he's suggesting you do this:
Code:
WriteTileToBuffer:
   tay
   lda (MetaTileTopLeft), y
   sta Buffer+0, x
   lda (MetaTileTopRight), y
   sta Buffer+1, x
   lda (MetaTileBottomLeft), y
   sta Buffer+32, x
   lda (MetaTileBottomRight), y
   sta Buffer+33, x
   inx
   inx
   rts


Say these are your tables:
Code:
;Yeah, they're all the same. Pretend they're different
;And long!
GrasslandsMetaTileBlock00:
  .db $23,$23,$23,$23
GrasslandsMetaTileBlock01:
  .db $20,$21,$30,$31
GrasslandsMetaTileBlock02:
  .db $22,$23,$32,$33
GrasslandsMetaTileBlock03:
  .db $01,$01,$01,$01

WaterlevelMetaTileBlock00:
  .db $23,$23,$23,$23

WaterlevelMetaTileBlock01:
  .db $20,$21,$30,$31

WaterlevelMetaTileBlock02:
  .db $22,$23,$32,$33

WaterlevelMetaTileBlock03:
  .db $01,$01,$01,$01


To use indirect code, MetaTileTopLeft etc now refer to RAM rather than ROM.
Code:
;MetaTileTopLeft (and MetaTileTopLeft+1) stores a 16bit address.

;To make it load from the grasslands set:
   lda #low(GrasslandsMetaTileBlock00);Not sure the syntax on your assembler
;But this loads the low byte of the address GrasslandsMetaTileBlock00
;Begins at
   sta MetatileTopLeft

   lda #high(GrasslandMetaTileBlock00);Same for High byte
  sta MetaTileTopLeft+1
;Do the same for the other 3 blocks

From there
Code:
   lda (MetaTileTopLeft), y
   sta Buffer+0, x

Would load the Grasslands data as you'd expect. If you write the address for the waterlevel block to MetatileTopleft etc, that same code will load the waterlevel data.
Re: Metatile troubles
by on (#118147)
Kasumi wrote:
This writes tile $00.
Code:
     ldy #$00
     jsr WriteTileToBuffer

Code:
     ldy #$0F
     jsr WriteTileToBuffer

This writes tile $0F.
How does WriteTileToBuffer write tile $0f? The first instruction is tay... that would copy the accumulator into y overwriting y. Right?
Re: Metatile troubles
by on (#118148)
That's true. My bad, so you'd load A with the tile you want instead of Y.
Re: Metatile troubles
by on (#118149)
Man this is confusing lol..... I guess I should have worded that better. I don't have separate banks of metatiles. Just one. No separate tileset..just the one. What i don't understand is in your code your lda'ing with all of the block labels. why? I was under the assumption that all one needed to do was supply the starting address of the metatiles data.

edit: say those labels weren't there. Could I just make a big table of the metatile data and make just one label?
Re: Metatile troubles
by on (#118150)
zkip wrote:
Yes. I'm not sure if I understand your code there.

Fair enough, you shouldn't use code you don't understand (don't ever copy/paste chunks of code together hoping they'll work if you don't understand the purpose of each chunk), but that's also not a reason to go for a solution way too more complex for your problem, specially if it's getting out of hand like in this case.

Quote:
It's using the labels I've made just for readability.

Not at all. The subroutine I wrote is indeed not a direct replacement of yours, but it's close enough. The only thing missing is the initialization of the X register, which has to be set to 0 before the decoding loop begins (since there is no pointer being updated anymore). Something like this (adding to what kasumi wrote):
Code:
   ldx #$00 ;star filling the buffer from 0
levelloop:
   sty scratch ;backup Y
   lda leveldata, y ;get the index of the metatile
   jsr WriteTileToBuffer ;decompress it to the buffer
   ldy scratch ;restore Y
   iny ;go to the next metatile
   cpx #$20 ;is the buffer full yet?
   bne levelloop ;if not, get another metatile


Quote:
wouldn't that be hardcoding them? What I'm trying to say here is how would I use your code but instead of the "block 01" "block 02" ect

With a loop similar to the above, like Kasumi said.

Quote:
edit: also, using your method is only viable for one single metatile right? because there's nothing to stop the next jsr to that routine to keep overwriting itself

The value in X is kept between calls to the function, so nothing is overwritten.

zkip wrote:
Man this is confusing lol..... I guess I should have worded that better. I don't have separate banks of metatiles. Just one. No separate tileset..just the one.

Then forget about pointers altogether, you really don't need them for this.

Quote:
What i don't understand is in your code your lda'ing with all of the block labels. why?

Because you need to load the four corners of the metatile, so we have one LDA instruction for each corner.

Quote:
I was under the assumption that all one needed to do was supply the starting address of the metatiles data.

Not necessary unless you have more than 256 metatiles. The index register (n this case, Y) can address up to 256 values, so if it contains #$05 and you do LDA MetaTileBlock00, y, that means "get the top left tile of metatile number $05". That will work up until metatile number 255, after that you'll need pointers.

You will maybe need pointers to read from your level map though, unless they're all < 256 metatiles (which could be the case if there's no scrolling, since you need 240 metatiles to fill the whole screen).
Re: Metatile troubles
by on (#118153)
Please forgive my abundance of question asking, and my un-ability to grasp this concept, but I still don't get it. :E

Quote:
The index register (n this case, Y) can address up to 256 values, so if it contains #$05 and you do LDA MetaTileBlock00, y, that means "get the top left tile of metatile number $05". That will work up until metatile number 255, after that you'll need pointers.


I understand this, but the way the labels are being used is confusing me. Say I've got 16 metatiles instead of the 4 we have now. MetaTileBlock00-MetaTileBlock10 in order. Now what? Would the routine still be effective? If so, why are only the first 4 corner offsets loaded? (edit: I know 4 tiles make up a 16x16 tile, but why load the first 4 labels instead of say the last 4 or middle 4?)
Re: Metatile troubles
by on (#118155)
zkip wrote:
I understand this, but the way the labels are being used is confusing me. Say I've got 16 metatiles instead of the 4 we have now. MetaTileBlock00-MetaTileBlock10 in order. Now what?

Oh, now I get why you're confused! Each label in your table represents a metatile, I assumed that each label represented a corner of the metatiles... The code I posted assumed the data was arranged like this:

Code:
TopLeftTile: .db $00, $01, $02, $03, $04, $05, (...)
TopRightTile: .db $20, $21, $22, $23, $24, $25, (...)
BottomLeftTile: .db $60, $61, $62, $63, $64, $65, (...)
BottomRightTile: .db $80, $81, $82, $83, $84, $85, (...)

Which means that metatile #0 is made of the following tiles: $00, $20, $60, $80. I often arrange data like this because the 6502 is better at reading structures of arrays rather than arrays of structures. In this case, even if you have 200 tiles or more you'll still have only 4 labels in the code, because they represent the corners, and all metatiles have 4 corners.

You can do it your way and store the metatiles linearly, but then you can only have 64 of them before you need to use pointers. To read linear metatiles, my routine would look like this:
Code:
WriteTileToBuffer:
   asl ;multiply by 4 because each metatile uses 4 bytes
   asl
   tay ;use this as an index into the array of metatiles
   lda MetaTiles+0, y ;copy top left tile
   sta Buffer+0, x
   lda MetaTiles+1, y ;copy top right tile
   sta Buffer+1, x
   lda MetaTiles+2, y ;copy bottom left tile
   sta Buffer+32, x
   lda MetaTiles+3, y ;copy bottom right tile
   sta Buffer+33, x
   inx ;increment the output index by 1 metatile horizontally
   inx
   rts

Like I said, this can only access 64 metatiles, which is why I prefer the other method (which is a better option than using pointers, because it's simpler and faster).
Re: Metatile troubles
by on (#118157)
Ah! Now I see. As I'm not planning on using very many metatiles at all. This seems to be perfect for my style. Thank you very much tokumaru. :)
Re: Metatile troubles
by on (#118176)
Update: Argh! I need to go back to NES school.. lol.

New problem: The code you all have been so gracious to help me with is for one column. Now, I assumed that right before I turned the screen on, I could JSR to that routine that sets the map up then return to turn the screen on. Basically, I imagined I could just set the writing area to $2000 and proceed on to a routine that would fetch 16 bytes from a level data table indexed by a which row to draw. However, my new problem is that I have no idea how to set the PPU writing area to what it needs to be. Like +$00 for row 1, +$16(?) for row 2, +$32 for 3(?) etc. My first assumption would be something like just using x to index what row. However, the PPU register is write-twice so that pretty much defeats the idea of that. How is this accomplished? Pointers? Sorry, my go-to is always pointers for some reason. lol

I mean, I know the nametable deal ($2007) increments with every write, however the buffer is only 64 bytes. Get what I'm trying to say?
Re: Metatile troubles
by on (#118177)
zkip wrote:
My first assumption would be something like just using x to index what row. However, the PPU register is write-twice so that pretty much defeats the idea of that. How is this accomplished? Pointers?

No, not pointers. Since the PPU is on a separate addressing space, there are not that many ways to access it. If you want direct access to any given byte, there's no other way than setting up the address through $2006 for every access. As you can imagine, this is slow should be avoided, specially during VBlank. Most programs take advantage of the auto-increment feature the PPU has: after each write/read, the address increments automatically (by 32, which is useful for drawing columns of tiles, or by 1, which is for nearly everything else).

Since you have divided your buffer into a top row and a bottom row, the easiest thing to do would be to set PPU address increments to 1 and just blast all 64 bytes in sequence. If you're drawing a whole screen (meaning rendering is turned off), you can even leave the PPU address untouched from one row of metatiles to the next. Something like this:

Code:
   lda #$20 ;have the PPU point to the beginning of the name table
   sta $2006
   lda #$00
   sta $2006

   ldy #$00 ;start decoding the first metatile of the map
DecodeRow:
   ldx #$00 ;start filling the buffer from 0
DecodeMetatile:
   sty Scratch ;backup Y
   lda (LevelData), y ;get the index of the metatile
   jsr WriteTileToBuffer ;decompress it to the buffer
   ldy Scratch ;restore Y
   iny ;go to the next metatile
   cpx #$20 ;is the buffer full yet?
   bne DecodeMetatile ;if not, do another metatile

   ldx #$00 ;start uploading from the beginning of the buffer
CopyToPPU:
   lda Buffer, x ;get the byte
   sta $2007 ;write it
   inx ;move on to the next byte
   cpx #$40 ;have we done 64 yet?
   bne CopyToPPU ;if not, copy another byte

   cpy #$f0 ;have we done all 240 metatiles yet?
   bne DecodeRow ;if not, decode another row