--- /dev/null
+# DOOM for AdrOS — Build system
+#
+# Prerequisites:
+# 1. Clone doomgeneric into this directory:
+# git clone https://github.com/ozkl/doomgeneric.git
+# 2. Place DOOM1.WAD (shareware) in this directory
+# 3. Run: make
+#
+# The output is doom.elf which gets packaged into the AdrOS initrd.
+
+CC := i686-elf-gcc
+AR := i686-elf-ar
+
+ULIBC_DIR := ../ulibc
+ULIBC_INC := $(ULIBC_DIR)/include
+ULIBC_LIB := $(ULIBC_DIR)/libulibc.a
+CRT0 := $(ULIBC_DIR)/src/crt0.o
+LINKER := ../linker.ld
+
+CFLAGS := -m32 -O2 -ffreestanding -fno-pie -no-pie -nostdlib \
+ -Wall -Wno-unused-parameter -Wno-sign-compare -Wno-pointer-sign \
+ -Wno-missing-field-initializers -Wno-implicit-fallthrough \
+ -I$(ULIBC_INC) -I. -Idoomgeneric \
+ -DNORMALUNIX -DLINUX
+
+LDFLAGS := -m32 -nostdlib -fno-pie -no-pie -Wl,-T,$(LINKER)
+
+# doomgeneric engine sources
+DG_SRC := $(wildcard doomgeneric/*.c)
+DG_OBJ := $(DG_SRC:.c=.o)
+
+# AdrOS adapter
+ADAPTER_OBJ := doomgeneric_adros.o
+
+ALL_OBJ := $(DG_OBJ) $(ADAPTER_OBJ)
+
+.PHONY: all clean check-doomgeneric
+
+all: check-doomgeneric doom.elf
+
+check-doomgeneric:
+ @if [ ! -d doomgeneric ]; then \
+ echo "ERROR: doomgeneric/ directory not found."; \
+ echo "Run: git clone https://github.com/ozkl/doomgeneric.git"; \
+ exit 1; \
+ fi
+
+doom.elf: $(ALL_OBJ) $(ULIBC_LIB) $(CRT0)
+ @$(CC) $(LDFLAGS) -o $@ $(CRT0) $(ALL_OBJ) $(ULIBC_LIB) -lgcc
+ @echo " LD $@"
+
+doomgeneric/%.o: doomgeneric/%.c
+ @$(CC) $(CFLAGS) -c $< -o $@
+ @echo " CC $<"
+
+%.o: %.c
+ @$(CC) $(CFLAGS) -c $< -o $@
+ @echo " CC $<"
+
+clean:
+ rm -f $(ALL_OBJ) doom.elf
+
--- /dev/null
+# DOOM on AdrOS
+
+Port of DOOM using [doomgeneric](https://github.com/ozkl/doomgeneric) with an AdrOS-specific platform adapter.
+
+## Prerequisites
+
+- AdrOS cross-compiler (`i686-elf-gcc`)
+- ulibc built (`../ulibc/libulibc.a`)
+- DOOM1.WAD (shareware, freely distributable)
+
+## Setup
+
+```bash
+# 1. Clone doomgeneric into this directory
+cd user/doom
+git clone https://github.com/ozkl/doomgeneric.git
+
+# 2. Download DOOM1.WAD shareware
+# Place doom1.wad in the AdrOS filesystem (e.g., /bin/doom1.wad in the initrd)
+
+# 3. Build
+make
+
+# 4. The resulting doom.elf must be added to the initrd via mkinitrd
+```
+
+## Architecture
+
+```
+user/doom/
+├── doomgeneric/ # cloned engine source (~70 C files)
+├── doomgeneric_adros.c # AdrOS platform adapter
+├── Makefile # build rules
+└── README.md
+```
+
+### Platform Adapter (`doomgeneric_adros.c`)
+
+Implements the doomgeneric interface functions:
+
+| Function | AdrOS Implementation |
+|----------|---------------------|
+| `DG_Init` | Opens `/dev/fb0`, queries resolution via ioctl, mmaps framebuffer; opens `/dev/kbd` |
+| `DG_DrawFrame` | Nearest-neighbor scales 320×200 DOOM buffer to physical framebuffer |
+| `DG_GetKey` | Reads raw PS/2 scancodes from `/dev/kbd`, maps to DOOM key codes |
+| `DG_GetTicksMs` | `clock_gettime(CLOCK_MONOTONIC)` |
+| `DG_SleepMs` | `nanosleep()` |
+| `DG_SetWindowTitle` | No-op |
+
+### Kernel Requirements (all implemented)
+
+- `/dev/fb0` — framebuffer device with `ioctl` + `mmap`
+- `/dev/kbd` — raw PS/2 scancode device (non-blocking read)
+- `mmap` syscall — anonymous + fd-backed
+- `brk` syscall — dynamic heap growth
+- `nanosleep` / `clock_gettime` — frame timing
+- Standard file I/O — WAD file loading (`open`/`read`/`lseek`/`close`)
+
+## Running
+
+From AdrOS shell (or as init process):
+```
+/bin/doom.elf -iwad /bin/doom1.wad
+```
+
+QEMU must be started with a VBE framebuffer (not text mode):
+```
+qemu-system-i386 -cdrom adros-x86.iso -vga std
+```
--- /dev/null
+/*
+ * doomgeneric_adros.c — AdrOS platform adapter for doomgeneric
+ *
+ * Implements the DG_* interface that the doomgeneric engine requires.
+ * Uses /dev/fb0 (mmap) for video, /dev/kbd for raw scancode input,
+ * and clock_gettime/nanosleep for timing.
+ */
+
+#include "doomgeneric/doomgeneric.h"
+#include "doomgeneric/doomkeys.h"
+
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+
+/* AdrOS ulibc headers */
+#include "unistd.h"
+#include "stdio.h"
+#include "sys/mman.h"
+#include "sys/ioctl.h"
+#include "time.h"
+
+/* Framebuffer ioctl definitions (must match kernel's fb.h) */
+#define FBIOGET_VSCREENINFO 0x4600
+#define FBIOGET_FSCREENINFO 0x4602
+
+struct fb_var_screeninfo {
+ uint32_t xres;
+ uint32_t yres;
+ uint32_t bits_per_pixel;
+};
+
+struct fb_fix_screeninfo {
+ uint32_t smem_start;
+ uint32_t smem_len;
+ uint32_t line_length;
+};
+
+/* ---- State ---- */
+
+static int fb_fd = -1;
+static int kbd_fd = -1;
+static uint32_t* framebuffer = NULL;
+static uint32_t fb_width = 0;
+static uint32_t fb_height = 0;
+static uint32_t fb_pitch = 0; /* bytes per line */
+static uint32_t fb_size = 0;
+
+/* Scancode ring buffer for key events */
+#define KEY_QUEUE_SIZE 64
+static struct {
+ int pressed;
+ unsigned char key;
+} key_queue[KEY_QUEUE_SIZE];
+static int key_queue_head = 0;
+static int key_queue_tail = 0;
+
+static void key_queue_push(int pressed, unsigned char key) {
+ int next = (key_queue_head + 1) % KEY_QUEUE_SIZE;
+ if (next == key_queue_tail) return; /* full, drop */
+ key_queue[key_queue_head].pressed = pressed;
+ key_queue[key_queue_head].key = key;
+ key_queue_head = next;
+}
+
+/* PS/2 Set 1 scancode to DOOM key mapping */
+static unsigned char scancode_to_doomkey(uint8_t sc) {
+ switch (sc & 0x7F) {
+ case 0x01: return KEY_ESCAPE;
+ case 0x1C: return KEY_ENTER;
+ case 0x0F: return KEY_TAB;
+ case 0x39: return KEY_USE; /* space */
+ case 0x1D: return KEY_FIRE; /* left ctrl */
+ case 0x2A: return KEY_RSHIFT; /* left shift */
+ case 0x38: return KEY_LALT; /* left alt */
+
+ /* Arrow keys */
+ case 0x48: return KEY_UPARROW;
+ case 0x50: return KEY_DOWNARROW;
+ case 0x4B: return KEY_LEFTARROW;
+ case 0x4D: return KEY_RIGHTARROW;
+
+ /* WASD */
+ case 0x11: return KEY_UPARROW; /* W */
+ case 0x1F: return KEY_DOWNARROW; /* S */
+ case 0x1E: return KEY_LEFTARROW; /* A */
+ case 0x20: return KEY_RIGHTARROW; /* D */
+
+ /* Number keys 1-9,0 for weapon select */
+ case 0x02: return '1';
+ case 0x03: return '2';
+ case 0x04: return '3';
+ case 0x05: return '4';
+ case 0x06: return '5';
+ case 0x07: return '6';
+ case 0x08: return '7';
+ case 0x09: return '8';
+ case 0x0A: return '9';
+ case 0x0B: return '0';
+
+ case 0x0E: return KEY_BACKSPACE;
+ case 0x19: return 'p'; /* pause */
+ case 0x32: return 'm'; /* map toggle */
+ case 0x15: return 'y';
+ case 0x31: return 'n';
+
+ /* F1-F12 */
+ case 0x3B: return KEY_F1;
+ case 0x3C: return KEY_F2;
+ case 0x3D: return KEY_F3;
+ case 0x3E: return KEY_F4;
+ case 0x3F: return KEY_F5;
+ case 0x40: return KEY_F6;
+ case 0x41: return KEY_F7;
+ case 0x42: return KEY_F8;
+ case 0x43: return KEY_F9;
+ case 0x44: return KEY_F10;
+ case 0x57: return KEY_F11;
+ case 0x58: return KEY_F12;
+
+ case 0x0C: return KEY_MINUS;
+ case 0x0D: return KEY_EQUALS;
+
+ default: return 0;
+ }
+}
+
+static void poll_keyboard(void) {
+ uint8_t buf[32];
+ int n = read(kbd_fd, buf, sizeof(buf));
+ if (n <= 0) return;
+ for (int i = 0; i < n; i++) {
+ uint8_t sc = buf[i];
+ int pressed = !(sc & 0x80);
+ unsigned char dk = scancode_to_doomkey(sc);
+ if (dk) {
+ key_queue_push(pressed, dk);
+ }
+ }
+}
+
+/* ---- DG interface implementation ---- */
+
+void DG_Init(void) {
+ /* Open framebuffer */
+ fb_fd = open("/dev/fb0", 0 /* O_RDONLY */);
+ if (fb_fd < 0) {
+ printf("[DOOM] Cannot open /dev/fb0\n");
+ _exit(1);
+ }
+
+ struct fb_var_screeninfo vinfo;
+ struct fb_fix_screeninfo finfo;
+ if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) {
+ printf("[DOOM] ioctl FBIOGET_VSCREENINFO failed\n");
+ _exit(1);
+ }
+ if (ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0) {
+ printf("[DOOM] ioctl FBIOGET_FSCREENINFO failed\n");
+ _exit(1);
+ }
+
+ fb_width = vinfo.xres;
+ fb_height = vinfo.yres;
+ fb_pitch = finfo.line_length;
+ fb_size = finfo.smem_len;
+
+ printf("[DOOM] Framebuffer: %ux%u %ubpp pitch=%u size=%u\n",
+ fb_width, fb_height, vinfo.bits_per_pixel, fb_pitch, fb_size);
+
+ /* mmap the framebuffer */
+ framebuffer = (uint32_t*)mmap(NULL, fb_size,
+ PROT_READ | PROT_WRITE,
+ MAP_SHARED, fb_fd, 0);
+ if (framebuffer == MAP_FAILED) {
+ printf("[DOOM] mmap /dev/fb0 failed\n");
+ _exit(1);
+ }
+
+ /* Open raw keyboard */
+ kbd_fd = open("/dev/kbd", 0);
+ if (kbd_fd < 0) {
+ printf("[DOOM] Cannot open /dev/kbd\n");
+ _exit(1);
+ }
+
+ printf("[DOOM] AdrOS adapter initialized.\n");
+}
+
+void DG_DrawFrame(void) {
+ if (!framebuffer || !DG_ScreenBuffer) return;
+
+ /*
+ * DG_ScreenBuffer is DOOMGENERIC_RESX * DOOMGENERIC_RESY in ARGB format.
+ * Scale to the physical framebuffer using nearest-neighbor.
+ */
+ uint32_t sx = (DOOMGENERIC_RESX > 0) ? fb_width / DOOMGENERIC_RESX : 1;
+ uint32_t sy = (DOOMGENERIC_RESY > 0) ? fb_height / DOOMGENERIC_RESY : 1;
+ uint32_t scale = (sx < sy) ? sx : sy;
+ if (scale == 0) scale = 1;
+
+ uint32_t off_x = (fb_width - DOOMGENERIC_RESX * scale) / 2;
+ uint32_t off_y = (fb_height - DOOMGENERIC_RESY * scale) / 2;
+
+ for (uint32_t y = 0; y < DOOMGENERIC_RESY; y++) {
+ uint32_t* src_row = &DG_ScreenBuffer[y * DOOMGENERIC_RESX];
+ for (uint32_t dy = 0; dy < scale; dy++) {
+ uint32_t fb_y = off_y + y * scale + dy;
+ if (fb_y >= fb_height) break;
+ uint32_t* dst_row = (uint32_t*)((uint8_t*)framebuffer + fb_y * fb_pitch);
+ for (uint32_t x = 0; x < DOOMGENERIC_RESX; x++) {
+ uint32_t pixel = src_row[x];
+ for (uint32_t dx = 0; dx < scale; dx++) {
+ uint32_t fb_x = off_x + x * scale + dx;
+ if (fb_x < fb_width) {
+ dst_row[fb_x] = pixel;
+ }
+ }
+ }
+ }
+ }
+}
+
+void DG_SleepMs(uint32_t ms) {
+ struct timespec ts;
+ ts.tv_sec = ms / 1000;
+ ts.tv_nsec = (ms % 1000) * 1000000;
+ nanosleep(&ts, NULL);
+}
+
+uint32_t DG_GetTicksMs(void) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+}
+
+int DG_GetKey(int* pressed, unsigned char* doomKey) {
+ /* Poll for new scancodes */
+ if (kbd_fd >= 0) {
+ poll_keyboard();
+ }
+
+ if (key_queue_tail == key_queue_head) return 0;
+
+ *pressed = key_queue[key_queue_tail].pressed;
+ *doomKey = key_queue[key_queue_tail].key;
+ key_queue_tail = (key_queue_tail + 1) % KEY_QUEUE_SIZE;
+ return 1;
+}
+
+void DG_SetWindowTitle(const char* title) {
+ (void)title;
+}