Fixing Memory Usage Issue for Noah’s Ark

I recently ran into a memory usage defect for my Noah’s Ark game. Basically, as the player scrolled to different tile maps in the overworld, memory usage would continue to climb, eventually bringing the game to a crawl. The fix was really simple but involved some more subtleties of C++ smart pointers, so it seemed worth writing about.

Graphics Management
First, it is important to understand how graphics are currently managed in the game code. Different types of graphics components are created from the GraphicsSystem. The GraphicsSystem is then responsible for rendering all of the graphics on screen.

I’m not necessarily sure this is the “best design” for the game (if you have other ideas, feel free to suggest them!). It is just something I’m experimenting with from various things I’ve read (mostly on entity component systems or ways to avoid having all of my game objects have explicit render methods). My main goal was to be able to easily apply global settings to all of the rendered graphics without having to touch a bunch of classes. Time will tell how well this works out.

Map Scrolling
The second major piece of the puzzle is to understand how the game scrolls between individual tile maps in the overworld. At any given time, the OverworldMap holds the current, top, bottom, left, and right TileMaps (assuming they exist):

The tile maps in memory at any given time in the Noah's Ark game.

When the player reaches the edge of the current map, the game scrolls to the appropriate adjacent map (if one exists), and then reloads any new maps surrounding this new map being scrolled to.

The Bug
There is a bit of a problem with the above approach, if you look at the details of the specific data types used. The graphics components created by the GraphicsSystem are returned as shared_ptrs and were (before the fix) also being stored as shared_ptrs in the GraphicsSystem class. These shared_ptrs are then used by whatever objects the graphics are for. In this case, these shared_ptrs were also used by Tiles in the TileMap class.

When scrolling to new TileMaps, the old TileMaps would be destructed, decrementing the reference counts for the shared_ptrs for Tiles in the map. However, since the GraphicsSystem still stored these shared_ptrs for the tile graphics, the reference count would still be 1, so the tile graphics would persist in memory. New maps would be loaded, creating new tile graphics components, but all of the old tiles would still exist. Obviously, this is problematic and leads to increasing memory usage over time, which ended up slowing the game to a crawl.

The Solution
I actually was aware of this problem during original implementation of the design outlined above but forgot about it by the time the defect actually manifested itself. This was a pretty major architecture problem, but I didn’t want to abandon the idea of having a centralized place (away from the main game objects) to control rendering.

I couldn’t use unique_ptrs since the graphics components needed to be accessible by both the GraphicsSystem and the individual objects the graphics are for. There are ways around this, such as using raw pointers, but that would make communicating when the resources were to be deleted from the GraphicsSystem pretty ugly (I also don’t like the lack of clarity that raw pointers can present when reading code, but that is for another blog post).

Fortunately, C++ had an elegant solution. Enter the weak_ptr. The description on the linked page is best to read for an understanding (and examples help), but a weak_ptr allowed me to break the > 0 reference count problem described above in a pretty nice way.

When an object like a Tile (part of a TileMap) is destructed, I wanted it to communicate to the GraphicsSystem that the graphics for the Tile could be deleted. To accomplish this, I made the following changes:

The GraphicsSystem was changed to store its graphics components as weak_ptrs instead of shared_ptrs:

std::list< std::weak_ptr<IGraphicsComponent> > m_graphicsComponents;

One nice thing is that the weak_ptr class has a constructor that allows it to be constructed from a shared_ptr, meaning I didn’t have to touch the code that added items to the above container.

Next, I had to update the for loops that iterated over the graphics components to use weak_ptrs instead of shared_ptrs. Before updating/rendering an individual graphics component, I needed to check if the object had already been deleted elsewhere (i.e. the Tile destructor) using the expired method:

if (!graphicsComponentWeakReference.expired())

(See the Wikipedia page for an overview of the concept of weak references.)

Assuming that the weak_ptr hasn’t expired, then the lock method can be called to obtain a usable shared_ptr to the object:

std::shared_ptr<IGraphicsComponent> graphicsComponent = graphicsComponentWeakReference.lock();

Tada! We’re almost done. The above code allows for checking if a graphics component has been deleted elsewhere, but it doesn’t actually solve the problem of increasing memory usage. The weak_ptrs are still stored in the list, so they need to be removed at some point. Fortunately, the list container has a remove_if that allows us to easily remove elements from the container that meet a certain condition (without resorting to the erase-remove idiom). Removing┬áthe expired graphics components can be done in what is effectively a single line of code:

m_graphicsComponents.remove_if([](std::weak_ptr<IGraphicsComponent>& graphicsComponent) { return graphicsComponent.expired(); });

That’s it! You can browse the full set of changes for this fix here. Finding and fixing this bug was a good learning experience for me, so hopefully it will be of use to some of you.

Leave a Reply