IJobs in Unity, with Random Number Generation

To provide background, I’ve been working on a project lately, involving generating random terrain in Unity based on a combination of Worley noises and Voronoi noises (cell noise), with a few matrix-based operations in them. This is a logically straightforward task (given that you understand how your noises work!) but ultimately very computationally expensive.

If I was using something like my-main-man C, I wouldn’t be so concerned; but Unity requires C#, which is both a blessing and a curse. C# is managed code, which makes it much harder to slip and have a memory leak. It’s also a mostly1 well-maintained language and is relatively easy to learn, without being abstracted to a crippling point.

That is, unless you’re doing something very close to the hardware, or very computationally intensive. The truth is, managing code comes with a price. If I were using C or Assembly, my code would be moving (literally) a hundred times faster (in most cases). Breakneck speeds through a lattice of severe safety pitfalls. That’s not usually a big deal, as modern processors are blisteringly fast and programming is not an easy skill—sometimes it’s more economical to have the job done fast than it is to have the completed job run in a fraction of the time, especially if you’re only saving milliseconds.

It’s my personal opinion that object-oriented programming (OOP), for all its legitimate benefits elsewhere, has contributed exceedingly little to the multimedia (and game development) scene. It has a tendency to turn people into champion office-software programmers, when loop-driven programs like games follow a completely different set of rules. Object pooling, as an example, and data-driven code, become foreign concepts, when they can make all of the difference in the world!

So, enter ECS. ECS stands for the Entity Component System, which is, I think, a very vague and obfuscated name for it. It involves data-driven development instead of object-oriented programming. Enter the Unity Jobs system, which we’ll be discussing here. Also enter the Burst Compiler, an equally poorly (but at least fashionably) named extension to Unity. All of them get around these obstructions in some way, without having to part from the comfort of C#—at least, no more than is strictly necessary.

Jobs bring in a specific new feature. Unity is a single-core program, in other words, it uses only a single processor thread. This isn’t unique to it, the vast majority of programs are single-core. You may be working with a 4.0 GHz Ryzen processor with sixteen threads, but an individual program usually only uses one. Exceptions typically include web browsers—Chrome tries to keep a different thread for each tab, as I understand it—and programs which are attempting to perform many tasks simultaneously, like some renderers.

The drawback of this is that, in classic Unity, every line of code you type ends up being queued on the same thread, henceforth the “main thread”. You can’t run more than one instruction at once, you’re just running them blisteringly fast. Since we’re usually still going around 120 frames per second, that isn’t typically a problem, until you have an extreme operation that does require more time than the main thread can spare.

This is where the job system enters. Jobs, unlike their ancestral coroutines, can run on their own thread. When accelerated with the Burst compiler, they can run exceedingly fast. (Remember that hundred-fold factor I was talking about? Here it is.) Additionally, unlike when using C, you can readily compile to any platform supported by Unity with minimal work.

So the naive thought from here is, why don’t we use it on everything? Well, therein lies the catch—only certain data types can be used, specifically blittable data types. Blitting is short for block transferring (just as bit was short for binary digit—programmers love these things). It means that a streamed chunk of data of this type can be copied, en mass, from lower memory to processor caches. Since, compared to the speed of your processor, your memory is wrapped in architectural red tape and moving at a snail’s pace, this is critical stuff.

Non-blittable types are typically pointers to locations in memory where the actual data is. Sometimes, they’re pointers to lists of pointers. There’s a lot of he-said-she-said reference tracing when using them, but they open up a lot of new possibilities like OOP in their use; and I’m not bad-mouthing OOP, I’m just saying where it’s out of place. If you only need a few of them, then that’s not a problem. If you need a bunch of them, even hundreds or thousands or even millions, then it’s going to cripple your operation.

Blittable types are, in the context of C#, defined as types that have the same representation in managed and unmanaged memory. The program’s marshaller, or the module that converts types between managed and unmanaged memory, does not touch them. That’s about as close to the hardware as I’ve seen anyone get with C#. It also means that they can be block-transferred to a new thread.

Threads, by design, cannot know what each other are doing. This is part of where the speed-up from using multiple threads comes from. Because of this, following reference (non-blittable) types becomes very difficult. By ensuring that every type is a blittable type, and all are present on the thread before it runs, we avoid this; in fact it’s required for many parts of the Unity Job System and Burst Compiler.

Blittable Types List
System.Byte
System.SByte
System.Int16
System.UInt16
System.Int32
System.UInt32
System.Int64
System.UInt64
System.IntPtr
System.UIntPtr
System.Single
System.Double

Additionally, there are functions which cannot be called from these jobs, some of which are surprisingly useful to have around. While many, such as Monobehaviour, are unsurprising and not that impeding, we unfortunately also lose access to virtually all parts of the UnityEngine namespace, including Random.

In my case, I was attempting to seed the random number generator with a coordinate location’s hash value, and then acquire feature points from it. Since that is no longer possible, we can follow the easiest and most controllable solution and simply roll our own LCG (linear congruential generator), a type of random number generator!

Inside your job—and that’s important, it needs to be local to it—feel free to insert this function and definition.

private const uint RAND_MAX = ((1U << 31) - 1);
uint LcgSeed;

private float rand()
{
	return (float)(LcgSeed = (LcgSeed * 1103515245 + 12345) & RAND_MAX)/RAND_MAX;
}

This is technically a word-for-world (aside from C# translation) copy of the BSD random number generator, which is an LCG where multiplier a = 1103515245, increment c = 12345, and modulus m = 231. It’s converted to a floating-point (Single) and then divided by its modulus so we can get a value between zero and one, which is what I needed. It’s a BSD license and is very well known.

Let’s break down that return line for clarity, as it’s a little complicated.

We’ll start with LcgSeed = . That is an assignment, but the assignment operator has a useful side effect of returning the assigned value. We need it, because we want LcgSeed to update with each random value, to prevent repeats. What it’s set to be equal to, (LcgSeed * 1103515245 + 12345) & RAND_MAX, is the basic LCG equation with parameters from BSD. It’s finally divided by its maximum possible value in an effort to limit it to, for me, the unit cube; but there are many occasions when a random value between zero and unity is of value!

Replace your typical InitState call, which is part of the UnityEngine package and inaccessible from the job, with a simple assignment to LcgSeed. Since I’m generating vectors which ideally need to be identical for each call at adjacent locations, I just use the Vector3’s hash value; but that’s much more complicated than the rest of this and will likely be addressed in a book, maybe cursorily on the website.

Remember that you will need to initialize LcgSeed before you call it, as another side effect of IJob, on account of it being a struct instead of a class, is that it can’t have initializers in field declarations. However, since in many cases we don’t want a truly random value so much as a one-way hash from a location, this is almost an advantage.

Replace each Job-internal call to Random.value with rand(), or your variation on it, and you will do just fine for both performance and noise generation on an IJob.

1There’s some concern about dirtying the language by introducing SQL syntax, which also bothers me a bit; but this is optional and relatively small.

Published by Michael Macha

I'm a game developer for both mobile and PC. My education is in physics, journalism, and neuroscience. Founder and CEO of Frontier Medicine Entertainment, located in the beautiful city of Santa Fe, New Mexico.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: