tuxanci2-engine
Game engine for tuxanci2
Log | Files | Refs | README | LICENSE
commit 6fc86a2c2d69add4cca144d6d4fce21a47880bbb
parent 7475a648273bebf997b8fecaa9fa6464037d6c82
Author: Connor Thomson <connor@gnufault.org>
Date:   Sun, 25 Oct 2026 15:10:31 -0700

Merge branch 'ps1'

Diffstat:
M.gitmodules | 3+++
MREADME.md | 1+
Aplatforms/ps1/.gitignore | 2++
Aplatforms/ps1/Makefile | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplatforms/ps1/ps1.c | 444+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 591 insertions(+), 0 deletions(-)

diff --git a/.gitmodules b/.gitmodules @@ -7,3 +7,6 @@ [submodule "platforms/saturn/joengine"] path = platforms/saturn/joengine url = https://github.com/johannes-fetz/joengine.git +[submodule "platforms/ps1/nugget"] + path = platforms/ps1/nugget + url = https://github.com/pcsx-redux/nugget.git diff --git a/README.md b/README.md @@ -9,6 +9,7 @@ Tuxánci currently supports: - DOS - Dreamcast - N64 +- PS1 - Saturn - SDL2 - SDL3 diff --git a/platforms/ps1/.gitignore b/platforms/ps1/.gitignore @@ -0,0 +1,2 @@ +tuxanci2.cue +tuxanci2.bin diff --git a/platforms/ps1/Makefile b/platforms/ps1/Makefile @@ -0,0 +1,141 @@ +MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +ROOT_DIR := $(abspath $(MAKEFILE_DIR)/../..) + +NUGGET ?= $(MAKEFILE_DIR)nugget +NUGGET := $(abspath $(NUGGET)) + +PS1_PREFIX ?= $(firstword $(foreach p,mipsel-none-elf mipsel-linux-gnu,$(if $(shell command -v $(p)-gcc 2>/dev/null),$(p)))) +ifeq ($(PS1_PREFIX),) +$(error You need to install mipsel-none-elf-gcc or mipsel-linux-gnu-gcc or set PS1_PREFIX) +endif +PS1_OFORMAT ?= $(if $(filter mipsel-none-elf,$(PS1_PREFIX)),elf32-littlemips,elf32-tradlittlemips) +PS1_CC := $(PS1_PREFIX)-gcc +PS1_AR := $(PS1_PREFIX)-gcc-ar +PS1_OBJCOPY := $(PS1_PREFIX)-objcopy +MKPSXISO ?= mkpsxiso + +PSXLICENSEPATCH ?= psxlicensepatch +PSXLICENSEDUMP ?= psxlicensedump + +PS1_REGION ?= US +PS1_TEXT ?= +PS1_TMD ?= + +PS1_BUILD ?= build/ps1 +PS1_BUILD_DIR := $(ROOT_DIR)/$(PS1_BUILD) +PS1_LIBRARY := $(PS1_BUILD_DIR)/tuxanci2.a +OBJ_DIR := $(PS1_BUILD_DIR)/platform +ISO_DIR := $(PS1_BUILD_DIR)/iso + +ifeq ($(wildcard $(NUGGET)/ps-exe.ld),) +$(error nugget not found at '$(NUGGET)'.) +endif + +PS1_ELF := $(PS1_BUILD_DIR)/psx.elf +PS1_MAP := $(PS1_BUILD_DIR)/psx.map +PS1_EXE := $(ISO_DIR)/PSX.EXE +SYSTEM_CNF := $(ISO_DIR)/SYSTEM.CNF +ISO_XML := $(ISO_DIR)/tuxanci2.xml + +TARGET_BIN := $(MAKEFILE_DIR)tuxanci2.bin +TARGET_CUE := $(MAKEFILE_DIR)tuxanci2.cue + +ARCHFLAGS := -march=mips1 -mabi=32 -EL -fno-pic -mno-shared -mno-abicalls -mfp32 -msoft-float -mno-llsc -G0 \ + -fno-stack-protector -nostdlib -ffreestanding +COMMONFLAGS := -ffunction-sections -fdata-sections -mno-gpopt -fomit-frame-pointer -fno-builtin \ + -fno-strict-aliasing -Wno-attributes + +PLATFORM_CFLAGS := -O2 $(ARCHFLAGS) $(COMMONFLAGS) -Wall -Wextra \ + -I$(ROOT_DIR)/include -I$(PS1_BUILD_DIR) -I$(NUGGET) -DTA_32BIT + +NUGGET_CFLAGS := -O2 $(ARCHFLAGS) $(COMMONFLAGS) -I$(NUGGET) + +ENGINE_CFLAGS := -O2 $(ARCHFLAGS) $(COMMONFLAGS) -DTA_32BIT -DTA_DISABLE_BILINEAR_FILTERING \ + -I$(ROOT_DIR)/include -I$(PS1_BUILD_DIR) \ + -std=c99 -nostdinc -Wall -Wextra -Werror -pedantic -pedantic-errors + +LDFLAGS := -Wl,-Map=$(PS1_MAP) -nostdlib -T$(NUGGET)/nooverlay.ld -T$(NUGGET)/ps-exe.ld -static \ + -Wl,--gc-sections $(ARCHFLAGS) -Wl,--oformat=$(PS1_OFORMAT) + +NUGGET_SRCS := common/crt0/crt0.s common/crt0/memory-s.s common/crt0/memory-c.c common/crt0/clz.c \ + third_party/arith64/arith64.c +NUGGET_OBJS := $(addprefix $(OBJ_DIR)/nugget/,$(addsuffix .o,$(basename $(NUGGET_SRCS)))) +PLATFORM_OBJS := $(OBJ_DIR)/ps1.o +OBJS := $(NUGGET_OBJS) $(PLATFORM_OBJS) + +ROOT_INPUTS := $(shell find $(ROOT_DIR)/src -type f -name '*.c') \ + $(wildcard $(ROOT_DIR)/include/*.h) \ + $(wildcard $(ROOT_DIR)/include/screens/*.h) \ + $(ROOT_DIR)/include/ta_config.h.in \ + $(shell find $(ROOT_DIR)/files -type f) \ + $(ROOT_DIR)/Makefile + +.DEFAULT_GOAL := all +.PHONY: all clean dump-license patch-license + +all: $(TARGET_CUE) $(TARGET_BIN) + +$(PS1_LIBRARY): $(ROOT_INPUTS) + $(MAKE) -C "$(ROOT_DIR)" CC="$(PS1_CC)" AR="$(PS1_AR)" BITS=32 \ + BUILD_DIR="$(PS1_BUILD)" TA_DISABLE_BILINEAR_FILTERING=true \ + CFLAGS="$(ENGINE_CFLAGS)" all + +$(PS1_BUILD_DIR)/ta_config.h: $(PS1_LIBRARY) + +$(OBJ_DIR)/ps1.o: $(MAKEFILE_DIR)ps1.c $(PS1_LIBRARY) $(wildcard $(ROOT_DIR)/include/*.h) + mkdir -p "$(@D)" + $(PS1_CC) $(PLATFORM_CFLAGS) -c "$<" -o "$@" + +$(OBJ_DIR)/nugget/%.o: $(NUGGET)/%.c + mkdir -p "$(@D)" + $(PS1_CC) $(NUGGET_CFLAGS) -c "$<" -o "$@" + +$(OBJ_DIR)/nugget/%.o: $(NUGGET)/%.s + mkdir -p "$(@D)" + $(PS1_CC) $(ARCHFLAGS) -I$(NUGGET) -c "$<" -o "$@" + +$(PS1_ELF): $(OBJS) $(PS1_LIBRARY) + mkdir -p "$(@D)" + $(PS1_CC) -o "$@" $(OBJS) $(PS1_LIBRARY) $(LDFLAGS) + +$(PS1_EXE): $(PS1_ELF) + mkdir -p "$(@D)" + $(PS1_OBJCOPY) -O binary "$<" "$@" + +$(SYSTEM_CNF): $(MAKEFILE_DIR)Makefile + mkdir -p "$(@D)" + printf 'BOOT = cdrom:\\PSX.EXE;1\r\nTCB = 4\r\nEVENT = 10\r\nSTACK = 801FFF00\r\n' > "$@" + +$(ISO_XML): $(MAKEFILE_DIR)Makefile + mkdir -p "$(@D)" + { \ + echo '<?xml version="1.0" encoding="UTF-8"?>'; \ + echo '<iso_project>'; \ + echo ' <track type="data">'; \ + echo ' <identifiers system="PLAYSTATION" application="PLAYSTATION" volume="TUXANCI2" volume_set="TUXANCI2"/>'; \ + echo ' <directory_tree>'; \ + echo ' <file name="SYSTEM.CNF" type="data" source="$(SYSTEM_CNF)"/>'; \ + echo ' <file name="PSX.EXE" type="data" source="$(PS1_EXE)"/>'; \ + echo ' <dummy sectors="1024"/>'; \ + echo ' </directory_tree>'; \ + echo ' </track>'; \ + echo '</iso_project>'; \ + } > "$@" + +$(TARGET_BIN) $(TARGET_CUE) &: $(PS1_EXE) $(SYSTEM_CNF) $(ISO_XML) + $(MKPSXISO) -y -q -o "$(TARGET_BIN)" -c "$(TARGET_CUE)" "$(ISO_XML)" + $(PSXLICENSEPATCH) --region $(PS1_REGION) \ + $(if $(PS1_TEXT),--text "$(PS1_TEXT)") \ + $(if $(PS1_TMD),--tmd "$(abspath $(PS1_TMD))") \ + "$(TARGET_BIN)" + +patch-license: $(TARGET_BIN) + $(PSXLICENSEPATCH) --region $(PS1_REGION) \ + $(if $(PS1_TEXT),--text "$(PS1_TEXT)") \ + $(if $(PS1_TMD),--tmd "$(abspath $(PS1_TMD))") \ + "$(TARGET_BIN)" + +clean: + $(RM) "$(TARGET_BIN)" "$(TARGET_CUE)" + $(MAKE) -C "$(ROOT_DIR)" BUILD_DIR="$(PS1_BUILD)" clean + $(RM) -r "$(PS1_BUILD_DIR)" diff --git a/platforms/ps1/ps1.c b/platforms/ps1/ps1.c @@ -0,0 +1,444 @@ +#include "ta_api.h" +#include "common/hardware/hwregs.h" +#include "common/hardware/irq.h" +#include "common/hardware/gpu.h" + +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 240 +#define WINDOW_BPP 32 + +#define STACK_RESERVE 0x20000u + +#define D2_MADR (*(volatile ta_uint32 *)0xbf8010a0) +#define D2_BCR (*(volatile ta_uint32 *)0xbf8010a4) +#define D2_CHCR (*(volatile ta_uint32 *)0xbf8010a8) + +#define SIO_DATA (*(volatile ta_uint8 *)0xbf801040) +#define SIO_STAT (*(volatile ta_uint16 *)0xbf801044) +#define SIO_MODE (*(volatile ta_uint16 *)0xbf801048) +#define SIO_CTRL (*(volatile ta_uint16 *)0xbf80104a) +#define SIO_BAUD (*(volatile ta_uint16 *)0xbf80104e) + +#define SIO_STAT_RXRDY 0x0002u +#define SIO_STAT_IRQ 0x0200u +#define SIO_CTRL_ACK 0x0010u +#define SIO_CTRL_RESET 0x0040u +#define SIO_CTRL_PAD 0x1003u + +extern char __bss_end[]; +extern char __ram_top[]; + +typedef struct heap_block { + ta_uint32 size; + ta_uint32 reserved; + struct heap_block *next; +} heap_block; + +#define HEAP_HEADER 8u +#define HEAP_MIN_PAYLOAD 8u + +static heap_block *heap_free_list; + +static void heap_init(void) { + ta_uint32 start = ((ta_uint32)__bss_end + 7u) & ~7u; + ta_uint32 end = ((ta_uint32)__ram_top - STACK_RESERVE) & ~7u; + + heap_free_list = (heap_block *)start; + heap_free_list->size = end - start - HEAP_HEADER; + heap_free_list->next = TA_NULL; +} + +void *ta_malloc(ta_size size) { + heap_block *previous = TA_NULL; + heap_block *block = heap_free_list; + + if (size == 0) { + return TA_NULL; + } + + size = (size + 7u) & ~7u; + if (size < HEAP_MIN_PAYLOAD) { + size = HEAP_MIN_PAYLOAD; + } + + while (block) { + if (block->size >= size) { + ta_uint32 remainder = block->size - (ta_uint32)size; + + if (remainder >= HEAP_HEADER + HEAP_MIN_PAYLOAD) { + heap_block *rest = (heap_block *)((ta_uint8 *)block + HEAP_HEADER + size); + + rest->size = remainder - HEAP_HEADER; + rest->next = block->next; + block->size = (ta_uint32)size; + + if (previous) { + previous->next = rest; + } else { + heap_free_list = rest; + } + } else if (previous) { + previous->next = block->next; + } else { + heap_free_list = block->next; + } + + return (ta_uint8 *)block + HEAP_HEADER; + } + + previous = block; + block = block->next; + } + + return TA_NULL; +} + +void ta_free(void *ptr) { + heap_block *block; + heap_block *previous = TA_NULL; + heap_block *next = heap_free_list; + + if (!ptr) { + return; + } + + block = (heap_block *)((ta_uint8 *)ptr - HEAP_HEADER); + + while (next && next < block) { + previous = next; + next = next->next; + } + + block->next = next; + + if (next && (ta_uint8 *)block + HEAP_HEADER + block->size == (ta_uint8 *)next) { + block->size += HEAP_HEADER + next->size; + block->next = next->next; + } + + if (previous) { + if ((ta_uint8 *)previous + HEAP_HEADER + previous->size == (ta_uint8 *)block) { + previous->size += HEAP_HEADER + block->size; + previous->next = block->next; + } else { + previous->next = block; + } + } else { + heap_free_list = block; + } +} + +void *ta_realloc(void *ptr, ta_size new_size) { + heap_block *block; + ta_uint8 *fresh; + ta_uint8 *source; + ta_size copy; + + if (!ptr) { + return ta_malloc(new_size); + } + + if (new_size == 0) { + ta_free(ptr); + return TA_NULL; + } + + block = (heap_block *)((ta_uint8 *)ptr - HEAP_HEADER); + if (block->size >= new_size) { + return ptr; + } + + fresh = (ta_uint8 *)ta_malloc(new_size); + if (!fresh) { + return TA_NULL; + } + + source = (ta_uint8 *)ptr; + copy = block->size; + for (ta_size i = 0; i < copy; i++) { + fresh[i] = source[i]; + } + + ta_free(ptr); + return fresh; +} + +void ta_exit(int status) { + (void)status; + + __asm__ volatile ( + "mfc0 $t0, $12\n\t" + "li $t1, 0xfffffffe\n\t" + "and $t0, $t0, $t1\n\t" + "mtc0 $t0, $12\n\t" + "lui $t0, 0xbfc0\n\t" + "jr $t0\n\t" + ); +} + +#define BUFFER_Y0 0 +#define BUFFER_Y1 SCREEN_HEIGHT + +static ta_uint16 framebuffer[SCREEN_WIDTH * SCREEN_HEIGHT] __attribute__((aligned(16))); +static int back_buffer; + +static void disable_interrupts(void) { + ta_uint32 status; + + __asm__ volatile("mfc0 %0, $12\n\tnop" : "=r"(status)); + status &= ~1u; + __asm__ volatile("mtc0 %0, $12\n\tnop\n\tnop" : : "r"(status)); +} + +static void wait_vblank(void) { + while ((IREG & IRQ_VBLANK) == 0) {} + IREG = ~(ta_uint32)IRQ_VBLANK; +} + +static ta_bool console_is_pal(void) { + return (ta_bool)(*(volatile char *)0xbfc7ff52 == 'E'); +} + +static void video_init(void) { + const ta_bool pal = console_is_pal(); + const ta_uint32 vertical_start = pal ? 0x23u : 0x10u; + const ta_uint32 vertical_end = vertical_start + SCREEN_HEIGHT; + struct FastFill fill; + + sendGPUStatus(0x00000000); + sendGPUStatus(0x08000001u | (pal ? 0x08u : 0x00u)); + sendGPUStatus(0x06000000u | 0x260u | ((0x260u + SCREEN_WIDTH * 8u) << 12)); + sendGPUStatus(0x07000000u | vertical_start | (vertical_end << 10)); + sendGPUStatus(generateDisplayArea(0, BUFFER_Y0)); + sendGPUStatus(0x04000002u); + + fill.c.packed = 0; + fill.x = 0; + fill.y = 0; + fill.w = SCREEN_WIDTH; + fill.h = SCREEN_HEIGHT * 2; + fastFill(&fill); + + enableDisplay(); + back_buffer = 1; +} + +static ta_uint16 pack_rgb(ta_uint32 r, ta_uint32 g, ta_uint32 b) { + return (ta_uint16)((r >> 3) | ((g >> 3) << 5) | ((b >> 3) << 10)); +} + +static void compose(void) { + const ta_uint32 count = ta_display_sprites ? ta_display_sprites->count : 0; + + ta_uint32 *clear = (ta_uint32 *)framebuffer; + + for (ta_uint32 i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT / 2; i += 8) { + clear[i + 0] = 0; + clear[i + 1] = 0; + clear[i + 2] = 0; + clear[i + 3] = 0; + clear[i + 4] = 0; + clear[i + 5] = 0; + clear[i + 6] = 0; + clear[i + 7] = 0; + } + + for (ta_uint32 s = 0; s < count; s++) { + const ta_display_sprite *sprite = ta_display_sprites->sprites[s]; + const ta_uint32 *source; + ta_uint32 width; + ta_uint32 height; + + if (!sprite || sprite->format != TA_DISPLAY_PIXEL_FORMAT_32 || + sprite->x >= SCREEN_WIDTH || sprite->y >= SCREEN_HEIGHT) { + continue; + } + + source = (const ta_uint32 *)sprite->pixels; + width = sprite->width; + height = sprite->height; + + for (ta_uint32 y = 0; y < height && sprite->y + y < SCREEN_HEIGHT; y++) { + const ta_uint32 *row = source + y * width; + ta_uint16 *destination = framebuffer + (sprite->y + y) * SCREEN_WIDTH + sprite->x; + + for (ta_uint32 x = 0; x < width && sprite->x + x < SCREEN_WIDTH; x++) { + const ta_uint32 pixel = row[x]; + const ta_uint32 alpha = pixel & 0xffu; + + if (alpha == 0) { + continue; + } + + if (alpha == 0xffu) { + destination[x] = pack_rgb(pixel >> 24, (pixel >> 16) & 0xffu, (pixel >> 8) & 0xffu); + } else { + const ta_uint16 below = destination[x]; + const ta_int32 dr = below & 0x1f; + const ta_int32 dg = (below >> 5) & 0x1f; + const ta_int32 db = (below >> 10) & 0x1f; + const ta_int32 sr = (ta_int32)(pixel >> 27); + const ta_int32 sg = (ta_int32)((pixel >> 19) & 0x1f); + const ta_int32 sb = (ta_int32)((pixel >> 11) & 0x1f); + const ta_int32 a = (ta_int32)alpha; + + destination[x] = (ta_uint16)( + (dr + (((sr - dr) * a + 128) >> 8)) | + ((dg + (((sg - dg) * a + 128) >> 8)) << 5) | + ((db + (((sb - db) * a + 128) >> 8)) << 10)); + } + } + } + } +} + +static void upload(int buffer) { + const ta_uint32 y = buffer ? BUFFER_Y1 : BUFFER_Y0; + const ta_uint32 words = SCREEN_WIDTH * SCREEN_HEIGHT / 2; + + waitGPU(); + GPU_DATA = 0xa0000000u; + GPU_DATA = 0u | (y << 16); + GPU_DATA = SCREEN_WIDTH | ((ta_uint32)SCREEN_HEIGHT << 16); + + DPCR |= 0x800u; + D2_MADR = (ta_uint32)framebuffer & 0x1fffffffu; + D2_BCR = ((words / 16u) << 16) | 16u; + D2_CHCR = 0x01000201u; + + while (D2_CHCR & 0x01000000u) {} +} + +static void flip(int buffer) { + sendGPUStatus(generateDisplayArea(0, buffer ? BUFFER_Y1 : BUFFER_Y0)); +} + +static ta_bool pad_exchange(ta_uint8 out, ta_uint8 *in, ta_bool wait_ack) { + ta_uint32 timeout = 0x4000; + + SIO_DATA = out; + + while (!(SIO_STAT & SIO_STAT_RXRDY)) { + if (--timeout == 0) { + return ta_false; + } + } + + *in = SIO_DATA; + + if (wait_ack) { + timeout = 0x400; + + while (!(SIO_STAT & SIO_STAT_IRQ)) { + if (--timeout == 0) { + return ta_false; + } + } + + SIO_CTRL = SIO_CTRL_PAD | SIO_CTRL_ACK; + } + + return ta_true; +} + +static ta_bool pad_read(ta_uint16 *buttons) { + static const ta_uint8 request[5] = { + 0x01, + 0x42, + 0x00, + 0x00, + 0x00 + }; + + ta_uint8 reply[5] = { + 0xff, + 0xff, + 0xff, + 0xff, + 0xff + }; + + ta_bool ok = ta_true; + + SIO_CTRL = SIO_CTRL_PAD; + + for (ta_uint32 i = 0; i < 5 && ok; i++) { + ok = pad_exchange(request[i], &reply[i], (ta_bool)(i < 4)); + } + + SIO_CTRL = 0; + + if (!ok || reply[2] != 0x5a) { + return ta_false; + } + + *buttons = (ta_uint16)(reply[3] | ((ta_uint16)reply[4] << 8)); + return ta_true; +} + +static void pad_init(void) { + SIO_CTRL = SIO_CTRL_RESET; + SIO_MODE = 0x000d; + SIO_BAUD = 0x0088; + SIO_CTRL = 0; +} + +static void set_key(ta_keyboard_keys key, ta_bool down) { + if (down) { + ta_keyboard_key_down(key); + } else { + ta_keyboard_key_up(key); + } +} + +static void update_controller_input(void) { + ta_uint16 buttons; + + if (!pad_read(&buttons)) { + ta_keyboard_clear(); + return; + } + + buttons = (ta_uint16)~buttons; + + set_key(KEY_UP, (ta_bool)((buttons & 0x0010u) != 0)); + set_key(KEY_DOWN, (ta_bool)((buttons & 0x0040u) != 0)); + set_key(KEY_LEFT, (ta_bool)((buttons & 0x0080u) != 0)); + set_key(KEY_RIGHT, (ta_bool)((buttons & 0x0020u) != 0)); + set_key(KEY_ENTER, (ta_bool)((buttons & 0x0008u) != 0)); +} + +int main(void) { + disable_interrupts(); + heap_init(); + video_init(); + pad_init(); + + ta_display->width = SCREEN_WIDTH; + ta_display->height = SCREEN_HEIGHT; + ta_display->bpp = WINDOW_BPP; + ta_display->sprite_mode = ta_true; + + ta_main_init(); + + for (;;) { + ta_bool redraw; + + update_controller_input(); + redraw = ta_main_update(); + + if (redraw) { + compose(); + upload(back_buffer); + } + + wait_vblank(); + + if (redraw) { + flip(back_buffer); + back_buffer ^= 1; + } + } + + return 0; +}