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:
M.gitmodules | 3+++
Aplatforms/dreamcast/.gitignore | 2++
Aplatforms/dreamcast/KallistiOS | 1+
Aplatforms/dreamcast/Makefile | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplatforms/dreamcast/dreamcast.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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"); + } +}