Skip to content

Deterministic RNG #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
135 changes: 135 additions & 0 deletions rfcs/55-deterministic_rng.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Feature Name: `Deterministic RNG`

## Summary

Include a source of entropy to (optionally) enable deterministic random number generation.

## Motivation

Bevy games / applications often need to use randomness[<sup>1</sup>](#1) but doing so makes execution non-deterministic. This is problematic for automated testing as well as deterministic execution. Deterministic execution is important for those wishing to create games with [deterministic lockstep](https://gafferongames.com/post/deterministic_lockstep/) as well as high-fidelity simulations.

Currently there is no official way to introduce randomness in bevy, a plugin, or an app. Other engines such as [Godot](https://docs.godotengine.org/en/stable/tutorials/math/random_number_generation.html), [Unity](https://docs.unity3d.com/ScriptReference/Random.html), and [Unreal](https://docs.unrealengine.com/4.27/en-US/BlueprintAPI/Math/Random/) include engine support for randomness.

<small>
<sup>1</sup><a name="1"></a> For example, 16 of bevy's examples currently use `rand`
<details>
<pre>
examples/games/contributors.rs
examples/games/alien_cake_addict.rs
examples/async_tasks/external_source_external_thread.rs
examples/async_tasks/async_compute.rs
examples/ecs/iter_combinations.rs
examples/stress_tests/transform_hierarchy.rs
examples/stress_tests/many_lights.rs
examples/animation/custom_skinned_mesh.rs
examples/stress_tests/bevymark.rs
examples/stress_tests/many_sprites.rs
examples/ecs/parallel_query.rs
examples/ecs/component_change_detection.rs
examples/ecs/ecs_guide.rs
examples/app/random.rs
crates/bevy_ecs/examples/resources.rs
crates/bevy_ecs/examples/change_detection.rs
</pre>
</details>
</small>

## User-facing explanation

### Overview

Games often use randomness as a core mechanic. For example, card games generate a random deck for each game and killing monsters in an RPG often rewards players with a random item. While randomness makes games more interesting and increases replayability, it also makes games harder to test and prevents advanced techniques such as [deterministic lockstep](https://gafferongames.com/post/deterministic_lockstep/).

Let's pretend you are creating a poker game where a human player can play against the computer. The computer's poker logic is very simple--when the computer has a good hand, it bets all of its money. To make sure the behavior works, you write a test to first check the computer's hand and if it is good confirm that all its money is bet. If the test passes does it ensure the computer behaves as intended? Sadly, no.

Because the deck is randomly shuffled for each game--without doing so the player would already know the card order from the previous game--it is not guaranteed that the computer player gets a good hand and thus the betting logic goes unchecked. While there are ways around this--a fake deck that is not shuffled, running the test many times to increase confidence, breaking the logic into units and testing those--it would be very helpful to have randomness as well as a way to make it _less_ random.

Luckily, when a computer needs a random number it doesn't use real randomness and instead uses a [pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator). Popular Rust libraries containing pseudorandom number generators are [`rand`](https://crates.io/crates/rand) and [`fastrand`](https://crates.io/crates/fastrand).

Pseudorandom number generators require a source of [entropy](https://en.wikipedia.org/wiki/Entropy) called a [random seed](https://en.wikipedia.org/wiki/Random_seed). The random seed is used as input to generate numbers that _appear_ random but are instead in a specific and deterministic order. For the same random seed, a pseudorandom number generator always returns the same numbers in the same order.

For example, let's say you seed a pseudorandom number generator with `1234`. You then ask for a random number between `10` and `99` and the pseudorandom number generator returns `12`. If you run the program again with the same seed (`1234`) and ask for another random number between `1` and `99`, you will again get `12`. If you then change the seed to `4567` and run the program, more than likely the result will not be `12` and will instead be a different number. If you run the program again with the `4567` seed, you should see the same number from the previous `4567`-seeded run.

There are many types of pseudorandom number generators each with their own strengths and weaknesses. Because of this, Bevy does not include a pseudorandom number generator. Instead, the `bevy_entropy` plugin includes a source of entropy to use as a random seed for your chosen pseudorandom number generator. The plugin can be completely disabled if no source of entropy is required, the default entropy from the OS can be used if randomness is needed but deterministic execution is not, or a **world seed** can be specified for deterministic random number generation.

Note that Bevy currently has [other sources of non-determinism](https://github.com/bevyengine/bevy/discussions/2480) unrelated to pseudorandom number generators.

### Usage

The `bevy_entropy` plugin ships with Bevy and is enabled by default. If you do not need randomness, you may [disable the plugin](https://docs.rs/bevy/latest/bevy/app/struct.PluginGroupBuilder.html#method.disable) or use Bevy's [minimal set of plugins](https://docs.rs/bevy/latest/bevy/struct.MinimalPlugins.html).

When enabled, `bevy_entropy` provides one world resource: `Entropy`. `Entropy` is then used as the seed for the pseudorandom number generator of your choosing. A complete example leveraging the [`StdRng`](https://docs.rs/rand/latest/rand/rngs/struct.StdRng.html) pseudorandom number generator from the [`rand`](https://crates.io/crates/rand) crate can be found in https://github.com/bevyengine/bevy/tree/main/examples.

The default source of world entropy [`Entropy::default()`] is non-deterministic and seeded from the operating system. It is guarenteed to be suitable for [cryptographic applications](https://en.wikipedia.org/wiki/Pseudorandom_number_generator#Cryptographic_PRNGs) but the actual seeding mechanism is an implementation detail that will change over time.

You may choose to determinstically seed your own world entropy via [`Entropy::from`]. The seed you choose may have security implications or influence the distribution of the resulting random numbers. See https://rust-random.github.io/book/guide-seeding.html for more details about how to pick a "good" random seed for your needs.

Depending on your game and the type of randomness you require, when specifying a seed you will normally do one of the following:

1. Get a good random seed out-of-band and hardcode it in the source.
2. Dynamically call to the OS and print the seed so the user can rerun deterministically. In games like [Factorio](https://www.factorio.com/) sharing random seeds is encouraged and supported.
3. Dynamically call to the OS and share the seed with a server so the client and server deterministically execute together.
4. Load the seed from a server so the client and server deterministically execute together.


## Implementation strategy

https://github.com/bevyengine/bevy/pull/2504

- The PR includes `rand` as a dependency but it is not in public API. We might be able to slim it down to [`getrandom`](https://crates.io/crates/getrandom).


## Drawbacks

- This may not be general enough to include in Bevy.
- This may not be general enough to be on by default.
- Includes `rand` as a dependency
- But not in public API.

## Rationale and alternatives

- Why is this design the best in the space of possible designs?
- It gives flexibility for PRNG crates while also enforcing standards on the ecosystem.
- It defaults to what someone would resonably expect if they wrote it themselves.
- It defaults to something safe (suitable for cryptographic functions), removing a possible footgun.
- What other designs have been considered and what is the rationale for not choosing them?
- We could go higher and expose an API closer to `rand`. This is what [Godot](https://docs.godotengine.org/en/stable/tutorials/math/random_number_generation.html), [Unity](https://docs.unity3d.com/ScriptReference/Random.html), and [Unreal](https://docs.unrealengine.com/4.27/en-US/BlueprintAPI/Math/Random/) do. We are not experts in PRNG API design, and while `rand` is clearly the most popular random crate in the Rust ecosystem we don't currently want to tie bevy to any particular API in case something better emerges.
- We could go lower and merely expose a static `WorldSeed`. I'm worried about what the default would be and forcing a seed at world creation feels heavyweight.
- We could default to a faster PRNG rather than a safer one. I wanted folks to fall into the pit of success.
- What objections immediately spring to mind? How have you addressed them?
- `rand` is too heavy of a dependency.
- This should not be in core.
- What is the impact of not doing this?
- There is no chance of the Bevy ecosystem supporting deterministic execution.
- Why is this important to implement as a feature of Bevy itself, rather than an ecosystem crate?
- There needs to be one true way to keep the ecosystem coherant.

## Prior art

* https://github.com/bevyengine/bevy/discussions/2480
* https://github.com/bevyengine/bevy/discussions/1678

Other engines such as [Godot](https://docs.godotengine.org/en/stable/tutorials/math/random_number_generation.html), [Unity](https://docs.unity3d.com/ScriptReference/Random.html), and [Unreal](https://docs.unrealengine.com/4.27/en-US/BlueprintAPI/Math/Random/) include engine support for randomness (with a higher-level API).

* https://towardsdatascience.com/how-to-use-random-seeds-effectively-54a4cd855a79
* https://www.whatgamesare.com/determinism.html
* https://gafferongames.com/post/deterministic_lockstep/
* https://ruoyusun.com/2019/03/29/game-networking-2.html
* https://arxiv.org/abs/2104.06262

## Unresolved questions

- What parts of the design do you expect to resolve through the RFC process before this gets merged?
- Do we want this?
- What about `rand`?
- Is `Entropy` naming too obscure?
- How do we deal with security vs speed tradeoff here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question here is really important, and given how "Too Much Security" can have an impact on performance/throughput, we probably need to ask which is the most common case in needing a source of randomness. There will be cases needing more cryptographically secure sources. But for randomising damage rolls, etc, there won't really be a need for something super secure, just random enough and fast. If more use-cases favour needing fast sources than secure sources, the default should be biased towards fast, and secure sources could be an optional feature to be enabled when needed. The random/entropy crate should be well documented and put this on the fore-front about such differences/needs.

Copy link
Author

@LegNeato LegNeato Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is merely the initial entropy pool. So for apps that don't need secure randomness, there is one initial slower call and that can be used to seed a faster rng (likely at startup). Then again, it could be an enormous perf footgun if someone does the obvious thing and just uses one pool. I'm not sure if I want a more common perf footgun or a less common security footgun 🤔.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I basically created bevy_turborand to explore this, by exposing various Rng structs while making them threadsafe. There's a GlobalRng as a resource to be used for the explicit purpose of seeding RngComponents. As long as GlobalRng is given a specific seed, then everything becomes deterministic from the source.

For non-deterministic Rng, I use instead thread_local Rngs initialised with a generate entropy function, and seed new Rng instances from that. Basically same mechanism as with fastrand does.

The end result of this should be Rngs that are seeded very quickly and with deterministically derived random states. At the moment, it's just WyRand so nothing that is cryptographically secure. But I hope to add ChaCha8 eventually and maybe play around with buffering so to increase throughput as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! That is similar to my vision of where this should go in future rfcs. The fact that most engines in the industry provide rng's means bevy should too, and this building block is a lower level layer that folks can drop down to if they need to customize anything. But it keeps the ecosystem potentially deterministic if both the baked in rngs and any plugin rngs decend from the same seed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I am not sure a thread local RNG is the right grouping fwiw...I like the idea of tying to systems more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rand and fastrand both have mechanisms to create an rng instance from a thread-local seed. This mechanism I would describe as non-deterministic because the intent is to provide as random state (and since these rngs are not threadsafe, they are provided in a thread local state). Which is fine for a lot of cases.

Now, one might want that globally, if given no seed, you derive a random seed (which can be generated in the above manner), and this would be how the GlobalRng gets initialised. From there, every other RngComponent gets its own seeded state deterministically from GlobalRng. So only one point needed to insert randomness while everything else is then deterministic.

- Is this the right level of abstraction? Lower? Higher?
- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC?
- Built-in higher-level randomness APIs
- Entropy per-system rather than per-world

## Future possibilities

- Higher-level randomness APIs
- Entropy per-system rather than per-world