tuxanci2-engine
Game engine for tuxanci2
Log | Files | Refs | README | LICENSE
commit 1dc451233cc598950129cc82c07df4efdd780c21
parent e734aa9fd5fbbb120e8aa0771c8ab26d4d562393
Author: Connor Thomson <connor@gnufault.org>
Date: Tue, 20 Oct 2026 08:18:44 -0700
Add dreamcast port
Diffstat:
5 files changed, 252 insertions(+), 0 deletions(-)
diff --git a/.gitmodules b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "platforms/xbox/nxdk"]
path = platforms/xbox/nxdk
url = https://github.com/XboxDev/nxdk.git
+[submodule "platforms/dreamcast/KallistiOS"]
+ path = platforms/dreamcast/KallistiOS
+ url = https://github.com/KallistiOS/KallistiOS.git
diff --git a/platforms/dreamcast/.gitignore b/platforms/dreamcast/.gitignore
@@ -0,0 +1,2 @@
+build/
+/tuxanci2.elf
diff --git a/platforms/dreamcast/KallistiOS b/platforms/dreamcast/KallistiOS
@@ -0,0 +1 @@
+Subproject commit 76e1b9ccf1c005908dcd1e309833ccbbd3704803
diff --git a/platforms/dreamcast/Makefile b/platforms/dreamcast/Makefile
@@ -0,0 +1,96 @@
+MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
+ROOT_DIR := $(abspath $(MAKEFILE_DIR)/../..)
+
+KOS_SOURCE ?= $(MAKEFILE_DIR)KallistiOS
+KOS_SOURCE := $(abspath $(KOS_SOURCE))
+BUILD_ROOT := $(ROOT_DIR)/build/dreamcast
+KOS_BASE := $(BUILD_ROOT)/KallistiOS
+KOS_CHAIN_DIR := $(KOS_BASE)/utils/kos-chain
+KOS_SOURCE_STAMP := $(BUILD_ROOT)/.kos-source-copied
+TOOLCHAIN_PREFIX := $(BUILD_ROOT)/toolchain/sh-elf
+KOS_CC_PATH := $(TOOLCHAIN_PREFIX)/bin/sh-elf-gcc
+KOS_ENV := $(BUILD_ROOT)/environ.sh
+KOS_CC_WRAPPER := $(KOS_BASE)/utils/build_wrappers/kos-cc
+KOS_CHAIN_JOBS ?= 2
+KOS_BUILD_JOBS ?= 2
+
+DREAMCAST_BUILD ?= build/dreamcast/engine
+DREAMCAST_BUILD_DIR := $(abspath $(ROOT_DIR)/$(DREAMCAST_BUILD))
+DREAMCAST_LIBRARY := $(DREAMCAST_BUILD_DIR)/tuxanci2.a
+KOS_LIBRARY := $(KOS_BASE)/lib/dreamcast/libkallisti.a
+APP_BUILD_DIR := $(BUILD_ROOT)/app
+APP_OBJECT := $(APP_BUILD_DIR)/dreamcast.o
+TARGET := $(MAKEFILE_DIR)tuxanci2.elf
+
+ROOT_INPUTS := $(shell find $(ROOT_DIR)/src -type f -name '*.c') \
+ $(wildcard $(ROOT_DIR)/include/*.h) \
+ $(ROOT_DIR)/include/ta_config.h.in \
+ $(shell find $(ROOT_DIR)/files -type f) \
+ $(ROOT_DIR)/Makefile
+ENGINE_CFLAGS := -DTA_32BIT -DTA_DISABLE_BILINEAR_FILTERING \
+ -I$(ROOT_DIR)/include -I$(DREAMCAST_BUILD_DIR) \
+ -std=c99 -ffreestanding -nostdlib -nostdinc \
+ -fno-builtin -fno-stack-protector \
+ -Wall -Wextra -Werror -pedantic -pedantic-errors
+
+CPPFLAGS += -I$(ROOT_DIR)/include -I$(DREAMCAST_BUILD_DIR)
+export CPPFLAGS
+export KOS_BASE
+
+.DEFAULT_GOAL := all
+.PHONY: all build clean distclean prepare-kos-source
+
+all: $(KOS_ENV)
+ . "$(KOS_ENV)" && $(MAKE) --no-print-directory build
+
+build: $(TARGET) | $(KOS_LIBRARY)
+
+prepare-kos-source:
+ mkdir -p "$(BUILD_ROOT)"
+ if [ ! -f "$(KOS_SOURCE_STAMP)" ]; then \
+ mkdir -p "$(KOS_BASE)"; \
+ tar --exclude=.git -C "$(KOS_SOURCE)" -cf - . | tar -C "$(KOS_BASE)" -xf -; \
+ touch "$(KOS_SOURCE_STAMP)"; \
+ fi
+
+$(KOS_CC_PATH): | prepare-kos-source
+ $(MAKE) -C "$(KOS_CHAIN_DIR)" platform=dreamcast \
+ kos_base="$(KOS_BASE)" toolchain_path="$(TOOLCHAIN_PREFIX)" \
+ makejobs="$(KOS_CHAIN_JOBS)" \
+ enable_cpp=0 enable_objc=0 enable_objcpp=0
+ test -x "$@"
+
+$(KOS_ENV): $(KOS_CC_PATH) $(KOS_SOURCE)/doc/environ.sh.sample
+ mkdir -p "$(@D)"
+ sed \
+ -e 's|^export KOS_BASE=.*|export KOS_BASE="$(KOS_BASE)"|' \
+ -e 's|^export KOS_CC_BASE=.*|export KOS_CC_BASE="$(TOOLCHAIN_PREFIX)"|' \
+ "$(KOS_SOURCE)/doc/environ.sh.sample" > "$@"
+
+$(KOS_LIBRARY): $(KOS_ENV)
+ . "$(KOS_ENV)" && $(MAKE) -C "$(KOS_BASE)" -j"$(KOS_BUILD_JOBS)"
+
+$(DREAMCAST_LIBRARY): $(ROOT_INPUTS)
+ $(MAKE) -C "$(ROOT_DIR)" CC="$(KOS_CC_WRAPPER)" AR="$(KOS_AR)" BITS=32 \
+ BUILD_DIR="$(DREAMCAST_BUILD)" TA_DISABLE_BILINEAR_FILTERING=true \
+ CFLAGS="$(ENGINE_CFLAGS)" all
+
+$(APP_OBJECT): dreamcast.c $(DREAMCAST_LIBRARY)
+ mkdir -p "$(@D)"
+ $(KOS_CC_WRAPPER) $(CFLAGS) $(CPPFLAGS) -c "$<" -o "$@"
+
+$(TARGET): $(APP_OBJECT) $(DREAMCAST_LIBRARY) | $(KOS_LIBRARY)
+ $(KOS_CC_WRAPPER) -o "$@" $^
+
+$(BUILD_ROOT):
+ mkdir -p "$@"
+
+clean:
+ $(RM) "$(TARGET)"
+ $(MAKE) -C "$(ROOT_DIR)" BUILD_DIR="$(DREAMCAST_BUILD)" clean
+ $(RM) "$(APP_OBJECT)"
+
+distclean: clean
+ $(RM) -r "$(BUILD_ROOT)"
+
+include $(KOS_SOURCE)/Makefile.rules
diff --git a/platforms/dreamcast/dreamcast.c b/platforms/dreamcast/dreamcast.c
@@ -0,0 +1,150 @@
+#include "ta_api.h"
+#include <kos.h>
+#include <dc/maple.h>
+#include <dc/maple/controller.h>
+#include <dc/pvr.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+KOS_INIT_FLAGS(INIT_DEFAULT);
+
+#define SCREEN_WIDTH 320
+#define SCREEN_HEIGHT 240
+#define WINDOW_BPP 32
+#define TEXTURE_WIDTH 512
+#define TEXTURE_HEIGHT 256
+
+alignas(WINDOW_BPP) static uint16_t texture_pixels[TEXTURE_WIDTH * TEXTURE_HEIGHT];
+
+static void update_controller_input(void) {
+ maple_device_t *controller = maple_enum_type(0, MAPLE_FUNC_CONTROLLER);
+ cont_state_t *state = controller ? (cont_state_t *)maple_dev_status(controller) : NULL;
+
+ const ta_keyboard_keys keys[] = {
+ KEY_UP,
+ KEY_DOWN,
+ KEY_LEFT,
+ KEY_RIGHT,
+ KEY_ENTER
+ };
+ const uint32_t buttons[] = {
+ CONT_DPAD_UP,
+ CONT_DPAD_DOWN,
+ CONT_DPAD_LEFT,
+ CONT_DPAD_RIGHT,
+ CONT_A
+ };
+
+ for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
+ if (state && (state->buttons & buttons[i])) {
+ ta_keyboard_key_down(keys[i]);
+ } else {
+ ta_keyboard_key_up(keys[i]);
+ }
+ }
+}
+
+static void convert_framebuffer(void) {
+ const uint32_t *source = (const uint32_t *)ta_fb.buffers[0];
+
+ for (size_t y = 0; y < SCREEN_HEIGHT; y++) {
+ for (size_t x = 0; x < SCREEN_WIDTH; x++) {
+ uint32_t pixel = source[y * SCREEN_WIDTH + x];
+ uint16_t red = (uint16_t)((pixel >> 16) & 0xff);
+ uint16_t green = (uint16_t)((pixel >> 8) & 0xff);
+ uint16_t blue = (uint16_t)(pixel & 0xff);
+
+ texture_pixels[y * TEXTURE_WIDTH + x] = (uint16_t)(((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3));
+ }
+ }
+}
+
+static void render_frame(const pvr_sprite_hdr_t *header) {
+ pvr_sprite_txr_t sprite = {
+ .flags = PVR_CMD_VERTEX_EOL,
+ .ax = 0.0f,
+ .ay = SCREEN_HEIGHT,
+ .az = 1.0f,
+ .bx = 0.0f,
+ .by = 0.0f,
+ .bz = 1.0f,
+ .cx = SCREEN_WIDTH,
+ .cy = 0.0f,
+ .cz = 1.0f,
+ .dx = SCREEN_WIDTH,
+ .dy = SCREEN_HEIGHT,
+ .dummy = 0,
+ .auv = PVR_PACK_16BIT_UV(0.0f, (float)SCREEN_HEIGHT / TEXTURE_HEIGHT),
+ .buv = PVR_PACK_16BIT_UV(0.0f, 0.0f),
+ .cuv = PVR_PACK_16BIT_UV((float)SCREEN_WIDTH / TEXTURE_WIDTH, 0.0f)
+ };
+
+ pvr_scene_begin();
+ pvr_list_begin(PVR_LIST_OP_POLY);
+ pvr_prim(header, sizeof(*header));
+ pvr_prim(&sprite, sizeof(sprite));
+ pvr_list_finish();
+ pvr_scene_finish();
+ pvr_wait_render_done();
+}
+
+int main(void) {
+ pvr_ptr_t texture;
+ pvr_sprite_cxt_t context;
+ pvr_sprite_hdr_t header;
+
+ vid_set_mode(DM_320x240, PM_RGB565);
+ if (pvr_init_defaults() < 0) {
+ return 1;
+ }
+
+ texture = pvr_mem_malloc(sizeof(texture_pixels));
+ if (!texture) {
+ pvr_shutdown();
+ return 1;
+ }
+
+ pvr_set_bg_color(0.0f, 0.0f, 0.0f);
+ pvr_sprite_cxt_txr(&context, PVR_LIST_OP_POLY, PVR_TXRFMT_RGB565 | PVR_TXRFMT_NONTWIDDLED, TEXTURE_WIDTH, TEXTURE_HEIGHT, texture, PVR_FILTER_NONE);
+ pvr_sprite_compile(&header, &context);
+
+ ta_fb.width = SCREEN_WIDTH;
+ ta_fb.height = SCREEN_HEIGHT;
+ ta_fb.bpp = WINDOW_BPP;
+ ta_main_init();
+
+ while (1) {
+ update_controller_input();
+ ta_main_update();
+ convert_framebuffer();
+ pvr_txr_load(texture_pixels, texture, sizeof(texture_pixels));
+ render_frame(&header);
+ }
+
+ pvr_mem_free(texture);
+ pvr_shutdown();
+ return 0;
+}
+
+void *ta_malloc(ta_size size) {
+ return malloc(size);
+}
+
+void ta_free(void *ptr) {
+ free(ptr);
+}
+
+void ta_exit(int status) {
+ (void)status;
+
+ __asm__ volatile(
+ "stc sr, r0\n\t"
+ "or #0xf0, r0\n\t"
+ "ldc r0, sr"
+ : : : "r0", "memory"
+ );
+
+ while (1) {
+ __asm__ volatile("sleep");
+ }
+}