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.
Today is the cleanup day. We went in fast and furious, typing our way through the code, and we have left behind some bugs and inconsistencies. We'll dedicate the next two days revisiting and, hopefully, optimizing them out.
(Top)
1 Small Fixes
1.1 Set Controller Type
1.2 Set Fixed Window Output Size
1.3 Improve build.bat
1.4 Get rid of the Topmost Layered Window
1.5 Get the File Edit Date Without Opening It
1.6 Remove Stub Functions for Platform Calls
2 Improve Recording Code
2.1 Remove References to MAX_PATH
2.2 Store Executable Filename
2.3 Build a Custom EXE Path Filename
2.4 Reorganize win32_handmade.cpp
2.5 Update Recording and Playback Functions
3 Recap
4 Navigation
The first thing on our list is to clean up the state of the controller. Currently, the controller is set to IsAnalog
only if it received input from D-Pad or the stick:
if ((NewController->StickAverageX != 0.0f) ||
(NewController->StickAverageY != 0.0f))
{
NewController->IsAnalog = true;
}
if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP)
{
NewController->StickAverageY = 1.0f;
NewController->IsAnalog = false;
}
if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN)
{
NewController->StickAverageY = -1.0f;
NewController->IsAnalog = false;
}
if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT)
{
NewController->StickAverageX = -1.0f;
NewController->IsAnalog = false;
}
if (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT)
{
NewController->StickAverageX = 1.0f;
NewController->IsAnalog = false;
}
At least, that's the assumption we make. However, this assumption is incorrect. See, we swap controllers back and forth at each frame:
game_input *Temp = NewInput;
NewInput = OldInput;
OldInput = Temp;
So if we set IsAnalog on the previous frame and then don't get any input on the next one, this information will not be carried on.
This is not a serious issue just yet, but you never know in the future. So let's simply pass over this information from the OldController
:
XINPUT_STATE ControllerState;
if (XInputGetState(ControllerIndex, &ControllerState) == ERROR_SUCCESS)
{
NewController->IsConnected = true; NewController->IsAnalog = OldController->IsAnalog;
// NOTE(casey): This controller is plugged in
// TODO(casey): See if ControllerState.dwPacketNumber increments too rapidly
XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;
// ...
}
Compile and make sure there're no errors. You can even step through the code and verify that the IsAnalog
state is preserved.
The next thing we want to tackle is graphics output to the window. You might have noticed that some lines that we draw are thicker than the other:
Image stretching is a more complex topic than it might seem. Pixel blending comes into play, calculations must be done on the sub-pixel level, and so on. We aren't ready to tackle all this just yet, so let's simply make the following hack: We will render pixels exactly how they appear in our buffer, without any stretching.
In practice, this means that, instead of passing WindowWidth
and WindowHeight
to the StretchDIBits
function, we will provide the buffer's width and height. This will result in the output not matching our window size, but that's fine for our purposes for now.
internal void
Win32DisplayBufferInWindow(...)
{ // NOTE(casey): For prototyping purposes, we're going to always blit
// 1-to-1 pixels to make sure we don't introduce artifacts with
// stretching while we are learning to code the renderer! StretchDIBits(DeviceContext, 0, 0, Buffer->Width, Buffer->Height, 0, 0, Buffer->Width, Buffer->Height,
Buffer->Memory,
&Buffer->Info,
DIB_RGB_COLORS, SRCCOPY);
}
build.bat
We have one little thing to fix in build.bat
. Right now, we're statically linking with the optimized version of the C Runtime Library. We're still not ready to ship our code to the final users, and we can definitely benefit from a slower, debug version of the library. This can be achieved by simply swapping the -MT
flag with -MTd
flag:
set compiler=%compiler% -Oi &:: Use assembly intrinsics where possibleset compiler=%compiler% -MTd &:: Include CRT library in the executable (static linking of the debug version)set compiler=%compiler% -Gm- &:: Disable minimal rebuild
If you compile and run, you'll notice that not much has changed. However, this change might be handy in the future as we dive deeper into the code.
Last time, we played around with the idea of having our window always on top and becoming semi-transparent when bringing it out off-focus. Unfortunately, this results in the window being continually in the way, as mouse clicks still end up inside the game.
Let's get rid of this implementation for now. We can re-enable it when we want.
HWND Window = CreateWindowEx(0, // WS_EX_TOPMOST | WS_EX_LAYERED, WindowClass.lpszClassName, "Handmade Hero",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
0, 0, Instance, 0);
case WM_CLOSE:
{
GlobalRunning = false;
} break;
case WM_ACTIVATEAPP:
{#if 0 if (WParam == TRUE)
{
SetLayeredWindowAttributes(Window, RGB(0, 0, 0), 255, LWA_ALPHA);
}
else
{
SetLayeredWindowAttributes(Window, RGB(0, 0, 0), 64, LWA_ALPHA);
}#endif} break;
case WM_DESTROY:
{
GlobalRunning = false;
} break;
If you compile and run now, you'll notice that the layering functionality is gone.
Another thing that's a concern is how we get the last write time of our handmade.dll
file. This is how we implemented Win32GetLastWriteTime
function:
inline FILETIME
Win32GetLastWriteTime(char *Filename)
{
FILETIME LastWriteTime = {};
WIN32_FIND_DATA FindData;
HANDLE FindHandle = FindFirstFileA(Filename, &FindData);
if (FindHandle != INVALID_HANDLE_VALUE)
{
LastWriteTime = FindData.ftLastWriteTime;
FindClose(FindHandle);
}
return (LastWriteTime);
}
We currently have to open the file (get the FindHandle) to check its last write time. That's not the safest thing to do, as it potentially creates conflicts with whoever tries to access the file at the same time. Additionally, we must never forget to close the handle, potentially another point of failure.
The best thing to do here is to check the last write time without opening the file handle. Luckily, such a solution exists in Windows; it can be achieved by calling the GetFileAttributesEx function.
GetFileAttributesEx
takes the following parameters:
GetFileExInfoStandard
. You shouldn't pass anything else here.
WIN32_FILE_ATTRIBUTE_DATA
structure that, if the function succeeds, will contain the information we're after.
In practice this results in the following rewriting of Win32GetLastWriteTime
.
FILETIME LastWriteTime = {};WIN32_FIND_DATA FindData;
HANDLE FindHandle = FindFirstFileA(Filename, &FindData);
if (FindHandle != INVALID_HANDLE_VALUE)
{
LastWriteTime = FindData.ftLastWriteTime;
FindClose(FindHandle);
}WIN32_FILE_ATTRIBUTE_DATA Data;
if(GetFileAttributesExA(Filename, GetFileExInfoStandard, &Data))
{
LastWriteTime = Data.ftLastWriteTime;
}return (LastWriteTime);
A small thing that we can also do to prevent compilation conflicts is to remove the stub functions in handmade.h
. We currently have only two, so getting rid of those will be simple:
#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) { }
// ...
#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) { }
This, however, means that if the load file failed, we must pass 0 instead:
internal win32_game_code
Win32LoadGameCode(char *SourceDLLName, char *TempDLLName)
{
win32_game_code Result = {};
// ...
if (!Result.IsValid)
{ Result.UpdateAndRender = 0;
Result.GetSoundSamples = 0; }
return(Result);
}
internal void
Win32UnloadGameCode(win32_game_code *GameCode)
{
if (GameCode->GameCodeDLL)
{
FreeLibrary(GameCode->GameCodeDLL);
GameCode->GameCodeDLL = 0;
}
GameCode->IsValid = false; GameCode->UpdateAndRender = 0;
GameCode->GetSoundSamples = 0;}
This, in turn, means that inside WinMain, where we call these functions, we must gate ourselves from reaching broken pointers and only call the procedures when they're not 0.
if (Game.UpdateAndRender)
{ Game.UpdateAndRender(&GameMemory, NewInput, &Buffer);}
// ...
if (Game.GetSoundSamples)
{ Game.GetSoundSamples(&GameMemory, &SoundBuffer);}
Finally, we also can add a small comment inside the win32_game_code
structure so that we don't forget that we need to check for this in the future.
struct win32_game_code
{
HMODULE GameCodeDLL;
FILETIME DLLLastWriteTime;
// IMPORTANT(casey): Either of the callbacks can be 0!
// You must check before calling. game_update_and_render *UpdateAndRender;
game_get_sound_samples *GetSoundSamples;
b32 IsValid;
};
We now come to the most significant change for today. Our recording code saves its data inside the data folder, which we don't necessarily want. Ideally, we'd have the debug recordings being kept inside the build folder so that, if necessary, we can simply delete the folder and recreate it without any data loss.
Let's quickly review our code for writing dlls:
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[MAX_PATH];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename) - 1, SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
char TempGameCodeDLLFilename[] = "handmade_temp.dll";
char TempGameCodeDLLFullPath[MAX_PATH];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(TempGameCodeDLLFilename) - 1, TempGameCodeDLLFilename,
sizeof(TempGameCodeDLLFullPath), TempGameCodeDLLFullPath);
We already refactored this code in the past once, by adding possibility of checking the executable path and rebuilding the dll paths accordingly.
Now, we want to pull this code out even further to have an agnostic function that we can recycle.
MAX_PATH
First, we want to expand our win32_state
to keep track of things like the EXE path and OnePastLastSlash
pointer. This way, you don't need to pass them around alone.
struct win32_state
{
u64 TotalSize;
void *GameMemoryBlock;
HANDLE RecordingHandle;
int InputRecordingIndex;
HANDLE PlaybackHandle;
int InputPlayingIndex;
char EXEFilename[MAX_PATH];
char *OnePastLastEXEFilenameSlash;};
That said, we already mentioned in the past that using MAX_PATH is a bad idea. MAX_PATH is an old convention often ignored by modern API (which allows having a much longer path). This puts us at risk where the executable would be located deep in someone's file system, disallowing us from functioning correctly.
Let's use an intermediary #define
so that we can eventually revisit and replace it.
#define WIN32_STATE_FILENAME_COUNT MAX_PATHstruct win32_state
{
u64 TotalSize;
void *GameMemoryBlock;
HANDLE RecordingHandle;
int InputRecordingIndex;
HANDLE PlaybackHandle;
int InputPlayingIndex;
char EXEFilename[WIN32_STATE_FILENAME_COUNT]; char *OnePastLastEXEFilenameSlash;
};
Now we can safely remove any instance of MAX_PATH
we have used in the past.
// NOTE(casey): Never use MAX_PATH in code that is user-facing, because it
// can be dangerous and lead to bad results.char EXEFilename[WIN32_STATE_FILENAME_COUNT];
DWORD SizeOfFilename = GetModuleFileNameA(0, EXEFilename, sizeof(EXEFilename));
char *OnePastLastSlash = EXEFilename;
for (char *Scan = EXEFilename;
*Scan;
++Scan)
{
if (*Scan == '\\')
{
OnePastLastSlash = Scan + 1;
}
}
char SourceGameCodeDLLFilename[] = "handmade.dll";char SourceGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename) - 1, SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
char TempGameCodeDLLFilename[] = "handmade_temp.dll";char TempGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(TempGameCodeDLLFilename) - 1, TempGameCodeDLLFilename,
sizeof(TempGameCodeDLLFullPath), TempGameCodeDLLFullPath);
Let's quickly extract the code storing the exe
filename.
internal void
Win32GetEXEFilename(win32_state *State)
{
char EXEFilename[WIN32_STATE_FILENAME_COUNT];
DWORD SizeOfFilename = GetModuleFileNameA(0, EXEFilename, sizeof(EXEFilename));
char *OnePastLastSlash = EXEFilename;
for (char *Scan = EXEFilename;
*Scan;
++Scan)
{
if (*Scan == '\\')
{
OnePastLastSlash = Scan + 1;
}
}
}
int CALLBACK
WinMain(...)
{ Win32GetEXEFilename(Win32State); char EXEFilename[WIN32_STATE_FILENAME_COUNT];
DWORD SizeOfFilename = GetModuleFileNameA(0, EXEFilename, sizeof(EXEFilename));
char *OnePastLastSlash = EXEFilename;
for (char *Scan = EXEFilename;
*Scan;
++Scan)
{
if (*Scan == '\\')
{
OnePastLastSlash = Scan + 1;
}
} // ...
}
We'll need to access Win32State
, so we'll move its initialization further up. Actually, this will be the first thing we do when we enter WinMain
:
win32_state Win32State = {};Win32GetEXEFilename(Win32State);
// ...
if (Window)
{ win32_state Win32State = {};
Win32State.InputRecordingIndex = 0;
Win32State.InputPlayingIndex = 0; // ...
}
Ok, back to the code that we extracted. We need to leverage the State
that we're passing in to store EXEFilename
and the place for others to write their filenames.
char EXEFilename[WIN32_STATE_FILENAME_COUNT];DWORD SizeOfFilename = GetModuleFileNameA(0, State->EXEFilename, sizeof(State->EXEFilename));
State->OnePastLastEXEFilenameSlash = State->EXEFilename;
for (char *Scan = State->EXEFilename; *Scan;
++Scan)
{
if (*Scan == '\\')
{ State->OnePastLastEXEFilenameSlash = Scan + 1; }
}
Now we can extract some more code. We have the following code that can be recycled:
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename) - 1, SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
char TempGameCodeDLLFilename[] = "handmade_temp.dll";
char TempGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(TempGameCodeDLLFilename) - 1, TempGameCodeDLLFilename,
sizeof(TempGameCodeDLLFullPath), TempGameCodeDLLFullPath);
In fact, if you look closely, this is the same code repeating twice! Let's extract the repeating code and replace these blocks with the calls to it:
internal void
Win32BuildEXEPathFilename(win32_state *State, char *Filename,
int DestCount, char *Dest)
{
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename) - 1, SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
}
int CALLBACK
WinMain(...)
{
win32_state Win32State = {};
Win32GetEXEFilename(Win32State);
char SourceGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
Win32BuildEXEPathFilename(&Win32State, "handmade.dll",
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
char TempGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
Win32BuildEXEPathFilename(&Win32State, "handmade_temp.dll",
sizeof(TempGameCodeDLLFullPath), TempGameCodeDLLFullPath);
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename) - 1, SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
char TempGameCodeDLLFilename[] = "handmade_temp.dll";
char TempGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(TempGameCodeDLLFilename) - 1, TempGameCodeDLLFilename,
sizeof(TempGameCodeDLLFullPath), TempGameCodeDLLFullPath);
LARGE_INTEGER PerfCountFrequencyResult;
// ...
}
Let's start cleaning up the function. First, we don't need the filename and the full path container, so we can simply get rid of these:
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[WIN32_STATE_FILENAME_COUNT];
Our CatStrings
call can cleaned up significantly:
CatStrings(State->OnePastLastEXEFilenameSlash - State->EXEFilename, State->EXEFilename,
StringLength(Filename), Filename, DestCount, Dest);
You'll notice that we didn't ask for the FilenameCount
; however, we did pass to CatStrings
a magic function that we called StringLength
. As we'll see in a moment, there's nothing magic about it.
C strings are just bytes of data, with the last byte being 0. (at least for the ASCII strings, Unicode is slightly more involved, but we won't mess around with that for now). So we'll count all the bytes until we reach 0, and that will be our string length:
internal int
StringLength(char *String)
{
int Count = 0;
while (*String++) // While the character is not 0 (do not confuse with '0' the character!)
{
++Count;
}
return (Count);
}
internal void
Win32BuildEXEPathFilename(...)
{
//...
}
Now we have a convenient function that we can use whenever we want to.
win32_handmade.cpp
Before we move on, let's do some reorganization. We need to move the functions CatStrings
, StringLength
, Win32GetEXEFilename
and Win32BuildEXEPathFilename
to the top of win32_handmade.cpp
, just before our utility functions:
typedef DIRECT_SOUND_CREATE(direct_sound_create);
internal void
CatStrings(size_t SourceACount, char *SourceA,
size_t SourceBCount, char *SourceB,
size_t DestCount, char *Dest)
{
// + contents
}
internal int
StringLength(char *String)
{
// + contents
}
internal void
Win32GetEXEFilename(win32_state *State)
{
// + contents
}
internal void
Win32BuildEXEPathFilename(win32_state *State, char *Filename,
int DestCount, char *Dest)
{
// + contents
}
DEBUG_PLATFORM_FREE_FILE_MEMORY(DEBUGPlatformFreeFileMemory)
{
// ...
}
// ...
internal void
CatStrings(size_t SourceACount, char *SourceA,
size_t SourceBCount, char *SourceB,
size_t DestCount, char *Dest)
{
// ...
}
internal int
StringLength(char *String)
{
// ...
}
internal void
Win32GetEXEFilename(win32_state *State)
{
// ...
}
internal void
Win32BuildEXEPathFilename(win32_state *State, char *Filename,
int DestCount, char *Dest)
{
// ...
}int CALLBACK
WinMain(...)
{
// ...
}
This allows us to use these path-assembling functions on our Input Recording/Playback functions! We don't want them to live inside our data
folder; this is the folder for good data.
Finally, let's do the fix that we wanted. We want to create a new function that would provide anyone asking for a universal Input Storage name. Let's also rename the file as loop_edit.hmi
instead of foo.hmi
(.hmi
standing for “HandMade Input”).
We'll use the new function inside both Win32BeginRecordingInput
and Win32BeginInputPlayback
.
Something for the future, we'll also assert that the SlotIndex
is always 1.
internal void
Win32GetInputFileLocation(win32_state *State, int SlotIndex, int DestCount, char *Dest)
{
Assert(SlotIndex == 1);
Win32BuildEXEPathFilename(State, "loop_edit.hmi", DestCount, Dest);
}
internal void
Win32BeginRecordingInput(win32_state *State, int InputRecordingIndex)
{
State->InputRecordingIndex = InputRecordingIndex;
char Filename[WIN32_STATE_FILENAME_COUNT];
Win32GetInputFileLocation(State, InputRecordingIndex, WIN32_STATE_FILENAME_COUNT, Filename); char *Filename = "foo.hmi"; State->RecordingHandle = CreateFileA(Filename, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0);
// ...
}
// ...
internal void
Win32BeginInputPlayback(win32_state *State, int InputPlayingIndex)
{
State->InputPlayingIndex = InputPlayingIndex;
char Filename[WIN32_STATE_FILENAME_COUNT];
Win32GetInputFileLocation(State, InputPlayingIndex, WIN32_STATE_FILENAME_COUNT, Filename); char *Filename = "foo.hmi";
State->PlaybackHandle = CreateFileA(Filename, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
// ...
}
We've made plenty lesser edits for one day, but still not enough to call it done. Next time, we will finish our prototype platform layer for good.
Previous: Day 23. Looped Live Code Editing
Up Next: Day 25. Finishing the Win32 Prototyping Layer