Fork of ozkl/doomgeneric — adapted for the Unheaded Protocol Computer (UPC)
30 FPS. 393M packets. 50.3B instructions executed. BPF compute engine — Unheaded.
DOOM running entirely inside the Linux kernel. No userspace game loop. IPv6 packets circulate through a ring of network namespaces, and each XDP program invocation executes a burst of MBC (Monad Bytecode) instructions. The packet is the clock. The network is the bus. BPF maps are the memory. eBPF is the CPU.
Doom C source → RISC-V cross-compile → MBC bytecode → BPF maps (ROM)
┌──────────────────────────────────────────────────────────────────┐
│ Packet Injector (Go) → 460K pkt/s → AF_PACKET raw socket │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
monad0 ──▶ monad1 ──▶ monad2 ──▶ monad3 ──▶ monad4 ──▶ monad5
▲ XDP XDP XDP XDP │
│ 256 insns 256 insns 256 insns 256 insns │
└──────────────────────────────────────────────────────────┘
XDP_TX bounce ring (255 laps)
Each packet: 256 insns × 255 bounces = 65,280 instructions
doom-bridge (Go) reads SCREEN_MAP → WebSocket → Browser canvas
| Conventional CPU | This Project |
|---|---|
| Clock crystal | Packet injector (Go, AF_PACKET) |
| Clock signal | IPv6 packet arrival event |
| ALU | XDP BPF program (monad_cpu) |
| Instruction memory | ROM_MAP — BPF Array, 1 MiB |
| Data memory | RAM_MAP — BPF Array, 64 MiB |
| Register file | CPU_MAP — 16 × 32-bit + flags |
| Memory-mapped I/O | SCREEN_MAP, KBD_MAP |
| Front-side bus | veth ring (6 namespaces, directed cycle) |
| Pipeline depth | MAX_INSN_PER_TICK = 256 |
doom.c (id Software, 1993)
+ doomgeneric platform layer
+ libc_monad.c (minimal libc stubs)
+ crt0_monad.S (BSS clear, stack setup)
│
▼ riscv64-unknown-elf-gcc -march=rv32i
│
doom.elf (RV32I ELF, ~180KB .text)
│
▼ rv32i_to_mbc (Rust translator)
│
doom.mbc (76,128 MBC instruction words)
doom.rv2mbc (45,934 address translation entries)
│
▼ doom-loader (Python, bpftool batch writes)
│
BPF maps pinned at /sys/fs/bpf/unheaded/doom-ring/maps/
L0: BPF Registers ~0 ns 16 × 32-bit MBC regs
L1: BPF Map (per-CPU) ~50 ns CPU_MAP, ROM_MAP
L2: BPF Map (shared) ~100 ns RAM_MAP, SCREEN_MAP, KBD_MAP
L3: Userspace Cache ~1 µs Loader pre-populated data
L4: Disk / WAD file ~1 ms doom1.wad, read once at load
| Metric | Value |
|---|---|
| Injection rate | ~460K packets/sec |
| Instructions per packet | 65,280 (256 × 255 bounces) |
| Mean instructions per frame | 1,465,116 |
| Effective throughput | ~20-30M insns/sec |
| Frame rate | ~20 fps (full 320×200) |
| BPF verifier states | ~51,200 (under 1M limit) |
| MBC expansion ratio | 1.65:1 (RV32I → MBC) |
doomgeneric/ ← upstream C source (id Software → ozkl → this fork)
doom.mbc ← compiled MBC bytecode (333K)
doom.rv2mbc ← RV32I → MBC address translation map (205K)
screenshots/ ← proof-of-concept screenshots
unheaded/ ← Go + eBPF integration layer
├── cmd/ ← CLI tools
│ ├── doom/ ← main orchestrator
│ ├── doom-bridge/ ← WebSocket frame server (Fenrir's Eye)
│ ├── doom-go-injector/ ← high-perf packet injector (Mjolnir)
│ ├── doom-cpu-dump/ ← BPF map state inspector
│ ├── doom-loader/ ← ROM/RAM loader
│ └── doom-injector-c/ ← C injector (reference)
├── ebpf/ ← eBPF/XDP programs (Rust + Aya)
│ ├── monad-cpu-ebpf/ ← the CPU — MBC instruction decoder
│ └── monad-common/ ← shared types (MbcCpuState, opcodes)
├── pkg/ ← Go libraries (BPF map access, types)
├── dashboard/ ← browser viewer (vanilla JS, WebSocket)
├── docker/ ← containerized ring setup
└── tests/ ← integration + security tests
docs/
├── doom/ ← implementation docs
│ ├── DOOM-BUILD-GUIDE.md ← full build + run instructions
│ ├── DOOM-BRIDGE-ARCHITECTURE.md ← WebSocket bridge design
│ ├── PERFORMANCE.md ← performance tuning (WS3)
│ ├── COMPUTATIONAL_GENERALITY.md ← beyond DOOM: Game Boy, SNES, Unix v4
│ ├── BPF-MAP-REFERENCE.md ← map layouts + access patterns
│ └── phase8-results.md ← D_DoomMain entry, RAM_MAP fix
├── protocol/ ← architecture specs
│ └── doom-over-ipv6-architecture.md ← PhD-level system design
└── binder-book/ ← 4-level explainer (ELI5 → Staff/PhD)
Requires Linux 5.15+, Go 1.21+, Rust/Aya toolchain, doom1.wad.
# Build the eBPF CPU program
cd unheaded/ebpf/monad-cpu-ebpf && cargo xtask build-ebpf
# Set up the namespace ring
sudo ./scripts/doom-ring.sh
# Load ROM + RAM into BPF maps
sudo ./scripts/doom-loader.sh doom.mbc doom.rv2mbc doom1.wad
# Start the bridge (WebSocket frame server)
go build -o /tmp/doom-bridge ./unheaded/cmd/doom-bridge
sudo /tmp/doom-bridge --port 6660 --static ./unheaded/dashboard
# Inject packets (start the clock)
go build -o /tmp/doom-go-injector ./unheaded/cmd/doom-go-injector
sudo ip netns exec monad0 /tmp/doom-go-injector --count 500000 --mode fast
# Open http://localhost:6660 in browserSee docs/doom/DOOM-BUILD-GUIDE.md for the full walkthrough.
The original doomgeneric library makes porting DOOM easy — implement 5 functions and you have a working port. This fork preserves full upstream compatibility.
| Function | Description |
|---|---|
DG_Init |
Initialize platform (window, framebuffer) |
DG_DrawFrame |
Frame ready in DG_ScreenBuffer — copy to screen |
DG_SleepMs |
Sleep in milliseconds |
DG_GetTicksMs |
Ticks since launch in milliseconds |
DG_GetKey |
Provide keyboard events |
Ported platforms: Windows, X11, SDL, emscripten, UPC/eBPF (this fork).
GPL-2.0 — see LICENSE
The doomgeneric/ directory and compiled MBC bytecode (doom.mbc, doom.rv2mbc) are GPL-2.0 per the original id Software release. The unheaded/ integration layer (Go, eBPF, tooling) uses separate licensing — see DOOM_IMPLEMENTATION.md for the GPL boundary.
