This is the beginner walkthrough. By the end you will have DOOM running on a new backend by writing one file with six functions. No prior DOOM-internals knowledge required.
Prerequisite mental model: read the top of
include/doomkit/doomkit.h(the power-socket analogy) and skimCONTRACT.md. That's all the theory you need. Hit an unfamiliar word? SeeGLOSSARY.md.
Every port needs a C compiler and make. Then add whatever your target requires. You don't need all of these — only the row you're building.
| You're building… | Install | Quick check |
|---|---|---|
| Anything (the base) | a C compiler + make — macOS: xcode-select --install; Debian/Ubuntu: sudo apt install build-essential; Windows: MSYS2/MinGW or WSL |
cc --version && make --version |
| The SDL desktop port | SDL2 dev libs — macOS: brew install sdl2; Debian/Ubuntu: sudo apt install libsdl2-dev |
sdl2-config --version |
| The browser (wasm) port | the Emscripten SDK | emcc --version |
| A language binding | that language's toolchain (Go, .NET, JDK 22+, Python 3, Rust/Cargo, Node.js) | e.g. go version, python3 --version |
| The Android port | Android Studio + the NDK | open the project in Android Studio |
You also always need the engine sources and a WAD (next step) for any
build that actually plays DOOM — but not for make test or make run-null,
which run with only the base compiler.
You need three things in one build:
- This package — the contract header (
include/) and helpers (src/). - The DOOM engine sources — the upstream
doomgeneric.c/.hfiles that providedoomgeneric_Create()/doomgeneric_Tick()and the whole game. This package does not ship them (they are unchanged GPL code). Get them from https://github.com/ozkl/doomgeneric and put theirdoomgeneric/folder where your build can see it. - A WAD file — the game data. The free, redistributable Freedoom IWAD
or the shareware
doom1.wadboth work. Where to get one (legally) and how to point the engine at it: WAD.md.
cp examples/platforms/template/platform_template.c platform_myplatform.cIt already contains main() and empty versions of the six callbacks, wired to
the dg_keyqueue helper.
Implement them against your platform's API. Use examples/platforms/sdl/platform_sdl.c as
a worked reference and examples/platforms/null/platform_null.c to see them run with no
backend at all. For a halfway point — the six callbacks driving the real engine
with no display — see examples/platforms/null/platform_null_engine.c
(make run-null-engine ENGINE=... WAD=...).
DG_Init— open a window/surface ofDOOMGENERIC_RESX × DOOMGENERIC_RESYand initialise input. Calldg_keyqueue_init(&my_queue).DG_DrawFrame— copyDG_ScreenBuffer(32-bit0x00RRGGBBpixels) to your display, then pump your OS event queue and, for each key, do:If your platform's key codes aren't SDL-like, copydg_keyqueue_push(&my_queue, is_down ? 1 : 0, dg_keymap_from_sdl(host_key));
dg_keymap_from_sdland swap thecaselabels — the shape stays the same.DG_SleepMs— sleep the given milliseconds.DG_GetTicksMs— return a monotonic millisecond counter.DG_GetKey—return dg_keyqueue_pop(&my_queue, pressed, doomKey);DG_SetWindowTitle— set the title, or leave empty.
Compile your platform file + this package's src/ + the engine sources:
cc -Iinclude -I<engine_dir> \
platform_myplatform.c \
src/dg_keyqueue.c src/dg_keymap.c \
<engine_dir>/*.c \
<your platform libs> \
-o doomA bare <engine_dir>/*.c glob is fine for a first try, but it pulls in files
that conflict with your platform file or need libraries you don't have. Two
categories must be excluded:
- Platform backends (
doomgeneric_allegro.c,doomgeneric_sdl.c,doomgeneric_xlib.c, …) — each defines its ownDG_*symbols, which clash with the ones inplatform_myplatform.c(duplicate-symbol link errors). Only your platform file should provide them. - Sound/driver stubs (
i_allegrosound.c,i_allegromusic.c,i_sdlsound.c,i_sdlmusic.c,gusconf.c,mus2mid.c,icon.c) — they require Allegro, SDL_mixer, or platform headers, and sound is out of scope for the six visualDG_*callbacks this package covers.
Replace the glob line with a filtered one (adjust the names for whatever backends your engine checkout ships):
$(ls <engine_dir>/*.c | grep -vE '(doomgeneric_|i_allegro|i_sdlsound|i_sdlmusic|gusconf|mus2mid|icon)')The doomgeneric_ prefix filter is safe because the engine core is plain
doomgeneric.c (no underscore) — only the doomgeneric_*.c files are backends.
This is exactly what the SDL port does — see
examples/platforms/sdl/README.md for
the full, runnable command.
(For pixel conversion the engine already contains its own i_video.c; the
dg_palette / dg_framebuffer helpers in this package are the clean, tested
reference for that same math — useful if you write a from-scratch video path.)
./doom -iwad doom1.wadYou should get a window with the title screen. Arrow keys move, Ctrl fires,
Space opens doors, Esc opens the menu.
No window backend yet (or running in CI)? You can still confirm the engine and your build wiring work headless:
make run-null-engine ENGINE=... WAD=...links the real engine to a no-display port, ticks it ~10 s, and writes a real DOOM frame tobuild/frame_engine.ppm.
| Symptom | Likely cause |
|---|---|
| Black window, no image | DG_DrawFrame not copying DG_ScreenBuffer, or wrong pitch — use DOOMGENERIC_RESX * sizeof(pixel_t) bytes per row. |
| Colours look swapped (red/blue) | Your display expects a different channel order; adjust the shifts (see dg_palette's red_shift/blue_shift) or your texture format. |
| Game runs too fast / too slow | DG_GetTicksMs not in milliseconds, or not monotonic. |
| Keys do nothing | You queued host codes instead of DOOM codes — run them through dg_keymap first. |
| Image stuck in a corner | You ignored the centring offset; see dg_framebuffer / examples/platforms/null. |
A single platform_myplatform.c, no engine edits, DOOM playable. That's the
whole point of doomkit: the hard part (a full 3D game) is already solved; you
only teach it about your screen, clock and keyboard.