Coming back to a large project after a break (Battlebeyz part 6)
The return started in late December 2025; the commits it describes run through April 2026 and are dated inline. Last updated 2026-09-17.
The last commit before the break is dated February 13, 2025, and it’s called “Pre-battle state checkpoint.” I know exactly what that means now. At the time I came back I had no idea, and that is what this post is about.
Between February and December I had a semester, an internship, and the small matter of nuking Windows for Omarchy in October. That last one is the reason the break ended the way it did. Battlebeyz was a Visual Studio solution with a vcpkg manifest and a lib/ folder containing every dependency checked in as source, 167 thousand lines of it. Part 2 of this series is me being delighted about switching to Visual Studio. Eight months later I was on a machine that couldn’t run it.

Hieroglyphics
Here is what reading your own ten-month-old code is like. Every name is familiar and none of them mean anything. There is a length() and a lengthTyped() and I could not remember which one was the safe one. There are four pairs of UI helper files. The Doxygen output in the repo describes classes that no longer exist. I opened PreBattleState.cpp, the last thing I’d touched, and could not tell whether what was there was a bug or the point where I’d stopped.
What actually helped, in order: the commit messages, which were more candid than any comment (December 24, 2024: “Fix shader problem FINALLY ahhhhhhhhhh”, followed forty minutes later by “Never mind, still shader problems”). The Units.txt file from Part 3 with the sane ranges for every physical quantity, which let me tell a wrong number from a right one. And the dependency graph generator I’d written in January 2025 for no particular reason, which drew me a map of sixty headers when I’d forgotten there were sixty headers.
What did not help: the code’s own documentation, because it was describing a September 2024 design. I deleted the Doxygen folder on January 1, 2026, and did not regret it.
Step one: make it build
The first commit back landed on New Year’s Day 2026 and removed 167,433 lines. The vendored lib/ folder went, and CMake grew a FetchContent block for each dependency: GLFW, glm, ImGui, nlohmann json, stb, tinyobjloader, FreeType, GLEW. Clone, configure, and it downloads and builds what it needs. GLFW got told about Wayland and X11. The README was rewritten to match, since the old one described a Visual Studio workflow that no longer existed.
That commit also touched Units.h for nine hundred lines, which sounds dramatic and was clang-format. The interesting compiler problems came later.
Step two: the things I’d been avoiding
Once it ran, two problems that had been there all along became obvious on a different machine.
The first was the loading screen. Parsing the Beyblade .obj files and decoding textures happened on the main thread, so the window froze for a couple of seconds on startup. On February 25, 2026 that became a JobSystem, a plain worker pool with no dependency graph, and a GLUploadQueue. The split is the important part: workers parse files into plain MeshData and ImageData structs, and only the main thread, the one that owns the GL context, turns those into buffers and textures. Every GL call still happens on one thread; the CPU work just doesn’t. The same commit finally wired up PreBattleState as the launch screen, closing the checkpoint from a year earlier.
The second was the physics timestep. PhysicsWorld::update had always taken the frame’s delta time, which meant the simulation depended on the frame rate. The commit message from February says it plainly: behaviour no longer varies with frame rate. The fix is the standard one:
int PhysicsWorld::advance(float frameDelta) {
// A long stall (window drag, a slow load) must not turn into hundreds of steps
accumulatorNs += std::llround(std::min(frameDelta, MAX_FRAME_DELTA) * 1e9);
int steps = 0;
while (accumulatorNs >= FIXED_DT_NS && steps < MAX_STEPS_PER_FRAME) {
update(FIXED_DT); // always 1/240 s
accumulatorNs -= FIXED_DT_NS;
++steps;
}
if (steps == MAX_STEPS_PER_FRAME) accumulatorNs = 0; // drop the debt, never spiral
interpolationAlpha = float(accumulatorNs) / float(FIXED_DT_NS);
return steps;
}
Physics runs at a fixed 240 Hz no matter what the display does, the leftover fraction of a step becomes an interpolation factor for rendering so motion stays smooth, and two caps keep a stall from turning into a catch-up spiral. The accumulator is in integer nanoseconds so it never drifts. This is in every game programming book and I’d skipped it for a year and a half because the frame rate on one Windows laptop was stable enough to hide the problem.
Step three: the boring stuff
On April 10, 2026 the warnings went on, -Wall -Wextra on GCC and clang and /W4 on MSVC, and the project got a test suite. Thirty-one doctest cases: eleven for the units system, eight for the Beyblade body, seven for the physics world, five for mesh parsing. The rule for what gets tested is “anything that doesn’t need a GL context,” which turns out to be most of the physics. The friction precedence bug from Part 3 would have been a one-line test. So would the impact function’s habit of setting velocity to the delta instead of adding it, which I noticed writing Part 3 and which no test pins down yet.
The tests are the answer to the hieroglyphics. The thing I’d lost over ten months wasn’t the code, it was the invariants: what a sane spin looks like, which length function keeps its units, what a collision is supposed to conserve. Tests are where those live now, in a form the next version of me can run instead of re-derive.
Step four: other people’s compilers
The last week of the return, April 14 to 18, was making it build somewhere other than my desk.
Windows first. MessageType::ERROR had been fine for a year and a half, until a header that pulled in windows.h arrived and ERROR became a macro. The enumerators are Error and Warning now. tinyobjloader had to be pinned to a specific tag because its main branch had changed an API.
Then macOS, which meant clang for the first time. clang refused two of my user-defined literal suffixes because they were reserved identifiers, a story that belongs to Part 5. It also wanted the constexpr literal definitions moved below the template bodies they depend on. The linker wanted the AGL framework, which Apple removed. And a couple of dozen files were missing standard includes, <string>, <vector>, <cstdint>, that MSVC’s headers had been pulling in transitively for me the whole time.
None of these were bugs in the game. All of them were things Visual Studio had been quietly tolerating, and the project is better for having met a second and third compiler. Fourteen months after the last old commit, Battlebeyz builds on three platforms from a single CMake file, downloads its own dependencies, runs its physics at a fixed rate, and has tests. It still can’t do opposite-spin collisions.