Game dev in Rust, a year later

I just bumped into this crate, which is super early (isn't even published, and the vulkan build is currently borked!) - but it looks pretty interesting!

Sits in the same space as wgpu but just has Metal and Vulkan as backends, is a lot lower level, and looks like it's trying to handle buffer sub-allocation nicely. Even has RT pipeline support!

Might be worth keeping an eye on longer term.

3 Likes

Meanwhile a primarily Rust-based game (Bevy + custom rendering etc.) became a bestseller this year, so your mileage may vary:

9 Likes

Hey wgpu Maintainer Here!

Yeah, I'm just surprised they bother locking (or whatever) the queue internally when it's thread safe in the majority of backends.

It's both. Vulkan requires external synchronization (Spec see "Host Synchronization"), but we also have a decent amount of tracking we need to do which completely falls apart if we don't have a concrete idea of which submissions happens before/after another.

That all being said, while we do have a significant number of locks, they're designed such that things only block other things that need to be blocked. Parallel command recording should work fine for example and command recording in parallel with resource creation should also work fine. If it doesn't, please file a bug!

Can you render into command buffers on separate threads? There's ways to reduce binds in other ways even without bindless

Yeah you can! In practice you hit about peak throughput around 3-4 threads, this is mainly because wgpu is memory bound, so you hit memory speed limits first.

14 Likes

WGPU supports the subset of Vulkan that WebGPU supports. Their priority is browsers. So, one queue, no bindless, not much parallelism.

This does not accurately represent the situation at all in basically every way.

Their priority is browsers.

There are broadly two groups of stakeholders in wgpu. Mozilla and the Community. 4 of our maintainers are from Mozilla, 3 are from the Community. The people at Mozilla will broadly focus on implementation correctness in their own work, as they are trying to ship WebGPU in Firefox. The Community will focus on whatever the individual contributors want to focus on. Because it is just a group of individuals, it has no real "focus" other than what people want. Mozilla is also understands just how much value having a community brings to the project, so dedicate a large amount of time to maintainership (reviews, etc) even on PRs that are completely unrelated.

one queue, no bindless, not much parallelism.

Multi-Queue is something we want to have, we just haven't had anyone champion the work on that yet!

We've had bindless since like 0.6 (was one of my first contributions to the project; added specifically for rend3). What we're talking about are improvements to the current bindless impl, some of which have already landed, many more still to go.

If you have any specific parallelism issues, please file issues and help debug them - wgpu is a complex system, the more information you can give us the better!

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.

Are you having issues with single queue or with multithreading, as they are unrelated in how they would cause bottlenecks. If it's single queue, having a transfer queue would delay the problem, but at some point you need to limit the amount of data uploaded in a single frame.

5 Likes

Oh interesting! I was under the impression that the backend queues were Sync, but I suppose I just was spoiled by the current safe wrappers in Rust. (And I never used threaded rendering in my few toy C++ attempts)

This is probably because of some rather oversimplified diagrams from when Vulkan was being introduced :sweat_smile:

Huh, you're talking about arrays of textures (etc.) right? (Not to be confused with texture arrays, thanks hardware vendors)

I remember messing with that forever ago, completely forgot it was there until now!

1 Like

Yeah - the less ambiguous term of art in wgpu would be "binding_array of textures"

As much as I give them crap for naming things badly, I must come to their defense that this is a "how users talk about it" problem, not hardware vendors being the problem themselves. "2D Texture Arrays" have existed for a very long time, well before bindless and binding_array was a thing.

3 Likes

Uniforms/storage has it worse with arrays of descriptors/uniforms or arrays within a uniform! (But not arrays of sets, we wouldn't want to confuse people) There sure are a lot of ways to have a lot of things...

Honestly, yeah it seems like a hard problem to give all these things good names, I was just giving them a bit of ribbing for this particular confusion, while giving a heads up to other readers that's they're not the same thing. (For example DX12 using "signature" instead of "layout" was way more confusing to me, so it's not like they have naming down either!)

1 Like

Yeah totally fair :slight_smile: Naming things is hard!

2 Likes

AWGY maintainer here - new submissions have been merged as recently as last week, and there's some work being done at the moment to clean up some of the old crates that are no longer actively developed.

So the site definitely isn't dead/abandoned. We are very reliant on PRs to keep things up to date, though (especially as I'm not as active in the gamedev space as I used to be), so I would encourage people to submit anything they think is missing!

15 Likes

Ok, but it's not what you linked.

My problem was, if you're gonna come in negative, at least have your arguments in order. If you can't deal with trivially provable wrong points, how would an uninformed reader like myself trust you on your more complicated points?

6 Likes

gamedev.rs is the blog for the (now largely inactive) Rust game development working group. That group has also maintained Are We Game Yet for the past few years, but they're seperate projects for the most part. The Discord server is run by someone else entirely.

But yes, you're right that the gamedev.rs newsletter hasn't updated since June :frowning:

Despite our best efforts to grow the team, we never ended up having more than three maintainers - and due to a mixture of personal circumstances and burnout, all of us ended up having to step back by the end of 2023. Someone did very kindly step up to try and get things back on track last year, but they have also become too busy to work on it, I think.

6 Likes

gamedev.rs is the blog for the (now largely inactive) Rust game development working group

Too bad. I wish someone was still "focusing on making Rust the default choice for game development".

I'd like to just use a graphics stack, not maintain or develop one. Without going all the way to an Bevy-level game engine that forces you to do things its way. Something with an API at about the level of three.js. (That means support for lighting, shadows, translucency, and culling, not just a wrapper around Vulkan.) Preferably with a big enough user community that others hit the big bugs before I do. Four years ago, that seemed to be very close. Today, it's more distant.

Yes. I commented in r/vulkan that Vulkan is not a renderer. It's a parts kit for building a renderer.

On the texture storage front, basically I'm constantly loading new textures into the GPU and unloading them. Size can range from 8 x 8 px to 2048 x 2048 px. Always a power of 2, but need not be square. The world being displayed is big, and you can move around it. Much asset shuffling required. This all works now in Sharpview, but asset loading into the GPU impacts frame rate by 3x to 5x. That's the problem I have to solve.

One big contiguous array of textures isn't too helpful. That's a good fit to games where everything is loaded, or scene loaders where you load a glTF file and then move the camera.

So I need the kind of bindless where each texture is in its own buffer, there's a big table of texture descriptors available to the shaders, and that table can be updated by other threads during rendering. Vulkan offers all this, but I don't think it's currently exported from WGPU.

1 Like

It's not supported by WebGPU, but it's in WGPU as a native-only feature: TEXTURE_BINDING_ARRAY

And you'll probably want one of NON_UNIFORM_INDEXING flags too.

4 Likes

I looked at the Bevy source to see how they dealt with the winit changes. They stayed with winit 0.30.0, before the changes, instead of keeping up. Current winit is 0.30.8 on crates.io, and 0.30.7 on trunk.

Oh, and wgpu just advanced to 24.x.x. Another big "refactoring".

I'm not sure what to do. This churn is eating up all my programming time just to stay in the same place.

What? "0.30.0" means "^0.30.0" which means ">=0.30.0,0.31.0" so cargo will automatically resolve that to 0.30.8. They're semver compatible. This means that bevy is already fully updated.

Oh, and wgpu just advanced to 24.x.x. Another big "refactoring".

The wgpu changes are very minor this cycle.

I'm not sure what to do. This churn is eating up all my programming time just to stay in the same place.

If it's truly this large of a task, you don't have to update. It's not that uncommon in game programming to find a version of deps that works, then just locking into it and not updating, or only updating as a dedicated project.

4 Likes

Right, but if you're not on the current version bug reports are ignored.

The winit thing is that they switched from library-style you-call-them to framework style they-call-you, in an area that's very brittle and has cross-platform complications. It's not a major code change, but it's delicate and easy to break. I have to revise rend3-framework to play well with winit's new ApplicationHandler trait.

It strikes me that there are now three layers of Arc. Sharpview has Arc around its own objects, which own Arc handles to Rend3 objects objects, which are Arc handles, which in turn own wgpu objects, which are becoming Arc handles.

There's a pattern that keeps appearing in graphics code. Something owns a thing at a lower level. When the upper level is done with it, and its Arc count goes to zero, it's dropped. But the necessary cleanup cannot be done immediately. Cleanup has to be deferred to the end of the frame cycle, or requires talking to the GPU, or a table of all the objects has to be updated. I have occurrences of that in Sharpview. Rend3 has things like that. So does WGPU. Something similar is probably happening at the Vulkan level.

Things which work this way tend to be brittle and have race conditions. Difficult concurrency bugs seem to come from instances of this pattern.

There should be safer ways to approach this sort of thing. It's all ad-hoc now.

The primary problem with high-level graphics crates in Rust (WGPU and its family in particular) is that they attempt to model ownership of driver-managed objects that they don't actually own. The driver owns them. The application only owns handles (numeric indices) to these objects.

This leads to a situation where these crates introduce a non-optional validation layer, causing performance penalties. Another issue is that the model proposed by a specific crate limits certain use cases compared to what the original raw graphics API offers (bindless is one such example).

What these crates are really trying to do is express the original API model in terms of Rust's memory model. This is not necessarily a bad idea, but it raises the question of whether the misuse of the Vulkan API is actually unsound from Rust's perspective.

The primary goal of Rust's memory model, as I see it, is to provide both the programmer and the compiler with a contract that allows the compiler to confidently perform MIR optimizations on CPU-based code. Passing two active mutable references to the same object to a function is unsafe (and unsound) because the compiler assumes that mutable borrows in Rust are always exclusive, allowing it to reorder stack memory accordingly.

Of course, Rust does not yet have a well-established definition of "undefined behavior", leaving room for broad interpretations. For example, WGPU and Vulkano consider simultaneous recording of command buffers (originating from the same pool) as undefined behavior. This may be true from Vulkan's perspective, but Rust's compiler gains nothing from enforcing exclusivity on command pools. Rust neither owns the command pool nor the command buffer. The driver owns and manages their memory independently of the application's source code, and the application code merely holds numeric indices (handles) to these objects.

Low-level crates such as Ash, therefore, do not introduce heavyweight abstractions. Instead, they provide access to raw graphics API bindings more or less as they are. These APIs are designed to be low-level, and as long as you follow the specification, they are ready to use. That's why I believe using them as they are is safe from Rust's point of view, regardless of what you do in Rust code. After all, C/C++ developers use them without additional abstractions, so why shouldn't we?

However, this still does not answer the question of how we should build high-level abstractions for community use. To address this, we must acknowledge that any higher-level abstraction on top of a low-level API comes at a cost, whether we build it in Rust or C++.

I would describe WGPU's niche as entry-level. It is certainly easier for newcomers who want to learn the basics of graphics or develop applications that are not critically dependent on performance. However, when developing a higher-level graphics engine or a performance-sensitive video game, we should probably avoid using this layer. Two layers of abstraction are too much.

I believe that a high-level Rust graphics library should be designed with a deep understanding of what end users will actually do with it. It does not necessarily need to cover all theoretical possibilities of computer graphics. Rather, it should focus on clear goals and address a limited but well-understood domain.

For example, if we were building a high-performance sprite graphics engine, the end user shouldn't have to deal with low-level details such as memory allocation or even writing shaders by hand. The crate should handle all of this under the hood, allowing us, as its developers, to provide the most performant, safe, and correct internal design using raw bindings without unnecessary abstraction layers.

To sum up, there is still a lack of diversity among such domain-specific crates in Rust. Essentially, we need more specialized, end-user-focused game engines.

I believe Rust has the potential to shine in games where cutting-edge graphics are not the primary focus, allowing developers to concentrate on interesting game logic rather than high-end rendering. A game like Factorio, which started in raw C++ over a decade ago, could perhaps be developed in Rust today. However, such a project would be unlikely to benefit from engines like Unity (or Bevy!).

Many of these games don't require heavyweight graphics, and to be realistic, I don't expect Rust to produce direct competitors to Unreal Engine or Red Dead Redemption 2 in the foreseeable future. However, developing specialized, performant, and user-friendly graphics infrastructure for games like Civilization, Age of Empires, Fallout, or Diablo 1 is an achievable and worthwhile goal.

4 Likes