Doom’s 386 Trick: The Lie That Made 3D Run Without a GPU


Oct 12th '26 1:38pm:
Doom’s 386 Trick: The Lie That Made 3D Run Without a GPU







Written by:[jomifisi](https://comuniq.xyz/profile?u=jomifisi_17) <br>Doom shipped on December 10, 1993, and its official floor was a 386 running at 33 MHz with 4 MB of RAM and a plain VGA card. No 3D accelerator, no GPU in any sense we'd use today, because none existed for consumers yet. The first mainstream one, 3dfx's Voodoo, was almost three years away. A processor that struggled with a large spreadsheet was pushing lit, textured corridors full of monsters at a playable speed. It wasn't magic. It was a carefully curated lie about what 3D is, and the lie is worth understanding in detail. ## The Budget: What a 386 Actually Had to Spend Start with the hardware, because the hardware is the whole story. - **CPU:** Intel 386DX at 33 MHz, 32-bit, with no built-in floating-point unit. The 387 coprocessor was an optional extra that almost nobody had in a home machine. - **RAM:** 4 MB minimum, and plenty of people were running exactly that. - **Video:** VGA in Mode 13h, 320×200 pixels, 256 colors from a palette. That is 64,000 bytes per frame. - **Timing:** game logic ran at 35 "tics" per second, decoupled from how fast frames were drawn. - **Graphics hardware:** a dumb framebuffer. The VGA card stored pixels and sent them to the monitor. Every single pixel was calculated by the CPU. Now the back-of-envelope math, which is rough and mine, not id's. A 386DX-33 delivered somewhere around 8 million instructions per second, give or take. A full 35 frames per second at 320×200 means about 2.24 million pixels per second. That leaves **three or four instructions per pixel**, before you've moved a single monster, checked a single collision or played a single sound. Nobody was hitting 35 fps on that chip at full screen, and the reason is visible right there in the arithmetic. So the real question isn't "how did Doom run on a 386." It's how you get a convincing 3D picture when your entire per-pixel budget is smaller than the instruction count of a texture lookup in a naive renderer. Two comparisons frame the answer. Ultima Underworld, from March 1992, did real texture-mapped 3D with slopes, room-over-room spaces and the ability to look up and down. It was impressive and it was slow, and players routinely shrank the view window to get a tolerable frame rate. Quake, in 1996, went to full polygonal 3D and leaned on the floating-point unit of the Pentium to make perspective-correct texturing affordable. Doom sits precisely between them: more ambitious than anything a 286-class engine could do, far more restrained than either of those. That restraint was the design. ## Wolfenstein's Ray Caster Was Already a Cheat Id's previous game, Wolfenstein 3D (May 1992), used ray casting. For each of the 320 columns on screen, the engine shot one ray into a map made of a rigid grid, found the first wall it hit, and drew a vertical strip scaled by distance. Every wall was the same height. Every wall met at right angles. Floors and ceilings were flat colors, because texturing them would have cost too much. It worked brilliantly on slow machines precisely because it refused to do most of what "3D" implies. Doom wanted more: walls at any angle, floors and ceilings at different heights, textured surfaces everywhere, light that faded with distance. Throwing a ray per column at arbitrary geometry would have eaten the budget whole. John Carmack needed a way to know what was visible *without searching for it every frame*. ## The BSP Tree: Doing the Sorting Before the Game Starts His answer was binary space partitioning, an idea from computer graphics research in the early 1980s by Henry Fuchs, Zvi Kedem and Bruce Naylor. The principle is simple to say and clever to exploit. Take a level, pick a wall, and extend it into an infinite line. Everything in the level is now either in front of that line or behind it. Split the level in two, then repeat for each half, and keep going until the pieces are small convex chunks. You've built a tree. The expensive part, building that tree, happened offline. Doom's levels were designed on NeXT workstations, and a separate node-building tool ran the partitioning before the map ever reached a player's PC. At runtime, the engine only had to walk the tree, and the walk answered a question that otherwise costs a fortune: *in what order should I draw things so nearer geometry covers farther geometry?* Drawing front to back meant that once a column of the screen was filled by a wall, the engine could ignore anything behind it for that column. No depth buffer, no sorting pass, no wasted overdraw. The renderer kept a running record of which screen columns were already "closed," and the tree traversal handed it surfaces in an order that made those closures happen as early as possible. That is the real inheritance from Wolfenstein: the same instinct to do less per frame. But it had moved from "limit what exists" to "precompute everything you can, so the console of a 386 only spends cycles on what the player can see." ## Columns and Rows: The Geometry Rule That Makes Texturing Cheap Here is the part most explanations skip, and it's the one I find most elegant. Texture mapping in true 3D is expensive because, in perspective, texture coordinates don't change linearly across a screen surface. Doing it correctly requires a division per pixel, and divisions on a 386 were slow enough to ruin your afternoon. Doom sidestepped this with a rule baked into the level format. **Walls are always vertical. Floors and ceilings are always horizontal.** From that rule comes a mathematical gift: along a vertical wall column on screen, distance from the camera is constant, so the texture can be sampled at a fixed step with no per-pixel division. Along a horizontal floor row, the same is true. The engine draws walls as vertical columns and floors and ceilings as horizontal spans, and in both cases it is doing cheap linear interpolation that happens to be perspective-correct, because the geometry was constrained to make it so. Texture data was even stored to help. Wall textures were laid out in column order in memory, so drawing a column meant reading sequential bytes. The inner loops, the routines that actually painted columns and spans, were tight enough to hand-tune in assembly. This is where most of the frame time lived, and where id spent its optimization effort. The cost of the rule is the thing people notice only after they've played enough Doom: you cannot build a bridge over a room. No sloped floors in the original. No true looking up and down. Two rooms can never occupy the same footprint on different floors. What felt like a three-dimensional world was a very detailed floor plan with variable heights, and the designers (Sandy Petersen and John Romero most famously) learned to hide that fact with clever layouts. ## Fixed-Point Math, Lookup Tables and the Inner Loop With no floating-point unit to count on, Doom did its arithmetic in **fixed point**, using 16.16 format: the top 16 bits of a 32-bit integer held the whole part, the bottom 16 the fraction. Integer instructions were what the 386 was good at, so everything from player position to wall distance lived there. Trigonometry was handled the old way: tables. Sines and tangents were precomputed and looked up by angle index. Light worked the same way. The game used a colormap that mapped each of the 256 palette colors to a darker version at each of 32 brightness levels, so shading a pixel by distance became one table read instead of any arithmetic on color. Damage flashes and item pickups swapped the whole palette, which cost almost nothing. The code ran in protected mode through the DOS/4GW extender, which gave 32-bit flat memory addressing and escaped the 640 KB real-mode box that had strangled earlier PC games. The compiler was Watcom C, with assembly in the hot paths. One more saving is easy to miss. Doom never cleared the screen between frames, because it assumed every pixel would be overwritten. That is a time-saver. It is also the origin of a famous glitch, which brings us to what the failures tell us. ## What Doom's Limits Reveal ### The failures that gave the trick away If you played enough Doom or its custom maps, you saw the machinery crack in specific, repeatable ways. The **hall of mirrors** effect appeared when a map had a hole the renderer didn't cover. Since the screen wasn't cleared, whatever was in the buffer smeared across the void, repeating the last frame's pixels in endless trails. It looked like a bug and was really a consequence of a deliberate saving. Then there were the hard limits. The renderer used fixed-size arrays to avoid allocating memory mid-frame, and mapmakers hitting them got the dreaded **"R_FindPlane: no more visplanes"** error, a crash caused by a map having more than 128 distinct floor or ceiling surfaces visible at once. Players and level designers learned the numbers. If you built a big, open, richly varied room, you had to count visplanes the way a sailor watches ballast. Every limit was a trade: predictable speed in exchange for a ceiling on complexity. Monsters gave the game away too. Enemies weren't 3D models but flat sprites with a handful of pre-drawn rotations, which is why they pivot in visible steps as you circle them. Nobody minded, because the sprites sold the fiction and cost a fraction of what polygons would have. ### What 386 owners did, and what I think it proves The actual people on 386s got through by shrinking the window. The game had a screen-size control that made the rendered area smaller, and a **low detail** mode that drew at half horizontal resolution, doubling pixels sideways to cut the fill cost. Players on a 386SX, whose external bus was only 16 bits wide, had it worst. Players on a 386DX at 33 MHz could get a playable game in a reduced window, usually somewhere short of the full 35 frames per second. The 486 DX2-66 became the machine people remembered because it finally gave the full-screen experience without sacrifice. Here is my read, for what it's worth. The usual story says Doom ran on weak hardware *despite* its limits. I think that gets it backwards. The limits were the engine. The flat floors, the vertical walls, the offline tree, the fixed arrays: each was a rule that deleted an entire category of work. When people praise Carmack's code, they usually mean the clever algorithms. I'd say the real skill was deciding what the game would never do, and building a world in which that never mattered. It also explains why Doom keeps getting ported to calculators, cameras and printers. A renderer built around "do almost nothing per pixel" runs anywhere there's a CPU and a frame buffer. The 386 wasn't a lucky target. It was the design target. ## The Part That Doesn't Resolve Id's own engines moved on quickly. Quake dropped the vertical-wall constraint in 1996 and went to real 3D, and within a year or two dedicated hardware made all this cleverness look like a historical curiosity. Every modern engine now throws orders of magnitude more compute at a frame than Doom had in a year of 386 time. And yet the interesting design decisions in games still come from scarcity. Constraint made Doom's levels feel deliberate, its pacing tight, its look unmistakable. So here is the question I can't settle: if Carmack had had a GPU on his desk in 1993, would Doom have been better, or just less strange?