File size: 3,469 Bytes
7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 6bff818 6eccfab 6bff818 6eccfab 2c00621 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab 7c15b94 6eccfab | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | ---
license: gpl-2.0
library_name: onnxruntime
tags:
- onnx
- emulation
- cpu
- riscv
- doom
- retro
- art
- cursed
language:
- en
pipeline_tag: other
inference: false
---
# Doom on ONNX

A single self-contained ONNX model (`doom.onnx`, ~8.4 MB) that, when run on
**any** ONNX Runtime CPU EP, boots and renders the original 1993 *Doom*.
No custom operators, no execution-provider plugins, no Python in the loop —
just standard ONNX ops (Add, BitwiseAnd, Where, Gather, ScatterElements,
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
baked into RAM as another initializer.
## Reference render
The `doom.gif` in this repo was assembled from 74 PNG frames captured during
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
rendering of actual gameplay** (frames 54–75)
## Performance
~2,000 simulated RV32IM instructions per second on a modern laptop CPU.
This is **not** a real-time emulator. One frame every ~9 minutes is the
reality on a single CPU thread. See `PERF_INVESTIGATION.md` in the source
repo for the full investigation (TL;DR: ORT's `MayInplace` alias doesn't
apply to Loop-carried state, so the 8 MiB RAM gets fully copied per
`ScatterElements`).
## Running
```python
import numpy as np
import onnxruntime as rt
sess = rt.InferenceSession("doom.onnx", providers=["CPUExecutionProvider"])
RAM_SIZE = 8 * 1024 * 1024
ram = np.load("initial_ram.npy") # Doom ELF baked into RAM
pc = np.array(0x1000, dtype=np.int32)
regs = np.zeros(32, dtype=np.int32)
MMIO_TICK = RAM_SIZE - 16
sim_ms = 0
for chunk in range(250):
ram[MMIO_TICK:MMIO_TICK + 4] = np.frombuffer(
np.uint32(sim_ms).tobytes(), dtype=np.uint8)
sim_ms += 100
pc, regs, ram = sess.run(None, {
"pc_in": pc, "regs_in": regs, "ram_in": ram,
"trip_count": np.array(100_000, dtype=np.int64),
})
# framebuffer at RAM_SIZE - 32 - 64000, 320×200 palette indices
```
The host only writes a millisecond counter into the MMIO tick register
between chunks and reads the framebuffer out of the returned `ram_out`.
## Inputs / outputs
| Name | Type | Shape | Role |
|---|---|---|---|
| `pc_in` | int32 | scalar | program counter |
| `regs_in` | int32 | [32] | x0..x31 (x0 forced to 0) |
| `ram_in` | uint8 | [8 MiB] | full writable memory |
| `trip_count` | int64 | scalar | how many insts to execute |
| `pc_out` / `regs_out` / `ram_out` | (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
GPL-2.0; the shareware doom1.wad ships under id Software's shareware
terms. This model card and model file inherit GPL-2.0.
## Acknowledgements
id Software for releasing Doom's source. The doomgeneric project for the
platform-agnostic port. The ONNX team for an op set this absurdly expressive.
|