Skip to content

Commit 85aa1cb

Browse files
authored
Merge pull request #58 from rivet-dev/examples/doom
feat: add Doom example (doomgeneric compiled to WASM)
2 parents 9eab922 + 7715b34 commit 85aa1cb

File tree

9 files changed

+929
-16
lines changed

9 files changed

+929
-16
lines changed

examples/doom/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
build/
2+
.cache/
3+
node_modules/
4+
dist/

examples/doom/Makefile

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# examples/doom/Makefile — Build Doom for the secure-exec sandbox
2+
#
3+
# Targets:
4+
# build Download doomgeneric + doom1.wad, compile to WASM
5+
# clean Remove build artifacts and downloaded sources
6+
#
7+
# Prerequisites:
8+
# - wasi-sdk built at native/wasmvm/c/vendor/wasi-sdk/
9+
# (run `make wasi-sdk` in native/wasmvm/c/ if missing)
10+
# - wasm-opt (binaryen) on PATH
11+
# - patched sysroot at native/wasmvm/c/sysroot/ for poll() support
12+
# (run `make sysroot` in native/wasmvm/c/ if missing)
13+
14+
REPO_ROOT := $(shell git -C $(dir $(lastword $(MAKEFILE_LIST))) rev-parse --show-toplevel)
15+
WASI_SDK_DIR := $(REPO_ROOT)/native/wasmvm/c/vendor/wasi-sdk
16+
SYSROOT := $(REPO_ROOT)/native/wasmvm/c/sysroot
17+
CC := $(WASI_SDK_DIR)/bin/clang
18+
19+
BUILD_DIR := build
20+
CACHE_DIR := .cache
21+
SRC_DIR := $(CACHE_DIR)/doomgeneric/doomgeneric
22+
23+
# doomgeneric source — pinned to a known-good commit
24+
DOOMGENERIC_REPO := https://github.com/ozkl/doomgeneric
25+
DOOMGENERIC_COMMIT := 3b1d53020373b502035d7d48dede645a7c429feb
26+
DOOMGENERIC_URL := $(DOOMGENERIC_REPO)/archive/$(DOOMGENERIC_COMMIT).zip
27+
28+
# Shareware WAD (freely distributable by id Software)
29+
WAD_URL := https://distro.ibiblio.org/slitaz/sources/packages/d/doom1.wad
30+
31+
# All doomgeneric engine sources (excluding platform-specific backends)
32+
ENGINE_SRCS := \
33+
$(SRC_DIR)/dummy.c \
34+
$(SRC_DIR)/am_map.c \
35+
$(SRC_DIR)/doomdef.c \
36+
$(SRC_DIR)/doomstat.c \
37+
$(SRC_DIR)/dstrings.c \
38+
$(SRC_DIR)/d_event.c \
39+
$(SRC_DIR)/d_items.c \
40+
$(SRC_DIR)/d_iwad.c \
41+
$(SRC_DIR)/d_loop.c \
42+
$(SRC_DIR)/d_main.c \
43+
$(SRC_DIR)/d_mode.c \
44+
$(SRC_DIR)/d_net.c \
45+
$(SRC_DIR)/f_finale.c \
46+
$(SRC_DIR)/f_wipe.c \
47+
$(SRC_DIR)/g_game.c \
48+
$(SRC_DIR)/hu_lib.c \
49+
$(SRC_DIR)/hu_stuff.c \
50+
$(SRC_DIR)/info.c \
51+
$(SRC_DIR)/i_cdmus.c \
52+
$(SRC_DIR)/i_endoom.c \
53+
$(SRC_DIR)/i_joystick.c \
54+
$(SRC_DIR)/i_scale.c \
55+
$(SRC_DIR)/i_sound.c \
56+
$(SRC_DIR)/i_system.c \
57+
$(SRC_DIR)/i_timer.c \
58+
$(SRC_DIR)/memio.c \
59+
$(SRC_DIR)/m_argv.c \
60+
$(SRC_DIR)/m_bbox.c \
61+
$(SRC_DIR)/m_cheat.c \
62+
$(SRC_DIR)/m_config.c \
63+
$(SRC_DIR)/m_controls.c \
64+
$(SRC_DIR)/m_fixed.c \
65+
$(SRC_DIR)/m_menu.c \
66+
$(SRC_DIR)/m_misc.c \
67+
$(SRC_DIR)/m_random.c \
68+
$(SRC_DIR)/p_ceilng.c \
69+
$(SRC_DIR)/p_doors.c \
70+
$(SRC_DIR)/p_enemy.c \
71+
$(SRC_DIR)/p_floor.c \
72+
$(SRC_DIR)/p_inter.c \
73+
$(SRC_DIR)/p_lights.c \
74+
$(SRC_DIR)/p_map.c \
75+
$(SRC_DIR)/p_maputl.c \
76+
$(SRC_DIR)/p_mobj.c \
77+
$(SRC_DIR)/p_plats.c \
78+
$(SRC_DIR)/p_pspr.c \
79+
$(SRC_DIR)/p_saveg.c \
80+
$(SRC_DIR)/p_setup.c \
81+
$(SRC_DIR)/p_sight.c \
82+
$(SRC_DIR)/p_spec.c \
83+
$(SRC_DIR)/p_switch.c \
84+
$(SRC_DIR)/p_telept.c \
85+
$(SRC_DIR)/p_tick.c \
86+
$(SRC_DIR)/p_user.c \
87+
$(SRC_DIR)/r_bsp.c \
88+
$(SRC_DIR)/r_data.c \
89+
$(SRC_DIR)/r_draw.c \
90+
$(SRC_DIR)/r_main.c \
91+
$(SRC_DIR)/r_plane.c \
92+
$(SRC_DIR)/r_segs.c \
93+
$(SRC_DIR)/r_sky.c \
94+
$(SRC_DIR)/r_things.c \
95+
$(SRC_DIR)/sha1.c \
96+
$(SRC_DIR)/sounds.c \
97+
$(SRC_DIR)/statdump.c \
98+
$(SRC_DIR)/st_lib.c \
99+
$(SRC_DIR)/st_stuff.c \
100+
$(SRC_DIR)/s_sound.c \
101+
$(SRC_DIR)/tables.c \
102+
$(SRC_DIR)/v_video.c \
103+
$(SRC_DIR)/wi_stuff.c \
104+
$(SRC_DIR)/w_checksum.c \
105+
$(SRC_DIR)/w_file.c \
106+
$(SRC_DIR)/w_main.c \
107+
$(SRC_DIR)/w_wad.c \
108+
$(SRC_DIR)/z_zone.c \
109+
$(SRC_DIR)/w_file_stdc.c \
110+
$(SRC_DIR)/i_input.c \
111+
$(SRC_DIR)/i_video.c \
112+
$(SRC_DIR)/doomgeneric.c \
113+
$(SRC_DIR)/icon.c
114+
115+
# Our terminal backend
116+
BACKEND_SRC := c/doomgeneric_terminal.c
117+
118+
WASM_CFLAGS := --target=wasm32-wasip1 \
119+
--sysroot=$(SYSROOT) \
120+
-O2 -flto \
121+
-I $(SRC_DIR) \
122+
-DNORMALUNIX -DLINUX -DSNDSERV -D_DEFAULT_SOURCE \
123+
-Wno-implicit-function-declaration \
124+
-Wno-int-conversion \
125+
-Wno-return-type \
126+
-Wno-pointer-sign
127+
128+
NATIVE_CC := $(shell command -v cc 2>/dev/null || command -v gcc 2>/dev/null || command -v clang 2>/dev/null)
129+
NATIVE_CFLAGS := -O2 -I $(SRC_DIR) \
130+
-DNORMALUNIX -DLINUX -DSNDSERV -D_DEFAULT_SOURCE \
131+
-Wno-implicit-function-declaration \
132+
-Wno-int-conversion \
133+
-Wno-return-type \
134+
-Wno-pointer-sign
135+
136+
.PHONY: build native clean fetch
137+
138+
build: $(BUILD_DIR)/doom $(BUILD_DIR)/doom1.wad
139+
@echo ""
140+
@echo "Build complete!"
141+
@echo " Binary: $(BUILD_DIR)/doom"
142+
@echo " WAD: $(BUILD_DIR)/doom1.wad"
143+
@echo ""
144+
@echo "Run with: pnpm start"
145+
146+
native: $(BUILD_DIR)/doom-native $(BUILD_DIR)/doom1.wad
147+
@echo ""
148+
@echo "Native build complete!"
149+
@echo " Binary: $(BUILD_DIR)/doom-native"
150+
@echo ""
151+
@echo "Run with: pnpm start:native"
152+
153+
# --- Fetch doomgeneric source ---
154+
155+
$(SRC_DIR)/doomgeneric.h:
156+
@echo "=== Downloading doomgeneric ==="
157+
@mkdir -p $(CACHE_DIR)
158+
@curl -fSL "$(DOOMGENERIC_URL)" -o "$(CACHE_DIR)/doomgeneric.zip"
159+
@cd $(CACHE_DIR) && unzip -qo doomgeneric.zip
160+
@mv "$(CACHE_DIR)/doomgeneric-$(DOOMGENERIC_COMMIT)" "$(CACHE_DIR)/doomgeneric"
161+
@rm -f "$(CACHE_DIR)/doomgeneric.zip"
162+
@# Fix out-of-bounds array reads in status bar icon rendering (crashes on WASM).
163+
@# Replace STlib_updateMultIcon with a bounds-safe version that validates
164+
@# both oldinum and *inum against the arms[] array size (2 entries).
165+
@# The approach: add pcount field, guard every mi->p[idx] access.
166+
@sed -i 's/ int\t\t\tdata;/ int\t\t\tdata;\n int\t\t\tpcount;/' \
167+
"$(SRC_DIR)/st_lib.h"
168+
@sed -i 's/i->data = 0;/i->data = 0;\n i->pcount = 0;/' \
169+
"$(SRC_DIR)/st_lib.c"
170+
@# Guard the oldinum erase path
171+
@sed -i 's/if (mi->oldinum != -1)/if (mi->oldinum >= 0 \&\& (mi->pcount <= 0 || mi->oldinum < mi->pcount))/' \
172+
"$(SRC_DIR)/st_lib.c"
173+
@# Guard the draw path
174+
@sed -i '/V_DrawPatch(mi->x, mi->y, mi->p\[\*mi->inum\]);/{s/.*/\tint __idx = *mi->inum; if (__idx >= 0 \&\& (mi->pcount <= 0 || __idx < mi->pcount)) V_DrawPatch(mi->x, mi->y, mi->p[__idx]);/}' \
175+
"$(SRC_DIR)/st_lib.c"
176+
@# Set pcount for the arms widgets (2 patches each: owned/not-owned)
177+
@sed -i '/STlib_initMultIcon(\&w_arms\[i\]/,/st_armson);/{s/\&st_armson);/\&st_armson); w_arms[i].pcount = 2;/}' \
178+
"$(SRC_DIR)/st_stuff.c"
179+
@echo "doomgeneric source ready (patched)"
180+
181+
fetch: $(SRC_DIR)/doomgeneric.h
182+
183+
# --- Fetch shareware WAD ---
184+
185+
$(BUILD_DIR)/doom1.wad:
186+
@echo "=== Downloading doom1.wad (shareware) ==="
187+
@mkdir -p $(BUILD_DIR)
188+
@curl -fSL "$(WAD_URL)" -o "$(BUILD_DIR)/doom1.wad"
189+
@echo "doom1.wad ready ($(shell du -h $(BUILD_DIR)/doom1.wad 2>/dev/null | cut -f1 || echo '?'))"
190+
191+
# --- Compile to WASM ---
192+
193+
$(BUILD_DIR)/doom: $(SRC_DIR)/doomgeneric.h $(BACKEND_SRC)
194+
@echo "=== Compiling Doom to WASM ==="
195+
@mkdir -p $(BUILD_DIR)
196+
$(CC) $(WASM_CFLAGS) \
197+
-Wl,--initial-memory=33554432 \
198+
-o $(BUILD_DIR)/doom.tmp.wasm \
199+
$(ENGINE_SRCS) $(BACKEND_SRC)
200+
@echo "Optimizing with wasm-opt..."
201+
wasm-opt -O3 --strip-debug $(BUILD_DIR)/doom.tmp.wasm -o $(BUILD_DIR)/doom
202+
@rm -f $(BUILD_DIR)/doom.tmp.wasm
203+
@echo "doom binary ready ($(shell du -h $(BUILD_DIR)/doom 2>/dev/null | cut -f1 || echo '?'))"
204+
205+
# --- Compile native (for testing without sandbox) ---
206+
207+
$(BUILD_DIR)/doom-native: $(SRC_DIR)/doomgeneric.h $(BACKEND_SRC)
208+
@echo "=== Compiling Doom (native) ==="
209+
@mkdir -p $(BUILD_DIR)
210+
$(NATIVE_CC) $(NATIVE_CFLAGS) \
211+
-o $(BUILD_DIR)/doom-native \
212+
$(ENGINE_SRCS) $(BACKEND_SRC) -lm
213+
214+
clean:
215+
rm -rf $(BUILD_DIR) $(CACHE_DIR)

examples/doom/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Doom in Secure Exec
2+
3+
Run the original 1993 Doom (shareware) inside a secure-exec sandbox, rendered
4+
in your terminal using ANSI true-color and Unicode half-block characters.
5+
6+
The Doom engine ([doomgeneric](https://github.com/ozkl/doomgeneric)) is compiled
7+
from C to WebAssembly using the same `wasi-sdk` toolchain the rest of the project
8+
uses. No WASM-specific code — just a standard POSIX terminal backend.
9+
10+
## Prerequisites
11+
12+
- wasi-sdk built at `native/wasmvm/c/vendor/wasi-sdk/` (`make wasi-sdk` in `native/wasmvm/c/`)
13+
- Patched sysroot at `native/wasmvm/c/sysroot/` (`make sysroot` in `native/wasmvm/c/`)
14+
- `wasm-opt` (binaryen) on PATH
15+
- A terminal with true-color support (most modern terminals)
16+
17+
## Build
18+
19+
```sh
20+
make build
21+
```
22+
23+
This downloads the doomgeneric source and `doom1.wad` (shareware, freely
24+
distributable by id Software), then compiles everything to a ~430KB WASM binary.
25+
26+
## Run
27+
28+
```sh
29+
pnpm tsx src/index.ts
30+
```
31+
32+
## Controls
33+
34+
| Key | Action |
35+
|----------------|----------------|
36+
| Arrow keys | Move / turn |
37+
| W/A/S/D | Move / strafe |
38+
| F | Fire |
39+
| Space | Open / use |
40+
| R | Run |
41+
| , / . | Strafe L / R |
42+
| Enter | Menu select |
43+
| Esc | Menu |
44+
| 1-7 | Switch weapon |
45+
| Q / Ctrl+C | Quit |
46+
47+
## How It Works
48+
49+
1. **C backend** (`c/doomgeneric_terminal.c`) implements 5 callbacks:
50+
- `DG_DrawFrame()` — converts BGRA framebuffer → ANSI half-block output
51+
- `DG_GetKey()` — reads keypresses via `poll()` + `read()` on stdin
52+
- `DG_GetTicksMs()` / `DG_SleepMs()``clock_gettime` / `nanosleep`
53+
- `DG_Init()` — switches to alternate screen buffer
54+
55+
2. **Makefile** compiles doomgeneric + our backend → `build/doom` (WASM)
56+
57+
3. **Node.js runner** (`src/index.ts`) creates a kernel, loads `doom1.wad`
58+
into the virtual filesystem, mounts the WASM runtime, and spawns doom
59+
via `kernel.spawn()` with raw stdin/stdout forwarding

0 commit comments

Comments
 (0)