/terminal-doom

Play DOOM in modern terminals

Primary LanguageC

Terminal Doom

Terminal Doom enables Doom-based games to play smoothly in modern terminals with original graphics and sound. It also works over fast ssh connections.

Demo with sound:

doom-sound.mp4

Building

There are no system dependencies, so just clone and build with Zig v0.13:

zig build -Doptimize=ReleaseFast

Run with zig-out/bin/terminal-doom

Terminal Doom uses the libvaxis Zig library to render and handle keyboard and mouse events. If you ever want to make a TUI app, I highly recommend this library.

Sound support

Sound is enabled by default. Add the -Dsound=false if you want to compile without sound support (like when running on a remote server via ssh)

All sound effects are included, and a few music tracks. You can download and add additional music tracks (mp3) yourself. Terminal Doom will automatically pick them up from the sound directory. See the sound section for naming.

Where does it run?

Tested on macOS and Linux. Compiles on Windows as well, but no terminal there seems to run it (WezTerm likely gets closest in ssh local mode)

Currently works best in Ghostty and Kitty as these have solid implementations of the required specs. WezTerm should work if you use 'f' instead of ctrl keys for firing the gun.

Playing

You can play keyboard-only (recommended) or in combination with a mouse. You can disable/enable mouse at any time by pressing m. This is useful when playing with keyboard on a laptop to avoid spurious input from the trackpad.

When using a mouse, make sure you adjust sensitivity in the Options menu if it's too fast or slow. Also try adjusting sensitivity on your mouse if it has buttons for this. Once sensitivity is right, playing with a mouse/keyboard combo is pretty efficient.

Keep in mind that mouse support in terminals comes with limitations, as apps are not able to capture the mouse.

Action Keys/Mouse Actions
Menu ESC to open/close, Enter to select
Walk / rotate Arrow keys or mouse. j, l also rotates.
Walk / strafe wasd
Fire f, i, control keys, mouse click
Use/open Spacebar, right mouse click
Strafe left/right Alt+arrow keys, a, d
Quit Ctrl+c
Disable/enable mouse m
Disable/enable scaling u

Most other Doom keys should work as well, such as Tab for map and F5 for adjusting detail level.

How it works

Rendering

While the Kitty graphics protocol is primarily intended to display images, modern terminals and computers are fast, with high memory bandwidth, SIMD support, and discrete GPUs. There's plenty of juice available to run this classic game smoothly over a text protocol.

Here's how it works: on every frame, doomgeneric calls DG_DrawFrame. Our job is now to turn the pixel data into a base64 encoded payload. This payload is then split into 4K chunks, each chunk wrapped by the Kitty protocol envelope. Actually, some terminals work without chunking, which is even faster, but that's not spec compliant and e.g. Kitty itself fails without chunking (thanks to rockorager for pointing this out)

With the encoded message ready, we now:

  1. Set synchronized output (mode 2026)
  2. Clear the screen
  3. Display the frame by sending the Kitty graphics message
  4. Reset synchronized output to flush updates to screen
  5. Handle any keyboard input

With the latest version, all of this is outsourced to libvaxis.

This sequence repeats for every frame. While this is enough to run Doom smoothly in a modern terminal, there are many optimizations that can be done, including SIMDifying pixel encoding.

Sound

The history of Doom has many interesting facets, and its sound library is no different. You can read about it here

Terminal Doom's sound support originally worked by calling out to SDL2, but that had a couple of problems. First of all, the implementation from doomgeneric was complicated and large. Second, depending on SDL2 made building harder on some systems.

This is the solution I came up with:

  1. Ditch all the complex midi sequencing and mixing logic.
  2. Make the wav and mp3 files part of the project
  3. Outsource playback to miniaudio

While large, miniaudio is a single header file, it's portable, and has a straightforward API.

Adding additional music tracks

Terminal Doom ships with a few tracks, such as for the intro and the first level. You can add additional mp3's to the sound directory. For Terminal Doom to pick these up, they must be named according to the Doom convention:

d_e1m1.mp3
d_e1m2.mp3
d_e1m3.mp3
d_e1m4.mp3
d_e1m5.mp3
d_e1m6.mp3
d_e1m7.mp3
d_e1m8.mp3
d_e1m9.mp3
d_inter.mp3
d_intro.mp3
d_victor.mp3

The actual content of these can obviously be anything you want, not just the orginal music.

Supported games

doom1.wad is included in the repository and other wad files are available on various online sites.

These should all work:

doom2.wad
plutonia.wad
tnt.wad
doom.wad
doom1.wad
chex.wad
hacx.wad
freedm.wad
freedoom2.wad
freedoom1.wad

Credits

  • The engine is based on the amazing doomgeneric project
  • Rendering and input is handled by libvaxis, a TUI library written in Zig
  • Sound is handled by miniaudio, a single-file sound playback library
  • Build system (and the main input/rendering loop) is all Zig
  • Testing and debugging in Ghostty's terminal inspector (currently closed beta), Kitty, and WezTerm

LICENSE

As Terminal Doom is based on the doomgeneric project, the project as a whole is licensed under GPL2.

The Zig-based renderer/handler, build file, and miniaudio bridge are licensed under MIT.