It has been a hot minute since my last update, hasn't it?
In my defense, I have been neck deep in tedious engine work.
We'll cover that briefly, but let's also talk about something more GIF-friendly: time traveling!
Porting to SDL
Macroquad is a minimalistic Rust library that will open a window for you, draw images onto the screen, check if keys are pressed, etc. It's neat and I recommend it as a no-fuss way to get started.
But I've been running into various issues1 so in the last 2 months I ported the whole game to SDL32 3 (and WGPU4 for graphics rendering).
The actual porting process is not particularly interesting so instead here is a Chaos Mode that I added to stress-test performance:
Wild screenshake aside, it's actually kinda fun as a challenge mode!
Performing Multithreading of Falling Sand
I got nerd-sniped into multithreading the falling-sand simulation (kinda like Noita5). The game runs about 2X faster in chaotic scenarios now!
Actually unlike Noita, which infamously only uses 4 CPU cores, Signs of Danger will use all your cores!6
Wishlist Signs of Danger now for multicore falling sand goodness!
I also separated the game simulation itself from the rendering:
Concretely, this means:
- More headroom (those blue bits) to play with completely game-breaking builds for those with good pcs. 7
- Lower system requirements for those with toasters - my Steam Deck now runs Signs of Danger at a solid 90FPS!
- More responsive rendering on high refresh rate monitors. 8
- Even when your buddy is lagging everything by blowing up the whole world, you can still use the Kick Player menu. 9
Now you might be wondering, why have those blue free time bits at all? Why not run as fast as possible?
Well, because if your game is optimized, you get this - and I need to emphasize that this is not sped-up via editing:
Still, there's always room for more improvement - my goal is to average 1ms of simulation time - but this will do for now.
Rewind, Play Back, Re-Play
Implementing online multiplayer via deterministic-lockstep-netcode-with-rollback-and-replay is a real pain in the butt.
But once you've done it, you now have a way to store old copies of the whole game world, so you can do wild things:
I built this as a debug tool so it's fairly naive,10 but now I'm thinking: I have to expose this to players as a post-death instant replay.
And I'm wondering whether I can actually pull off some kind of time-related Sign; someone suggested a "rewind physics" Sign which would be awesome.
Playable web build
Anyway that's all I have time to write for now, but you can try the Macroquad-free web build here. If it doesn't work, please let me know!
(This web build is still single-threaded, but it might still perform better than before due to a lot of other optimizations I did?11 I dunno, you tell me.)
Known issue: gamepad support is broken on web - can't shoot and X & Y buttons (on xbox gamepads) are swapped. Keyboard and mouse should work fine though!
You might also spot some other changes, but I'll leave those for another update!
If you are a Rust person, you may be surprised I chose SDL3 instead of winit, which is the "rewrite-it-in-Rust" library for windowing and input handling. But it's pretty simple:
- SDL3 is the industry-standard library.
- Winit has lots of breaking changes; SDL3 doesn't.
- SDL3 exposes neat features that Rust libraries like gilrs don't (e.g. changing color of gamepad lights).
I only use SDL3 for the desktop build: SDL3 on the web requires compiling to Rust's Emscripten target (which is not really maintained and is incompatible with some other libraries I use that require Rust's wasm32-unknown-unknown target - annoying). So for the web build, I use wasm-bindgen to interface with the browser directly.
On the other hand, Winit does support Web out of the box, so it'd be easier to get started with.
WGPU is an abstraction layer over WebGPU, a newish browser-compatible graphics API. But I actually still target WebGL2 because WebGPU hasn't rolled out to all the major browsers yet and I'm an inclusive kind of guy.
That does mean I can't use goodies like Compute Shaders or other graphics features from the last decade, but eventually I'll stop publishing playable web builds.
I'm not gonna make an exhaustive list because it is not GIF-friendly, but some of the big ones were avoiding reallocating render textures when zooming in and out (particularly bad as camera zooms in/out during co-op), only sending atom-chunks that are dirty over to the GPU and using a texture atlas for rigid bodies (also culling bodies that are out of view).
At least on Desktop. Web is still single-threaded: Rayon doesn't officially support multithreading on the web.
Well, it's actually a lot more complicated than what Noita sketches if you have rollback multiplayer. I hope to make a video about it at some point, but the tricky part is that calculating the next game state must be 100% deterministic, you gotta keep old worlds around to roll back to, AND it all has to be really performant.
Multiple threads love to interfere with each other and break determinism, so I have a tricky lock-free design involving atomics and copy-on-write that makes it all work.
More responsive in that you see the updated world sooner. And in future it ought to be smoother too, if I can work out how to interpolate world states or simulate a subset of the world forwards in time on the render thread.
Noita has a problem where some combinations of spells will grind the game to a halt. Moving the simulation to a background thread solves that entirely!
Most CPUs from the last 15 years have at least 4 cores but not all CPUs run at high clock speeds! So development work (and nondeterminism multiplayer netcode desync hazards) aside, it's beneficial to multithread as much as possible.
Like storing all historical game states uncompressed in memory. 30 seconds consumes about 8GB and that ain't gonna fly when RAM has 4x'd in cost over the last 6 months.
Between compression and storing only every 5-10 gamestates, I expect I can cut that down to a gig or less.
See my reddit thread for some of the issues, but the straw that broke the camel's back for me is that I wanted finer-grained control over my rendering: Macroquad has a very convenient rendering API, but that comes at the cost of flexibility, and for the last 6 months I've found myself fighting that more and more.
