DOOM in the kernel, or fibers in eBPF
DOOM is not supposed to run inside eBPF. Linux should reject a program like that before executing its first instruction.
BPF has a tiny stack, five argument registers, and limited call depth. Recursion is forbidden. A loop must be finite not merely because the programmer says so, but in terms the verifier can prove. You cannot simply store a pointer in memory, load it later, and dereference it: the kernel must remember where it came from and what it is allowed to address.
And yet an unmodified Linux kernel accepts my BPF object, checks it with the stock verifier, and runs it through the stock JIT. DOOM initialization, game logic, and rendering all execute in the kernel. One game tick, including the complete frame, finishes in a single BPF invocation. Userspace supplies the WAD and keyboard input and gets back a pointer to the finished framebuffer.
First, a little context on eBPF. It lets user-supplied programs run inside the Linux kernel without a kernel module. A program is compiled to bytecode for a small register machine, loaded with the bpf(2) system call, and attached to a hook—for example, an incoming packet or a system-call tracepoint. The kernel's JIT compiles the bytecode to machine code, which runs whenever the hook fires. But before the program can run, the verifier must accept it. That is where the constraints above come from: code the verifier cannot prove safe is rejected. This check, rather than the bytecode itself, is what makes DOOM inside eBPF look impossible.
The project is called BPF Capsule. It is a compiler and runtime for large C programs inside ordinary BPF, with no kernel patches and no separate virtual machine in userspace. The oldest supported target profile is Linux 5.15. A profile determines which kernel capabilities the compiler may use. I have loaded and run the programs on both x86-64 and arm64.
Nobody needs games in the kernel, of course. But complex application logic is useful there: parsing packets, for example, or keeping statistics about them. When such a program does not fit eBPF's constraints, it has to be simplified and rewritten by hand until the verifier is satisfied. Capsule explores another path: it takes C, C++, or no_std Rust code and transforms it into a shape that stock Linux accepts.
DOOM is not the application here but a stress test for that approach. Lua, QuickJS, SQLite, zlib, wasm3, llama2.c, no_std Rust, and CPython 3.14 run on the same scheme today, and Lua and Python inspect live packets straight from XDP. This article follows the road from a hand-trimmed port through a slow interpreter to regions and fibers—and measures what they cost at run time.
You can try it with one command on any supported kernel. You need Nix and a WAD file — for obvious reasons the WAD is not in the repository — and the rest of the requirements are in the README:
$ sudo nix run github:ayles/bpf-capsule#doom -- /path/to/doom1.wad tty
... continue reading