
The random number generator of DOOM - danshapiro
https://github.com/id-Software/DOOM/blob/master/linuxdoom-1.10/m_random.c
======
perlin
In 1982 (several years before DOOM), Ken Perlin invented an algorithm meant to
produce random numbers that more closely resembled a human's interpretation of
"random" numbers (versus those generated by computers). Humans, it seemed,
tended to choose a lot of different numbers, whereas random numbers from a CPU
actually produced lots of repeating sequences. His work in the field was vital
to developing the first 3D shaded graphics in a Hollywood film (Tron), for
which he won an Academy Award for Technical Achievement.

Perlin Noise is still used to this day for generating clouds, natural-looking
terrains, and other textures that are pleasing to the human brain.

Python implementation:
[https://github.com/caseman/noise/blob/master/perlin.py](https://github.com/caseman/noise/blob/master/perlin.py)

Excellent talk by its creator:
[http://www.noisemachine.com/talk1/](http://www.noisemachine.com/talk1/)

~~~
tvmalsv
I remember many years ago the iTunes player had to include an option for
"humanizing" the randomness of their shuffle play (or whatever they called
it).

Turns out that good randomness didn't seem random to people. They might hear
three songs in a row from the same album and think "that's not right, it
should be random!" Of course, humans often have a different idea of what
"random" is.

~~~
yiyus
What I expect from a music player is to play all my songs in random order,
such that once a song has been played, it is not picked again until all songs
have been played.

There is a difference between randomly picking an element from a list and
reordering a list in a random order. This is not a matter of how random really
are the random numbers.

~~~
SmellyGeekBoy
Hence the term "shuffle" rather than "random" I suppose

------
pdw
There's enough old Softdisk/id Software code released that you can trace the
evolution of this RNG.

First, check out Catacomb (1989). I think this is the oldest of Carmack's
games for which source code has been released. This game uses a lagged
fibonacci generator:

[https://github.com/FlatRockSoft/Catacomb/blob/master/CATASM....](https://github.com/FlatRockSoft/Catacomb/blob/master/CATASM.ASM#L397)

This makes sense, it's a generator that requires little memory and no
expensive operations such as multiplications. You'll probably also find it in
Softdisk's Apple II releases.

Carmack's first 3D game is Hovertank 3D, released in 1991. The LFG still
exists, but now a "table-based RND generator" also appears. This seems to be
the first appearance of the "Doom RNG".

[https://github.com/FlatRockSoft/Hovertank3D/blob/master/IDAS...](https://github.com/FlatRockSoft/Hovertank3D/blob/master/IDASM.ASM#L698)

The LFG is used when setting up the map, while the table-based is used for
enemy AI during gameplay. Obviously every cycle counts when trying to do 3D on
a 286.

The same year also saw the release of Keen Dreams, in which only the table-
based RNG survives:

[https://github.com/keendreams/keen/blob/master/id_us_a.asm](https://github.com/keendreams/keen/blob/master/id_us_a.asm)

(The state variables of the LFG are still defined, but the code is missing.)

~~~
tmoertel
That's a fascinating history. Thanks for tracing it back.

According to a comment in the Catacomb source, the LFG logic was derived from
a macro supplied with the Merlin assembler for Apple 2 GS computers:

    
    
        ;this routine was converted from
        ;the Random macro on Merlin GS

------
paulannesley
Related:
[http://jmtd.net/log/deterministic_doom/](http://jmtd.net/log/deterministic_doom/)

Via:
[https://news.ycombinator.com/item?id=9429889](https://news.ycombinator.com/item?id=9429889)

~~~
naugtur
Haha, just wanted to link that. To draw people's attention: this article
explores results of replacing this array of numbers with predictable values.

------
pgy
There was a post about tampering with randomness in Doom some time ago with
some interesting hn comments explaining the reason for this design:
[https://news.ycombinator.com/item?id=9429889](https://news.ycombinator.com/item?id=9429889)

------
GaiusCoffee
I hope I don't sound too dumb, but how does it work, exactly? It doesn't look
random to me, more like an unordered (but repeating) sequence..

~~~
veddox
It is basically just that :-)

What I don't understand is the following line:

    
    
        prndindex = (prndindex+1)&0xff;
    

(I'm not a C programmer.) AFAIK, the &0xff is a pointer - but what precisely
does it do?

~~~
barsonme
It's not a pointer, it's a bitwise AND.

It's essentially the same[0] as `prndindex = (prndinex+1) % 256`, except
bitwise AND takes less effort to compute.

Modulo is essentially[1]: `mod = a - b * (a / b)` unless the compiler can
manage to use bitwise shifting/masks instead.

Back when Doom was popular, the bitmask would have been much more efficient.

[0]
[http://stackoverflow.com/q/3072665/2967113](http://stackoverflow.com/q/3072665/2967113)
[1] I'm not 100% positive that's how it's implemented with an IDIV
instruction.

~~~
derf_
Any reasonable compiler will implement x%256 using bitwise arithmetic, but C
semantics require that x == d*(x/d) + (x%d), which means that if x is negative
(x%256) must be negative, while (x&0xff) will be positive.

This difference means that the AND is still faster than the MOD when used on
signed integers, if the compiler is not smart enough to prove that the input
will never be negative.

For example, with gcc 4.7.3 the resulting assembly is

    
    
      movl %edi, %edx
      sarl $31, %edx
      shrl $24, %edx
      leal (%rdi, %rdx), %eax
      andl $255, %eax
      subl %edx, %eax
    

I.e., six instructions instead of one. This is still faster than using a
divide.

------
sytelus
This is great RNG for games although most computer scientists would strongly
disagree. We focus so heavily on having huge cycle length for RNG that we
forget that human memory is not all that great at remembering exact sequence
of even just handful of random numbers, let alone 255 of them. So you don't
need RNGs with guarantees of huge cycles for gaming scenarios like adding
error in to projectile's path. The advantage of this RNG is that it's
blazingly fast (think all the cache hits!). Of course this would be useless
for any real work on physics simulation or weather simulation etc.

------
tbrake
Curious. Just eyeballing I see there's no 1, 2 and 222 are repeated as well as
36 and 136. There are probably more.

If there's a purpose behind the table being constructed with those
omissions/repetitions it's lost on me.

~~~
Zikes
Maybe they were chosen at random?

~~~
tbrake
Looks like it by the note in the other comment by serf.

I had begun to wonder if certain values caused issues when used in code and it
was deemed easier to just not get certain values back.

That's crazy talk though.

~~~
detaro
Naa, that sounds like something game developers totally would do.

------
megablast
random_number = 4// just tested it with a dice

~~~
oneeyedpigeon
For anyone missing the reference, here's the obligatory xkcd:
[https://xkcd.com/221/](https://xkcd.com/221/)

------
frontfor
Interestingly, Half-Life (released in 1998) uses a slightly more complex
table-based generator:

[https://github.com/ValveSoftware/halflife/blob/master/dlls/u...](https://github.com/ValveSoftware/halflife/blob/master/dlls/util.cpp)

Note that this is only used for certain parts of the game. For others it uses
a closed source non-table based generator.

------
TomGullen
Interested as to why this method was picked over say doing "MSPlayed % 256"

~~~
deathanatos
Assuming by "MSPlayed" you mean "milliseconds played";

Another poster[1] links to a Doom wiki article[2], which explains,

> The reason for the existence of two individual indexes is to maintain
> multiplayer synchronisation

If you sync the PRNG's state at the beginning of the game, you can simply
transmit the other client's input (such as keyboard/mouse): since each PRNG is
in the same state on all the players' machines, you don't need to distribute
over the network the results of PRNG choices: each client can just compute it
locally. So long as all the code uses the PRNG's output stream for the same
purposes, in the same order, everything is deterministic (while appearing to
be random).

Milliseconds played would also likely require some sort of OS/hardware
interaction, to get to a timer. This is an add, an AND, and a memory lookup:
likely significantly quicker (bear in mind the hardware Doom was created
with/for). (Perhaps there are cycle counts, but I'm not sure that instruction
was a thing yet? Though it was more reliable then…)

Also see this post:
[https://news.ycombinator.com/item?id=9429889](https://news.ycombinator.com/item?id=9429889)

[1]:
[https://news.ycombinator.com/item?id=9810073](https://news.ycombinator.com/item?id=9810073)

[2]:
[http://doomwiki.org/wiki/Pseudorandom_number_generator](http://doomwiki.org/wiki/Pseudorandom_number_generator)

~~~
TomGullen
That makes sense, thanks!

------
userbinator
That seems like a very unusual (and large) way to produce a pseudorandom
sequence. Why not a linear congruential generator or LFSR? They have
approximately the same state storage requirements, but much less static data.

~~~
paulannesley
Presumably they were optimizing for CPU cycles rather than RAM, and the 256
bytes that it does consume isn't much in the scheme of a game like Doom.

This must be pretty fast (and compact) code: rndindex = (rndindex+1)&0xff;
return rndtable[rndindex];

The example C code at
[https://en.wikipedia.org/wiki/Linear_feedback_shift_register](https://en.wikipedia.org/wiki/Linear_feedback_shift_register)
looks like it would compile to more bytes, and require more CPU cycles to
execute.

Based on
[https://en.wikipedia.org/wiki/Linear_congruential_generator](https://en.wikipedia.org/wiki/Linear_congruential_generator)
it seems LCGs also micro-optimize for memory rather than CPU performance.

Doom was probably squeezing every last cycle out of 386 @ 25 MHz machines to
render frames, but they did have at least 4MB–8MB RAM; 256 bytes seems a good
trade-off.

------
kdrakon
For a video game, I suppose it was random 'enough'.

------
vortico
What does the P and M stand for in the function names?

~~~
mahouse
P for "Game logic/behaviour", M for "Miscellaneous".

[http://doomwiki.org/wiki/Doom_source_code](http://doomwiki.org/wiki/Doom_source_code)

------
i_have_to_speak
Mandatory xkcd mention: [https://xkcd.com/221/](https://xkcd.com/221/)

------
xiaq
If you realize that any RNG on [0,256) always has a cycle of at most 256, this
is actually pretty decent.

~~~
eterm
Is that true?

Imagine I want to sample [0,2), then I can use the following table:
[0,0,0,1,1,0,1,1]. This has cycle length 8. Much longer cycles can be
obtained, it just requires the state of the prng to be larger than range being
sampled.

~~~
deathanatos
> Is that true?

It is not, at least not the way you (and me) are interpreting it. A RNG's
period is bounded by the number of states it has, internally[1]:

> The period is bounded by the number of the states

> If a PRNG's internal state contains n bits, its period can be no longer than
> 2 ^ n results, and may be much shorter.

Our state in the Doom generator is the variable rndindex (or prndindex; we
have two generators here); rndindex holds an index in [0, 256) (it is 8 bits),
hence we have at most that many states (2 ^ 8, or 256). (I'm assuming the
table does not repeat itself, as that would be silly, so in this case, the
period is exactly 256 outputs long.)

[1]:
[https://en.wikipedia.org/wiki/Pseudorandom_number_generator#...](https://en.wikipedia.org/wiki/Pseudorandom_number_generator#Periodicity)

~~~
xiaq
Thanks for pointing this out. I have confused the size of output with the size
of state.

------
netheril96
Yet another random number generator that depends on global mutable state and
therefore unsafe to be used in multithreaded context.

~~~
pjc50
DOOM was, of course, single-threaded and built to run on the single-core CPUs
of the time.

~~~
netheril96
People should not get into the habit of depending on global mutable state.
What if the project evolves and now going to work multithreaded? What if parts
of the project are extracted to be used in another one? What if a toy project
grows to be massive?

I say this because I have investigated many crypto libraries for usage and
nearly every one of them (especially openssl) have random generators that
depend on global state. The attitude of not caring about correctness and
thread safety is just pervasive.

~~~
Tepix
You do realize this code is around 20 years old?

~~~
netheril96
I did not. Perhaps I was being too harsh to criticize this particular project.

~~~
ionised
Have you really never heard of DOOM or know how old it is?

I would have thought it was quite well known among the tech crowd, even non-
gamers.

~~~
pjc50
Doom was released in 1993. I'm guessing that netheril96 wasn't made until 3
years later.

Tech is a very ahistorical discipline. We don't look back at the "old
masters". We reinvent things every generation (or in Javascript land, every
week).

~~~
stcredzero
As Alan Kay said, Programming is "almost a field" because it forgets its own
history. As the now-typical HN commenter says, "Who is Alan Kay?"

~~~
ralphw_oz
That's truly sad. And ironic.

