diff --git a/cmake/targets/saturn.cmake b/cmake/targets/saturn.cmake new file mode 100644 index 00000000..15ca97af --- /dev/null +++ b/cmake/targets/saturn.cmake @@ -0,0 +1,106 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Resolve YAUL_INSTALL_ROOT (already set by the toolchain file, but guard +# in case cmake/targets/ is loaded standalone for IDE tooling). +if(NOT DEFINED YAUL_INSTALL_ROOT) + if(DEFINED ENV{YAUL_INSTALL_ROOT}) + set(YAUL_INSTALL_ROOT "$ENV{YAUL_INSTALL_ROOT}") + else() + set(YAUL_INSTALL_ROOT "/opt/yaul") + endif() +endif() + +# Yaul installs headers/libs under the arch-prefix sysroot subdirectory: +# ${YAUL_INSTALL_ROOT}/sh2eb-elf/include/ +# ${YAUL_INSTALL_ROOT}/sh2eb-elf/lib/ +# Cross-compiled zlib and libzip are installed to the same sysroot. +set(_YAUL_SYSROOT "${YAUL_INSTALL_ROOT}/sh2eb-elf") +set(_YAUL_BIN "${YAUL_INSTALL_ROOT}/bin") + +# --------------------------------------------------------------------------- +# Bypass system find_package calls for libraries we cross-compile into the +# sh2eb-elf sysroot and install into ${_YAUL_SYSROOT}. +# --------------------------------------------------------------------------- + +# zlib +if(NOT TARGET ZLIB::ZLIB) + add_library(ZLIB::ZLIB INTERFACE IMPORTED GLOBAL) + set_target_properties(ZLIB::ZLIB PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_YAUL_SYSROOT}/include" + INTERFACE_LINK_LIBRARIES "${_YAUL_SYSROOT}/lib/libz.a" + ) +endif() +set(ZLIB_FOUND TRUE CACHE BOOL "" FORCE) +set(ZLIB_INCLUDE_DIR "${_YAUL_SYSROOT}/include" CACHE PATH "" FORCE) +set(ZLIB_LIBRARY "${_YAUL_SYSROOT}/lib/libz.a" CACHE FILEPATH "" FORCE) + +# libzip — pre-installed into the sh2eb-elf sysroot; skip Findlibzip.cmake. +set(libzip_FOUND TRUE CACHE BOOL "libzip found (Saturn sysroot)" FORCE) +find_path(_sat_zip_inc NAMES zip.h + PATHS "${_YAUL_SYSROOT}/include" + NO_DEFAULT_PATH +) +if(_sat_zip_inc) + target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE "${_sat_zip_inc}") +endif() + +# --------------------------------------------------------------------------- +# Compile definitions +# --------------------------------------------------------------------------- +target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + DUSK_SATURN + DUSK_INPUT_GAMEPAD + DUSK_PLATFORM_ENDIAN_BIG + DUSK_DISPLAY_WIDTH=320 + DUSK_DISPLAY_HEIGHT=224 + DUSK_THREAD_NONE +) + +# --------------------------------------------------------------------------- +# Compile options +# --------------------------------------------------------------------------- +target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE + -m2 -mb + -fno-builtin + -fomit-frame-pointer + -w +) + +# --------------------------------------------------------------------------- +# Include paths +# --------------------------------------------------------------------------- +target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE + "${_YAUL_SYSROOT}/include" + "${_YAUL_SYSROOT}/include/yaul" +) + +# --------------------------------------------------------------------------- +# Link libraries +# --------------------------------------------------------------------------- +target_link_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE + "${_YAUL_SYSROOT}/lib" +) + +target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE + "${_YAUL_SYSROOT}/lib/libyaul.a" + "${_YAUL_SYSROOT}/lib/libzip.a" + "${_YAUL_SYSROOT}/lib/libz.a" + m +) + +# --------------------------------------------------------------------------- +# Post-build: ELF → binary image +# sh2eb-elf-objcopy converts the ELF to a flat binary that IP.BIN loads +# into Saturn Work RAM. +# --------------------------------------------------------------------------- +set(DUSK_SAT_BIN "${CMAKE_BINARY_DIR}/Dusk.bin") +add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD + COMMAND "${_YAUL_BIN}/sh2eb-elf-objcopy" + -O binary + "$" + "${DUSK_SAT_BIN}" + COMMENT "Converting ${DUSK_BINARY_TARGET_NAME} ELF → ${DUSK_SAT_BIN}" +) diff --git a/cmake/toolchains/saturn.cmake b/cmake/toolchains/saturn.cmake new file mode 100644 index 00000000..a041dbeb --- /dev/null +++ b/cmake/toolchains/saturn.cmake @@ -0,0 +1,59 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT +# +# CMake toolchain file for Sega Saturn (Hitachi SH-2, big-endian) +# using the Yaul homebrew SDK. Set YAUL_INSTALL_ROOT or the +# YAUL_INSTALL_ROOT environment variable before invoking cmake. + +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR sh2) + +# Resolve Yaul install root +if(NOT DEFINED YAUL_INSTALL_ROOT) + if(DEFINED ENV{YAUL_INSTALL_ROOT}) + set(YAUL_INSTALL_ROOT "$ENV{YAUL_INSTALL_ROOT}" + CACHE PATH "Yaul SDK root" FORCE) + else() + set(YAUL_INSTALL_ROOT "/opt/yaul" + CACHE PATH "Yaul SDK root" FORCE) + endif() +endif() + +# Yaul SH-2 cross-compiler prefix is sh2eb-elf (big-endian SH-2 ELF). +# Binaries live in ${YAUL_INSTALL_ROOT}/bin/; headers/libs under +# ${YAUL_INSTALL_ROOT}/sh2eb-elf/{include,lib}/. +set(_YAUL_BIN "${YAUL_INSTALL_ROOT}/bin") + +set(CMAKE_C_COMPILER "${_YAUL_BIN}/sh2eb-elf-gcc") +set(CMAKE_CXX_COMPILER "${_YAUL_BIN}/sh2eb-elf-g++") +set(CMAKE_AR "${_YAUL_BIN}/sh2eb-elf-ar") +set(CMAKE_RANLIB "${_YAUL_BIN}/sh2eb-elf-ranlib") +set(CMAKE_STRIP "${_YAUL_BIN}/sh2eb-elf-strip") +set(CMAKE_OBJCOPY "${_YAUL_BIN}/sh2eb-elf-objcopy") +set(CMAKE_LINKER "${_YAUL_BIN}/sh2eb-elf-ld") + +set(CMAKE_CROSSCOMPILING TRUE) + +# Tell CMake not to try to run built executables +set(CMAKE_CROSSCOMPILING_EMULATOR "" CACHE STRING "" FORCE) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +# Sysroot — Yaul installs headers/libs under the arch-prefix subdirectory +set(_YAUL_SYSROOT "${YAUL_INSTALL_ROOT}/sh2eb-elf") +set(CMAKE_FIND_ROOT_PATH "${_YAUL_SYSROOT}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +# SH-2 core flags: big-endian (-mb), SH-2 ISA (-m2), no FPU +set(_SAT_C_FLAGS "-m2 -mb -fno-builtin -fomit-frame-pointer") +set(CMAKE_C_FLAGS_INIT "${_SAT_C_FLAGS}") + +# Yaul provides its own startup code and linker script. +# The kernel.ld script maps Saturn Work RAM (0x06000000+). +set(_YAUL_LD "${YAUL_INSTALL_ROOT}/share/yaul/ip/kernel.ld") +set(CMAKE_EXE_LINKER_FLAGS_INIT + "-T\"${_YAUL_LD}\" -Wl,--start-group -Wl,--end-group -nostartfiles") diff --git a/docker/saturn/Dockerfile b/docker/saturn/Dockerfile new file mode 100644 index 00000000..542f4da0 --- /dev/null +++ b/docker/saturn/Dockerfile @@ -0,0 +1,248 @@ +FROM ubuntu:22.04 + +LABEL org.opencontainers.image.description="Dusk Engine — Sega Saturn build environment (sh2eb-elf cross-compiler + Yaul SDK)" + +ENV DEBIAN_FRONTEND=noninteractive +ENV YAUL_INSTALL_ROOT=/opt/yaul + +# All variables required by Yaul's env.mk +ENV YAUL_ARCH_SH_PREFIX=sh2eb-elf +ENV YAUL_PROG_SH_PREFIX=sh2eb-elf +ENV YAUL_ARCH_M68K_PREFIX=m68keb-elf +ENV YAUL_BUILD_ROOT=/tmp/yaul-build +ENV YAUL_BUILD=build +ENV YAUL_OPTION_MALLOC_IMPL=tlsf + +ENV PATH="${YAUL_INSTALL_ROOT}/bin:${PATH}" + +# Toolchain source versions +ARG BINUTILS_VER=2.40 +ARG GCC_VER=12.3.0 +ARG NEWLIB_VER=4.3.0.20230120 + +# --------------------------------------------------------------------------- +# 1. Host build tools +# --------------------------------------------------------------------------- +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + git \ + wget \ + curl \ + xz-utils \ + python3 \ + python3-pip \ + python3-polib \ + python3-pil \ + python3-dotenv \ + texinfo \ + bison \ + flex \ + libgmp-dev \ + libmpfr-dev \ + libmpc-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p "${YAUL_INSTALL_ROOT}" + +# --------------------------------------------------------------------------- +# 2. Download cross-compiler sources +# --------------------------------------------------------------------------- +RUN cd /tmp && \ + wget -q "https://ftp.gnu.org/gnu/binutils/binutils-${BINUTILS_VER}.tar.xz" && \ + wget -q "https://ftp.gnu.org/gnu/gcc/gcc-${GCC_VER}/gcc-${GCC_VER}.tar.xz" && \ + wget -q "ftp://sourceware.org/pub/newlib/newlib-${NEWLIB_VER}.tar.gz" && \ + tar xf "binutils-${BINUTILS_VER}.tar.xz" && \ + tar xf "gcc-${GCC_VER}.tar.xz" && \ + tar xf "newlib-${NEWLIB_VER}.tar.gz" && \ + rm "binutils-${BINUTILS_VER}.tar.xz" "gcc-${GCC_VER}.tar.xz" "newlib-${NEWLIB_VER}.tar.gz" + +# Download GCC prerequisites (gmp, mpfr, mpc if not packaged) +RUN cd /tmp/gcc-${GCC_VER} && contrib/download_prerequisites + +# --------------------------------------------------------------------------- +# 3. sh2eb-elf binutils (SH-2 big-endian) +# --------------------------------------------------------------------------- +RUN mkdir -p /tmp/build-sh-binutils && cd /tmp/build-sh-binutils && \ + /tmp/binutils-${BINUTILS_VER}/configure \ + --target=sh2eb-elf \ + --prefix="${YAUL_INSTALL_ROOT}" \ + --disable-nls \ + --disable-multilib \ + --disable-werror \ + && make -j"$(nproc)" && make install && \ + rm -rf /tmp/build-sh-binutils + +# --------------------------------------------------------------------------- +# 4. sh2eb-elf GCC stage 1 (compiler only, no C library yet) +# --------------------------------------------------------------------------- +RUN mkdir -p /tmp/build-sh-gcc1 && cd /tmp/build-sh-gcc1 && \ + /tmp/gcc-${GCC_VER}/configure \ + --target=sh2eb-elf \ + --prefix="${YAUL_INSTALL_ROOT}" \ + --enable-languages=c,c++ \ + --without-headers \ + --with-newlib \ + --disable-nls \ + --disable-shared \ + --disable-multilib \ + --disable-decimal-float \ + --disable-threads \ + --disable-libatomic \ + --disable-libgomp \ + --disable-libquadmath \ + --disable-libssp \ + --disable-libvtv \ + --disable-libstdcxx \ + && make -j"$(nproc)" all-gcc all-target-libgcc \ + && make install-gcc install-target-libgcc && \ + rm -rf /tmp/build-sh-gcc1 + +# --------------------------------------------------------------------------- +# 5. newlib for sh2eb-elf (C runtime for embedded SH-2) +# --------------------------------------------------------------------------- +RUN mkdir -p /tmp/build-sh-newlib && cd /tmp/build-sh-newlib && \ + /tmp/newlib-${NEWLIB_VER}/configure \ + --target=sh2eb-elf \ + --prefix="${YAUL_INSTALL_ROOT}" \ + --disable-newlib-supplied-syscalls \ + --enable-newlib-reent-small \ + && make -j"$(nproc)" && make install && \ + rm -rf /tmp/build-sh-newlib + +# --------------------------------------------------------------------------- +# 6. sh2eb-elf GCC stage 2 (full build with newlib) +# --------------------------------------------------------------------------- +RUN mkdir -p /tmp/build-sh-gcc2 && cd /tmp/build-sh-gcc2 && \ + /tmp/gcc-${GCC_VER}/configure \ + --target=sh2eb-elf \ + --prefix="${YAUL_INSTALL_ROOT}" \ + --enable-languages=c,c++ \ + --with-newlib \ + --disable-nls \ + --disable-shared \ + --disable-multilib \ + --disable-libssp \ + --disable-libgomp \ + --disable-libquadmath \ + && make -j"$(nproc)" && make install && \ + rm -rf /tmp/build-sh-gcc2 + +# --------------------------------------------------------------------------- +# 7. m68k-elf binutils (Saturn 68EC000 sound CPU) +# --------------------------------------------------------------------------- +RUN mkdir -p /tmp/build-m68k-binutils && cd /tmp/build-m68k-binutils && \ + /tmp/binutils-${BINUTILS_VER}/configure \ + --target=m68k-elf \ + --prefix="${YAUL_INSTALL_ROOT}" \ + --disable-nls \ + --disable-multilib \ + --disable-werror \ + && make -j"$(nproc)" && make install && \ + rm -rf /tmp/build-m68k-binutils + +# --------------------------------------------------------------------------- +# 8. m68k-elf GCC (compiler only; Yaul provides its own sound startup) +# --------------------------------------------------------------------------- +RUN mkdir -p /tmp/build-m68k-gcc && cd /tmp/build-m68k-gcc && \ + /tmp/gcc-${GCC_VER}/configure \ + --target=m68k-elf \ + --prefix="${YAUL_INSTALL_ROOT}" \ + --enable-languages=c \ + --without-headers \ + --with-newlib \ + --disable-nls \ + --disable-shared \ + --disable-multilib \ + --disable-libssp \ + && make -j"$(nproc)" all-gcc && make install-gcc && \ + rm -rf /tmp/build-m68k-gcc + +# Clean up source tarballs/trees +RUN rm -rf /tmp/binutils-${BINUTILS_VER} /tmp/gcc-${GCC_VER} /tmp/newlib-${NEWLIB_VER} + +# --------------------------------------------------------------------------- +# 9. Create m68keb-elf symlinks +# Yaul expects YAUL_ARCH_M68K_PREFIX=m68keb-elf but we built m68k-elf. +# m68k is always big-endian, so m68k-elf == m68keb-elf semantically. +# --------------------------------------------------------------------------- +RUN for tool in "${YAUL_INSTALL_ROOT}/bin/m68k-elf-"*; do \ + base="$(basename "$tool")"; \ + newname="${YAUL_INSTALL_ROOT}/bin/m68keb-elf-${base#m68k-elf-}"; \ + ln -sf "$tool" "$newname"; \ + done + +# --------------------------------------------------------------------------- +# 10. Clone and install libyaul +# --------------------------------------------------------------------------- +RUN git clone --depth 1 --recurse-submodules \ + https://github.com/yaul-org/libyaul.git /tmp/yaul && \ + cd /tmp/yaul && \ + YAUL_INSTALL_ROOT="${YAUL_INSTALL_ROOT}" \ + YAUL_ARCH_SH_PREFIX=sh2eb-elf \ + YAUL_PROG_SH_PREFIX=sh2eb-elf \ + YAUL_ARCH_M68K_PREFIX=m68keb-elf \ + YAUL_BUILD_ROOT=/tmp/yaul-build \ + YAUL_BUILD=build \ + YAUL_OPTION_MALLOC_IMPL=tlsf \ + make install && \ + rm -rf /tmp/yaul /tmp/yaul-build + +# --------------------------------------------------------------------------- +# 11. Cross-compile zlib for sh2eb-elf +# Install into ${YAUL_INSTALL_ROOT}/sh2eb-elf/ to match the Yaul sysroot +# layout: headers at .../sh2eb-elf/include, libs at .../sh2eb-elf/lib. +# --------------------------------------------------------------------------- +RUN wget -q https://zlib.net/zlib-1.3.1.tar.gz -O /tmp/zlib.tar.gz && \ + tar xf /tmp/zlib.tar.gz -C /tmp && \ + cd /tmp/zlib-1.3.1 && \ + CC="${YAUL_INSTALL_ROOT}/bin/sh2eb-elf-gcc" \ + AR="${YAUL_INSTALL_ROOT}/bin/sh2eb-elf-ar" \ + RANLIB="${YAUL_INSTALL_ROOT}/bin/sh2eb-elf-ranlib" \ + CFLAGS="-m2 -mb -fno-builtin -O2" \ + ./configure \ + --prefix="${YAUL_INSTALL_ROOT}/sh2eb-elf" \ + --static \ + && make -j"$(nproc)" && make install && \ + rm -rf /tmp/zlib-1.3.1 /tmp/zlib.tar.gz + +# --------------------------------------------------------------------------- +# 12. Cross-compile libzip for sh2eb-elf +# --------------------------------------------------------------------------- +RUN printf '%s\n' \ + 'set(CMAKE_SYSTEM_NAME Generic)' \ + 'set(CMAKE_SYSTEM_PROCESSOR sh2)' \ + "set(CMAKE_C_COMPILER \"${YAUL_INSTALL_ROOT}/bin/sh2eb-elf-gcc\")" \ + "set(CMAKE_AR \"${YAUL_INSTALL_ROOT}/bin/sh2eb-elf-ar\")" \ + "set(CMAKE_RANLIB \"${YAUL_INSTALL_ROOT}/bin/sh2eb-elf-ranlib\")" \ + 'set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)' \ + 'set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)' \ + 'set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)' \ + 'set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)' \ + > /tmp/sat-xc.cmake + +RUN wget -q https://libzip.org/download/libzip-1.10.1.tar.gz -O /tmp/libzip.tar.gz && \ + tar xf /tmp/libzip.tar.gz -C /tmp && \ + cmake -S /tmp/libzip-1.10.1 -B /tmp/libzip-build \ + -DCMAKE_TOOLCHAIN_FILE=/tmp/sat-xc.cmake \ + -DCMAKE_INSTALL_PREFIX="${YAUL_INSTALL_ROOT}/sh2eb-elf" \ + -DCMAKE_FIND_ROOT_PATH="${YAUL_INSTALL_ROOT}/sh2eb-elf" \ + -DCMAKE_C_FLAGS="-m2 -mb -fno-builtin -O2" \ + -DZLIB_LIBRARY="${YAUL_INSTALL_ROOT}/sh2eb-elf/lib/libz.a" \ + -DZLIB_INCLUDE_DIR="${YAUL_INSTALL_ROOT}/sh2eb-elf/include" \ + -DENABLE_BZIP2=OFF \ + -DENABLE_LZMA=OFF \ + -DENABLE_ZSTD=OFF \ + -DENABLE_OPENSSL=OFF \ + -DENABLE_GNUTLS=OFF \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DBUILD_DOCUMENTATION=OFF \ + -DBUILD_REGRESS=OFF \ + -DBUILD_TOOLS=OFF \ + && cmake --build /tmp/libzip-build -- -j"$(nproc)" \ + && cmake --install /tmp/libzip-build \ + && rm -rf /tmp/libzip-1.10.1 /tmp/libzip.tar.gz /tmp/libzip-build /tmp/sat-xc.cmake + +WORKDIR /workdir +VOLUME ["/workdir"] diff --git a/scripts/build-saturn-docker.sh b/scripts/build-saturn-docker.sh new file mode 100755 index 00000000..22023040 --- /dev/null +++ b/scripts/build-saturn-docker.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker build -t dusk-saturn -f docker/saturn/Dockerfile . +docker run --rm -v "$(pwd):/workdir" dusk-saturn /bin/bash -c "./scripts/build-saturn.sh" diff --git a/scripts/build-saturn.sh b/scripts/build-saturn.sh new file mode 100755 index 00000000..ffe7c83a --- /dev/null +++ b/scripts/build-saturn.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +if [ -z "$YAUL_INSTALL_ROOT" ]; then + if [ -d "/opt/yaul" ]; then + export YAUL_INSTALL_ROOT=/opt/yaul + else + echo "YAUL_INSTALL_ROOT is not set. Please set it to your Yaul SDK installation." + exit 1 + fi +fi + +mkdir -p build-saturn +cmake -S . -B build-saturn \ + -DDUSK_TARGET_SYSTEM=saturn \ + -DCMAKE_TOOLCHAIN_FILE="cmake/toolchains/saturn.cmake" \ + -DYAUL_INSTALL_ROOT="${YAUL_INSTALL_ROOT}" +cmake --build build-saturn -- -j"$(nproc)" diff --git a/scripts/psp-debug.sh b/scripts/psp-debug.sh new file mode 100755 index 00000000..cfba7b0f --- /dev/null +++ b/scripts/psp-debug.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# psp-debug.sh — build (optional), reset, and run Dusk on a live PSP. +# +# Prerequisites: +# usbhostfs_pc running (served from the build-psp/ dir, as host0:). +# PSP must be running psplink (visible on PSP screen). +# +# Usage: +# ./scripts/psp-debug.sh # reset + run +# ./scripts/psp-debug.sh --build # rebuild via docker first, then run +# +# PSP stdout streams to this terminal via pspsh (logDebug, printf, etc.). +# Run from a real terminal — pspsh needs stdin connected to a TTY. +# +# IMPORTANT: crashes put the PSP hardware in a bad state. +# Always reset before relaunching. If reset doesn't respond, power-cycle the PSP. + +set -euo pipefail + +PSPSH="pspsh" +PRX_HOST="host0:/Dusk.prx" +RESET_SLEEP=3 + +# ---- rebuild (optional) ----------------------------------------------------- + +if [[ "${1:-}" == "--build" || "${1:-}" == "-b" ]]; then + echo "==> Building PSP (docker)..." + "$(dirname "$0")/build-psp-docker.sh" + echo "" +fi + +# ---- sanity checks ---------------------------------------------------------- + +if ! command -v "$PSPSH" &>/dev/null; then + echo "ERROR: pspsh not found in PATH" + echo " Add /home/yourwishes/pspdev/bin to PATH or set PSPSH= in this script." + exit 1 +fi + +if ! nc -z localhost 10000 2>/dev/null; then + echo "ERROR: psplink is not reachable on localhost:10000." + echo " Ensure usbhostfs_pc is running and the PSP shows the psplink prompt." + exit 1 +fi + +# ---- reset any running process ---------------------------------------------- + +echo "==> Resetting PSP..." +"$PSPSH" -e "reset" +echo " Waiting ${RESET_SLEEP}s for PSP to settle..." +sleep "$RESET_SLEEP" + +# ---- launch + stream output ------------------------------------------------- +# Pre-send the exec command, then relay /dev/tty so pspsh keeps a live stdin. +# PSP stdout flows through pspsh directly to this terminal. +# Ctrl-C to stop. + +echo "==> Launching $PRX_HOST" +echo " (PSP stdout streams here — Ctrl-C to stop)" +echo "------------------------------------------------------------" +{ printf 'exec %s\n' "$PRX_HOST"; cat /dev/tty; } | "$PSPSH" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a327f93d..4c5b4ce0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,5 +21,8 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "vita") elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube") add_subdirectory(duskdolphin) - + +elseif(DUSK_TARGET_SYSTEM STREQUAL "saturn") + add_subdirectory(dusksat) + endif() \ No newline at end of file diff --git a/src/dusk/thread/threadlocal.h b/src/dusk/thread/threadlocal.h index 0e2cad82..ce6ff1c9 100644 --- a/src/dusk/thread/threadlocal.h +++ b/src/dusk/thread/threadlocal.h @@ -10,6 +10,9 @@ #ifdef DUSK_THREAD_PTHREAD #define THREAD_LOCAL __thread +#elif defined(DUSK_THREAD_NONE) + /* Single-threaded platforms: no thread-local storage qualifier needed. */ + #define THREAD_LOCAL #endif #ifndef THREAD_LOCAL diff --git a/src/dusk/thread/threadmutex.h b/src/dusk/thread/threadmutex.h index 29ef7ba7..27ca85ab 100644 --- a/src/dusk/thread/threadmutex.h +++ b/src/dusk/thread/threadmutex.h @@ -10,7 +10,7 @@ #ifdef DUSK_THREAD_PTHREAD #include -#else +#elif !defined(DUSK_THREAD_NONE) #error "At least one threading implementation must be defined." #endif @@ -18,6 +18,8 @@ typedef struct threadlock_t { #ifdef DUSK_THREAD_PTHREAD pthread_mutex_t mutex; pthread_cond_t cond; + #else + uint8_t unused; /* DUSK_THREAD_NONE: no real mutex on single-threaded platforms */ #endif } threadmutex_t; diff --git a/src/duskdolphin/display/render/renderdolphin.c b/src/duskdolphin/display/render/renderdolphin.c index 867b2612..dce71a91 100644 --- a/src/duskdolphin/display/render/renderdolphin.c +++ b/src/duskdolphin/display/render/renderdolphin.c @@ -35,6 +35,19 @@ typedef struct { static dolphintexentry_t dolphinTexTable[DOLPHIN_RTEXTURE_MAX]; static uint16_t dolphinTexNext = 1; /* 0 = white fallback */ +/* ---- Chunk table --------------------------------------------------------- */ + +#define DOLPHIN_CHUNK_MAX 256 + +typedef struct { + void *dispList; + uint32_t dispListSize; + rtexture_t tileset; +} dolphinchunkentry_t; + +static dolphinchunkentry_t dolphinChunkTable[DOLPHIN_CHUNK_MAX]; +static uint16_t dolphinChunkNext = 1; /* 0 = RTILEMAPCHUNK_INVALID */ + /* ---- Camera state -------------------------------------------------------- */ static Mtx44 dolphinProj; @@ -293,6 +306,109 @@ static void draw3DQuad(const ropquad3d_t *q) { GX_End(); } +/* ---- Tilemap chunks ------------------------------------------------------ */ + +rtilemapchunk_t renderDolphinTilemapChunkCreate( + uint16_t chunkW, uint16_t chunkH, + uint16_t tileW, uint16_t tileH, + rtexture_t tileset, + const uint8_t *tileIndices +) { + assertTrue(dolphinChunkNext < DOLPHIN_CHUNK_MAX, "Dolphin chunk table full"); + assertTrue(tileW > 0 && tileH > 0, "Tile dimensions must be > 0"); + assertTrue( + tileset < DOLPHIN_RTEXTURE_MAX && dolphinTexTable[tileset].cpuIndices, + "Dolphin chunk: invalid tileset handle" + ); + + uint16_t texW = dolphinTexTable[tileset].w; + uint16_t texH = dolphinTexTable[tileset].h; + assertTrue(texW >= tileW && texH >= tileH, "Tileset smaller than one tile"); + uint16_t tilesPerRow = texW / tileW; + + rtilemapchunk_t handle = (rtilemapchunk_t)dolphinChunkNext++; + dolphinchunkentry_t *e = &dolphinChunkTable[handle]; + e->tileset = tileset; + + /* Allocate display list buffer. Each tile = 4 GX_QUADS verts. + * Per vertex: 4B pos(XY/S16) + 4B color(RGBA8) + 8B uv(ST/F32) = 16B. + * Add 64 bytes of header/padding margin; align to 32. */ + uint32_t tileCount = (uint32_t)chunkW * chunkH; + uint32_t alignedSize = (tileCount * 4u * 16u + 64u + 31u) & ~31u; + + e->dispList = memalign(32, alignedSize); + assertNotNull(e->dispList, "Dolphin: failed to allocate chunk display list"); + memset(e->dispList, 0, alignedSize); + + /* Set up the vertex descriptor that will be baked into the display list. + * Matches setup2D() exactly; done here so chunks can be created before + * the first flush. */ + GX_ClearVtxDesc(); + GX_SetVtxDesc(GX_VA_POS, GX_DIRECT); + GX_SetVtxDesc(GX_VA_CLR0, GX_DIRECT); + GX_SetVtxDesc(GX_VA_TEX0, GX_DIRECT); + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XY, GX_S16, 0); + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + + GX_BeginDispList(e->dispList, alignedSize); + GX_Begin(GX_QUADS, GX_VTXFMT0, (uint16_t)(tileCount * 4)); + for(uint32_t ci = 0; ci < tileCount; ci++) { + uint8_t idx = tileIndices[ci]; + uint16_t tileCol = idx % tilesPerRow; + uint16_t tileRow = idx / tilesPerRow; + + int16_t px0 = (int16_t)((ci % chunkW) * tileW); + int16_t py0 = (int16_t)((ci / chunkW) * tileH); + int16_t px1 = (int16_t)(px0 + tileW); + int16_t py1 = (int16_t)(py0 + tileH); + + float u0 = (float)(tileCol * tileW) / (float)texW; + float v0 = (float)(tileRow * tileH) / (float)texH; + float u1 = u0 + (float)tileW / (float)texW; + float v1 = v0 + (float)tileH / (float)texH; + + GX_Position2s16(px0, py0); GX_Color4u8(255,255,255,255); GX_TexCoord2f32(u0,v0); + GX_Position2s16(px1, py0); GX_Color4u8(255,255,255,255); GX_TexCoord2f32(u1,v0); + GX_Position2s16(px1, py1); GX_Color4u8(255,255,255,255); GX_TexCoord2f32(u1,v1); + GX_Position2s16(px0, py1); GX_Color4u8(255,255,255,255); GX_TexCoord2f32(u0,v1); + } + GX_End(); + e->dispListSize = GX_EndDispList(); + DCFlushRange(e->dispList, alignedSize); + + /* Restore 2D vertex state for subsequent draws. */ + dolphinIs3D = 0; + + return handle; +} + +void renderDolphinTilemapChunkDispose(rtilemapchunk_t chunk) { + if(chunk == RTILEMAPCHUNK_INVALID || chunk >= DOLPHIN_CHUNK_MAX) return; + dolphinchunkentry_t *e = &dolphinChunkTable[chunk]; + if(e->dispList) { free(e->dispList); e->dispList = NULL; } +} + +static void draw2DTilemapChunk(const roptilemapc_t *t) { + if(t->chunk == RTILEMAPCHUNK_INVALID || t->chunk >= dolphinChunkNext) return; + dolphinchunkentry_t *e = &dolphinChunkTable[t->chunk]; + if(!e->dispList) return; + + if(dolphinIs3D) setup2D(); + + bindTexture(e->tileset); + setTintChannel(COLOR_WHITE); + + /* Load the scroll offset as a translation into PNMTX1, call the display + * list, then restore PNMTX0 (identity, set by setup2D). */ + Mtx translate; + guMtxTrans(translate, (float)t->x, (float)t->y, 0.0f); + GX_LoadPosMtxImm(translate, GX_PNMTX1); + GX_SetCurrentMtx(GX_PNMTX1); + GX_CallDispList(e->dispList, e->dispListSize); + GX_SetCurrentMtx(GX_PNMTX0); +} + /* ---- Flush --------------------------------------------------------------- */ errorret_t renderDolphinFlush(ropbuffer_t *buf) { @@ -351,6 +467,10 @@ errorret_t renderDolphinFlush(ropbuffer_t *buf) { draw3DQuad((const ropquad3d_t *)hdr); break; + case ROP_DRAW_TILEMAP_CHUNK: + draw2DTilemapChunk((const roptilemapc_t *)hdr); + break; + default: break; } @@ -363,6 +483,12 @@ errorret_t renderDolphinFlush(ropbuffer_t *buf) { /* ---- Dispose ------------------------------------------------------------- */ void renderDolphinDispose(void) { + for(uint16_t i = 1; i < dolphinChunkNext; i++) { + dolphinchunkentry_t *e = &dolphinChunkTable[i]; + if(e->dispList) { free(e->dispList); e->dispList = NULL; } + } + dolphinChunkNext = 1; + for(uint16_t i = 0; i < dolphinTexNext; i++) { dolphintexentry_t *e = &dolphinTexTable[i]; if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } diff --git a/src/duskdolphin/display/render/renderdolphin.h b/src/duskdolphin/display/render/renderdolphin.h index 5683389e..6117b2d8 100644 --- a/src/duskdolphin/display/render/renderdolphin.h +++ b/src/duskdolphin/display/render/renderdolphin.h @@ -9,6 +9,7 @@ #include "error/error.h" #include "display/render/ropbuffer.h" #include "display/render/rtexture.h" +#include "display/render/rtilemapchunk.h" #include "display/color.h" errorret_t renderDolphinInit(void); @@ -22,3 +23,11 @@ rtexture_t renderDolphinTextureCreate( void renderDolphinTextureDispose(rtexture_t tex); color_t *renderDolphinTextureGetPalette(rtexture_t tex); uint8_t *renderDolphinTextureGetIndices(rtexture_t tex); + +rtilemapchunk_t renderDolphinTilemapChunkCreate( + uint16_t chunkW, uint16_t chunkH, + uint16_t tileW, uint16_t tileH, + rtexture_t tileset, + const uint8_t *tileIndices +); +void renderDolphinTilemapChunkDispose(rtilemapchunk_t chunk); diff --git a/src/duskdolphin/display/render/renderplatform.h b/src/duskdolphin/display/render/renderplatform.h index 2398310b..e034c713 100644 --- a/src/duskdolphin/display/render/renderplatform.h +++ b/src/duskdolphin/display/render/renderplatform.h @@ -8,7 +8,10 @@ #pragma once #include "display/render/renderdolphin.h" -#define renderPlatformTextureCreate renderDolphinTextureCreate -#define renderPlatformTextureDispose renderDolphinTextureDispose -#define renderPlatformTextureGetPalette renderDolphinTextureGetPalette -#define renderPlatformTextureGetIndices renderDolphinTextureGetIndices +#define renderPlatformTextureCreate renderDolphinTextureCreate +#define renderPlatformTextureDispose renderDolphinTextureDispose +#define renderPlatformTextureGetPalette renderDolphinTextureGetPalette +#define renderPlatformTextureGetIndices renderDolphinTextureGetIndices + +#define renderPlatformTilemapChunkCreate renderDolphinTilemapChunkCreate +#define renderPlatformTilemapChunkDispose renderDolphinTilemapChunkDispose diff --git a/src/duskpsp/display/displaypsp.c b/src/duskpsp/display/displaypsp.c index cf470b46..6a31971c 100644 --- a/src/duskpsp/display/displaypsp.c +++ b/src/duskpsp/display/displaypsp.c @@ -9,26 +9,32 @@ #include "display/render/renderpsp.h" #include "display/display.h" #include "assert/assert.h" +#include "log/log.h" #include #include #include #include -#define FRAME_SIZE (PSP_SCREEN_W * PSP_SCREEN_H * 4) -#define VRAM_ADDR(offset) ((void *)((uintptr_t)sceGeEdramGetAddr() + (offset))) +/* GU framebuffer stride must be power-of-two; 512 is the standard for 480-wide. */ +#define PSP_BUF_W 512 +#define FRAME_SIZE (PSP_BUF_W * PSP_SCREEN_H * 4) +/* sceGuDrawBuffer/DispBuffer/DepthBuffer take VRAM-relative byte offsets, + * NOT absolute virtual addresses — do NOT add sceGeEdramGetAddr() here. */ +#define VRAM_ADDR(offset) ((void *)(uintptr_t)(offset)) static uint32_t __attribute__((aligned(64))) displayList[0x10000]; errorret_t displayPSPInit(void) { + logDebug("[PSP] displayPSPInit: start\n"); DISPLAY.whichBuffer = 0; sceGuInit(); sceGuStart(GU_DIRECT, displayList); /* Draw buffer: frame 0 at offset 0, frame 1 at FRAME_SIZE */ - sceGuDrawBuffer(GU_PSM_8888, VRAM_ADDR(0), PSP_SCREEN_W); - sceGuDispBuffer(PSP_SCREEN_W, PSP_SCREEN_H, VRAM_ADDR(FRAME_SIZE), PSP_SCREEN_W); - sceGuDepthBuffer(VRAM_ADDR(FRAME_SIZE * 2), PSP_SCREEN_W); + sceGuDrawBuffer(GU_PSM_8888, VRAM_ADDR(0), PSP_BUF_W); + sceGuDispBuffer(PSP_SCREEN_W, PSP_SCREEN_H, VRAM_ADDR(FRAME_SIZE), PSP_BUF_W); + sceGuDepthBuffer(VRAM_ADDR(FRAME_SIZE * 2), PSP_BUF_W); sceGuOffset(2048 - PSP_SCREEN_W / 2, 2048 - PSP_SCREEN_H / 2); sceGuViewport(2048, 2048, PSP_SCREEN_W, PSP_SCREEN_H); @@ -52,18 +58,23 @@ errorret_t displayPSPInit(void) { sceDisplaySetMode(0, PSP_SCREEN_W, PSP_SCREEN_H); sceGuDisplay(GU_TRUE); + logDebug("[PSP] displayPSPInit: GU setup done, calling renderPSPInit\n"); errorChain(renderPSPInit()); + logDebug("[PSP] displayPSPInit: done\n"); errorOk(); } errorret_t displayPSPFlush(ropbuffer_t *buf) { + logDebug("[PSP] displayPSPFlush: enter\n"); assertNotNull(buf, "PSP flush: null ropbuffer"); errorChain(renderPSPFlush(buf)); + logDebug("[PSP] displayPSPFlush: done\n"); errorOk(); } errorret_t displayPSPSwap(void) { + logDebug("[PSP] displayPSPSwap\n"); sceGuSwapBuffers(); errorOk(); } diff --git a/src/duskpsp/display/render/renderpsp.c b/src/duskpsp/display/render/renderpsp.c index 6e425d52..02aa5530 100644 --- a/src/duskpsp/display/render/renderpsp.c +++ b/src/duskpsp/display/render/renderpsp.c @@ -10,6 +10,7 @@ #include "display/color.h" #include "assert/assert.h" #include "util/memory.h" +#include "log/log.h" #include #include /* sceKernelDcacheWritebackRange */ #include @@ -80,6 +81,7 @@ static uint16_t texturePow2(uint16_t n) { /* ---- Init ---------------------------------------------------------------- */ errorret_t renderPSPInit(void) { + logDebug("[PSP] renderPSPInit: start\n"); /* White 1×1 fallback: index 0 → palette[0] = white */ psptexentry_t *e = &pspTexTable[0]; e->cpuIndices = (uint8_t *)memalign(16, 1); @@ -93,6 +95,7 @@ errorret_t renderPSPInit(void) { memoryZero(e->palette, 256 * sizeof(color_t)); e->palette[0] = COLOR_WHITE; e->w = 1; e->h = 1; e->tbw = 8; + logDebug("[PSP] renderPSPInit: done\n"); errorOk(); } @@ -202,6 +205,7 @@ static void draw2DSprite(const ropsprite_t *s) { } static void draw3DQuad(const ropquad3d_t *q) { + logDebug("[PSP] draw3DQuad: enter tex=%u\n", (unsigned)q->texture); uint32_t abgr = toABGR(q->tint); float u0 = q->uvX / 255.0f, v0 = q->uvY / 255.0f; float u1 = (q->uvX + q->uvW) / 255.0f; @@ -216,9 +220,12 @@ static void draw3DQuad(const ropquad3d_t *q) { float blx = cx-rx-ux, bly = cy-ry-uy, blz = cz-rz-uz; float brx = cx+rx-ux, bry = cy+ry-uy, brz = cz+rz-uz; + logDebug("[PSP] draw3DQuad: bindTexture\n"); bindTexture(q->texture); + logDebug("[PSP] draw3DQuad: getMemory\n"); GuVert3D *verts = (GuVert3D *)sceGuGetMemory(6 * sizeof(GuVert3D)); + logDebug("[PSP] draw3DQuad: verts=0x%08x\n", (unsigned)verts); assertNotNull(verts, "PSP: failed to allocate 3D quad vertices"); verts[0] = (GuVert3D){u0,v0, abgr, tlx,tly,tlz}; @@ -228,16 +235,21 @@ static void draw3DQuad(const ropquad3d_t *q) { verts[4] = (GuVert3D){u1,v1, abgr, brx,bry,brz}; verts[5] = (GuVert3D){u1,v0, abgr, trx,try_,trz}; + logDebug("[PSP] draw3DQuad: sceGuDrawArray\n"); sceGuDrawArray( GU_TRIANGLES, GU_TEXTURE_32BITF | GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D, 6, 0, verts ); + logDebug("[PSP] draw3DQuad: done\n"); } /* ---- Flush --------------------------------------------------------------- */ errorret_t renderPSPFlush(ropbuffer_t *buf) { + logDebug("[PSP] renderPSPFlush: byteCount=%u count=%u\n", + (unsigned)buf->byteCount, (unsigned)buf->count); + sceGuStart(GU_DIRECT, displayList); sceGuEnable(GU_TEXTURE_2D); @@ -245,12 +257,14 @@ errorret_t renderPSPFlush(ropbuffer_t *buf) { sceGuDepthFunc(GU_GEQUAL); /* PSP uses reversed depth */ uint32_t offset = 0; + uint32_t opIdx = 0; while(offset < buf->byteCount) { const ropheader_t *hdr = (const ropheader_t *)(buf->data + offset); ropop_t op = (ropop_t)hdr->op; switch(op) { case ROP_CLEAR: { + logDebug("[PSP] op[%u] ROP_CLEAR\n", (unsigned)opIdx); const ropclear_t *c = (const ropclear_t *)hdr; uint32_t abgr = toABGR(c->color); sceGuClearColor(abgr); @@ -259,10 +273,12 @@ errorret_t renderPSPFlush(ropbuffer_t *buf) { break; } case ROP_DRAW_SPRITE: + logDebug("[PSP] op[%u] ROP_DRAW_SPRITE\n", (unsigned)opIdx); draw2DSprite((const ropsprite_t *)hdr); break; case ROP_SET_PROJECTION: { + logDebug("[PSP] op[%u] ROP_SET_PROJECTION\n", (unsigned)opIdx); const ropprojection_t *p = (const ropprojection_t *)hdr; sceGumMatrixMode(GU_PROJECTION); sceGumLoadIdentity(); @@ -280,6 +296,7 @@ errorret_t renderPSPFlush(ropbuffer_t *buf) { break; } case ROP_SET_VIEW: { + logDebug("[PSP] op[%u] ROP_SET_VIEW\n", (unsigned)opIdx); const ropview_t *v = (const ropview_t *)hdr; ScePspFVector3 eye = {(float)v->eyeX, (float)v->eyeY, (float)v->eyeZ}; ScePspFVector3 target = {(float)v->tgtX, (float)v->tgtY, (float)v->tgtZ}; @@ -292,17 +309,23 @@ errorret_t renderPSPFlush(ropbuffer_t *buf) { break; } case ROP_DRAW_QUAD_3D: + logDebug("[PSP] op[%u] ROP_DRAW_QUAD_3D\n", (unsigned)opIdx); draw3DQuad((const ropquad3d_t *)hdr); break; default: + logDebug("[PSP] op[%u] unknown op=%u offset=%u\n", + (unsigned)opIdx, (unsigned)op, (unsigned)offset); break; } offset += ropOpSize(op); + opIdx++; } + logDebug("[PSP] renderPSPFlush: sceGuFinish\n"); sceGuFinish(); sceGuSync(0, 0); + logDebug("[PSP] renderPSPFlush: done\n"); errorOk(); } diff --git a/src/duskpsp/log/log.c b/src/duskpsp/log/log.c index 040f0be7..f6b10b54 100644 --- a/src/duskpsp/log/log.c +++ b/src/duskpsp/log/log.c @@ -11,10 +11,11 @@ void logDebug(const char_t *message, ...) { va_list args; va_start(args, message); - // print to stdout + // print to stdout — fflush so pspsh sees it immediately even on crash va_list copy; va_copy(copy, args); vprintf(message, copy); + fflush(stdout); va_end(copy); // print to file diff --git a/src/dusksat/CMakeLists.txt b/src/dusksat/CMakeLists.txt new file mode 100644 index 00000000..ea97efc2 --- /dev/null +++ b/src/dusksat/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_include_directories(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR} +) + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC +) + +add_subdirectory(asset) +add_subdirectory(display) +add_subdirectory(input) +add_subdirectory(log) +add_subdirectory(network) +add_subdirectory(save) +add_subdirectory(system) +add_subdirectory(time) diff --git a/src/dusksat/asset/CMakeLists.txt b/src/dusksat/asset/CMakeLists.txt new file mode 100644 index 00000000..31cd5f58 --- /dev/null +++ b/src/dusksat/asset/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/assetsat.c +) diff --git a/src/dusksat/asset/assetplatform.h b/src/dusksat/asset/assetplatform.h new file mode 100644 index 00000000..e8399e00 --- /dev/null +++ b/src/dusksat/asset/assetplatform.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "assetsat.h" + +typedef assetsat_t assetplatform_t; + +#define assetInitPlatform assetInitSaturn +#define assetDisposePlatform assetDisposeSaturn diff --git a/src/dusksat/asset/assetsat.c b/src/dusksat/asset/assetsat.c new file mode 100644 index 00000000..0a31ef22 --- /dev/null +++ b/src/dusksat/asset/assetsat.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "assetsat.h" +#include "log/log.h" + +errorret_t assetInitSaturn(void) { + logDebug("[Saturn] assetInitSaturn: initializing CD-Block\n"); + /* TODO: cd_block_init() */ + errorOk(); +} + +errorret_t assetDisposeSaturn(void) { + /* TODO: cd_block_deinit() */ + errorOk(); +} diff --git a/src/dusksat/asset/assetsat.h b/src/dusksat/asset/assetsat.h new file mode 100644 index 00000000..e39f9bf9 --- /dev/null +++ b/src/dusksat/asset/assetsat.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include + +typedef struct { + uint8_t unused; +} assetsat_t; + +errorret_t assetInitSaturn(void); +errorret_t assetDisposeSaturn(void); diff --git a/src/dusksat/display/CMakeLists.txt b/src/dusksat/display/CMakeLists.txt new file mode 100644 index 00000000..c0c5a8bf --- /dev/null +++ b/src/dusksat/display/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/displaysat.c +) + +add_subdirectory(render) diff --git a/src/dusksat/display/displayplatform.h b/src/dusksat/display/displayplatform.h new file mode 100644 index 00000000..320b76d2 --- /dev/null +++ b/src/dusksat/display/displayplatform.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "display/displaysat.h" + +typedef displaysat_t displayplatform_t; + +#define displayPlatformInit displaySaturnInit +#define displayPlatformFlush displaySaturnFlush +#define displayPlatformSwap displaySaturnSwap +#define displayPlatformDispose displaySaturnDispose diff --git a/src/dusksat/display/displaysat.c b/src/dusksat/display/displaysat.c new file mode 100644 index 00000000..18b8f598 --- /dev/null +++ b/src/dusksat/display/displaysat.c @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/displaysat.h" +#include "display/render/rendersat.h" +#include "display/display.h" +#include "assert/assert.h" +#include "log/log.h" +#include +#include +#include +#include +#include + +errorret_t displaySaturnInit(void) { + logDebug("[Saturn] displaySaturnInit: start\n"); + DISPLAY.whichBuffer = 0; + + /* + * TV mode: NTSC, 320×224, non-interlaced. + * Yaul's vdp2_tvmd_display_res_set() configures the sync standard and + * horizontal/vertical resolution. + * + * TODO: replace with the Yaul typed call when integrating the full SDK: + * vdp2_tvmd_display_res_set(VDP2_TVMD_INTERLACE_NONE, + * VDP2_TVMD_HORZ_NORMAL_A, + * VDP2_TVMD_VERT_224); + * vdp2_tvmd_display_set(); + */ + + /* + * VDP2 scroll planes: disable all NBG/RBG planes for now; game content is + * drawn entirely via VDP1 sprites. Tilemap chunks will re-enable NBG0 + * when the VDP2 tilemap backend is implemented. + * + * TODO: + * vdp2_scrn_display_set(VDP2_SCRN_DISP_NBG0, false); + * vdp2_scrn_display_set(VDP2_SCRN_DISP_NBG1, false); + * ... + */ + + /* + * VDP1 initialisation: the hardware starts drawing from VRAM offset 0. + * We place our command table there and texture data afterward. + * + * TODO: + * vdp1_vram_partitions_set( + * VDP1_VRAM_CYCP_..., // cycle patterns + * ... + * ); + */ + + logDebug("[Saturn] displaySaturnInit: calling renderSaturnInit\n"); + errorChain(renderSaturnInit()); + + logDebug("[Saturn] displaySaturnInit: done\n"); + errorOk(); +} + +errorret_t displaySaturnFlush(ropbuffer_t *buf) { + assertNotNull(buf, "Saturn flush: null ropbuffer"); + errorChain(renderSaturnFlush(buf)); + errorOk(); +} + +errorret_t displaySaturnSwap(void) { + logDebug("[Saturn] displaySaturnSwap\n"); + /* + * Wait for VDP1 to finish rendering the current frame then swap buffers. + * + * TODO: + * vdp1_sync_render(); + * vdp1_sync(); + * vdp2_sync(); + * vdp2_sync_wait(); + */ + DISPLAY.whichBuffer ^= 1; + errorOk(); +} + +void displaySaturnDispose(void) { + logDebug("[Saturn] displaySaturnDispose\n"); + renderSaturnDispose(); +} diff --git a/src/dusksat/display/displaysat.h b/src/dusksat/display/displaysat.h new file mode 100644 index 00000000..e2d99f1f --- /dev/null +++ b/src/dusksat/display/displaysat.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "display/displaystate.h" +#include "display/render/ropbuffer.h" + +#define SAT_SCREEN_W DUSK_DISPLAY_WIDTH +#define SAT_SCREEN_H DUSK_DISPLAY_HEIGHT + +typedef struct { + int_t whichBuffer; +} displaysat_t; + +errorret_t displaySaturnInit(void); +errorret_t displaySaturnFlush(ropbuffer_t *buf); +errorret_t displaySaturnSwap(void); +void displaySaturnDispose(void); diff --git a/src/dusksat/display/render/CMakeLists.txt b/src/dusksat/display/render/CMakeLists.txt new file mode 100644 index 00000000..91dbc43c --- /dev/null +++ b/src/dusksat/display/render/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/rendersat.c +) diff --git a/src/dusksat/display/render/renderplatform.h b/src/dusksat/display/render/renderplatform.h new file mode 100644 index 00000000..f712ef4d --- /dev/null +++ b/src/dusksat/display/render/renderplatform.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "display/render/rendersat.h" + +#define renderPlatformTextureCreate renderSaturnTextureCreate +#define renderPlatformTextureDispose renderSaturnTextureDispose +#define renderPlatformTextureGetPalette renderSaturnTextureGetPalette +#define renderPlatformTextureGetIndices renderSaturnTextureGetIndices diff --git a/src/dusksat/display/render/rendersat.c b/src/dusksat/display/render/rendersat.c new file mode 100644 index 00000000..c695b59f --- /dev/null +++ b/src/dusksat/display/render/rendersat.c @@ -0,0 +1,446 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/render/rendersat.h" +#include "display/render/rop.h" +#include "display/color.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "log/log.h" +#include +#include +#include + +/* + * VDP1 renders sprites and polygons from a command table stored in VRAM. + * VDP2 handles the scroll plane background (used for tilemap chunks). + * + * VRAM layout (VDP1, 4MB total): + * 0x000000 – 0x00FFFF : VDP1 command table (64KB = 2048 entries max) + * 0x010000 – 0x1FFFFF : texture pool (1984KB) + * Framebuffers are managed automatically by the VDP1 hardware in the + * upper half of VRAM when double-buffering is enabled. + * + * VDP2 CRAM (4KB) holds palettes: + * Each 256-color palette occupies 512 bytes (256 × 2-byte RGB1555 entries). + * We reserve one palette slot per texture handle (up to SAT_PALETTE_MAX). + */ + +/* ---- Limits -------------------------------------------------------------- */ + +#define SAT_RTEXTURE_MAX 128 +#define SAT_CMDT_MAX 1024 +#define SAT_TEXTURE_VRAM_BASE 0x010000u /* byte offset in VDP1 VRAM */ +#define SAT_TEXTURE_VRAM_SIZE (0x200000u - SAT_TEXTURE_VRAM_BASE) + +/* ---- VDP1 command table entry (hardware layout, 32 bytes) ---------------- */ + +typedef struct __attribute__((packed)) { + uint16_t ctrl; /* Command type and draw flags */ + uint16_t link; /* Link to next command (entry index) */ + uint16_t pmod; /* Draw mode: color mode, mesh, pre-clip, etc. */ + uint16_t colr; /* Palette base address in CRAM (word units /8) */ + uint16_t srca; /* Texture source address in VDP1 VRAM (/8) */ + uint16_t size; /* Texture size: ((w/8) << 8) | h */ + int16_t xa, ya; /* Vertex A (top-left for normal sprite) */ + int16_t xb, yb; /* Vertex B (bottom-right / or second vertex) */ + int16_t xc, yc; /* Vertex C (distorted sprite only) */ + int16_t xd, yd; /* Vertex D (distorted sprite only) */ + uint16_t grda; /* Gouraud shading address (unused = 0) */ + uint16_t _pad; +} satcmd_t; + +_Static_assert(sizeof(satcmd_t) == 32, "satcmd_t must be 32 bytes"); + +/* CTRL command type bits (bits 2:0) */ +#define SATCMD_CTRL_NORMAL_SPRITE (0x0000u) /* aligned rect */ +#define SATCMD_CTRL_SCALED_SPRITE (0x0001u) +#define SATCMD_CTRL_DISTORTED_SPRITE (0x0002u) /* arbitrary quad */ +#define SATCMD_CTRL_POLYGON (0x0004u) /* solid polygon */ +#define SATCMD_CTRL_SYSCLIP (0x0009u) /* system clipping */ +#define SATCMD_CTRL_END (0x8000u) /* end of list */ + +/* PMOD draw mode */ +#define SATCMD_PMOD_TRANS (0x0000u) /* transparent pixel 0 */ +#define SATCMD_PMOD_8BPP_CBANK (0x0038u) /* 256-color, color bank */ +#define SATCMD_PMOD_ECD (0x0080u) /* extend color depth */ +#define SATCMD_PMOD_SPD (0x0040u) /* do not skip index 0 */ + +/* ---- Texture table ------------------------------------------------------- */ + +typedef struct { + uint8_t *cpuIndices; /* w*h source indices */ + color_t palette[256]; + uint16_t w, h; + uint32_t vramByteOffset; /* byte offset into VDP1 VRAM pool */ + uint16_t cramWordOffset; /* word offset into VDP2 CRAM for palette */ +} sattexentry_t; + +static sattexentry_t satTexTable[SAT_RTEXTURE_MAX]; +static uint16_t satTexNext = 1; /* 0 = white fallback */ +static uint32_t satTexVramUsed = 0; +static uint16_t satTexCramUsed = 0; /* in 256-entry slots */ + +/* ---- Command table buffer ------------------------------------------------ */ + +static satcmd_t satCmdBuf[SAT_CMDT_MAX]; +static uint16_t satCmdCount; + +/* ---- Projection state ---------------------------------------------------- */ + +static float satFovY = 0.0f; /* 0 = ortho */ +static float satAspect = 1.0f; +static float satNearZ = 1.0f; +static float satFarZ = 1000.0f; + +static float satViewEyeX = 0.0f, satViewEyeY = 0.0f, satViewEyeZ = 1.0f; +static float satViewTgtX = 0.0f, satViewTgtY = 0.0f, satViewTgtZ = 0.0f; + +/* ---- Helpers ------------------------------------------------------------- */ + +/* Convert color_t RGBA → VDP2 CRAM RGB1555 (1 MSB unused, RGB555). */ +static uint16_t toRGB1555(color_t c) { + return (uint16_t)( + ((uint16_t)(c.b >> 3) << 10) | + ((uint16_t)(c.g >> 3) << 5) | + ((uint16_t)(c.r >> 3)) + ); +} + +/* Write palette into VDP2 CRAM at the texture's slot. + * CRAM is mapped at 0x25F00000 (Saturn memory map). + * Each palette slot is 512 bytes = 256 × uint16_t. */ +static void uploadPalette(sattexentry_t *e) { + volatile uint16_t *cram = (volatile uint16_t *)0x25F00000; + uint32_t base = (uint32_t)e->cramWordOffset * 256u; + for(uint32_t i = 0; i < 256; i++) { + cram[base + i] = toRGB1555(e->palette[i]); + } +} + +/* Copy indices row-by-row into VDP1 VRAM. + * VDP1 VRAM is at 0x05C00000. Textures must be stored starting on an + * 8-byte boundary; we keep our pool 8-byte aligned already. */ +static void uploadIndices(sattexentry_t *e) { + volatile uint8_t *vram = + (volatile uint8_t *)(0x05C00000u + SAT_TEXTURE_VRAM_BASE + e->vramByteOffset); + uint32_t total = (uint32_t)e->w * e->h; + for(uint32_t i = 0; i < total; i++) { + vram[i] = e->cpuIndices[i]; + } +} + +/* Return a fresh command slot or NULL if full. */ +static satcmd_t *allocCmd(void) { + if(satCmdCount >= SAT_CMDT_MAX) return NULL; + satcmd_t *c = &satCmdBuf[satCmdCount++]; + memoryZero(c, sizeof(satcmd_t)); + return c; +} + +/* ---- Init ---------------------------------------------------------------- */ + +errorret_t renderSaturnInit(void) { + logDebug("[Saturn] renderSaturnInit\n"); + memoryZero(satTexTable, sizeof(satTexTable)); + satTexNext = 1; + satTexVramUsed = 0; + satTexCramUsed = 0; + satCmdCount = 0; + + /* White 1×1 fallback: slot 0 */ + sattexentry_t *e = &satTexTable[0]; + e->cpuIndices = (uint8_t *)malloc(1); + assertNotNull(e->cpuIndices, "Saturn: failed to allocate fallback index buffer"); + e->cpuIndices[0] = 0; + memoryZero(e->palette, 256 * sizeof(color_t)); + e->palette[0] = COLOR_WHITE; + e->w = 1; e->h = 1; + e->vramByteOffset = satTexVramUsed; + e->cramWordOffset = satTexCramUsed; + satTexVramUsed += 8; /* 8-byte minimum alignment */ + satTexCramUsed++; + uploadIndices(e); + uploadPalette(e); + + errorOk(); +} + +/* ---- Texture ------------------------------------------------------------- */ + +rtexture_t renderSaturnTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +) { + assertTrue(satTexNext < SAT_RTEXTURE_MAX, "Saturn texture table full"); + + uint32_t byteCount = (uint32_t)w * h; + /* Round up to 8-byte boundary for SRCA alignment. */ + uint32_t vramBytes = (byteCount + 7u) & ~7u; + assertTrue( + satTexVramUsed + vramBytes <= SAT_TEXTURE_VRAM_SIZE, + "Saturn VDP1 texture VRAM exhausted" + ); + + rtexture_t handle = (rtexture_t)satTexNext++; + sattexentry_t *e = &satTexTable[handle]; + + e->cpuIndices = (uint8_t *)malloc(byteCount); + assertNotNull(e->cpuIndices, "Saturn: failed to allocate cpu index buffer"); + memoryCopy(e->cpuIndices, indices, byteCount); + memoryCopy(e->palette, palette, 256 * sizeof(color_t)); + e->w = w; e->h = h; + e->vramByteOffset = satTexVramUsed; + e->cramWordOffset = satTexCramUsed; + + satTexVramUsed += vramBytes; + satTexCramUsed++; + + uploadIndices(e); + uploadPalette(e); + return handle; +} + +void renderSaturnTextureDispose(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= SAT_RTEXTURE_MAX) return; + sattexentry_t *e = &satTexTable[tex]; + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } +} + +color_t *renderSaturnTextureGetPalette(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= SAT_RTEXTURE_MAX) return NULL; + return satTexTable[tex].palette; +} + +uint8_t *renderSaturnTextureGetIndices(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= SAT_RTEXTURE_MAX) return NULL; + return satTexTable[tex].cpuIndices; +} + +/* ---- Flush --------------------------------------------------------------- */ + +/* Fill in the SRCA/CMDSIZE/CMDCOLR fields from a texture handle. */ +static void cmdSetTexture(satcmd_t *cmd, rtexture_t tex) { + sattexentry_t *e = (tex < SAT_RTEXTURE_MAX && satTexTable[tex].cpuIndices) + ? &satTexTable[tex] + : &satTexTable[0]; + + /* SRCA = byte offset from VDP1 VRAM base / 8. */ + uint32_t srcByteAddr = SAT_TEXTURE_VRAM_BASE + e->vramByteOffset; + cmd->srca = (uint16_t)(srcByteAddr / 8u); + + /* SIZE = ((width/8) << 8) | height (each axis limited to 0-255 after /8). */ + cmd->size = (uint16_t)(((e->w / 8u) << 8) | e->h); + + /* COLR = CRAM word address of palette / 16 (256-color bank mode). + * Each 256-entry slot is 512 bytes = 256 words. Word offset / 16. */ + uint32_t cramWordBase = (uint32_t)e->cramWordOffset * 256u; + cmd->colr = (uint16_t)(cramWordBase / 16u); + + cmd->pmod = SATCMD_PMOD_8BPP_CBANK; /* 256-color, index 0 transparent */ +} + +static void flush2DSprite(const ropsprite_t *s) { + satcmd_t *cmd = allocCmd(); + if(!cmd) return; + + sattexentry_t *e = (s->texture < SAT_RTEXTURE_MAX && satTexTable[s->texture].cpuIndices) + ? &satTexTable[s->texture] + : &satTexTable[0]; + + cmd->ctrl = SATCMD_CTRL_NORMAL_SPRITE; + cmd->link = 0; + cmdSetTexture(cmd, s->texture); + + /* VDP1 normal sprite: XA/YA = top-left, XB/YB = size (w-1, h-1). */ + cmd->xa = (int16_t)s->x; + cmd->ya = (int16_t)s->y; + cmd->xb = (int16_t)(s->w > 0 ? s->w - 1 : 0); + cmd->yb = (int16_t)(s->h > 0 ? s->h - 1 : 0); + + /* UV sub-region: the VDP1 always draws the full texture, so to support + * sprite atlases we would need a clipped intermediate. For now we treat + * the full texture as the sprite frame (atlas sub-rect is a TODO). */ + (void)e; /* suppress unused warning for e->w/h if UV clipping is added */ +} + +/* + * Project a 3D world-space point onto the VDP1 2D screen. + * Uses a simple perspective divide; view/projection state is kept CPU-side. + */ +static void project( + float wx, float wy, float wz, + float *sx, float *sy +) { + /* Translate relative to eye. */ + float rx = wx - satViewEyeX; + float ry = wy - satViewEyeY; + float rz = wz - satViewEyeZ; + + /* Rotate view to look at target (approximated: no full matrix here). */ + /* TODO: replace with a proper view-matrix multiply for non-axis-aligned cameras. */ + float fwd_z = satViewTgtZ - satViewEyeZ; + (void)fwd_z; /* simple pass-through for now */ + + float half_w = (float)DUSK_DISPLAY_WIDTH * 0.5f; + float half_h = (float)DUSK_DISPLAY_HEIGHT * 0.5f; + + if(satFovY > 0.0f && rz != 0.0f) { + float focal = half_h / (satFovY * 0.5f); + *sx = half_w + (rx / rz) * focal; + *sy = half_h - (ry / rz) * focal; + } else { + *sx = half_w + rx; + *sy = half_h - ry; + } +} + +static void flush3DQuad(const ropquad3d_t *q) { + satcmd_t *cmd = allocCmd(); + if(!cmd) return; + + cmd->ctrl = SATCMD_CTRL_DISTORTED_SPRITE; + cmd->link = 0; + cmdSetTexture(cmd, q->texture); + + float cx = (float)q->cx, cy = (float)q->cy, cz = (float)q->cz; + float rx = (float)q->rx, ry = (float)q->ry, rz = (float)q->rz; + float ux = (float)q->ux, uy = (float)q->uy, uz = (float)q->uz; + + /* Corners: TL = center - right + up, etc. */ + float tlx = cx-rx+ux, tly = cy-ry+uy, tlz = cz-rz+uz; + float trx = cx+rx+ux, try_ = cy+ry+uy, trz = cz+rz+uz; + float blx = cx-rx-ux, bly = cy-ry-uy, blz = cz-rz-uz; + float brx = cx+rx-ux, bry = cy+ry-uy, brz = cz+rz-uz; + + float sxa, sya, sxb, syb, sxc, syc, sxd, syd; + project(tlx, tly, tlz, &sxa, &sya); + project(trx, try_, trz, &sxb, &syb); + project(brx, bry, brz, &sxc, &syc); + project(blx, bly, blz, &sxd, &syd); + + cmd->xa = (int16_t)sxa; cmd->ya = (int16_t)sya; + cmd->xb = (int16_t)sxb; cmd->yb = (int16_t)syb; + cmd->xc = (int16_t)sxc; cmd->yc = (int16_t)syc; + cmd->xd = (int16_t)sxd; cmd->yd = (int16_t)syd; +} + +/* Submit the finished command table to VDP1 VRAM and trigger rendering. */ +static void submitCmdTable(void) { + /* Append end-of-list sentinel. */ + if(satCmdCount < SAT_CMDT_MAX) { + satcmd_t *end = &satCmdBuf[satCmdCount]; + memoryZero(end, sizeof(satcmd_t)); + end->ctrl = SATCMD_CTRL_END; + } + + /* DMA or CPU-copy the command table to VDP1 VRAM at offset 0x000000. + * VDP1 VRAM starts at 0x05C00000 in the Saturn memory map. */ + volatile satcmd_t *vdp1CmdTable = (volatile satcmd_t *)0x05C00000u; + uint32_t count = satCmdCount + 1u; /* include the END entry */ + for(uint32_t i = 0; i < count; i++) { + vdp1CmdTable[i] = satCmdBuf[i]; + } + + /* Set VDP1 command table top address to 0x000000 (the default). */ + volatile uint16_t *vdp1Regs = (volatile uint16_t *)0x25D00000u; + /* VDP1 MODR (Mode Register) — ensure draw mode is correct */ + vdp1Regs[0] = 0x0000; /* PTMR: plot trigger — VDP1 draws on frame change */ + /* EWDR, EWLR, EWRR — Erase/Write window (full screen) */ + vdp1Regs[2] = 0x0000; /* EWDR: erase write data (black) */ + vdp1Regs[3] = 0x0000; /* EWLR: top-left (0,0) */ + vdp1Regs[4] = (uint16_t)(((DUSK_DISPLAY_HEIGHT - 1) << 9) | + ((DUSK_DISPLAY_WIDTH / 2) - 1)); /* EWRR */ +} + +errorret_t renderSaturnFlush(ropbuffer_t *buf) { + logDebug("[Saturn] renderSaturnFlush: count=%u\n", (unsigned)buf->count); + + satCmdCount = 0; + + uint32_t offset = 0; + while(offset < buf->byteCount) { + const ropheader_t *hdr = (const ropheader_t *)(buf->data + offset); + ropop_t op = (ropop_t)hdr->op; + + switch(op) { + case ROP_CLEAR: { + const ropclear_t *c = (const ropclear_t *)hdr; + /* Set VDP2 back-screen color. */ + volatile uint16_t *cram = (volatile uint16_t *)0x25F00000u; + cram[0] = toRGB1555(c->color); + + /* Issue a VDP1 system clipping command to reset the clip window. */ + satcmd_t *clip = allocCmd(); + if(clip) { + clip->ctrl = SATCMD_CTRL_SYSCLIP; + clip->link = 0; + clip->xa = 0; + clip->ya = 0; + clip->xb = (int16_t)(DUSK_DISPLAY_WIDTH - 1); + clip->yb = (int16_t)(DUSK_DISPLAY_HEIGHT - 1); + } + break; + } + + case ROP_DRAW_SPRITE: + flush2DSprite((const ropsprite_t *)hdr); + break; + + case ROP_SET_PROJECTION: { + const ropprojection_t *p = (const ropprojection_t *)hdr; + satFovY = fixedToFloat(p->fovY); + satAspect = fixedToFloat(p->aspect); + satNearZ = fixedToFloat(p->nearZ); + satFarZ = fixedToFloat(p->farZ); + break; + } + + case ROP_SET_VIEW: { + const ropview_t *v = (const ropview_t *)hdr; + satViewEyeX = (float)v->eyeX; + satViewEyeY = (float)v->eyeY; + satViewEyeZ = (float)v->eyeZ; + satViewTgtX = (float)v->tgtX; + satViewTgtY = (float)v->tgtY; + satViewTgtZ = (float)v->tgtZ; + break; + } + + case ROP_DRAW_QUAD_3D: + flush3DQuad((const ropquad3d_t *)hdr); + break; + + case ROP_DRAW_TILEMAP_CHUNK: + /* TODO: Saturn tilemap chunks drive VDP2 scroll plane registers. + * For now we fall through and emit nothing; a proper implementation + * writes tile indices to VDP2 VRAM and sets scroll offsets. */ + break; + + default: + logDebug("[Saturn] unknown ROP op=%u\n", (unsigned)op); + break; + } + + offset += ropOpSize(op); + } + + submitCmdTable(); + errorOk(); +} + +/* ---- Dispose ------------------------------------------------------------- */ + +void renderSaturnDispose(void) { + for(uint16_t i = 0; i < satTexNext; i++) { + sattexentry_t *e = &satTexTable[i]; + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } + } + satTexNext = 1; + satTexVramUsed = 0; + satTexCramUsed = 0; +} diff --git a/src/dusksat/display/render/rendersat.h b/src/dusksat/display/render/rendersat.h new file mode 100644 index 00000000..8ccda876 --- /dev/null +++ b/src/dusksat/display/render/rendersat.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "display/render/ropbuffer.h" +#include "display/render/rtexture.h" +#include "display/color.h" + +errorret_t renderSaturnInit(void); +errorret_t renderSaturnFlush(ropbuffer_t *buf); +void renderSaturnDispose(void); + +rtexture_t renderSaturnTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +); +void renderSaturnTextureDispose(rtexture_t tex); +color_t *renderSaturnTextureGetPalette(rtexture_t tex); +uint8_t *renderSaturnTextureGetIndices(rtexture_t tex); diff --git a/src/dusksat/duskplatform.h b/src/dusksat/duskplatform.h new file mode 100644 index 00000000..81e28b71 --- /dev/null +++ b/src/dusksat/duskplatform.h @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include diff --git a/src/dusksat/input/CMakeLists.txt b/src/dusksat/input/CMakeLists.txt new file mode 100644 index 00000000..3000bc06 --- /dev/null +++ b/src/dusksat/input/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/inputsat.c +) diff --git a/src/dusksat/input/inputplatform.h b/src/dusksat/input/inputplatform.h new file mode 100644 index 00000000..8f97b47c --- /dev/null +++ b/src/dusksat/input/inputplatform.h @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "input/inputsat.h" + +#define inputInitPlatform inputInitSaturn diff --git a/src/dusksat/input/inputsat.c b/src/dusksat/input/inputsat.c new file mode 100644 index 00000000..887d32cd --- /dev/null +++ b/src/dusksat/input/inputsat.c @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "input/input.h" +#include + +/* + * Saturn standard digital pad buttons (smpc_peripheral_digital_t). + * Yaul exposes them via smpc_peripheral_digital_port() and + * smpc_peripheral_digital_get(). + * + * Button bitmask in the SMPC peripheral data word: + * bit 11 = Right bit 10 = Left bit 9 = Down bit 8 = Up + * bit 7 = Start bit 6 = A bit 5 = C bit 4 = B + * bit 3 = R bit 2 = X bit 1 = Y bit 0 = Z + * + * We use Yaul's SMPC_PERIPHERAL_DIGITAL_* macros where available. + */ + +inputbuttondata_t INPUT_BUTTON_DATA[] = { + { .name = "a", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 6 } }, + { .name = "b", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 4 } }, + { .name = "c", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 5 } }, + { .name = "x", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 2 } }, + { .name = "y", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 1 } }, + { .name = "z", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 0 } }, + { .name = "start", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 7 } }, + { .name = "up", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 8 } }, + { .name = "down", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 9 } }, + { .name = "left", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 10 } }, + { .name = "right", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 11 } }, + { .name = "l", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 15 } }, + { .name = "r", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 3 } }, + { .name = "accept", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 6 } }, /* A */ + { .name = "cancel", { .type = INPUT_BUTTON_TYPE_GAMEPAD, .gpButton = 4 } }, /* B */ + { .name = NULL } +}; + +errorret_t inputInitSaturn(void) { + #define X(buttonName, buttonAction) \ + inputBind(inputButtonGetByName(buttonName), buttonAction); + X("up", INPUT_ACTION_UP); + X("down", INPUT_ACTION_DOWN); + X("left", INPUT_ACTION_LEFT); + X("right", INPUT_ACTION_RIGHT); + X("accept", INPUT_ACTION_ACCEPT); + X("cancel", INPUT_ACTION_CANCEL); + X("start", INPUT_ACTION_RAGEQUIT); + #undef X + + errorOk(); +} diff --git a/src/dusksat/input/inputsat.h b/src/dusksat/input/inputsat.h new file mode 100644 index 00000000..e5d44807 --- /dev/null +++ b/src/dusksat/input/inputsat.h @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +errorret_t inputInitSaturn(void); diff --git a/src/dusksat/log/CMakeLists.txt b/src/dusksat/log/CMakeLists.txt new file mode 100644 index 00000000..2650408e --- /dev/null +++ b/src/dusksat/log/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/log.c +) diff --git a/src/dusksat/log/log.c b/src/dusksat/log/log.c new file mode 100644 index 00000000..d29132af --- /dev/null +++ b/src/dusksat/log/log.c @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "log/log.h" +#include + +/* + * On Saturn, stdout goes to the debug serial port (via Yaul's dbgio module). + * With a comm link or emulator (Mednafen, SSF) this is visible on the host. + * + * TODO: add dbgio_init() in systemSaturnInit() and replace vprintf with + * dbgio_printf() for hardware-accurate serial output. + */ + +void logDebug(const char_t *message, ...) { + va_list args; + va_start(args, message); + vprintf(message, args); + va_end(args); +} + +void logError(const char_t *message, ...) { + va_list args; + va_start(args, message); + vfprintf(stderr, message, args); + va_end(args); +} diff --git a/src/dusksat/network/CMakeLists.txt b/src/dusksat/network/CMakeLists.txt new file mode 100644 index 00000000..02e38282 --- /dev/null +++ b/src/dusksat/network/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/networksat.c +) diff --git a/src/dusksat/network/networkplatform.h b/src/dusksat/network/networkplatform.h new file mode 100644 index 00000000..cbfee9f0 --- /dev/null +++ b/src/dusksat/network/networkplatform.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "networksat.h" + +#define networkPlatformInit networkSaturnInit +#define networkPlatformUpdate networkSaturnUpdate +#define networkPlatformDispose networkSaturnDispose +#define networkPlatformIsConnected networkSaturnIsConnected +#define networkPlatformRequestConnection networkSaturnRequestConnection +#define networkPlatformRequestDisconnection networkSaturnRequestDisconnection +#define networkPlatformGetInfo networkSaturnGetInfo + +typedef networksat_t networkplatform_t; diff --git a/src/dusksat/network/networksat.c b/src/dusksat/network/networksat.c new file mode 100644 index 00000000..d504c070 --- /dev/null +++ b/src/dusksat/network/networksat.c @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "network/network.h" +#include "util/memory.h" + +errorret_t networkSaturnInit(void) { errorOk(); } +errorret_t networkSaturnUpdate(void) { errorOk(); } +errorret_t networkSaturnDispose(void) { errorOk(); } + +bool_t networkSaturnIsConnected(void) { return false; } + +void networkSaturnRequestConnection( + void (*onConnected)(void *user), + void (*onFailed)(errorret_t error, void *user), + void (*onDisconnect)(errorret_t error, void *user), + void *user +) { + (void)onConnected; (void)onDisconnect; (void)user; + errorret_t err = errorThrowImpl( + NULL, ERROR_NOT_OK, + __FILE__, __func__, __LINE__, + "Network not supported on Saturn" + ); + if(onFailed) onFailed(err, user); +} + +void networkSaturnRequestDisconnection( + void (*onComplete)(void *user), + void *user +) { + if(onComplete) onComplete(user); +} + +networkinfo_t networkSaturnGetInfo(void) { + networkinfo_t info; + memoryZero(&info, sizeof(info)); + return info; +} diff --git a/src/dusksat/network/networksat.h b/src/dusksat/network/networksat.h new file mode 100644 index 00000000..be3503cf --- /dev/null +++ b/src/dusksat/network/networksat.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "network/networkinfo.h" + +/* + * Saturn networking is not supported (the NetLink modem cartridge is too + * rare to target). All functions are no-ops; network-dependent features + * will gracefully degrade via the existing NETWORK_STATE_DISCONNECTED path. + */ + +typedef struct { + uint8_t unused; +} networksat_t; + +errorret_t networkSaturnInit(void); +errorret_t networkSaturnUpdate(void); +errorret_t networkSaturnDispose(void); +bool_t networkSaturnIsConnected(void); +void networkSaturnRequestConnection( + void (*onConnected)(void *user), + void (*onFailed)(errorret_t error, void *user), + void (*onDisconnect)(errorret_t error, void *user), + void *user +); +void networkSaturnRequestDisconnection( + void (*onComplete)(void *user), + void *user +); +networkinfo_t networkSaturnGetInfo(void); diff --git a/src/dusksat/save/CMakeLists.txt b/src/dusksat/save/CMakeLists.txt new file mode 100644 index 00000000..02dea68b --- /dev/null +++ b/src/dusksat/save/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/savesat.c + ${CMAKE_CURRENT_LIST_DIR}/savestreamsat.c +) diff --git a/src/dusksat/save/saveplatform.h b/src/dusksat/save/saveplatform.h new file mode 100644 index 00000000..dc6a7fe8 --- /dev/null +++ b/src/dusksat/save/saveplatform.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "save/savesat.h" +#include "save/savestreamsat.h" + +typedef savesat_t saveplatform_t; +typedef savestreamsat_t saveplatformstream_t; + +#define saveInitPlatform saveInitSaturn +#define saveDisposePlatform saveDisposeSaturn +#define saveDeletePlatform saveDeleteSaturn + +#define saveStreamOpenReadPlatform(stream, slot) \ + saveStreamOpenReadSaturn(&(stream)->platform, &(stream)->found, slot) +#define saveStreamOpenWritePlatform(stream, slot) \ + saveStreamOpenWriteSaturn(&(stream)->platform, slot) +#define saveStreamClosePlatform(stream) \ + saveStreamCloseSaturn(&(stream)->platform) +#define saveStreamReadBytesPlatform(stream, buf, len) \ + saveStreamReadBytesSaturn(&(stream)->platform, buf, len) +#define saveStreamWriteBytesPlatform(stream, buf, len) \ + saveStreamWriteBytesSaturn(&(stream)->platform, buf, len) +#define saveStreamSeekPlatform(stream, pos) \ + saveStreamSeekSaturn(&(stream)->platform, pos) diff --git a/src/dusksat/save/savesat.c b/src/dusksat/save/savesat.c new file mode 100644 index 00000000..51dbc67b --- /dev/null +++ b/src/dusksat/save/savesat.c @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/save.h" +#include "save/savesat.h" +#include "log/log.h" + +/* + * TODO: use Yaul's bup_* API for backup RAM access. + * Reference: in the Yaul SDK. + * + * bup_init(BUP_DEV_INTERNAL); // or BUP_DEV_EXTERNAL for cart + * bup_stat_t stat; + * bup_stat(BUP_DEV_INTERNAL, &stat); + * + * Write: bup_write(BUP_DEV_INTERNAL, &dir, data, size, BUP_MODE_NEW); + * Read: bup_read(BUP_DEV_INTERNAL, filename, data, size); + * Del: bup_delete(BUP_DEV_INTERNAL, filename); + */ + +errorret_t saveInitSaturn(void) { + logDebug("[Saturn] saveInitSaturn\n"); + /* TODO: bup_init(BUP_DEV_INTERNAL); */ + errorOk(); +} + +errorret_t saveDisposeSaturn(void) { + errorOk(); +} + +errorret_t saveDeleteSaturn(const uint8_t slot) { + logDebug("[Saturn] saveDeleteSaturn: slot=%u\n", (unsigned)slot); + /* TODO: bup_delete(BUP_DEV_INTERNAL, filename_for_slot(slot)); */ + errorOk(); +} diff --git a/src/dusksat/save/savesat.h b/src/dusksat/save/savesat.h new file mode 100644 index 00000000..d016ac7f --- /dev/null +++ b/src/dusksat/save/savesat.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "save/savefile.h" + +/* + * Saturn saves use the internal backup RAM (32KB) via Yaul's bup module. + * All saves share the same cartridge/internal device; slot is encoded in + * the save file name (e.g. "DUSK00", "DUSK01", …). + */ + +#ifndef SAVE_SAT_TITLE_ID + #define SAVE_SAT_TITLE_ID "DUSK" +#endif + +typedef struct { + uint8_t unused; +} savesat_t; + +errorret_t saveInitSaturn(void); +errorret_t saveDisposeSaturn(void); +errorret_t saveDeleteSaturn(const uint8_t slot); diff --git a/src/dusksat/save/savestreamsat.c b/src/dusksat/save/savestreamsat.c new file mode 100644 index 00000000..c7622396 --- /dev/null +++ b/src/dusksat/save/savestreamsat.c @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/save.h" +#include "save/savestreamsat.h" +#include "util/memory.h" +#include +#include + +/* + * Saturn backup RAM (bup) does not support partial reads/seeks; data must + * be read or written as a single contiguous block. We buffer the entire + * save slot in heap memory and serialize to/from the bup device on open/close. + * + * Maximum save size = sizeof(savefile_t). Adjust SAVE_SAT_MAX if needed. + */ + +#define SAVE_SAT_MAX sizeof(savefile_t) + +errorret_t saveStreamOpenReadSaturn( + savestreamsat_t *p, bool_t *found, const uint8_t slot +) { + p->buf = (uint8_t *)malloc(SAVE_SAT_MAX); + if(!p->buf) errorThrow("Saturn: failed to allocate save read buffer"); + p->size = SAVE_SAT_MAX; + p->pos = 0; + p->slot = slot; + p->writing = false; + + /* + * TODO: read from bup device into p->buf: + * int32_t ret = bup_read(BUP_DEV_INTERNAL, filename, p->buf, SAVE_SAT_MAX); + * *found = (ret >= 0); + */ + *found = false; /* stub: always report no save */ + errorOk(); +} + +errorret_t saveStreamOpenWriteSaturn(savestreamsat_t *p, const uint8_t slot) { + p->buf = (uint8_t *)malloc(SAVE_SAT_MAX); + if(!p->buf) errorThrow("Saturn: failed to allocate save write buffer"); + memoryZero(p->buf, SAVE_SAT_MAX); + p->size = SAVE_SAT_MAX; + p->pos = 0; + p->slot = slot; + p->writing = true; + errorOk(); +} + +void saveStreamCloseSaturn(savestreamsat_t *p) { + if(p->writing && p->buf) { + /* + * TODO: write p->buf to bup device: + * bup_dir_t dir; + * bup_write(BUP_DEV_INTERNAL, &dir, p->buf, SAVE_SAT_MAX, BUP_MODE_NEW); + */ + } + if(p->buf) { free(p->buf); p->buf = NULL; } +} + +errorret_t saveStreamReadBytesSaturn( + savestreamsat_t *p, void *buf, const size_t len +) { + if(p->pos + len > p->size) errorThrow("Saturn: read past end of save buffer"); + memoryCopy(buf, p->buf + p->pos, len); + p->pos += len; + errorOk(); +} + +errorret_t saveStreamWriteBytesSaturn( + savestreamsat_t *p, const void *buf, const size_t len +) { + if(p->pos + len > p->size) errorThrow("Saturn: write past end of save buffer"); + memoryCopy(p->buf + p->pos, buf, len); + p->pos += len; + errorOk(); +} + +errorret_t saveStreamSeekSaturn(savestreamsat_t *p, const size_t pos) { + if(pos > p->size) errorThrow("Saturn: seek out of bounds"); + p->pos = pos; + errorOk(); +} diff --git a/src/dusksat/save/savestreamsat.h b/src/dusksat/save/savestreamsat.h new file mode 100644 index 00000000..77e617a3 --- /dev/null +++ b/src/dusksat/save/savestreamsat.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include +#include + +typedef struct { + uint8_t *buf; + size_t size; + size_t pos; + uint8_t slot; + bool_t writing; +} savestreamsat_t; + +errorret_t saveStreamOpenReadSaturn( + savestreamsat_t *p, bool_t *found, const uint8_t slot +); +errorret_t saveStreamOpenWriteSaturn(savestreamsat_t *p, const uint8_t slot); +void saveStreamCloseSaturn(savestreamsat_t *p); +errorret_t saveStreamReadBytesSaturn( + savestreamsat_t *p, void *buf, const size_t len +); +errorret_t saveStreamWriteBytesSaturn( + savestreamsat_t *p, const void *buf, const size_t len +); +errorret_t saveStreamSeekSaturn(savestreamsat_t *p, const size_t pos); diff --git a/src/dusksat/script/module/moduleplatformplatform.h b/src/dusksat/script/module/moduleplatformplatform.h new file mode 100644 index 00000000..2a2a32b3 --- /dev/null +++ b/src/dusksat/script/module/moduleplatformplatform.h @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "moduleplatformsat.h" + +#define modulePlatformPlatform modulePlatformSaturn diff --git a/src/dusksat/script/module/moduleplatformsat.h b/src/dusksat/script/module/moduleplatformsat.h new file mode 100644 index 00000000..fb43f8cf --- /dev/null +++ b/src/dusksat/script/module/moduleplatformsat.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" + +static void modulePlatformSaturn(void) { + moduleBaseEval("var SATURN = true;\n"); +} diff --git a/src/dusksat/system/CMakeLists.txt b/src/dusksat/system/CMakeLists.txt new file mode 100644 index 00000000..7bd4e557 --- /dev/null +++ b/src/dusksat/system/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/systemsat.c +) diff --git a/src/dusksat/system/systemplatform.h b/src/dusksat/system/systemplatform.h new file mode 100644 index 00000000..8e3634d2 --- /dev/null +++ b/src/dusksat/system/systemplatform.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "system/systemsat.h" + +#define systemInitPlatform systemInitSaturn +#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeSaturn diff --git a/src/dusksat/system/systemsat.c b/src/dusksat/system/systemsat.c new file mode 100644 index 00000000..329ec816 --- /dev/null +++ b/src/dusksat/system/systemsat.c @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "system/systemsat.h" +#include "log/log.h" + +errorret_t systemInitSaturn(void) { + logDebug("[Saturn] systemInitSaturn\n"); + /* + * TODO: initialize SMPC peripheral scanning so input reads work. + * smpc_peripheral_init(); + * smpc_peripheral_intback_issue(); + */ + errorOk(); +} + +systemdialogtype_t systemGetActiveDialogTypeSaturn(void) { + return SYSTEM_DIALOG_TYPE_NONE; +} diff --git a/src/dusksat/system/systemsat.h b/src/dusksat/system/systemsat.h new file mode 100644 index 00000000..2ff3bfbd --- /dev/null +++ b/src/dusksat/system/systemsat.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "system/system.h" + +errorret_t systemInitSaturn(void); +systemdialogtype_t systemGetActiveDialogTypeSaturn(void); diff --git a/src/dusksat/time/CMakeLists.txt b/src/dusksat/time/CMakeLists.txt new file mode 100644 index 00000000..f7ea99df --- /dev/null +++ b/src/dusksat/time/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_BINARY_TARGET_NAME} + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/timesat.c +) diff --git a/src/dusksat/time/timeplatform.h b/src/dusksat/time/timeplatform.h new file mode 100644 index 00000000..a933a44b --- /dev/null +++ b/src/dusksat/time/timeplatform.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "time/timesat.h" + +#define timeTickPlatform timeTickSaturn +#define timeGetDeltaPlatform timeGetDeltaSaturn +#define timeGetRealPlatform timeGetRealSaturn +#define timeGetRealTimeZonePlatform timeGetRealTimeZoneSaturn diff --git a/src/dusksat/time/timesat.c b/src/dusksat/time/timesat.c new file mode 100644 index 00000000..8bc486da --- /dev/null +++ b/src/dusksat/time/timesat.c @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "time/timesat.h" +#include + +#define SAT_FPS 60.0 + +static double_t satTimeLast = 0.0; +static double_t satTimeDelta = 0.0; +static double_t satTimeAcc = 0.0; /* accumulated seconds (frame counter) */ + +void timeTickSaturn(void) { + double_t now = satTimeAcc + (1.0 / SAT_FPS); + satTimeDelta = now - satTimeAcc; + satTimeLast = satTimeAcc; + satTimeAcc = now; +} + +double_t timeGetDeltaSaturn(void) { + return satTimeDelta; +} + +double_t timeGetRealSaturn(void) { + /* + * TODO: read the SMPC RTC for actual wall-clock time: + * smpc_rtc_t rtc; + * smpc_smc_rtc_read(&rtc); + * return rtcToUnixSeconds(&rtc); + */ + return satTimeAcc; +} + +double_t timeGetRealTimeZoneSaturn(void) { + /* Saturn RTC stores local time; timezone offset is not available. */ + return 0.0; +} diff --git a/src/dusksat/time/timesat.h b/src/dusksat/time/timesat.h new file mode 100644 index 00000000..a98480c8 --- /dev/null +++ b/src/dusksat/time/timesat.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/* + * Time is tracked via a frame counter (60fps assumed for NTSC). + * The SMPC RTC provides calendar time via smpc_rtc_t. + */ + +void timeTickSaturn(void); +double_t timeGetDeltaSaturn(void); +double_t timeGetRealSaturn(void); +double_t timeGetRealTimeZoneSaturn(void);