Having tried writing a game engine in Rust, I can't imagine that Rust would become the future of game development. Lack of safety is a feature in game development, because the optimizations required are typically unorthodox and a super strict language slows development down. Additionally, object ownership can be unclear in a game development setting, which typically makes use of global variables for state. The benefits of using Rust over C++ for game development are unclear in these regards.
Isn't this basically saying that you can't have the compiler guaranteeing you aren't including bugs in those optimizations / global variable usage because it's more efficient to just write and then ultimately ship some bugs?
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.
Game development problems are very often global state manipulation problems. In a given game, many things may have genuine need to be able to read and write to multiple global variables once per frame (that's usually somewhere between 30 and 240 times per second), and that is after all unnecessary global state has been removed.
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.
The one thing I will note is that most of what you have said about performance is relevant primarily in the AAA game space (and in higher end mobile titles, which increasingly look like AAA games).
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.
But for a lot of indie games smooth running is not a significant problem, because simply their scenes are not complex enough and it is not a big deal even if they are written on some programming language which is not of the highest performance ones like C# or Java (The original version of Minecraft for example is written in Java). There are many successful indie games which are even not 3D (FTL or Darkest Dungeon for example).
if your game is riddled with bugs... you make no money or at least less.
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.
The new AC:Valhalla as well. Pretty much any new sport EA game too. Wasteland 3 has been riddled with game breaking bugs on launch and still people have been buying it like there is no tomorrow.
In enterprise, if you introduce a bug that causes a business to not function, you must fix it right away because you may receive lost business and possibly a lawsuit.
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.
Most of the remarks here emphasized performance, but I believe the actual thing that is "lost" in Rust's model is the ability to quickly rearrange the data dependencies.
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.
That's counter productive when it comes to gamedev. Most of the code you write for a game is going to be thrown out at some point. Being able to get the idea onto the screen and iterate rapidly is worth a lot more than not having a dev build crash after a while. Make it work, make it right, make it fast except the last two are usually transposed in gamedev.
> iterate rapidly is worth a lot more than not having a dev build crash after a while
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.
You only have to debug the code if you're going to keep it. I'm not going to spend days debugging a prototype made of spaghetti, it'll have served its purpose and I'll start over with optimization in mind and refine the idea. If you wanted a programming language that would be a real win for game development you'd choose something that had the option of no type or memory safety and no boilerplate for prototyping with an option to turn those options on for when you actually integrate the feature with the rest of the code.
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.
I know this is only responding to your one example, and perhaps your statement is still true generally, but I think that in this particular case it is not.
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.
Right, the branch may or may not be optimized out; probably it depends on llvm's alias analysis. That's not good, it means you might break it accidentally.
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
> you really do have to babysit Rust to get efficient codegen
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.
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.
I'm curious; is the extra cmp something that's actually required by Rust's semantics, or is this more along the lines of an optimizer bug/missed optimization?
This particular case is because array delegates to slice for Eq, and slice has the pointer comparison - though it looks like there's some churn here with 'guaranteed_eq' so it may be fixed soon [1].
Actually "won't let you do" or "makes you clearly mark where you did those things"? That is, are you referring to things it doesn't actually support doing that you want/need to do, or just that it's unsafe?
I'm not familiar with rust or if their `unsafe` block mitigates the issue somewhat - I know that for example with memory management, you'd rather ship a game that leaks a bit of memory (since games are generally only open for a few hours at a time, not a lot of memory should be leaked) in exchange for less stutter/performance issues from the GC.
Unfortunately, Rust has limitation on what it can prove to be safe using the ownership and lifetimes approach. One of the simplest things which can't safely be done in Rust is a doubly-linked list.
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.
You can do these things safely, you just can't do them safely and efficiently at the same time. You could do a doubly linked list with reference counting, and it would be in safe code, it just wouldn't be classic implementation you'd be imagining, and not as efficient.
You can do circular structures like doubly linked lists in Rust. You can do them efficiently. But you do need to use "unsafe". Of course you do, a moments thought shows you why.
It is not as "safe" as Rust without "unsafe" blocks, but it is still reasonably safe.
I haven't found any problem where a linked list could not be replaced with a vector of index-element pairs (or triples) with a reductions in allocationd.
I'm open for discussion as I haven't been able to convince myself of the use of any linked list.
>Unfortunately, Rust has limitation on what it can prove to be safe using the ownership and lifetimes approach. One of the simplest things which can't safely be done in Rust is a doubly-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.
The technical progress will be unpredictable as usual, but at the level of the problem definition, I think it's safe to say compiling Rust is easier than compiling C++.
Jai used to be interesting, but after a while it feels like Jon believes his language is a competitive advantage, and will never come out. It's either that or he painted himself into a corner and needed years to get some must-have feature working.
Jonathan Blow has a reputation for taking a rather long time to release something eagerly awaited, and the result being impressive and critically acclaimed.
I hope Jai will be the same way. We'll see what happens.
Sure, but that works for a game where you can release it once and then everyone consumes it.
Languages are ecosystems. They have tools, and libraries, and shims to integrate with other tools. They need evangelism. They grow with community involvement. As far as I know, no one has ever released a new language that was perfect in its initial release - there has always been feedback and syntax quirks that get addressed in future versions.
If a "0.1" compiler with warts was out there for people to play with and improve on, I'd have faith. But if you can't get a demo that you're willing to share within 5+ years, then I have very little faith you'll ever release anything, and absolutely zero faith that you'll be able to compromise your mental idea of perfection enough to build a thriving community around a product.
Lack of safety is a feature in game development, because the optimizations required are typically unorthodox and a super strict language slows development down.
One of my first experiences interacting with a golang programmer, was in the context of my game server project. As an optimization, I had a race condition in the code that disconnected clients. After all, if the connection is about to die, I don't care if it dies in frame n, n+1, n+2...etc. This golang programmer was totally out to tell me I'm stupid, ugly, and bad, because I should always be using -race and always have 0 race conditions.
Like, it would have been one thing if he could've had a cogent discussion around how the build should be free of warnings, because that makes such signals super clear, but no, it was just pure dogmatism.
Additionally, object ownership can be unclear in a game development setting, which typically makes use of global variables for state.
In ECS, why should object ownership be an issue? The Systems can just own everything.
given that a behavior in a race condition can be undefined, he or she may have been right. You might get the merely delayed behavior your want now, but something else on a future version of the compiler.
Perhaps you are aware of all these subtleties and knew it was ok in this case.
given that a behavior in a race condition can be undefined, he or she may have been right
If the other party really had an interest in understanding, and not just condemning, he would've looked for more facts. He wouldn't have been talking over me and cutting me off.
The main problem which global state introduces, is making the bad assumption that there will only be one example of a given sort of data.
In games, that's a safe assumption for many parts of the system, since you're definitely only going to be running one instance of the game world at a time. So making that state private just means you have to pass around references to it on the stack, and keep track of what you're doing.
Classic webcrap is near-stateless, with state in the database only. That way, any server in the farm can handle the next client request. Games have a consistent world to maintain in memory. Sometimes a really big consistent world.
I feel like this is an underrated sentiment. A lot of Rust’s design decisions (lack of inheritance, borrow checking, immutability by default) are complete non-issues when you’re reading a JSON string into some business struct or an enum and then serializing it back out to a DB again. Everything can be flat data types and functions can be pure and stateless.
But not if you actually have to keep things around in-memory. Then all the sudden your functions can’t be pure, you have to add additional parameters to each method to account for additional state, and you can’t design flat structs anymore since you have many different object types and they have to go somewhere and you can’t just declare N variables line by line. So you have to grapple with the limited definition of trait objects, or non-extensible enums, or throw everything out and use Any. It gets complicated.
Rust is just one of the current Silicon Valley memes at the moment. You could hit the front page with an article titled "Why Rust could mean you'll never run into another broken McDonald's ice cream machine again".
"Machine learning let me train my dog twice as fast."
"How AI will revolutionize marriage counseling"
"Study: Using Go results in 10% better senior programmer retention"
> Lack of safety is a feature in game development,
Bullshit.
How many games have had releases delayed and major wrenches thrown into marketing plans because of progress ruining heisenbugs? How many failed certification passes from banal data races and other undefined behavior?
We trap UI in actionscript or javascript, and gameplay programmers in other scripting languages - perhaps python or lua - for faster iteration times, hot reloading, and safety. Because it's difficult enough to keep the build stable when it's merely all the engine programmers who should know better screwing things up with C++.
This results in large messes of poorly performing, poorly optimized, garbage-collector laden code that Rust would handle much faster. We're leaving lots of performance on the table, often for little other purpouse than "safety".
Console first day patches may have taken off some of the pressure for getting the first release right, but handhelds aren't always online and still have a pretty high bar.
> the optimizations required are typically unorthodox
Rust's `unsafe` keyword and intrinsics let you do all the unaligned intrinsic-laden data-racey technically-undefined-behavior micro-optimizations you might want to do in C++ in Rust just fine.
It'll hopefully trigger a more stringent code review and force you to justify your pile of bugs, but that's a good thing. Or you can skip the code review if your entire company really disagrees.
> a super strict language slows development down
C++ is also super strict, just in an unenforced-at-compile-time way that result in plenty of late nights chasing heisenbugs. Don't get me wrong - language strictness can slow development down - but that's one of the reasons people eschew C++, too.
As for C++ vs Rust? I'm going to spend more time and be far less certain of catching the issues in a C++ code review than I would be in a Rust code review. And while it took a few months for my development speed in Rust to catch up with my development speed in C++, it did happen.
Rust merely forces you to acknowledge when you're being sloppy.
> Additionally, object ownership can be unclear in a game development setting, which typically makes use of global variables for state.
I have solved so many sources of endless heisenbugs by eliminating some of these global variables. John Carmack as far back as 2013 was using phrases like "horror show" to describe similar parts of his own codebases[1] and has been agitating for more functional styles.
An unclear mudball of global variables is entirely possible - and easy - in Rust if you really want that. You merely need to make it either thread safe, or resort to `unsafe` if you really can't tolerate the overhead of not having threading-related heisenbugs.
Just counting my personal experience, I've seen multiple titles fail cert and delay release thanks to bug backlogs that were mostly filled with memory issues, even after months of working to reduce the backlog of crash bugs. Perhaps you can't blame that entirely on the C++ codebase, but you sure can't absolve it entirely either.
At every single studio I've worked at I've helped build out crash collecting, symbolizing, or deduplicating infrastructure just to get a lead on where shit is exploding, and seen major productivity outages when tooling crashed frequently enough to disrupt the workflows of my coworkers. Address sanitizer, valgrind, extra debug allocators - I've sunk a lot of time into just making these crashes shallower to debug and quicker to catch.
One of the smallest and quickest projects that comes to mind was an NDS -> iOS and Android port. Do you think most of the time was spent in porting APIs and control schemes? More than half the time on that project was spent just chasing down really stupid memory related crash bugs that were exacerbated in frequency and time by the port. I made damn sure to communicate my concerns about the schedule, and if my memory serves me correctly, we managed to soft launch "only" a week or two late.
C++'s strengths are it's lean portability, speed, and huge existing codebases and tallent pools. Undefined behavior is simply the cost we begrudgingly pay, not it's strength, and even that we often mitigate by trying to figure out ways to write as little C++ as possible - keeping it only for the parts of our codebase that are actually performance sensitive. Tooling is often C#, Python, anything but C++. UB is a pox and constant time sink.
Whenever people call out C++'s UB as it's strength, I'm left wondering how many of their bugs do they actually fix, and how many are begrudgingly fixed by their coworkers. Most of the people whom I've worked with with that attitude don't pull their own weight in the debugging department, so they don't see the full costs of it.
I don't share the same experience. Crashes do happen but I wouldn't call the time sunk there a lot. If you get a ctd, usually a callstack and a bit of thought is enough for figuring out what is going on. At worst case you have valgrind. Very rarely it goes beyond that but even with that I would say it is a well trade off on how relaxed C++'s memory management is compared to Rust. And I believe C++ is relatively safe when you have well designed systems where ownership is clear enough. I have a feeling that if my company suddenly flipped to Rust now, the struggling against borrow checker would be a bigger time sink
For me majority of game development is feature development and fixing gameplay related bugs. Which will be same regardless of language you use. And depending on language feature development might be even slower
> If you get a ctd, usually a callstack and a bit of thought is enough for figuring out what is going on.
Very different experiences. Sometimes that happens.
Sometimes the symbols have been evicted from the symbol server. Sometimes the minidump didn't capture relevant memory (and the full dump would be a couple dozen gigs, making external QA reluctant to constantly capture those). Sometimes the crash is the result of memory corruption from unrelated systems minutes ago that doesn't reproduce when you enable any of your debug allocators because reproing relies on pointer reuse in a hashtable. Frequently custom allocators defeat tools like valgrind and address sanitizer, requiring extra work to either bypass or explicitly annotate valid/invalid memory ranges. Sometimes the process only exit(3)s (technically not even a crash!) from an unrelated thread on a specific bit of UI with no relevant callstacks nor logging, and if you open up the windows charm bar for more than 10 seconds without a debugger attached, and you resort to bisecting p4 history once you've spent the several days even figuring out repo steps.
That wasn't even our bug, but it was our workaround... even when the codebase is good, the middleware often isn't! And that is but one of many I've had to deal with.
> C++ is relatively safe when you have well designed systems where ownership is clear enough.
Meanwhile, the poster I was originally replying was pointing out that you can have unclear object ownership full of globals and C++ is somehow supposedly good at dealing with this. Hopefully you're at least agreeing with me here, in disagreeing with that! :)
A lot of gamedev code isn't very well designed, IME, and "relatively" safe can be suprisingly unsafe as well.
> I have a feeling that if my company suddenly flipped to Rust now, the struggling against borrow checker would be a bigger time sink
The first couple months after I picked up Rust, I had that phase. Now it's quite easy for me - sometimes it requires a deep `.clone()` or two, but in the equivalent C++ codebase you wouldn't have even dreamed of not making the deep copy - because in equivalent C++ code it'd be impossible to do the equivalent fancy zero-copy borrowing nonsense even remotely safely.
Rust over C++ for single player games is definitely of questionable benefit right now. For multiplayer games I think a much more compelling case can be made, certainly on the server if not both server and client.