<-- Chapter 13 | Back to Index | Chapter 15 -->
Yesterday, we began to manage manage our game state. We did this with a bunch of local_persist variables in our GameUpdateAndRender() function. This isn't ideal, as these local_persist variables are basically globals, and rather disorganised.
The first thing we'll probably want to do is to move all of our game state into a game_state structure. So we'd have such a thing as:
struct game_state { int BlueOffset; int GreenOffset; int ToneHz; };We can then pass a pointer to the game_state to our GameUpdateAndRender() function.
We're going to need memory to put our game state (and assets like graphics and sounds) into. There are a couple of strategies we can have to manage this memory. A popular way to do this is to use malloc() or the C++ new operator. These let us allocate a new bit of memory for our struct. For example, we'd have a function to allocate and initialise an object:
internal game_state* CreateGameState() { game_state *State = new game_state; if (State) { State->BlueOffset = 0; State->GreenOffset = 0; State->ToneHz = 256; } return State; }This is a common technique, but it introduces a problem: this can fail. If we allocate a lot throughout the game, we could run out of memory halfway through. It can also be problematic on consoles, where you have to deal with memory fragmentation as well. Finally, it also introduces another place where the game code will need to call into our platform code (if we use new or malloc() in our game code, it'd suddenly be platform-specific).
Instead, we're going to allocate a large chunk of memory up front, and just pass it to our game. We'll create a game_memory struct to handle it:
struct game_memory { uint64 PersistentStorageSize; void *PersistentStorage; uint64 TransientStorageSize; void TransientStorage; };You'll note that we actually are allocating two blocks. Sometimes we'll need to allocate memory that we won't free until the program quits. We can take advantage of that if we keep them in a different blocks.
Neat trick! It's often worth grouping memory allocations by their lifetime. This lets you free or reuse large blocks of memory at once, which is more efficient. |
We don't know for sure how much memory our game will need yet, but we can make a guess: 64 MB of permanent storage and 4 GB of transient storage. Because we have to specify the sizes in bytes, let's make some macros to ease our way:
#define Kilobytes(Value) ((Value)*1024LL) #define Megabytes(Value) (Kilobytes(Value)*1024LL) #define Gigabytes(Value) (Megabytes(Value)*1024LL) #define Terabytes(Value) (Gigabytes(Value)*1024LL)Simple though it looks, there are actually a couple of tricks in these macros which you'll have to keep in mind. Firstly, we're multiplying by 1024, not 1000. This means that we're technically using "Kibibytes", "Mibibytes", etc. Since no-one can say "Gibibyte" with a straight face, we'll just use the wrong terms for now: 1 KB = 1024 B is a widely accepted definition. Secondly, we have these LL suffixes: these tell the compiler that we want to be dealing with 64-bit numbers (the LL stands for long long, which is a type which most compilers have as 64-bit). Otherwise, we woudln't be able to represent sizes greater than or equal to 4 GB, which would be something of a shame.
To allocate this structure, we'll be using mmap(), which you might remember from Chapter 4.
game_memory GameMemory = {}; GameMemory.PermanentStorageSize = Megabytes(64); GameMemory.TransientStorageSize = Gigabytes(4); GameMemory.PermanentStorage = mmap(0, GameMemory.PermanentStorageSize, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); GameMemory.TransientStorage = mmap(0, GameMemory.TransientStorageSize, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);It'd be nice if this memory was initialised to zero for us, and if we read the mmap() man page, we find that anonymous mappings are zeroed by default.
It would be nice, for debugging, if we could always have our memory have the same address in virtual memory. This way, the memory layout would be the same in debug builds. Let's start be working out what we mean by a debug build. Rather than just having a debug build and a release build, we'll separate out versions along two axes.
-DHANDMADE_INTERNAL=1 -DHANDMADE_SLOW=1
Now we can set the initial address of our memory. On Windows we have to be careful, though, if the range of pages we request is not available, the allocation will fail. We therefore only manually set the address in HANDMADE_INTERNAL builds. On Linux, this is not a big deal: if the range we request isn't available, we'll just get a different range (unless we specify MAP_FIXED). We'll follow the Windows behaviour anyway, though, as it is possible that the operating system security features would like to randomise our address.
#if HANDMADE_INTERNAL void *BaseAddress = (void *)Terabytes(2); #else void *BaseAddress = (void *)(0); #endif game_memory GameMemory = {}; GameMemory.PermanentStorageSize = Megabytes(64); GameMemory.TransientStorageSize = Gigabytes(4); uint64 TotalStorageSize = GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize; GameMemory.PermanentStorage = mmap(BaseAddress, TotalStorageSize, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); GameMemory.TransientStorage = (uint8*)(GameMemory.PermanentStorage) + GameMemory.PermanentStorageSize;You'll notice that we're now allocating one large block and subdividing it. We might reconsider this decision so that we can catch errors later, but for now it works. You'll also note that, by default, we're setting the BaseAddress to 2 TB. This is obviously inaccessible on 32-bit systems, though we wouldn't be able to allocate 4 GB of transient storage on these systems either. If you're following along on a 32-bit system, lower these numbers a bit.
We're making a number of assumptions throughout our code. We need to both document and validate these assumptions, and we can do that using an assertion. If we want to ensure that a particular expression evaluates to true, we can simply have some code that checks it, and notifies us in some fashion if it is false. We typically do this with a preprocessor macro:
#if HANDMADE_SLOW #define Assert(Expression) if(!(Expression)) {*(int *)0 = 0;} #else #define Assert(Expression) #endifIn this case, we're deliberately crashing our program if our assertion is false. Eventually, we'll change this to pop up a message, but crashing deliberately is a good way of making the debugger stop on the line that failed. The other thing we're doing is disabling the assertion if HANDMADE_SLOW is not defined. This lets us remove all of the extra checking code when we're not trying to debug things. It also means that we can add as many assertions as we want without fear that they will make our game too slow.
We can assert, for instance, that we get back a valid pointer with:
Assert(GameMemory.PermanentStorage);Eventually we'll want a nice user-friendly error to, uh, encourage them to buy more RAM, but an assertion is fine for now.
Be warned! This Assert() macro doesn't behave quite like a function. For example, if you were to
write code like:
if(blah) Assert(1==2); else blah;it won't work. The way to solve this it to wrap the contents of the macro inside a do { } while(0)We'll look at this in more detail at a later date. |
It's nice to have some memory allocated, isn't it. Tomorrow will be file IO: how exciting! As tomorrow's stream is at 11:00 AM PST, I'll be unable to watch it live, and hence the tutorial will be somewhat delayed. Sorry!
If you've bought Handmade Hero, the source for the Linux version can be downloaded here. Note that you will require the official source code for handmade.h and handmade.cpp.