SNES programming tutorial. Example 3.
So, this is the big lesson. There are about a dozen things we need to do just to see a picture on the screen. We need to set a video mode. We need to enable a layer on the main screen. We need to set the address for tiles. We need to set the address for a tilemap. We need to make tiles and a tilemap (and a palette). We need to copy all the things to the VRAM. And we need to turn screen brightness on (and end forced blank).
I picked a random picture of a moon. Open in GIMP, resize to 128×128. Resize canvas to 256 pixel wide. Change picture mode to index, 16 colors, saved (export) as PNG with no compression.
(a little later, the image looked stretched out when I got it running on my actual SNES, due to the aspect ratio of SNES pixels being about 8/7. And also the order of the palette colors seemed oddly out of order, so I started over…)
OK. GIMP. Open file, resize to 112×128 to fix the sideways stretching. Resize canvas to 256 pixel wide. Then I made a 16 color grayscale palette like #000000 #101010 #202020 etc. Then I changed color mode to indexed using the custom palette (I shouldn’t have left the “remove unused colors” checked… oh, well). This fixed the out of order palette issue. That isn’t a big deal now, but if I had to process multiple images using the same SNES palette, you should have a standard GIMP palette for all the images to use. YY-CHR can rearrange color indexes, but it becomes a real pain for 16 colors over multiple images.
Then export as PNG with no compression.
Now we need superfamiconv.
This is a great tool to convert PNG to SNES palette, tiles, and map. The image needs to be 256 wide to correctly make a map. The PNG needs to be 16 color indexed and no compression. Superfamiconv is a command line tool, but I wrote a batch file (.bat) to automate it. (Just double click on the batch file)
and it produced moon.pal, moon.chr, and moon.map.
The .chr file is in 4bpp SNES format.
Now I can load all 3 files in M1TE (my mode 1 tile editor tool).
When I loaded the .map file, I clicked a few tiles down from the top and chose “load map to selected Y”. I changed the map height to 28, and resaved the full map as moon2.map.
All these files (moon.pal, moon.chr, and moon2.map) were added to the project by adding .incbin lines at the bottom of main.asm.
Now the code, which I will go over, line by line… but first I want to figure out where we are putting things in the VRAM. This is what I have been using, and it seems to work for my current needs. This arrangement is optional. You can rearrange the VRAM any way you like.
$0000 4bpp BG tiles (768 of them)
$3000 2bpp BG tiles (512 of them)
$4000 4bpp sprite tiles (512 of them)
$6000 layer 1 map (up to 2 screens)
$6800 layer 2 map (up to 2 screens)
$7000 layer 3 map (up to 2 screens)
$7800 layer 4 map (up to 2 screens)
So we need to put the 4bpp tiles at $0000 and the layer 1 map at $6000.
; DMA from Palette_Copy to CGRAM ; see previous tutorial page for explanation ; with A8 and XY16 ; DMA from Tiles to VRAM lda #V_INC_1 ; the value $80 sta vram_inc ; $2115 register, set the increment mode +1 ; each write will go +1 the previous write address ldx #$0000 stx vram_addr ; set an address in the vram of $0000 ; now we set up the DMA lda #1 sta $4300 ; transfer mode, 2 registers 1 write ; $2118 and $2119 are a pair Low/High lda #$18 ; $2118 sta $4301 ; destination, vram data ldx #.loword(Tiles) stx $4302 ; source lda #^Tiles sta $4304 ; bank ldx #(End_Tiles-Tiles) ; let the assembler calculate the size of transfer ; using 2 labels before and after our tiles. stx $4305 ; length lda #1 sta $420b ; start dma, channel 0 ; DMA from Tilemap to VRAM ldx #$6000 stx vram_addr ; set an address in the vram of $6000 lda #1 sta $4300 ; transfer mode, 2 registers 1 write ; $2118 and $2119 are a pair Low/High lda #$18 ; $2118 sta $4301 ; destination, vram data ldx #.loword(Tilemap) stx $4302 ; source lda #^Tilemap sta $4304 ; bank ldx #$700 stx $4305 ; length lda #1 sta $420b ; start dma, channel 0 ; a is still 8 bit. lda #1 ; mode 1, tilesize 8x8 all sta bg_size_mode ; $2105 stz bg12_tiles ; $210b tiles for BG 1+2 at VRAM address $0000 lda #$60 ; bg1 map at VRAM address $6000 sta tilemap1 ; $2107 lda #BG1_ON ; $01 = only bg 1 is active sta main_screen ; $212c lda #FULL_BRIGHT ; $0f = turn the screen on (end forced blank) sta fb_bright ; $2100
Let’s go over these last lines. $2105 is for video mode. We want mode 1, and 8×8 tiles. Our tiles will need to be 4bpp for layers 1 and 2. (we are only using layer 1). The bits of $2105 are 4321Emmm, with mmm for BG Mode. E will affect priority of BG3. 4321 are zero so all the layers have 8×8 tiles. If any of these are set, the corresponding layer will have 16×16 tiles.
$210b tells the PPU where our tiles are (for layer 1 and 2). Low nibble for layer 1 and high nibble for layer 2. Our tiles are at $0000 so we are just storing zero. But if we wanted to, we could put our tiles at $1000 for layer 1 by storing #1 to 210b. They are steps of $1000.
This is the perfect opportunity to point out that for all these “VRAM address X is for Y” registers the upper bit is always zero. There are only $8000 VRAM addresses, and the registers always look like they go up to $FFFF, but they don’t. My guess is that the original engineers were told that there would be 128 kB of VRAM, but some bean counter said “128 kB is too expensive, we only need 64 kB”.
So, bbbb aaaa is really -bbb -aaa (a = layer 1, b = layer2).
So, for 210b, only values 0-7 make sense for each nibble.
Maps have 6 bits for VRAM address. They are steps of $400, but the low 2 bits of the $2107 register are for map size… aaaaaayx where a is VRAM address and yx is map size (is really -aaaaayx with upper bit always 0 since VRAM addresses don’t go above $8000). It looks like you are just multiplying by $100. The value $60 is for VRAM address $6000, where our tile map for layer 1 will go.
Next time I will discuss the yx bits. You can reference this page for more information.
And we need to turn on layer 1 on the main screen $212c. (It would look really weird if we turned on ALL layers right now. All the other maps are still set to $0000 where our tiles are).
My main focus was setting up a tool chain for putting an image on screen. Here’s what it looks like. Try to repeat the process with a different picture. Maybe something more colorful?
What’s that RLE folder?
Another option for M1TE files (map and tiles) is to save as .rle (run length encoding). It’s slightly more advanced than a simple rle. It is designed for map compression. It works especially well for this particular tool chain (superfamiconv) because that tool puts tiles in order 1,2,3,4,etc. on the map and this .rle format can compress that. The full screen map is 1792 bytes and the compressed version is 76 bytes. It didn’t do so well for compressing tiles (5632 bytes became 4763 bytes). Other tilesets compress 50% or so.
The code to decompress is unrle.asm provided in the same main folder. It is set to decompress to $7f0000 and has a original maximum file size of $8000 (32k) bytes.
See mainB.asm. You pass a pointer to the compressed file to unrle the subroutine. With AXY16, A = source address, X = source bank. JSR unrle, it decompresses to $7f0000 (sometimes does a second rearrangement of bytes, so it may not be exactly 7f0000). And it returns AX with the address of the unpacked data, and Y the number of bytes.) We pass those to the DMA registers and send our data to the VRAM.
You may choose to leave everything uncompressed and skip the RLE stuff, and only use it if you run out of ROM space. You decide.