fadEout
Intro
Hi! Nice of you to join me!
I'll cut to the chase: I read Dominic Szablewski's post about rewriting wipEout and wanted to see if I, very much not a gamedev type programmer, could do that.
This comes (freezing cold) on the heels of me reading Ready Player One (and Racing The Beam), and deciding to write an Atari 2600 game for the lulz. I went for a demake of Nidhogg, and got as far as implementing an engine with player inertia, gravity and collision/hit detection (yes, I had good reasons to not just use the collision detection bit built into the Atari's display hardware), and that was fun.
I particularly enjoyed the puzzle of building a "kernel" (essentially, the rendering routine for a single line) that didn't use the HSYNC interrupt, but instead was meticulously cycle-counted so that it always took the exact same number of cycles to execute that the beam does to scan a single line.
To do this, you have to make sure that all possible paths through your code take the same number of CPU cycles to execute. This is tricky, because in general, each line of each frame of a game is not the same as every other. I'd never even thought about balancing both sides of an if before that, and only rarely about CPU registers, but this project forced me to, and again, that was fun.
Not that any of the two previous paragraphs is particularly important for this project, but I figure it gives you a sort of idea of what kind of nerd I am, and of how likely I am to finish this project. If you want to download a rewrite of wipEout for modern OSs, don't wait on me. Download Dominic's rewrite instead.
With that out of the way, on to the most important part: A catchy name. I landed on fadEout, because fades are a type of scene transition, just like wipes. Good enough for a project that may never see any sort of release.
Audience
Who's this series for? Well, if you made it through that introduction without your eyes glazing over (too much), you just might be the target audience! You will have a much easier time following along if you know about a few things going in:
- The C++ programming language.
No need to be a template-slinging rock star programmer, but I'm not going to stop and explain what a pointer (or an
std::vector) is. I will link to cppreference.com frequently, though! - The CMake build system (spoilers).
- The SDL development library. Although I'm not, so maybe we'll both learn together!
- Video Games.
But first and foremost I'm writing this for myself, to be honest. Writing about my thoughts, plans, successes and failures and editing them helps me in a few different ways:
Firstly, breaking down a problem into chunks means that I can more easily pick up one of those chunks to tackle later. As a bonus, it ideally makes it easier to estimate how much time I'll need too. Not that I'm on any deadlines, but I'm sure this skill transfers to situations where I am.
Secondly, writing for an audience, even imagined, that is not just myself, hones my "tech writing" voice. I want that voice nice and sharp, on several axes:
-
Effectiveness:
If I can't teach you, esteemed reader1, anything by writing this, then why do it in the first place?
-
Efficiency:
Effectiveness isn't everything. If it takes me a thousand words to explain what
int add(int a, int b)does, everyone involved is going to get bored before anything interesting happens. This is where editing comes in, and I hope I can find a balance between lore-dumping and dry facts. Which leads me to... -
Personality:
I'm not a robot, but a human being with opinions, thoughts, interests and (gasp!) flaws. So while I maybe could adopt a totally objective style of writing, I don't think that would be very fun for you or me. Instead, I'll sprinkle in anecdotes, pop culture references and puns, as well as explaining why I do some things differently than others. In that, I strive to be respectful and empathetic to others. If you see me calling anyone misguided, an idiot, or worse on here, please do call me out on that (you'll find ways to contact me in the footer of these pages).
Thirdly, and lastly, I've alluded to the fact that I tend to pick up and not finish projects. I am shamelessly using you, the reader, to provide peer pressure to actually get fadEout done. Thank you!
Motivation
So, what do I bring to the table that Dominic/phoboslab didn't?
Well, for one, I'm not a game dev. Like, at all. My day job is programming realtime robot control systems, which have some overlap with games in that there are deadlines you really don't want to miss.
But also, the flavor of coding we use at work is much more... enterprise-y than what I've seen of game devs so far.
No RobotFactoryFactoryInterfaces, but we do value a few things more than the (admittedly few) game code bases I've seen:
-
Descriptive type and variable names
Yes, it will take you longer to type
input_file_bufferthanbuf(or, god forbid,b), but it's 2025, and computers are very good at saving you the work of actually typing (no, not like that). You'll thank yourself for thinking of a descriptive name a week from now. -
Maintainability
This is a bit diffuse, but one example is guarding your interface boundaries. If callers can reach into the guts of what you're building too much, then you've built yourself a prison. Any behavior of your system can and will become its API (cf. Hyrum's Law and of course, a relevant XKCD).
-
Documentation
If you're not the only one, or not even the only team, working on something, it really does pay off to document things well. You can only figure out if something's working correctly after you understand how it is supposed to work. This of course goes hand in hand with...
-
Testability
Let's be real: Documentation is probably going to be incomplete or out of date at least some of the time. Tests, on the other hand, are a cold, hard truth2. So it makes sense to design code in a way that makes testing easy.
For example, while the Java
FactoryFactoryImpl(pejorative) is a meme for a reason, there is something to be said in favor of dependency injection.Being able to test parts of your system in isolation, while mocking the things they interact with, is a powerful technique to avoid (and discover/reproduce) bugs.
I'll be honest: I've never gone full TDD (Test-Driven Development), but comprehensive test suites are extremely useful and, on a personal note, very satisfying to see turn from red to green.
You'll note that I haven't mentioned performance. That's because despite working on realtime software, my work has never been (badly) resource constrained yet. Now's a great time to remind you that realtime does not mean fast3. That said, our code does go fast. Even our slowest robots run at a frequency that's more than the refresh rate of almost any gamer's monitor.
No, I've just been lucky enough to work on pretty beefy machines so far. And also, importantly, robot control usually isn't bottlenecked in the same ways that a video game would be. Memory bandwidth isn't a huge issue, for example, because the amounts of data we shuffle around are miniscule when compared to even a modestly-sized frame buffer.
And yet, I imagine that for all of the dependency injection, virtual function calls and not packing structs for maximum efficiency, the way I've gotten used to coding at work doesn't lead to horribly slow performance. Part of the reason I'm embarking on this project is getting a chance to test that belief.
Worst case, I'm wrong about performance, and I gain a visceral appreciation for the kinds of performance optimizations that game devs regularly preach about. And maybe I4 learn some humility.
Goals
So, what are my goals for this? Of course, the major goal is pretty obvious:
Play Wipeout (1995) on my (linux) laptop
Other platforms are a stretch goal. One that's really tickling me is the playdate. Maybe there is a way to crunch things down enough for that to work. Aside from that, a VR headset would be interesting (and I have one to test).
But let's not get ahead of ourselves. Those are goals for after I have a working build. What about before that? If I've learned anything programming things for more than a decade, it's that many things become easier if you break the work down into manageable chunks. Not least of those things is keeping up my motivation, which is important for a project that I'm not getting paid for and that needs to compete with friends, family, sports, other hobbies and doomscrolling for my attention.
With that in mind, I'm setting myself a few intermediate goals:
-
This is making a pretty big assumption, but I'll probably want an Arena Allocator or something, to cut down on heap allocations. I'll explain why I am making that assumpion in some more detail in the actual post, but basically, I don't want my game to stop and ask the operating system for more memory, ideally ever. No need to implement it right away, but I should define an API to use in the parts that follow.
The API is going to be pretty simple:
void* Alloc(size)/bool Free(void* ptr)Of course, I don't want to throw around raw pointers, so I'll have to build a few helpers that basically wrap my allocator so that I can work with smart pointers instead.
-
Wipeout uses a few different file formats. Some are bespoke, others are commercial, and loading them is going to be my first priority.
-
View Assets
Having the assets in RAM is all well and good, but I want to see them to check if they actually make sense. I'm pretty sure it's a good idea to experiment with how to render these to a screen before tackling the next step, which is...
-
Full Render Pipeline
Having rendered the assets (textures, models, fonts?) in isolation, I can move on to assembling them. Models with textures! And shading! And UI elements on top! If nothing else, reaching this point will let me make neat little dioramas of wipEout tracks.
-
Game Logic
All the steps so far have been very graphics focused. However, contrary to what some Gamerz(tm) would have you believe, graphics alone do not a video game make. So next up is the game logic. Thinking about it, this will probably break apart into a few different categories, like
- Overall UI state machine (start screen vs. menus vs. gameplay)
- Race logic (including physics and controls)
- Enemy AI
-
Final Assembly / Packaging
All that should be left to do, at this point, is figure out how to package the rewritten game such that it runs on computers other than my own. How hard could that be, huh?
I'll update the list above with links as I go along. Now, it's almost time to sign off, except that I forgot about something!
Episode 0: Build System and Toolchain
Any project that's larger than "Hello World" needs some sort of build system. In my case, that build system is going to be CMake. Not the most lightweight or flashy choice, but one that I'm somewhat familiar with from my ROS days... although it's probably gained a fair few features since then.
cmake_minimum_required(VERSION 3.23)
project(fadEout LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
And there's the next choice already: I'm using C++20 at least. There's just so many things in (post-)modern C++ that are really really nice to have.
While I'm at it, I'll build with the newest version of LLVM I can reasonably use.
Mostly because I want to give RTsan a whirl.
I already know that fadEout won't be hard realtime (because GPU drivers generally aren't), but it's an interesting challenge that I mostly know how to deal with, and it won't hurt5.
Also, it would be borderline silly not to use clang-tidy, both for style and footgun avoidance. So:
find_program(CLANG_TIDY_EXE NAMES "clang-tidy")
set(CLANG_TIDY_COMMAND "${CLANG_TIDY_EXE}" "-checks=modernize-*,nonportable-include-path" "--fix" "--fix-notes")
set_property(DIRECTORY PROPERTY CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND}")
These are fairly radical settings for clang-tid, but I figure it's an easy way to keep my finger on the pulse of what contemporary C++ can/should look like.
Finally, I mentioned tests above, so let's pull in googletest:
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
Did you know you can just pull in a package from GitHub in CMake like that? I didn't!
That's it for now, join me next time when it's time to allocate some memory!