>> Devlog 03 - Memory <<
As I had talked about batching sprites together, I was getting a little concerned. The Jaguar has 2MB RAM available, and I knew that large objects can very very quickly eat up memory space - that was the reason for splitting the scene into sprites to begin with.
At this point, I honestly don't know how tight memory usage is going to be, but I'm assuming somewhere in the region of "very". However one thing was obvious - I'm going to have to keep an eye on the memory usage. If I were writing in assembly at this point, I might maybe try to manually specify memory locations for everything I need - I may still do this. But for now two things make this a bit more difficult. Firstly, I'm still writing mostly in C - while I can specify memory locations to place things, C's stack and heap doesn't necessarily respect the fact that I just used that location. The other thing is that, at this point, I don't know what I'm doing. I have ideas for gameplay, ideas for story, ideas for implementation. None of them are anything like fully final. It still feels like I'm very much in a prototyping phase still. I don't believe that I have the experience to create a flexible memory map, or a solid enough design to just wing it.
So. Right now the very hacky way that the game is written is, most things are stack allocated in main, and pointers stored globally - this is because I seem to have a bug where, for some reason, allocating anything globally just doesn't work. I haven't looked into why, but globals are bad, so I didn't worry about it too much, figuring I'd remove the pointers anyway.
So, I've written allocators.
I have written three allocators. A final one will be added, but that will basically be a NOP, with the intention being to use it to block out memory, which can, at a later date, be used as above, in a more standard memory map.
The first allocator to be implemented was an arena allocator. This works by not actually reusing memory, but just continuing to allocate at the end. Then, at a later point, the allocator is reset. This can be used for per-frame allocations, with the allocator automatically reusing the same memory every for every frame, with no need to manually free it. In my initial test case, this is used for the room data. When you enter a room, it starts populating it, and this is then all allocated from the room alloctor. When you change room, this allocator is flushed, and the new room can start fresh. On debug builds, each allocation is prefixed with some metadata including the source file and line number and size, so allocations can be tracked. In release builds, nothing is stored. In release builds freeing results in basically a NOP, while in debug it does mark it as being freed, so when the allocator is finally flushed, some information about how much memory had been previously cleaned could be provided.
The next allocator is a stack allocator. This requires all deallocations to be made in the reverse order as the allocations. Debug builds will check for this and can assert if this is not the case. This is currently in use just for UI, where opening and closing UI elements can follow this pattern. Like the arena allocator, this will store some additional information about the allocation in debug builds in front of the allocation, however in release builds it will just push the stack back to the element that has just been popped off. As long as the logic doesn't change between debug and release (it shouldn't!) this is fine.
Finally, a fixed size allocator. The intention for this is that it acts like an pool of objects. There are currently two in use, one for the character objects, and one for sprites. No additional information is stored about each allocation, and unlike the other allocators, there are very few differences between debug and release builds. This functions by defining a bitmap at the beginning of the allocator. This is one bit per element. It acts as a flag to show which elements have been allocated, and which are free. 1 is defined as being available, and the bitmap is set to 0xFF. This means that I can step through the bitmap in bytes, checking if any are non-zero, before looking at exactly which bit, and therefore which element, is able to be allocated.
The only thing remaining to allow full control over memory is the system allocations. Using libraries by theRemovers meant that a few of the things used malloc or calloc internally. It was fairly trivial to create my own functions for these which just passed to a stack allocator. A couple of the libraries needed to be slightly rewritten to ensure deallocating in the correct order. This then meant that every dynamic allocation in the game would run through one of these allocators.
I put these at the bottom of RAM, from 0x1000, and defining the size of each, meaning that they automagically laid themselves out in memory. I could then write a simple printer, which would interrogate the allocator's current state, and let me know how much memory I am using. I have sized the allocators to being "sensible" for now, which means I have a bit of headroom over what I'm currently using. These allocators are using roughly 700kb of memory. To modern eyes, that's a tiny amount, but keep in mind that I have only 2MB to play with. The intention is therefore to keep the bottom 1MB for dynamic allocations, and use the top 1MB for things I can manage more directly, such as for example the current music track being played. This is not totally accurate, as the stack also grows down from the 2MB mark, but I'm checking that it doesn't grow above 64KB, so I can still use the majority of the top 1MB for these allocations.
Still to be added is adding a pattern to the last few bytes of the allocator (0xDEADBEEF, 0xCDCDCDCD etc) and then check that this remains intact in debug builds, whenever an alloc or free is called on that allocator, to ensure that nothing overwrites them. Also, once the game has game states, any error in the allocators in release should push a debug error state, probably styled after a Windows BSoD.
The allocators haven't improved the game, they haven't added any gameplay features, they feel very much like busy work or perhaps yak shaving. However they are making me more confident about my ability to make an actual game going forward, now that I can be more in control, and more aware of memory usage.
I am going to have to draw up physical memory maps to ensure I know what memory is going where.