Hot Code Reloading

The videogame development industry relies heavily on fast programming languages such as C/C++, for obvious reasons, but most of the time also some scripting language can be used on top of the core layers of the engine. Languages like Python, C# or Lua allow for an easy and modular production of gameplay code, which is often based on the fine tuning of several parameters (especially at the final stages of development). Without a scripting language it would be necessary to recompile and relink the code for each tiny modification of any parameter, and this of course lowers the productivity of the entire team. A piece of code written with a scripting language can be modified and reloaded at runtime, a technique usually known as hot code reloading. However, the integration of several different programming languages increases the overall complexity of the engine, and requires to write one or more very robust parsers that are capable of translating the information from one language to the other. The aim of this post is to describe a technique for hot code reloading that doesn’t require to use a scripting language, but can be directly implemented in C/C++ with a slight modification of the engine architecture. Yes, you heard me well, it’s actually possible to leave the executable running and to modify any piece of code at runtime, immediately seeing the results and eventually keep tuning the transparency of that pesky particle system. The first time i even heard of this method was during an episode of Handmade Hero, the awesome streaming about game engine coding from Casey Muratori. I’ll simply describe my own implementation and my experience in using this feature, possibly also demystifying that black magic feeling behind it!

 

Requirements

The first key component for the implementation of hot code reloading is the separation between platform code and game code. This feature is already a good idea without hot reloading in mind, because it makes easier to port the game to a different platform (even on console) since the code that interacts with the operating system is decoupled from the actual game. Platform and game can interact in the same way a scripting language interacts with the engine, provided that the platform code is built as an executable and the game is built as a dll. The platform code keeps the entire application running, while we can modify the game dll at will and reloading it each time a new recompilation process is sensed by the platform itself. In particular, since the game is going to be imported just like a library, we need to identify and group its main functions in order to export them as extern function pointers. For example we might choose to develop game logic and rendering as one library, audio processing as another and finally also a library for eventual debug services. Of course each of these function pointers can contain calls to many other functions, just like a main function contains the entire codebase in itself, even if technically the main function of a dll is a little bit different thing and it is optional. I have used three general arguments for our function pointers (memory, inputs and render commands) that need to be provided by the platform code, but of course the entire system can be expanded if needed. It’s important to remember that the following code has to be defined in a header file which is visible from both platform and dll code:

// At first we introduce a macro that simply defines a function, and then we define a C-style function pointer. 
// This allows to eventually introduce a "stub" version of itself, for example in order to handle cases of failed initialization
#define GAME_UPDATE_AND_RENDER(name) void name(game_memory* memory, game_input* input, game_render_commands* renderCommands)
typedef GAME_UPDATE_AND_RENDER(game_update_and_render);

#define GAME_GET_SOUND_SAMPLES(name) void name(game_memory* memory, game_sound_output_buffer* soundBuffer)
typedef GAME_GET_SOUND_SAMPLES(game_get_sound_samples);

#define DEBUG_GAME_END_FRAME(name) void name(game_memory* memory, game_input* input, game_render_commands* renderCommands)
typedef DEBUG_GAME_END_FRAME(debug_game_end_frame);

// We specify that this function is going to be exported. Extern "C" is for name mangling
extern "C" __declspec(dllexport) GAME_UPDATE_AND_RENDER(gameUpdateAndRender)
{
	// Implement the game
	...
}

// Repeat also for audio and debug services
...

The second key component to a successful hot code reloading is to handle correctly the memory. If we ask for memory in the game code, in fact, after the reloading process all pointers are not going to be valid anymore. This is another good reason to keep platform and game as separated chunks! The platform code, being the one responsible for the comunication with the OS, acts as the gatekeeper for memory and it is the one that holds the pointers. In this way, even when the game is reloaded the pointers to memory are still going to be valid because the platform code is always running. The game of course has the right to ask for more memory, but the request has to pass through the platform layer every time. Many game engines that i know already apply this strategy for memory management (which is a big allocation at startup followed by eventual expansions when needed), but if your engine absolutely needs to call new or malloc every time also in the dll then maybe hot code reloading can be a tough feature to introduce in your codebase. For this reason, it would be ideal to plan for hot code reloading during the early stages of development.

 

Implementation

Using Windows and the Win32 API, the actual code reloading can be achieved by retrieving the function pointers with GetProcAddress and assigning them to pointers of the platform layer. This of course needs to be done once before the main game loop, and can be repeated each time the current dll write time is different than the last one. As you can see in the next code snippet, every time we load the game code we also save the current write time in the win32_game_code struct:

struct win32_game_code
{
	HMODULE gameCodeDLL;
	FILETIME lastDLLwriteTime;
	
	game_update_and_render* updateAndRender;
	game_get_sound_samples* getSoundSamples;
	debug_game_end_frame* DEBUG_EndFrame;

	bool32 isValid;
}

inline FILETIME getLastWriteTime(char* fileName)
{
	FILETIME lastWriteTime = {};
	WIN32_FILE_ATTRIBUTE_DATA data;
	if (GetFileAttributesEx(fileName, GetFileExInfoStandard, &data))
		lastWriteTime = data.ftLastWriteTime;

	return lastWriteTime;
}

win32_game_code loadGameCode(char* sourceDLLname, char* tempDLLname, char* lockFileName)
{
	win32_game_code result = {};

	WIN32_FILE_ATTRIBUTE_DATA ignored;
	if (!GetFileAttributesEx(lockFileName, GetFileExInfoStandard, &ignored))
	{
		result.lastDLLwriteTime = getLastWriteTime(sourceDLLname);

		CopyFile(sourceDLLname, tempDLLname, FALSE);
		result.gameCodeDLL = LoadLibraryA(tempDLLname);
		if (result.gameCodeDLL)
		{
			result.updateAndRender = (game_update_and_render*)GetProcAddress(result.gameCodeDLL, "gameUpdateAndRender");
			result.getSoundSamples = (game_get_sound_samples*)GetProcAddress(result.gameCodeDLL, "gameGetSoundSamples");
			result.DEBUG_EndFrame = (debug_game_end_frame*)GetProcAddress(result.gameCodeDLL, "DEBUGGameEndFrame");
			result.isValid = (result.updateAndRender && result.getSoundSamples && result.DEBUG_EndFrame);
		}
	}

	if (!result.isValid)
	{
		result.updateAndRender = 0;
		result.getSoundSamples = 0;
		result.DEBUG_EndFrame = 0;
	}

	return result;
}

void unloadGameCode(win32_game_code* gameCode)
{
	if (gameCode->gameCodeDLL)
	{
		FreeLibrary(gameCode->gameCodeDLL);
		gameCode->gameCodeDLL = 0;
	}

	gameCode->isValid = false;
	gameCode->updateAndRender = 0;
	gameCode->getSoundSamples = 0;
}

The arguments of the loadGameCode function are three strings: source, temp and lock names. The first two are the paths to the locations of game dll and a temporary file that holds its copy, and they can be hardcoded or can be determined at runtime using a combination of Win32 functions (such as GetModuleFileNameA) and string manipulation. The last one, the lock, has to do with Visual Studio locking the .pdb file even after the dll is unloaded. One way to overcome this issue is to force Visual Studio to generate a pdb with a different name every time the code is recompiled, for example using the following expression:

// Right click the DLL project and select Properties->Configuration Properties->Linker->Debugging->Generate Program Database File
$(OutDir)$(TargetName)-$([System.DateTime]::Now.ToString("TMA_mm_ss_fff")).pdb

However, now the pdb files are going to pile up in the output folder after every recompilation. We can define a pre-build event for the dll in which we delete all .pdb files: 

// Right click the DLL project and select Properties->Configuration Properties->Build Events->Pre-Build Event
del "$(Outdir)*.pdb" > NUL 2> NUL

The final issue we have with the .pdb file is that the MSVC compiler actually writes the dll file before it, and therefore during code hotloading the dll is going to be loaded immediately while the .pdb still has to be written. At this point Visual Studio would fail to load the .pdb, and debugging the code after reloading would be impossible. This is why we create the lock file passed as argument in loadGameCode! This lock is just a dummy file that is created in a pre-build event and deleted in a post-build event:

// During pre-build event
del "$(Outdir)*.pdb" > NUL 2> NUL 
echo WAITING FOR PDB > "$(Outdir)lock.tmp"

// During post-build event
del "$(Outdir)lock.tmp"

During its lifetime, the lock file is a living proof that the code is still being loaded. For this reason, in the loadGameCode function we check if the lock file is not present by calling if (!GetFileAttributesEx(…)) and only under this circumstance we proceed to load the dll. This ensures that also the .pdb file has been written and allows to debug the code as usual.

Finally, let’s see how to actually trigger the hotloading after each recompilation:

// Before game loop (platform layer)
win32_game_code game = loadGameCode(sourceGameCodeDLLpath, tempGameCodeDLLpath, gameCodeLockpath);

...

// Inside the game loop (platform layer), between the update and the render
FILETIME newDLLwriteTime = getLastWriteTime(sourceGameCodeDLLFullPath); 					
bool32 isExeReloaded = false;

// Check if the name of the DLL has changed. Since we create a unique DLL name each time, this event
// indicates that a code reloading needs to happen
bool32 doesExeNeedReloading = (CompareFileTime(&newDLLwriteTime, &game.lastDLLwriteTime) != 0);
if (doesExeNeedReloading)
{
	// If the code is multithreaded and a work queue is implemented, complete all
	// the task now because the callbacks may point to invalid memory after the reloading
	...
	
	// If the debug system is designed to record events along the codebase, now it's the
	// time to stop it
	...

	unloadGameCode(&game);
	game = loadGameCode(sourceGameCodeDLLpath, tempGameCodeDLLpath, gameCodeLockpath);
	isExeReloaded = true;
}

That’s it! A current limit of this hot code reloading implementation is the lack of robustness towards changes of the data layout inside classes and/or structs. If variables are added, removed or reordered inside a class the static memory is going to fail because it would read/write from wrong locations. While it’s possible to solve this issue by improving both memory management and the reloading code, possibly with some mild refactoring, i believe that a feature like this should be dictated by real necessities from the gameplay programming side. If changing data layout at runtime were effectively a time-saving feature for some specific type of debugging or tuning, then i would be interested in investing more time to improve the system. Otherwise, i would just consider this as premature optimization, and you already know what more experienced programmers than me have to say about that! 

 

Demo

In this example i launch the executable outside the debugger. I am interested in tuning the movement speed of an animation, and thanks to hot code reloading i can change the source code and immediately see the effect on my application:

 

 

That’s all for now, happy code reloading!