Skip to content

MAP16 Objects

BrunoValads edited this page Feb 16, 2018 · 7 revisions

MAP16 objects are foreground entities displayed in BG1. They comprise the graphics of static objects, e.g. the walls, floors, ceilings and even certain objects that Yoshi interacts with. They are split up into 8x8 tiles (as almost everything is in SNES VRAM).

Tile Graphics

The actual pixels shown in the tiles are governed by which BG1 tileset is loaded in VRAM for the given level. These get loaded in on level load and don't change as you are scrolling through a level (unless you spawn a tileset changer sprite); instead, the tilemaps change (i.e. which of those tiles are displayed where).

MAP16 Index

There is a huge table in ROM spanning from $18B3F2 up to $19D619 which holds a ton of word-sized entries. Each entry is a raw native SNES VRAM 16-bit tilemap format (todo: add page describing this), and therefore is the data that ultimately gets submitted to VRAM BG1 tilemap sections. The table is split up into "pages" which vary in size - from 8 to 2048 bytes. The pages are further split up into 8-byte (4-word) chunks, each representing a full 16x16 tile (four 8x8 entries together).

The game employs a succinct 16-bit indexing, which shall be named the "MAP16 index", to refer to one single 16x16 tile within one page. The format is pppppppptttttttt, where t (low byte) is the tile index within page and p (high byte) is the page index. The table at $18B2A4 holds all the offsets for each page. As an example, MAP16 index $3D12 (or $12 followed by $3D because little endian) gives us $3D for the page index and $12 for the tile index. The first step is to take the page index $3D and index into the page offset table, giving us $18B2A4 + $3D * 2 = $18B31E. This address contains offset $4420, so now we know the beginning of our page, $18B3F2 + $4420 = $18F812. This page contains several tiles, each 8 bytes in size, so to get to tile $12 we do $12 * 8 + $18F812 = $18F8A2, and these four words starting here (namely, $0C0A, $0C0B, $0C14, $0C14) are the 4 8x8 tilemap entries for this 16x16 tile.

These MAP16 indices are built up and stored for an entire level in the code in banks $12 and $13. This happens mostly during level load, but also can happen with dynamic stage changes during gamemode $0F. A large table in RAM is dedicated to storing MAP16 indices for the entire current level. It is at $7F8000 and spans all the way to $7FFFFF. This table governs what tiles will be shown as you are scrolling around a level. The table is split up by screen ID ($00-$3F), and then further by tile # within screen ($00-$FF). Tile # is row-major, so the index really looks like (in bit form): 0ssssssrrrrcccc0, where s=screen ID ($00-$3F), r=row ($0-$F), c=column ($0-$F), and a 0 at the end because it's multiplied by 2 (word-sized entries). As an example, screen # 5, row $C, column $8 would give us the index 0 000101 1100 1000 0 = $0B90, meaning $7F8B90. The word at this address is a MAP16 index as described above, representing the full 16x16 tile to display at that location.

Tile Spawning

Gamemode 0F uses 01 for its tilemap size, meaning there are 64x32 tiles or two tilemaps side by side, X-wise. The locations in VRAM for both tilemaps are $6800 and $6C00. In line with this, during gameplay there are two screen regions' worth of MAP16 tilemap data stored for any given moment. They are stored as MAP16 indices in $70409E table, and correspond with the two VRAM tilemaps.

The game checks for new column & row spawning in routine $109058 by testing whether the camera has travelled outside the bounds of the current row/column. If this happens, let’s say just for column, a new column’s data must be loaded into $70409E. The data must be fetched from $7F8000 table, as that contains the MAP16 tiles currently present in the entire stage. One column is 16 MAP16 tiles tall, therefore it may span two screens’ worth of data. As an example, it might be the bottom 5 tiles of screen $20 and the top 11 tiles of screen $2E. This is why the fetch from $7F8000 is done in a two-step fashion:

   JSR load_partial_column                   ; $10910C | load upper y screen column part
   LDA $003006                               ; $10910F |\  counter for lower y screen
   BEQ .convert_tmap                         ; $109113 | | if it's 0, no need to bother
   STA $06                                   ; $109115 |/  with lower screen, skip
   TYA                                       ; $109117 |\  when the tile # goes above max ($03FE)
   AND #$03FF                                ; $109118 | | this wraps back around to restart
   TAY                                       ; $10911B | | the range for the screen below
   STA $003004                               ; $10911C |/  (effectively modulus)
   LDA $04                                   ; $109120 |\
   CLC                                       ; $109122 | | take upper screen #
   ADC #$0010                                ; $109123 | | add $10 to get one screen below
   AND #$007F                                ; $109126 |/  (lower screen)
   TAX                                       ; $109129 |\
   LDA $006CA9,x                             ; $10912A | | load screen ID
   AND #$3F00                                ; $10912E | | of lower screen
   ASL A                                     ; $109131 | | then throw column into it:
   ORA $02                                   ; $109132 | | 0ssssss0000cccc0
   TAX                                       ; $109134 |/  no row; we restart at the top of lower
   JSR load_partial_column                   ; $109135 | load lower y screen column part

As you see above, load_partial_column is called twice, and that routine simply loops over the piece of the column for that particular screen within $7F8000 and loads the data into $70409E.

After $70409E is all set, there are two more steps to the full picture. First is to convert this new column into raw tilemap data, ready to be DMA'd into VRAM. This part is done on the Super FX at $09F9E8, and the destination buffers of the column conversions are $700DAA (left half) and $70DEA (right half), and for rows, $700E2A (top half) and $700E6E (bottom half). The conversion process is explained above in MAP16 Index section.

Finally, these buffers that hold only the newest column/row spawned in are DMA'd into VRAM at the proper place within $6800-$6FFF. Note that it only does that one column/row, and does not fully change the entire tilemap; instead, it just scrolls to align the screen properly, and the scrolling wraps around to make it look just fine.