Pure-Go DOOM (id Tech 1, 1993)
engine for bare-metal TamaGo + UEFI under the cloud-boot
loader. Sibling of the go-quake1 family
(go-quake1 / go-quake2 / go-quake3 — the id Tech 1/2/3 ports).
This repository is a fork of AndreRenaud/gore
at commit 7dc6c65493b8a29e0b93dd00dac21a2f10a0068a (2026-05-11).
gore itself is a Go transpilation of doomgeneric
performed with modernc.org/ccgo/v4
and then hand-cleaned. The engine has no CGO dependency, only standard
library, and exposes its host bindings through a small DoomFrontend
interface -- exactly the shape we need to wire DOOM into cloud-boot's
virtio device tree.
The upstream README.md is preserved as README.upstream.md.
cloud-boot is building an OS-agnostic OCI boot architecture on top of TamaGo + UEFI. To showcase Phase 3 we want to run classic DOOM as the "OS payload", with rendering, sound, and input wired straight to virtio devices instead of SDL / X11 / a kernel.
This fork therefore adds:
backend/tamago/-- a newDoomFrontendimplementation that drives cloud-boot's virtio-gpu, virtio-sound, and virtio-input drivers (currently stub interfaces; real wiring lands oncego-virtio/soundandgo-virtio/inputship in their sibling sprints).internal/embedwad/-- anio/fs.FSshim that serves a WAD blob from memory, so the engine can load the IWAD without a real filesystem.
The upstream gore engine (doom.go, ~990 KB of transpiled code, plus
doom_test.go) and its existing terminal / web / Ebitengine / SDL
examples are kept verbatim, so we can rebase against upstream cleanly.
- The repository as a whole is GPL-2.0-only (LICENSE). The
DOOM engine (
doom.go,doom_test.go) comes fromdoomgenericby way ofgoreunder GPL-2.0-only, and GPL-2.0 §2 puts a work based on it, as a whole, under the same terms. - Each file says which licence it is under, in an
SPDX-License-Identifierheader, and that header is authoritative (NOTICE.md):- GPL-2.0-only: the engine,
backend/,cmd/,embedwad/,ociblob/,wadcheck/; - GPL-2.0-or-later: the music bridge and most of
music/; - LGPL-2.1-or-later (LICENSE-LGPL-2.1):
music/opl/, the Go port of Nuked OPL3; - BSD-3-Clause (LICENSE-BSD):
netgame/.
- GPL-2.0-only: the engine,
- The rest of the cloud-boot stack is BSD-3-Clause and is not
affected: cloud-boot's bootloader, runtime, and host components do not
import this package. The integration happens via a thin separate
"livedoom" boot artifact whose only job is to call
godoom.Run. That artifact will itself be GPL-2.0, isolated to a single directory in the cloud-boot/tamago-uefi tree.
.
|-- doom.go # upstream gore engine (transpiled DOOM)
|-- doom_test.go # upstream gore reference-frame tests
|-- example/ # upstream gore examples (termdoom, web, ...)
|-- backend/
| |-- tamago/ # NEW: DoomFrontend over virtio-gpu/sound/input
| `-- wasmbox/ # NEW: DoomFrontend over the wasmbox desktop protocol
|-- cmd/
| `-- doom-wasmbox/ # NEW: the wasmbox desktop client (GOOS=js GOARCH=wasm)
|-- embedwad/ # NEW: io/fs.FS shim over an in-memory WAD
|-- ociblob/ # NEW: pull + verify one file from an OCI /v2 mirror
|-- wadcheck/ # NEW: bound a WAD's header + lump directory (fuzzed)
|-- state.go # NEW: read-only game-state snapshot (gore.Observe)
|-- PORT.md # Adaptation plan (read this)
`-- README.md
| Aspect | Status |
|---|---|
| Pure Go (CGO=0) | yes (engine + all new code) |
| Builds on Go 1.27.1 | yes (lib + tamago backend + pure-Go examples) |
| Cross-compiles linux/amd64 | yes (CGO=0) |
| Cross-compiles linux/arm64 | yes (CGO=0) |
| Runs shareware DOOM1.WAD | yes (engine ticks; verified via TestMenus harness) |
| TamaGo backend wired | scaffold only; real drivers land in follow-up sprint |
| Music (MUS/MIDI + OPL2/OPL3 synth) | yes -- pure-Go OPL path (see music/) |
| Multiplayer (netgame lockstep) | yes -- protocol + lockstep (see netgame/) |
| wasmbox desktop client | yes -- cmd/doom-wasmbox, Freedoom, software renderer (see below) |
cmd/doom-wasmbox runs the engine as a window of the
wasmbox web desktop, beside Quake
(launch it from the root menu: Applications -> Doom). It is a Web Worker
client of the wasmbox protocol (backend/wasmbox): each 320x200 frame is
scaled to 640x480 (4:3, the proportions Doom was drawn for), copied into the
window's SharedArrayBuffer under the protocol's seqlock and committed; the
compositor scale-fits it to the window and locks the resize grip to 4:3.
Keys are the classic ones (arrows, Ctrl fire, Space use, Shift run, Alt
strafe, , . strafe, Escape menu, Enter, digits for weapons); the left
mouse button fires and the right one uses.
Game data is Freedoom: Phase 1 0.13.0
(BSD-3-Clause), never committed anywhere: the wasmbox site workflow downloads
the official release, checks it against the release's signed CHECKSUM file,
and serves freedoom1.wad from a same-origin /v2/doom-assets OCI mirror.
The client pulls it with ociblob and trusts nothing it receives:
- it fetches only from its own origin (the worker publishes the site root; the client refuses any other origin);
- the WAD's sha256 is pinned in the binary (
main.WADSHA256), the manifest must name exactly that digest, and the blob's size and sha256 are recomputed (64 MiB cap); wadcheckthen bounds the header and every lump offset and size against the file and the engine's 4095-lump table before the engine opens it -- the transpiled loader itself has no bounds checks. Both parsers are fuzzed in CI.
Build it with GOOS=js GOARCH=wasm go build ./cmd/doom-wasmbox; the wasmbox
repository's Pages and CI workflows do that and assemble the mirror. The
client logs DOOM: ... lines (DOOM: state=... on every game-state change,
DOOM: fps=... every five seconds) that wasmbox's browser tests assert on.
Sound effects (DMX lumps, decoded and panned as chocolate-doom does) and OPL
music play in the wasmbox window through wasmbox's audio relay. A dedicated
worker has no AudioContext, so the client mixes the effects (Mixer) with the
OPL music at 44.1 kHz and writes interleaved float32 PCM into a lock-free
SharedArrayBuffer ring
(github.com/wasmdesk/wasmbox/audioring);
the wasmbox page plays it from an AudioWorklet, resuming on the first click or
key. The ring is 4096 frames (93 ms) and is kept about half full. The client
logs audio=relay (running once the page has played frames). A worker-scope
AudioContext, where one exists, remains the fallback; with neither, the client
runs silent and logs audio=unavailable. wasmbox's test/probe-audio.mjs
checks it in headless Chromium against the real client, including a planted
negative with the writer disabled.
The chocolate-doom-faithful music and netgame paths ship as standalone, differentially-tested packages, all CGO-free and building on the six 64-bit arches:
| Package | Role |
|---|---|
music/mus |
DMX MUS (D_* lumps) parse + mus2mid MUS->MIDI conversion |
music/midi |
Standard MIDI File parser (port of midifile.c) |
music/genmidi |
GENMIDI lump (the DMX OPL instrument bank) parser |
music/opl |
Nuked OPL3 (Yamaha YMF262) emulator -> PCM |
music/oplplayer |
MIDI + GENMIDI -> OPL register writes -> PCM (i_oplmusic) |
netgame |
classic doomcom/ticcmd lockstep protocol + transport seam |
The engine installs the OPL synth as its music_module (see music_opl.go);
the host frontend pulls synthesised stereo PCM through gore.ReadMusicPCM.
Differential oracles. music/mus conversion is asserted byte-identical
to chocolate-doom's mus2mid.c (compiled as an oracle over synthetic MUS
vectors). music/opl is validated bit-exact against a Nuked OPL3 C
register->PCM trace. music/oplplayer is checked against a captured
i_oplmusic.c register-write trace for D_INTRO. netgame runs two, three
and four in-process engines over an in-memory transport and asserts they stay in
lockstep with byte-identical per-tic state hashes; ticcmd serialisation is
byte-faithful to the engine's own vanilla layout.
Music and multiplayer do not perturb the frame/audio determinism oracles: the
oracle/ frames and audio-event log remain byte-equal after wiring.
# 1. fetch the shareware IWAD (free re-release):
curl -sSL -o doom1.wad https://distro.ibiblio.org/slitaz/sources/packages/d/doom1.wad
# 2. terminal renderer (pure Go):
CGO_ENABLED=0 go run ./example/termdoom -iwad doom1.wadThe TamaGo backend in backend/tamago/ consumes three driver
interfaces declared in that package:
| Frontend method | Backend interface | Driver repo (sibling) |
|---|---|---|
DrawFrame(*image.RGBA) |
GPU.Flip |
github.com/go-virtio/gpu |
CacheSound / PlaySound |
Sound.Cache/Play |
github.com/go-virtio/sound |
GetEvent(*DoomEvent) |
Input.Poll |
github.com/go-virtio/input |
The WAD is delivered to the engine via gore.SetVirtualFileSystem(fs.FS)
using the internal/embedwad helper, whose backing bytes can come from
either (a) a go:embed blob in the cloud-boot "livedoom" boot artifact,
or (b) a runtime stream from a cloud-boot OCI artifact mount.
The actual wire-in (phase3_oci_doom_boot.go in cloud-boot/tamago-uefi)
is a follow-up sprint and is intentionally NOT done here.
See PORT.md for the detailed adaptation plan.
The provable test protocol lives in
cloud-boot/docs/docs/architecture/doom-provable-protocol.md.
Short version:
# Re-harvest the host oracle (byte-equal reproducible — proves
# engine + WAD + determinism hooks are intact):
cd go-doom/engine
GOWORK=off go run ./cmd/harvest-reference \
-wad embedwad/doom1.wad \
-seed 0 \
-checkpoints 1,35,70,140,350,1050 \
-out /tmp/fresh-oracle
# Compare against the committed oracle — every SHA must match.
diff <(cd oracle && sha256sum frame_tic*.ppm) \
<(cd /tmp/fresh-oracle && sha256sum frame_tic*.ppm)For the full guest-stack gate (boots the EFI probe in QEMU+EDK2 and compares virtio-gpu output against the guest oracle):
cd cloud-boot/tamago-uefi
task doomboot:efi:amd64
bash internal/livedoomboot/provable_test.sh amd64The oracle/ directory is committed (~1.1 MiB of PPMs + manifest).
The WAD itself is .gitignored (see embedwad/.gitignore). The
determinism hooks live in seed.go (sibling of doom.go, never
modifying the transpiled engine source).
