Game dev in Rust, a year later

Well, here we are, a year after I wrote "Game dev in Rust, some notes on the mess." Things haven't changed much.

I'm still using the Rend3/WGPU/Vulkan graphics stack. It works reasonably well at this point.

Notes:

  • Several major game projects abandoned Rust in 2024. Some found ownership too restrictive. Some complained about compile times.
  • arewegameyet.rs stopped updating last July.
  • Rend3 was abandoned. Now I have to maintain that myself, which I do as rend3-hp. I now have it up to current Wgpu, Winit, Egui, etc. versions, and finally found and fixed the race condition which kept causing crashes. I've been able to run for over 40 hours without a panic. Things are looking up. If you want to try it, use branch "wgpu23safe". I'd appreciate someone trying that on MacOS, which I don't use. Build it and run rend3-examples. If anybody is really interested, I could release it to crates.io in a month or two. Right now, it still has patches to library crates, which crates.io does not allow.
  • The graphics stack is still too slow if you put in enough content to keep the GPU busy. Runs out of CPU time at around 25% GPU load with a NVidia 3070. Needs Vulkan bindless. Needs multiple Vulkan queues. Might get those from Wgpu in 2025. If you don't need max performance, this stack should work OK.
  • There was some promising work with the Orbit rendering demo, but that seems to have been abandoned. That was some advanced thinking about how to make the GPU do more of the job.
  • Renderling has some promise, but it's not ready for use yet and it's only one developer. They do have some EU funding.
  • If you do 3D work in Rust, expect to expend half of your time maintaining the lower levels. Budget accordingly.
  • Crate churn remains a problem. Winit, Wgpu, Egui, and their friends have to advance in lockstep as the APIs change. When one of them changes, it's about 1-2 months before the others catch up. Then you get to fix your own code to deal with the breaking changes.
  • Whenever there's been a serious problem that was hard to find, it was always because somebody built their own allocation system instead of using safe Rust constructs. Putting everything in an array and passing around array indices in multi-threaded programs is usually trouble.

At a more fundamental level, Rust rendering lacks the spatial component. Most of these renderers compute shadows by brute force - every light vs every object. This is O(N * M) and too slow as scenes get large.

This is a consequence of where you cut apart the problem. Whether you can do an efficient renderer without becoming a whole game engine that has a full scene graph, like Bevy, is a big question. Either the renderer has to get its spacial info from the layer above, or it has to build its own spatial info. The renderer has to be able to calculate quickly which objects are in range of a light, for example.

The price Bevy pays for this is that it doesn't really use Rust ownership. Bevy has its own run-time dynamic ECS system. You have to do everything Bevy's way.

It's possible to do serious 3D in Rust, but it's a real grind.

"If it was easy, someone else would have done it" - Pepe's Towing, LA.

Demo video here.

24 Likes

All the examples, except scene_viewer build and run fine on my mac. (scene_viewer builds, but the instructions written to the console when run do not work).

Not sure if you're asking for someone to just build & run, or look out for anything looking out of order. There are some minor visual artifacts on the cube's outer edges, but those could be intentional? (I haven't run on any other platforms, so I don't know if it's mac-specific).

Intel-based mac, running macOS 15.2, using Rust 1.83.0.

Looking good!

Thanks.

Why such a tall and thin image? It's usually square, but resizable. "cube" is supposed to be a full cube in a square window. Try "animation" - that's a harder test.

The shadow artifacts are a known rendering bug. It's a roundoff error problem that causes flat surfaces to shadow themselves.

scene_viewer doesn't ship with the scene files, unfortunately. Licensing issues. It reads standard glTF files. The standard Khronos demo files should work. Now that Khronos put those on Github, I should make scene_viewer look there.

Good points, that need to be fixed before a release to crates.io.

1 Like

Yeah, it wasn't a brilliant move of me to attach that image without clearly spelling out that it was just a screenshot of a small area just to show the artifacts. The actual application shows the entire thing.

The animation demo works great; resizing the window works fine (animation keeps running; aspect ratio is preserved, and the client area is redrawn as expected).

Kudos for getting low-level GPU stuff to run well on a platform you couldn't test it on.

Kudos for getting low-level GPU stuff to run well on a platform you couldn't test it on.

That's not me. That's really the Winit and WGPU groups. They're the ones who do the cross-platform stuff.

1 Like

New problem: Winit is becoming a framework. You call a library, while a framework calls you. The framework owns the event loop. This isn't a bad way to do things, but as a breaking change to an existing library, it's a major headache.

The trouble is, Rend3 also became a framework a few years ago. It currently owns the event loop. So now, two frameworks in use both want to be in charge. I now have to resolve that.

Not yet sure how big a problem this will be. It may mess up ownership and lifetime of some major structures in my code. It's yet another problem that takes up a few weeks without moving things forward.

"Now, here, you see, it takes all the running you can do, to keep in the same place." - the Red Queen, Alice in Wonderland.

1 Like

Anecdotally, as a beginner with little experience, I've already come up against some friction from the problem you're describing.

I'm learning by making some toy game engines and came across pixels to help render 2d pixel buffers.

Most of the examples are based on winit (minimal example) to supply the window and event loop, but are currently now all broken with the new version of winit which requires a very different code structure.

I managed to refactor the basic example but it now requires an Arc wrapper around the Window and a .clone call in order to construct the pixel buffer because of being forced to run the event loop through an app struct instead of simply having direct access to the loop.

1 Like

It's hard to take you seriously when you say obviously incorrect things.

Scroll to Game Engines | Are we game yet? look up musi_lili. Added two weeks ago. I thought it was supposed to be not updated for 6 months?

Also not the definition of what framework vs library is. Library is highly specialized tool, and framework is set of libraries that form a basis of a skeleton for developing some kind of software.

Your observation seems to be correct but this seems more like a disruptive refactor.

That said, Rust gamedev has problems but the main issue is lack of foundational libs for games. Games are notorious for failing all the time.

Note that there is an escape hatch in winit to get back to essentially the old API, but of course it has the same platform compatibility issues that design had (IIRC something about MacOS resume?)

1 Like

The blog for arewegameyet.rs, at https://gamedev.rs/ stopped updating in June 2024. At least I think that was the arewegameyet.rs blog. Not sure what's under the same management. There's a blog, a substack section, a Discord, various lists of resources, etc.

It's nice that someone added another retro 2D sprite based game engine, but that doesn't solve any of the hard problems.

Note that there is an escape hatch in winit to get back to essentially the old API

In the current version there's a deprecation warning at compile time, but the old way still works. In the unreleased version, it looks like the old way goes away entirely. I tried compiling against trunk and failed.

This has always been the case with winit. Older versions provided polling methods that allowed the event loop to yield to the caller. Those are long gone in the spirit of unifying every supported platform. (In other words: the API is the lowest common denominator between all platforms, including mobile and web.)

It kind of is a bad way, though.

I tend to rant about this because it is a real problem that has very ugly workarounds. Event loop composition is painful. (As in, it doesn't really work.) Your window manager wants to be the event loop, and your audio mixer wants to be the event loop, and your networking library wants to be the event loop. And they can't all be the event loop. You can almost get by if you run each in its own thread and jump through hoops to synchronize between them.

winit has always had this problem. But you're right, recent versions have made it worse.

3 Likes

Are you talking about the old run_app?

I meant pump_app_events which isn't deprecated, though it has:

This API is designed to enable applications to integrate Winit into an external event loop, for platforms that can support this.
[...]
You almost certainly shouldn’t use this API, unless you absolutely know it’s the only practical option you have.

1 Like

That's a good summary. Everybody wants to own the event loop, and they can't all own it.

On some platforms, windowing and GPU work have to be on the main thread. Winit supports phones and browsers, which mostly want to be single-thread environments, or at least the main thread is very special. That forces winit toward owning the event loop. Unfortunately, in 3D work and game work, usually the renderer or the game owns the event loop and controls the frame cycle. That's where I'm in trouble.

At least with modern rendering you want to do rendering and logic in separate threads (ideally many threads) so you're already in synchronization hell that makes who owns the event loop a bit academic.

I'm already there in Sharpview. Almost everything other than window events, 2D draw, and 3D draw is done off the main thread. Unfortunately, although the APIs of Rend3 and WGPU seem to be multi-thread, internally both have single internal queues managed by the main thread, so I don't get the concurrency Vulkan can provide. This is a boat-anchor on performance. On really complex scenes I'm down to 10 FPS.

On the compatibility front, I need to rework rend3-framework in rend3-hp to use the new winit "Application" trait.

I have hopes of switching to Renderling at some point, but that project isn't yet ready for prime time.

Weird, GL compatibility I assume? I took a quick look at WGPU docs, it seems like they only create a Queue with the device which seems pretty lame, but matches the current WebGPU spec, but even then I don't see why it would need to sync internally on most backends.

WGPU supports the subset of Vulkan that WebGPU supports. Their priority is browsers. So, one queue, no bindless, not much parallelism.
Binding is a bottleneck. This is a huge headache in a big world system, where the GPU is constantly loading and unloading assets as the viewpoint moves. When other threads are loading content, frame rate drops from 60 FPS to 10 FPS.

The number of people doing multi-threaded 3D graphics in Rust is small. (It may be just me. If there's anyone else out there, please message me.) So I hit bugs and bottlenecks few others do. I expected the first-person shooter people to be pushing the graphics stack in this area, but nobody ever did a high-performance big-world FPS in Rust. The enthusiasm for game dev in Rust four years ago kind of fizzled out before the graphics stack got really good.

For me, the low level graphics stack is something I want to use, not fix.

(Currently: trying to find a race condition in Rend3. Someone allocated a big vector and then used index numbers to address its slots. Somewhere, an index outlives the entry it points to. Fails about once every 1 to 12 hours, so it's a pain to test. The index values are encapsulated in handle structures, but the handles are copyable, so they can live too long. It's a dangling pointer in another form. Working on cleaning this up.)

(Most of the really hard bugs I find in Rust crates involve someone writing an allocator for something. Avoid writing allocators. If you have to do an allocator, make it self protecting so that it can't be broken from outside itself, provide checking code, and provide tough unit tests. Thank you.)

3 Likes

Yeah, I'm just surprised they bother locking (or whatever) the queue internally when it's thread safe in the majority of backends: AFAIK GL is the only odd one out here. Even with the existing WebGPU Queue API this doesn't seem like it would be a problem, but perhaps I'm missing something?

There are lots of interlocking problems. I don't fully understand them. I suspect I will have to.

If WGPU gets bindless (they plan this) that will probably get me enough of a performance improvement to get acceptable frame rates. Right now, about half the rendering thread CPU time goes into binding.

Can you render into command buffers on separate threads? There's ways to reduce binds in other ways even without bindless (texture atlases, push constants, etc.) but they also benefit from threading your rendering.