anthonypjshaw/doom-onnx · Hugging Face
Log In<br>Sign Up
Doom on ONNX
A single self-contained ONNX model (doom.onnx, ~8.4 MB) that, when run on<br>any ONNX Runtime CPU EP, boots and renders the original 1993 Doom.<br>No custom operators, no execution-provider plugins, no Python in the loop —<br>just standard ONNX ops (Add, BitwiseAnd, Where, Gather, ScatterElements,<br>Loop, If, …) executing inside a single InferenceSession.run call.
The model contains:
an RV32IM CPU built entirely out of ONNX operators,
the doom1.wad shareware game data as a read-only initializer,
the doomgeneric Doom source cross-compiled to bare-metal RV32IM and<br>baked into RAM as another initializer.
Reference render
The doom.gif in this repo was assembled from 74 PNG frames captured during<br>a single InferenceSession.run invocation:
Total : 80,000,000 RV32IM instructions, 10.8 hours wall time
Rate : 1,562 IPS (init code) → 2,053 IPS (in-game rendering)
Reached : title wipe → menu → DEMO1 load → game logic → 3D BSP<br>rendering of actual gameplay (frames 54–75)
Performance
~2,000 simulated RV32IM instructions per second on a modern laptop CPU.<br>This is not a real-time emulator. One frame every ~9 minutes is the<br>reality on a single CPU thread. See PERF_INVESTIGATION.md in the source<br>repo for the full investigation (TL;DR: ORT's MayInplace alias doesn't<br>apply to Loop-carried state, so the 8 MiB RAM gets fully copied per<br>ScatterElements).
Running
import numpy as np<br>import onnxruntime as rt
sess = rt.InferenceSession("doom.onnx", providers=["CPUExecutionProvider"])
RAM_SIZE = 8 * 1024 * 1024<br>ram = np.load("initial_ram.npy") # Doom ELF baked into RAM<br>pc = np.array(0x1000, dtype=np.int32)<br>regs = np.zeros(32, dtype=np.int32)
MMIO_TICK = RAM_SIZE - 16<br>sim_ms = 0<br>for chunk in range(250):<br>ram[MMIO_TICK:MMIO_TICK + 4] = np.frombuffer(<br>np.uint32(sim_ms).tobytes(), dtype=np.uint8)<br>sim_ms += 100<br>pc, regs, ram = sess.run(None, {<br>"pc_in": pc, "regs_in": regs, "ram_in": ram,<br>"trip_count": np.array(100_000, dtype=np.int64),<br>})<br># framebuffer at RAM_SIZE - 32 - 64000, 320×200 palette indices
The host only writes a millisecond counter into the MMIO tick register<br>between chunks and reads the framebuffer out of the returned ram_out.
Inputs / outputs
Name<br>Type<br>Shape<br>Role
pc_in<br>int32<br>scalar<br>program counter
regs_in<br>int32<br>[32]<br>x0..x31 (x0 forced to 0)
ram_in<br>uint8<br>[8 MiB]<br>full writable memory
trip_count<br>int64<br>scalar<br>how many insts to execute
pc_out / regs_out / ram_out<br>(same types)
post-state
rom (the WAD) is a read-only initializer baked into the model.
License
The CPU + DMA glue is MIT. doomgeneric and the original Doom source are<br>GPL-2.0; the shareware doom1.wad ships under id Software's shareware<br>terms. This model card and model file inherit GPL-2.0.
Acknowledgements
id Software for releasing Doom's source. The doomgeneric project for the<br>platform-agnostic port. The ONNX team for an op set this absurdly expressive.
Downloads last month -
Downloads are not tracked for this model. How to track
Inference Providers NEW<br>Other
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support