Loading...
Forty-three programs are descended from the 1997 release of DOOM's source code. Measure only their shape and you can still tell who copied whom, and which translations kept the original blueprint.
Strip a program of every name, every comment, every line of readable text. What's left is its shape: how long each function is, how often it branches, which parts of the program call which. That shape, it turns out, works like a fingerprint.
DOOM is the perfect place to test it. When id Software released the game's source code in 1997 (the version known as linuxdoom), programmers started copying it, porting it and rebuilding it, and they have not stopped. We gathered 43 of those descendants: faithful copies, souped-up source ports, and translations into about twenty languages, from Go and Scheme to Fortran, COBOL and even SQL. Then we fed them to a code-measuring engine that never reads the code, and asked it where each one came from.
First, the old-fashioned way. Before trusting a machine that never reads code, we read it: we compared every descendant's source text with the original's, function by function. That gives the ground truth for everything that follows. Then, in the next part, we throw the text away.
Start with the obvious way to trace a program's ancestry: look at what its functions are called. It does not work as well as you'd think. Chocolate Doom, whose whole mission is an accurate reproduction of the original "including bugs", keeps 87% of the original's function names, but only 35% of those functions still contain the same code. Doom Retro keeps 69% of the names and just 0.2% of the code. GZDoom, a heavily modernised engine, keeps 32% of the names and 0.2% of the code.
Chocolate Doom keeps 87% of DOOM's function names, and 35% of the code inside them.
Averages hide the fun part, which is following one function through the whole family. The original has 662 functions big enough to score. Pick one below and see what became of it in every descendant.
Pick a famous one, or type any of the original's 662 functions. Each tile is one descendant; its colour is what became of that function there. Tap a tile to open that descendant at the bottom of the page.
So skip the names. The engine we used records, for every function, dozens of measurements: its length, its branches, its inputs, what it touches, and which other functions it calls. A companion tool then hunts for "look-alikes": functions in a descendant whose measurements match one of the original's too closely to be a coincidence. It never sees the code or the names.
Then we checked its work against the actual source text. 93% of the look-alikes it found really are copies, and it caught about two-thirds of all the copies the text comparison found.
The real test is what happens with programs that have nothing to do with DOOM. Quake and Quake II, id Software's own later games, and the Lua programming language are all written in C, like DOOM. They share between 0 and 0.6% of DOOM's shape. Chocolate Doom shares 31%. And the fingerprint is not tuned to DOOM: Quake and Quake II, which really are related, share 12.7% with each other.
Put those fingerprints together with each project's own account of where it came from and you can draw the whole family, the way biologists draw an evolutionary tree. One honest difference: a biologist infers who descends from whom. Here the family links are what each project says about itself; only the branch lengths are measured.
Drawn like an evolutionary tree: short branch = close copy, long branch = little code in common. Branch length is one minus the code shape a descendant shares with its parent. Who descends from whom is as each project states it, not inferred; the lengths are measured. Translations rewrite every line, so they all reach the far edge, even the ones that kept DOOM's design; that is the next part of the story. Hover a name for its numbers; click to open it below.
Swipe sideways to see the whole tree.
Translations are the hard case. Rewrite DOOM in Go and every line changes, every function gets a new name, and the look-alike hunt finds nothing. So what, if anything, survives?
We tried a blind test. DOOM's code is organised into 16 parts: the renderer, the monsters and physics, the sound, the status bar and so on. We hid every name and file path in a translation and asked the engine to sort each function into one of those parts using only its shape and who it calls. Then we checked where the names say it belongs.
For the Go translation, the engine sorts 60% of the functions correctly; shuffled labels manage 20%. Among the quarter of functions it was most sure about, it was right 99% of the time. Scramble the Go program's wiring, keeping how many calls each function makes, and it drops to 39%: the wiring itself carries much of the signal.
Hide every name, and a Go port's functions still land in the right part of DOOM 60% of the time. Guessing manages 20%.
If the wiring carries the signal, look at the wiring directly. For every descendant we drew a map of which parts of the game call which, and compared it with the original's map. To know how much of the match is real, we also scored each program against itself with its functions shuffled at random between the parts. The gap between the two is the design that survived.
The Scheme and Go translations keep the original's wiring as far above chance as Chocolate Doom does. The Dart translation keeps some. The Fortran, Java, Kotlin and C# translations keep none, and programs written from scratch, or not related to DOOM at all, sit at chance, as they should. Don't take our word for it: draw the maps yourself.
Each circle is a part of the game, placed where the original's wiring puts it; each line is calls between two parts, thicker for more. A descendant wired like the original draws the same lines. Pick one.
Every descendant has its own story: when it started, how big it grew, which famous functions it kept, how far it drifted year by year. Pick one from the list, or compare any two.
Loading...
Loading...
Written by Joe Esquibel, using the GitGalaxy code-scanning engine.
Every number on this page comes from public code and can be rebuilt from it. Here is everything you need to examine the data and the methods, or to run them yourself.
Found something that doesn't add up? Open an issue on the GitGalaxy repository.