Blog

Atom feed is here.


2025-11-27

The year is 750 AD. It's central Japan, the Nara Period, the tail end of winter. The cold is biting.

It's early in the morning. The sun has just started to peak over the mountains and through the trees. You're outside, wrapped in multiple layers, shivering. But you don't think about the cold. There are tens of other people around you doing the same. Everyone is facing a single person standing before everyone else. Let's call him the foreman. The foreman is giving out the instructions for the day. People will split into teams and go here and there. Everyone's general task is to cut down trees and prepare planks from them for carpenters to further refine.

You pick up your tools, hands hardened from all the work you've done up until now. You move with your team to your designated area. You self-organize into smaller units, some working in tandem to cut trees, others cutting fallen trees into logs, stripping bark and branches off of them. You get stuck in the group that has to chop the trees down. You and your partners gather around various trees with axes, each chopping away. You had an idea to create a long sawblade that two people could hold, one on each end, and you could fell trees faster with less work individually. It'll have to wait, though, as you can't create that on the spot. You wished you were in the team stripping logs or hauling them back to camp. Chopping down trees is hard work, and your team is the bottleneck. The others are resting while waiting for another tree to log and haul.

As you work, your body heats up, and you start to sweat. You remove a layer. It's uncomfortable, and you can smell the odor of your teammates as well. But these trees aren't going to fell themselves, and you can't let everyone else down. You try not to think about tomorrow, but you know you'll be back to do the same thing all over again. At least you'll get to rotate to a different responsibility.

6 months pass. You haven't always been felling trees for this time, but you've been doing similar manual labor otherwise. You think about the nobles who have come through town while you work. Ah, to be like them and not have to work so hard. Whatever, どうでも良い.

It's the middle of summer now, and its sweltering hot. You sit on cool stone in the middle of the day, unlike your days prior toiling away when it was colder. You're shaded from the midday sun by a large wooden overhang that is just one part of a large castle-temple complex. Last year this overhang wasn't here, and everyone felt its lack literally. You remember getting caught in the summer rain brought by the typhoons. It wasn't even refreshing. The air was so humid, and the rain was somehow warm. You were hot and miserable, and everyone smelled.

But this year is different. You sit under a tangible structure protecting you from the elements. Many others do the same. You weren't the carpenter, you weren't the architect, you aren't a noble, but everyone knows that you pitched in and is thankful for that. It starts to rain. You and your buddies look out into the damp heat and then look at each other, smile, and give a sigh of relief.

Summer transitions into fall. It's cooler now, and you're back to work, moving large stones from a quarry to a mason's workshop. The mason does his thing, and then you move shaped stones elsewhere around the castle-temple complex. It's monotonous drudgery. You wish you could be out helping your parents plant or pick rice like you did when you were 8 years old, daydreaming about goofing off with your friends later on or listening to some bizarre stories and wondering about the world. Whatever, どうでも良い. These stones aren't going to move themselves.

The next winter comes, and the cold is once again biting. The town doesn't need as much wood this time around, so you've moved on to other tasks, doing grunt work on farms, for smiths, for masons. This year the cold is more pronounced, though, since you have something to compare it to. You and your buddies look out into the snow falling around you, recalling simultaneously freezing and sweating this time last year. You're a little too hot, so you lift yourself out of the onsen, and steam billows from your body like you're some sort of deity. You feel like one, at least, knowing that the blood and sweat you put into all that work with wood and stone has provided for everyone around you, and everyone else knows it too. You built something that has measurably improved your own life and the lives of people around you, and you presumably get to enjoy it for the rest of your days.

The year is 2025 AD. It's Tokyo, the Reiwa Period, the tail end of winter. The cold is biting.

It's early in the morning. The sun has just started to peak over the mountains and through the trees. You're outside, wrapped in multiple layers, shivering. You think about the cold. There are tens of other people around you doing the same. Everyone is facing a single person standing before everyone else. Let's call it the crossing signal. The signal is giving out the instructions for when to walk. At work, people are split into teams. Everyone's general task is to move information from one service the company pays for to another service the company pays for.

You unlock your computer, heart hardened from all the work you've done up until now. You check-in with your team on your designated Slack channel. You self-organize into smaller units, some working in tandem to address this problem, others essentially creating new problems. You get stuck in the group that has to fix problems. You and your partners gather around various documents, each cross-checking, debugging. You had an idea to create an automated test barrage, so you could catch issues earlier, faster, with less work individually. It'll have to wait, though, as you can't create that on the spot. Thankfully they run the heater late into the night, so you can stay late and develop it on your own time. You wished you were in the team creating new problems for everyone else. At least they get to create puzzles for themselves and then code up solutions. Fixing problems is hard work, and your team is the bottleneck. Management is waiting for another fix to send to the customer.

As you work, your body heats up, and you start to sweat. You remove a layer. It's uncomfortable, and you can smell the odor of your teammates as well. But these tasks aren't going to finish themselves, and you can't let everyone else down. You try not to think about tomorrow, but you know you'll be back to do the same thing all over again. At least next quarter you'll get to rotate to a different responsibility.

6 months pass. You haven't always been fixing problems for this time, but you've been doing similar labor otherwise. You think about the managers who have come through the org while you work. Ah, to be like them and not have to work so hard. Whatever, しようがない.

It's the middle of summer now, and its sweltering hot. You sit in a climate-controlled office in the middle of the day, just like your days prior toiling away when it was colder. You're shaded from the midday sun by a large, sterile ceiling that's just one part of your company's massive office. Last year this ceiling was there, and you didn't even think about it. You were caught in the summer rain brought by the typhoons on your way in. It wasn't even refreshing. The air was so humid, and the rain was somehow warm. You were hot and miserable, and everyone smelled.

This year is no different. You sit under a tangible structure protecting you from the elements. Many others do the same. You weren't the carpenter, you weren't the architect, you aren't a manager, and you didn't pitch in. No one even takes a moment to be thankful for the structure. It starts to rain. You and your buddies don't even look out into the damp heat, are fixated on meeting deadlines for people they don't care about, building something they'll never use, for people that will never know them, if the product isn't scrapped on the whim of someone entirely disconnected from the work.

Summer transitions into fall. It's cooler now, and you're on a different project. The teams around you do their thing, and then you integrate it. It's monotonous drudgery. You wish you could be out helping your parents garden like you did when you were 8 years old, daydreaming about goofing off with your friends later on or playing some fantasy RPG and wondering about that world. Whatever, どうでも良い. These tasks aren't going to complete themselves.

The next winter comes, and the cold is once again biting. Your org didn't need as many people this time around, so you've moved on to other tasks, doing grunt work for the next project. This year the cold is no more pronounced. You look out the window into the snow falling, recalling nothing. You're a little too hot so you take your jacket off. Steam billows from your coffee like some sort of demon. It feels like one, at least, knowing that you depend on it to keep you going, pouring sweat and life into all that thankless work you have to do. Everyone knows it, so you tell yourself its okay, but they don't care, because that's just life, isn't it? You built something that has measurably improved nothing, and you don't even get to enjoy it, because you never really needed it anyway. And you never will for the rest of your days.


2025-11-24

I had joined a startup a few months back, but I recently left. Back to my own thing at this time in my life.

When I first left industry to do my own thing, so many people asked me what was next for me and were surprised by "doing my own thing" that I had to refine my answer to make the inevitable same-conversation-many-times efficient. The common question to me was, "what would it take for me to work for someone else again?"

My answer was, simply needing money aside, three criteria.

  1. The company is a small group of experts.
  2. The group has a single, measurable mission.
  3. The mission and tasks resonate with me.

I've since added a fourth criteria: everyone is humble.

Also, I've created a new rule for myself for any interview I participate in from here on out. That rule is, if I want to join a company, I need to first come up with my own plan and explanation of how I would accomplish their mission, end-to-end, meaning from hiring people and starting with an empty text file in front of me as a software engineer, to shipping and maintenance. It's most important to run this by company leaders to get a sense for their familiarity and experience in the domain and also to get an early read on points I might disagree with or things I might be wrong about and expectations I need to adjust.

With that out of the way, here is my next development update. I was previously writing development logs mostly daily, but now I think I will do so weekly, unless there is some singular topic I want to on about at length. It's Monday already, but I'll be posting about last week. Also, it's a three-day weekend here in Japan, but when you work for yourself and have no income yet, there is no such thing.

I'll experiment with posting a slim log and then touching on this and that. The command I'm using to print the log is

git log --pretty=format:"%ad%n%B" --date=short --reverse
2025-11-15
docs: initial commit

2025-11-15
feat: open a window, attempt FPS target

2025-11-17
feat: render a simple map the size of the window; add build docs

2025-11-17
feat: introduce create/destroy entity function; create player

2025-11-17
feat: implement basic movement and collision

2025-11-18
tweak: double running speed in tiles per second

2025-11-18
feat: introduce GameMap struct

2025-11-19
feat: add camera that follows player; also add clip rect on main window

This fixes a rendering slowdown caused by extending the map size in an
earlier commit and rendering the entire map--even portions out of the
frustum--on each frame.

2025-11-19
docs: index documentation and add docs on coordinate systems in use

2025-11-20
chore: switch tile size to 16 pixels to anticipate maps; extend docs

2025-11-21
feat: add small paths and strings library

2025-11-21
feat: add aseprite to binary map conversion wrapper tool

2025-11-21
feat: add a map parser; this commit does not include assets/, data/

2025-11-21
feat: add string view and parent path utilities

I restarted the codebase. I did this mostly to refamiliarize myself with what will become the entire implementation. I'm still using SDL3 and, by consequence, CMake. I considered switching to Jai or Odin but stuck with my favorite dialect of "C++," which is C with operator and function overloading. Aside, this podcast had some hilarious parts, where the host was hoping for hot debate around what C could do better and where it shines, didn't get it, and was getting jokingly visibly frustrated when the guests mostly kept agreeing with each other about how bad C was and why they nonetheless stick to most of its core principles.

Currently the game is entirely tile-based with tile-based positions and movement. Entities are just a struct of a position and sprite, in that order. Collision is a simple can-move-to-world-tile check at this time.

I had never implemented a camera before, so that was interesting. Being real with you, it took me an embarassingly long amount of time to conceptualize the transformation in my head. My understanding is that there are two common types of camera setups in top-down 2D games, and they differ in how the camera position is tracked. One setup tracks the camera position in screen pixels as the top-left corner of the screen (if your render system places (0, 0) at the top-left of the screen. The other setup tracks the camera position in screen pixels as the center of the screen. In my case, I track it at the center of the screen. This is so that if I want to "watch" any particular tile or entity, all I need to do is set the camera to that entity's position. Currently I do so for the player, so the camera tracks the player as the player moves.

Regarding the transformation, consider the following scenario:

+-------+(0,0) px
|
v                        Screen
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X  |                                                        X
X------------------ screen width ---------------------------X
X  |                                                        X      Game Map
X  |                     +----------------------------------X----------------+
X  |                     |                                  X                |
X  |                     |                                  X                |
X  |                     |                 o                X                |
X  |                     |                 ^                X                |
X  |                     |                 |                X                |
X  | screen              |                 +---+tile to     X                |
X  | height              |    o                 render on   X                |
X  |                     |    ^                 screen      X                |
X  |                     |    |                             X                |
X  |                     |    +---+camera pos               X                |
X  |                     |                                  X                |
X  |                     |                                  X                |
X  |                     |                                  X                |
X  |                     |                                  X                |
X  |                     |                                  X                |
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX                |
                         |                                                   |
                         |                                                   |
                         |                                                   |
                         |                                                   |
                         +---------------------------------------------------+

Assume we want to render the point labeled "tile to render on screen." To do this, we need to find that tile's position in screen space. The tile has a position in game-map tile space (Px, Py). The camera has a special property of having a position known in two frames. It has a game-map position (Cx, Cy), and we also know it has a fixed position in screen space in the center, which is (screen width / 2, screen height / 2). The formula to transform the tile to render from game-map space it screen space (Psx, Psy) is thus

(Psx, Psy) = ((Px, Py) - (Cx, Cy)) * (tile width px, tile height px) +
             (screen width / 2, screen height / 2)

(Px, Py) - (Cx, Cy) gets the tile position in game map space relative to our camera position. If the camera were at (0, 0) in game map space, the position relative to the camera would simply be (Px, Py). Multiplying by (tile width px, tile height px) transforms the game-map tile coordinates into game-map pixel coordinates (the coordinate now points to the upper-left corner of a particular tile). Finally, to transform game-map pixel coordinates into screen pixel coordinates, we need to add the camera offset. Again, if the camera were at (0, 0), we would be done.

This transform can produce coordinates outside of the visible screen space. As one of my commit messages above suggests, if we don't avoid rendering outside of the screen space, we will pay the cost of rendering. I found with SDL3 that even if I set the render clip rectangle to the screen space, I still run over my frame times. So, I also have some logic to determine the subset of the game map to consider for drawing to the screen on a given frame, and then I apply the transform shown above to convert visible tiles to screen space.

On to game map design, I will stick with pixel art and Aseprite. I made a small patch to my Aseprite tilemap binary exporter to adjust the exported file names, and then I introduced a Python utility on my game-code side to transform .aseprite files to binary map files. The script ultimately transforms as follows:

test-map.aseprite -> .
                     ├── test-map/
                     │   ├── tileset1.png
                     │   └── tileset2.png
                     └── test-map.bin

test-map.bin is what my game code parses to load in map data, and resources corresponding to "test-map" are stored in a folder of the same name peer to the binary file. Paths in test-map.bin are relative to the location of test-map.bin. Avoiding absolute paths in this case is for portability, especially since the resource location on disk in the source tree is different than the location resources are placed after a build. I put my resources in a data folder, map resources in data/maps in particular. To accomodate relocation in CMake, I have

add_custom_command(TARGET the_target POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different
    "${CMAKE_SOURCE_DIR}/data"
    "$<TARGET_FILE_DIR:the_target>/data"
)
add_custom_command(TARGET the_target POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different
    "${CMAKE_SOURCE_DIR}/data/maps/"
    "$<TARGET_FILE_DIR:the_target>/data/maps"
)

This requires CMake 3.26. The annoying thing is that copy_directory_if_different does not do a recursive comparison, so I need to check the top-level data directory and data/maps directory explicitly. Also, content removal from the source directory does not count as a difference, meaning that previously-removed resources can linger around in the build output. I could remove and re-copy all resources on every build, but until I hit some gotcha, using an old resource by accident, I won't bother.

SDL3 provides a convenient SDL_GetBasePath which allows me to get the path the executable is run from. This allows me to then discover my executable-relative resource directory and correctly resolve the relative paths in the map binary files.

Something like std::filesystem would be handy here, but I'm avoid std:: as much as I can, so I have reimplemented just what I need. Toward this, I introduced a simple string view utility. It's trivially defined as

struct StringView {
  char* str;
  size_t len;
};

where the characters are ASCII-only and the len portion of the backing string is not guaranteed to contain a null terminator. We'll see how useful this turns out to be. I mostly introduced it because I have not introduced my scratch allocator yet. If I had the latter, I'd allocate without thinking much about it, knowing the memory I allocated will be reclaimed shortly after.

Toward what I need from std::filesystem, I introduced path_concat and path_parent functions. path_parent finds the parent in a provided path.

StringView path_parent(const char* p, size_t len);

This also took me an embarassingly long amount of time to write elegantly. I won't share the code in the event you want to challenge yourself. (By the way, AI output was rather bloated and inefficient at the time of this writing.) My implementation only handles Unix path separators at this time, so this will be one implementation I need to port when going to Windows. Here are the test cases I pass, if you want to take a go at it yourself.

struct TestCase { const char* input; const char* expected; };
TestCase cases[] = {
  { "/a/b/c//", "/a/b", },
  { "/a/b//c", "/a/b" },
  { "/a//b/c", "/a//b" },
  { "/a/b/c/", "/a/b" },
  { "/a/b/c", "/a/b" },
  { "a/b/c", "a/b" },
  { "a//", "." },
  { "//", "/" },
  { "/ab", "/" },
  { "/a", "/" },
  { "./a", "." },
  { "//a", "/" },
  { "//", "/" },
  { "a", "." },
  { "/", "/" },
};

Speaking of going to Windows, one reason I'm using SDL3 is to allow myself to develop on Linux and macOS and "just" test on Windows. We'll see how that goes. I really like the idea of being able to bring my macBook wherever with me and still develop and play my game, don't really have a decent portable option with Windows at this time.

Regarding map loading and whatnot, I may ultimately bake the map data into the final binary. I may either use the recent #embed or, more than likely, just implement a tool myself to achieve the same.


2025-10-15

I heard a quote yesterday--I think it was from Chris Bumstead--along the lines of

What used to drive me now exhausts me.

I think it very succinctly captures my feelings about software development at this stage of my life.

I used to study programming for programming's sake. All you had to say was, "did you know?" and you had my attention for an entire weekend, studying more sophisitcated ways to do the same old thing, diving into arcana, reciting the new mantras. Because I liked puzzles. Because I wanted to be in the group that knew. "Did you know" is not a question anyone asks to actually learn anything. It's a lead-in to a flex. Virtue signaling. A waste of time. Studying the esoterica of physics has an end and ultimately leads to simple solutions for everyone. I find that studying the esoterica of most modern software leaves one prone to normalize and even perpetuate it.

Now the arcana exhausts me. It's mostly mental gyration, or even worse, just puzzles we have created for ourselves that get in the way of making meaningful progress for the species.

Use as few tricks as possible.
Write as little code as possible.
Add as few abstractions as possible.

Software engineering is a means to an end.

We've normalized "best practice" as a header file, an implementation file, object-oriented whatever, 5 different kinds of constructors, meta-programmed interfaces that help account for non-ref, ref, const ref, universal (lol) ref, etc. Newcomers to the field look at the awful tools and think, mastery of this is what it means to be great. The tools are so complicated that tool use itself was reified into meaningful work when no one was looking. This is why competent leadership in a company is so important--you need leaders who can discriminate between someone who knows everything about hammers and someone who knows which to reach for to build something.

If you want to transform your ego into code, that's fine, but keep both off of the critical path of something we're all supposed to use in the end. Physics doesn't care about your ego, so I'd like to keep ego out of software engineering for the species as well.

Did you know you can do pretty much everything with structs, function overloading, and operator overloading?


2025-208

A Japanese person posted the following question in English on a language exchange application I use: "What makes someone feel romantic love for another person?" I've wanted to be able to discuss things like "love" in Japanese, so this seemed like a good opportunity for me to learn in context. My Japanese is still insufficient, but I'm sharing my reply here, in part because it does capture the essence I've come to understand of love, and in part to act as a benchmark of how far I've come with Japanese and how much further I still have to go.

この投稿をありがとう! 私の意見を日本語でよく説明できるかどうかわからないけど、頑張る笑

「ラブ」は人によって意味が違うね。 例えば、昔にギリシャ人はラブを三種類に別れた。 この投稿に関している「ラブ」の種類は二つがあると思う(ギリシャの分類とよく合わない)。

先ずはロマンチックなこと、つまり恋。 恋は相手の見た目、言動、地位、肉体的な相性などに関しているものだ。 恋の原因は先天的(例えば遺伝)と後天的(例えば文化、環境、社会)な要因だと思う。 このラブは二人がいて、私と私の相手だね。 カップルは関係のために取引している。 私は何かをあげる、例えば親切な言葉や時間や思いやりな贈り物や恋愛だ。 相手も適当なことをくれる。 もちろん、いい関係にはこの取引が仕事のようなことじゃなくて、カップルは互いにこうしたいから自然なことだね。 でも結局二人、私と私の相手、がいる。

一方、愛ということもあるね。 これは一体感を探すことだ。 愛の原因は、なんとか相手の中に自分自身を見ることだ。 恋の二人より、愛は一人だけがいて、「私たち」だ。 相手にちゃんと自分自身を見るために、自分自身をよくわかって愛することが必要だと思う。 もちろん相手は自分の興味や目標があるけど、相手の中に自分自身を見るから自然に心の底から応援できるようになる、もう自分のことを愛しているのおかげで。 これは愛の逆説でしょう、相手を愛するようになるために、先ずは自分のことを愛しなくちゃいけない。 自分のことを愛するために、先ずは自分のことをわからなくちゃいけない。 自分をわかるというのは、自分の目標や価値観や前のトラウマを解けることなどだ。

恋の始まりは外に見ることによる。 愛の始まりは中に見ることによる。 恋の方がわかりやすい、映画や曲やソーシャルメディアでイメージが多いから。 愛をわかるの方が難しい、あまり誰にも自分の心の底を見えないから。 (例えば両親の方が他の人より自分の子供の心をわかりやすいと思う。) もちろん、人は年に取るにつれて変える、でも愛の原因のおかげで、カップルが一緒に変えることもできると思う。

私にとって、いい関係をできるために、恋と愛とどっちも必要だと思う。 英語ではこのラブの種類について説明しかたもちろんあるけど、あまり特別の言葉じゃない(日本語とかギリシャ語より)。 私の返信のために新し言葉と表現をならなくちゃいけなかったから、もし日本語の言葉のニュアンスを誤解したなら、すみませんね! ここまで読んでくれてありがとうございます! またおもしろう投稿もありがとう!

Posted after realizing I left out another thing I wanted to share:

ところで、いい関係の基準について意見をシェアしたいと思っている。 関係は履歴証で並んでいる項目のようなことに基けば基づくほど、不安定になるの可能性が高いと思う。 給料、仕事、趣味、実績、どんな学校など、これに興味があるのは問題ない。 でもこんなものは相手の性格じゃなくて、相手の性格の結果でしょう。 私にとって、適当な基準は相手と一緒にいる時心地いいや相手を深く信じられる、ユーモアが好きや相手の親切さか思いやりが好きなど。 こんなことの方が説明するのは難しいけど、恋より人生のどの時期においてもより安定していると思う。 それにしても、恋も大事なものでしょう。


2025-191

Since I last wrote, I extended the bump allocator from last time to also provide storage persistent across frames. This is necessary to contain at least two things: the general game state struct and tilemap indices. Both of these need to persist across frames. Previously, I was instantiating the game state on the platform side and passing a pointer to it into my game-update function. The platform is otherwise game-state-agnostic, though, so this didn't make much sense. For the tilemap indices, I was just malloc'ing space for them. My recent goal was to remove all instances of malloc from the game logic-side code.

Now, the allocator type definition looks like this:

struct Allocator {
  void* base_temp = nullptr;
  size_t size_temp = 0;
  size_t used_temp = 0;
  // Implementation detail: `base_persist` shall point to the beginning of the
  // allocated memory.
  void* base_persist = nullptr;
  size_t size_persist = 0;
  size_t used_persist = 0;
};

The implementation detail is because I allocate a single region of memory for both types of storage. The temporary storage starts size_persist bytes after base_persist. I also modified the API to separate allocations to temporary storage and persistent storage and to only provide the reset ability on temporary storage. Besides resetting, there is still no general ability to free allocated memory, meaning that allocations in persistent storage are expected to live for the lifetime of the program runtime.

The platform side sets up the allocator and its memory and passes it to my game update function. Perhaps a bit dirty, but although I am dealing with an allocator, it really is my game's memory pool with functions to manage the memory. Recall from the previous post that I do use the temporary storage a bit on the platform side to build a couple strings. This is no problem since no one should rely on exact positions of anything in temporary storage.

On the other hand, I require that the platform never allocate from persistent storage the game will use. This way, my game can always assume its game state object lives at the beginning of persistent storage. At the beginning of each pass through my game update function, I get at the game state via:

GameState* gs = (GameState*)allocator->base_persist;

My game state object has an initialized flag that tells me whether I need to actually set it up or not. Because I use mmap to allocate the backing storage, the storage is guaranteed to be zeroed after allocation. As a sanity check, I make sure that if the game state is unintialized that allocator->used_persist is also 0. I then stash pointers to this and that in persistent storage in my game state instance, and from there I can navigate the game memory across frames.

When initializing a tilemap, I previously dynamically allocated space for as many tilesets and layers as I needed based on what I discovered in my binary-encoded tilemap. Now, I set up storage for some fixed number of these things in persistent storage and point a tilemap instance at these persistent resources during tilemap initialization. This way I can avoid dynamic allocation entirely in the game logic, at the expense of allocating more memory up front than I may ultimately need. I can tune the allocations as the game matures.

Right now my game update function interface looks like this:

/**
 * Update the game state based on input.
 *
 * @param a The allocator containing the memory for the game. The game requires
 *          that the provider does not modify permanent storage in any way.
 * @param input The input to use to update the game state, assumed to be input
 *              toward the next frame to render.
 * @param renderer The rendering destination for all graphics updates.
 */
void game_update(Allocator* a, Buttons* input, SDL_Renderer* renderer);

At first I thought it would be nice to keep SDL things out of the game logic code, but SDL itself is supposed to be my operating system abstraction, so I'm fine bringing it into the game logic code. I do call SDL routines that (de)allocate, for example creating or destroying textures. To avoid this, I would need to build a graphics abstraction on the game logic side, probably via various memory buffers allocated on the platform side and passed to the game update function, have the game code write into those buffers, and then have the platform side move content from those buffers into SDL resources. But that is just abstraction on top of the thing I'm using for abstraction, which is not the way to go.


2025-189

As an unintentional follow-up to yesterday's post, I ended up writing a simple bump allocator. The motivation was that I wanted to build a routine to convert relative paths to absolute paths. For this, I need a place to store the absolute path. The relative paths are relative to the folder the game binary sits in, so I can't predict the full string length of the absolute path, meaning I need to dynamically allocate.

Here is the API for the bump allocator; no surprises if you've seen one before.

struct Allocator {
  void* base = nullptr;
  size_t size = 0;
  size_t used = 0;
};

/**
 * Set up an allocator. The allocator's memory is guaranteed to be zeroed.
 *
 * @param a A pointer to an allocator to set up; cannot be `NULL`.
 * @param size The number of bytes to allocate. Must be greater than 0.
 * @param addr The beginning address to allocate from. Pass `NULL` to defer base
 *             address selection to the operating system. Note that any other
 *             value greatly reduces portability.
 * @return `false` if setting up the allocator failed for any reason; `true`
 *         otherwise.
 */
bool allocator_init(Allocator* a, size_t size, void* addr);

/**
 * Allocate memory from the allocator.
 *
 * @param a The allocator to allocate from; cannot be `NULL`.
 * @param size The number of bytes to allocate. Must be greater than 0.
 * @return If successful, returns a pointer to the beginning of the allocated
 *         region. If unsuccessful for whatever reason, returns `NULL`.
 */
void* allocator_alloc(Allocator* a, size_t size);

/**
 * Reset an allocator, meaning that its memory pool is considered entirely
 * unused and the next attempt to allocate will start at the beginning of the
 * backing memory pool. This does not deallocate anything from the operating
 * system's perspective, but anything previously allocated in the allocator's
 * memory pool should be considered invalid after calling this.
 *
 * @param a The allocator to reset; cannot be `NULL`.
 */
void allocator_reset(Allocator* a);

/**
 * Destroy an allocator.
 *
 * @param a The allocator to destroy; cannot be `NULL`.
 * @return `false` if destroying the allocator failed for any reason.
 */
bool allocator_destroy(Allocator* a);

I give the user the option to specify the base address of the memory pool or let the operating system pick it. In production, I'll let the operating system pick it, but in development, I'll control the base address so I can predict the general location of things in memory across program runs.

In the implementation of allocator_init, I use mmap instead of malloc, like so:

bool allocator_init(Allocator* a, size_t size, void* addr) {
  assert(a);
  assert(size);
  int flags = MAP_PRIVATE | MAP_ANONYMOUS;
  if (addr) {
    flags |= MAP_FIXED_NOREPLACE;
  }

  a->base = mmap(addr, size, PROT_READ | PROT_WRITE, flags, -1, 0);
  if (!a->base) {
    return false;
  }

  a->size = size;
  a->used = 0;
  return true;
}

In short, we permit reading and writing on the allocated space, the memory region is private to our process (MAP_PRIVATE), we do not use file-backed memory (MAP_ANONYMOUS and -1 for the file descriptor parameter), and should the caller request a specific base address, we tell the operating system to use a fixed address and force failure if our requested address has already been mapped by us elsewhere in our process (MAP_FIXED_NOREPLACE).

I'm using this as a frame allocator, meaning that at the beginning of each frame (at the beginning of each iteration through the game loop), I call allocator_reset. I set up the allocator right after initializing SDL near the top of main, outside of the game loop. This allows me to use the allocator before entering the game loop for any one-off scratch work I need to do, and then immediately upon entering the game loop (with the exception of capturing the frame start timestamp), the allocator is immediately reset.

Before entering the game loop, I use my relative-path-to-absolute-path utility to build the full path to the game logic library so that I can check its last modification time via stat. I then periodically re-build the full path in the game loop when I go to re-check the modification time, so in the future, I will likely also introduce separate, persistent storage to cache such things.

Instead of having to implement my own way to get the base path of the game binary across operating systems (using convenience functions in Windows and checking /proc things in Linux, for example), I can thankfully just leverage SDL_GetBasePath.

The interface of my routine to build absolute paths looks like this:

char* abs_path(const char* path, Allocator* a);

It takes an allocator to use to obtain storage for the final string. The Jai language I mentioned in a previous post does something similar for routines that need to dynamically allocate but passes the current allocator to the function implicitly in an argument called the context. I don't have that language feature in C/C++ without approximating via OOP, globals, or function/method partials, so the allocator is an explicit parameter in my case. I don't like this signature much, but it accomplishes what I need. I haven't covered this detail yet, but I have also separated more of the game logic code from the "platform" and exposed a higher-level game_update function from the game logic library to the platform. The game_update function will likely eventually receive a handle to the frame allocator so that it can allocate whatever it needs in a single frame in a manner I can easily control and introspect. That is how game logic code will also be able to use things like abs_path without owning any particular allocator or backing memory pool.

Here are a couple other things I learned along the way. First, it is not only Linux that has alloca; Windows has it as well. The main difference besides the literal function name is that Linux's causes undefined behavior on stack overflow whereas Windows's will raise a structured exception. I could use this for building strings as I don't anticipate memory required for paths to be so large that it would cause a problem on the stack. The second thing I learned is that man pages can even reference books. I noticed the man page for mmap(2) (Linux man-pages 6.7, 2023-10-31) referenced what appeared to be a book, and upon looking it up, indeed, it is "POSIX. 4: Programming for the Real World," by Gallmeister. Interesting, might want to skim through it.


2025-188

In my game, I want to control the pool of memory from which all allocations are made. This will help with debugging and also general stability, if I can allocate all memory I will need up front and then parcel that memory out internally on-demand. I haven't gone into much detail about why I am not using a game engine, but having more control over allocations or at least the memory pool being used is one of them.

SDL provides SDL_SetMemoryFunctions which a developer can use to specify the functions to call in place of the typical malloc, calloc, realloc, and free. I have previously implemented my own simple linear allocator, from which all I ever did was "malloc" a few times. SDL is its own animal, though, and until I read its code, I need to treat it as if it could ({de|re})allocate at any time. This is unfortunate since it complicates the problem for me, but it's one price I pay for using a powerful abstraction like SDL.

Although I am compiling SDL3 from source for my game, I have not read into the code yet. To get a rough sense for how many allocations are happening and which memory-related functions are actually called, I wrote shims for the memory functions, registered them, and ran my game. One such shim looks like

static int my_malloc_count = 0;
void* my_malloc(size_t s) {
  printf("my_malloc called; c: %d\n", my_malloc_count++);
  return malloc(s);
}

What I see is that a lot of allocation happens initially, both with calloc and malloc. This is SDL initializing its subsystems. On shutdown, a lot of freeing happens, which is cleanup. On the first pass through the game loop, calloc is called hundreds of times, realloc is called maybe about ten times, and malloc is called a few times. realloc is also called, although extremely rarely relative to the other calls. Presumably SDL lazy-initializes some things, and the first pass through the game loop is having SDL finish its initialization as I use various subsystems. On all other iterations through the game loop, I see only the following (keep in mind my game loop is relatively sparse right now).

my_calloc called: c: 1160
my_free called; c: 251
my_free called; c: 252
my_malloc called; c: 297

Debugging shows that SDL_RenderPresent is what I call that in turn calls other functions which are responsible for the (de)allocations above. For what it's worth, I am using the GL rendering backend. I switched to the software rendering backend and experience the same behavior.

In a previous life, the linear allocator I mentioned didn't need to support any efficient freeing because I only ever allocated a fixed number of times to set up buffers I would later reuse. What I see here is that SDL will routinely allocate and free. So should I want to point the memory allocators at my custom memory pool, I need to design the pool and the functions that use it in a way that will efficiently recycle memory.

By the way, I was initially calling SDL_Log in my allocator shims for debugging, but this was a bad idea as SDL_Quit also uses the memory functions after it has turned at least some subsystems off, causing SDL to request re-initialization of the logging subsystem and then hang indefinitely in what appears to be a sleep-and-check loop.

my_free called
^C
Program received signal SIGINT, Interrupt.

(gdb) where
#0 in __GI___clock_nanosleep
#1 in __GI___nanosleep
#2 in SDL_SYS_DelayNS
#3 in SDL_Delay_REAL
#4 in SDL_ShouldInit_REAL
#5 in SDL_InitLog
#6 in SDL_CheckInitLog
#7 in SDL_GetLogPriority_REAL
#8 in SDL_LogMessageV_REAL
#9 in SDL_Log
#10 in my_free
#11 in SDL_free_REAL
#12 in SDL_RemoveHintCallback_REAL
#13 in SDL_QuitLog
#14 in SDL_Quit_REAL
#15 in SDL_Quit
#16 in main
(gdb) quit

I also noticed SDL_aligned_alloc in the SDL documentation. It has a corresponding SDL_aligned_free function as well. The documentation says that SDL_aligned_free must be used to free memory allocated with SDL_aligned_alloc, so I'm not concerned about having to accommodate these (de)allocations in my implementation. But their presence does raise the question of whether they would ever be called in my runtime. If they are called, my hopes of centralizing and controlling memory allocation are dashed. SDL3/SDL_Surface.h defines the surface flag SDL_SURFACE_SIMD_ALIGNED with the documentation

Surface uses pixel memory allocated with SDL_aligned_alloc()

and grep suggests the aligned (de)allocator is generally only used when leveraging SIMD. I also see the aligned memory functions used in implementations for the PS2, PSP, and PS Vita, but these are not targets of mine. I checked whether that surface flag was set on surfaces I am loading, and it is not. So I can probably safely ignore the aligned (de)alloc interface.

So now the question is, how much work is it going to be to implement my own memory pool and allocator to support SDL's behavior? Regardless of the implementation effort, there is also the testing effort, such as unit tests and then fuzzing with asan enabled or something like that.

...

This explanation of slab allocators in older verions of Linux tells me I'm going to be spending a lot of time writing an efficient allocator. The slab allocator is an interesting concept and an appropriate one to use in my case, in my opinion. One feature is that it caches previous allocations of particular sizes, anticipating that such allocations may be requested again. This is exactly the case in my game loop. It also does more advanced things such as adding "coloring" (padding) between slabs to try and keep different allocated regions in different cache lines, so that re-allocating one region does not evict neighboring regions from the cache.

Then there is the backing buddy allocator which handles coarser-grained allocations, typically on the order of pages. I don't need to bother much with this, as for my own pool I'd make a single mmap or malloc call to the system to get the memory for my pool.

I would like to leverage something like an arena allocator for per-frame storage and just clear the storage at the top of each iteration of the game loop, but that SDL_RenderPresent calls malloc and doesn't seem to free that allocation until the next call through tells me I shouldn't free the underlying memory on a frame boundary. The Jai language has a dedicated temporary storage that is recommended to be used in just this way for applications with something like a game loop in them. (Aside: Jai is my favorite language in the C++-contender space. I am just using my favorite dialect of C++, which is C with funtion and operator overloading, for early game development work because I want to reduce the amount of things I need to get better at all at once.) I'd even accept two memory pools, one persistent across the lifetime of the application for things like SDL subsystems, and the other being that frame-temporary storage. Alas, I don't think I can find the lifetime boundaries of allocations made by dependencies so easily and thus don't know when an arena could be reset.

So I wondered, could I use something like jemalloc and just hand over a memory pool to the library and have it allocate from there? The answer is yes, although this is non-trivial, and it of course brings in another sophisticated dependency. So at the expense of making my game harder to trace and debug, I would have more control over the memory (de)allocations. I'm not ready to make that trade.

Lastly, I am toying with the idea of just letting SDL use the system-provided allocators and introducing a simple bump or arena allocator for per-frame storage. The current conclusion of my investigation is to put this off to a later time. The control/complexity tradeoff is not in my favor yet.


2025-187

I implemented basic code hot reloading. Say in your game, you are in the middle of playtesting some long, complicated level, and you find some undesirable behavior. You want to modify your game code to fix the behavior, but then you have to recompile your game and get back to where you found the issue in order to test your change--that or hack up a minimal reproducible example for this one case--to verify if your fix worked.

Another approach is to separate the code for the game logic from the platform code and then dynamically load the game logic into the running platform. This way, you can change the game logic while keeping the game with all of its game state alive and test changes much more quickly. I have to thank Handmade Hero for this idea. I'm currently developing on Ubuntu, so this post will cover how I achieved hot reloading on Linux.

First, a simple demonstration. The video below shows a running game instance where I have moved the player somewhere away from its origin. The tile the player currently occupies is initially red, but then I change the game logic to render it as green, recompile the game logic library, and the game instance detects the library change, reloads the library, and then the tile is rendered as green, all while keeping the game and its state alive.

Until now I kept all the game code in a single file, but I pulled some (rather arbitrary) code out into a separate file to have something to compile into the game logic library while I was standing up hot reloading. Once we do this, we need to add a layer of abstraction between the functions the game logic library provides and the callsites outside the library. Essentially, we replace direct calls into some code with calls through function pointers that we point to implementations of routines we want to call. The implementations exist somewhere in our game logic library which we will now be dynamically loading into our platform.

The gist is like this.

#include <dlfcn.h>

// ...

constexpr int64_t TARGET_FPS = 60;
constexpr int64_t GAME_LIB_CHECK_EVERY_N_FRAMES = TARGET_FPS; // Every second.

typedef int (*FooFn)(Foo*, Bar);

struct stat lib_stat = {0};
const char* lib_pathname = "./path/to/lib.so";
int stat_ret = stat(lib_pathname, &lib_stat);
assert(stat_ret == 0);
uint64_t lib_stat_frame_counter = 0;
void* lib_handle = dlopen(lib_pathname, RTLD_LAZY);
assert(lib_handle);
FooFn foo_fn = (FooFn)dlsym(lib_handle, "foo");
assert(foo_fn);

while (run_game_loop) {
  // ...
  // Maybe reload game logic library.
  // ...

  Foo f{};
  Bar b{};
  foo_fn(&f, b);
}

The typedef establishes an interface between the library to hot reload and the end user. Unlike what I show here, I'd put it in header files corresponding to the library to be reloaded. We then deal with stat(2) and stat(3) which we will be using to get at the file modification timestamp on our game logic library. stat needs to get at inode information and is thus a syscall, so it is "slow," but we also aren't going to be recompiling our library every frame, so we don't need to poll for the modification timestamp on every iteration through the game loop. That's where lib_stat_frame_counter comes in--we'll only poll every so many frames; in this case, once a second.

We next call dlopen which loads a library and gives us back a handle to it. We perform lazy symbol loading with RTLD_LAZY to avoid loading in all symbols our library might contain when I will only use a few of them on the platform side. To load symbols of interest, we call dlsym. We cast its return value to the type of the interface our requested symbol represents.

Not shown in the code above, but in the game loop, every GAME_LIB_CHECK_EVERY_N_FRAMES, we call stat again and check if the modification timestamp on our library has changed. If it has, we dlclose our current handle on our game logic library and then repeat the dlopen and dlsym work to get at the new implementation of our exported routines.

Finally, the game loop calls our routines through our function pointers; shown above is a call through foo_fn.

In a C++ context, the symbol name of the function foo will be mangled. To prevent this, we wrap our declaration of foo in our game logic side inside extern "C" { }. If you wanted to see symbol names in your library on Linux, you can do so with nm. Here are actual (truncated) examples from the test code I hoisted into my game logic library, first mangled and then not.

$ nm -D build/Debug/libgame.so 
000000000000120f T _Z17map_pixel_to_tileP7Tilemapm8Vector2f
000000000000130e T _Z24player_render_tile_underP6PlayerP7TilemapP12SDL_Renderer
00000000000011b9 T _Z5round8Vector2f
$ nm -D build/Debug/libgame.so 
000000000000120f T map_pixel_to_tile
000000000000130e T player_render_tile_under
00000000000011b9 T _Z5round8Vector2f

I don't need round on the platform side, so I leave its symbol name mangled. In this test, I do want map_pixel_to_tile and player_render_tile_under, though, so I needed to not mangle their names.

Code hot-reloading is for development and debugging only. Release builds will link the game library in statically.


2025-186

I implemented simple collision with the environment. This time, it is probably clearer for me to show it in action first.

There are a few new things going on in this video. The first is that the map looks different from last time. I updated the tilemap to add a few things the player can collide with besides the walls along the edges. I am currently exclusively using Aseprite for tilesets and tilemaps. Unlike map editors such as Tiled, Aseprite does not provide a way to annotate tiles in a tilemap with auxiliary information. In Tiled, you can do so, and one such attribute is drawing invisible bounding boxes which can be used to indicate collision areas.

The way I annotated collision in Aseprite was to use an additional tilemap layer. The tileset currently has only 2 tiles, technically 1 tile and the empty tile which occupies an index in the tileset. I set the collision layer to be higher in the layer stack than the map itself, set the opacity of the collision layer to somewhere between 25 and 50 percent, and then I drew over any tile I wanted to add collision to. I used a pink and black checkered pattern for the tile indicating a collision area in hopes that it would stand out from other tiles I may draw on the map.

This approach is not convenient because I need to manage at least 2 tilemaps for every level now. It's bug prone because I might change the underlying map without updating the collision layer or vice versa, or I may change the index values of tiles in the collision map but forget to do so in the game code. It also forces a particular software design on my game, having to parse a unique collision map for every tilemap I load. With all of that said, it's simple, and it works, so I'm going with it for now.

I could design an alternative tilemap representation toward my game, as I do control the binary format of the exported tilemap. One alternative is to set one of the high bits in the tile IDs in the map that logically have collision, essentially merging my collision map into a bitfield in each corresponding tile in the game map. If I recall correctly, Tiled does something similar with bit-flag attributes on tiles. At this point in time, it's not worth me implementing this myself, though.

The ultimate goal is an in-game level editor, but I'm far from that at this time.

The next thing in the video is the red square that seems to be following the player. This is a visual annotation of the tile the player is considered to occupy. I've added it for debugging. Unlike other kinds of software development, it's not so straightforward or even realistic to write traditional, automated unit tests for implementations of various things in games. Often times we get more information more quickly from visual feedback. Testing this way could also explain how so many bugs slip through QA in games all the time.

Computing the tile the player occupies introduces us to what will become a handful of coordinate systems in the game. Currently, for ease of development and debugging, I have only one coordinate system in the game. (0, 0) is at the upper-left corner of the game window, and it is the origin for both the camera and the map. Furthermore, everything is measured in pixels, until now. Determining the tile coordinate from a pixel coordinate, assuming the pixel and tile coordinates share the same origin, is straightforward. In my case, positions support sub-pixel accuracy and so are floating-point numbers.

Vector2d round(Vector2f v) {
  return {
    .x = (int32_t)(v.x + 0.5f),
    .y = (int32_t)(v.y + 0.5f),
  };
}

Vector2f pixel_xy = // ... ;
Vector2d int_pos = round(pixel_xy);
size_t tile_pos_x = int_pos.x / TILE_WIDTH_PX;
size_t tile_pos_y = int_pos.y / TILE_HEIGHT_PX;

I designed the collision map and game map so that they are the same dimensions and so that the tile width and height are the same across maps. I can therefore compute the index into the collision map tiles and check whether a tile has collision area or not. If the player tries to move into a map tile with backing collision area, I disallow the position update. That's what we see with me running into walls in the first video.

Now that we color the tile the player is occupying, we can also see that although the player is currently rendered as an entire tile, the player position is represented as a single point, the upper-leftmost point of the player tile. Because of this, the player can somewhat walk into walls to their right and below them. I'll address this later on by changing the position of the player relative to the player's rendered tile or perhaps changing the player's collision point to a collision area.

Lastly, although it is difficult to tell, we stick to walls somewhat. For example, if I press against a wall on my left side and then attempt to move diagonally up-left or down-left, I won't move at all. This is because my proposed position is up-left or down-left of me which enters the collision area on my left, and my entire position update is disallowed. I'll fix this eventually, likely by checking whether we can move horizontally separately from whether we can move vertically.


2025-185

I implemented basic player movement in the game. First, I created a simple tileset for the player and loaded that into the game as a single SDL texture. Each tile in the tileset is 16x16 pixels, and there are 4 tiles. The tiles are blue squares with a white arrow showing which direction the player is facing, so I have one tile per cardinal direction.

I then created a simple Player struct to hold a pointer to the texture and other supporting information such as the cardinal direction the player is facing and the player's position.

On each iteration through the game loop, I poll for and handle keyboard events. In the event handling logic, I save off the new key states into an instance of my own Buttons struct. I don't modify the player state immediately in the event-handling code. The simple Buttons struct looks as follows.

struct Button {
  bool pressed = false;
};

struct Buttons {
  Button up = {};
  Button down = {};
  Button left = {};
  Button right = {};
};

For now, we just keep track of whether the button is down on the current frame. We don't track whether it was pressed this frame or has been held down for some number of frames. With this setup, if we wanted to understand the input delta from the last frame, we could simply create a second instance of Buttons and store the latest frame's inputs in the second instance at the end of the game loop so that it is available on the next iteration through the loop.

Buttons previous_buttons = {};

while (running) {
  Buttons buttons = {};
  while (SDL_PollEvent(&event)) {
    switch (event.type) {
      case SDL_EVENT_KEY_UP:
        // Fall through.
      case SDL_EVENT_KEY_DOWN: {
          if (event.key.key == SDLK_DOWN) {
            buttons.down.pressed = event.key.type == SDL_EVENT_KEY_DOWN;
          } else if (/* ... */) {
            // ...
          }
       }
        break;
      default:
        break;
    }
  }

  // ...
  previous_buttons = buttons;
}

When it comes time to update the player based on input, I just check whether a certain button is pressed and then update the player's facing direction and a proposed position.

Vector2f proposed_position = player.position;
float dpos = (TARGET_FRAME_TIME_NS / 1000000000.0f) *
             RUNNING_VELOCITY_MS / METERS_PER_PIXEL;

if (buttons.down.pressed) {
  player.facing = Direction::DOWN;
  proposed_position.y += dpos;
} else if (buttons.up.pressed) {
  player.facing = Direction::UP;
  proposed_position.y -= dpos;
}

// The the same for right, left.

player.position = proposed_position;

Here I assume that I always hit my target frame time, and then I multiply that time converted to seconds with my configured running velocity to get a distance the player would have moved in meters. The screen is drawn in pixels, though, so I convert meters to pixels, and that is my delta position (dpos).

I also handle down-up and right-left separately. This way the player can move both vertically and horizontally at the same time. I handle down before up, meaning that if the player presses both down and up at the same time, down takes priority. They will face and move downward. (Notice that I add, not subtract, dpos in the down case--SDL's coordinate system places (0, 0) at the top-left of the rendering space.)

Handling right-left separately means that the player will always prioritize facing a right-left cardinal direction instead of a down-up one if either right or left were pressed on a frame.

Although I simply update the player's position with the proposed position here, I am setting myself up for handling collision. It's possible we should not be able to move where we want to move, so I keep the proposed position separate so I can test it before updating the player state.

Here is a video of what I have so far. The grey border is supposed to be walls, and the white is just a placeholder tile for the ground. You can see the blue player moving around with the arrow updating in the direction the player is facing (or the prioritized direction if the player is moving diagonally). The frame rate is low, but you can tell by the diagonal movement that while the map is based on tiles, the movement is based on pixel positions and is not tile-based. Two outstanding issues are the fact that there is no collision yet and that the player is faster if moving diagonally as opposed to just vertically or horizontally.