diff --git a/cmake/toolchains/saturn.cmake b/cmake/toolchains/saturn.cmake index a041dbeb..e268a2cf 100644 --- a/cmake/toolchains/saturn.cmake +++ b/cmake/toolchains/saturn.cmake @@ -52,8 +52,9 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 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") +# Yaul installs yaul.specs to ${YAUL_INSTALL_ROOT}/sh2eb-elf/lib/ and +# ldscripts/yaul.x to ${YAUL_INSTALL_ROOT}/sh2eb-elf/lib/ldscripts/. +# GCC searches ${prefix}/${target}/lib/ for specs, so -specs=yaul.specs works +# without an absolute path once the SDK is installed. set(CMAKE_EXE_LINKER_FLAGS_INIT - "-T\"${_YAUL_LD}\" -Wl,--start-group -Wl,--end-group -nostartfiles") + "-specs=yaul.specs -Wl,--gc-sections -nostartfiles -Wl,--start-group -Wl,--end-group") diff --git a/docker/saturn/Dockerfile b/docker/saturn/Dockerfile index 542f4da0..63f67937 100644 --- a/docker/saturn/Dockerfile +++ b/docker/saturn/Dockerfile @@ -18,7 +18,6 @@ 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 @@ -51,11 +50,9 @@ RUN mkdir -p "${YAUL_INSTALL_ROOT}" 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" + rm "binutils-${BINUTILS_VER}.tar.xz" "gcc-${GCC_VER}.tar.xz" # Download GCC prerequisites (gmp, mpfr, mpc if not packaged) RUN cd /tmp/gcc-${GCC_VER} && contrib/download_prerequisites @@ -98,35 +95,10 @@ RUN mkdir -p /tmp/build-sh-gcc1 && cd /tmp/build-sh-gcc1 && \ && 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 +# Newlib does not recognise the sh2eb CPU name, and Yaul ships its own C +# runtime in libyaul/libc/ anyway. Stage 1 (compiler + libgcc) is all +# we need; Yaul's specs file overrides *startfile:/*endfile:/*lib: to empty +# so nothing from a host C library is linked in. # --------------------------------------------------------------------------- # 7. m68k-elf binutils (Saturn 68EC000 sound CPU) @@ -159,7 +131,7 @@ RUN mkdir -p /tmp/build-m68k-gcc && cd /tmp/build-m68k-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} +RUN rm -rf /tmp/binutils-${BINUTILS_VER} /tmp/gcc-${GCC_VER} # --------------------------------------------------------------------------- # 9. Create m68keb-elf symlinks @@ -189,25 +161,11 @@ RUN git clone --depth 1 --recurse-submodules \ 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 +# 11. Cross-compile zlib for sh2eb-elf via CMake +# Using CMake (not ./configure) so we can set +# CMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY and skip link tests that +# the bare-metal cross-compiler can't satisfy. +# Install into ${YAUL_INSTALL_ROOT}/sh2eb-elf/ (Yaul sysroot layout). # --------------------------------------------------------------------------- RUN printf '%s\n' \ 'set(CMAKE_SYSTEM_NAME Generic)' \ @@ -221,6 +179,20 @@ RUN printf '%s\n' \ 'set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)' \ > /tmp/sat-xc.cmake +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 && \ + cmake -S /tmp/zlib-1.3.1 -B /tmp/zlib-build \ + -DCMAKE_TOOLCHAIN_FILE=/tmp/sat-xc.cmake \ + -DCMAKE_INSTALL_PREFIX="${YAUL_INSTALL_ROOT}/sh2eb-elf" \ + -DCMAKE_C_FLAGS="-m2 -mb -fno-builtin -O2" \ + -DBUILD_SHARED_LIBS=OFF \ + && cmake --build /tmp/zlib-build -- -j"$(nproc)" \ + && cmake --install /tmp/zlib-build \ + && rm -rf /tmp/zlib-1.3.1 /tmp/zlib.tar.gz /tmp/zlib-build + +# --------------------------------------------------------------------------- +# 12. Cross-compile libzip for sh2eb-elf (reuses /tmp/sat-xc.cmake above) +# --------------------------------------------------------------------------- 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 \ @@ -242,7 +214,7 @@ RUN wget -q https://libzip.org/download/libzip-1.10.1.tar.gz -O /tmp/libzip.tar. -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 + && rm -rf /tmp/libzip-1.10.1 /tmp/libzip.tar.gz /tmp/libzip-build /tmp/sat-xc.cmake /tmp/zlib.tar.gz 2>/dev/null || true WORKDIR /workdir VOLUME ["/workdir"] diff --git a/src/dusk/display/render/render.c b/src/dusk/display/render/render.c index a84bb8a7..d64dfbb2 100644 --- a/src/dusk/display/render/render.c +++ b/src/dusk/display/render/render.c @@ -71,33 +71,33 @@ rtexture_t renderTextureCreate( uint16_t w, uint16_t h, const uint8_t *indices, const color_t *palette ) { -#ifdef renderPlatformTextureCreate - return renderPlatformTextureCreate(w, h, indices, palette); -#else - return RTEXTURE_NONE; -#endif + #ifdef renderPlatformTextureCreate + return renderPlatformTextureCreate(w, h, indices, palette); + #else + return RTEXTURE_NONE; + #endif } void renderTextureDispose(rtexture_t tex) { -#ifdef renderPlatformTextureDispose - renderPlatformTextureDispose(tex); -#endif + #ifdef renderPlatformTextureDispose + renderPlatformTextureDispose(tex); + #endif } color_t *renderTextureGetPalette(rtexture_t tex) { -#ifdef renderPlatformTextureGetPalette - return renderPlatformTextureGetPalette(tex); -#else - return NULL; -#endif + #ifdef renderPlatformTextureGetPalette + return renderPlatformTextureGetPalette(tex); + #else + return NULL; + #endif } uint8_t *renderTextureGetIndices(rtexture_t tex) { -#ifdef renderPlatformTextureGetIndices - return renderPlatformTextureGetIndices(tex); -#else - return NULL; -#endif + #ifdef renderPlatformTextureGetIndices + return renderPlatformTextureGetIndices(tex); + #else + return NULL; + #endif } rtilemapchunk_t renderTilemapChunkCreate( @@ -106,19 +106,19 @@ rtilemapchunk_t renderTilemapChunkCreate( rtexture_t tileset, const uint8_t *tileIndices ) { -#ifdef renderPlatformTilemapChunkCreate - return renderPlatformTilemapChunkCreate( - chunkW, chunkH, tileW, tileH, tileset, tileIndices - ); -#else - return RTILEMAPCHUNK_INVALID; -#endif + #ifdef renderPlatformTilemapChunkCreate + return renderPlatformTilemapChunkCreate( + chunkW, chunkH, tileW, tileH, tileset, tileIndices + ); + #else + return RTILEMAPCHUNK_INVALID; + #endif } void renderTilemapChunkDispose(rtilemapchunk_t chunk) { -#ifdef renderPlatformTilemapChunkDispose - renderPlatformTilemapChunkDispose(chunk); -#endif + #ifdef renderPlatformTilemapChunkDispose + renderPlatformTilemapChunkDispose(chunk); + #endif } void renderTilemapChunk( diff --git a/src/dusk/display/screen.h b/src/dusk/display/screen.h new file mode 100644 index 00000000..5145920b --- /dev/null +++ b/src/dusk/display/screen.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 "dusk.h" + +#define SCREEN_WIDTH ((int32_t)DUSK_DISPLAY_WIDTH) +#define SCREEN_HEIGHT ((int32_t)DUSK_DISPLAY_HEIGHT) diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index c136d3ce..daf6045d 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -35,7 +35,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(sceneInit()); consolePrint("Engine initialized"); - sceneSet(SCENE_TYPE_TEST); + sceneSet(SCENE_TYPE_WHITE32); errorOk(); } diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt index 219a010f..30f7198c 100644 --- a/src/dusk/scene/CMakeLists.txt +++ b/src/dusk/scene/CMakeLists.txt @@ -11,4 +11,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} # Subdirs add_subdirectory(overworld) -add_subdirectory(test) \ No newline at end of file +add_subdirectory(rainbownothing) +add_subdirectory(white32) \ No newline at end of file diff --git a/src/dusk/scene/rainbownothing/CMakeLists.txt b/src/dusk/scene/rainbownothing/CMakeLists.txt new file mode 100644 index 00000000..aec33acb --- /dev/null +++ b/src/dusk/scene/rainbownothing/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_LIBRARY_TARGET_NAME} + PUBLIC + scenerainbownothing.c +) diff --git a/src/dusk/scene/rainbownothing/scenerainbownothing.c b/src/dusk/scene/rainbownothing/scenerainbownothing.c new file mode 100644 index 00000000..33ca2e20 --- /dev/null +++ b/src/dusk/scene/rainbownothing/scenerainbownothing.c @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scene/rainbownothing/scenerainbownothing.h" +#include "display/render/render.h" +#include "display/color.h" + +#define RAINBOW_PERIOD 60 +#define RAINBOW_COUNT 6 + +static const color_t RAINBOW_COLORS[RAINBOW_COUNT] = { + { 255, 0, 0, 255 }, /* red */ + { 255, 128, 0, 255 }, /* orange */ + { 255, 255, 0, 255 }, /* yellow */ + { 0, 255, 0, 255 }, /* green */ + { 0, 0, 255, 255 }, /* blue */ + { 148, 0, 211, 255 }, /* violet */ +}; + +static int32_t sceneRainbowNothingFrame; + +errorret_t sceneRainbowNothingInit(scenedata_t *data) { + sceneRainbowNothingFrame = 0; + errorOk(); +} + +errorret_t sceneRainbowNothingUpdate(scenedata_t *data) { + sceneRainbowNothingFrame++; + errorOk(); +} + +errorret_t sceneRainbowNothingRender(scenedata_t *data) { + int32_t step = (sceneRainbowNothingFrame / RAINBOW_PERIOD) % RAINBOW_COUNT; + renderClear(RAINBOW_COLORS[step]); + errorOk(); +} + +errorret_t sceneRainbowNothingDispose(scenedata_t *data) { + errorOk(); +} diff --git a/src/dusk/scene/rainbownothing/scenerainbownothing.h b/src/dusk/scene/rainbownothing/scenerainbownothing.h new file mode 100644 index 00000000..a114c875 --- /dev/null +++ b/src/dusk/scene/rainbownothing/scenerainbownothing.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "scene/scenebase.h" + +typedef struct { + int32_t _unused; +} scenerainbownothing_t; + +errorret_t sceneRainbowNothingInit(scenedata_t *data); +errorret_t sceneRainbowNothingUpdate(scenedata_t *data); +errorret_t sceneRainbowNothingRender(scenedata_t *data); +errorret_t sceneRainbowNothingDispose(scenedata_t *data); diff --git a/src/dusk/scene/scenetype.c b/src/dusk/scene/scenetype.c index 6115934d..056ba698 100644 --- a/src/dusk/scene/scenetype.c +++ b/src/dusk/scene/scenetype.c @@ -17,10 +17,17 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = { .dispose = sceneOverworldDispose }, - [SCENE_TYPE_TEST] = { - .init = sceneTestInit, - .update = sceneTestUpdate, - .render = sceneTestRender, - .dispose = sceneTestDispose + [SCENE_TYPE_RAINBOWNOTHING] = { + .init = sceneRainbowNothingInit, + .update = sceneRainbowNothingUpdate, + .render = sceneRainbowNothingRender, + .dispose = sceneRainbowNothingDispose + }, + + [SCENE_TYPE_WHITE32] = { + .init = sceneWhite32Init, + .update = sceneWhite32Update, + .render = sceneWhite32Render, + .dispose = sceneWhite32Dispose }, }; diff --git a/src/dusk/scene/scenetype.h b/src/dusk/scene/scenetype.h index 90ef70d5..d0b352d0 100644 --- a/src/dusk/scene/scenetype.h +++ b/src/dusk/scene/scenetype.h @@ -8,11 +8,13 @@ #pragma once #include "scene/scenebase.h" #include "scene/overworld/sceneoverworld.h" -#include "scene/test/scenetest.h" +#include "scene/rainbownothing/scenerainbownothing.h" +#include "scene/white32/scenewhite32.h" typedef union scenedata_u { - sceneoverworld_t overworld; - scenetest_t test; + sceneoverworld_t overworld; + scenerainbownothing_t rainbownothing; + scenewhite32_t white32; } scenedata_t; typedef errorret_t (*scenecallback_t)(scenedata_t *); @@ -27,7 +29,8 @@ typedef struct { typedef enum { SCENE_TYPE_NULL, SCENE_TYPE_OVERWORLD, - SCENE_TYPE_TEST, + SCENE_TYPE_RAINBOWNOTHING, + SCENE_TYPE_WHITE32, SCENE_TYPE_COUNT } scenetype_t; diff --git a/src/dusk/scene/test/scenetest.c b/src/dusk/scene/test/scenetest.c index c075ac63..b3d7511d 100644 --- a/src/dusk/scene/test/scenetest.c +++ b/src/dusk/scene/test/scenetest.c @@ -6,211 +6,31 @@ */ #include "scene/test/scenetest.h" -#include "assert/assert.h" #include "display/render/render.h" #include "display/color.h" -#include "time/time.h" -#include "util/fixed.h" -#include "util/memory.h" +#include "engine/engine.h" -/* Initial data for the 3×3 test texture. After create, we write directly - * to the texture's CPU buffers via renderTextureGet* — these statics - * are only referenced in sceneTestInit. */ -static const uint8_t initIndices[3 * 3] = { - 0, 1, 2, - 3, 4, 5, - 6, 7, 8, -}; +#define SCENE_TEST_FRAMES 200 -static const color_t initPalette[256] = { - [0] = { 255, 0, 0, 255 }, /* red */ - [1] = { 0, 255, 0, 255 }, /* green */ - [2] = { 0, 0, 255, 255 }, /* blue */ - [3] = { 255, 255, 0, 255 }, /* yellow */ - [4] = { 255, 0, 255, 255 }, /* magenta */ - [5] = { 0, 255, 255, 255 }, /* cyan */ - [6] = { 255, 165, 0, 255 }, /* orange */ - [7] = { 128, 0, 255, 255 }, /* purple */ - [8] = { 255, 255, 255, 255 }, /* white */ -}; - -/* Rainbow hue cycle using 120°-offset cosines. */ -static color_t hueColor(float t) { - uint8_t r = (uint8_t)((cosf(t) * 0.5f + 0.5f) * 255.0f); - uint8_t g = (uint8_t)((cosf(t - 2.094f) * 0.5f + 0.5f) * 255.0f); - uint8_t b = (uint8_t)((cosf(t - 4.189f) * 0.5f + 0.5f) * 255.0f); - return color(r, g, b, 255); -} - -static rtexture_t testTex; - -/* ---- aspect ratio for 3D tests (matches linux default 640×480) ---------- */ -#define ASPECT FIXED(640.0f / 480.0f) -/* fovY ≈ 60° in radians */ -#define FOV_Y FIXED(1.0472f) - -/* ---- Tilemap test -------------------------------------------------------- */ -/* Two 16×16 tiles packed side-by-side in a 32×16 tileset texture. - * The chunk is 64px wider than the 640px window; the scroll range is exactly - * 4 tile widths = 2 checkerboard periods, so the wrap is seamless. */ -#define TILEMAP_TILE_W 16 -#define TILEMAP_TILE_H 16 -#define TILEMAP_CHUNK_W 44 /* tiles; 44*16=704px */ -#define TILEMAP_CHUNK_H 13 /* tiles; 13*16=208px */ -#define TILEMAP_Y 262 /* screen-space y: just below 2D tests */ -#define TILEMAP_SCROLL_SPEED 40.0f /* px / second */ -#define TILEMAP_SCROLL_RANGE 64.0f /* px; = 4 tile widths = 2 chk periods */ - -static rtexture_t tilemapTileset; -static rtilemapchunk_t tilemapChunk; - -/* ========================================================================= - * Test 1 — single 2D textured quad - * top-left quadrant (x: 50–240, y: 30–190) - * ======================================================================= */ -static void renderTest2DQuad(void) { - renderSprite(50, 30, 192, 160, 0, testTex, COLOR_WHITE); -} - -/* ========================================================================= - * Test 2 — three 2D quads with Z-ordering - * top-right quadrant (x: 340–620, y: 30–200) - * Depth: 0 = frontmost, 32767 = backmost. - * Back quad (depth 24000) tinted blue-grey, drawn first visually behind. - * Mid quad (depth 12000) tinted orange. - * Front quad (depth 0) full texture, drawn on top. - * ======================================================================= */ -static void renderTest2DZOrder(void) { - renderSprite(340, 30, 160, 160, 24000, testTex, color(100, 100, 220, 255)); - renderSprite(380, 60, 160, 160, 12000, testTex, color(220, 140, 60, 255)); - renderSprite(420, 90, 160, 160, 0, testTex, COLOR_WHITE); -} - -/* ========================================================================= - * Test 3 — single 3D textured quad spinning around Y axis - * World position: left side, x=-180 cm. - * ======================================================================= */ -static void renderTest3DQuad(void) { - float angle = fixedToFloat(TIME.time); - renderQuad3D( - -180, 0, 0, - (int16_t)(cosf(angle) * 60.0f), 0, (int16_t)(sinf(angle) * 60.0f), - 0, 60, 0, - 0, testTex, COLOR_WHITE - ); -} - -/* ========================================================================= - * Test 4 — two 3D quads overlapping (depth test) - * Right side of world space. - * Quad A (behind, z=-60 cm): blue tint. - * Quad B (in front, z=60 cm): white/texture. - * Both quads share screen-space centre so depth buffer decides ordering. - * ======================================================================= */ -static void renderTest3DOverlap(void) { - /* behind */ - renderQuad3D( - 150, 0, -60, - 60, 0, 0, - 0, 60, 0, - 0, testTex, color(100, 100, 220, 255) - ); - /* in front */ - renderQuad3D( - 190, 0, 60, - 60, 0, 0, - 0, 60, 0, - 0, testTex, COLOR_WHITE - ); -} - -/* ========================================================================= - * Test 5 — scrolling tilemap (bottom of screen) - * Checkerboard of two hue-cycling tiles. - * x offset scrolls left by TILEMAP_SCROLL_SPEED px/s, wraps seamlessly. - * ======================================================================= */ -static void renderTestTilemap(void) { - float t = fixedToFloat(TIME.time); - float scrollPx = fmodf(t * TILEMAP_SCROLL_SPEED, TILEMAP_SCROLL_RANGE); - int16_t x = -(int16_t)(int)scrollPx; - renderTilemapChunk(x, TILEMAP_Y, 8000, tilemapChunk); -} - -/* ---- Lifecycle ----------------------------------------------------------- */ +static int32_t sceneTestFrame; errorret_t sceneTestInit(scenedata_t *data) { - testTex = renderTextureCreate(3, 3, initIndices, initPalette); - assertTrue(testTex != RTEXTURE_NONE, "Failed to create test texture"); - - /* Tileset: 32×16, two 16×16 tiles — left half = index 0, right = index 1 */ - uint8_t tsIdx[32 * 16]; - color_t tsPal[256]; - memoryZero(tsPal, sizeof(tsPal)); - for(int y = 0; y < 16; y++) - for(int x = 0; x < 32; x++) - tsIdx[y * 32 + x] = (x < 16) ? 0 : 1; - tsPal[0] = color(210, 180, 100, 255); /* warm sand — overridden each frame */ - tsPal[1] = color( 60, 130, 60, 255); /* cool grass — overridden each frame */ - tilemapTileset = renderTextureCreate(32, 16, tsIdx, tsPal); - assertTrue(tilemapTileset != RTEXTURE_NONE, "Failed to create tilemap tileset"); - - /* Chunk: checkerboard — (col+row)%2 selects tile 0 or tile 1 */ - uint8_t chunkIdx[TILEMAP_CHUNK_W * TILEMAP_CHUNK_H]; - for(int row = 0; row < TILEMAP_CHUNK_H; row++) - for(int col = 0; col < TILEMAP_CHUNK_W; col++) - chunkIdx[row * TILEMAP_CHUNK_W + col] = (uint8_t)((col + row) % 2); - tilemapChunk = renderTilemapChunkCreate( - TILEMAP_CHUNK_W, TILEMAP_CHUNK_H, - TILEMAP_TILE_W, TILEMAP_TILE_H, - tilemapTileset, - chunkIdx - ); - /* RTILEMAPCHUNK_INVALID is valid on platforms that don't implement chunk - * creation yet; renderTilemapChunk() and renderTilemapChunkDispose() both - * handle it as a no-op. */ - + sceneTestFrame = 0; errorOk(); } errorret_t sceneTestUpdate(scenedata_t *data) { - float t = fixedToFloat(TIME.time); - - /* Palette demo: entry 8 (bottom-right pixel) cycles through hue */ - renderTextureGetPalette(testTex)[8] = hueColor(t * 2.0f); - - /* Index demo: center pixel (1,1) steps through entries 0-8 once per second */ - renderTextureGetIndices(testTex)[1 * 3 + 1] = (uint8_t)((uint32_t)t % 9u); - - /* Tilemap palette animation: complementary hues so tiles always contrast */ - color_t *tilePal = renderTextureGetPalette(tilemapTileset); - tilePal[0] = hueColor(t * 0.7f); - tilePal[1] = hueColor(t * 0.7f + 3.14159f); - + sceneTestFrame++; + if(sceneTestFrame >= SCENE_TEST_FRAMES) engineExit(); errorOk(); } errorret_t sceneTestRender(scenedata_t *data) { - renderClear(color(24, 24, 40, 255)); - - /* 2D tests — no camera needed */ - renderTest2DQuad(); - renderTest2DZOrder(); - - /* 3D tests — shared camera looking slightly down from above */ - renderSetProjection(FOV_Y, ASPECT, FIXED(10), FIXED(10000)); - renderSetView(0, 150, 400, 0, 0, 0); - renderTest3DQuad(); - renderTest3DOverlap(); - - /* Tilemap test — full-width scrolling strip below the 2D tests */ - renderTestTilemap(); - + renderClear(color(0, 128, 255, 255)); + renderSprite(0, 0, 32, 32, 0, RTEXTURE_NONE, COLOR_WHITE); errorOk(); } errorret_t sceneTestDispose(scenedata_t *data) { - renderTilemapChunkDispose(tilemapChunk); - renderTextureDispose(tilemapTileset); - renderTextureDispose(testTex); errorOk(); } diff --git a/src/dusk/scene/test/scenetest.h b/src/dusk/scene/test/scenetest.h index b0f177f8..84ea0be9 100644 --- a/src/dusk/scene/test/scenetest.h +++ b/src/dusk/scene/test/scenetest.h @@ -9,7 +9,7 @@ #include "scene/scenebase.h" typedef struct { - int32_t unused; + int32_t frame; } scenetest_t; errorret_t sceneTestInit(scenedata_t *data); diff --git a/src/dusk/scene/white32/CMakeLists.txt b/src/dusk/scene/white32/CMakeLists.txt new file mode 100644 index 00000000..f0fb3903 --- /dev/null +++ b/src/dusk/scene/white32/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_LIBRARY_TARGET_NAME} + PUBLIC + scenewhite32.c +) diff --git a/src/dusk/scene/white32/scenewhite32.c b/src/dusk/scene/white32/scenewhite32.c new file mode 100644 index 00000000..b1ec592e --- /dev/null +++ b/src/dusk/scene/white32/scenewhite32.c @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scene/white32/scenewhite32.h" +#include "display/render/render.h" +#include "display/color.h" +#include "engine/engine.h" + +errorret_t sceneWhite32Init(scenedata_t *data) { + errorOk(); +} + +errorret_t sceneWhite32Update(scenedata_t *data) { + errorOk(); +} + +errorret_t sceneWhite32Render(scenedata_t *data) { + renderClear(COLOR_CORNFLOWER_BLUE); + renderSprite(0, 0, 32, 32, 0, RTEXTURE_NONE, COLOR_WHITE); + errorOk(); +} + +errorret_t sceneWhite32Dispose(scenedata_t *data) { + errorOk(); +} diff --git a/src/dusk/scene/white32/scenewhite32.h b/src/dusk/scene/white32/scenewhite32.h new file mode 100644 index 00000000..e75e59ed --- /dev/null +++ b/src/dusk/scene/white32/scenewhite32.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "scene/scenebase.h" + +typedef struct { + void *nothing; +} scenewhite32_t; + +errorret_t sceneWhite32Init(scenedata_t *data); +errorret_t sceneWhite32Update(scenedata_t *data); +errorret_t sceneWhite32Render(scenedata_t *data); +errorret_t sceneWhite32Dispose(scenedata_t *data); diff --git a/src/duskpsp/display/displaypsp.c b/src/duskpsp/display/displaypsp.c index 6a31971c..70fd8c2d 100644 --- a/src/duskpsp/display/displaypsp.c +++ b/src/duskpsp/display/displaypsp.c @@ -9,11 +9,11 @@ #include "display/render/renderpsp.h" #include "display/display.h" #include "assert/assert.h" -#include "log/log.h" #include #include #include #include +#include /* GU framebuffer stride must be power-of-two; 512 is the standard for 480-wide. */ #define PSP_BUF_W 512 @@ -25,7 +25,6 @@ static uint32_t __attribute__((aligned(64))) displayList[0x10000]; errorret_t displayPSPInit(void) { - logDebug("[PSP] displayPSPInit: start\n"); DISPLAY.whichBuffer = 0; sceGuInit(); @@ -58,23 +57,19 @@ 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"); + sceDisplayWaitVblankStart(); sceGuSwapBuffers(); errorOk(); } @@ -83,4 +78,5 @@ void displayPSPDispose(void) { renderPSPDispose(); sceGuDisplay(GU_FALSE); sceGuTerm(); + sceKernelExitGame(); } diff --git a/src/duskpsp/display/render/renderpsp.c b/src/duskpsp/display/render/renderpsp.c index 02aa5530..4fe72055 100644 --- a/src/duskpsp/display/render/renderpsp.c +++ b/src/duskpsp/display/render/renderpsp.c @@ -10,7 +10,6 @@ #include "display/color.h" #include "assert/assert.h" #include "util/memory.h" -#include "log/log.h" #include #include /* sceKernelDcacheWritebackRange */ #include @@ -29,15 +28,16 @@ static uint32_t __attribute__((aligned(64))) displayList[DISPLAY_LIST_SIZE / 4]; /* GU T8 (8-bit indexed) textures. * cpuIndices : w*h row-major bytes, user-writable source of truth. - * gpuIndices : tbw*h padded for sceGuTexImage, re-derived at bind. + * gpuIndices : tbw*tph padded for sceGuTexImage, re-derived at bind. * palette : 256 RGBA entries, user-writable source of truth. * Converted to ABGR on-the-fly at bind into pspAbgrBuf. - * tbw : power-of-two stride ≥ 8 required by the GU. */ + * tbw : power-of-two stride ≥ 16 required by T8 (16-byte cache line). + * tph : power-of-two height required by sceGuTexImage (≥ 1). */ typedef struct { uint8_t *cpuIndices; uint8_t *gpuIndices; color_t palette[256]; - uint16_t w, h, tbw; + uint16_t w, h, tbw, tph; } psptexentry_t; /* Shared 16-byte-aligned ABGR buffer used during every CLUT load. */ @@ -48,11 +48,13 @@ static uint16_t pspTexNext = 1; /* 0 = white fallback */ /* ---- Vertex types -------------------------------------------------------- */ -/* 2D sprite: two corner vertices define the rect (GU_SPRITES uses 2 verts). */ +/* 2D sprite: two corner vertices define the rect (GU_SPRITES uses 2 verts). + * Must match GU_TEXTURE_32BITF|GU_COLOR_8888|GU_VERTEX_32BITF — the PSP GU + * reference samples always use 32-bit float coords for textured 2D sprites. */ typedef struct { - uint16_t u, v; /* texel coords (integer, not normalised) */ + float u, v; /* texel coords (float, in texels) */ uint32_t color; /* ABGR */ - int16_t x, y, z; + float x, y, z; } __attribute__((packed)) GuVert2D; /* 3D triangle vertex */ @@ -71,8 +73,15 @@ static uint32_t toABGR(color_t c) { ((uint32_t)c.r); } -/* Smallest power-of-two ≥ n and ≥ 8 (PSP GU minimum stride for T8). */ +/* Smallest power-of-two ≥ n and ≥ 16 (T8 cache line is 16 bytes = 16 pixels). */ static uint16_t texturePow2(uint16_t n) { + uint16_t p = 16; + while(p < n) p = (uint16_t)(p << 1); + return p; +} + +/* Smallest power-of-two ≥ n and ≥ 8 (PSP minimum texture dimension > 4). */ +static uint16_t texturePow2Height(uint16_t n) { uint16_t p = 8; while(p < n) p = (uint16_t)(p << 1); return p; @@ -81,21 +90,20 @@ 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 */ + /* White 8×8 fallback: all pixels = index 0 → palette[0] = white. + * PSP requires texture width/height > 4; tbw >= 16 for T8. */ psptexentry_t *e = &pspTexTable[0]; - e->cpuIndices = (uint8_t *)memalign(16, 1); + e->cpuIndices = (uint8_t *)memalign(16, 8 * 8); assertNotNull(e->cpuIndices, "PSP: failed to allocate fallback cpu index buffer"); - e->cpuIndices[0] = 0; + memoryZero(e->cpuIndices, 8 * 8); - e->gpuIndices = (uint8_t *)memalign(16, 8); /* tbw=8 minimum */ + e->gpuIndices = (uint8_t *)memalign(16, 16 * 8); /* tbw=16, tph=8 */ assertNotNull(e->gpuIndices, "PSP: failed to allocate fallback gpu index buffer"); - memoryZero(e->gpuIndices, 8); + memoryZero(e->gpuIndices, 16 * 8); 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"); + e->w = 8; e->h = 8; e->tbw = 16; e->tph = 8; errorOk(); } @@ -108,6 +116,7 @@ rtexture_t renderPSPTextureCreate( assertTrue(pspTexNext < PSP_RTEXTURE_MAX, "PSP texture table full"); uint16_t tbw = texturePow2(w); + uint16_t tph = texturePow2Height(h); rtexture_t handle = (rtexture_t)pspTexNext++; psptexentry_t *e = &pspTexTable[handle]; @@ -116,7 +125,8 @@ rtexture_t renderPSPTextureCreate( assertNotNull(e->cpuIndices, "PSP: failed to allocate cpu index buffer"); memoryCopy(e->cpuIndices, indices, cpuBytes); - uint32_t gpuBytes = (uint32_t)tbw * h; + /* GPU buffer must cover tbw*tph rows — sceGuTexImage requires power-of-2 height. */ + uint32_t gpuBytes = (uint32_t)tbw * tph; e->gpuIndices = (uint8_t *)memalign(16, gpuBytes); assertNotNull(e->gpuIndices, "PSP: failed to allocate gpu index buffer"); memoryZero(e->gpuIndices, gpuBytes); @@ -124,7 +134,7 @@ rtexture_t renderPSPTextureCreate( memoryCopy(e->gpuIndices + row * tbw, indices + row * w, w); memoryCopy(e->palette, palette, 256 * sizeof(color_t)); - e->w = w; e->h = h; e->tbw = tbw; + e->w = w; e->h = h; e->tbw = tbw; e->tph = tph; return handle; } @@ -150,22 +160,26 @@ static void bindTexture(rtexture_t tex) { ? &pspTexTable[tex] : &pspTexTable[0]; - /* Re-pad cpuIndices → gpuIndices (stride tbw, rows w wide). */ - memoryZero(e->gpuIndices, (uint32_t)e->tbw * e->h); - for(uint16_t row = 0; row < e->h; row++) - memoryCopy(e->gpuIndices + row * e->tbw, e->cpuIndices + row * e->w, e->w); - sceKernelDcacheWritebackRange(e->gpuIndices, (uint32_t)e->tbw * e->h); + /* Write texture indices and palette via uncached alias so data lands in + * physical RAM immediately — the GE DMA reads physical RAM, not CPU cache, + * so writing through the uncached alias (|0x40000000) avoids stale reads. */ + uint8_t *gpuUC = (uint8_t *)((uint32_t)e->gpuIndices | 0x40000000u); + uint32_t *clutUC = (uint32_t *)((uint32_t)pspAbgrBuf | 0x40000000u); - /* Convert palette color_t RGBA → ABGR into the shared aligned buffer. */ - for(int i = 0; i < 256; i++) pspAbgrBuf[i] = toABGR(e->palette[i]); - sceKernelDcacheWritebackRange(pspAbgrBuf, 256 * sizeof(uint32_t)); + memoryZero(gpuUC, (uint32_t)e->tbw * e->tph); + for(uint16_t row = 0; row < e->h; row++) + memoryCopy(gpuUC + row * e->tbw, e->cpuIndices + row * e->w, e->w); + + for(int i = 0; i < 256; i++) clutUC[i] = toABGR(e->palette[i]); sceGuTexMode(GU_PSM_T8, 0, 0, GU_FALSE); + sceGuTexImage(0, texturePow2Height(e->w), texturePow2Height(e->h), e->tbw, e->gpuIndices); sceGuClutMode(GU_PSM_8888, 0, 0xFF, 0); - sceGuClutLoad(32, pspAbgrBuf); /* 32 × 8 entries = 256 */ - sceGuTexImage(0, e->tbw, e->h, e->tbw, e->gpuIndices); + sceGuClutLoad(32, pspAbgrBuf); + sceGuTexFlush(); sceGuTexFunc(GU_TFX_MODULATE, GU_TCC_RGBA); sceGuTexFilter(GU_NEAREST, GU_NEAREST); + sceGuTexWrap(GU_CLAMP, GU_CLAMP); sceGuTexScale(1.0f, 1.0f); sceGuTexOffset(0.0f, 0.0f); } @@ -183,33 +197,38 @@ static void draw2DSprite(const ropsprite_t *s) { float v1 = ((s->uvY + s->uvH) / 255.0f) * (float)e->h; bindTexture(s->texture); + sceGuAmbientColor(abgr); GuVert2D *verts = (GuVert2D *)sceGuGetMemory(2 * sizeof(GuVert2D)); assertNotNull(verts, "PSP: failed to allocate sprite vertices"); - verts[0].u = (uint16_t)u0; verts[0].v = (uint16_t)v0; + verts[0].u = u0; verts[0].v = v0; verts[0].color = abgr; - verts[0].x = s->x; verts[0].y = s->y; verts[0].z = 0; + verts[0].x = (float)s->x; verts[0].y = (float)s->y; verts[0].z = 0.0f; - verts[1].u = (uint16_t)u1; verts[1].v = (uint16_t)v1; + verts[1].u = u1; verts[1].v = v1; verts[1].color = abgr; - verts[1].x = (int16_t)(s->x + s->w); - verts[1].y = (int16_t)(s->y + s->h); - verts[1].z = 0; + verts[1].x = (float)(s->x + s->w); + verts[1].y = (float)(s->y + s->h); + verts[1].z = 0.0f; sceGuDrawArray( GU_SPRITES, - GU_TEXTURE_16BIT | GU_COLOR_8888 | GU_VERTEX_16BIT | GU_TRANSFORM_2D, + GU_TEXTURE_32BITF | GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_2D, 2, 0, verts ); } static void draw3DQuad(const ropquad3d_t *q) { - logDebug("[PSP] draw3DQuad: enter tex=%u\n", (unsigned)q->texture); + psptexentry_t *e = (q->texture < PSP_RTEXTURE_MAX && pspTexTable[q->texture].cpuIndices) + ? &pspTexTable[q->texture] + : &pspTexTable[0]; 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; - float v1 = (q->uvY + q->uvH) / 255.0f; + /* UV in 0-255 range → scale to texel coords the same way draw2DSprite does. */ + float u0 = (q->uvX / 255.0f) * (float)e->w; + float v0 = (q->uvY / 255.0f) * (float)e->h; + float u1 = ((q->uvX + q->uvW) / 255.0f) * (float)e->w; + float v1 = ((q->uvY + q->uvH) / 255.0f) * (float)e->h; 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; @@ -220,12 +239,9 @@ 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}; @@ -235,21 +251,16 @@ 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); @@ -257,14 +268,12 @@ 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); @@ -273,12 +282,10 @@ 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(); @@ -296,7 +303,6 @@ 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}; @@ -309,23 +315,17 @@ 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(); }