tuxanci2-engine
Game engine for tuxanci2
Log | Files | Refs | README | LICENSE
commit 9d4f5eb4f036b09a58c67535fe3209d25454df5e
parent 771e683f0d8d34b5fd69a3b7d5ddea7ca97c1275
Author: Connor Thomson <connor@gnufault.org>
Date:   Wed, 21 Oct 2026 17:35:37 -0700

Add saturn platform

Diffstat:
M.gitmodules | 3+++
MMakefile | 4+++-
MREADME.md | 1+
Aplatforms/saturn/.gitignore | 2++
Aplatforms/saturn/Makefile | 45+++++++++++++++++++++++++++++++++++++++++++++
Aplatforms/saturn/jo.mk | 27+++++++++++++++++++++++++++
Aplatforms/saturn/joengine | 1+
Aplatforms/saturn/saturn.c | 216+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 298 insertions(+), 1 deletion(-)

diff --git a/.gitmodules b/.gitmodules @@ -4,3 +4,6 @@ [submodule "platforms/dreamcast/KallistiOS"] path = platforms/dreamcast/KallistiOS url = https://github.com/KallistiOS/KallistiOS.git +[submodule "platforms/saturn/joengine"] + path = platforms/saturn/joengine + url = https://github.com/johannes-fetz/joengine.git diff --git a/Makefile b/Makefile @@ -54,6 +54,8 @@ $(foreach asset,$(ASSETS),$(eval $(call ASSET_C_RULE,$(asset)))) all: $(CONFIG_HEADER) $(GENERATED_SRCS) $(OUT) +generated: $(CONFIG_HEADER) $(GENERATED_SRCS) + $(OUT): $(OBJS) $(AR) $(ARFLAGS) $@ $^ @@ -84,4 +86,4 @@ clean: FORCE: -.PHONY: all clean FORCE +.PHONY: all generated clean FORCE diff --git a/README.md b/README.md @@ -8,6 +8,7 @@ Tuxánci currently supports: - DOS - Dreamcast - N64 +- Saturn - SDL2 - SDL3 - Wayland diff --git a/platforms/saturn/.gitignore b/platforms/saturn/.gitignore @@ -0,0 +1,2 @@ +game.cue +game.iso diff --git a/platforms/saturn/Makefile b/platforms/saturn/Makefile @@ -0,0 +1,45 @@ +MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +ROOT_DIR := $(abspath $(MAKEFILE_DIR)/../..) + +JOENGINE ?= $(MAKEFILE_DIR)joengine +JOENGINE := $(abspath $(JOENGINE)) + +SATURN_BUILD ?= build/saturn +SATURN_BUILD_DIR := $(ROOT_DIR)/$(SATURN_BUILD) + +ENGINE_SRCS := $(shell find $(ROOT_DIR)/src -type f -name '*.c') +ENGINE_LINKS := $(patsubst $(ROOT_DIR)/src/%.c,$(SATURN_BUILD_DIR)/src/%.c,$(ENGINE_SRCS)) +PLATFORM_LINK := $(SATURN_BUILD_DIR)/saturn.c + +.DEFAULT_GOAL := all +.PHONY: all generated stage clean + +all: stage + $(MAKE) -C "$(SATURN_BUILD_DIR)" -f "$(MAKEFILE_DIR)jo.mk" \ + MAKEFILE="$(MAKEFILE_DIR)jo.mk" \ + JOENGINE="$(JOENGINE)" \ + ROOT_DIR="$(ROOT_DIR)" \ + SATURN_BUILD_DIR="$(SATURN_BUILD_DIR)" + cp "$(SATURN_BUILD_DIR)/game.cue" "$(SATURN_BUILD_DIR)/game.iso" "$(MAKEFILE_DIR)" + +generated: + $(MAKE) -C "$(ROOT_DIR)" BUILD_DIR="$(SATURN_BUILD)" BITS=32 generated + +$(SATURN_BUILD_DIR)/src/%.c: $(ROOT_DIR)/src/%.c + mkdir -p "$(dir $@)" + ln -sf "$<" "$@" + +$(PLATFORM_LINK): $(MAKEFILE_DIR)saturn.c + mkdir -p "$(dir $@)" + ln -sf "$<" "$@" + +stage: generated $(ENGINE_LINKS) $(PLATFORM_LINK) + mkdir -p "$(SATURN_BUILD_DIR)/cd" + printf 'TUXANCI 2\r\n' > "$(SATURN_BUILD_DIR)/cd/ABS.TXT" + printf 'TUXANCI 2\r\n' > "$(SATURN_BUILD_DIR)/cd/CPY.TXT" + printf 'TUXANCI 2\r\n' > "$(SATURN_BUILD_DIR)/cd/BIB.TXT" + +clean: + $(MAKE) -C "$(ROOT_DIR)" BUILD_DIR="$(SATURN_BUILD)" clean 2>/dev/null || true + $(RM) -r "$(SATURN_BUILD_DIR)" + find "$(JOENGINE)/jo_engine" -name '*.o' -delete 2>/dev/null || true diff --git a/platforms/saturn/jo.mk b/platforms/saturn/jo.mk @@ -0,0 +1,27 @@ +JO_COMPILE_WITH_VIDEO_MODULE = 0 +JO_COMPILE_WITH_BACKUP_MODULE = 0 +JO_COMPILE_WITH_TGA_MODULE = 0 +JO_COMPILE_WITH_AUDIO_MODULE = 0 +JO_COMPILE_WITH_3D_MODULE = 0 +JO_COMPILE_WITH_PSEUDO_MODE7_MODULE = 0 +JO_COMPILE_WITH_EFFECTS_MODULE = 0 +JO_COMPILE_WITH_STORYBOARD_MODULE = 0 +JO_COMPILE_WITH_SOFTWARE_RENDERER_MODULE = 0 + +JO_GLOBAL_MEMORY_SIZE_FOR_MALLOC = 262144 + +JO_MAX_SPRITE = 16 + +JO_ENGINE_SRC_DIR = $(JOENGINE)/jo_engine +COMPILER_DIR = $(JOENGINE)/Compiler + +ENGINE_OBJ_SRCS := $(shell find src \( -type f -o -type l \) -name '*.c') +ASSET_SRCS := $(shell find images fonts -type f -name '*.c') + +SRCS = saturn.c $(ENGINE_OBJ_SRCS) $(ASSET_SRCS) + +include $(COMPILER_DIR)/COMMON/jo_engine_makefile + +_CCFLAGS = -I$(ROOT_DIR)/include -I$(SATURN_BUILD_DIR) -DTA_32BIT -DTA_DISABLE_BILINEAR_FILTERING + +$(OBJS): $(wildcard $(ROOT_DIR)/include/*.h $(ROOT_DIR)/include/screens/*.h) $(SATURN_BUILD_DIR)/ta_config.h diff --git a/platforms/saturn/joengine b/platforms/saturn/joengine @@ -0,0 +1 @@ +Subproject commit 556d081146211b6a1cfa6591d70f9487d406758b diff --git a/platforms/saturn/saturn.c b/platforms/saturn/saturn.c @@ -0,0 +1,216 @@ +#include <jo/jo.h> +#include "ta_api.h" + +#define SCREEN_WIDTH JO_TV_WIDTH +#define SCREEN_HEIGHT JO_TV_HEIGHT +#define WINDOW_BPP 32 +#define LWRAM 0x00200000 +#define LWRAM_HEAP_SIZE 0x00100000 +#define MAX_SLOTS 16 +#define VRAM_BUDGET 400000 +#define VDP1_MAX_WIDTH 504 +#define VDP1_MAX_HEIGHT 255 +#define BASE_Z 500 +#define RGB555_OPAQUE 0x8000 +#define ALPHA_THRESHOLD 128 + +typedef struct { + const ta_display_sprite *source; + ta_uint16 x, y, width, height; + ta_uint32 checksum; + unsigned int vram_bytes; +} slot_t; + +static slot_t slots[MAX_SLOTS]; +static unsigned int slot_count; +static unsigned int vram_used; +static int first_sprite_id; + +static jo_color upload_buffer[SCREEN_WIDTH * SCREEN_HEIGHT] __attribute__((aligned(32))); + +static void update_controller_input(void) { + static const struct { + jo_gamepad_keys pad; + ta_keyboard_keys key; + } map[] = { + { JO_KEY_UP, KEY_UP }, + { JO_KEY_DOWN, KEY_DOWN }, + { JO_KEY_LEFT, KEY_LEFT }, + { JO_KEY_RIGHT, KEY_RIGHT }, + { JO_KEY_A, KEY_ENTER }, + { JO_KEY_C, KEY_ENTER }, + { JO_KEY_START, KEY_ENTER } + }; + ta_bool enter_down = ta_false; + + for (unsigned int i = 0; i < sizeof(map) / sizeof(map[0]); i++) { + ta_bool down = jo_is_pad1_available() && jo_is_pad1_key_pressed(map[i].pad); + + if (map[i].key == KEY_ENTER) { + enter_down = (ta_bool)(enter_down || down); + continue; + } + + if (down) { + ta_keyboard_key_down(map[i].key); + } else { + ta_keyboard_key_up(map[i].key); + } + } + + if (enter_down) { + ta_keyboard_key_down(KEY_ENTER); + } else { + ta_keyboard_key_up(KEY_ENTER); + } +} + +static ta_uint32 sprite_checksum(const ta_display_sprite *sprite) { + const ta_uint32 *pixels = (const ta_uint32 *)sprite->pixels; + const unsigned int count = (unsigned int)sprite->width * sprite->height; + const unsigned int stride = count / 64 + 1; + ta_uint32 sum = count; + + for (unsigned int i = 0; i < count; i += stride) { + sum = ((sum << 5) | (sum >> 27)) ^ pixels[i]; + } + + return sum; +} + +static ta_bool slot_matches(const slot_t *slot, const ta_display_sprite *sprite) { + return (ta_bool)(slot->source == sprite && slot->x == sprite->x && slot->y == sprite->y && slot->width == sprite->width && slot->height == sprite->height && slot->checksum == sprite_checksum(sprite)); +} + +static ta_bool upload_sprite(const ta_display_sprite *sprite) { + if (slot_count >= MAX_SLOTS || sprite->format != TA_DISPLAY_PIXEL_FORMAT_32 || sprite->width == 0 || sprite->height == 0 || sprite->width > VDP1_MAX_WIDTH || sprite->height > VDP1_MAX_HEIGHT) { + return ta_false; + } + + const unsigned int padded_width = ((unsigned int)sprite->width + 7) & ~7u; + const unsigned int bytes = padded_width * sprite->height * (unsigned int)sizeof(jo_color); + + if (padded_width * sprite->height > sizeof(upload_buffer) / sizeof(upload_buffer[0]) || + vram_used + bytes > VRAM_BUDGET) { + return ta_false; + } + + const ta_uint32 *source = (const ta_uint32 *)sprite->pixels; + + for (unsigned int y = 0; y < sprite->height; y++) { + const ta_uint32 *row = source + (unsigned int)y * sprite->width; + jo_color *destination = upload_buffer + (unsigned int)y * padded_width; + unsigned int x = 0; + + for (; x < sprite->width; x++) { + const ta_uint32 pixel = row[x]; + + if ((pixel & 0xff) < ALPHA_THRESHOLD) { + destination[x] = 0; + } else { + destination[x] = (jo_color)(RGB555_OPAQUE | (((pixel >> 11) & 0x1f) << 10) | (((pixel >> 19) & 0x1f) << 5) | ((pixel >> 27) & 0x1f)); + } + } + + for (; x < padded_width; x++) { + destination[x] = 0; + } + } + + jo_img image = { + (unsigned short)padded_width, sprite->height, + upload_buffer + }; + + const int id = jo_sprite_add(&image); + + if (id != first_sprite_id + (int)slot_count) { + return ta_false; + } + + slot_t *slot = &slots[slot_count++]; + slot->source = sprite; + slot->x = sprite->x; + slot->y = sprite->y; + slot->width = sprite->width; + slot->height = sprite->height; + slot->checksum = sprite_checksum(sprite); + slot->vram_bytes = bytes; + vram_used += bytes; + + return ta_true; +} + +static void sync_sprites(void) { + const unsigned int count = ta_display_sprites ? ta_display_sprites->count : 0; + unsigned int keep = 0; + + while (keep < slot_count && keep < count && slot_matches(&slots[keep], ta_display_sprites->sprites[keep])) { + keep++; + } + + if (keep < slot_count) { + jo_sprite_free_from(first_sprite_id + (int)keep); + + for (unsigned int i = keep; i < slot_count; i++) { + vram_used -= slots[i].vram_bytes; + } + + slot_count = keep; + } + + for (unsigned int i = keep; i < count; i++) { + if (!upload_sprite(ta_display_sprites->sprites[i])) { + break; + } + } +} + +static void game_frame_callback(void) { + update_controller_input(); + + if (ta_main_update()) { + sync_sprites(); + } + + for (unsigned int i = 0; i < slot_count; i++) { + jo_sprite_draw3D2(first_sprite_id + (int)i, slots[i].x, slots[i].y, BASE_Z - (int)i); + } +} + +void jo_main(void) { + jo_core_init(JO_COLOR_Black); + jo_add_memory_zone((unsigned char *)LWRAM, LWRAM_HEAP_SIZE); + + first_sprite_id = jo_get_last_sprite_id() + 1; + + ta_display->width = SCREEN_WIDTH; + ta_display->height = SCREEN_HEIGHT; + ta_display->bpp = WINDOW_BPP; + ta_display->sprite_mode = ta_true; + + ta_main_init(); + + jo_core_add_callback(game_frame_callback); + jo_core_run(); +} + +void *ta_malloc(ta_size size) { + if (size == 0) { + return TA_NULL; + } + + return jo_malloc((unsigned int)size); +} + +void ta_free(void *ptr) { + if (ptr) { + jo_free(ptr); + } +} + +void ta_exit(int status) { + (void)status; + + jo_goto_boot_menu(); +}