Flash Free Screens From SD

From Uzebox Wiki
Jump to navigation Jump to search

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.


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 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).

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 RamifyFromSD(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
	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_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:

RamifyFromSD(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 intelligent design I have managed to get a few 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 slightly 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 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.


Drawing Screens And Using Sprites

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
		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 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...

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 with all ram tile effects, these details are highly dependent on what you are doing.