Flash Free Screens From SD: Difference between revisions

From Uzebox Wiki
Jump to navigation Jump to search
m (added link to "Flash Free Screens From SPI Ram")
 
(29 intermediate revisions by the same user not shown)
Line 1: Line 1:
==Introduction==
==Introduction==




This is another short and sweet tutorial. This in particular will be very useful in many games, especially if you are already using the SD card for something else(and you already paid the flash space footprint for the code). The basic concept here is to use a large ram buffer to grab graphical data from the SD card. Then we want to use that ram to display to screen. Conveniently, Mode 3 ram_tiles[] is a large buffer of data that we can readily display on the screen. We can also load some data into vram, which will the actual tile indices to draw. It's just like we might normally do with a map stored in flash with DrawMap2(). This technique can easily be applied to any of the tile only modes that lack ram tiles, to at least save the space of the map in flash. Without ram tiles, there is no way to actually store the tile data in ram in a way we can display it on the screen. Even just maps take up a lot of space, so this can still help us considerably. After you understand the rather simple technique it could also be applied to the bitmap modes, an RLE mode I hope to release, etc. This tutorial will assume Mode 3. There are DEFINITELY limitations where this method will not work, but you can probably design something within the limits to fit your situation. See the last section for special considerations.
 
This is what I consider one of the most important tutorials I have made. This in particular will be very useful in many games, especially if you are already using the SD card for something else(and you already paid the flash space footprint for the code). The basic concept here is to use a large ram buffer to grab graphical data from the SD card. Then we want to use that ram to display to screen. Conveniently, Mode 3 ram_tiles[] is a large buffer of data that we can readily display on the screen. We can also load some data into vram, which will be the actual tile indices to draw. It's just like we might normally do with a map stored in flash with DrawMap2(). This technique can easily be applied to any of the tile only modes that lack ram tiles, to at least save the space of the map in flash. Without ram tiles, there is no way to actually store the tile data in ram in a way we can display it on the screen. Even just maps take up a lot of space, so this can still help us considerably. After you understand the rather simple technique it could also be applied to the bitmap modes, an RLE mode I hope to release, etc. This tutorial will assume Mode 3. There are DEFINITELY limitations where this method will not work, but you can probably design something within the limits to fit your situation. See the last section for special considerations.






==Formatting The Data==
==Formatting The Data==




Before we can draw something on the screen, we need the data to be on the SD card. We will need to create a file where we will store our graphics data in binary form, or perhaps you already have it for other resources. To make it easier, and because the storage of the SD card is nearly infinite for most purposes related to Uzebox, we will just use totally uncompressed data. There are many ways this could be done, either manually with a hex editor and a little copy/paste by hand or a program. The method I have used is simply to use TileStudio with a custom conversion script(you can use gconvert of course) to generate the tile and map data as per usual. Using a text editor like notepad++ delete all characters that are not actual hexadecimal representations of the data. Now that you have the pure bytes of your tile graphics and map layout, open up your binary image and paste the tile graphics at some offset and write down that offset. Now someplace after that data paste the map data and note that offset. If everything went ok and there weren't any issues with formatting you should have all the pixels for your tiles, and the map for the screen you wish to draw. This is truly a kludge that I don't enjoy as it is a very tedious process. Please feel free to recommend a better tool and or process to use! This does work fine and probably quicker than writing a custom program though, that's why I do it that way/lazy.
Before we can draw something on the screen, we need the data to be on the SD card. We will need to create a file where we will store our graphics data in binary form, or perhaps you already have it for other resources. To make it easier, and because the storage of the SD card is nearly infinite for most purposes related to Uzebox, we will just use totally uncompressed data. There are many ways this could be done, either manually with a hex editor and a little copy/paste by hand or a program. The method I have used is simply to use TileStudio with a custom conversion script(you can use gconvert of course) to generate the tile and map data as per usual. Using a text editor like notepad++ delete all characters that are not actual hexadecimal representations of the data. Now that you have the pure bytes of your tile graphics and map layout, open up your binary image and paste the tile graphics at some offset and write down that offset. Now someplace after that data paste the map data and note that offset. If everything went ok and there weren't any issues with formatting you should have all the pixels for your tiles, and the map for the screen you wish to draw. This is truly a kludge that I don't enjoy as it is a very tedious process. Please feel free to recommend a better tool and or process to use! This does work fine and probably quicker than writing a custom program though, that's why I do it that way/lazy.




==Creating An Index Table==
==Creating An Index Table==




Imagine we have repeated that process 15 times and have a huge amount of data in a hex file. If you scroll through and look at it, there is really no clear order to it all-that's why I said write down the offsets! What we need to do, is store the offsets to the graphics data and the map data somewhere at a known location within the binary file. That way our program can go to this known offset, apply an additional offset based on which screen to draw(that offset will of course be the size of an index entry multiplied by which screen) and then know exactly where the data we want is. It will vary based on your games needs. This method is primarily meant for parts of a game where speed is not an issue, and we can devote all our ram tiles to draw the screen. My format for the current game I'm working on is as easy as this:
Imagine we have repeated that process 15 times and have a huge amount of data in a hex file. If you scroll through and look at it, there is really no clear order to it all-that's why I said write down the offsets! What we need to do, is store the offsets to the graphics data and the map data somewhere at a known location within the binary file. That way our program can go to this known offset, apply an additional offset based on which screen to draw(that offset will of course be the size of an index entry multiplied by which screen) and then know exactly where the data we want is. It will vary based on your games needs. This method is primarily meant for parts of a game where speed is not an issue, and we can devote all our ram tiles to draw the screen. My format for the current game I'm working on is as easy as this:
2 bytes - offset to first byte of ram tile data, 2 bytes - offset to first byte of tile data...that's it. In Lolo, I am attempting some pretty cool looking "Full Motion Video" using long sequences of these screens. Obviously that quickly surpasses the 16bit boundary so I ended up going with 3 byte offsets. Just an example of a few modification you may wish to make if you are doing something tricky. Most people can probably just use this very simple method and actually get something done instead of stewing over overly complex features :| This is what it looks like in a hex editor, notice that I am only really using the first 4 bytes but each of the entries takes a while line equaling 16 bytes. I personally do this because hex editors work nicely without changing setting for 16 wide columns, a little waste is pretty meaningless here, and it allows me to write some comments in case I have to track down a problem with the graphics(which will generally be because of a problem with the formatting while copy/pasting to the hex editor window).
2 bytes - offset to first byte of ram tile data, 2 bytes - offset to first byte of tile data...that's it. In Lolo, I am attempting some pretty cool looking "Full Motion Video" using long sequences of these screens. Obviously that quickly surpasses the 16bit boundary so I ended up going with 3 byte offsets. Just an example of possible modification you may wish to make if you are doing something different. Most people can probably just use this very simple method and actually get something done instead of stewing over overly complex features :| This is what it looks like in a hex editor, notice that I am only really using the first 4 bytes, but each of the entries takes a whole line equaling 16 bytes. I personally do this because hex editors work nicely without changing setting for 16 wide columns, a little waste is pretty meaningless here, and it allows me to write some comments in case I have to track down a problem with the graphics(which will generally be because of a problem with the formatting while copy/pasting to the hex editor window).
 


[[File:hexeditorneo1.png]]
[[File:hexeditorneo1.png]]




==Putting It On Screen==
==Putting It On Screen==




Now we have all the pixel data for our graphics tiles, we have the information for which tiles to draw where, and our program can easily find exactly where all that is in the SD card. The next part is just as simple, and it's where our hard work pays off! Understanding everything we have read so far, the following code should hopefully be self explanatory:
Now we have all the pixel data for our graphics tiles, we have the information for which tiles to draw where, and our program can easily find exactly where all that is in the SD card. The next part is just as simple, and it's where our hard work pays off! Understanding everything we have read so far, the following code should hopefully be self explanatory:


  void RamifyFromSD(uint8_t id){
 
  void DrawMapSD(uint8_t id){
  HideSprites();//IMPORTANT! This will take more than 1 frame. Sprite blitting during SD read will ruin our buffered data
  HideSprites();//IMPORTANT! This will take more than 1 frame. Sprite blitting during SD read will ruin our buffered data
  FRESULT res;
  FRESULT res;
Line 39: Line 48:
  off2 = (uint32_t)((ram_tiles[2]<<8)+(ram_tiles[3]));//map offset
  off2 = (uint32_t)((ram_tiles[2]<<8)+(ram_tiles[3]));//map offset
   
   
  res |= pf_lseek(off);//go to the offset we found in the table
  res |= pf_lseek(off);//go to the offset we found in the table for tile data
  res |= pf_read((BYTE *)ram_tiles,(RAM_TILES_COUNT*64),&br);//load up the pixel data to ram tiles
  res |= pf_read((BYTE *)ram_tiles,(RAM_TILES_COUNT*64),&br);//load up the pixel data to ram tiles
  res |= pf_lseek(off2);//go to the offset we found in the table
  res |= pf_lseek(off2);//go to the offset we found in the table for map data
  res |= pf_read((BYTE *)vram,(VRAM_TILES_H*SCREEN_TILES_V),&br);//load the map into vram
  res |= pf_read((BYTE *)vram,(VRAM_TILES_H*SCREEN_TILES_V),&br);//load the map into vram
   
   
Line 48: Line 57:
  }
  }


There you go that's it. From here on you can draw an entire screen of graphics anywhere you want by simply calling:


  RamifyFromSD(screen_num);
There you go that's it. You don't have to worry about anything it entirely loads up all your ram tiles with data and your vram with the map. If you have less unique tiles than you have ram tiles that is fine as those ram tiles will be filled with whatever data is after, but wont show up on screen since your map doesn't point to them. From here on you can draw an entire screen of graphics anywhere you want by simply calling:
 
DrawMapSD(screen_num);


Your title screen, intermission, level select, credits, game over, ending, everything can be drawn to the screen form only a couple bytes for the function call.
Your title screen, intermission, level select, credits, game over, ending, everything can be drawn to the screen form only a couple bytes for the function call.




Line 58: Line 69:




The main thing to keep in mind when using this in Mode 3, is that you cannot store more tiles in ram than you have ram tiles. Having more ram tiles is your friend here obviously, but with intelligent design I have managed to get a few impressive scenes to fit within the limits:
 
The main thing to keep in mind when using this in Mode 3, is that you cannot store more tiles in ram than you have ram tiles. Having more ram tiles is your friend here obviously, but with a little clever design you can get some impressive scenes to fit within the limits:


[[File:sdscreenexample.png]]
[[File:sdscreenexample.png]]


I hope that you don't dismiss the idea as impractical before you at least give it a try. None of those screens above cost me any flash space. I could easily draw 1,000 more of them in a single game if I had the time and cared. No part of the process is difficult after you do it a couple times, and the space savings are ''massive''. It is the difference between games not having the screens it needs to feel complete, to the point where you have to stop yourself from finding unnecessary situations to draw a screen. You can still draw screens using more unique tiles than you have ram tiles, and you still save the flash space for the tiles that will fit in ram. You can also use sprites with this method, but the amount of ram tiles required to draw your sprites will reduce the number of unique tiles you can have before having to rely on flash. You must also be slightly careful when doing so.
I hope that you don't dismiss the idea as impractical before you at least give it a try. None of those screens above cost me any flash space. I could easily draw 1,000 more of them in a single game if I had the time and cared. No part of the process is difficult after you do it a couple times, and the space savings are ''massive''. It is the difference between games not having the screens it needs to feel complete, to the point where you have to stop yourself from finding unnecessary situations to draw a screen. You can still draw screens using more unique tiles than you have ram tiles, and you still save the flash space for the tiles that will fit in ram. You can also use sprites with this method, but the amount of ram tiles required to draw your sprites will reduce the number of unique tiles you can have before having to rely on flash. You must also be careful when doing so.
 
 


==Drawing With More Unique Tiles==
==Drawing With More Unique Tiles==




If you have a screen that must have more than 35-40 unique tiles, you only have to make a couple small modifications before you can use it. If you have 40 ram tiles, then you are still saving 40*64 = 2,560 bytes for tiles + 840 for tiles map = 3,400 bytes from what it would have cost to draw it the normal way. That pays for the cost of the flash footprint for the SD card code, even if you don't need SD for anything but screens! The only difference here, is that you must know how many ram tiles you will have before converting your graphics. Then using the same method you chose before and storing all that data on SD(and if it's easier, just put every tile there even if the later ones wont make it into ram tiles), simply take all tiles that will not fit and put them into a const char[] PROGMEM like you would do for a normal map. Now, the indices from your conversion process will be correct for the ram tiles, but since you removed say 40(or however many ram tiles you have) tiles from the data you store in flash they will be off if you simply SetTileTable(&TilesThatWouldNotFit); This is extremely easy to fix since ram tiles are independent of your tile table, just SetTileTable((&TileThatWouldNotFit)-(RAM_TILES_COUNT*64)); Now the indices that are pointing to ram tiles will be drawn as they should, but since we moved the tile table pointer backwards from the real start of the data, the offsets will be correct. If that doesn't make sense just ask in the Weber's Rants thread. It might sound pretty iffy, but you literally just alter 1 line of code from your normal process.
 
If you have a screen that must have more than 35-40 unique tiles, you only have to make a couple small modifications before you can use it. If you have 40 ram tiles, then you are still saving 40*64 = 2,560 bytes for tiles + 840 for tiles map = 3,400 bytes '''each screen''' from what it would have cost to draw it the normal way. That pays for the cost of the flash footprint for the SD card code, even if you don't need SD for anything but screens! The only difference here, is that you must know how many ram tiles you will have before converting your graphics. Then using the same method you chose before and storing all that data on SD(and if it's easier, just put every tile there even if the later ones wont make it into ram tiles), simply take all tiles that will not fit and put them into a const char[] PROGMEM like you would do for normal tiles you would use in DrawMap2. Now, the indices from your conversion process will be correct for the ram tiles, but since you removed the first 40(or however many ram tiles you have) tiles from the data you store in flash they will be off if you simply SetTileTable(TilesThatWouldNotFit); This is extremely easy to fix since ram tiles are independent of your tile table, just SetTileTable(((const char *)TileThatWouldNotFit)-(RAM_TILES_COUNT*64)); Now the indices that are pointing to ram tiles will be drawn as they should, but since we moved the tile table pointer backwards from the real start of the data, the offsets will be correct. This might sound a bit confusing as it is difficult to explain. If that doesn't make sense just ask in the Weber's Rants thread. You literally just alter 1 line of code from your normal process.
 




Line 73: Line 89:




Here is another situation that is likely to be an issue. Luckily this is extremely easy to fix. Your sprites will take a certain amount of ram tiles depending on where they will end up being positioned. To use sprites with this method, you must know exactly how many ram tiles the sprites can take during any point of whatever you are doing with them. Do your conversion process exactly as you have been doing it. Now, BEFORE you start moving sprites around we need to take the tile data out of the ram tiles that the sprites will use. This is to avoid that data being corrupted by the sprite blitting. We just need to put them somewhere else where the sprites wont bother our tile graphics. This is easily accomplished AFTER we have loaded our SD screen like normal. Just do something like this:


for(uint16_t roff=RAM_TILES_COUNT*64;roff>MAX_NUMBER_OF_RAM_TILES_SPRITES_WILL_TAKE*64;roff--)//move our tile data to a safe location
Here is another situation that is likely to be an issue. Luckily this can be an easy thing to fix. Your sprites will take a certain amount of ram tiles depending on where they will end up being positioned. To use sprites with this method, you must know exactly how many ram tiles the sprites can take during any point of whatever you are doing with them. Do your conversion process exactly as you have been doing it. Now, BEFORE you start moving sprites around we need to take the tile data out of the ram tiles that the sprites will use. This is to avoid that data being corrupted by the sprite blitting. We just need to put them somewhere else where the sprites wont bother our tile graphics. This is easily accomplished AFTER we have loaded our SD screen like normal. Just do something like this:
ram_tiles[roff] = ram_tiles[roff-(MAX_NUMBER_OF_RAM_TILES_SPRITES_WILL_TAKE*64)];
 
 
for(uint16_t roff=RAM_TILES_COUNT*64;roff>MAX_NUMBER_OF_RAM_TILES_SPRITES_WILL_TAKE*64;roff--)//move our tile data to a safe location
ram_tiles[roff] = ram_tiles[roff-(MAX_NUMBER_OF_RAM_TILES_SPRITES_WILL_TAKE*64)];


  for(uint16_t voff=0;voff<VRAM_SIZE;voff++)//update our vram indices
  for(uint16_t voff=0;voff<VRAM_SIZE;voff++)//update our vram indices
Line 82: Line 100:




There, now the sprites will not corrupt our ram tiles data and our graphics will still draw just like we expect. We still have 1 issue that is not exclusive to just this, but any time you are messing around with this stuff behind the kernels back. Sprites that overlap part of vram that is set to a ram tile index(which is likely all parts of vram at this point, depending on what you have done), will be blit on top of that ram tile. Now all locations where that tile is used will display the pixels which were blit over it. This is generally not desirable, and so we must make sure that our sprite never overlap part of the screen that is a ram tile. The only way we can do this is to put tiles that are stored in flash there. Depending on the graphics for your screen and what your sprites are doing on top of it, this could be a simple or complex process. I will use the ending scene from a game I made as example. Nevermind the wrong colors as it wasn't meant to be used at the hack location I put it for this quick demonstration...
There, now the sprites will not corrupt our ram tiles data and our graphics will still draw just like we expect. We still have 1 issue that is not exclusive to just this, but any time you are messing around with this stuff behind the kernel's back. Sprites that overlap part of vram that is set to a ram tile index(which is likely all parts of vram at this point, depending on what you have done), will be blit on top of that ram tile. Now all locations where that tile is used will display the pixels which were blit over it. This is generally not desirable, and so we must make sure that our sprites never overlap part of the screen that is a ram tile. The only way we can do this is to put tiles that are stored in flash there. Depending on the graphics for your screen and what your sprites are doing on top of it, this could be a simple or complex process. I will use the ending scene from a game I made as example. Nevermind the wrong colors as it wasn't meant to be used at the hack location I put it for this quick demonstration...
 


[[File:aefreescreenexample.png]]
[[File:aefreescreenexample.png]]


This is a rather easy situation. The sixth unique tile in my graphics data(I have a short screen here, the first tile is the orange-ish brick) is a black tile.  
 
This is a rather easy situation as the sprites only ever jump straight up and down here and will only overlap where it is black. The sixth unique tile in my graphics data(I have a short screen here, the first tile is the orange-ish brick) is a black tile.  
 


[[File:sdfreescreentilesetexample.png]]
[[File:sdfreescreentilesetexample.png]]


I do not want every black tile to have pieces of sprites. So for each position in vram where that black tile will be, I simply point it to a place where there is a black tile in flash. You could just make a black tile for this purpose, but most likely you already have one somewhere in your font graphics or whatever. Point all those places in vram to that location. That all depends on whether you are using more unique tiles than you have ram tiles, how you set up SetTileTable, and the specifics of your game/graphics. As long as vram in all locations the sprite will overlap are pointed to a flash tile you will be fine. If you are having problems with "sprite smear", then it isn't pointed to a flash tile! In this game all I did was slightly modify the example code above:
I do not want every black tile to have pieces of sprites. So for each position in vram where that black tile will be, I simply point it to a place where there is a black tile in flash. You could just make a black tile for this purpose, but most likely you already have one somewhere in your font graphics or whatever. Point all those places in vram to that location. That all depends on whether you are using more unique tiles than you have ram tiles, how you set up SetTileTable, and the specifics of your game/graphics. As long as vram in all locations the sprite will overlap are pointed to a flash tile you will be fine. If you are having problems with "sprite smear", then it isn't pointed to a flash tile! In this game all I did was slightly modify the example code above:


  for(uint16_t voff=0;voff<VRAM_SIZE;voff++){
  for(uint16_t voff=0;voff<VRAM_SIZE;voff++){
  if(vram[voff] == 5)//black ram tile
  if(vram[voff] == 5)//black ram tile
  vram[voff] = FNTSTRT;//point to black tile in flash so sprites don't smear
  vram[voff] = FNTSTRT+RAM_TILES_COUNT;//point to black tile in flash so sprites don't smear
  else
  else
  vram[voff] += 6;
  vram[voff] += 6;
Line 101: Line 124:
  ram_tiles[roff] = ram_tiles[roff-(6*64)];
  ram_tiles[roff] = ram_tiles[roff-(6*64)];


As with all ram tile effects, these details are highly dependent on what you are doing.
 
As you can see I moved all tile data out of the first 6 ram tiles. The Meta Sprites are both 1x2 sprites or 8x16 pixels, they are always aligned on the x axis and so each one will at maximum take 3 ram tiles each. Combined they will use the first 6 ram tiles, and of course I moved everything by that much to avoid corruption. As with all ram tile effects, these details are highly dependent on what you are wanting to do. Although you can likely pull off simple examples knowing nothing about ram tile effects, I highly recommend reading [[Ram Tile Effects Primer]] and [[Sprite Techniques]] if you haven't done so or this is a bit confusing as to how and why.
 
 
 
 
==Closing Notes==
 
 
 
There is a lot of gain to be had here, which is why I am kind of pushing this idea in the forums. Even tile modes, and especially tile modes with 16bit vram(Mode 1) could save up to as much as 40x28x2 = 2,240 for '''''each''''' screen just on the map alone! This will result in more impressive Uzebox games with better presentation because I have not seen anything else that so easily frees up massive flash space. Ideally we would have a program that just takes a bitmap and does all the conversion and dumps it into a binary file we can directly use or attach at the end of some other file our game needs. I don't have the time for that right now, but I do realize it is an important element for this method to be adopted widely. Doing things manually and twiddling with paste into a hex editor is error prone. Yep, that's it, try it out, thanks for reading!
 
You may also be interested in [[Flash Free Screens From SPI Ram]], which has some advantages in speed and flexibility.

Latest revision as of 18:10, 23 September 2017

Introduction

This is what I consider one of the most important tutorials I have made. This in particular will be very useful in many games, especially if you are already using the SD card for something else(and you already paid the flash space footprint for the code). The basic concept here is to use a large ram buffer to grab graphical data from the SD card. Then we want to use that ram to display to screen. Conveniently, Mode 3 ram_tiles[] is a large buffer of data that we can readily display on the screen. We can also load some data into vram, which will be the actual tile indices to draw. It's just like we might normally do with a map stored in flash with DrawMap2(). This technique can easily be applied to any of the tile only modes that lack ram tiles, to at least save the space of the map in flash. Without ram tiles, there is no way to actually store the tile data in ram in a way we can display it on the screen. Even just maps take up a lot of space, so this can still help us considerably. After you understand the rather simple technique it could also be applied to the bitmap modes, an RLE mode I hope to release, etc. This tutorial will assume Mode 3. There are DEFINITELY limitations where this method will not work, but you can probably design something within the limits to fit your situation. See the last section for special considerations.


Formatting The Data

Before we can draw something on the screen, we need the data to be on the SD card. We will need to create a file where we will store our graphics data in binary form, or perhaps you already have it for other resources. To make it easier, and because the storage of the SD card is nearly infinite for most purposes related to Uzebox, we will just use totally uncompressed data. There are many ways this could be done, either manually with a hex editor and a little copy/paste by hand or a program. The method I have used is simply to use TileStudio with a custom conversion script(you can use gconvert of course) to generate the tile and map data as per usual. Using a text editor like notepad++ delete all characters that are not actual hexadecimal representations of the data. Now that you have the pure bytes of your tile graphics and map layout, open up your binary image and paste the tile graphics at some offset and write down that offset. Now someplace after that data paste the map data and note that offset. If everything went ok and there weren't any issues with formatting you should have all the pixels for your tiles, and the map for the screen you wish to draw. This is truly a kludge that I don't enjoy as it is a very tedious process. Please feel free to recommend a better tool and or process to use! This does work fine and probably quicker than writing a custom program though, that's why I do it that way/lazy.


Creating An Index Table

Imagine we have repeated that process 15 times and have a huge amount of data in a hex file. If you scroll through and look at it, there is really no clear order to it all-that's why I said write down the offsets! What we need to do, is store the offsets to the graphics data and the map data somewhere at a known location within the binary file. That way our program can go to this known offset, apply an additional offset based on which screen to draw(that offset will of course be the size of an index entry multiplied by which screen) and then know exactly where the data we want is. It will vary based on your games needs. This method is primarily meant for parts of a game where speed is not an issue, and we can devote all our ram tiles to draw the screen. My format for the current game I'm working on is as easy as this: 2 bytes - offset to first byte of ram tile data, 2 bytes - offset to first byte of tile data...that's it. In Lolo, I am attempting some pretty cool looking "Full Motion Video" using long sequences of these screens. Obviously that quickly surpasses the 16bit boundary so I ended up going with 3 byte offsets. Just an example of possible modification you may wish to make if you are doing something different. Most people can probably just use this very simple method and actually get something done instead of stewing over overly complex features :| This is what it looks like in a hex editor, notice that I am only really using the first 4 bytes, but each of the entries takes a whole line equaling 16 bytes. I personally do this because hex editors work nicely without changing setting for 16 wide columns, a little waste is pretty meaningless here, and it allows me to write some comments in case I have to track down a problem with the graphics(which will generally be because of a problem with the formatting while copy/pasting to the hex editor window).


Hexeditorneo1.png


Putting It On Screen

Now we have all the pixel data for our graphics tiles, we have the information for which tiles to draw where, and our program can easily find exactly where all that is in the SD card. The next part is just as simple, and it's where our hard work pays off! Understanding everything we have read so far, the following code should hopefully be self explanatory:


void DrawMapSD(uint8_t id){
	HideSprites();//IMPORTANT! This will take more than 1 frame. Sprite blitting during SD read will ruin our buffered data
	FRESULT res;

	uint32_t off = (uint32_t)(id*16);
	uint32_t off2;
	WORD br;
	
	res	|=	pf_lseek(off);//go to our index table
	res	|=	pf_read(ram_tiles,16,&br));//load up the entry data
	off	= (uint32_t)((ram_tiles[0]<<8)+(ram_tiles[1]));//graphics offset
	off2	= (uint32_t)((ram_tiles[2]<<8)+(ram_tiles[3]));//map offset

	res |=		pf_lseek(off);//go to the offset we found in the table for tile data
	res |=		pf_read((BYTE *)ram_tiles,(RAM_TILES_COUNT*64),&br);//load up the pixel data to ram tiles
	res |=		pf_lseek(off2);//go to the offset we found in the table for map data
	res |=		pf_read((BYTE *)vram,(VRAM_TILES_H*SCREEN_TILES_V),&br);//load the map into vram

	if(res)
		SDCrash();//whatever error handling you want to do, if any.
}


There you go that's it. You don't have to worry about anything it entirely loads up all your ram tiles with data and your vram with the map. If you have less unique tiles than you have ram tiles that is fine as those ram tiles will be filled with whatever data is after, but wont show up on screen since your map doesn't point to them. From here on you can draw an entire screen of graphics anywhere you want by simply calling:

DrawMapSD(screen_num);

Your title screen, intermission, level select, credits, game over, ending, everything can be drawn to the screen form only a couple bytes for the function call.


Limitations

The main thing to keep in mind when using this in Mode 3, is that you cannot store more tiles in ram than you have ram tiles. Having more ram tiles is your friend here obviously, but with a little clever design you can get some impressive scenes to fit within the limits:

Sdscreenexample.png

I hope that you don't dismiss the idea as impractical before you at least give it a try. None of those screens above cost me any flash space. I could easily draw 1,000 more of them in a single game if I had the time and cared. No part of the process is difficult after you do it a couple times, and the space savings are massive. It is the difference between games not having the screens it needs to feel complete, to the point where you have to stop yourself from finding unnecessary situations to draw a screen. You can still draw screens using more unique tiles than you have ram tiles, and you still save the flash space for the tiles that will fit in ram. You can also use sprites with this method, but the amount of ram tiles required to draw your sprites will reduce the number of unique tiles you can have before having to rely on flash. You must also be careful when doing so.


Drawing With More Unique Tiles

If you have a screen that must have more than 35-40 unique tiles, you only have to make a couple small modifications before you can use it. If you have 40 ram tiles, then you are still saving 40*64 = 2,560 bytes for tiles + 840 for tiles map = 3,400 bytes each screen from what it would have cost to draw it the normal way. That pays for the cost of the flash footprint for the SD card code, even if you don't need SD for anything but screens! The only difference here, is that you must know how many ram tiles you will have before converting your graphics. Then using the same method you chose before and storing all that data on SD(and if it's easier, just put every tile there even if the later ones wont make it into ram tiles), simply take all tiles that will not fit and put them into a const char[] PROGMEM like you would do for normal tiles you would use in DrawMap2. Now, the indices from your conversion process will be correct for the ram tiles, but since you removed the first 40(or however many ram tiles you have) tiles from the data you store in flash they will be off if you simply SetTileTable(TilesThatWouldNotFit); This is extremely easy to fix since ram tiles are independent of your tile table, just SetTileTable(((const char *)TileThatWouldNotFit)-(RAM_TILES_COUNT*64)); Now the indices that are pointing to ram tiles will be drawn as they should, but since we moved the tile table pointer backwards from the real start of the data, the offsets will be correct. This might sound a bit confusing as it is difficult to explain. If that doesn't make sense just ask in the Weber's Rants thread. You literally just alter 1 line of code from your normal process.


Drawing Screens And Using Sprites

Here is another situation that is likely to be an issue. Luckily this can be an easy thing to fix. Your sprites will take a certain amount of ram tiles depending on where they will end up being positioned. To use sprites with this method, you must know exactly how many ram tiles the sprites can take during any point of whatever you are doing with them. Do your conversion process exactly as you have been doing it. Now, BEFORE you start moving sprites around we need to take the tile data out of the ram tiles that the sprites will use. This is to avoid that data being corrupted by the sprite blitting. We just need to put them somewhere else where the sprites wont bother our tile graphics. This is easily accomplished AFTER we have loaded our SD screen like normal. Just do something like this:


	for(uint16_t roff=RAM_TILES_COUNT*64;roff>MAX_NUMBER_OF_RAM_TILES_SPRITES_WILL_TAKE*64;roff--)//move our tile data to a safe location
		ram_tiles[roff] = ram_tiles[roff-(MAX_NUMBER_OF_RAM_TILES_SPRITES_WILL_TAKE*64)];
	for(uint16_t voff=0;voff<VRAM_SIZE;voff++)//update our vram indices
		vram[voff] += MAX_NUMBER_OF_RAM_TILES_SPRITES_WILL_TAKE;


There, now the sprites will not corrupt our ram tiles data and our graphics will still draw just like we expect. We still have 1 issue that is not exclusive to just this, but any time you are messing around with this stuff behind the kernel's back. Sprites that overlap part of vram that is set to a ram tile index(which is likely all parts of vram at this point, depending on what you have done), will be blit on top of that ram tile. Now all locations where that tile is used will display the pixels which were blit over it. This is generally not desirable, and so we must make sure that our sprites never overlap part of the screen that is a ram tile. The only way we can do this is to put tiles that are stored in flash there. Depending on the graphics for your screen and what your sprites are doing on top of it, this could be a simple or complex process. I will use the ending scene from a game I made as example. Nevermind the wrong colors as it wasn't meant to be used at the hack location I put it for this quick demonstration...


Aefreescreenexample.png


This is a rather easy situation as the sprites only ever jump straight up and down here and will only overlap where it is black. The sixth unique tile in my graphics data(I have a short screen here, the first tile is the orange-ish brick) is a black tile.


Sdfreescreentilesetexample.png


I do not want every black tile to have pieces of sprites. So for each position in vram where that black tile will be, I simply point it to a place where there is a black tile in flash. You could just make a black tile for this purpose, but most likely you already have one somewhere in your font graphics or whatever. Point all those places in vram to that location. That all depends on whether you are using more unique tiles than you have ram tiles, how you set up SetTileTable, and the specifics of your game/graphics. As long as vram in all locations the sprite will overlap are pointed to a flash tile you will be fine. If you are having problems with "sprite smear", then it isn't pointed to a flash tile! In this game all I did was slightly modify the example code above:


	for(uint16_t voff=0;voff<VRAM_SIZE;voff++){
		if(vram[voff] == 5)//black ram tile
			vram[voff] = FNTSTRT+RAM_TILES_COUNT;//point to black tile in flash so sprites don't smear
		else
			vram[voff] += 6;
	}
	for(uint16_t roff=RAM_TILES_COUNT*64;roff>6*64;roff--)
		ram_tiles[roff] = ram_tiles[roff-(6*64)];


As you can see I moved all tile data out of the first 6 ram tiles. The Meta Sprites are both 1x2 sprites or 8x16 pixels, they are always aligned on the x axis and so each one will at maximum take 3 ram tiles each. Combined they will use the first 6 ram tiles, and of course I moved everything by that much to avoid corruption. As with all ram tile effects, these details are highly dependent on what you are wanting to do. Although you can likely pull off simple examples knowing nothing about ram tile effects, I highly recommend reading Ram Tile Effects Primer and Sprite Techniques if you haven't done so or this is a bit confusing as to how and why.



Closing Notes

There is a lot of gain to be had here, which is why I am kind of pushing this idea in the forums. Even tile modes, and especially tile modes with 16bit vram(Mode 1) could save up to as much as 40x28x2 = 2,240 for each screen just on the map alone! This will result in more impressive Uzebox games with better presentation because I have not seen anything else that so easily frees up massive flash space. Ideally we would have a program that just takes a bitmap and does all the conversion and dumps it into a binary file we can directly use or attach at the end of some other file our game needs. I don't have the time for that right now, but I do realize it is an important element for this method to be adopted widely. Doing things manually and twiddling with paste into a hex editor is error prone. Yep, that's it, try it out, thanks for reading!

You may also be interested in Flash Free Screens From SPI Ram, which has some advantages in speed and flexibility.