Doom's renderer, compiled into transformer weights – no training anywhere

physicsrob1 pts1 comments

Doom, compiled into a transformer — Out of Distribution

Posts · Doom<br>Doom, compiled into a transformer<br>Rob Porter · August 13, 2026 · Code ↗ · Share ↗

Transformers are black boxes … mostly. We normally train them to predict the<br>next token and call the result an LLM. But for years, I’ve wondered what else<br>you could put inside one by constructing its weights directly.

This led me down the road of creating torchwright,<br>a compiler whose output format is a transformer. Feed it a computation graph —<br>a definition of how to get from input tokens to output tokens — and it emits<br>transformer weights. Run inference on the resulting model, and the graph<br>executes. I started by making calculators — simple<br>transformers that evaluate expressions like “12*34”. Once those worked as<br>vanilla Hugging Face checkpoints, I wondered how far I could go. The natural<br>question any nerd would ask: Can it run Doom?

The answer? Yes.

I ported Doom’s renderer into an ordinary LLM architecture. Hugging Face loads<br>the checkpoint with no custom code. Instead of training it to imitate Doom<br>frames, I translated Doom’s original rendering algorithm into that architecture<br>as faithfully as I could. Given a prompt containing the level data, the player’s<br>position, and the viewing direction, the transformer generates the frame Doom<br>would have drawn. My compiler constructed every weight directly from the<br>resulting computation graph. There was no training anywhere.

PROMPT &middot; GAME STATEPROMPT viewx 1056<br>viewy -3616<br>viewz 41<br>viewangle 90<br>node 0<br>node.x 1384<br>node.y -2592<br>node.dx -40<br>node.dy -288<br>node.child1 65<br>node.child0 93<br>node.bbox1.top -2592<br>node.bbox1.bottom -2880<br>node.bbox1.left 1344<br>node.bbox1.right 1384<br>node.bbox0.top -2592

TRANSFORMER MULTI-HEAD ATTENTION<br>FEED-FORWARD

MULTI-HEAD ATTENTION<br>FEED-FORWARD

MULTI-HEAD ATTENTION<br>FEED-FORWARD

38 LAYERS

OUTPUT &middot; TOKENS REPLAYED INTO A FRAMEOUTPUT

What exactly did I build?

Before I started, I had to decide what would count as running Doom. I wanted an<br>honest win: the checkpoint itself had to execute substantially the same<br>rendering algorithm as Doom. The map and player state had to enter through<br>the prompt, and the program outside the weights could only convert the model’s<br>output mechanically into pixels.

The result is a stock, decoder-only checkpoint that Hugging Face loads with no<br>custom model code (it uses Phi3ForCausalLM). Generation produces a stream of<br>drawing operations and intermediate values. The host ignores most of them<br>and acts on five drawing commands:

setCursorX(x) and setCursorY(y) move the cursor.

setCursorDirectionX and setCursorDirectionY choose whether it advances<br>horizontally or vertically after a draw.

pixel(color, w) paints a run of w pixels at the cursor using Doom’s<br>palette.

The checkpoint is not tied to a particular frame, player position, or even E1M1.<br>The prompt contains the map geometry, BSP tree, sector heights, texture<br>references, light levels, player position, and viewing direction. Those can all<br>change without recompiling. What is fixed at compile time is the 320×200 output<br>and the texture library (nine wall textures and six floor and ceiling textures<br>chosen for E1M1’s opening scene). The same checkpoint can render any world scene<br>built from those textures; adding another texture requires recompiling.

This is Doom’s renderer, not the complete game. It uses Doom’s low-detail mode,<br>rendering the 3D view in 160 two-pixel-wide columns. Sprites are not implemented,<br>and the weapon and status bar are fixed to Doom’s pistol-start state. For the<br>frame above, 97% of the 64,000 output pixels exactly match the reference<br>renderer.

Below is the model rendering the opening frame of E1M1. On the left is the<br>sequence produced by the transformer. On the right, the host mechanically<br>applies the drawing commands as they are generated.

Outside the weights

The program outside the weights is small. It remembers a cursor, looks up one of<br>Doom’s 256 RGB colors, and paints the horizontal runs requested by the model.<br>The complete 43-line program is below if you want to check that boundary for<br>yourself.

Complete host program 43 lines · Python python · minimal program Copy

import json

from pathlib import Path

from huggingface_hub import hf_hub_download

from PIL import Image

from transformers import pipeline

MODEL = "physicsrob/torchwright-doom-e1m1"

SCREEN = (320, 200)

10

11<br>prompt = Path(hf_hub_download(MODEL, "examples/e1m1_prompt.txt")).read_text()

12<br>colors = json.loads(

13<br>Path(hf_hub_download(MODEL, "doom_palette.json")).read_text()

14<br>)["colors"]

15

16<br>image = Image.new("RGB", SCREEN)

17<br>x = y = 0

18<br>advance_x = False

19

20<br>generate = pipeline(

21<br>"text-generation", model=MODEL, device_map="auto", trust_remote_code=False

22

23<br>output = generate(prompt, return_full_text=False)[0]["generated_text"]

24

25<br>for token in output.split():

26<br>command, _, arguments = token.rstrip(")").partition("(")

27<br>if command ==...

doom node model transformer output from

Related Articles