14 KiB
Days 1-25
Initial setup and platform layer bootstrapping.
Day 1
Project initially setup. Followed the video and the book.
I went extra since I already have a perferred setup I've recently grown accoustmed to and I'm going to take advantage of the metaprogramming library I've recently made for myself. Although I doubt it'll see much action...
I'll be keeping the code pretty C like as Casey does in the series. Main purpose is to be able to use whitebox.
Day 2
Pretty smooth so far, modular header library is showing its limitations...
I have enough practice messsing with forwards and library linkage now that it wasn't much of a show stopper at least. All the mess is quarantined in the in win32.h and so far it looks like it will be enough to keep it all there.
I added extra stuff in grime.h
, macros.h
, and types.h
that he will either go over soon from my vauge memory last time I just watched him (instead of doing) + stuff from gencpp's version of the zpl library.
Day 3
Was able to follow along just fine, lots of more Gdi32`` stuff to forward in
win32.h`.
Day 11
Architecture discussion was great. Did some diagramming of it.
I'm going to use a 3 layer approach to the architecture:
- Platform
- Engine
- Game (Handmade)
It will be style 2 so they each will provide a service to the layer below it and platform will provide an API to both engine and game.
Day 12
This day was a mess to follow along.
Always write the usage code first if you can, when designing something like an API.
Casey decided to do some heavy hits the audio (makes sense) due to the use of DirectSound. I'm going to preserve his implmentation as is for the most part since I rather keep things simple so that this project actually gets done.
Only introduce added complexity when you need it.
There are two reasons you allow for a more complex interface, you NEED the feature. It actually is required for the project to have for the UX to preserve the user-level features. Or, there is a performance bottleneck that you need to address.
Day 13
Started to lifting input, not happy about my abstraction to gamepad state in hindsight. Which is kinda lead by how Casey is doing it.
The reality of how input is actually done in the games; that I've worked on; is we don't abstract the gamepad. We just make a layout per device we decide to support (Dualsense/shock, xinput, nintendo, etc) and then just see what device is mapped to what player. This allows us to have a tailored layout to the device. Doing otherwise usually leads to some discrepancy in the UX for that layout for the user.
Casey has no concept of a player nor a device beyond xinput for this engine (at least up to this point), which is most likely fine for the scope of this project.
For the purposes of my version of Handmade I will make 3 devices that could be mapped to a Controller
struct: Keyboard & Mouse
, Dualsense
, and XInput
.
This is to retain implementation Casey is already making while also natively supporting the Dualsense controller from sony which I personally perfer to use.
From the 3 layer abstraction (platform/engine/game):
Platform will deal with the raw polling (and most likely down the line have a thread dedicated to running it as fast as possible).
It will provide the engine layer the pad states in the following struct:
struct InputState
{
ControllerState Controllers[4];
};
There can only be four controllers available at a time and are equivalnet to the input aspect of a local player in Unreal.
(Sort of)
Each controller can be assigned the following:
struct ControllerState
{
KeyboardState* Keyboard;
MousesState* Mouse;
XInputPadState* XPad;
DualsensePadState* DSPad;
};
If any are null that means the controller has no assignment.
Each pad state is the latest state but the platform keeps a record of the previous frame's state to compare against.
Exmaple for Dualsense:
struct DualsensePadState
{
struct
{
AnalogStick Left;
AnalogStick Right;
} Stick;
AnalogAxis L2;
AnalogAxis R2;
union {
DigitalBtn Btns[14];
struct {
struct {
DigitalBtn Up;
DigitalBtn Down;
DigitalBtn Left;
DigitalBtn Right;
} DPad;
DigitalBtn X;
DigitalBtn Circle;
DigitalBtn Square;
DigitalBtn Triangle;
DigitalBtn Share;
DigitalBtn Options;
DigitalBtn L1;
DigitalBtn R1;
};
};
b32 using_analog()
{
return true;
};
};
The game layer handles mapping an input state of a controller to an action binding.
Possible action binding when we get to that point:
struct ActionBinding
{
String InternalName;
StringCached LocalizedName;
DigitalBtnBinding* BtnBinds;
AxisBinding* AxisBinds;
StickBinding* StickBinds;
MouseBinding* MouseBinds;
};
Day 14
The game_state
example felt like a forced way to start doing memory management but thats fine.
I'm not making any design changes to it other than setting it as engine::Memory instead of game_memory (since I'm doing 3 api layers instead of the 2 hes doing).
Learning about the virtual memory mapping was nice, it was something on a backburner for me to learn about but I never got around to it.
The address setting was quite awesome to see.
Some thoughts on platform API
I like Casey's one call idea he emphasized in the Q&A, don't really want to detract from that.
Instead I just want to change it so that by the time you enter the engine loop you are guaranteed to have all the engine data provided by the platform layer.
But, without having to shove everything in the procedure captures. Instead, just have an global engine context thats invalid until engine initialization is done by the platfomr layer.
So it might end up something like this:
It ends up making two api paths, one for engine to do initialization when platform provides all the critical data engine needs, and another where the platform provides the engine update function.
It will be known to the user and engine that platform will update some of the engine data ( like input, render, sound, etc ), which will be done in the platform update function.
This is already the case, just need to convey it more strongly if I'm decoupling it from the procedure captures.
This is so that I don't have to do insane chaining to pass these captures when I know some of this data should be contextually known to all procedures within the engine.
I have a feeling Casey may already aleivate this or show that it's not a problem in the future so we'll see and I'll adjust accordingly.
I premtively also did the boxes for an 'editor' and 'game' api. Where there could be possible n
amount of api layers. I'm not sure if I'll do this, but I'm leaving it open for now. What will most likely happen is they'll still be priority to keep it to a very minimal amount, and not give the game or editor the ability to poke at the engine much at all (which leads to the circular API Casey is warning about).
Day 15
Casey decides to restrict File IO to 32 bit limits for file size (under 4 gigs). This is due to a limitation do the win32 API in how much it can read into a provided buffer to ReadFile
by the size of DWORD (4-bytes). For the purposes of this engine and problably almost all applications this is alright.
IF somehow more is needed, just reading in chunks of 4 gigs at a time is fine.
I ended up watching a vod earlier recommended: Mysteries of Memory Management Revealed,with Mark Russinovich Part 1 Part 2
Was really nice, I got some of the equivalent info for linux.
Day 16
Finding out about the linker's map file was nice.
Day 17
Talks about functional programming. Mostly an emphasis of not abusing global state fields or constricting the interface a state may be musted from. With the 'ideal' if performance is not heavily impacted is to use a pure function.
Like I said in Day 14 however I do want to setup a less stack-spam way to pass around data for ergonomics, so instead I'll be using static analysis with gencpp to make sure they are only used in the correct places (there will be a list of function names that are allowed to use the global state, and if it is used in any other function it will throw an error, preventing handmade from compiling).
I wish Casey used the GetAsyncKeyState
instead of this window messaging.
(I'll be using it, not a fan of getting input callbacks from the window)
I didn't do iteration accross all controllers, I'll worry about that when I setup multiple players. When it comes to the "controller stick averaging" and abstracting that to actions (like he did) hes bleeding into what should really be an input system with bounded actions, but he doesn't want to make an input system (so its just rapid hardcoding).
I'll do the averaging on the platform layer (for now) since it simple, but like said previously, I will not be allowing the engine layer to abstract what API devices are assigned to a "controller".
Day 19
I started to do some cleanup while also following along.
handmade_win32.cpp
now just holds the cpp build order when running the build script for unity builds.
I want to eventually support segemnted builds as well, that way I can make sure there isn't some unhandled usaged of a global or some other symbol in one of the source files.
Evetually I can also use gencpp perform some static analysis to make sure differnt layers are not accessing symbols that they should not.
I started to setup also pulling the polling of input out to a different function.
There are a few routes to go about it:
- Make a separate function per input type: ( poll_keyboard, poll_xinput, poll_jsl ).
- Make a single function that handles all input polling (poll_input or process_input).
- (problably more...)
I'm gravitating toward the second option, the amount of content is not that large, and would keep the code all together along with its control flow.
Overall this was pretty brutual to follow along with for the vod. Thankfully handmade-notes exists..
Reading the end of the notes page for the episode showed a forum link to mmozeiko discussing use of the Windows Core Audio API to alleviate the large amount of latency required to operate with DirectSound.
I'm going to wait until after day 20 since Casey has that one titled "Debugging the Aduio Sync"
Day 20
This stream was easier to follow than the last. I'm starting to gain some intuition on his style.
Archtecture wise
I'm deciding to cause some more complexity leakage to the API layers..
I'll restrict it to on-demand but I want to have the engine have configurable options modern games tend to have, if I find it fullfilling todo so I may also expose some configuration for aspects of the engine, most games do not allow the user to mess with.
This was something I wanted to do with my previous attempt and constructing a "probeable" game engine, however, that attempt failed because I kept the API granularity far to high resolution.. and it was Modern C++ which is the project I learned the hardway was not worth the mental effort to maintain or engage with.
(I also dove straight into graphics programming with Vulkan... making my own vulkan wrapper library... At least I learned a good chunk of the driver's graphics pipeline..)
Debug visualization for the audio was great.
Day 21
So I learned today the good reason why he doesn't use static variables; So that when he makes this library dynamically loaded we don't lose the state of the game on reload.
This day was extremely gratifying to get working.
I went the extra mile than what Casey did and allow for name mangled symbols (for both clang and msvc).
It took a few more steps than his solution but I get to keep the eronomics of namespaces.
After the linker finishes emitting, I use the build script to parse the .map file and extract the decorated symbols I need to load in the platform layer. Those are exported to a file called handmade_engine.symbols
and then in the platform layer I load them up using an enum as the lookup table for the line the symbol will be in the file. From there its just loading up the symbol with GetProcAddress!
Day 22
There is an issue with the hot-reload on vscode, does not show up in vs2022 debugger nor remedybg so I won't bother with it. (Debugging hot-reloaded module doesn't work with vscode's debugger)
Day 23
Getting state saving was nice.
I decided to expose the file inteface through the platform API permanently, so that I can use it for other things later on.
I'm changing my varaible naming convention in structus from UpperCamel to lower_snake_case. I realized that semantic highlighting covers any contextual issues along with editor intellisense. Even at worst case with public/protected/private specifers intellisense covers it.
So I'm going with the option that keeps my naming convention consistent and more flexible to use with Casey's exploratory programming style.
Day 24
Did lots of cleanup & refactoring on my side.
Started to use gencpp to generate the engne module symbol constants for the platform layer.
Day 25
Him doing replay save states at the platform layer is causing issues for me...
I'm going to most likely end up doing two categories of save states, one for engine level and one for platform level.
The engine level can just store it in the memory struct, and the game memory can just stay where it is. I want to use the same "save state" slots for both engine and game since there is no need different memory buckets for that. They will at worst case be ofcourse the size of engine's memory block.
So if there is 4, we are looking at 5x the actual memory size. This is fine for now since the engine block is 2 gigs.
Snapshots of of memory are are handled using take and load snapshot functions. They can be used separate from the replay looping functionality.
Replay looping however is dependent on snapshots so if a replay is save/loaded it will use the same snapshot slot of memory to do that state management.
Feels great to be past the platform layer bulk!!!