Ants Projects Hub ← All projects

Early WIP · forked from id-Software/DOOM

DOOM Ants

Early WIPWINLNXLatest: v0.6.0
The classic DOOM title screen with the New Game menu, rendered by DOOM AntsClassic DOOM gameplay in a widescreen view — a marine facing a computer-room level over a blue floorThe same DOOM level with hardware ray tracing — realistic shadows and dynamic lighting darkening the room

DOOM_Ants

A modern, ray-traced reimagining of id Software's DOOM, built on the original GPL source code.

Still plays like DOOM — just with the lights turned on.

Read the full guideShow less

What this is

In 1997 id Software released the source code to DOOM. It's a brilliant piece of 1990s engineering: it fakes 3D with a clever 2D trick and draws every pixel on the CPU. DOOM_Ants takes that code and modernises it in two stages:

  1. Make it run today. Get the 1997 engine compiling and playing on modern 64-bit Linux (and Windows), replacing the ancient X11 graphics / OSS sound code with SDL2. ✅ Done — the engine builds and plays.
  2. The spin. Evolve the renderer toward true 3D with hardware ray tracing (and path tracing where it's feasible), dynamic and volumetric lighting, HD materials, and a solid 60 FPS floor — without losing the feel of the original. 🚧 In progress.

See ROADMAP.md for the plan and CHANGELOG.md for what's shipped.

Status

🎮 Playable. The 1997 engine runs on modern 64-bit Linux and Windows via SDL2, and you can pick between three ways of drawing the world:

  • Classic — the original 1997 software renderer, pixel-for-pixel, now with widescreen support.
  • Solid — a hardware (Vulkan) rasteriser: the same DOOM world drawn on the GPU, with dynamic lights and contact shadows.
  • Ultra — a hardware path tracer: ray-traced lighting and shadows, HD PBR materials, a moving flashlight, ambient occlusion, and grimier, de-tiled surfaces. (Needs a ray-tracing-capable GPU; developed and tested on an AMD RX 6600.)

You switch between them from the in-game menu. Latest release: 0.6.0. Grab a build from the Releases page, or build it yourself below.

Building & running

You'll need a DOOM .wad data file (e.g. the shareware doom1.wad, or retail doom.wad / doom2.wad), which is not included here for licensing reasons.

Dependencies (Linux): a C++23 compiler (GCC/Clang), make, and the dev packages for SDL2, SDL2_mixer, and the Vulkan loader + headers. The mold linker is optional — the build uses it automatically when it's installed and falls back to the default linker when it isn't.

cd linuxdoom-1.10
make                     # builds linux/linuxxdoom
./linux/linuxxdoom -iwad /path/to/doom.wad

Handy flags: -iwad <file> picks the game data explicitly; -warp <map> jumps straight into a level. The Solid and Ultra views need a working Vulkan driver; Classic runs anywhere SDL2 does.

Windows builds are produced with a mingw-w64 cross-compile toolchain and published on the Releases page.

Licence

GPL v2, inherited from the original DOOM source. See LICENSE.TXT. Original code © id Software; see README.TXT for the 1997 release notes.

Added

  • A Render Effects screen that shows what every graphics toggle is set to (DOOM-0205) Options -> Renderer -> Render Effects lists the flashlight, SSAO, de-tile, dirt/grime, wet liquid, volumetric fog and the profiler, each with its current setting beside it. The toggles were hidden behind hotkeys, so the only way to know what was on was to remember — which caused a false bug report when two effects were off in a saved config and the game looked wrong for no visible reason.

  • Menus can now be opened from the command line in developer builds, so they can be screenshotted (DOOM-0318) -devmenu <name> opens a named settings screen as the level loads — main, options, renderer, effects, video, sound or developer. Automated screenshots could already reach any view of the world but never a menu, because opening one needs a keypress and the desktop will not let a script press keys inside the game. Two long-open menu items had been waiting on someone checking them by hand for that reason.

  • -inspect and -freeze command-line flags for the developer view (DOOM-0294) The Inspect preset (monsters ignore you, nothing can hurt you) and the freeze-monsters switch were reachable only through the developer menu, so an automated capture run could not use them. A capture taken in a live level is not a measurement: an A/B of the wet-liquid layer reported 15% of pixels moved where the real signal was 13.8%, the rest being a monster walking through frame and the health counter ticking down.

  • Volumetric fog now takes its colour from the room: green over nukage, a red haze through Hell (DOOM-0011 L4) Two area profiles feed the fog march. A nukage pool fogs green, and a Hell level (Inferno, or DOOM II from map 20) gains a thin red haze over everything. They stack — a goo room on a Hell level reads green through red. Torch shafts keep their own warm colour rather than taking the room's tint. Ultra's ray-traced view only; measured at under 0.02 ms.

  • Add a make compile_commands.json target for the static analysers cppcheck and clang-tidy take a file's language from its extension, so a whole-tree sweep was parsing the 10k-line Vulkan back-end as C and analysing none of it. A compilation database gives each file the compiler, standard and include paths the real build used.

  • Drifting fog now rolls through a torch's glow and cuts it, instead of sliding behind a still patch of light (DOOM-0300) Light reaching the fog is attenuated along its own path from the lamp, so billows passing in front of a torch actually dim it. Before this the glow was a fixed shape painted over moving fog; now the two move together.

  • Torches and lava now light the fog around them (ray-traced views) (DOOM-0011) A lamp, a light panel or a pool of nukage now glows into the air near it instead of only lighting the walls. Which lights can reach which patch of air is worked out once when the level loads, using the game's own line-of-sight test, so a torch never shines through a wall and the effect costs no extra ray tracing while you play. Indoors, where there is no sky to light the mist, a nearby light is now the thing you see by.

  • Developer screenshot: F12 saves the frame you are looking at (DOOM-0294) F12 (or Developer → Capture Screenshot) writes exactly what is on screen, HUD and all, to dev-shots/shot-NNNN.png — full display resolution, in any 3D view, raster or ray-traced. The Classic tier saves DOOM's own .pcx instead, since the software renderer never builds an image for the GPU to present. Developer builds only (make DEV=1).

  • Developer view: a testing menu for jumping to any level and looking at it (DOOM-0294) An Options → Developer menu that jumps straight to any level in either game at any skill, switches the monsters' attention off (or freezes them) so a map can be walked and looked at rather than fought through, opens locked doors, picks a path-tracer diagnostic view by name, and prints where you are standing as the -warpto line that reproduces the spot. A Play/Inspect switch flips the whole set at once, so the same jump can also be used to test a level as a player meets it. Developer builds only (make DEV=1): a released binary is compiled without any of it.

  • Fog now billows: a near-white base with banks of visibly varying thickness drifting past (Ultra/Solid ray-traced view) (DOOM-0011) The haze is no longer even. Two layers of slowly drifting 3-D noise thicken and thin the air, so mist pools in one part of a room and clears in another, and a bank passes in front of and behind a pillar as you turn. Costs about 6% of frame time on an RX 6600; the ; key still dials it Off / Low / Medium / High.

  • Fog now drifts indoors through doorways and windows (DOOM-0011) Indoor fog used to stop dead at a roofline. It now fades in near an opening onto the outdoors and thins as you walk deeper in, while a sealed room that merely shares a wall with the outside stays clear — the game works out the route through doorways, not the straight line through the wall.

  • Floor fog outdoors — mist that pools around your feet (DOOM-0272) A second fog layer that fades with distance from the camera as well as with height, so the air at your feet can be genuinely misty without the far end of a courtyard turning white. Outdoors only for now; indoor rooms follow once the engine can tell a room with a window from a room buried three doors deep.

  • Test coverage for the mus2mid happy path, font metrics, asset-path resolution and degenerate emitter input mus2mid_test asserted only rejection paths; it now checks three hand-built scores byte for byte, covering note on/off encoding, the track-length back-patch, percussion-channel mapping, the DMX 0x80 clamp quirk and the MIDI-channel allocator's skip of channel 9. Also new: rb_text vertical metrics and the atlas-doubling retry, rb_materials' rb_asset_root/rb_asset_path joining, and emissive_derive's null/zero-area guard.

  • Headless boot-smoke CI gate that actually runs the engine (DOOM-0203) A new -bootsmoke [N] engine flag boots the game on the GPU-free software renderer, simulates N tics (default ~3 s) through the real game loop, and exits cleanly. CI now runs it after the build + unit tests (under SDL dummy drivers against a free Freedoom IWAD), so a boot or per-frame render regression is caught automatically instead of only on a manual play-test.

Changed

  • Say how to fix it when the HD art is missing (DOOM-0042) Ultra falling back to the 1997 art looks like a lighting regression in a screenshot rather than a missing asset directory, so the message now names DOOMASSETDIR as the thing to set.

  • Fog wisps churn at Silent Hill 2's rate, and each map gets its own drift heading (DOOM-0300) The fog's glow read as a painted patch because the wisp pattern needed 24 seconds to cross one noise cell, where Silent Hill 2's fog fully restructures in under 2.2. Both octaves now drift 15x faster and are exactly opposed, so the fog dissipates and reforms in place instead of blowing past -- measured at 0.4% of the change explained by translation, against SH2's own 0-1%. Each level also draws its own heading, seeded from the map so captures stay reproducible.

  • Bake the sun's fixed direction into a load-time clearance field and delete L2's per-sample ray. (DOOM-0289) The volumetric fog's sun visibility is now baked into a load-time field instead of a shadow ray fired from every fog sample, so the fog costs 3.2% of a frame instead of 44% -- 28 fps back up to 42 on the test machine, with the picture unchanged.

  • The play area is never framed by a border -- a fresh install fills the screen (DOOM-0285) The old default drew the 1993 decorative frame around a shrunken picture. The status bar stays; only the border goes.

  • Ultra's ray-traced view is about 17% faster — 41 to 48 FPS on the test machine (DOOM-0197) The processor and the graphics card were taking turns instead of working at the same time: each frame spent 3.6ms preparing moving doors and lifts and only then started waiting on the card. That preparation now happens while the card is still drawing the previous frame, which is time the frame no longer spends. Nothing about the picture changes.

  • Indoor mist now reaches further in from an opening, and no longer halves at the threshold (DOOM-0281) Fog standing in a doorway was capped at half the density of the air just outside it, so it visibly halved the moment it crossed the opening; and it faded out within about two door-widths, so anyone standing back in a room saw none of it. Mist now carries most of the outdoor strength at an opening and reaches roughly twice as far inside. A sealed room stays clear, which is guaranteed by how the two dials are defined rather than by the values chosen.

  • Ultra's ray-traced fog is ~7.5 ms/frame cheaper — 31 to 41 FPS with fog on (DOOM-0276) The fog used to fire a test ray straight up from every one of its 24 samples per pixel, just to ask "is there sky above here?". DOOM is flat-mapped — one ceiling per spot on the floor — so that answer was already in the small per-level map the fog builds for its doorway seep. It now reads it from there instead. Measured on an RX 6600 in E1M1 at 50% render scale: fog costs +35% of frame time before and +4% after. The one visible cost is that the line between misty outdoor air and clear indoor air now follows that map's 64-unit grid, so it can sit up to half a cell from the wall.

  • nee_sampling_test's unbiasedness bound is derived from the sample count instead of a flat 0.5% The old fixed tolerance sat only ~3.9 sigma from the estimator's true standard error on two of the five weight sets, well short of the 6 sigma the neighbouring frequency check uses. The bound is now computed from the exact estimator variance, so it scales with N and the weights; all five sets currently land within 1.6 sigma.

Fixed

  • The Windows build no longer fails to compile Two faults, both from the developer-capture work and both invisible on Linux. -warpto used a 64-bit whole number without asking for the header that defines one — Linux picked it up by accident from a neighbouring header, the Windows cross-compiler did not. The F12 screenshot folder was then created with a two-argument call that Windows spells with one, the way -cdrom already does elsewhere in the engine. Every Windows build since those flags landed stopped with an error. Caught by the release build, which makes both versions before it publishes anything.

  • The lighting self-test now covers the glowing-sprite light path it was skipping (DOOM-0122) -rtverify proves the renderer's lighting maths is unbiased by comparing two estimators that should agree. Both were being handed a light list that stopped short of the sprite lights — fireballs, glowing pickups — so the newer half of the lighting code was never actually checked. Both estimators now receive the real split, and the verify run reports how many lights of each kind it covered, so a future gap is visible rather than silent.

  • -devshot N now captures the Classic tier as well as Solid and Ultra (DOOM-0294) Classic never builds a Vulkan swapchain image, so the flag was a silent no-op there -- and a harness that finds no new file will happily read a stale one. All three tiers now write dev-shots/shot-NNNN.png under one shared naming helper.

  • Bound the scaled patch blitter and the Classic menu's text rows (DOOM-0230) The scaled patch draw added for the menu work never got the on-screen bounds check its two siblings have, and the menu text helper that calls it clipped sideways but not vertically. Off-screen text is now dropped with a one-off note instead of writing outside the frame.

  • Skip the frame when the video texture cannot be locked A failed SDL_LockTexture leaves the pixel pointer untouched, and the Classic renderer wrote the whole frame through it anyway.

  • Ordinary walls no longer glow like lights, and DOOM's own light fixtures now do (DOOM-0307) A curated list of light-source wall textures replaces the brightness test that decided this before. That test rated a texel by its strongest colour channel, and DOOM's palette ramps nearly all end at 255 in some channel, so the palest cement highlight scored exactly as pure fire did — 82 of DOOM II's 428 wall textures cast light, including all nine CEMENTs and every Wolfenstein wall. It was wrong in the other direction too: the game's own light panels (LITE3, LITE5, LITEBLU, COMPSTA) sat below the threshold and emitted nothing, so the lamps were dark while the walls glowed. Both are fixed; sprites, flats and liquids are unchanged.

  • A torch revealed by a door that opens now lights the fog in front of it, instead of waiting for a level reload (DOOM-0296) (DOOM-0296) Which torches can light which patches of air was worked out once, when the level loaded — so a torch behind a shut door stayed unknown to the fog even after you opened the door. The answer is now recalculated whenever a door or lift finishes moving.

    Honest note on what you will see: on the stock maps, very little. This was measured rather than assumed — opening every door in DOOM 2's MAP01 changes the picture by about a five-hundredth of the threshold the project uses to decide a look has changed at all. Vanilla DOOM rarely puts a torch behind a shut door and close enough to matter. It is a correctness fix that stops a wrong answer being possible, and any map or texture set that does place a bright light behind a door gets it for free.

  • The renderer's -rtverify self-test failed on DOOM 2 and passed on DOOM 1, on the same build (DOOM-0297) The check was under-sampled, not the renderer wrong. DOOM 2's lights are fewer and more clustered, so the estimator needs more samples there to settle; the test now takes them (262144 vs 16384) and both games pass the same unchanged 0.50% bar. Raising the sample count can only make the test stricter, so nothing was loosened to get there.

  • A nukage pool glowed only in patches, because the per-texel emissive mask was applied to liquids too. (DOOM-0302) Nukage pools looked like they had random glowing spots in them. Now the whole pool glows evenly.

  • The -shotverify capture is not tic-deterministic, so a cold-cache run blesses a different golden. (DOOM-0287) Adds a -noinput flag so an automated test or profiling run ignores the keyboard, mouse and gamepad and leaves the pointer to whoever is using the desktop. The -shotverify / -shotcompare capture modes imply it, which also makes their golden-image comparison trustworthy: a stray mouse movement during a capture used to turn the camera and change the image.

  • Screenshot captures are no longer time-dependent, so the visual-regression gate compares like with like (DOOM-0011) The drifting fog and the liquid ripples both ride a wall clock, which made every capture a slightly different image.

  • Fog now rolls into a room whose wall opens during play (DOOM-0281) The seep field that decides how far fog reaches indoors was flooded once at level load from the doors' spawn state, so a wall that opened in play left the room behind it permanently clear. It is now re-flooded when an opening actually appears or vanishes, and the fog eases across to the new answer over about a second so the mist drifts in through the opening instead of popping into place. A door that shuts eases it back out. Ultra's ray-traced view; costs nothing on a map where nothing has moved.

  • Fog and seep no longer read the empty-space sentinel along a level's far edges (DOOM-0276) The seep map's grid was sized with a rounding-down divide, which left its outer "nothing here" ring overlapping real air at the top and right edges of a level. E1M1's grid was one column short. Found by the review of the change above, and it had to be fixed for that change to be correct.

  • Fix the correctness and doc-drift findings from the 2026-07-26 audit + indie-review sweep. (DOOM-0263) The baked global-illumination pass was reading the sky as if it were a wall; several smaller bugs and six stale documentation claims are fixed too.

  • game_select_test drives the real IWAD selection loop rather than a copy of it (DOOM-0244) (DOOM-0244) The preference scan moved from D_DetectIwads into iwad_select_reps() in iwad_detect.h, parameterised by an "is it installed?" predicate — access() for the engine, an in-memory set for the test. Changing the real selection order now fails the test, which it could not do before.

  • rb_text_test bakes the bundled Oxanium instead of skipping itself when no system font is installed (DOOM-0243) (DOOM-0243) It used to look for DejaVu at three distro paths and, finding none, print "skipped" and exit 0 — a green result that ran none of its assertions. It now bakes the embedded assets/Oxanium-SemiBold.ttf the engine itself ships, so a missing font is impossible; fonts-dejavu-core is no longer a CI dependency.

  • Test suite no longer passes vacuously — assert() retired for a check() helper (DOOM-0242) (DOOM-0242) assert() is deleted by -DNDEBUG along with any call inside it, so the four tests that wrote assert(function_under_test(...) == x) would have printed "all passed" while running nothing. Reproduced, then fixed by moving the whole suite onto tests/check_util.h, whose check() is an ordinary function that also records a failure and keeps going instead of aborting and hiding every later case.

  • Corrected mold from a required to an optional dependency in the README. The README listed the mold linker among the required Linux dependencies. The Makefile detects it and falls back to the default linker when it's absent, so a first-time builder was being sent after a package they don't need.

  • The local CI mirror now runs the same boot smoke as GitHub Actions. packaging/ci-local.sh announced itself as "exactly what GitHub Actions runs", but its container mode ran only build + unit tests and skipped the DOOM-0203 headless boot smoke. A boot regression could pass locally and still turn CI red. The container path now installs Freedoom and runs the smoke, as the workflow does.

  • release.sh now updates README's "Latest release" line (DOOM-0202 debt sweep). The release standard says the version lives in three places in lockstep — the git tag, the CHANGELOG heading, and README's "Latest release" line — all moved by the release tool. The tool only ever moved two of them, so every release shipped with a stale README. It now rewrites the README line too, and fails loudly if it can't find it.

  • Golden-image gate no longer inherits your fog setting (DOOM-0202). The -shotverify / -shotcompare capture pins a canonical render configuration so a screenshot comparison can't be poisoned by a play-test tweak left in ~/.doomrc. Volumetric fog (rt_fog) shipped after that pin was written and was never added to it, so the fog level leaked back in — exactly the config-dependence the pin exists to prevent. It is now pinned with the rest.

  • Golden-image visual-regression gate is now config-independent (DOOM-0208). The -shotverify / -shotcompare modes rendered the normal path, which inherited the live ~/.doomrc — so a play-test tweak (a brighter rt_brightness, a stuck-on flashlight, a flipped effect toggle) silently poisoned the golden and made the gate fail on an unchanged render. They now pin a canonical, config-independent RT configuration (the shipped defaults) whenever armed, so a capture is reproducible regardless of the user's live config. Re-blessed the E1M1 Ultra-RT golden under the canonical config (fresh compare is bit-exact, mae=0.000).

  • Fix three low-severity bounds nits: donut NULL-deref, sfx off-by-one, basedefault snprintf. (DOOM-0220) Three small safety tidy-ups: a malformed WAD donut, a sound-index edge, and a long $HOME path can no longer misbehave.

  • Fix -warp E/M bounds so -warp 3 on a non-commercial IWAD can't NULL-deref. (DOOM-0219) Typing doom -warp 3 on DOOM 1 crashed at startup; now it's handled gracefully.

  • Guard RB_BuildPSprites against a negative sprite lump index. (DOOM-0218) A missing weapon-sprite frame could read out of bounds; now it's skipped, matching the world-sprite path.

  • Drain the GPU before RB_Vulkan_BuildLevel frees/recreates live buffers. (DOOM-0217) Loading a new level could, in rare timing, free graphics memory the GPU was still using; now the GPU is drained first.

  • Clamp menu value-name indices (showMessages/fpsCorner) against a hand-edited config. (DOOM-0216) Editing ~/.doomrc to an out-of-range value could crash the game when opening the menu; the value is now clamped.

Security

  • Validate the REJECT lump against the map's sector count (DOOM-0119) The light cull indexed the REJECT visibility table by sector pair without checking the lump was big enough to hold that many sectors, so a map shipping a short REJECT read past the end of it on both the CPU and the GPU. Too small now simply turns the cull off, and says so.

  • Bound the last unguarded WAD and savegame indices (DOOM-0254) P_LoadLineDefs dereferenced its two vertex indices straight out of the PWAD while the nine sibling indices around it were already guarded, and savegame loading cast stored state, type and player indices back into array subscripts unchecked -- a saved player index of 0 read players[-1]. Both now go through the same refuse-the-file guard the rest of the level loader uses.

  • Harden every untrusted-input boundary the 2026-07-26 audit + indie-review sweep found. (DOOM-0254) A hostile or broken WAD, savegame, config file or command line can no longer make the game read or write memory it does not own.

  • Clamp netconsole (packet player field) to MAXPLAYERS in d_net GetPackets. (DOOM-0215) A malformed network packet could write out of bounds using a bogus player number; now such packets are skipped.

  • Clamp netgame packet numtics against BACKUPTICS in i_net PacketGet. (DOOM-0214) A malformed network packet could overflow an internal array before any validation; now oversized packets are dropped.

  • Bound mus2mid MUS->MIDI reads by the real lump length, not the header's own claim. (DOOM-0213) A crafted in-WAD music track could make DOOM read up to ~64KB past the end of the lump; now reads are capped at the true lump size.

  • Harden W_AddFile/W_Reload: validate the WAD directory against the real file size. (DOOM-0212) A hand-made/corrupt WAD could previously make DOOM read random memory while loading; now a bad directory is rejected cleanly.

All releases on GitHub →