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:
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();
+}