A wasm interpreter in c++ constexpr evaluation to run doom in compile time. The interpreter supports both compile and runtime time execution of game. A stripped down version of wad was used to keep the memory consumption as low as possible, although compile time execution still requires close to 400GB of memory, which was alloted by allocating swapspace, imagine having 400GBs of RAM! ;).
- C++ 20
- g++12 / clang
- Python
Table of Contents
The repo contains multiple helper python files which provide essential support in the setup of state for the program, visualisation, and parsing of the generated wasm source code.
Install these dependecies to covert c/cpp code to wasm and from wasm to wat (Wat is just a human readable form of wasm binary), along with that close to 400GBs of memory will be required to contain the huge AST and state created by g++/clang, I found gcc conservative in the memory usage while clang tends to allocate alot of ram upfornt. (Swap space will work and that's what I allocated, but it will be a magnitude slower, I got 50 frames rendered in 5 hours so 10fph.)
- wabt
- clang
- lld
- python
- g++
# compiler
sudo apt install g++-12
# wasm2wat is part of wabt (WebAssembly Binary Toolkit)
sudo apt install wabt
# clang with wasm32 target
sudo apt install clang lld
# verify
clang --version
wasm2wat --version
Constexpr-Doom/
├── preview/ # Preview assets (rendered output images)
├── test/ # Minimal WASM test programs
│
├── .gitignore
├── LICENSE # GPL-2.0
├── Makefile # Build system — supports RUNTIME_MODE and constexpr mode
├── README.md
│
├── constants.hpp # Shared constants (MEMORYSIZE, STACKSIZE, MAXFUNCTIONS, …)
├── display.py # Renders the ASCII frame buffer output to terminal
├── dump.txt # Debug dump of interpreter state
├── frame.txt # Captured ASCII art frame output
│
├── handler.hpp # WASM opcode dispatch (HandleI, HandleF, HandleCall, …)
├── implementation.hpp # Doom platform layer stubs (I_FinishUpdate, I_StartTic, …)
├── inspect.hpp # Compile-time string_view → template char pack trick
├── main.cpp # Entry point — constexpr or runtime mode via #ifdef
├── parser.hpp # WAT text parser (parses functions, globals, data segments)
├── parsedState.hpp # cpp state code responsible for setting up state create by runtimeParser.py
├── runner.hpp # Constexpr interpreter loop + RunNoCheck()
├── runtimeParser.py # WAT → parsedState.hpp emitter (generates make_state())
├── run.sh # Shell script to build and run in runtime mode
│
├── state.hpp # All interpreter state structs (Stack, Memory, Function, …)
├── syscall.hpp # C stdlib stubs (malloc, memcpy, printf, realloc, …)
└── types.hpp # Shared types (OP, Member, ParamType, STATUS, …)
test.wbat
│
runtimeParser.py ──────────────────▶ parsedState.hpp
│
test.cpp ──clang wasm32──▶ test.wasm │
│ │
wasm2wat │
│ │
test.wbat │
│ ▼
(program) g++ -std=c++20 -O2
│
┌────────────────┴────────────────┐
│ │
RUNTIME_MODE constexpr mode
│ │
./ctwr (fast) compile-time evaluation
│ │
frame.txt inspect<SV>()
| File | Purpose |
|---|---|
runtimeParser.py |
Parses WAT and emits make_state() and then initialises the full Doom interpreter state at compile time including WAD bytes, function table, memory, and virtual table |
parsedState.hpp |
This file is Auto-generated and contains the 524 KB WAD array and complete state initialisation (its a stripped down version of doom) |
runner.hpp |
Main interpreter loop which executes WASM opcodes either at compile time (constexpr) or runtime |
handler.hpp |
Handles every WASM opcode: arithmetic, memory, branches, calls, locals, globals |
syscall.hpp |
C standard library syscall stubs like malloc, realloc, memcpy, printf, open, read, … |
implementation.hpp |
Doom platform layer like I_FinishUpdate renders the ASCII frame, I_StartTic injects key events |
state.hpp |
All interpreter state: Stack, Memory, Function, FrameBuffer, FileSystem, … |
types.hpp |
Enums and shared types: OP, Member, ParamType, STATUS, Data |
inspect.hpp |
Exposes the compile-time result via ShowChars<> template trick |
constants.hpp |
Single source of truth for buffer sizes shared between C++ and Python |
make RUNTIME_MODE=1
./ctwrchmod +x run.sh
./run.shNote: Constexpr mode requires significant compiler resources. GCC with
-fconstexpr-ops-limitset high enough. (You can kill the make process once output is dumped to dump.txt it may take compiler to free all of the allocated space)
Earlier I had used parser.hpp to set up the state for doom/program at the compile time as well, but since doom source code is so large I switched to runtimeParser.py to output generated state and just load it at compile-time
python3 runtimeParser.py test/test.wbat parsedState.hpp- Doom is compiled to WASM using
clang --target=wasm32 - The WAT text format is parsed by
runtimeParser.pywhich emits a C++make_state()function make_state()initialises the full interpreter state: function table, memory, WAD data, virtual table- The interpreter loop in
runner.hppdispatches WASM opcodes viahandler.hpp - Platform calls (
I_FinishUpdate,I_GetTime, etc.) are stubbed inimplementation.hpp - System calls (
malloc,open,printf, etc.) are stubbed insyscall.hpp - After N frames (defined in constants.hpp),
I_FinishUpdateconverts the palette-indexed screen buffer to ASCII art and returnsSTATUS::ISBADto halt execution - In constexpr mode, the ASCII frame is available as a
string_viewat compile time viainspect<SV>()(I used the compiler template initialisation technique to get the output frame, take a look in inspect.hpp, its very cool ;P) - To pass in input I have hardcoded
EnterKey press inI_STARTTICwhich presses and release enter every frame to get past menu, skill selection and level selction.
One issue that I am sure you saw is the status bar rendered incorrectly, and you are right, I have been facing this issue lately but not sure whats the root cause is, as I found debugging the program really hard, I have some info why it is happening but not sure what is the root cause,
Here is an overview of what the issue is, during status bar rendering multiple calls to V_CopyRect are made, and there are certain checks that each patch needs to satify which this function verifies and calls I_Error with ("Bad V_CopyRect") and one of these call fails.
if (srcx < 0
|| srcx + width > SCREENWIDTH
|| srcy < 0
|| srcy + height > SCREENHEIGHT
|| destx < 0
|| destx + width > SCREENWIDTH
|| desty < 0
|| desty + height > SCREENHEIGHT ← this check fails with desty 191 and height 16 > 200 (SCREENHEIGHT)
|| (unsigned)srcscrn > 4
|| (unsigned)destscrn > 4)To get to render first frame I have manually removed this specific condition in if clause by updating the generated test.wbat manually, if you know what is causing the issue please let me know, I have already ran the wad file I use with chocolate-doom and it renders properly, so my wad is not corrupted I believe (see the ss above), and since constexpr evaluation cannot have undefined behaviour - I assume I dont have some ub in my interpreter execution.
Fun Fact Some of the errors I caught were because constexpr refused to compile with UB ;) Look at this
f4578be556975b81a236afd8d2779e83f3e4e2accommit if interested there were some signed overflows errors (which are ub).
V_CopyRect
...
...
i32.add
local.tee 9
i32.const 217 <- was 201
i32.lt_s
br_if 1 (;@1;)Email ID - lakshay21059@iiitd.ac.in
-
Thanks to Dimitri from Michigan TypeScript that made me believe something like this was remotely possible! Checkout the TypeScript Doom here it's way cooler and more complex here.
-
Jason Turner for awesome cpp talks and especially Constexpr All The Things!.
-
And my comrades of Ninth-Circle
GPL-2.0 — see LICENSE