To me it seems like this would require a significant adjustment in how certain problems are approached, but the outcome would likely be more effective development as you could eliminate a lot of cost that's spent dealing with bugs in the future.
To me it seems like this would require a significant adjustment in how certain problems are approached, but the outcome would likely be more effective development as you could eliminate a lot of cost that's spent dealing with bugs in the future.
Enterprise developers often look very poorly upon the solutions that game developers come up with, but game development is an entirely different realm, very far away from any enterprise software development stuff I've worked on, or even heard of. I've looked. I didn't believe this myself for a long time.
Games have very strict performance requirements that enterprise software simply does not have. Virtually no one in enterprise software cares about performance, really, and certainly not as a primary concern. Enterprise developers will often just throw more RAM or CPU at the problem until it goes away. I mean why not, that's an option that is on the table. Of course they're going to take that route sometimes. Game developers can't do this, because they're not in control of the hardware that their game is run on.
Games are often scored on how good they look, and how well they perform. Game sales are a function (at least partially) of the score/rating that the title attains, and game developers are paid out of the money that game sales provide. Concerns that do not result in an improved critical game rating are secondary. So, there is a very strong hunger there for performance which simply does not exist in other software development fields. Until enterprise software developers are paid based on the performance of the applications they write, no one who has NOT developed a game and fought performance problems along the way can ever understand.
There are games which consume nearly the entire bandwidth between CPU and RAM on a modern PC -- continuously -- and this is after many optimization passes to prune the data that is needed from RAM at any particular moment in time and make it as small as possible.
Game development is just an entirely different thing than any other kind of software development. Languages created with general purpose use in mind may achieve (and have achieved) some success in game development, but those languages will get in the way of the developer at least as often as they aid the developer. Same rule applies for general software engineering rules.
In the indie space, you see a lot more titles where performance is a secondary consideration. The concept (and the style of the visuals) is often more important than having cutting edge graphics.
There are also segments of the business that are more like enterprise software. If you look at something like League or WoW, you have titles that are continuously evolved over many many years, and getting the infrastructure right matters a lot.
With the added mention that the game still has to run smoothly, otherwise it's extremely annoying, no matter how great the concept.
further, the whole point of rust is that is fast and safe.
safe doesn't imply slow.
a lot of game development is entity component systems, and there are several of those for rust - it will be a matter of time until these are good and fast enough to be used in larger and larger games.
In game development, if you introduce a bug that causes a game to not function, you must fix it right away because you may receive lost business and possibly a death threat.
A lot of the time, a game's bug surfaces in a way that will never cause a crash: it's just undesired behavior. At some level, the behavior can be defined in terms of "must" and "cannot" constraints, RFC-style.
And in theory you could do some kind of automatic verification of each behavior. But...the majority of the plumbing of the game isn't in the rule logic itself - almost all of the rules are simple. It's in making sure that the mutations produced by one rule will flow neatly into the next, without introducing some form of synchronization bug. This is a real minefield because you're often iterating over lots of similar elements, and it can look like you can bundle their update code to look "clean" or present an opaque interface, but then in practice, you need to split out the iteration differently to resolve a dependency issue.
Developers responded to this by introducing an event-bus abstraction, with bubbling and priorities and all the bells and whistles. But that doesn't really solve having dependencies, it just presents another way to surface a scripting layer.
Repeat this class of problem enough times and you will start inlining the bulk of the update into a single main loop, because that makes manual review of the dependencies more of a spatially engaging process, one where you don't have to provide new boilerplate so that the build process can tell you that you screwed up: instead you just look carefully, see that the ordering is wrong, and shovel around the block of code. Done.
And while you can do this in Rust, it renders a great deal of the language irrelevant, because the features it helps most with are those parts where you want tighter access control across function boundaries. When you start leveraging that access model, behavioral changes can start to take hours instead of seconds, which makes it appropriate for inner-layer backends that need deep optimization, but ergonomically terrible for the greenfield case, which is what games need to bend towards early in production when major features might be changing.
You are missing the part about wasting hours or days to investigate the crash, easily offsetting the time saved with fast and loose coding.
It would be nice to see examples to judge whether a stricter language like Rust can save more time on the debugging and fixing side (by reducing mistakes) than it costs with verbose syntax and "unnecessary" safety mechanisms on the design and coding side.
For example, consider a function for 16 byte equality:
fn equals(v1: [u8; 16], v2: [u8; 16]) -> bool {
v1 == v2
}
rustc emits a branch to test if v1 and v2 point to the same thing, which is unnecessary (it can never be true!) and can ruin loop vectorization, etc. The useless branch is not serving any safety purpose but requires unsafe to avoid.https://rust.godbolt.org/z/6973jG
The efficient version is quite a bit more verbose :(
In this godbolt [0] I tried using the safe version of equals with two values (from stdin so it can't just optimize the computation away, though I don't think it would) and it seems that when the compiler inlines the equals function, the inefficiencies are removed (which can be seen in the example::main section of the output; lines 212-216 look nearly identical, if not identical, to equals_unsafe in your example).
Please let me know if I'm missing something or if there are any other safe APIs to be wary of.
IME Rust is more dependent than C++ on these fragile optimizations. I think it is because idiomatic Rust is more abstract, and also because C++ has more advanced specialization features which allows implementing optimizations directly. For example in C++ you can say "if the type is uint8_t then do this instead..." but this is harder in Rust.
You asked for other examples: consider a simple operation over an array, like bitwise not. A C-style for-loop does the obvious thing but idiomatic Rust emits a gazillion one-byte 'not' instructions. This doesn't involve 'unsafe' but it does illustrate that you really do have to babysit Rust to get efficient codegen. https://rust.godbolt.org/z/PjW5vo
(In fairness C++ has its own stupid pitfalls too: https://travisdowns.github.io/blog/2019/11/19/toupper.html)
Reminds me of the time we had to write a high performance SQL engine at university. In Java, and it boiled down to exactly the same realization: We spent a lot of time babysitting the GC instead of actually making the actual SQL engine better.
The plan was to use C++ instead, but that option was dropped by the lecturer shortly before the project started :/
That's true, and while I agree that it is a concern, is there ever a way to gain both ergonomics and optimization? In other words, aren't we always leaving it up to the compiler one way or another? You mention C++'s specialization, and I suppose that is a solution, but then you are (potentially, I can't say for certain) sacrificing compile times. Although I will admit that this is probably a fair trade for certainty of optimization, and it would be nice if Rust at least offered some amount of specializing.
> you really do have to babysit Rust to get efficient codegen. https://rust.godbolt.org/z/PjW5vo
Oh wow, that is interesting. Yeah, that probably speaks to Rust still being a relatively new language, although I would have expected LLVM to do something about that... In any case, that's certainly no zero-cost abstraction.
Thanks for the neat info!
1: https://doc.rust-lang.org/src/core/slice/mod.rs.html#6610
I'm certain that Rust will help some parts of game development a lot, but I'm also certain that game developers sometimes use tricks safety of which cannot be proven by Rust's type system.
It is not as "safe" as Rust without "unsafe" blocks, but it is still reasonably safe.
But circular structures are usually a bad choice.
I'm open for discussion as I haven't been able to convince myself of the use of any linked list.
It can be done "safely" in Rust, as in without actual bugs; it can't be done in "safe Rust", that is, Rust without `unsafe{}` blocks. But it's reasonable to assume that any Rust game would make liberal use of `unsafe{}` blocks, because as we have all agreed, game developers don't actually care that much about memory safety anyway.
As such it seems wrong to suggest that the primary barrier to Rust in games development is its memory safety. Perhaps the culture of today's Rust community that shuns `unsafe{}` is unsuitable for games development, but Rust offers plenty of advantages beyond memory safety, and in contrast with C++ the particular advantages I would see as most relevant are:
- no legacy types/structures, important when you have a large team on a shoestring budget working late nights with sporadic communication; there will be fewer discrepancies in code styles
- potentially faster compile times than C++ when Rust is more mature
- distant future item, but there's the possibility for better FFI with game scripting languages when Rust is more mature
None of the above really depends on memory safety except kinda the FFI, but it's all relevant for a game dev.
While the adoption of "safe Rust" has left the tarmac, the best practices and widespread use of "unsafe Rust", a perfectly reasonable language in itself, have barely begun to develop. It's likely that any Rust games will generally be written with lots of `unsafe{}` and while nobody would learn Rust to use `unsafe{}` specifically, game developers might learn mostly safe Rust in CS courses that cover systems programming and then take those skills to the game industry and already know most of what they need to ramp up on an unsafe Rust codebase.
That last sentence is also why I think the Rust games era is at least a decade away; game shops don't usually want to retrain their employees on new languages; they'll just use what's in the field. That's also why Rust will probably beat out Zig (or Nim/Jai), since even though the latter is a little more suited for games, the former is much more likely to be taught in schools.
First cargo needs to also handle binary dependencies, and C++20 has modules now.