Welcome to “Handmade Hero Notes”, the book where we follow the footsteps of Handmade Hero in making the complete game from scratch, with no external libraries. If you'd like to follow along, preorder the game on handmadehero.org, and you will receive access to the GitHub repository, containing complete source code (tagged day-by-day) as well as a variety of other useful resources.
People often ask: are we going to use a scripting language for Handmade Hero? We definitely won't use an existing one, maybe will eventually develop one. But the correct question is: is it a great idea in first place?
(Top)
1 Plan For Today
1.1 About Scripting Languages
1.2 Hot Reloading
2 Change build.bat
2.1 Clean Up Build Variables
2.2 Request handmade.dll
Compilation
2.3 Export Functions
3 Separate Game from Platform
3.1 Move Shared Types
3.2 Update API
3.3 Inspect Linker Errors
3.4 Load Game Code
3.5 Services to the Game
3.6 Guard Against Name Mangling
4 Dynamic Reloading
4.1 Unload Game Code
4.2 Slow Down Reloading
4.3 Allow DLL Rewriting
5 Recap
6 Side Debugging
6.1 Fix Sound Issue
7 Navigation
To answer this question, we need to determine why we'd be using a scripting language and whether it covers the downsides. And the downsides, if you look closely, are many:
If you look at these, it's not really a matter of 'scripting languages are bad'. However, it takes a lot of time to get all those things right, so such use should just be evaluated on a case-by-case basis. For Handmade Hero, it's definitely a no-go.
But maybe we can get all the benefits without having to do all the hard work? When you think of a scripting language, these benefits come to your mind (compared to the programming language the engine is developed on):
This is a debatable point, really. Instead, it seems to be coming from lousy API practices in the programming languages rather than the scripting ones' strengths. Since we're going to really focus on writing a good API for our game, we should be fine in C.
Now, this is a great tool to have. When it comes to fine-tuning the game, having our program running while you modify its values can save a lot of time. Otherwise, you need to exit debugging, change the necessary values, recompile, restart the game, jump to whatever encounter you were tuning, realize the edit wasn't perfect, jump back... This indeed can become very old very quickly.
Of course, some debuggers, like Visual Studio, have “Edit and Continue” functionality, where you could set a breakpoint, edit a variable, and continue from where you left. Unfortunately, this functionality is usually quite limited and doesn't really work in more complex situations.
Maybe we could implement our own fool-proof version of “Edit and Continue”? Something that would allow us to replicate the same functionality of “Change the code live” → “Recompile” without breaking or exiting the game!
We're just out of a very complex sound debugging part, so it's about time for us to do something fun! Let's see if we can use Windows to help us do it.
If you review our game code structure so far, you'll see that it's already quite neatly separating Windows (platform-specific layer) from our core game functionality. So, to start implementing the live code editing, we first should ask ourselves: could we split those two things apart entirely? You'll have the platform code (containing WinMain and other platform-specific code) compiled separately from the game code.
And then, will it be possible to unload the game part without killing the executable?
The answer is “yes” to both of these questions. We already have the ability of loading libraries that we implemented for our XInput code when we were working on the controller input:
internal void
Win32LoadXInput()
{
HMODULE XInputLibrary = LoadLibraryA("Xinput1_4.dll");
if(!XInputLibrary)
{
XInputLibrary = LoadLibraryA("Xinput1_3.dll");
}
if(!XInputLibrary)
{
XInputLibrary = LoadLibraryA("Xinput9_1_0.dll");
}
if(XInputLibrary)
{
XInputGetState = (x_input_get_state *)GetProcAddress(XInputLibrary, "XInputGetState");
XInputSetState = (x_input_set_state *)GetProcAddress(XInputLibrary, "XInputSetState");
}
}
Here, we first call LoadLibraryA
to load a DLL. After we ensure that the operation succeeded we retrieve the necessary functions using GetProcAddress
and use them as usual.
If we want, we can just do it with our own code. Instead of compiling our program monolithically, we will build the program as two separate translation units. In simpler terms, we will run our compilation on two different code chunks separately: one for the platform and one for the game. The platform-independent code will be loaded as a .dll
to do whatever we need.
After that, it's simple. When we change our game code, we will simply unload, rebuild, and reload the DLL without killing the executable. It should just work!
What really enables us to do it this way is passing memory to the game in a single chunk. We said in the past that there're many good reasons for doing it, well, that's one of them. Our platform layer reserves the memory and passes the whole chunk to the game. When the game is done doing its work, the memory is preserved for the main loop's next iteration. This allows us to unload the game, swap the game code with the new version, and give it the memory the previous guy was using. And poof! The game will keep running as before.
Now, there're a couple of caveats where the magic won't work in all places. For instance, if the structures have changed (members were added or removed), this can potentially affect the layout of the memory using those structures, so the game wouldn't be able to read old memory anymore and require a restart. However, it's a simple way of envisioning how such a code would work, at least for the time being.
Another issue that one can think about are the compile times. Some projects are notoriously slow to build, but this comes from lousy programming practices rather than a codebase's complexity. If your code takes more than 10 seconds to compile, you're doing it wrong!
build.bat
The first step on our journey would be updating our build.bat
.
Last time, we might have gone a little bit overboard with the variables. Our cl
line currently looks like this:
cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %win32_link%
where:
%compiler%
contains various compiler flags.
%defines%
stands for the defines for -DHANDMADE_INTERNAL=1
and -DHANDMADE_SLOW=1
.
%debug%
adds a couple flags for debug mode (-FC
and -Z7
).
%code_path% is a shortcut to our code path, should it change.
*
%win32_libs% contains all the libraries we need to compile on Windows.
*
%win32_link%` contains a couple switches to pass to the linker.
Now, the last one can be reworked. We only have 1 Windows-specific flag, the subsystem, which we can pass directly instead of a variable, while the common linker flags can be grouped and expanded together. We want to add -incremental:no
flag to the linker flags, simply to make sure we do the complete recompile each time.
:: WIN32 PLATFORM LIBRARIES
set win32_libs= user32.lib
set win32_libs=%win32_libs% gdi32.lib
set win32_libs=%win32_libs% winmm.lib:: COMMON LINKER SWITCHES
set link= -opt:ref &:: Remove unused functions
set link=%link% -incremental:no &:: Perform full link each time:: WIN32 LINKER SWITCHES
set win32_link= -subsystem:windows,5.2 &:: subsystem, 5.1 for x86
set win32_link=%win32_link% -opt:ref &:: Remove unused functions
:: No optimizations (slow): -Od; all optimizations (fast): -O2
cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %link% -subsystem:windows,5.2
Remember that you still need to add /link
so that the compiler knows it needs to pass the flags that follow to the linker.
Feel free to expand or change the variables as you feel appropriate to your workflow.
handmade.dll
Compilation
Because we want to make two separate things, we want to call the compiler, cl
, twice. This will, in return, produce two files (aside from a bunch of debug files and build artifacts):
win32_handmade.exe
will contain the Windows platform code.
handmade.dll
will contain the cross-platform game code.In the new line, we can specify:
win32_handmade.cpp
.
handmade.cpp
'; the map file will also have a different name.
:: No optimizations (slow): -Od; all optimizations (fast): -O2
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link %link%cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %link% -subsystem:windows,5.2
We miss one last step here.
DLL is not much different from an EXE. It essentially contains executable code, like an .exe
. Contrary to the latter, though, a DLL has a specific table of functions. These functions can be called from outside the DLL by whoever asks for them. It's this table that a function GetProcAddress
goes to read in an attempt to find a specific location to call. This allows linking at runtime, dynamically (hence the name Dynamically Linked Library, or DLL for short).
To allow such functionality, we need to tell the linker which functions we're exporting. There're a couple of ways to do this:
__declspec(dllexport)
.
build.bat
.The first method seems quite simple, except it has several drawbacks. The attribute is not standardized, so different operating systems (and even different compilers!) have their own designations. Using a specific one inside the code file, we're essentially tying ourselves to the Microsoft's compiler, and that's not necessarily something that we'd love to do.
We don't have many functions to export. We only want to export GameUpdateAndRender
and GameGetSoundSamples
, so let's go with the second option.
The keyword we're after is?. Inside the build.bat
file, you can write directly on the compile line, or you can add another variable to use for simpler reading later on, as we did below.
:: COMMON LINKER SWITCHES
set link= -opt:ref &:: Remove unused functions
set link=%link% -incremental:no &:: Perform full link each time:: DLL LINKER SWITCHES
set dll_link= /EXPORT:GameUpdateAndRender
set dll_link=%dll_link% /EXPORT:GameGetSoundSamples
:: No optimizations (slow): -Od; all optimizations (fast): -O2
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link %link% %dll_link%cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %link% -subsystem:windows,5.2
Of course, if we try to build now, the compiler will complain loudly of all the things it doesn't like. Let's get to finishing separating the two layers.
Up until now, we were building the game as one big monolithic block. Our platform layer and the game were compiled as a single translation unit, even if we separated the two semantically. We have some shared items that we have between the two, but we also have some components that should never talk with each other.
Let's unify the shared and separate even further specific code.
The first step would be to move the type definitions and common includes away from win32_handmade.cpp
header. We also want to remove any reference from handmade.cpp
:
// TODO(casey): Implement sine ourselves
#include <math.h>
#include <stdint.h>
typedef int8_t s8;
typedef int16_t s16;
typedef int32_t s32;
typedef int64_t s64;
typedef s32 b32;
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef float f32;
typedef double f64;
#define internal static
#define local_persist static
#define global_variable static
#define Pi32 3.14159265359f
#include "handmade.cpp"#include "handmade.h"#include <windows.h>
#include <stdio.h>
#include <xinput.h>
#include <dsound.h>
#include "win32_handmade.h"
Our handmade.h
currently fulfills the role of a common interface quite nicely. Let's move all this cut code on top of the file (except handmade.cpp
include, we don't want circular includes!).
#if !defined(HANDMADE_H)
/*
NOTE(casey):
HANDMADE_INTERNAL:
0 - Build for public release
1 - Build for developer only
HANDMADE_SLOW:
0 - No slow code allowed!
1 - Slow code welcome.
*/
// TODO(casey): Implement sine ourselves
#include <math.h>
#include <stdint.h>
typedef int8_t s8;
typedef int16_t s16;
typedef int32_t s32;
typedef int64_t s64;
typedef s32 b32;
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef float f32;
typedef double f64;
#define internal static
#define local_persist static
#define global_variable static
#define Pi32 3.14159265359f
#if HANDMADE_SLOW
#define Assert(Expression) if (!(Expression)) { *(int *)0 = 0; }
#else
#define Assert(Expression)
#endif
Let's try to compile and let the compiler errors guide us. The first error that we get is actually a compiler warning! It's a good thing that we increased our warning level and asked the compiler to treat all warnings as errors:
handmade.h(178) : warning C4505: 'GameUpdateAndRender' : unreferenced local function has been removed
handmade.h(178) : warning C4505: 'GameGetSoundSamples' : unreferenced local function has been removed
That's a good warning to catch! The keyword we're looking at here is local
. We already asked build.bat
to export these functions as publicly available, but in our code we still mark them as internal
(or static
if we go back to internal
definition). Let's fix that! We need to update both handmade.h
and handmade.cpp
:
void GameUpdateAndRender(game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer);
// NOTE(casey): At the moment, this has to be a very fast function, it cannot be
// more than a millisecond or so.
// TODO(casey): Reduce the pressure on this function's performance by measuring it
// or asking about it, etc.void GameGetSoundSamples(game_memory *Memory, game_sound_output_buffer *SoundBuffer);
voidGameUpdateAndRender(game_memory* Memory, game_input *Input, game_offscreen_buffer* Buffer)
{
//...
}
voidGameGetSoundSamples(game_memory* Memory, game_sound_output_buffer *SoundBuffer)
{
// ...
}
If we look at the compiler errors further down, you'll see that we have similar warnings for the debug services we provide to the game: for now, it's only file I/O (reading/writing files), but this might change in the future. Let's update make them non-static, as well.
debug_read_file_result DEBUGPlatformReadEntireFile(char *Filename);
void DEBUGPlatformFreeFileMemory(void *Memory);
b32 DEBUGPlatformWriteEntireFile(char *Filename, u32 MemorySize, void *Memory);
voidDEBUGPlatformFreeFileMemory(void *Memory)
{
// ...
}
debug_read_file_resultDEBUGPlatformReadEntireFile(char *Filename)
{
//...
}
b32DEBUGPlatformWriteEntireFile(char *Filename, u32 MemorySize, void *Memory)
{
// ...
}
We don't need to export these functions through build.bat
. We'll do something else to pass them to the game further down the line.
If you've done everything correctly, the compiler will spit out something scary-looking like this:
handmade.cpp
handmade.obj : error LNK2019: unresolved external symbol "struct debug_read_file_result __cdecl DEBUGPlatformReadEntireFile(char *)" (?DEBUGPlatformReadEntireFile@@YA?AUdebug_read_file_result@@PEAD@Z) referenced in function "void __cdecl GameUpdateAndRender(struct game_memory *,struct game_input *,struct game_offscreen_buffer *)" (?GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z)
handmade.obj : error LNK2019: unresolved external symbol "void __cdecl DEBUGPlatformFreeFileMemory(void *)" (?DEBUGPlatformFreeFileMemory@@YAXPEAX@Z) referenced in function "void __cdecl GameUpdateAndRender(struct game_memory *,struct game_input *,struct game_offscreen_buffer *)" (?GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z)
handmade.obj : error LNK2019: unresolved external symbol "int __cdecl DEBUGPlatformWriteEntireFile(char *,unsigned int,void *)" (?DEBUGPlatformWriteEntireFile@@YAHPEADIPEAX@Z) referenced in function "void __cdecl GameUpdateAndRender(struct game_memory *,struct game_input *,struct game_offscreen_buffer *)" (?GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z)
handmade.dll : fatal error LNK1120: 3 unresolved externals
Don't worry, these are simply linker errors, and it's good news for us! First, it means that compiler doesn't have anything to complain about, and it passed the work to the linker. Linker, on the other hand, has its own set of errors marked by LNK[error number]
.
In this case, each error says:
Hey, you tried to call this function “bla bla bla” (symbol name %@scary#looking#something!%!), you even declared it here: “bla bla bla” (more scary symbols), but I didn't find it anywhere. Where is it?These are the places where the two halves of our code call each other, and they cannot find the respective code to execute. We have to find a way to resolve these.
On win32_handmade.cpp
side, we pretty much have the infrastructure ready to load the game code: we'll simply replicate the behavior of Win32LoadXInput
function and repeat the magic of the stub functions.
Let's start with the stub functions first. If you remember, this is what we needed to do:
#define
a macro creating functions of a specific signature (“if I say in code FUNCTION(FunctionName)
I really mean void FunctionName(int something)
").
typedef
a new type of this exact signature.
We can do it for both GameUpdateAndRender
and GameGetSoundSamples
. Also, we can remove the function declarations we had earlier because they are no longer necessary.
#define GAME_UPDATE_AND_RENDER(name) void name(game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer)
typedef GAME_UPDATE_AND_RENDER(game_update_and_render);
GAME_UPDATE_AND_RENDER(GameUpdateAndRenderStub) { }void GameUpdateAndRender(game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer);// NOTE(casey): At the moment, this has to be a very fast function, it cannot be
// more than a millisecond or so.
// TODO(casey): Reduce the pressure on this function's performance by measuring it
// or asking about it, etc.#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);
GAME_GET_SOUND_SAMPLES(GameGetSoundSamplesStub) { }void GameGetSoundSamples(game_memory *Memory, game_sound_output_buffer *SoundBuffer);
We now also have an added benefit in that, should our function signature ever change, we only need to modify it in one place (instead of editing both the .h
file and the .cpp
file.). Let's edit our GameUpdateAndRender
and GameGetSoundSamples
function declarations one last time to enable this:
GAME_UPDATE_AND_RENDER(GameUpdateAndRender){
// ...
}
GAME_GET_SOUND_SAMPLES(GameGetSoundSamples){
// ...
}
Hopefully, you can see what's going on here. Instead of writing void GameUpdateAndRender(...)
we say GAME_UPDATE_AND_RENDER(GameUpdateAndRender)
and define the actual function signature elsewhere.
Now that we have the necessary types and stubs, we can easily use them in our platform code. Let's write a function to load the game code.
internal void
Win32LoadGameCode()
{
HMODULE GameCodeDLL = LoadLibraryA("handmade.dll");
if(GameCodeDLL)
{
GameUpdateAndRender = (game_update_and_render *)GetProcAddress(GameCodeDLL, "GameUpdateAndRender");
GameGetSoundSamples = (game_get_sound_samples *)GetProcAddress(GameCodeDLL, "GameGetSoundSamples");
}
}
internal void
Win32LoadXInput()
For XInput, we used global variables to store the function calls. Let's try something different this time. Instead of using globals, we can think of a win_game_code
structure that initialized and returned directly from this function. Furthermore, this allows us to store the DLL module handle and a failsafe to ensure our code is valid. Let's add all this to win32_handmade.h
:
struct win32_debug_time_marker
{
// ...
};
struct win32_game_code
{
HMODULE GameCodeDLL;
game_update_and_render *UpdateAndRender;
game_get_sound_samples *GetSoundSamples;
b32 IsValid;
};
We can now use this new structure in Win32LoadGameCode
: we will return it as the result of our function, and we'll also make sure that this result is valid.
The validity will be determined by whether or not we successfully load the functions. If GetProcAddress
returns 0 at least for one of the exports, the whole win32_game_code
will be flagged as invalid.
internal win32_game_codeWin32LoadGameCode()
{ win32_game_code Result = {}; Result.GameCodeDLL = LoadLibraryA("handmade.dll");
if(Result.GameCodeDLL) { Result.UpdateAndRender =
(game_update_and_render *)GetProcAddress(Result.GameCodeDLL, "GameUpdateAndRender");
Result.GetSoundSamples =
(game_get_sound_samples *)GetProcAddress(Result.GameCodeDLL, "GameGetSoundSamples");
Result.IsValid = Result.GetSoundSamples && Result.UpdateAndRender; }
if (!Result.IsValid)
{
Result.UpdateAndRender = GameUpdateAndRenderStub;
Result.GetSoundSamples = GameGetSoundSamplesStub;
}
return(Result);}
Let's load the game code before going into GlobalRunning
. Later we might need to move it since we'll be swapping it on the fly, but for the time being, it should suffice. We'll then replace the previous calls to the game code we had to use our Game
.
While we're at it, let's remove our debug code for determining sound update frequency.
#if 0
// NOTE(casey): This tests the PlayCursor/WriteCursor update frequency
// On the Handmade Hero machine, it was 480 samples.
while (GlobalRunning)
{
DWORD PlayCursor;
DWORD WriteCursor;
GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor);
char TextBuffer[256];
_snprintf_s(TextBuffer, sizeof(TextBuffer),
"PC:%u WC:%u\n", PlayCursor, WriteCursor);
OutputDebugStringA(TextBuffer);
}
#endifwin32_game_code Game = Win32LoadGameCode();
u64 LastCycleCount = __rdtsc();
while (GlobalRunning)
{
// ...
game_offscreen_buffer Buffer = {};
Buffer.Memory = GlobalBackbuffer.Memory;
Buffer.Width = GlobalBackbuffer.Width;
Buffer.Height = GlobalBackbuffer.Height;
Buffer.Pitch = GlobalBackbuffer.Pitch; Game.UpdateAndRender(&GameMemory, NewInput, &Buffer); // ...
game_sound_output_buffer SoundBuffer = {};
SoundBuffer.SamplesPerSecond = SoundOutput.SamplesPerSecond;
SoundBuffer.SampleCount = BytesToWrite / SoundOutput.BytesPerSample;
SoundBuffer.Samples = Samples;
Game.GetSoundSamples(&GameMemory, &SoundBuffer);
// ...
}
Now our Win32 platform layer should compile correctly. When compiling you should see the output like this:
handmade.cpp
Creating library handmade.lib and object handmade.exp
handmade.obj : error LNK2019: [linker error description]
handmade.obj : error LNK2019: [linker error description]
handmade.obj : error LNK2019: [linker error description]
handmade.dll : fatal error LNK1120: 3 unresolved externals
win32_handmade.cpp
This means that we couldn't compile handmade.dll
because there were 3 linker errors. On the other hand, win32_handmade.cpp
compiled without any issues.
Win32_handmade.exe
will be loading game code if the latter is available. But, if you run it, the program should execute its main loop anyway. It display only the debug sound lines because these are all calculated on the platform side. Of course, all the DLL-side functionality will be missing: you'll also a distinct lack of the gradient or sound, and the lines will eventually overlap because we don't clear the screen to black at each frame.
We have a significant roadblock to overcome for what might concern the game code: how would we load platform-specific services (like reading and writing files)?
We could load the executable in the DLL, load a handle for it and then reverse call the functions. But is it indispensable?
The thing is, when we call GameUpdateAndRender
or GameGetSoundSamples
, we pass a bunch of different services: things like memory or the buffers to fill. We could simply pass the pointers to whichever platform function the game might want to call. We've seen this already: if you think about the DirectSound buffers, they also come with the respective function pointers to call (things like the famous SecondaryBuffer->GetCurrentPosition(...)
).
In practice, this will result in the exact same syntax we have for things like XInput or GameUpdateAndRender. Except for this time, instead of calling Windows' function GetProcAddress
, we'll straight up pass the function pointers to the game.
As an added benefit, we won't even need the stub functions.
#if HANDMADE_INTERNAL
struct debug_read_file_result
{
u32 ContentsSize;
void *Contents;
};
#define DEBUG_PLATFORM_FREE_FILE_MEMORY(name) void name (void *Memory)
typedef DEBUG_PLATFORM_FREE_FILE_MEMORY(debug_platform_free_file_memory);
#define DEBUG_PLATFORM_READ_ENTIRE_FILE(name) debug_read_file_result name (char *Filename)
typedef DEBUG_PLATFORM_READ_ENTIRE_FILE(debug_platform_read_entire_file);
#define DEBUG_PLATFORM_WRITE_ENTIRE_FILE(name) b32 name (char *Filename, u32 MemorySize, void *Memory)
typedef DEBUG_PLATFORM_WRITE_ENTIRE_FILE(debug_platform_write_entire_file);
debug_read_file_result DEBUGPlatformReadEntireFile(char *Filename);
void DEBUGPlatformFreeFileMemory(void *Memory);
b32 DEBUGPlatformWriteEntireFile(char *Filename, u32 MemorySize, void *Memory);#endif
These somewhat verbose names should, however, be quite descriptive in what they do. They are Debug, so they shouldn't be used in user-facing code, They're platform-independent, and they do a specific operation, like reading a file, writing to a file, or freeing its memory.
This allows passing the function pointers as if they were other types: int
, DWORD
, u32
... debug_platform_write_entire_file
has now become just another type.
Again, as an optional step to potentially prevent headaches in the future, let's replace the signature of the function definitions in win32_handmade.cpp
with our macros:
DEBUG_PLATFORM_FREE_FILE_MEMORY(DEBUGPlatformFreeFileMemory){
// ...
}
DEBUG_PLATFORM_READ_ENTIRE_FILE(DEBUGPlatformReadEntireFile){
// ...
}
DEBUG_PLATFORM_WRITE_ENTIRE_FILE(DEBUGPlatformWriteEntireFile){
// ...
}
Now, how do we pass these to the game? Well, we are passing the memory by packaging it in game_memory
structure. We can expand it to include the pointers to these functions as follows:
struct game_memory
{
u64 PermanentStorageSize;
void *PermanentStorage;
u64 TransientStorageSize;
void *TransientStorage;
b32 IsInitialized;
debug_platform_free_file_memory *DEBUGPlatformFreeFileMemory;
debug_platform_read_entire_file *DEBUGPlatformReadEntireFile;
debug_platform_write_entire_file *DEBUGPlatformWriteEntireFile;};
Of course, we should remember to actually pass the pointers so that we don't crash and burn painfully (Attempting to call a function located at sector 0 is a good way to crash, that's how we do our Assert
right now).
game_memory GameMemory = {};
GameMemory.PermanentStorageSize = Megabytes(64);
GameMemory.TransientStorageSize = Gigabytes(1);GameMemory.DEBUGPlatformFreeFileMemory = DEBUGPlatformFreeFileMemory;
GameMemory.DEBUGPlatformReadEntireFile = DEBUGPlatformReadEntireFile;
GameMemory.DEBUGPlatformWriteEntireFile = DEBUGPlatformWriteEntireFile;u64 TotalStorageSize = (GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize);
GameMemory.PermanentStorage = VirtualAlloc(BaseAddress, (size_t)TotalStorageSize,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
GameMemory.TransientStorage = ((u8 *)GameMemory.PermanentStorage +
GameMemory.PermanentStorageSize);
Finally, we need to actually use these functions in the game. We aren't doing anything useful with them right now, we simply have this block for testing:
debug_read_file_result FileData = DEBUGPlatformReadEntireFile(__FILE__);
if (FileData.Contents)
{
DEBUGPlatformWriteEntireFile("test.out", FileData.ContentsSize, FileData.Contents);
DEBUGPlatformFreeFileMemory(FileData.Contents);
}
We pass game_memory
as a pointer named Memory
. So, in order to use the functions we will simply dereference them from the pointer (with the ->
operator).
debug_read_file_result FileData = Memory->DEBUGPlatformReadEntireFile(__FILE__);if (FileData.Contents)
{ Memory->DEBUGPlatformWriteEntireFile("test.out", FileData.ContentsSize, FileData.Contents);
Memory->DEBUGPlatformFreeFileMemory(FileData.Contents);}
And... that's it.
What we did here is a much simpler version of something called a VTable in C++, almost a Memory Dispatch of sorts. The major difference is that, in C++, all of this happens under the hood. Here, we remove this functionality from its proverbial black box so that we can see what it does and do whatever we want with it.
But there's still one caveat we haven't discussed yet: name mangling.
If you remember, we recently encountered the absolutely out-of-the-world names in our linker errors: something like GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z
. As we discussed on day 16, this is called name mangling, and it's a compiler technique to distinguish functions with the same name but different signatures. It becomes even more necessary in a universe where you have classes and namespaces; each may have multiple instances of the same function name... Something that you see a lot in C++.
We don't need any of this, even less so in our exported functions. Luckily, there's a way to disable name mangling for specific functions (or even bigger code blocks). To do that, you need to use a very unique function prefix extern "C"
. This effectively regresses whatever code block you're using from C++ to C, and the latter doesn't support function overloading nor allows name mangling. Problem solved, right?
Well, yes and no. You see, now this code cannot be compiled in C anymore. So what you need to do is verify first if the program is being compiled in C++ mode! The final code becomes like this:
#if defined __cplusplus
extern "C"
#endifGAME_UPDATE_AND_RENDER(GameUpdateAndRender)
{
// ...
}
#if defined __cplusplus
extern "C"
#endifGAME_GET_SOUND_SAMPLES(GameGetSoundSamples)
{
// ...
}
Finally, we can compile everything without errors. If you want to verify that the necessary functions are exported correctly, Visual Studio packages the utility dumpbin
that you can call for this exact purpose. Simply type `dumpbin /exports [path-to-dll]/handmade.dll in your console to hopefully see the following:
W:\handmade>dumpbin /exports build\handmade.dllMicrosoft (R) COFF/PE Dumper Version XX.XX.XXXXX.X
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file build\handmade.dll
File Type: DLL
Section contains the following exports for handmade.dll
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
2 number of functions
2 number of names
ordinal hint RVA name
1 0 000014D0 GameGetSoundSamples = GameGetSoundSamples
2 1 00001270 GameUpdateAndRender = GameUpdateAndRender
Summary
2000 .data
1000 .pdata
A000 .rdata
1000 .reloc
C000 .text
1000 _RDATA
If you see the exported functions GameUpdateAndRender
and GameGetSoundSamples
, great! If not, the game will not see them and GetProcAddress
will fail. In such case verify if, for example, you included %dll_link%
in your build.bat
file.
If everything's in order, run the game. Success!
So far, we didn't do a whole lot. We basically replicated the functionality that we already had of the game functions loading with the rest of the game. Now we can add the thing we set off doing in the first place: dynamic code reloading!
Today we won't make the entire thing. Some parts of it will need to be fleshed out next time, we'll just make the quick and dirty change.
To load a new version of the DLL (and even try to recompile it), we need to unload the old version first. You can imagine it by simply calling a function like Win32UnloadGameCode
.
internal win32_game_code
Win32LoadGameCode()
{
// ...
}
internal void
Win32UnloadGameCode(win32_game_code *GameCode)
{
if (GameCode->GameCodeDLL)
{
FreeLibrary(GameCode->GameCodeDLL);
GameCode->GameCodeDLL = 0;
}
GameCode->IsValid = false;
GameCode->UpdateAndRender = GameUpdateAndRenderStub;
GameCode->GetSoundSamples = GameGetSoundSamplesStub;
}
Let's do something ridiculous and do the game loading and unloading at each frame.
win32_game_code Game = Win32LoadGameCode();
u64 LastCycleCount = __rdtsc();
while (GlobalRunning)
{ Win32UnloadGameCode(&Game);
Game = Win32LoadGameCode();
// ...
}
It works! Somewhat. We still cannot reap the benefits since the change happens so quickly that we don't have time to recompile the code. Also, you might uncover a sound bug. Let's investigate it.
Feel free to find the bug yourself and then check the solution in subsection 6.1.
Right now we are reloading our DLL 30 times per second. That's a lot of unnecessary I/O. In the future, we'll try to find a way to detect changes in the DLL, but for now, let's simply add a timer, so that the DLL reload attempt happens only every few seconds:
win32_game_code Game = Win32LoadGameCode();u32 LoadCounter = 0;
u64 LastCycleCount = __rdtsc();
while (GlobalRunning)
{ if (LoadCounter++ > 120)
{ Win32UnloadGameCode(&Game);
Game = Win32LoadGameCode(); LoadCounter = 0;
} // ...
}
Even if the reloading is happening every few seconds, we still can't recompile the DLL because unloading and loading occur back to back.
We can add a quick and dirty solution now and flesh it out next time. This solution won't work inside the debugger for several reasons, but it will work if you simply run win32_handmade.exe
directly from Windows.
The solution is simply to copy our DLL to another location and load it from there. To do this, we'll use the aptly named CopyFile
function (MSDN reference) inside Win32LoadGameCode
:
win32_game_code Result = {};
CopyFile("handmade.dll", "handmade_temp.dll", FALSE);Result.GameCodeDLL = LoadLibraryA("handmade_temp.dll");
If you've done everything correctly, you can now do live code editing as on the video below!
We can now edit our game on the fly, and the state is preserved! We don't need to do anything, almost as good as having a scripting language.
The system is not perfect yet, though. For the time being, we can't use our debugger; besides, there're other things to potentially tune up (like waiting 4 seconds before reloading).
We'll do it all next time.
Let's inspect our sound code inside handmade.cpp
:
internal void
GameOutputSound(game_sound_output_buffer *SoundBuffer, int ToneHz)
{
local_persist f32 tSine;
s16 ToneVolume = 3000;
int WavePeriod = SoundBuffer->SamplesPerSecond / ToneHz;
s16 *SampleOut = SoundBuffer->Samples;
for (int SampleIndex = 0;
SampleIndex < SoundBuffer->SampleCount;
++SampleIndex)
{
f32 SineValue = sinf(tSine);
s16 SampleValue = (s16)(SineValue * ToneVolume);
*SampleOut++ = SampleValue;
*SampleOut++ = SampleValue;
tSine += 2.0f * Pi32 * 1.0f / (f32)WavePeriod;
if (tSine > 2.0f * Pi32)
{
tSine -= 2.0f * Pi32;
}
}
}
After a short inspection, it's clear to us that the culprit is tSine
, a variable we use to calculate the sound oscillation in our debug code. It's a static variable (local_persist
), and, as such, it has its own block of memory allocated on the stack. When the DLL is reloaded, all of its static data is reset, tSine
resets to zero, thus producing sound bug.
It's time for our tSine
to move to GameState
, the dedicated block of memory that we take from the persistent memory and use for our game code.
struct game_state
{
int ToneHz; f32 tSine;
int XOffset;
int YOffset;
};
Now we can change our GameOutputSound
code to actually use the correct tSine
. We also need to pass the pointer to the game state, which you'll grow to do by default as this course progresses.
internal voidGameOutputSound(game_state *GameState, game_sound_output_buffer *SoundBuffer, int ToneHz){ local_persist f32 tSine; s16 ToneVolume = 3000;
int WavePeriod = SoundBuffer->SamplesPerSecond / ToneHz;
s16 *SampleOut = SoundBuffer->Samples;
for (int SampleIndex = 0;
SampleIndex < SoundBuffer->SampleCount;
++SampleIndex)
{ f32 SineValue = sinf(GameState->tSine); s16 SampleValue = (s16)(SineValue * ToneVolume);
*SampleOut++ = SampleValue;
*SampleOut++ = SampleValue; GameState->tSine += 2.0f * Pi32 * 1.0f / (f32)WavePeriod;
if (GameState->tSine > 2.0f * Pi32)
{
GameState->tSine -= 2.0f * Pi32;
} }
}
// ...
GAME_GET_SOUND_SAMPLES(GameGetSoundSamples)
{
game_state *GameState = (game_state*)Memory->PermanentStorage GameOutputSound(GameState, SoundBuffer, GameState->ToneHz);}
Once the code is recompiled, you'll notice that the sound bug has gone away. Modern computers are fast (and our code is small)!
(Continue to Subsection 4.2)
Previous: Day 20. Debugging the Audio Sync
Up Next: Day 21. Instantaneous Live Code Editing