This is the second of three catch-up posts. The first one wrapped up the asset system. This one is about the platform layer, the part of the engine that talks to the operating system, which I rewrote almost completely this month.


How It Started

It started small. I wanted the debug and profiler information in a separate window, so that it wouldn’t cover the editor. At the same time I was porting the engine to Linux, using GLFW for windows and input. A while ago I already used GLFW in an earlier refactor, but the Windows build still ran on the original win32 layer that came from Handmade Hero, and the GLFW layer was mostly a copy of it with Linux bits added.

Before going further I sat down and reviewed the boundary between the engine and the platform, and it was worse than I thought:

  • The engine didn’t always go through the platform API. Some code used the C standard library directly, with a small shim to make fopen_s compile outside of Windows.
  • The engine passed Windows names to the platform, like arkham.dll and paths with backslashes, and the GLFW layer rewrote them.
  • glfw_engine.cpp mixed GLFW code with #if PLATFORM_WINDOWS blocks of raw OS code.
  • The two platform layers duplicated a lot of code.
  • engine_platform.h, which is supposed to be the boundary, held a lot more than the boundary.

Patching each of these one by one would have left the same structure with fewer problems. I decided to restructure it instead.


The New Structure

The rule is simple: GLFW is the window and input layer on every OS, and miniaudio handles sound. Everything that GLFW and miniaudio don’t cover goes into a small OS layer, one file per OS.

That OS layer is described by a single header, platform_os.h. It declares the functions that every OS file has to implement: memory allocation, sleeping, files, directories, copying files, write times, loading libraries, threads, semaphores, and running external commands. linux_os.cpp and win32_os.cpp implement all of them, and they are around six hundred lines each. Nothing above that layer touches the OS directly. Paths are UTF-8 and always use /, and the Windows file converts them when it has to.

On top of that sits the shared code: the GLFW window, input and main loop, the implementation of the platform API that the engine sees, the miniaudio output, and the OpenGL setup. That part is the same on every OS.

And on top of that is engine_platform.h, which now holds only the boundary between the platform and the engine. The engine does all file access through Platform.*, asks for files by type instead of by path, and never knows which OS it’s running on.

Once the GLFW layer was working on both systems, I deleted the old win32 layer. That one commit removed a little over four thousand lines.


Building on Two Systems

The engine now builds and runs on Windows and on Linux with X11. All third-party libraries (GLFW, GLEW, Nuklear, miniaudio) are included in the repository as source and built by the build scripts the first time, so there is nothing to install separately.

On Windows, build.bat finds the Visual Studio environment by itself, so it works from a normal terminal. It uses clang-cl when it is installed and falls back to cl otherwise, and both compilers have to build cleanly in debug and release. I check all four combinations before every commit, which caught a few things that only one compiler complained about.

On Linux, build.sh uses clang or gcc. If X11 development headers are missing, the script prints the install command for your package manager instead of failing with a wall of compiler errors.

One problem showed up only after everything else worked: hot reloading the engine stopped working on Windows when the editor was open. The build script rebuilt nuklear.dll every time, but the running editor had it loaded, so the link failed and the script stopped before it got to the engine. Now Nuklear is only built when it’s missing, and the platform executable is skipped when it’s locked, so rebuilding with the editor open reloads the engine as it should.


The Debug Window

The second window turned out to be one of the most useful changes.

The debug window has its own GLFW window, its own Nuklear state, and its own OpenGL context that shares objects with the main one, so both can use the same font atlas. Events go to the window they came from, and only the main window drives the engine. With a second monitor, the debug window goes fullscreen there. With one monitor it opens as a normal window.

I also dropped the old debug trees from the Handmade Hero days. The window now shows the frame rate in the top left, an always open profiler panel, and on the right a panel for the running project (more on that in the next post) over a log.

The log is new. LOG, LOG_WARNING and LOG_ERROR work like printf, are available to the platform, the engine and games, and compile to nothing in release builds. Every line goes into a ring buffer that the debug window shows, newest first, with warnings in yellow and errors in red, and is also appended to build/kaban.log. I went through the engine and added log lines to startup, failures, file writes, asset loading and builds. Nothing logs per frame, and I want to keep it that way, because a log that scrolls by sixty times per second is a log that nobody reads.


Small Things

A few smaller changes came along with the rewrite.

Both windows are borderless fullscreen now instead of exclusive fullscreen, so switching between them and other programs doesn’t make the monitor go black. On Windows there was still a black blink when clicking between windows, which went away only after making the window one pixel taller than the monitor. I don’t love that fix, but it works on my machine, and I have not found a cleaner one.

Alt+Enter toggles fullscreen for whichever window received the key. The UI toggle moved from E to Alt+E, because a plain letter key that hides the UI is not a good idea when the UI has text boxes. Alt+M mutes the audio.

On Linux I use i3, which ignores window placement requests. Both windows now set X11 class and instance hints, so a window manager rule can send the editor and the debug window to the right monitors.


What’s Next

The platform layer is in a state where I’m not afraid to touch it, which wasn’t true before. The boundaries are clear, both systems build from the same code, and the debug window gives me a much better view of what’s going on.

All of this was groundwork for the change I actually wanted, which is moving games out of the engine. That’s the topic of the last post.