From 0e5e33ac0ef604e216951c1e87ae3d761f3c5072 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Fri, 7 Aug 2026 17:05:34 +0200 Subject: [PATCH] initial commit - doom port --- .gitignore | 6 + .gitmodules | 3 + Makefile | 63 ++++ README.md | 26 ++ games-fps/doom/Makefile | 101 +++++++ games-fps/doom/doom.svg | 1 + games-fps/doom/manifest.toml | 8 + games-fps/doom/src/SDL_mixer.h | 4 + games-fps/doom/src/doom_wad_check.cpp | 22 ++ games-fps/doom/src/doomgeneric_montauk.c | 269 +++++++++++++++++ games-fps/doom/src/i_sound_montauk.c | 362 +++++++++++++++++++++++ games-fps/doom/upstream | 1 + mk/port.mk | 131 ++++++++ 13 files changed, 997 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 README.md create mode 100644 games-fps/doom/Makefile create mode 100644 games-fps/doom/doom.svg create mode 100644 games-fps/doom/manifest.toml create mode 100644 games-fps/doom/src/SDL_mixer.h create mode 100644 games-fps/doom/src/doom_wad_check.cpp create mode 100644 games-fps/doom/src/doomgeneric_montauk.c create mode 100644 games-fps/doom/src/i_sound_montauk.c create mode 160000 games-fps/doom/upstream create mode 100644 mk/port.mk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d256d26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +out/ +obj/ +*.o +*.d +*.a +*.elf diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c20dac0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "games-fps/doom/upstream"] + path = games-fps/doom/upstream + url = https://github.com/ozkl/doomgeneric.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..49b5a9f --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +# Makefile - MontaukOS ports tree +# +# make Build every port into out/apps// +# make doom Build a single port (shorthand for its category path) +# make games-fps/doom Same, by full path +# make install Copy built bundles into the MontaukOS ramdisk tree +# make clean Remove build artifacts +# +# Ports build against a MontaukOS checkout; override its location with +# MONTAUKOS=/path/to/MontaukOS (default ~/dev/MontaukOS). + +MAKEFLAGS += -rR +.SUFFIXES: + +MTKPORTS_ROOT := $(CURDIR) +export MTKPORTS_ROOT + +MONTAUKOS ?= $(HOME)/dev/MontaukOS +export MONTAUKOS + +OUTDIR := $(MTKPORTS_ROOT)/out +export OUTDIR + +# Ports to build, as /, in dependency order: library ports +# come before the applications that link them. Categories follow Gentoo +# naming (dev-libs, media-libs, net-libs, www-client, games-fps, ...). +PORTS := \ + games-fps/doom + +# Bundle directories are keyed by port name, not category path. +PORT_NAMES := $(notdir $(PORTS)) + +.PHONY: all clean install $(PORTS) $(PORT_NAMES) + +all: $(PORTS) + +$(PORTS): + $(MAKE) -C $@ + +# Shorthand target per port name, so `make doom` works as well as +# `make games-fps/doom`. +define PORT_ALIAS +$(notdir $(1)): $(1) +endef +$(foreach p,$(PORTS),$(eval $(call PORT_ALIAS,$(p)))) + +# Drop finished bundles into the OS ramdisk staging tree. install_apps.sh +# builds in-tree app bundles at the same path; ports simply add to it. +install: all + @mkdir -p $(MONTAUKOS)/programs/bin/apps + @for p in $(PORTS); do \ + n=$$(basename $$p); \ + if [ -d "$(OUTDIR)/apps/$$n" ]; then \ + cp -r "$(OUTDIR)/apps/$$n" "$(MONTAUKOS)/programs/bin/apps/"; \ + echo "installed port: $$p"; \ + fi; \ + done + @echo "ports: installed into $(MONTAUKOS)/programs/bin/apps/" + @echo "ports: run 'make ramdisk' (or 'make') in $(MONTAUKOS) to rebuild the image" + +clean: + @for p in $(PORTS); do $(MAKE) -C $$p clean; done + rm -rf $(OUTDIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3b70a7 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# mtkports + +MontaukOS software ports + +## Build + +```bash +git clone --recurse-submodules ~/dev/mtkports +cd ~/dev/mtkports +make # build every port into out/apps// +make doom # build one port +make install # copy bundles into MontaukOS/programs/bin/apps/ +``` + +Then from the MontaukOS tree: + +```bash +make ports # builds + installs ports (equivalent to the above) +make ramdisk +``` + +## Ports + +| Port | Upstream | Notes | +|------|----------|-------| +| `doom` | [ozkl/doomgeneric](https://github.com/ozkl/doomgeneric) | `doom1.wad` is proprietary and deliberately not bundled. | diff --git a/games-fps/doom/Makefile b/games-fps/doom/Makefile new file mode 100644 index 0000000..521e825 --- /dev/null +++ b/games-fps/doom/Makefile @@ -0,0 +1,101 @@ +# DOOM (doomgeneric) for MontaukOS +# +# Upstream: https://github.com/ozkl/doomgeneric (submodule at upstream/, +# pinned; no local patches). MontaukOS support lives entirely in src/: +# doomgeneric_montauk.c (video/input), i_sound_montauk.c (audio), +# doom_wad_check.cpp (missing-WAD dialog). +# +# doom1.wad is proprietary id Software content and is deliberately NOT +# bundled; the engine (GPLv2) ships without it. + +PORT_NAME := doom +PORT_ICON := doom.svg + +include ../../mk/port.mk + +# ==== Paths =========================================================== + +DOOM_SRC := upstream/doomgeneric +LOCAL_SRC := src + +# ==== Flags =========================================================== + +CFLAGS := \ + -std=gnu11 \ + $(PORT_CFLAGS_COMMON) \ + -Wall \ + -Wno-unused-parameter \ + -Wno-unused-variable \ + -Wno-missing-field-initializers \ + -Wno-sign-compare \ + -Wno-pointer-sign \ + -DNORMALUNIX \ + -DLINUX \ + -DFEATURE_SOUND \ + -I $(LOCAL_SRC) \ + -I $(DOOM_SRC) \ + -isystem $(shell $(CC) -print-file-name=include) + +CXXFLAGS := \ + -std=gnu++20 \ + $(PORT_CFLAGS_COMMON) \ + -Wall \ + -Wno-unused-parameter \ + -fno-rtti \ + -fno-exceptions \ + -I $(LOCAL_SRC) + +LDFLAGS := $(PORT_LDFLAGS_COMMON) + +LIBS := $(LIBLOADER_A) $(LIBC_A) + +# ==== Sources ========================================================= +# Upstream engine sources, excluding platform ports and sound backends. + +DOOM_SRCS := \ + am_map.c d_event.c d_items.c d_iwad.c d_loop.c d_main.c d_mode.c \ + d_net.c doomdef.c doomgeneric.c doomstat.c dstrings.c dummy.c \ + f_finale.c f_wipe.c g_game.c gusconf.c hu_lib.c hu_stuff.c i_cdmus.c \ + i_endoom.c i_input.c i_joystick.c i_scale.c i_sound.c i_system.c \ + i_timer.c i_video.c info.c m_argv.c m_bbox.c m_cheat.c m_config.c \ + m_controls.c m_fixed.c m_menu.c m_misc.c m_random.c memio.c mus2mid.c \ + p_ceilng.c p_doors.c p_enemy.c p_floor.c p_inter.c p_lights.c p_map.c \ + p_maputl.c p_mobj.c p_plats.c p_pspr.c p_saveg.c p_setup.c p_sight.c \ + p_spec.c p_switch.c p_telept.c p_tick.c p_user.c r_bsp.c r_data.c \ + r_draw.c r_main.c r_plane.c r_segs.c r_sky.c r_things.c s_sound.c \ + sha1.c sounds.c st_lib.c st_stuff.c statdump.c tables.c v_video.c \ + w_checksum.c w_file.c w_file_stdc.c w_main.c w_wad.c wi_stuff.c \ + z_zone.c + +LOCAL_SRCS := doomgeneric_montauk.c i_sound_montauk.c +LOCAL_CXX_SRCS := doom_wad_check.cpp + +DOOM_OBJS := $(addprefix $(OBJDIR)/upstream/,$(DOOM_SRCS:.c=.o)) +LOCAL_OBJS := $(addprefix $(OBJDIR)/local/,$(LOCAL_SRCS:.c=.o)) +LOCAL_CXX_OBJS := $(addprefix $(OBJDIR)/local/,$(LOCAL_CXX_SRCS:.cpp=.o)) +ALL_OBJS := $(DOOM_OBJS) $(LOCAL_OBJS) $(LOCAL_CXX_OBJS) + +# ==== Rules =========================================================== + +.PHONY: all clean +all: bundle + +$(TARGET): $(ALL_OBJS) $(SDK_LINK_LD) Makefile $(LIBS) | $(BUNDLE_DIR) + $(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) $(LIBS) -o $@ + @echo "Built: $@ (SDK apiVersion $(SDK_API_VERSION))" + +$(OBJDIR)/upstream/%.o: $(DOOM_SRC)/%.c Makefile + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -MMD -MP -c $< -o $@ + +$(OBJDIR)/local/%.o: $(LOCAL_SRC)/%.c Makefile + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -MMD -MP -c $< -o $@ + +$(OBJDIR)/local/%.o: $(LOCAL_SRC)/%.cpp Makefile + @mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@ + +clean: clean-bundle + +-include $(ALL_OBJS:.o=.d) diff --git a/games-fps/doom/doom.svg b/games-fps/doom/doom.svg new file mode 100644 index 0000000..5975a8c --- /dev/null +++ b/games-fps/doom/doom.svg @@ -0,0 +1 @@ + diff --git a/games-fps/doom/manifest.toml b/games-fps/doom/manifest.toml new file mode 100644 index 0000000..caf541b --- /dev/null +++ b/games-fps/doom/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "DOOM" +binary = "doom.elf" +icon = "doom.svg" + +[menu] +category = "Games" +visible = true diff --git a/games-fps/doom/src/SDL_mixer.h b/games-fps/doom/src/SDL_mixer.h new file mode 100644 index 0000000..e9b1b33 --- /dev/null +++ b/games-fps/doom/src/SDL_mixer.h @@ -0,0 +1,4 @@ +/* Dummy SDL_mixer.h - not needed for MontaukOS sound backend */ +#ifndef SDL_MIXER_H +#define SDL_MIXER_H +#endif diff --git a/games-fps/doom/src/doom_wad_check.cpp b/games-fps/doom/src/doom_wad_check.cpp new file mode 100644 index 0000000..911419f --- /dev/null +++ b/games-fps/doom/src/doom_wad_check.cpp @@ -0,0 +1,22 @@ +/* + * doom_wad_check.cpp + * Warns the user and exits cleanly if no DOOM WAD is present + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include + +extern "C" [[noreturn]] void doom_warn_missing_wad(const char* path) { + char message[400]; + snprintf(message, sizeof(message), + "No DOOM WAD file was found at %s.\n\n" + "MontaukOS cannot ship proprietary DOOM WADs (doom1.wad, doom.wad, " + "freedoom1.wad, etc.) on its ISO images due to copyright restrictions.\n\n" + "Copy a legally obtained WAD file to that path and relaunch DOOM.", + path); + + gui::dialogs::message_box("DOOM", message, gui::dialogs::MESSAGE_BOX_OK); + montauk::exit(1); +} diff --git a/games-fps/doom/src/doomgeneric_montauk.c b/games-fps/doom/src/doomgeneric_montauk.c new file mode 100644 index 0000000..a0522f2 --- /dev/null +++ b/games-fps/doom/src/doomgeneric_montauk.c @@ -0,0 +1,269 @@ +/* + * doomgeneric_montauk.c + * DOOM platform implementation for MontaukOS (standalone window server client) + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "doomgeneric.h" +#include "doomkeys.h" + +#include +#include + +/* ---- Raw syscall interface (C versions) ---- */ + +static inline long _zos_syscall0(long nr) { + long ret; + __asm__ volatile("syscall" : "=a"(ret) : "a"(nr) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +static inline long _zos_syscall1(long nr, long a1) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +static inline long _zos_syscall2(long nr, long a1, long a2) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "mov %[a3], %%rdx\n\t" + "mov %[a4], %%r10\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +/* Syscall numbers (must match kernel/src/Api/Syscall.hpp) */ +#define SYS_EXIT 0 +#define SYS_SLEEP_MS 2 +#define SYS_PRINT 4 +#define SYS_GETMILLISECONDS 14 +#define SYS_WINCREATE 54 +#define SYS_WINDESTROY 55 +#define SYS_WINPRESENT 56 +#define SYS_WINPOLL 57 + +/* Window server structs (must match montauk::abi::WinCreateResult and montauk::abi::WinEvent) */ +struct WinCreateResult { + int id; /* -1 on failure */ + unsigned _pad; + unsigned long pixelVa; /* VA of pixel buffer in caller's address space */ +}; + +struct WinEvent { + unsigned char type; /* 0=key, 1=mouse, 2=resize, 3=close */ + unsigned char _pad[3]; + union { + struct { + unsigned char scancode; + char ascii; + unsigned char pressed; + unsigned char shift; + unsigned char ctrl; + unsigned char alt; + } key; + struct { int x, y, scroll; unsigned char buttons, prev_buttons; } mouse; + struct { int w, h; } resize; + }; +}; + +/* ---- Window state ---- */ + +static int g_winId = -1; +static uint32_t* g_pixBuf = 0; + +/* ---- Circular key queue ---- */ + +#define KEY_QUEUE_SIZE 64 + +struct KeyQueueEntry { + int pressed; + unsigned char doomkey; +}; + +static struct KeyQueueEntry g_keyQueue[KEY_QUEUE_SIZE]; +static int g_keyQueueRead = 0; +static int g_keyQueueWrite = 0; + +static void key_queue_push(int pressed, unsigned char doomkey) { + int next = (g_keyQueueWrite + 1) % KEY_QUEUE_SIZE; + if (next == g_keyQueueRead) return; /* full, drop */ + g_keyQueue[g_keyQueueWrite].pressed = pressed; + g_keyQueue[g_keyQueueWrite].doomkey = doomkey; + g_keyQueueWrite = next; +} + +static int key_queue_pop(int* pressed, unsigned char* doomkey) { + if (g_keyQueueRead == g_keyQueueWrite) return 0; + *pressed = g_keyQueue[g_keyQueueRead].pressed; + *doomkey = g_keyQueue[g_keyQueueRead].doomkey; + g_keyQueueRead = (g_keyQueueRead + 1) % KEY_QUEUE_SIZE; + return 1; +} + +/* ---- PS/2 scancode to ASCII table (set 1, unshifted) ---- */ + +static const char scancode_to_ascii[128] = { + 0, 27, '1','2','3','4','5','6','7','8','9','0','-','=','\b', + '\t','q','w','e','r','t','y','u','i','o','p','[',']','\n', + 0, 'a','s','d','f','g','h','j','k','l',';','\'','`', + 0, '\\','z','x','c','v','b','n','m',',','.','/', 0, + '*', 0, ' ' +}; + +/* ---- PS/2 scancode to DOOM key mapping ---- */ + +static unsigned char scancode_to_doomkey(unsigned char scancode, char ascii) { + switch (scancode) { + case 0x48: return KEY_UPARROW; + case 0x50: return KEY_DOWNARROW; + case 0x4B: return KEY_LEFTARROW; + case 0x4D: return KEY_RIGHTARROW; + case 0x1C: return KEY_ENTER; + case 0x01: return KEY_ESCAPE; + case 0x39: return KEY_USE; /* Space = use */ + case 0x1D: return KEY_FIRE; /* LCtrl = fire */ + case 0x2A: return KEY_RSHIFT; /* LShift = run */ + case 0x36: return KEY_RSHIFT; /* RShift = run */ + case 0x38: return KEY_RALT; /* Alt = strafe */ + case 0x0E: return KEY_BACKSPACE; + case 0x0F: return KEY_TAB; + /* F1-F10 */ + 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; + /* Equals and minus for screen size */ + case 0x0D: return KEY_EQUALS; + case 0x0C: return KEY_MINUS; + default: + /* Pass through printable ASCII as lowercase */ + if (ascii >= 'a' && ascii <= 'z') return (unsigned char)ascii; + if (ascii >= '0' && ascii <= '9') return (unsigned char)ascii; + return 0; + } +} + +/* ---- Poll window events and enqueue key events ---- */ + +static void poll_keyboard(void) { + struct WinEvent evt; + while (_zos_syscall2(SYS_WINPOLL, (long)g_winId, (long)&evt) > 0) { + if (evt.type == 0) { + /* Key event */ + unsigned char baseSc = evt.key.scancode & 0x7F; + + char ascii = 0; + if (baseSc < 128) + ascii = scancode_to_ascii[baseSc]; + + unsigned char dk = scancode_to_doomkey(baseSc, ascii); + if (dk != 0) { + key_queue_push(evt.key.pressed ? 1 : 0, dk); + } + } else if (evt.type == 3) { + /* Close event — exit the process */ + _zos_syscall1(SYS_EXIT, 0); + } + } +} + +/* ---- DG platform functions ---- */ + +void DG_Init(void) { + struct WinCreateResult result; + _zos_syscall4(SYS_WINCREATE, (long)"DOOM", (long)DOOMGENERIC_RESX, + (long)DOOMGENERIC_RESY, (long)&result); + + if (result.id < 0) { + _zos_syscall1(SYS_EXIT, 1); + } + + g_winId = result.id; + g_pixBuf = (uint32_t*)result.pixelVa; +} + +void DG_DrawFrame(void) { + /* Poll keyboard first */ + poll_keyboard(); + + /* Copy DG_ScreenBuffer into the shared pixel buffer */ + if (g_pixBuf == 0 || DG_ScreenBuffer == 0) return; + + memcpy(g_pixBuf, DG_ScreenBuffer, + DOOMGENERIC_RESX * DOOMGENERIC_RESY * sizeof(uint32_t)); + + /* Mark window as dirty so the compositor picks it up */ + _zos_syscall1(SYS_WINPRESENT, (long)g_winId); +} + +void DG_SleepMs(uint32_t ms) { + _zos_syscall1(SYS_SLEEP_MS, (long)ms); +} + +uint32_t DG_GetTicksMs(void) { + return (uint32_t)_zos_syscall0(SYS_GETMILLISECONDS); +} + +int DG_GetKey(int* pressed, unsigned char* doomKey) { + return key_queue_pop(pressed, doomKey); +} + +void DG_SetWindowTitle(const char* title) { + (void)title; +} + +/* ---- Entry point ---- */ + +/* Shows a warning dialog explaining that MontaukOS cannot ship proprietary + * DOOM WADs and does not return (implemented in doom_wad_check.cpp, which + * links against the shared dialog library). */ +extern void doom_warn_missing_wad(const char* path); + +void _start(void) { + static const char* wadPath = "0:/apps/doom/doom1.wad"; + + FILE* wad = fopen(wadPath, "rb"); + if (!wad) { + doom_warn_missing_wad(wadPath); + /* unreachable: doom_warn_missing_wad() exits the process */ + } + fclose(wad); + + char *argv[] = { "doom", "-iwad", (char*)wadPath, 0 }; + doomgeneric_Create(3, argv); + for (;;) { + doomgeneric_Tick(); + } +} diff --git a/games-fps/doom/src/i_sound_montauk.c b/games-fps/doom/src/i_sound_montauk.c new file mode 100644 index 0000000..fdfafab --- /dev/null +++ b/games-fps/doom/src/i_sound_montauk.c @@ -0,0 +1,362 @@ +/* + * i_sound_montauk.c + * DOOM sound effects and music modules for MontaukOS + * Implements software mixing of 16 SFX channels into the HDA audio device. + * Copyright (c) 2026 Daniel Hammer + */ + +#include "config.h" + +#include +#include +#include + +#include "deh_str.h" +#include "i_sound.h" +#include "i_system.h" +#include "i_swap.h" +#include "m_argv.h" +#include "m_misc.h" +#include "w_wad.h" +#include "z_zone.h" +#include "doomtype.h" + +/* ======================================================================== + Raw syscall interface for audio + ======================================================================== */ + +static inline long _snd_syscall1(long nr, long a1) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +static inline long _snd_syscall3(long nr, long a1, long a2, long a3) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "mov %[a3], %%rdx\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +#define SYS_AUDIOOPEN 80 +#define SYS_AUDIOCLOSE 81 +#define SYS_AUDIOWRITE 82 + +/* ======================================================================== + Audio mixing engine + ======================================================================== */ + +/* Required by i_sound.c config binding */ +int use_libsamplerate = 0; +float libsamplerate_scale = 0.65f; + +#define NUM_CHANNELS 16 +#define MIX_RATE 44100 +#define MIX_CHANNELS 2 +#define MIX_BITS 16 + +/* Samples per mix frame (~35 fps, slightly oversized for timing jitter) */ +#define MIX_SAMPLES 1260 +#define MIX_BUF_BYTES (MIX_SAMPLES * MIX_CHANNELS * (MIX_BITS / 8)) + +/* Channel state */ +struct snd_channel { + const uint8_t *data; /* 8-bit unsigned PCM samples */ + uint32_t length; /* number of samples */ + uint32_t pos; /* playback position (16.16 fixed-point) */ + uint32_t step; /* step per output sample (16.16 fixed-point) */ + int vol_left; /* left volume (0-127) */ + int vol_right; /* right volume (0-127) */ + sfxinfo_t *sfxinfo; /* NULL = inactive */ +}; + +static struct snd_channel channels[NUM_CHANNELS]; +static int audio_handle = -1; +static boolean sound_initialized = false; +static boolean use_sfx_prefix; + +/* Static mix buffer (stereo interleaved int16_t) */ +static int16_t mix_buf[MIX_SAMPLES * MIX_CHANNELS]; + +/* ======================================================================== + Sound effect loading (DMX format from WAD lumps) + ======================================================================== */ + +static void GetSfxLumpName(sfxinfo_t *sfx, char *buf, size_t buf_len) { + if (sfx->link != NULL) + sfx = sfx->link; + + if (use_sfx_prefix) + M_snprintf(buf, buf_len, "ds%s", DEH_String(sfx->name)); + else + M_StringCopy(buf, DEH_String(sfx->name), buf_len); +} + +/* Parse a DMX sound lump. Returns the raw 8-bit PCM data and fills in + samplerate and length. Returns NULL on failure. */ +static const uint8_t *ParseDmxSound(int lumpnum, int *out_rate, uint32_t *out_len) { + unsigned int lumplen = W_LumpLength(lumpnum); + const uint8_t *data = W_CacheLumpNum(lumpnum, PU_STATIC); + + if (lumplen < 8 || data[0] != 0x03 || data[1] != 0x00) + return NULL; + + int samplerate = (data[3] << 8) | data[2]; + uint32_t length = (data[7] << 24) | (data[6] << 16) | (data[5] << 8) | data[4]; + + if (length > lumplen - 8 || length <= 48) + return NULL; + + /* DMX skips first 16 and last 16 bytes of sample data */ + *out_rate = samplerate; + *out_len = length - 32; + return data + 16; +} + +/* ======================================================================== + Volume/separation helpers + ======================================================================== */ + +static void SetChannelVolSep(int ch, int vol, int sep) { + /* vol: 0-127, sep: 0-254 (0=full left, 127=center, 254=full right) */ + channels[ch].vol_left = ((254 - sep) * vol) / 127; + channels[ch].vol_right = (sep * vol) / 127; +} + +/* ======================================================================== + Software mixer - mix all active channels into mix_buf + ======================================================================== */ + +static void MixChannels(int num_samples) { + /* Clear mix buffer */ + memset(mix_buf, 0, (size_t)(num_samples * MIX_CHANNELS) * sizeof(int16_t)); + + for (int ch = 0; ch < NUM_CHANNELS; ch++) { + struct snd_channel *c = &channels[ch]; + if (c->sfxinfo == NULL) + continue; + + int16_t *out = mix_buf; + + for (int i = 0; i < num_samples; i++) { + uint32_t ipos = c->pos >> 16; + + if (ipos >= c->length) { + /* Sound finished */ + c->sfxinfo = NULL; + break; + } + + /* Convert 8-bit unsigned (0-255, 128=silence) to signed (-128..127) + then scale to 16-bit range */ + int sample = ((int)c->data[ipos] - 128) << 8; + + /* Apply volume and accumulate (additive mixing) */ + int left = out[i * 2 + 0] + ((sample * c->vol_left) >> 7); + int right = out[i * 2 + 1] + ((sample * c->vol_right) >> 7); + + /* Clamp to int16_t range */ + if (left > 32767) left = 32767; + else if (left < -32768) left = -32768; + if (right > 32767) right = 32767; + else if (right < -32768) right = -32768; + + out[i * 2 + 0] = (int16_t)left; + out[i * 2 + 1] = (int16_t)right; + + c->pos += c->step; + } + } +} + +/* ======================================================================== + sound_module_t implementation + ======================================================================== */ + +static boolean I_Montauk_InitSound(boolean _use_sfx_prefix) { + use_sfx_prefix = _use_sfx_prefix; + + for (int i = 0; i < NUM_CHANNELS; i++) { + memset(&channels[i], 0, sizeof(channels[i])); + } + + audio_handle = (int)_snd_syscall3(SYS_AUDIOOPEN, + (long)MIX_RATE, (long)MIX_CHANNELS, (long)MIX_BITS); + + if (audio_handle < 0) { + return false; + } + + sound_initialized = true; + return true; +} + +static void I_Montauk_ShutdownSound(void) { + if (!sound_initialized) + return; + + for (int i = 0; i < NUM_CHANNELS; i++) + channels[i].sfxinfo = NULL; + + if (audio_handle >= 0) { + _snd_syscall1(SYS_AUDIOCLOSE, (long)audio_handle); + audio_handle = -1; + } + + sound_initialized = false; +} + +static int I_Montauk_GetSfxLumpNum(sfxinfo_t *sfx) { + char namebuf[9]; + GetSfxLumpName(sfx, namebuf, sizeof(namebuf)); + return W_GetNumForName(namebuf); +} + +static void I_Montauk_UpdateSound(void) { + if (!sound_initialized || audio_handle < 0) + return; + + /* Mix one frame of audio and submit to the device */ + MixChannels(MIX_SAMPLES); + + const uint8_t *ptr = (const uint8_t *)mix_buf; + int remaining = MIX_BUF_BYTES; + + /* Write as much as the device accepts (non-blocking) */ + while (remaining > 0) { + int written = (int)_snd_syscall3(SYS_AUDIOWRITE, + (long)audio_handle, (long)ptr, (long)remaining); + if (written <= 0) + break; + ptr += written; + remaining -= written; + } +} + +static void I_Montauk_UpdateSoundParams(int channel, int vol, int sep) { + if (!sound_initialized || channel < 0 || channel >= NUM_CHANNELS) + return; + if (channels[channel].sfxinfo == NULL) + return; + SetChannelVolSep(channel, vol, sep); +} + +static int I_Montauk_StartSound(sfxinfo_t *sfxinfo, int channel, int vol, int sep) { + if (!sound_initialized || channel < 0 || channel >= NUM_CHANNELS) + return -1; + + /* Stop any sound already on this channel */ + channels[channel].sfxinfo = NULL; + + /* Load and parse the sound lump */ + if (sfxinfo->lumpnum == -1) + return -1; + + int samplerate; + uint32_t length; + const uint8_t *data = ParseDmxSound(sfxinfo->lumpnum, &samplerate, &length); + if (data == NULL) + return -1; + + /* Set up the channel */ + channels[channel].data = data; + channels[channel].length = length; + channels[channel].pos = 0; + /* Resampling step: source_rate / MIX_RATE in 16.16 fixed-point */ + channels[channel].step = ((uint32_t)samplerate << 16) / MIX_RATE; + + SetChannelVolSep(channel, vol, sep); + channels[channel].sfxinfo = sfxinfo; + + return channel; +} + +static void I_Montauk_StopSound(int channel) { + if (!sound_initialized || channel < 0 || channel >= NUM_CHANNELS) + return; + channels[channel].sfxinfo = NULL; +} + +static boolean I_Montauk_SoundIsPlaying(int channel) { + if (!sound_initialized || channel < 0 || channel >= NUM_CHANNELS) + return false; + return channels[channel].sfxinfo != NULL; +} + +static void I_Montauk_PrecacheSounds(sfxinfo_t *sounds, int num_sounds) { + /* No-op: we load on demand */ + (void)sounds; + (void)num_sounds; +} + +static snddevice_t sound_montauk_devices[] = { + SNDDEVICE_SB, + SNDDEVICE_PAS, + SNDDEVICE_GUS, + SNDDEVICE_WAVEBLASTER, + SNDDEVICE_SOUNDCANVAS, + SNDDEVICE_AWE32, +}; + +sound_module_t DG_sound_module = { + sound_montauk_devices, + arrlen(sound_montauk_devices), + I_Montauk_InitSound, + I_Montauk_ShutdownSound, + I_Montauk_GetSfxLumpNum, + I_Montauk_UpdateSound, + I_Montauk_UpdateSoundParams, + I_Montauk_StartSound, + I_Montauk_StopSound, + I_Montauk_SoundIsPlaying, + I_Montauk_PrecacheSounds, +}; + +/* ======================================================================== + music_module_t implementation (stub - no MIDI synthesis) + ======================================================================== */ + +static boolean I_Montauk_InitMusic(void) { return true; } +static void I_Montauk_ShutdownMusic(void) {} +static void I_Montauk_SetMusicVolume(int vol) { (void)vol; } +static void I_Montauk_PauseMusic(void) {} +static void I_Montauk_ResumeMusic(void) {} +static void *I_Montauk_RegisterSong(void *data, int len) { (void)data; (void)len; return (void *)1; } +static void I_Montauk_UnRegisterSong(void *h) { (void)h; } +static void I_Montauk_PlaySong(void *h, boolean loop) { (void)h; (void)loop; } +static void I_Montauk_StopSong(void) {} +static boolean I_Montauk_MusicIsPlaying(void) { return false; } +static void I_Montauk_PollMusic(void) {} + +static snddevice_t music_montauk_devices[] = { + SNDDEVICE_SB, + SNDDEVICE_ADLIB, + SNDDEVICE_GUS, +}; + +music_module_t DG_music_module = { + music_montauk_devices, + arrlen(music_montauk_devices), + I_Montauk_InitMusic, + I_Montauk_ShutdownMusic, + I_Montauk_SetMusicVolume, + I_Montauk_PauseMusic, + I_Montauk_ResumeMusic, + I_Montauk_RegisterSong, + I_Montauk_UnRegisterSong, + I_Montauk_PlaySong, + I_Montauk_StopSong, + I_Montauk_MusicIsPlaying, + I_Montauk_PollMusic, +}; diff --git a/games-fps/doom/upstream b/games-fps/doom/upstream new file mode 160000 index 0000000..fc60163 --- /dev/null +++ b/games-fps/doom/upstream @@ -0,0 +1 @@ +Subproject commit fc601639494e089702a1ada082eb51aaafc03722 diff --git a/mk/port.mk b/mk/port.mk new file mode 100644 index 0000000..4db8063 --- /dev/null +++ b/mk/port.mk @@ -0,0 +1,131 @@ +# port.mk - Shared rules for MontaukOS ports. +# +# A port builds against a MontaukOS checkout (the SDK) and produces an app +# bundle in $(OUTDIR)/apps// containing: +# +# .elf the binary +# manifest.toml launcher metadata (name, binary, icon, category) +# .svg the launcher icon +# .port marker so the OS release step can tell ports from +# in-tree apps (see MontaukOS scripts/install_apps.sh) +# +# The desktop readdirs 0:/apps/ at runtime and parses each manifest.toml, so a +# bundle dropped into the ramdisk needs no changes anywhere in the OS tree. +# +# A port Makefile sets PORT_NAME, PORT_ICON and its own sources/flags, then +# includes this file and uses $(BUNDLE_DIR) as its link target directory. + +MAKEFLAGS += -rR +.SUFFIXES: + +# This file defines rules before the including Makefile does, so pin the +# default goal rather than letting the first bundle rule claim it. +.DEFAULT_GOAL := all + +# ==== Repo root ======================================================= +# Derived from this file's own location so a port builds the same whether it +# is invoked from the top-level Makefile or directly with `make -C /`, +# and regardless of how deeply categories nest it. + +PORT_MK_DIR := $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST)))) +MTKPORTS_ROOT := $(abspath $(PORT_MK_DIR)/..) + +# ==== SDK location ==================================================== +# Ports build against a live MontaukOS checkout rather than a copied +# sysroot, so libc/header changes are picked up without a sync step. + +MONTAUKOS ?= $(HOME)/dev/MontaukOS + +ifeq ($(wildcard $(MONTAUKOS)/programs/link.ld),) + $(error MontaukOS tree not found at $(MONTAUKOS). Pass MONTAUKOS=/path/to/MontaukOS) +endif + +# ==== Toolchain ======================================================= + +TOOLCHAIN_PREFIX := $(MONTAUKOS)/toolchain/local/bin/x86_64-montauk- + +ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + $(error x86_64-montauk toolchain not found. Run $(MONTAUKOS)/toolchain/build-montauk-toolchain.sh first) +endif + +CC := $(TOOLCHAIN_PREFIX)gcc +CXX := $(TOOLCHAIN_PREFIX)g++ +AR := $(TOOLCHAIN_PREFIX)ar +LD := $(TOOLCHAIN_PREFIX)gcc + +# ==== SDK paths ======================================================= + +SDK_INC := $(MONTAUKOS)/programs/include +SDK_LIBC_INC:= $(MONTAUKOS)/programs/include/libc +SDK_LIB := $(MONTAUKOS)/programs/lib +SDK_LINK_LD := $(MONTAUKOS)/programs/link.ld + +LIBC_A := $(SDK_LIB)/libc/liblibc.a +LIBLOADER_A := $(SDK_LIB)/libloader/liblibloader.a +LIBTLS_A := $(SDK_LIB)/tls/libtls.a +LIBBEARSSL_A:= $(SDK_LIB)/bearssl/libbearssl.a +LIBJPEG_A := $(SDK_LIB)/libjpeg/libjpeg.a + +# The kernel ABI version this port is being built against. Recorded in the +# bundle so a stale port can be spotted after a syscall/ABI bump rather than +# crashing mysteriously at runtime. +SDK_API_VERSION := $(shell sed -n 's/.*apiVersion = \([0-9]\+\);.*/\1/p' \ + $(MONTAUKOS)/kernel/src/Api/Info.hpp 2>/dev/null | head -1) + +# ==== Output ========================================================== + +OUTDIR ?= $(MTKPORTS_ROOT)/out +BUNDLE_DIR := $(OUTDIR)/apps/$(PORT_NAME) +OBJDIR ?= obj +TARGET := $(BUNDLE_DIR)/$(PORT_NAME).elf + +# ==== Common flags ==================================================== +# Mirrors the in-tree convention: freestanding, no stdlib, linked with the +# shared program link script. + +PORT_CFLAGS_COMMON := \ + -g -O2 -pipe \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -ffunction-sections \ + -fdata-sections \ + -msse \ + -msse2 \ + -isystem $(SDK_LIBC_INC) \ + -I $(SDK_INC) + +PORT_LDFLAGS_COMMON := \ + -nostdlib \ + -Wl,--gc-sections \ + -T $(SDK_LINK_LD) + +# ==== Bundle assembly ================================================= +# Ports depend on $(BUNDLE_FILES) to get manifest + icon + marker staged +# alongside the ELF their link rule produces. + +BUNDLE_FILES := $(BUNDLE_DIR)/manifest.toml $(BUNDLE_DIR)/.port +ifneq ($(PORT_ICON),) + BUNDLE_FILES += $(BUNDLE_DIR)/$(PORT_ICON) +endif + +$(BUNDLE_DIR)/manifest.toml: manifest.toml | $(BUNDLE_DIR) + cp $< $@ + +$(BUNDLE_DIR)/$(PORT_ICON): $(PORT_ICON) | $(BUNDLE_DIR) + cp $< $@ + +# Marker file consumed by the MontaukOS release step to prune ports from a +# base ISO. Carries the ABI version so mismatches are diagnosable. +$(BUNDLE_DIR)/.port: Makefile | $(BUNDLE_DIR) + printf 'port = "%s"\napi_version = "%s"\n' '$(PORT_NAME)' '$(SDK_API_VERSION)' > $@ + +$(BUNDLE_DIR): + mkdir -p $@ + +.PHONY: bundle +bundle: $(TARGET) $(BUNDLE_FILES) + +.PHONY: clean-bundle +clean-bundle: + rm -rf $(OBJDIR) $(BUNDLE_DIR)