Doom on AbsoluteTelnet/SSH

# Yes, It Runs DOOM. Three Different Ways.

In 1993 DOOM asked for a 386 and four megabytes of RAM, and in return it gave you a 320×200 window into hell at 35 frames per second. I played it on exactly that kind of machine when I was in colledge, and the thing I remember most is that it never felt like the computer was struggling. DOOM was fast on hardware that couldn’t run a modern web page.

Thirty-three years later the terminal emulator is the last piece of software that still thinks in rows and columns, and people keep porting DOOM to it anyway. Three of those ports use three completely different ways of getting pixels onto a screen that was designed for text. Together they make a surprisingly honest benchmark for a terminal, so we pointed all three at AbsoluteTelnet/SSH and recorded what happened.

To be clear about the point of this article: nobody should play DOOM in an SSH client. It is a terrible way to play DOOM. It is, however, an excellent way to find out whether a terminal can decode, parse, and paint a full screen of new content thirty-five times a second without falling over. That is a question that matters for real work, because the same machinery draws your `htop`, your `lazygit`, your notcurses dashboards, and your Neovim image previews.

## Round one: pixels over the kitty graphics protocol

Kitty DOOM by Jim Huang is DOOM built on the PureDOOM single-header port, with the rendering swapped out for the kitty graphics protocol. Every frame is a raw 320×200 RGB bitmap. The game base64-encodes it, chops it into 4 KB chunks, wraps each chunk in an escape sequence, and writes the lot to standard output. The first frame is transmitted and placed so it stretches to fill the whole window, and every frame after that arrives as an in-place frame update followed by an animation command that tells the terminal to show it.

The arithmetic is what makes this interesting. A 320×200 RGB frame is 192,000 bytes. Base64 inflates that to 256,000. At 35 frames per second the game is asking the terminal to swallow, decode, and paint close to 9 MB of escape sequences every second, and to do it over an SSH channel that is decrypting every byte on the way in. Kitty DOOM skips frames that changed less than five percent, which helps on the title screen and not at all once the shooting starts.

What the terminal has to get right: chunked transmission, frame composition, the animation control command, cursor placement policy, and enough SSH channel window to keep the pipe full. Get any of them wrong and you see it immediately. The game either stalls, tears, or slowly walks up the screen.

## Round two: pixels over Sixel, the 1980s way

VTDoom by James Holderness does the same job with Sixel, the DEC bitmap format that shipped on the VT240 and VT340 forty years ago. Each frame is re-encoded as a Sixel image, which means a palette definition followed by the picture in six-pixel-tall bands, and streamed to the terminal as a single device control string.

Sixel was designed for printing a graph once, not for animation, so keeping DOOM on the screen takes a few things that a lot of Sixel implementations never bother with. The game switches on Sixel display mode so each frame paints in place at the top of the screen instead of scrolling the previous one into history. It defines the palette on the first frames and then only selects colors afterwards, which only works if the terminal keeps the color registers alive between images. And it needs to know when a key is released as well as pressed, so it uses the win32-input-mode keyboard protocol that James also designed for Windows Terminal. Without press-and-release events you cannot hold down a movement key, and DOOM becomes a game about tapping.

AbsoluteTelnet does all of that, and it does it while decoding the Sixel stream fast enough that the game stays playable. If your terminal claims Sixel support, this is the port that finds out whether it means it.

## Round three: no pixels at all

doom-ascii by Wojciech Graj throws away the graphics protocols entirely. It downsamples the frame to 80×50 pixels and draws each one as two characters wide, picking a character by brightness and setting a 24-bit foreground color per cell. What comes out of the program is plain text with ANSI color codes, the same thing any colorized `ls` produces, just 8,000 cells of it at a time, many times a second.

This is the round that stresses the part of the terminal people take for granted. There are no images to decode. There is a full screen of text to parse, with a color change on nearly every cell, and then a full screen to repaint, over and over. A terminal that parses escape sequences slowly, or that repaints more than it has to, or that lets a redraw scroll the whole screen up by one line, shows it here in a way that no static screenshot ever would. A full frame can run to about 170 KB of text at the default resolution, which is a lot of `ESC [ 38;2;r;g;b m` to get through before the next one lands.

## What this actually proves

Three different transports, one conclusion: the terminal keeps up. On the pixel rounds the frame rate is set by how fast the game can encode and how fast the SSH link can deliver, not by how fast AbsoluteTelnet can paint. On the text round the same workload runs at the same speed in AbsoluteTelnet as it does in Windows Terminal on the same machine, which is the bar we measure against.

The reason to care is not DOOM. The reason is that a terminal fast enough to keep DOOM playable over SSH is a terminal that never makes you wait on a big `git diff`, a chatty build log, a busy `tail -f`, or a dashboard that redraws every second. Speed in a terminal is not a feature you notice. Its absence is.

## Try it yourself

All three ports are open source and each needs the shareware `doom1.wad`, which is free. Build the one you want on any Linux box, connect to it with AbsoluteTelnet/SSH, and run it:

“`

# kitty graphics

git clone https://github.com/jserv/kitty-doom && cd kitty-doom && make && ./build/kitty-doom

# Sixel

# grab a release binary from https://github.com/j4james/vtdoom/releases

./vtdoom -iwad doom1.wad

# text

git clone https://github.com/wojciech-graj/doom-ascii && cd doom-ascii && make

./_unix/obj/doom-ascii -iwad doom1.wad

“`

For doom-ascii, a terminal at least 160 columns wide and 50 rows tall fits the default frame. For the other two, any size works and the picture stretches to fit.

AbsoluteTelnet/SSH 14.15 is available now. Download it HERE

If you have a terminal-DOOM port we missed, or one of these renders differently for you than it does in the videos, leave a comment. We want to know.

Leave a Comment

Your email address will not be published. Required fields are marked *