Showing posts with label programming. Show all posts
Showing posts with label programming. Show all posts

2008/05/21

Fragmentation And You!

In the continuing series of posts about Stuff They Never Tell You About Game Development, I'm going to rant a bit about fragmentation, why it's evil, and how you can stop its nefariousness.

Fragmentation is basically where you've got a something that's X big but all the wee tiny bits where you can place your something are too small even though your something would totally fit if the space weren't partitioned so badly. Consider me trying to park in Fell's Point (this is near the water in Baltimore for those wondering):

This happens ALL the time.

If you try to park on the street in Fell's Point, you will rapidly learn that there are no lines on the street. This means that people with giant SUVs getting less than 10 mpg highway have NO IDEA where to park their environment destroyingly inefficient vehicles. I drive a tiny Saturn which is mostly made of plastic. It does not ever fit in the spaces left. If only (heaven forbid) I could rearrange these parking challenged bozos' vehicles, I would have plenty of room!

Like you've never had this thought before.

This, friends, is fragmentation at its ugliest. It also happens in your computer's memory (among other places).

Backing Stores and Virtual Memory and Consoles, Oh My!
Memory fragmentation is basically like the two images above: you've got X amount free but it's all broken up into tiny bits that you can't really use. It's a Bad Thing (TM). Occasionally you need to allocate something big and contiguous like an image or something and those tiny bits just aren't going to cut it even though it would totally fit if you could add up all the tiny bits. This requires another chunk of memory to be grabbed from the free store to satisfy your request.

For a normal modern OS, this usually isn't a big deal because we have a) a backing store like a hard drive, and b) a good virtual memory system that can page things in and out if you get near the end of physical RAM a la your undergraduate OS course. The worst that usually happens is the program is a little more chuggy (or a lot more chuggy, depending on your system) and you get fragged more often because your framerate dips and you just can't dodge that guy's rail anymore (bastard).

On a console or embedded device (like the iPhone) it's a whole lot more catastrophic. We may or may not have a backing store. We may or may not even have a basic virtual memory system. It probably doesn't use the backing store that we may or may not have to page RAM even if it has the hardware to do so. So we may or may not be screwed when trying to allocate a giant piece of memory for the screen grab of your most recent death because railboy is totally hacking. By "screwed" here I mean "crash" because that's usually what such devices do. And, sadly, in this situation, we're usually screwed.

How It Be Happenin'
Consider the following horribly-contrived-yet-so-close-to-actual-production-code-I've-seen-recently-that-it-makes-me-shiver-just-typing-it example:


vector StuffToLoad;
StuffToLoad.push_back( "some_prefs.xml" );
StuffToLoad.push_back( "a_texture.dds" );
StuffToLoad.push_back( "this_is_temp.dds" );
StuffToLoad.push_back( "big_honkin_asset.stuff" );
StuffToLoad.push_back( "another_asset.stuff" );
for ( uint ii=0; ii<stufftoload.size(); i++ )
{
LoadAsset( StuffToLoad[ii] );
}



The basic idea is that it's trying to load a bunch of stuff. During the loading of those assets, it really needs the "this_is_temp.dds" texture prior to loading "big_honking_asset.stuff" but then gets rid of both of it and the prefs XML file. Those of you who have dealt with this issue are probably headpalming right now (just play along). For this exercise, we'll assume that LoadAsset allocates exactly once per asset. and UnloadAsset properly deallocates that one allocation. So how swiss-cheeseified does this snippet make your memory?

Seems simple enough?

That's the big blocks, certainly. What about the rest of it? The bad news is that depending on your particular compiler and the version of the Standard C++ Library you're using (otherwise known as the STL), it might be much, much worse. At the very least, the vector is going to allocate at least once but more likely two or three times. Each string pushed into the vector is probably going to allocate once for the string. If LoadAsset's prototype looks like this:


BOOL LoadAsset( string AssetToLoad );


it probably allocates once per function call for the pass by value (hooray for copy constructors!) as well. So by the time you get to allocating the one chunk for your optimzed LoadAsset, you've probably blown two tiny temporary allocations on strings! Load and unload a bunch of assets and you're nickling and diming your memory to screwedness!

Closer!

For people not used to thinking about this kind of optimization, this often comes as a complete shock. This kind of thing usually requires someone (typically me), to go through piles of code to root these things out of them because memory is prodigiously perforated and the game crashes if you play it a lot. This is both tedious and error prone and a better solution is to not write it like this in the first place.

This is one way fragmentation happens and it's really, really painful to deal with after the fact. Such issues tend to not manifest themselves until the very end of the game which tends to only get played near the end of the project which is where disasters tend to collect leading to some very, very long work weeks. Don't ask how I know this. To make matters worse, it only takes one dood to sprinkle such gems throughout a significant portion of your codebase.

If You Were Looking For an Easy Fix, You Will Be Disappointed
WIthout going into a lot of gory details about How To Write A Memory Manager in C++, I'm going to have to gloss over some details (besides, that's for a different post). The basic gist of how to deal with fragmentation is to have a decent idea of how your program's memory is going to be used and be uber-careful with anything that might allocate.

For the example above, we know that both string and vector are going to allocate at least some memory. We can provide an allocator to it or remove them completely since, at least in this example, we don't actually need most of what they do and our list is hardcoded anyway. We can further make this better by having a temporary heap that we know is going to be churning a bunch and pass that into the LoadAsset function so our two temporary assets don't fragment the main heap either. At that point, we can optimize the heap function for the temp heap to prevent fragmentation since we're going to hammer it (and believe me, once you have such a heap, you will hammer it). Here's a somewhat less fragmenty version:


struct AssetLoadInfo
{
const char *mAssetName;
uint mHeapIndex;
};
AssetLoadInfo *c_StuffToLoad[] =
{
{ "some_prefs.xml", c_tempHeap },
{ "a_texture.dds", c_defaultHeap },
{ "this_is_temp.dds", c_tempHeap },
{ "big_honkin_asset.stuff", c_defaultHeap },
{ "another_asset.stuff", c_defaultHeap },
{ NULL, 0 }
};

BOOL LoadAsset( const AssetLoadInfo &rfAssetInfo );
BOOL UnloadAsset( const char *AssetName );
for ( uint ii=0; NULL != c_StuffToLoad[ii].mAssetName; ii++ )
{
LoadAsset( c_StuffToLoad[ii] );
}
UnloadAsset( "some_prefs.xml" );
UnloadAsset( "this_is_temp.jpg" );


We aren't talking rocket science here, but it does require programmers to carefully consider what they're doing. C++ makes it stupifyingly easy to make a mess of things and you just don't want to get yourself backed into a corner with fragmentation when your game is being released on a console. People don't like it when their games crash (believe me on this one).

There's WAY more to it than just small optimizations like the one above. Having a proper memory strategy, making sure that you have a proper memory budget, and making your own memory allocation devices are all super important in memory limited situations and can save you an awful lot of painful debugging. See you next time for the next installment of "Stuff They Never Tell You About Game Development"!

2008/05/09

MIND THE GAP!

Ever been on one of those projects where someone (usually the leadership) thinks it's going way better than it actually is? If you've spent a reasonable amount of time in software, you probably have. Here, then, is my sole addition to the software engineering lexicon. Feel free to use this whenever appropriate.

    The distance between the actual doneness of a project and the perception of doneness is hereby defined as the Reality Gap.


So, assuming you had some magical way to determine precisely how far along on a project (or task, or whatever) you are, the distance between there and where you think you are is the Reality Gap. If the Reality Gap is large, then, well, your team may be tremendously optimistic or, sadly, in denial. Welcome to deathmarch.

I mention this for no reason whatsoever. None.

2007/11/10

What You Should Know About Load Times

(cleverly abbreviated as WYSKALT)

If I have time to get pissed off and say "I can't help but notice that I'm not playing your game RIGHT NOW," then it means your load times suck and I'm probably wondering why I bought your game in the first place.

This seems bad!

Loading your game off media isn't rocket science. I'm going to assume that we're loading off some kind of disc with a 'c' media like in a console game rather than your hard drive like on your PC of choice. However, anything applied to optimize load times on disc-with-a-c will also optimize disk-as-in-hard-drive as well. I'm not going to cover everything with load times in this post, but this is the absolute bare minimum of stuff you need to know but I couldn't fit it in quite as catchy an acronym. There's lots more stuff that will make load times even better which I might rant about at a later date.

So at any rate, here we go.

Opening Files Sucks
Most times we don't notice this. Occasionally you run into something where you need to load N files which are kinda small but where N is something gigantic (like 1000). Then you notice--even if you have an uber fast machine. On this generation of console, N is usually on the order of thousands for a given level load for those who might be curious.

Fixing this is uber-easy: pack all your files into one giant uber-file so you only ever need to have one or at most a small number of file handles open. We often know of these as zips, wads, paks, pigs, etc. (The one I use for BEER is "keg". Clever, no?)

This makes load times suck less which is the goal. Sadly, they still suck.

Loading Non-Sequential Data Sucks
So now you've got your data all archived and it's time to load it. Surprise! It still takes a long time because you're constantly seeking around in your uber file. I can hear you smug Win32 people scoffing--but remember that your file mapping eats virtual address space and I know that since you're still using WinXP that you only get two gigs of VM. Ha!

This is pretty easy conceptually: stick all the data in your archive in order. The bad news is that depending on your particular data set and load time trickery, this might be really hard in practice. Chances are good that it leans toward the really hard side because you might only load some subset of your data and probably don't have a fixed order for it. If you can make your loading respect a fixed order then the problem moves pretty quickly to the easy side again.

I've found that in most cases the best we can hope for is that some subset can be made to lean toward "most" for fixed sequentiality. Sometimes you can change the loading code to do nicer things; sometimes you can't for <fill in the blank> reason that fits your situation. In either case you need to generate a sequence list for some set of your data then stick all the data in your archive in that order as best you can. Neither of these are particularly difficult assuming you have source code and enough time to do stuff and an archive builder that doesn't totally blow chunks.

Why does this work? Once file open issues are dealt with, seek time tends to be the dominant factor in unruly data sets. On discs-with-a-c, each seek is going to weigh in at roughly between 50 and 150 milliseconds depending on your particular hardware. So, if you're loading 10MB of data but seeking 100 times, your load bandwidth for that data can be no better than 1MB/s in the best case (assuming a flat 100ms/seek which is normal). If you try to load a gig of data with that bandwidth, then, well, I'll not be alone in returning your game.

So: if you can significantly reduce the number of seeks your game needs to do, you can significantly reduce the load time of your game. I've seen particularly poorly loading data sets extend the load time of a 100MB data set more than five minutes. For those in the audience reaching for calculators, that's a bandwidth of less than 1MB/3seconds. Glacial.

But Our Game Is On the Hard Drive!
Don't care. These will still make your loads better. Your seeks are still on the order of 8ms and your bandwidth isn't free. Wait till your users start fragmenting the crap out out their drives. Galactic Civilizations is a reasonably fun game but its load times are absolutely brutal. Why? Tons of tiny files none of which seem to be in easy-to-load formats just sittin' there on the disk. Lots of games are like this for the PC and it's a damned shame.

Oh! You did that for your modders, did you? Guess what; you can still put most of your stuff in order in archive. You can even give modders your packing tools so their stuff loads fast too! Sure, you have to design for it but you're already designing extra stuff in for modders, right?

Odds, Ends, and Disclaimers
As mentioned atop, this is the bare minimum you need to know about load times. There are lots of other things you can optimize to make your loads suck less but I'll save that for another rant.

This has been a public service announcement paid for in part by the "Your Game Loads Too Slowly" foundation of Maryland and made possible by viewers like you!

2007/11/03

Script-fu

Not terribly long ago, I was a self-proclaimed scripting hater. "Oh sure," I said, "You can do some neat stuff in script-land with your not-real languages and your dynamic types. But wow, my C++ can do all of that and uber-more!" Yeah. So it's time to fess up and admit that I might have, ever so slightly, been somewhat mistaken on some 'a that.

I've had this completely irrational thing for Ruby for, oh, about a year now which mostly manifested itself as not doing Ruby. This (irrational thing for Ruby) is mostly the fault of the blogosphere, notably one Steve Yegge who, despite often being somewhat long winded with a propensity for ten cent words and compilers, is also quite often terribly amusing and a lot of the time spot on. He's got loads of stuff trumpeting the awesome that is Ruby. So I bought a book and read some online tutorials and then got a new job and moved cross country and got tied up in godawful amounts of stuff that's not ruby and wow is that a pretty laptop I should totally get one of those while not doing Ruby.

I used to know perl in much the same way that most people who knew (past tense) perl once did. That is to say, I have absolutely no ability to do perl now because a) it's very obtuse, b) it looks like line noise, and c) it's so unbelievably obtuse that sane human beings flee in terror when confronted with it. Have I mentioned its obtusitude?* Some have described Ruby as "perl with the suck removed" and as I mentioned before, I have a thing for Ruby.

<yet another entry for the worst segue ever category>

I'm now in charge of making load times not suck on my current project. I'd built a couple tools to build giant archive files and to parse in-game spew to do various things that are reasonable and useful toward that goal. I'd built them in C++. This in and of itself is not odd since by and large, I'm a C++ programmer. What I did notice is how godawful difficult seemingly simple things are in C++. This also is not odd since by and large, I'd been noticing that on and off for the last, say, decade. So moving from "hey, I think I can fix our load times" to "holy sweet moby jebus, I have to fix our load times!" I decided that I needed better tools.

Enter Ruby, stage left.

So I used it in a real "have to get this crap working right now kind of situation...and it was good.

So now I gotta take back some of the mean things I said about scripting languages and the people who promote them. Uncle already. I may have been a bit hasty with all of that. Ruby has made the otherwise unbelievably daunting data mangling tasks I've got slightly less unbelievably daunting because now at my very fingertips I have the power to mangle data like never before! Data the world around, tremble before my gemstone inspired might!

I'll save some gory details and horribly contrived examples from Teh Real World(TM) for a later date. So, if you're like me and only really ever looked at your primary statically-typed language for solving all manner of problems, you might go and pick up a scripting language just cause. It might save you some unbelievably daunting.

*obtusitude is not a real word. I made that up, honest.

2007/10/04

Random or Not?

Here's a not-quite-hypothetical problem: you have a fixed size graph and want to arrange N points on that graph in a random manner. You could use such a thing for lots of different stuff not limited to distributing player positions on a map, etc.

So how do you do it? Feel free to code along at home.

Totally random.
First try: completely random. We'll just use our good friend rand() with some "proper" seed to choose X and Y values and mod them into the map space. Smashing. If you do that, it looks something like those on the left.

I saw the results and wondered where my bug was cause that sure as crap doesn't look random. Hint: there was no bug.

Small influence circles.
Hokay. Welp, I don't want those points to be within N distance of one another where N is something I can calculate easily because, er, I want it to run fast (not because I'm lazy--no not that at all).

Closer...

If you were looking for a point, you may have just found it: when I say random in this particular context, really mean evenly distributed rather than really random. In fact, truly random is not what I want at all.

Final results.
On the left here are my final results after a mess of hacky heuristics. It's not the prettiest code but it seems to give good results on every map I've generated and it has a calculable upper bound on computation time. As a final step not shown here, I jitter each position inside the grid line so even points that are on the same row or column on the grid appear a little less regular (left as an exercise for the reader).

Now, I freely admit that I'm one to run off and code something because a) I figure I might learn something, b) I need the practice, and c) I reall like coding. Try as I might I was unable to find a better solution than the hackery I came up with. Apparently my google-fu is not as righteous as I thought. I'm pretty convinced that there's some crazy technique to do this in like 3.47 instructions on a 68020 or whatever but I sure as crap can't find it. If anyone knows of one, let me know.

(If anyone wants the code, just ask.)

2007/01/07

Thousander Club

The Thousander Club is a challenge issued last year by some of the folks at the Low Poly Coop based on an article on GBGames. Original article here with 2007 followup here. The basics of the article was that you need roughly 10,000 hours over the course of 10 years to achieve basic mastery of a subject. Numbers aside, this doesn't seem unreasonable so long as those putting in the time are actually trying to learn, get better, don't get their brains smooshed by aliens or whatever. So the Thousander Club is a group of people trying to put 1K hours into their choice of things to practice while posting their progress in a public place.

I ran into this last year and was fascinated by it, but the ups and downs of stuff and the lateness in the year (Octoberish) meant that I wouldn't put up big numbers. I did manage to log 187 hours total from September 14th to December 31st, notably the first day of BEER development and the last day of GWA. So this year I've thrown my hat in the ring with these guys to see what I can do. I expect that this will be a hard year working two jobs and working through a year of crunch so, sadly, I already forecast a pretty major failure to hit 1000. Nonetheless, it'll be an adventure and I'm looking forward to it.

2007/01/01

Happy New Year

Welp, another year has come and gone. Here's hoping that y'all had an awesome holiday season this time 'round.

I know it's a tradition to list your New Year's Resolutions somewhere (like, say, your blog) but I'm not going to. Oh no. Given my past track record for sticking to them (exactly 0.000%), I'm not going to jinx it. Instead, I'll be keeping them private, thanks much, in order that I might actually accomplish some of it. Though, to be painfully honest, they're the same things I've been working toward for a while so it's not like you're missing any huge bold statements or anything.

For the few people that are wondering how last month's project went, I present the following screen shots:
I hate writing UIs.

OMGWTFKABLOOIE!
If all goes well, I'll be following this post with a download link and more information. Contained within "more information" would be a postmortem of the project selections of which will be posted here. Also therein will be two versions of the design doc, one as designed, and one old version annotated with my notes.

2006/11/28

Programming Evil #143

I hope everyone had a good holiday weekend but that's not why we're here. Oh no. We're here to talk about programming evils and in true rant style I'm going to number these but not in any rational way.

I've found a simple piece of code that seems benign enough (details hidden to protect the guilty):

byte buffer[ BUFFER_SIZE ];
sprintf( buffer, "map_path/%s", szGetMapName() );

Astute readers might note that if szGetMapName() happens to return a really big name or a NULL, that bad things might happen to the game. Me, in an attempt to be a good worker bee and concerned with security, decided I'd change it thusly:

byte buffer[ BUFFER_SIZE ];
snprintf( buffer, sizeof(buffer), "map_path/%s", szGetMapName() );

except that this now errors out on every overrun rather than crashing. Better, but still not what I was going for. Note that standard C snprintf does not error out or anything weird. It is, in fact, quite a well-behaved function. We've overridden snprintf to our own crazy version of snprintf that errors on buffer overflow. Our version is not quite so well-behaved.

I hereby christen this as Programming Evil #143: If you override standard functions, don't change the functionality!

In this particular case, the chance of buffer overrun is almost nil and the function played by szGetMapName() can't return NULL and the define played by BUFFER_SIZE is pretty big making it exceptionally hard to overrun it. Regardless, I wanted to make it more bulletproof--an attempt that has been quite thwarted.