Jfgeyelin's dev blog
Currently working on PewPew Live
Sunday, August 16, 2026
Announcing PewPew and PewPew 2 Remasters
Sunday, May 4, 2025
Unexpected Gotchas in Making a Game Deterministic
When aiming for a fully deterministic program, it is common knowledge that you have to deterministically seed your random number generators, be careful about multithreading, and the floating point computations.
In this post I want to highlight a few less commonly mentioned pitfalls I encountered when making my game deterministic.
Foreshadowing
- different compilers and compiler options.
- different standard library implementations (STLs).
- different architectures.
Random numbers
#include <random>
int get_random_number() {
int64_t seed = 9876543210;
std::mt19937 rng(seed); // a generator based on mersenne twister
std::uniform_int_distribution<int> dist(0, 10); // to get integers in [0, 10]
return dist(rng);
}
1. uint_fast32_t
The issue? The std::mt19937 implementation uses uint_fast32_t, which becomes uint32_t or uint64_t, depending on the architecture:
typedef mersenne_twister_engine<uint_fast32_t, 32, 624, 397, 31,
0x9908b0df, 11, 0xffffffff,
7, 0x9d2c5680,
15, 0xefc60000,
18, 1812433253> mt19937;
The fix was replacing the uint_fast32_t with an uint32_t.
In general, if you are targeting 32 bit platforms, beware of uint_fast32_t!
2. Standard Library Distributions
You trace it back to STL's std::uniform_int_distribution. This makes sense! There are multiple ways of generating a uniform distribution from a random number generator.
To be precise, from what I've seen the STLs all use the same algorithm, but the exact implementations can and do vary.
In my case I discovered the problem when porting the game to Linux. I solved the problem by using my own distribution implementation.
Sorting
Order of evaluation of parameters
Say you want to generate a random coordinate in a 10x10 square and you write the following code:
You may get {2,8} on linux, but {8,2} on Windows!
Pointers
Enforcing limits on memory usage
In my game I've restricted the memory consumption of the Lua interpreter: the interpreter returns an error when a user-provided script uses too much memory.
I would like this error to occur deterministically.
Unfortunately that is not possible because the memory usage of the Lua interpreter is not deterministic:
A given structure will not be the same size on every platform (e.g. because the pointer size is different, because the alignment requirements are different, because the STL is different, because of a compiler flag, etc...).
The lesson here is that if you want to deterministically enforces limits on memory consumption, you need to work at a higher level of abstraction than just the allocation size.
Deterministically limiting memory usage is a niche requirement though and I am not aware of any library that does that, let alone any interpreter.
More blog posts on determinism
- Gafferongames [2010]
- Explains why having deterministic floating point calculation are non trivial.
- Floating point determinism (Bruce Dawson) [2013]
- Goes into a lot of details as to why deterministic floating point can be hard, including how having more precision than needed is a problem.
- Riot Games (League of Legends) [2017]
- Covers making an existing game deterministic.
- Touches upon uninitialized variables and non-deterministic pointer values.
- Box2D [2024]
- Explains under which conditions he's been able to get deterministic floating point calculations.
- Explains that Box2D does not support rollbacking. Fortunately we've seen a technique to rollback non-rollbackable libraries.
Friday, February 9, 2024
PewPew Live's look in a nutshell
- Draw everything with lines, including the text and the various icons.
It's a lot of work, but besides looking unique it creates a consistent appearance which is a thing that a lot of indie games struggle with.
The lines are screen-space projected lines with miter joins. - Draw the lines with additive rendering. This means that if a red and green line overlap, the overlap will be yellow.
There are a few things not drawn with additive rendering (like the background of buttons to improve readability), but they are exceptions. - Add bloom.
There's lots of different bloom implementations. Nowadays I use a bloom that is similarly to the one in blender's eevee.
If you see banding, use dithering. - Optional: Add even more post-processing like (very slight) chromatic aberration, lens dirt, scan lines, curved monitor, and vignette.
![]() |
| No post-processing, just lines |
![]() |
| Bloom! Ignore the missing bloom at the top |
![]() |
| All the post-processing turned on |
One final thing that is pretty neat in PPL (which is not often seen in mobile games) is that you can configure every single effect, which allows you to see how the individual effects contribute to the final look:
Saturday, February 27, 2021
A general state rollback technique for C++
Rollback-based multiplayer
Serializing is *the* challenge of rollback-based multiplayer
A solution: Rollback ALL the memory that contains the state of the game
Also, any time a new element is added to the game, it has the possibility of affecting your game state and making it so rollbacks could cause a desync. There’s no getting around that. When Keits comes to me and says "hey, I want these projectiles to be random and sometimes four of them come out and they loop around in odd patterns," these are now elements I have to make sure can be rolled back, including adding new elements to the code. I can train programmers to make sure their variables are registered with our rollback system, but it’s unavoidably more work.
Implementation details
Efficient snapshotting
I call an allocator that allows taking snapshot and restoring them a SnapshotableAllocator.
For the record, my implementation of a SnapshotableAllocator in use in PewPew Live is available here.
Coexisting with the rest of the code
In order to tell the compiler that a section of the code needs to use a different allocator, I find that using RAII is convenient. When I want to wrap a section of the code with a different allocator, I create a ScopedAllocatorUsage object which changes the thread_local pointer in its constructor. When the ScopedAllocatorUsage goes out of scope, its destructor restores the thread_local pointer to the original value.
Example
SnapshotableAllocatorImpl allocator(8);
std::unique_ptr<std::string> s;
// Initialize the string with "hello"
{
ScopedAllocatorUsage sau(&allocator);
s = std::make_unique<std::string>();
*s = "hello";
}
// Take a snapshot
auto snapshot = allocator.TakeSnapshot();
// Change the value of the string to "world"
{
ScopedAllocatorUsage sau(&allocator);
*s = "world ";
}
// Verify the value of the string before and after the rollback.
assert(*s == "world ");
allocator.LoadMemoryFromSnapshot(*snapshot);
assert(*s == "hello");
// Free the memory
{
ScopedAllocatorUsage sau(&allocator);
s.reset();
}
Memory usage
Those replays are used to verify the validity of scores, but they are also used by the players to watch and learn from the best players.
Those replay only allow you to simulate the game forward, but we can re-use the rollback system to provide rewind capabilities.
In PewPew Live, rewind is implemented by taking a snapshot anytime you've simulated an additional 0.25% of the replay. This still means that by the end of the replay, 400 snapshots will be kept in memory.
Persisting the snapshots (disclaimer: I haven't actually tried this.)
void* ptr = mmap((void*)0x800000000,
4096 * 32,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE,
-1, 0);
Pitfalls
Obviously this example was not made up out of thin air, it actually happened to me.
Conclusion
Monday, October 19, 2020
Abusing constexpr to implement gettext's _() macro with zero overhead.
You usually use the C library gettext with a _() macro that returns a localized version of the text passed in parameter.
For example:
will result in a runtime search of the string "Hello", and _() eventually returns the localized version of the string. I assume the search is implemented using some sort of binary search, so it's probably extremely fast.
It's fast, but we can get rid of the search altogether using constexpr functions!
The trick is to use a constexpr function that at compile time gets the ID of the given string. Then it's a matter of doing an array lookup in the table of strings for the desired locale. The array lookup is still unavoidable because we don't know at compile time which locale the app will use.
In my case, _() is implemented using I18NStringIndex which looks like this:
constexpr bool I18N_EQ(char const * a, char const * b) { return std::string_view(a)==b; } constexpr int I18NStringIndex(const char* s) { if (I18N_EQ(s, "off")) { return 0; } if (I18N_EQ(s, "on")) { return 1; } if (I18N_EQ(s, "ok")) { return 2; } if (I18N_EQ(s, "Quick pause")) { return 3; } if (I18N_EQ(s, "Quick pause %s")) { return 4; } if (I18N_EQ(s, "Continue")) { return 5; } if (I18N_EQ(s, "Exit")) { return 6; } if (I18N_EQ(s, "Pause")) { return 7; } if (I18N_EQ(s, "Restart")) { return 8; } if (I18N_EQ(s, "Game Over")) { return 9; } if (I18N_EQ(s, "Play again")) { return 10; } if (I18N_EQ(s, "Cancel")) { return 11; } if (I18N_EQ(s, "Game is full")) { return 12; } if (I18N_EQ(s, "Local lobby")) { return 13; } if (I18N_EQ(s, "Ready")) { return 14; } if (I18N_EQ(s, "Waiting for other players...")) { return 15; } etc...
It is brutal, but the compiler still manages to transform I18NStringIndex("Game Over") to 9, and without noticeably slowing down the compilation!
It is used together with static arrays of strings, such as this one:
std::array<const char*, 138> kStrings_FR = { "off", "on", "ok", "Mini-pause", "Mini-pause %s", "Continuer", "Quitter", "Pause", "Recommencer", "Game Over", "Rejouer", "Annuler", "Partie déjà complète", "Partie locale", "Je suis prêt !", "En attente des autres joueurs...", etc...
I18NStringIndex is generated by going over the code with a regex looking for patterns like _(".*"), and kStrings_LANG is generated using the .po files the translators filled.
One danger of this technique is that if for whatever reason the compiler can't run I18NStringIndex at compile time, then you pay a heavy price at runtime. Fortunately in C++20 you can specify the function to be consteval to make sure it does not happen.
Another downside of this technique is that whenever I18NStringIndex is re-generated, all the files containing _() need to be recompiled.
It's not a problem for me because I re-generate that function only when I'm about to send the .po files to the translators, which does not happen often.
Thursday, September 10, 2020
Exponential = dangerous
Note: This post was initially written in 2013, right after the release of Pacifism. For some reason I never published it. It's still relevant today, so here it is seven years later.
![]() |
| x=number of enemies killed. f(x)=the bonus for x enemies killed. |
I made sure that the exponential grew slowly: even if a player managed to be twice as good as me and destroy twice as many enemies as me (200), they would only make around 55000 points, which is high but not absurdly so.
Once I was pleased with the feeling of the game, I released it and waited for the scores to come in.
As the high scores started coming in, I got to see the replays of people playing the game mode for the very first time and figuring out how to play. That was really cool.
Quickly though, you could see in the replays the players getting better and better.
And eventually, some players realized that by using the fastest ship, you could simply circle around the level dodging the bombs for a minute with the enemies never catching up with you. When they did blow a bomb, several hundreds enemies would explode resulting in bonuses of hundred of millions of points.
The lessons I learned:
- Use 64 bit integers for scores.
- Put a cap to exponential scores, or at least be very careful. You never know what the player are going to.
- Be ready to reset the scores and prevent people with old version of the game to send "bad" scores.
- Replays are so useful, it's ridiculous. Without them, I would have had no idea what the players were doing, and I probably would have assumed they were cheating and sending fake scores.
Saturday, June 20, 2020
PewPew Live released!
Let's go over what's new compared to PewPew 2:
- LAN Multiplayer
Multiplayer is the main difference with PewPew 2, and the reason PewPew Live exists in the first place. The initial plan was to support online multiplayer, but this proved to be very complex. For now, the game will only support LAN.
- Support for custom levels
Custom levels are another big new thing. Allowing users to create levels will increase the replayability of the game.I had to choose where on the spectrum would the creation be: should the players be given building blocks that they can remix (low barrier of entry, low variety of levels), or should the players have to directly write code (high barrier of entry, high variety of levels) like I do when creating levels? I chose the latter because that's what I would have liked as a player, it introduces people to programming and tools that lower the barrier of entry can always be built on top.
- New game modes
- Unlock-able ships, trails, bullets
- Improved graphics
- Support for high-refresh-rate screens
PewPew Live supports refresh rates higher than 60 fps because the rendering system can interpolate between game states.
- Abstraction of the rendering API
PewPew 2 was directly using OpenGL. PewPew Live has an abstraction over the rendering API that improves portability. For example, using directly Vulcan should be possible.Shader-based renderingNow that shaders are available on almost all mobile devices, PewPew Live can depend on them for animations.
- Resizable window support
The UI now dynamically adapts to the size of the window.
- Deterministic replays
I had retrofitted determinism in PewPew 2, but there were some bugs that I never solved. Now the game is designed from the start to be deterministic. This should make it harder to cheat.
- An overall better game architecture of the game
It gives me more flexibility to do weird things. E.g. you can run multiple instances of the game simultaneously.
- Editor
In PewPew 1 I was editing SVGs in Adobe Illustrator and exporting them to PewPew. This time around I invested time to make a tailored editor for graphics and level. This should lead to higher quality models and levels.
- Better collision detection for walls
In PewPew, a single wall was a quadrilateral with a width. This made editing levels painful, and if the wall was too thin compared to the velocity of an entity, collisions would be missed. This time around, the walls are actual lines and collisions can't be missed.
- Sound synthesizing
In PewPew sound was made of .wav files. In PewPew Live, the sounds are synthesized at runtime. This allows more flexibility in the sounds and takes less space. It does limit the kind of sound effects the game can have, but the upside is that it makes the sound effects more consistent.
Announcing PewPew and PewPew 2 Remasters
PP (PewPew) and PP2 (PewPew 2) haven't been compatible with modern devices for a couple of years now as I focused on PPL (PewPew Live), ...
-
I wanted to write this post for a while. It describes a C++ technique to implement rollback in the context of multiplayer games that I feel ...
-
When aiming for a fully deterministic program, it is common knowledge that you have to deterministically seed your random number generators,...
-
PewPew Live was released today on Android ! Let's go over what's new compared to PewPew 2: LAN Multiplayer Multiplayer is the main d...






