diff --git a/.claude/display.md b/.claude/display.md index 31816404..f6e40529 100644 --- a/.claude/display.md +++ b/.claude/display.md @@ -8,6 +8,143 @@ For the planned render-queue refactor (required for Saturn), see [display-refact --- +## Render / ROP system (`display/render/`) + +The ROP (Render OPcode) system is the low-level, backend-agnostic drawing API. All game drawing goes through this layer; backends (`rendergl.c`, `renderpsp.c`, `renderdolphin.c`) execute the commands at display flush time. + +### API (`display/render/render.h`) + +```c +/* Clear the framebuffer */ +void renderClear(color_t color); + +/* 2D textured quad at pixel coordinates */ +void renderSprite( + int16_t x, int16_t y, int16_t w, int16_t h, + int16_t depth, /* 0=front … 32767=back */ + rtexture_t texture, color_t tint +); + +/* Set perspective projection for subsequent 3D draws */ +void renderSetProjection( + fixed_t fovY, fixed_t aspect, fixed_t nearZ, fixed_t farZ +); + +/* Set camera position/target for subsequent 3D draws */ +void renderSetView( + int16_t eyeX, int16_t eyeY, int16_t eyeZ, + int16_t tgtX, int16_t tgtY, int16_t tgtZ +); + +/* World-space quad: center point + right half-extent + up half-extent */ +void renderQuad3D( + int16_t cx, int16_t cy, int16_t cz, + int16_t rx, int16_t ry, int16_t rz, + int16_t ux, int16_t uy, int16_t uz, + int16_t depth, + rtexture_t texture, color_t tint +); + +/* Create / dispose an 8-bit indexed palette texture */ +rtexture_t renderTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, /* w×h pixel indices (0-255) */ + const color_t *palette /* 256 RGBA colour entries */ +); +void renderTextureDispose(rtexture_t tex); + +/* Mutable pointers to the texture's CPU-side data. + * Write directly to these; the next draw call picks up the changes. + * GL: dirty flag set on getter call; glTexSubImage2D at next bind. + * PSP: re-pads indices and converts palette → ABGR at bind time. + * Dolphin: re-tiles CI8 and converts palette → RGB5A3 at bind time. */ +color_t *renderTextureGetPalette(rtexture_t tex); /* color_t[256] */ +uint8_t *renderTextureGetIndices(rtexture_t tex); /* uint8_t[w*h] */ +``` + +### Coordinate conventions + +| Domain | Type | Scale | Notes | +|---|---|---|---| +| 3D world positions | `int16_t` | 1 unit = 1 cm | Matches PS1 GTE / N64 RSP native format | +| Camera/projection params | `fixed_t` | Q24.8 | `FIXED(x)` for literals | +| 2D screen positions | `int16_t` | pixels | Origin top-left | +| UV coords | `uint8_t` | 0–255 → 0.0–1.0 | Stored in ROP structs | + +### Palettized textures + +All textures are 8-bit indexed. `renderTextureCreate` takes: +- `indices`: one `uint8_t` per pixel (0–255), row-major +- `palette`: exactly **256** `color_t` RGBA entries + +**Per-platform storage:** + +| Platform | CPU source of truth | GPU/native format | When derived | +|---|---|---|---| +| GL (Linux/Vita) | `color_t palette[256]` + `uint8_t *cpuIndices` in slot | `GL_R8` index tex + `GL_RGBA` 256×1 palette tex | Lazy: dirty flag set by getter, `glTexSubImage2D` at next bind | +| PSP | `color_t palette[256]` + unpadded `uint8_t *cpuIndices` | Stride-padded indices (POT ≥ 8) + ABGR8888 CLUT in shared `pspAbgrBuf` | Every `bindTexture` call; dcache-flushed before GU reads | +| Dolphin/GC/Wii | `color_t palette[256]` + unpadded `uint8_t *cpuIndices` | CI8 tiled (8×4 tiles, 32 B/tile) + RGB5A3 TLUT in `tlutData` | Every `bindTexture` call; `DCFlushRange` before GX load | + +**GL palette shader detail**: The fragment shader samples the R8 index texture, converts the normalised float back to an exact texel centre with `raw*(255/256) + 0.5/256`, then looks up the 256×1 palette texture. This gives pixel-exact results for all 256 index values and allows independent real-time updates to indices or palette. + +**Dolphin RGB5A3 encoding**: +- Opaque (`a == 255`): bit 15 = 1, RGB555 +- Transparent: bit 15 = 0, A3RGB4 (alpha quantised to 3 bits — dithered transparency is planned for a future pass) + +### ROP buffer (`display/render/ropbuffer.h` / `rop.h`) + +Commands are written into `ROPBUFFER` (a static byte array) then replayed by the backend at flush time. All ops are fixed-size aligned structs: + +| Op | Struct | Size | +|---|---|---| +| `ROP_CLEAR` | `ropclear_t` | 32 bytes | +| `ROP_DRAW_SPRITE` | `ropsprite_t` | 32 bytes | +| `ROP_SET_PROJECTION` | `ropprojection_t` | 32 bytes | +| `ROP_SET_VIEW` | `ropview_t` | 32 bytes | +| `ROP_DRAW_QUAD_3D` | `ropquad3d_t` | 64 bytes | +| `ROP_DRAW_TILEMAP_CHUNK` | `roptilemapc_t` | 32 bytes | + +`ropOpSize(op)` returns the byte size for any op. Backends iterate with `offset += ropOpSize(op)`. + +### Texture handles (`display/render/rtexture.h`) + +`rtexture_t` is a `uint16_t` index into the platform's texture table. `RTEXTURE_NONE` (0 or a sentinel) means "white fallback". Tables are platform-static; handles are valid until `renderTextureDispose` is called. + +### Tilemap chunk handles (`display/render/rtilemapchunk.h`) + +`rtilemapchunk_t` is a `uint16_t` index into the platform's chunk table. `RTILEMAPCHUNK_INVALID` (0) means no-op. Chunks are pre-built at map load time; each backend constructs its native draw structure once (VAO+VBO on GL, display list on PSP/GX/N64) and the ROP entry costs only a handle lookup + single native draw call per frame. + +```c +/* Build once at map load */ +rtilemapchunk_t chunk = renderTilemapChunkCreate( + chunkW, chunkH, /* size in tiles */ + tileW, tileH, /* pixels per tile */ + tileset, /* rtexture_t of the packed tileset */ + tileIndices /* uint8_t[chunkW*chunkH], row-major tile indices */ +); + +/* Each frame for visible chunks */ +renderTilemapChunk(screenX, screenY, depth, chunk); + +/* At map unload */ +renderTilemapChunkDispose(chunk); +``` + +Animated tiles should be drawn on top as separate `renderSprite()` calls; the chunk itself is treated as static geometry and never rebuilt at runtime. + +**Per-platform build:** + +| Platform | What's built at create time | Draw cost per frame | +|---|---|---| +| GL (Linux/Vita) | VAO + VBO (`GL_STATIC_DRAW`), `uOffset` uniform translates to screen pos | 1 `glDrawArrays` | +| PSP | GU display list in uncached EDRAM | 1 `sceGuCallList` | +| GC/Wii | Compiled GX display list | 1 `GX_CallDispList` | +| PS1 | Pre-linked POLY_FT4/SPRT chain | Linked into OT at one slot | +| N64 | RDP display list with pre-scheduled `LOAD_TILE` batches (TMEM-aware) | 1 `gSPDisplayList` | +| Saturn | VDP2 plane config + VRAM tilemap data | Scroll register write only | + +--- + ## Initialization order Within the display system, init must follow this order (enforced in `engine.c`): diff --git a/.claude/style.md b/.claude/style.md index 9f918a67..2329d5ee 100644 --- a/.claude/style.md +++ b/.claude/style.md @@ -227,6 +227,27 @@ errorret_t spriteBatchFlush(void); --- +## Unused parameters + +Do **not** use `(void)param;` casts to suppress unused-parameter warnings. They are +redundant noise. If a callback signature is fixed by a function-pointer type and the +parameter is genuinely unused, just leave it — do not suppress: + +```c +// correct — parameter unused, no cast +errorret_t sceneTestUpdate(scenedata_t *data) { + errorOk(); +} + +// wrong +errorret_t sceneTestUpdate(scenedata_t *data) { + (void)data; + errorOk(); +} +``` + +--- + ## Global subsystem state Each subsystem exposes a single global instance declared `extern` in the header and defined (once) in the `.c` file: diff --git a/cmake/targets/psp.cmake b/cmake/targets/psp.cmake index 242791d0..b56b5304 100644 --- a/cmake/targets/psp.cmake +++ b/cmake/targets/psp.cmake @@ -9,12 +9,10 @@ set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_C OFF CACHE BOOL "" FORCE) find_package(SDL2 REQUIRED) -find_package(OpenGL REQUIRED) target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC ${SDL2_LIBRARIES} SDL2 pthread - OpenGL::GL zip bz2 z @@ -22,12 +20,13 @@ target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC mbedcrypto lzma m - + pspdebug pspdisplay pspge pspctrl pspgu + pspgum pspaudio pspaudiolib psputility @@ -47,11 +46,9 @@ target_include_directories(${DUSK_BINARY_TARGET_NAME} PRIVATE target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC DUSK_SDL2 - DUSK_OPENGL DUSK_PSP DUSK_INPUT_GAMEPAD DUSK_PLATFORM_ENDIAN_LITTLE - DUSK_OPENGL_LEGACY DUSK_DISPLAY_WIDTH=480 DUSK_DISPLAY_HEIGHT=272 DUSK_THREAD_PTHREAD diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 093f4719..a327f93d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,7 +13,6 @@ if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli") elseif(DUSK_TARGET_SYSTEM STREQUAL "psp") add_subdirectory(duskpsp) add_subdirectory(dusksdl2) - add_subdirectory(duskgl) elseif(DUSK_TARGET_SYSTEM STREQUAL "vita") add_subdirectory(duskvita) diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index c4635e08..d99f093c 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -58,7 +58,6 @@ add_subdirectory(assert) add_subdirectory(asset) add_subdirectory(console) add_subdirectory(display) -add_subdirectory(render) add_subdirectory(log) add_subdirectory(engine) add_subdirectory(error) diff --git a/src/dusk/display/CMakeLists.txt b/src/dusk/display/CMakeLists.txt index 2ca73681..9779d059 100644 --- a/src/dusk/display/CMakeLists.txt +++ b/src/dusk/display/CMakeLists.txt @@ -15,3 +15,4 @@ dusk_run_python( --output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h ) add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_color_defs) +add_subdirectory(render) diff --git a/src/dusk/display/display.c b/src/dusk/display/display.c index add2ded9..f8b19b61 100644 --- a/src/dusk/display/display.c +++ b/src/dusk/display/display.c @@ -6,7 +6,7 @@ */ #include "display/display.h" -#include "render/ropbuffer.h" +#include "display/render/ropbuffer.h" #include "scene/scene.h" #include "util/memory.h" diff --git a/src/dusk/render/CMakeLists.txt b/src/dusk/display/render/CMakeLists.txt similarity index 100% rename from src/dusk/render/CMakeLists.txt rename to src/dusk/display/render/CMakeLists.txt diff --git a/src/dusk/display/render/render.c b/src/dusk/display/render/render.c new file mode 100644 index 00000000..a84bb8a7 --- /dev/null +++ b/src/dusk/display/render/render.c @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/render/render.h" +#include "display/render/renderplatform.h" + +void renderClear(color_t color) { + ropclear_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_CLEAR); + cmd->color = color; +} + +void renderSprite( + int16_t x, int16_t y, int16_t w, int16_t h, + int16_t depth, + rtexture_t texture, color_t tint +) { + ropsprite_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_DRAW_SPRITE); + cmd->header.depth = depth; + cmd->x = x; + cmd->y = y; + cmd->w = w; + cmd->h = h; + cmd->uvX = 0; + cmd->uvY = 0; + cmd->uvW = 255; + cmd->uvH = 255; + cmd->tint = tint; + cmd->texture = texture; +} + +void renderSetProjection(fixed_t fovY, fixed_t aspect, fixed_t nearZ, fixed_t farZ) { + ropprojection_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_SET_PROJECTION); + cmd->fovY = fovY; + cmd->aspect = aspect; + cmd->nearZ = nearZ; + cmd->farZ = farZ; +} + +void renderSetView( + int16_t eyeX, int16_t eyeY, int16_t eyeZ, + int16_t tgtX, int16_t tgtY, int16_t tgtZ +) { + ropview_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_SET_VIEW); + cmd->eyeX = eyeX; cmd->eyeY = eyeY; cmd->eyeZ = eyeZ; + cmd->tgtX = tgtX; cmd->tgtY = tgtY; cmd->tgtZ = tgtZ; +} + +void renderQuad3D( + int16_t cx, int16_t cy, int16_t cz, + int16_t rx, int16_t ry, int16_t rz, + int16_t ux, int16_t uy, int16_t uz, + int16_t depth, + rtexture_t texture, color_t tint +) { + ropquad3d_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_DRAW_QUAD_3D); + cmd->header.depth = depth; + cmd->cx = cx; cmd->cy = cy; cmd->cz = cz; + cmd->rx = rx; cmd->ry = ry; cmd->rz = rz; + cmd->ux = ux; cmd->uy = uy; cmd->uz = uz; + cmd->uvX = 0; cmd->uvY = 0; + cmd->uvW = 255; cmd->uvH = 255; + cmd->tint = tint; + cmd->texture = texture; +} + +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 +} + +void renderTextureDispose(rtexture_t tex) { +#ifdef renderPlatformTextureDispose + renderPlatformTextureDispose(tex); +#endif +} + +color_t *renderTextureGetPalette(rtexture_t tex) { +#ifdef renderPlatformTextureGetPalette + return renderPlatformTextureGetPalette(tex); +#else + return NULL; +#endif +} + +uint8_t *renderTextureGetIndices(rtexture_t tex) { +#ifdef renderPlatformTextureGetIndices + return renderPlatformTextureGetIndices(tex); +#else + return NULL; +#endif +} + +rtilemapchunk_t renderTilemapChunkCreate( + uint16_t chunkW, uint16_t chunkH, + uint16_t tileW, uint16_t tileH, + rtexture_t tileset, + const uint8_t *tileIndices +) { +#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 +} + +void renderTilemapChunk( + int16_t x, int16_t y, + int16_t depth, + rtilemapchunk_t chunk +) { + roptilemapc_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_DRAW_TILEMAP_CHUNK); + cmd->header.depth = depth; + cmd->x = x; + cmd->y = y; + cmd->chunk = chunk; +} diff --git a/src/dusk/display/render/render.h b/src/dusk/display/render/render.h new file mode 100644 index 00000000..686f66b1 --- /dev/null +++ b/src/dusk/display/render/render.h @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "display/render/rop.h" +#include "display/render/ropbuffer.h" +#include "display/render/rtexture.h" +#include "display/render/rtilemapchunk.h" + +/* ---- 2D ------------------------------------------------------------------ */ + +void renderClear(color_t color); + +void renderSprite( + int16_t x, int16_t y, int16_t w, int16_t h, + int16_t depth, + rtexture_t texture, color_t tint +); + +/* ---- 3D ------------------------------------------------------------------ */ + +void renderSetProjection( + fixed_t fovY, fixed_t aspect, fixed_t nearZ, fixed_t farZ +); + +void renderSetView( + int16_t eyeX, int16_t eyeY, int16_t eyeZ, + int16_t tgtX, int16_t tgtY, int16_t tgtZ +); + +/* World-space quad: center + right half-extent + up half-extent. */ +void renderQuad3D( + int16_t cx, int16_t cy, int16_t cz, + int16_t rx, int16_t ry, int16_t rz, + int16_t ux, int16_t uy, int16_t uz, + int16_t depth, + rtexture_t texture, color_t tint +); + +/* ---- Textures ------------------------------------------------------------ */ + +/* 8-bit indexed palette texture. palette must contain exactly 256 entries. */ +rtexture_t renderTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +); +void renderTextureDispose(rtexture_t tex); + +/* Mutable access to the CPU-side palette and index arrays. + * Write to these pointers directly; the next draw call picks up the changes. + * GL re-uploads to the GPU texture lazily at bind (dirty flag); PSP/Dolphin + * re-convert their native format (ABGR/CI8) at bind time from these arrays. */ +color_t *renderTextureGetPalette(rtexture_t tex); /* color_t[256] */ +uint8_t *renderTextureGetIndices(rtexture_t tex); /* uint8_t[w*h], row-major */ + +/* ---- Tilemap chunks ------------------------------------------------------- */ + +/* Build a pre-baked chunk from a tile-index array. The backend constructs + * its native draw structure (VAO+VBO on GL, display list on PSP/GX/N64) once + * at create time; subsequent renderTilemapChunk() calls cost only one ROP + * entry and one native draw call per chunk. + * + * chunkW/chunkH — chunk size in tiles + * tileW/tileH — pixel dimensions of one tile in the tileset texture + * tileset — palettized texture containing all tiles packed in a grid + * tileIndices — chunkW*chunkH uint8_t values; each is a tile index into the + * tileset grid (row-major, tile 0 = top-left of tileset) */ +rtilemapchunk_t renderTilemapChunkCreate( + uint16_t chunkW, uint16_t chunkH, + uint16_t tileW, uint16_t tileH, + rtexture_t tileset, + const uint8_t *tileIndices +); +void renderTilemapChunkDispose(rtilemapchunk_t chunk); + +/* Emit a ROP_DRAW_TILEMAP_CHUNK entry. x/y are the screen-space pixel position + * of the chunk's top-left corner. */ +void renderTilemapChunk( + int16_t x, int16_t y, + int16_t depth, + rtilemapchunk_t chunk +); diff --git a/src/dusk/display/render/rop.h b/src/dusk/display/render/rop.h new file mode 100644 index 00000000..0c4b44cf --- /dev/null +++ b/src/dusk/display/render/rop.h @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "display/color.h" +#include "display/render/rtexture.h" +#include "display/render/rtilemapchunk.h" +#include "util/fixed.h" + +/* Fixed size for all 2D/state opcodes. 3D quads use ROP_SIZE_3D. */ +#define ROP_SIZE 32 +#define ROP_SIZE_3D 64 + +typedef enum { + ROP_NOP = 0, + ROP_CLEAR, + ROP_DRAW_SPRITE, /* 2D screen-space quad ROP_SIZE */ + ROP_SET_PROJECTION, /* perspective / ortho params ROP_SIZE */ + ROP_SET_VIEW, /* camera eye + target ROP_SIZE */ + ROP_DRAW_QUAD_3D, /* world-space textured quad ROP_SIZE_3D */ + ROP_DRAW_TILEMAP_CHUNK, /* pre-built tile chunk handle ROP_SIZE */ + ROP_COUNT +} ropop_t; + +#define ROP_FLAG_BLEND ((uint8_t)(1 << 0)) + +/* Returns the byte size of a given opcode. */ +static inline uint32_t ropOpSize(ropop_t op) { + if(op == ROP_DRAW_QUAD_3D) return ROP_SIZE_3D; + return ROP_SIZE; +} + +/* 4-byte header that starts every opcode. */ +typedef struct { + uint8_t op; + uint8_t flags; + int16_t depth; +} ropheader_t; + +_Static_assert(sizeof(ropheader_t) == 4, "ropheader_t must be 4 bytes"); + +/* ROP_CLEAR — 32 bytes */ +typedef struct { + ropheader_t header; /* 4 */ + color_t color; /* 4 */ + uint8_t pad[24];/* 24 */ +} ropclear_t; + +_Static_assert(sizeof(ropclear_t) == ROP_SIZE, "ropclear_t must be ROP_SIZE bytes"); + +/* ROP_DRAW_SPRITE — 32 bytes, screen-space pixel coordinates */ +typedef struct { + ropheader_t header; /* 4 */ + int16_t x, y; /* 4 */ + int16_t w, h; /* 4 */ + uint8_t uvX, uvY; /* 2 */ + uint8_t uvW, uvH; /* 2 */ + color_t tint; /* 4 */ + uint16_t texture; /* 2 handle, 0 = white */ + uint16_t palette; /* 2 */ + uint8_t pad[8]; /* 8 */ +} ropsprite_t; + +_Static_assert(sizeof(ropsprite_t) == ROP_SIZE, "ropsprite_t must be ROP_SIZE bytes"); + +/* ROP_SET_PROJECTION — 32 bytes. fovY==0 means orthographic. */ +typedef struct { + ropheader_t header; /* 4 */ + fixed_t fovY; /* 4 radians; 0 = ortho */ + fixed_t aspect; /* 4 w/h */ + fixed_t nearZ; /* 4 */ + fixed_t farZ; /* 4 */ + uint8_t pad[12]; /* 12 */ +} ropprojection_t; + +_Static_assert(sizeof(ropprojection_t) == ROP_SIZE, "ropprojection_t must be ROP_SIZE bytes"); + +/* ROP_SET_VIEW — 32 bytes. Camera eye position and look-at target. */ +typedef struct { + ropheader_t header; /* 4 */ + int16_t eyeX, eyeY, eyeZ; /* 6 */ + int16_t tgtX, tgtY, tgtZ; /* 6 */ + uint8_t pad[16]; /* 16 */ +} ropview_t; + +_Static_assert(sizeof(ropview_t) == ROP_SIZE, "ropview_t must be ROP_SIZE bytes"); + +/* ROP_DRAW_QUAD_3D — 64 bytes. + * Quad defined by center + two half-extent vectors. + * Corners: center ± right ± up + */ +typedef struct { + ropheader_t header; /* 4 */ + int16_t cx, cy, cz; /* 6 center */ + int16_t rx, ry, rz; /* 6 right half-ext */ + int16_t ux, uy, uz; /* 6 up half-ext */ + uint8_t uvX, uvY; /* 2 */ + uint8_t uvW, uvH; /* 2 */ + color_t tint; /* 4 */ + uint16_t texture; /* 2 handle, 0=white */ + uint8_t pad[32]; /* 32 */ +} ropquad3d_t; + +_Static_assert(sizeof(ropquad3d_t) == ROP_SIZE_3D, "ropquad3d_t must be ROP_SIZE_3D bytes"); + +/* ROP_DRAW_TILEMAP_CHUNK — 32 bytes. + * References a pre-built backend chunk by handle; x/y are the screen-space + * pixel offset applied at draw time. */ +typedef struct { + ropheader_t header; /* 4 */ + int16_t x, y; /* 4 screen-space pixel offset */ + rtilemapchunk_t chunk; /* 2 handle, RTILEMAPCHUNK_INVALID = no-op */ + uint8_t pad[22]; /* 22 */ +} roptilemapc_t; + +_Static_assert(sizeof(roptilemapc_t) == ROP_SIZE, "roptilemapc_t must be ROP_SIZE bytes"); diff --git a/src/dusk/render/ropbuffer.c b/src/dusk/display/render/ropbuffer.c similarity index 55% rename from src/dusk/render/ropbuffer.c rename to src/dusk/display/render/ropbuffer.c index 77b00939..16bcbe7b 100644 --- a/src/dusk/render/ropbuffer.c +++ b/src/dusk/display/render/ropbuffer.c @@ -5,21 +5,27 @@ * https://opensource.org/licenses/MIT */ -#include "render/ropbuffer.h" +#include "display/render/ropbuffer.h" #include "util/memory.h" #include "assert/assert.h" ropbuffer_t ROPBUFFER; void ropBufferReset(ropbuffer_t *buf) { - buf->count = 0; + buf->byteCount = 0; + buf->count = 0; } void *ropBufferAlloc(ropbuffer_t *buf, ropop_t op) { - assertTrue(buf->count < ROPBUFFER_MAX_COMMANDS, "ROP buffer is full"); - uint8_t *ptr = buf->data + (buf->count * ROP_SIZE); - memoryZero(ptr, ROP_SIZE); + uint32_t size = ropOpSize(op); + assertTrue( + buf->byteCount + size <= ROPBUFFER_BYTE_SIZE, + "ROP buffer is full" + ); + uint8_t *ptr = buf->data + buf->byteCount; + memoryZero(ptr, size); ((ropheader_t *)ptr)->op = (uint8_t)op; + buf->byteCount += size; buf->count++; return ptr; } diff --git a/src/dusk/render/ropbuffer.h b/src/dusk/display/render/ropbuffer.h similarity index 51% rename from src/dusk/render/ropbuffer.h rename to src/dusk/display/render/ropbuffer.h index 1dfcdc4c..fcdd30c0 100644 --- a/src/dusk/render/ropbuffer.h +++ b/src/dusk/display/render/ropbuffer.h @@ -6,13 +6,15 @@ */ #pragma once -#include "render/rop.h" +#include "display/render/rop.h" -#define ROPBUFFER_MAX_COMMANDS 4096 +/* 256 KB pool — enough for ~8192 2D sprites or ~4096 3D quads mixed. */ +#define ROPBUFFER_BYTE_SIZE (256 * 1024) typedef struct { - uint8_t data[ROPBUFFER_MAX_COMMANDS * ROP_SIZE]; - uint32_t count; + uint8_t data[ROPBUFFER_BYTE_SIZE]; + uint32_t byteCount; /* bytes consumed this frame */ + uint32_t count; /* number of commands */ } ropbuffer_t; extern ropbuffer_t ROPBUFFER; diff --git a/src/dusk/display/render/rtexture.h b/src/dusk/display/render/rtexture.h new file mode 100644 index 00000000..cc67aca4 --- /dev/null +++ b/src/dusk/display/render/rtexture.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 + +typedef uint16_t rtexture_t; + +#define RTEXTURE_NONE ((rtexture_t)0) diff --git a/src/dusk/display/render/rtilemapchunk.h b/src/dusk/display/render/rtilemapchunk.h new file mode 100644 index 00000000..8a919eb7 --- /dev/null +++ b/src/dusk/display/render/rtilemapchunk.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 + +typedef uint16_t rtilemapchunk_t; + +#define RTILEMAPCHUNK_INVALID ((rtilemapchunk_t)0) diff --git a/src/dusk/render/render.c b/src/dusk/render/render.c deleted file mode 100644 index 0c45ac97..00000000 --- a/src/dusk/render/render.c +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "render/render.h" - -void renderClear(color_t color) { - ropclear_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_CLEAR); - cmd->color = color; -} - -void renderSprite( - int16_t x, int16_t y, - int16_t w, int16_t h, - color_t tint -) { - ropsprite_t *cmd = ropBufferAlloc(&ROPBUFFER, ROP_DRAW_SPRITE); - cmd->x = x; - cmd->y = y; - cmd->w = w; - cmd->h = h; - cmd->uvX = 0; - cmd->uvY = 0; - cmd->uvW = 255; - cmd->uvH = 255; - cmd->tint = tint; -} diff --git a/src/dusk/render/render.h b/src/dusk/render/render.h deleted file mode 100644 index 60bd9b57..00000000 --- a/src/dusk/render/render.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "render/rop.h" -#include "render/ropbuffer.h" - -void renderClear(color_t color); - -void renderSprite( - int16_t x, int16_t y, - int16_t w, int16_t h, - color_t tint -); diff --git a/src/dusk/render/rop.h b/src/dusk/render/rop.h deleted file mode 100644 index 7e461c55..00000000 --- a/src/dusk/render/rop.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/color.h" - -#define ROP_SIZE 32 - -typedef enum { - ROP_NOP = 0, - ROP_CLEAR, - ROP_DRAW_SPRITE, - ROP_COUNT -} ropop_t; - -#define ROP_FLAG_BLEND ((uint8_t)(1 << 0)) - -/* 4 bytes, every opcode starts with this */ -typedef struct { - uint8_t op; - uint8_t flags; - int16_t depth; -} ropheader_t; - -_Static_assert(sizeof(ropheader_t) == 4, "ropheader_t must be 4 bytes"); - -/* ROP_CLEAR — 32 bytes */ -typedef struct { - ropheader_t header; /* 4 */ - color_t color; /* 4 */ - uint8_t pad[24];/* 24 */ -} ropclear_t; - -_Static_assert(sizeof(ropclear_t) == ROP_SIZE, "ropclear_t must be ROP_SIZE bytes"); - -/* ROP_DRAW_SPRITE — 32 bytes, screen-space pixel coordinates */ -typedef struct { - ropheader_t header; /* 4 */ - int16_t x, y; /* 4 */ - int16_t w, h; /* 4 */ - uint8_t uvX, uvY; /* 2 */ - uint8_t uvW, uvH; /* 2 */ - color_t tint; /* 4 */ - uint16_t texture; /* 2 handle, 0 = white */ - uint16_t palette; /* 2 */ - uint8_t pad[8]; /* 8 */ -} ropsprite_t; - -_Static_assert(sizeof(ropsprite_t) == ROP_SIZE, "ropsprite_t must be ROP_SIZE bytes"); diff --git a/src/dusk/scene/test/scenetest.c b/src/dusk/scene/test/scenetest.c index ea3e5c53..c075ac63 100644 --- a/src/dusk/scene/test/scenetest.c +++ b/src/dusk/scene/test/scenetest.c @@ -6,27 +6,211 @@ */ #include "scene/test/scenetest.h" -#include "render/render.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" + +/* 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, +}; + +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 ----------------------------------------------------------- */ errorret_t sceneTestInit(scenedata_t *data) { - (void)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. */ + errorOk(); } errorret_t sceneTestUpdate(scenedata_t *data) { - (void)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); + errorOk(); } errorret_t sceneTestRender(scenedata_t *data) { - (void)data; - renderClear(color(32, 32, 48, 255)); - renderSprite(100, 100, 32, 32, COLOR_WHITE); + 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(); + errorOk(); } errorret_t sceneTestDispose(scenedata_t *data) { - (void)data; + renderTilemapChunkDispose(tilemapChunk); + renderTextureDispose(tilemapTileset); + renderTextureDispose(testTex); errorOk(); } diff --git a/src/duskdolphin/display/CMakeLists.txt b/src/duskdolphin/display/CMakeLists.txt index 88bf05f8..a284b6ee 100644 --- a/src/duskdolphin/display/CMakeLists.txt +++ b/src/duskdolphin/display/CMakeLists.txt @@ -7,11 +7,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC displaydolphin.c - # debug.c ) -# Subdirs -add_subdirectory(framebuffer) -add_subdirectory(mesh) -add_subdirectory(texture) -add_subdirectory(shader) \ No newline at end of file +add_subdirectory(render) \ No newline at end of file diff --git a/src/duskdolphin/display/displaydolphin.c b/src/duskdolphin/display/displaydolphin.c index 62445b7d..30c17e91 100644 --- a/src/duskdolphin/display/displaydolphin.c +++ b/src/duskdolphin/display/displaydolphin.c @@ -6,8 +6,10 @@ */ #include "display/display.h" +#include "display/render/renderdolphin.h" #include "util/memory.h" #include "engine/engine.h" +#include "assert/assert.h" errorret_t displayInitDolphin(void) { VIDEO_Init(); @@ -79,50 +81,26 @@ errorret_t displayInitDolphin(void) { GX_SetColorUpdate(GX_TRUE); // Describe mesh vertex format. - GX_ClearVtxDesc(); - GX_SetVtxDesc(GX_VA_POS, GX_INDEX16); - #if MESH_ENABLE_COLOR - GX_SetVtxDesc(GX_VA_CLR0, GX_INDEX16); - #endif - GX_SetVtxDesc(GX_VA_TEX0, GX_INDEX16); - GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); - #if MESH_ENABLE_COLOR - GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); - #endif - GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + /* Vertex format is configured per-draw by the render backend */ + GX_SetCullMode(GX_CULL_NONE); + GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); + GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); + GX_SetColorUpdate(GX_TRUE); + GX_SetAlphaUpdate(GX_TRUE); + errorChain(renderDolphinInit()); errorOk(); } -errorret_t displayUpdateDolphin(void) { +errorret_t displayFlushDolphin(ropbuffer_t *buf) { + assertNotNull(buf, "Dolphin flush: null ropbuffer"); ENGINE.running = SYS_MainLoop(); + errorChain(renderDolphinFlush(buf)); errorOk(); } -errorret_t displaySetStateDolphin(displaystate_t state) { - if(state.flags & DISPLAY_STATE_FLAG_CULL) { - GX_SetCullMode(GX_CULL_FRONT); - } else { - GX_SetCullMode(GX_CULL_NONE); - } - - if(state.flags & DISPLAY_STATE_FLAG_DEPTH_TEST) { - GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); - } else { - GX_SetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE); - } - - if(state.flags & DISPLAY_STATE_FLAG_BLEND) { - GX_SetBlendMode( - GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR - ); - } else { - GX_SetBlendMode( - GX_BM_NONE, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR - ); - } - - errorOk(); +void displayDisposeDolphin(void) { + renderDolphinDispose(); } errorret_t displaySwapDolphin(void) { diff --git a/src/duskdolphin/display/displaydolphin.h b/src/duskdolphin/display/displaydolphin.h index 8b992c8b..132e0e3d 100644 --- a/src/duskdolphin/display/displaydolphin.h +++ b/src/duskdolphin/display/displaydolphin.h @@ -8,6 +8,7 @@ #pragma once #include "error/error.h" #include "display/displaystate.h" +#include "display/render/ropbuffer.h" #define DISPLAY_DOLPHIN_FIFO_SIZE (256*1024) @@ -22,18 +23,6 @@ typedef struct { * Initializes the display system on Dolphin. */ errorret_t displayInitDolphin(void); - -/** - * Tells the display system to actually draw the frame on Dolphin. - */ -errorret_t displayUpdateDolphin(void); - -/** - * Swaps the display buffers on Dolphin. - */ +errorret_t displayFlushDolphin(ropbuffer_t *buf); errorret_t displaySwapDolphin(void); - -/** - * Sets the display state on Dolphin. - */ -errorret_t displaySetStateDolphin(displaystate_t state); \ No newline at end of file +void displayDisposeDolphin(void); \ No newline at end of file diff --git a/src/duskdolphin/display/displayplatform.h b/src/duskdolphin/display/displayplatform.h index ed9b61a9..53443b29 100644 --- a/src/duskdolphin/display/displayplatform.h +++ b/src/duskdolphin/display/displayplatform.h @@ -6,11 +6,11 @@ */ #pragma once -#include "displaydolphin.h" +#include "display/displaydolphin.h" -#define displayPlatformInit displayInitDolphin -#define displayPlatformUpdate displayUpdateDolphin -#define displayPlatformSwap displaySwapDolphin -#define displayPlatformSetState displaySetStateDolphin +typedef displaydolphin_t displayplatform_t; -typedef displaydolphin_t displayplatform_t; \ No newline at end of file +#define displayPlatformInit displayInitDolphin +#define displayPlatformFlush displayFlushDolphin +#define displayPlatformSwap displaySwapDolphin +#define displayPlatformDispose displayDisposeDolphin \ No newline at end of file diff --git a/src/duskdolphin/display/render/CMakeLists.txt b/src/duskdolphin/display/render/CMakeLists.txt new file mode 100644 index 00000000..8abf08bb --- /dev/null +++ b/src/duskdolphin/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_LIBRARY_TARGET_NAME} + PUBLIC + renderdolphin.c +) diff --git a/src/duskdolphin/display/render/renderdolphin.c b/src/duskdolphin/display/render/renderdolphin.c new file mode 100644 index 00000000..867b2612 --- /dev/null +++ b/src/duskdolphin/display/render/renderdolphin.c @@ -0,0 +1,373 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/render/renderdolphin.h" +#include "display/render/rop.h" +#include "assert/assert.h" +#include "util/memory.h" +#include +#include + +/* ---- Texture table ------------------------------------------------------- */ + +#define DOLPHIN_RTEXTURE_MAX 256 + +/* GX CI8 (8-bit indexed) textures. + * cpuIndices : w*h row-major bytes, user-writable source of truth. + * tiledData : CI8 8×4-texel tiles (32 bytes/tile), re-derived at bind. + * palette : 256 RGBA entries, user-writable source of truth. + * tlutData : 256 × uint16_t RGB5A3, re-derived at bind. + * All textures share GX_TLUT0 (loaded per bind). */ +typedef struct { + GXTexObj obj; + GXTlutObj tlut; + void *tiledData; /* CI8 tiled, memalign(32) */ + void *tlutData; /* 256 × uint16_t RGB5A3, memalign(32) */ + uint8_t *cpuIndices; + color_t palette[256]; + uint16_t w, h; +} dolphintexentry_t; + +static dolphintexentry_t dolphinTexTable[DOLPHIN_RTEXTURE_MAX]; +static uint16_t dolphinTexNext = 1; /* 0 = white fallback */ + +/* ---- Camera state -------------------------------------------------------- */ + +static Mtx44 dolphinProj; +static Mtx dolphinView; +static int dolphinIs3D = 0; + +/* ---- Helpers ------------------------------------------------------------- */ + +/* Convert color_t RGBA → GX RGB5A3 16-bit big-endian. */ +static uint16_t toRGB5A3(color_t c) { + if(c.a == 0xFF) { + /* Opaque: RGB555 with bit15=1 */ + return (uint16_t)(0x8000u + | ((uint16_t)(c.r >> 3) << 10) + | ((uint16_t)(c.g >> 3) << 5) + | ((uint16_t)(c.b >> 3))); + } + /* Transparent: A3RGB4 with bit15=0 */ + return (uint16_t)(((uint16_t)(c.a >> 5) << 12) + | ((uint16_t)(c.r >> 4) << 8) + | ((uint16_t)(c.g >> 4) << 4) + | ((uint16_t)(c.b >> 4))); +} + +/* Convert linear row-major indices to GX CI8 tiled layout (8×4 tiles). */ +static void toCI8Tiled( + uint8_t *dst, const uint8_t *src, uint16_t w, uint16_t h +) { + uint16_t tW = (uint16_t)((w + 7) / 8); + uint16_t tH = (uint16_t)((h + 3) / 4); + for(uint16_t ty = 0; ty < tH; ty++) { + for(uint16_t tx = 0; tx < tW; tx++) { + uint8_t *tile = dst + ((uint32_t)(ty * tW + tx)) * 32; + for(uint16_t row = 0; row < 4; row++) { + for(uint16_t col = 0; col < 8; col++) { + uint16_t px = (uint16_t)(tx * 8 + col); + uint16_t py = (uint16_t)(ty * 4 + row); + tile[row * 8 + col] = (px < w && py < h) ? src[py * w + px] : 0; + } + } + } + } +} + +/* ---- Init ---------------------------------------------------------------- */ + +errorret_t renderDolphinInit(void) { + /* White 1×1 fallback: CI8 single tile (32 bytes), palette[0]=white */ + dolphintexentry_t *e = &dolphinTexTable[0]; + + e->cpuIndices = (uint8_t *)memalign(32, 1); + assertNotNull(e->cpuIndices, "Dolphin: failed to allocate fallback cpu indices"); + e->cpuIndices[0] = 0; + + e->tiledData = (uint8_t *)memalign(32, 32); + assertNotNull(e->tiledData, "Dolphin: failed to allocate fallback tile data"); + memset(e->tiledData, 0, 32); + DCFlushRange(e->tiledData, 32); + + e->tlutData = (uint16_t *)memalign(32, 256 * sizeof(uint16_t)); + assertNotNull(e->tlutData, "Dolphin: failed to allocate fallback TLUT"); + memset(e->tlutData, 0, 256 * sizeof(uint16_t)); + memset(e->palette, 0, sizeof(e->palette)); + e->palette[0] = COLOR_WHITE; + ((uint16_t *)e->tlutData)[0] = 0xFFFF; /* RGB5A3 white */ + DCFlushRange(e->tlutData, 256 * sizeof(uint16_t)); + + e->w = 1; e->h = 1; + GX_InitTlutObj(&e->tlut, e->tlutData, GX_TL_RGB5A3, 256); + GX_InitTexObjCI(&e->obj, e->tiledData, 1, 1, GX_TF_CI8, GX_CLAMP, GX_CLAMP, GX_FALSE, GX_TLUT0); + GX_InitTexObjFilterMode(&e->obj, GX_NEAR, GX_NEAR); + + guMtxIdentity(dolphinView); + guPerspective(dolphinProj, 60.0f, 4.0f/3.0f, 0.1f, 100.0f); + + errorOk(); +} + +/* ---- Texture ------------------------------------------------------------- */ + +rtexture_t renderDolphinTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +) { + assertTrue(dolphinTexNext < DOLPHIN_RTEXTURE_MAX, "Dolphin texture table full"); + + rtexture_t handle = (rtexture_t)dolphinTexNext++; + dolphintexentry_t *e = &dolphinTexTable[handle]; + + uint32_t cpuBytes = (uint32_t)w * h; + e->cpuIndices = (uint8_t *)memalign(32, cpuBytes); + assertNotNull(e->cpuIndices, "Dolphin: failed to allocate cpu index buffer"); + memcpy(e->cpuIndices, indices, cpuBytes); + + uint16_t tW = (uint16_t)((w + 7) / 8); + uint16_t tH = (uint16_t)((h + 3) / 4); + uint32_t tileBytes = (uint32_t)tW * tH * 32; + e->tiledData = (uint8_t *)memalign(32, tileBytes); + assertNotNull(e->tiledData, "Dolphin: failed to allocate tile data"); + toCI8Tiled((uint8_t *)e->tiledData, indices, w, h); + DCFlushRange(e->tiledData, tileBytes); + + e->tlutData = (uint16_t *)memalign(32, 256 * sizeof(uint16_t)); + assertNotNull(e->tlutData, "Dolphin: failed to allocate TLUT data"); + memcpy(e->palette, palette, 256 * sizeof(color_t)); + uint16_t *tlut = (uint16_t *)e->tlutData; + for(int i = 0; i < 256; i++) tlut[i] = toRGB5A3(palette[i]); + DCFlushRange(e->tlutData, 256 * sizeof(uint16_t)); + + e->w = w; e->h = h; + GX_InitTlutObj(&e->tlut, e->tlutData, GX_TL_RGB5A3, 256); + GX_InitTexObjCI(&e->obj, e->tiledData, w, h, GX_TF_CI8, GX_CLAMP, GX_CLAMP, GX_FALSE, GX_TLUT0); + GX_InitTexObjFilterMode(&e->obj, GX_NEAR, GX_NEAR); + return handle; +} + +void renderDolphinTextureDispose(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= DOLPHIN_RTEXTURE_MAX) return; + dolphintexentry_t *e = &dolphinTexTable[tex]; + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } + if(e->tiledData) { free(e->tiledData); e->tiledData = NULL; } + if(e->tlutData) { free(e->tlutData); e->tlutData = NULL; } +} + +color_t *renderDolphinTextureGetPalette(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= DOLPHIN_RTEXTURE_MAX) return NULL; + return dolphinTexTable[tex].palette; +} + +uint8_t *renderDolphinTextureGetIndices(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= DOLPHIN_RTEXTURE_MAX) return NULL; + return dolphinTexTable[tex].cpuIndices; +} + +static void bindTexture(rtexture_t tex) { + dolphintexentry_t *e = + (tex < DOLPHIN_RTEXTURE_MAX && dolphinTexTable[tex].cpuIndices) + ? &dolphinTexTable[tex] + : &dolphinTexTable[0]; + + /* Re-tile cpuIndices → tiledData and flush for GX DMA. */ + toCI8Tiled((uint8_t *)e->tiledData, e->cpuIndices, e->w, e->h); + uint16_t tW = (uint16_t)((e->w + 7) / 8); + uint16_t tH = (uint16_t)((e->h + 3) / 4); + DCFlushRange(e->tiledData, (uint32_t)tW * tH * 32); + + /* Re-convert palette color_t RGBA → RGB5A3 and flush. */ + uint16_t *tlut = (uint16_t *)e->tlutData; + for(int i = 0; i < 256; i++) tlut[i] = toRGB5A3(e->palette[i]); + DCFlushRange(e->tlutData, 256 * sizeof(uint16_t)); + + GX_LoadTlut(&e->tlut, GX_TLUT0); + GX_LoadTexObj(&e->obj, GX_TEXMAP0); +} + +/* ---- 2D vertex format setup ---------------------------------------------- */ + +static void setup2D(void) { + 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); + + Mtx44 ortho; + guOrtho(ortho, 0, DUSK_DISPLAY_HEIGHT, 0, DUSK_DISPLAY_WIDTH, -1.0f, 1.0f); + GX_LoadProjectionMtx(ortho, GX_ORTHOGRAPHIC); + + Mtx identity; + guMtxIdentity(identity); + GX_LoadPosMtxImm(identity, GX_PNMTX0); + GX_SetCurrentMtx(GX_PNMTX0); + + dolphinIs3D = 0; +} + +/* ---- 3D vertex format setup ---------------------------------------------- */ + +static void setup3D(void) { + 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_XYZ, GX_F32, 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_LoadProjectionMtx(dolphinProj, GX_PERSPECTIVE); + GX_LoadPosMtxImm(dolphinView, GX_PNMTX0); + GX_SetCurrentMtx(GX_PNMTX0); + + dolphinIs3D = 1; +} + +/* ---- Tint channel -------------------------------------------------------- */ + +static void setTintChannel(color_t tint) { + GX_SetTevColor(GX_TEVREG0, (GXColor){tint.r, tint.g, tint.b, tint.a}); + GX_SetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXC, GX_CC_C0, GX_CC_ZERO); + GX_SetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_TEXA, GX_CA_A0, GX_CA_ZERO); + GX_SetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GX_SetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); +} + +/* ---- 2D sprite ----------------------------------------------------------- */ + +static void draw2DSprite(const ropsprite_t *s) { + if(dolphinIs3D) setup2D(); + + bindTexture(s->texture); + setTintChannel(s->tint); + + float u0 = (s->uvX / 255.0f); + float v0 = (s->uvY / 255.0f); + float u1 = ((s->uvX + s->uvW) / 255.0f); + float v1 = ((s->uvY + s->uvH) / 255.0f); + int16_t x0 = s->x, y0 = s->y; + int16_t x1 = (int16_t)(s->x + s->w), y1 = (int16_t)(s->y + s->h); + + GX_Begin(GX_QUADS, GX_VTXFMT0, 4); + GX_Position2s16(x0, y0); GX_Color4u8(s->tint.r,s->tint.g,s->tint.b,s->tint.a); GX_TexCoord2f32(u0,v0); + GX_Position2s16(x1, y0); GX_Color4u8(s->tint.r,s->tint.g,s->tint.b,s->tint.a); GX_TexCoord2f32(u1,v0); + GX_Position2s16(x1, y1); GX_Color4u8(s->tint.r,s->tint.g,s->tint.b,s->tint.a); GX_TexCoord2f32(u1,v1); + GX_Position2s16(x0, y1); GX_Color4u8(s->tint.r,s->tint.g,s->tint.b,s->tint.a); GX_TexCoord2f32(u0,v1); + GX_End(); +} + +/* ---- 3D quad ------------------------------------------------------------- */ + +static void draw3DQuad(const ropquad3d_t *q) { + if(!dolphinIs3D) setup3D(); + + bindTexture(q->texture); + setTintChannel(q->tint); + + float u0 = q->uvX/255.0f, v0 = q->uvY/255.0f; + float u1 = (q->uvX+q->uvW)/255.0f, v1 = (q->uvY+q->uvH)/255.0f; + + 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; + + 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; + uint8_t r=q->tint.r, g=q->tint.g, b=q->tint.b, a=q->tint.a; + + GX_Begin(GX_QUADS, GX_VTXFMT0, 4); + GX_Position3f32(tlx,tly,tlz); GX_Color4u8(r,g,b,a); GX_TexCoord2f32(u0,v0); + GX_Position3f32(trx,try_,trz); GX_Color4u8(r,g,b,a); GX_TexCoord2f32(u1,v0); + GX_Position3f32(brx,bry,brz); GX_Color4u8(r,g,b,a); GX_TexCoord2f32(u1,v1); + GX_Position3f32(blx,bly,blz); GX_Color4u8(r,g,b,a); GX_TexCoord2f32(u0,v1); + GX_End(); +} + +/* ---- Flush --------------------------------------------------------------- */ + +errorret_t renderDolphinFlush(ropbuffer_t *buf) { + assertNotNull(buf, "Dolphin flush: null ropbuffer"); + + setup2D(); + + GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); + GX_SetAlphaUpdate(GX_TRUE); + GX_SetColorUpdate(GX_TRUE); + + 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; + GXColor bg = {c->color.r, c->color.g, c->color.b, c->color.a}; + GX_SetCopyClear(bg, GX_MAX_Z24); + break; + } + case ROP_DRAW_SPRITE: + draw2DSprite((const ropsprite_t *)hdr); + break; + + case ROP_SET_PROJECTION: { + const ropprojection_t *p = (const ropprojection_t *)hdr; + if(p->fovY > FIXED_ZERO) { + float fovDeg = fixedToFloat(p->fovY) * (180.0f / 3.14159f); + float aspect = fixedToFloat(p->aspect); + guPerspective(dolphinProj, + fovDeg, aspect, + fixedToFloat(p->nearZ), fixedToFloat(p->farZ)); + } else { + float aspect = fixedToFloat(p->aspect); + guOrtho(dolphinProj, + -1.0f, 1.0f, -aspect, aspect, + fixedToFloat(p->nearZ), fixedToFloat(p->farZ)); + } + break; + } + case ROP_SET_VIEW: { + const ropview_t *v = (const ropview_t *)hdr; + guVector eye = {(float)v->eyeX, (float)v->eyeY, (float)v->eyeZ}; + guVector target = {(float)v->tgtX, (float)v->tgtY, (float)v->tgtZ}; + guVector up = {0.0f, 1.0f, 0.0f}; + guLookAt(dolphinView, &eye, &up, &target); + if(dolphinIs3D) { + GX_LoadPosMtxImm(dolphinView, GX_PNMTX0); + } + break; + } + case ROP_DRAW_QUAD_3D: + draw3DQuad((const ropquad3d_t *)hdr); + break; + + default: + break; + } + offset += ropOpSize(op); + } + + errorOk(); +} + +/* ---- Dispose ------------------------------------------------------------- */ + +void renderDolphinDispose(void) { + for(uint16_t i = 0; i < dolphinTexNext; i++) { + dolphintexentry_t *e = &dolphinTexTable[i]; + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } + if(e->tiledData) { free(e->tiledData); e->tiledData = NULL; } + if(e->tlutData) { free(e->tlutData); e->tlutData = NULL; } + } + dolphinTexNext = 1; +} diff --git a/src/duskdolphin/display/render/renderdolphin.h b/src/duskdolphin/display/render/renderdolphin.h new file mode 100644 index 00000000..5683389e --- /dev/null +++ b/src/duskdolphin/display/render/renderdolphin.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 renderDolphinInit(void); +errorret_t renderDolphinFlush(ropbuffer_t *buf); +void renderDolphinDispose(void); + +rtexture_t renderDolphinTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +); +void renderDolphinTextureDispose(rtexture_t tex); +color_t *renderDolphinTextureGetPalette(rtexture_t tex); +uint8_t *renderDolphinTextureGetIndices(rtexture_t tex); diff --git a/src/duskdolphin/display/render/renderplatform.h b/src/duskdolphin/display/render/renderplatform.h new file mode 100644 index 00000000..2398310b --- /dev/null +++ b/src/duskdolphin/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/renderdolphin.h" + +#define renderPlatformTextureCreate renderDolphinTextureCreate +#define renderPlatformTextureDispose renderDolphinTextureDispose +#define renderPlatformTextureGetPalette renderDolphinTextureGetPalette +#define renderPlatformTextureGetIndices renderDolphinTextureGetIndices diff --git a/src/duskgl/CMakeLists.txt b/src/duskgl/CMakeLists.txt index 39ec5dd4..f7507042 100644 --- a/src/duskgl/CMakeLists.txt +++ b/src/duskgl/CMakeLists.txt @@ -9,4 +9,4 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} ) add_subdirectory(error) -add_subdirectory(render) +add_subdirectory(display) diff --git a/src/duskgl/display/CMakeLists.txt b/src/duskgl/display/CMakeLists.txt new file mode 100644 index 00000000..f8148edf --- /dev/null +++ b/src/duskgl/display/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_subdirectory(render) diff --git a/src/duskgl/display/render/CMakeLists.txt b/src/duskgl/display/render/CMakeLists.txt new file mode 100644 index 00000000..8861f1d3 --- /dev/null +++ b/src/duskgl/display/render/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_LIBRARY_TARGET_NAME} + PUBLIC + rendergl.c + rendertexturegl.c + rendertilemapcgl.c +) diff --git a/src/duskgl/display/render/rendergl.c b/src/duskgl/display/render/rendergl.c new file mode 100644 index 00000000..2b0d75e9 --- /dev/null +++ b/src/duskgl/display/render/rendergl.c @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/render/rendergl.h" +#include "display/render/rendertexturegl.h" +#include "display/render/rendertilemapcgl.h" +#include "error/errorgl.h" +#include "display/render/rop.h" +#include + +/* ---- 2D shader: xy rgba uv, depth from uniform -------------------------- */ + +static const char *VERT2D = + "#version 330 core\n" + "layout(location=0) in vec2 aPos;\n" + "layout(location=1) in vec4 aColor;\n" + "layout(location=2) in vec2 aUV;\n" + "uniform vec2 uRes;\n" + "uniform vec2 uOffset;\n" + "uniform float uDepth;\n" + "out vec4 vColor; out vec2 vUV;\n" + "void main() {\n" + " vec2 clip = ((aPos + uOffset) / uRes) * 2.0 - 1.0;\n" + " clip.y = -clip.y;\n" + " gl_Position = vec4(clip, uDepth, 1.0);\n" + " vColor = aColor; vUV = aUV;\n" + "}\n"; + +/* ---- 3D shader: xyz rgba uv, full MVP ----------------------------------- */ + +static const char *VERT3D = + "#version 330 core\n" + "layout(location=0) in vec3 aPos;\n" + "layout(location=1) in vec4 aColor;\n" + "layout(location=2) in vec2 aUV;\n" + "uniform mat4 uMVP;\n" + "out vec4 vColor; out vec2 vUV;\n" + "void main() {\n" + " gl_Position = uMVP * vec4(aPos, 1.0);\n" + " vColor = aColor; vUV = aUV;\n" + "}\n"; + +/* Palette shader: uTexIndices is a GL_R8 W×H texture (one byte per pixel); + * uTexPalette is a 256×1 RGBA texture (the colour table). + * The index value (0-255) stored as a normalised float in R8 is converted back + * to an exact texel centre using (raw*(255/256) + 0.5/256) before sampling, + * giving pixel-exact palette lookups for all 256 possible indices. */ +static const char *FRAG = + "#version 330 core\n" + "in vec4 vColor; in vec2 vUV;\n" + "uniform sampler2D uTexIndices;\n" + "uniform sampler2D uTexPalette;\n" + "out vec4 fragColor;\n" + "void main() {\n" + " float raw = texture(uTexIndices, vUV).r;\n" + " float u = raw * (255.0/256.0) + (0.5/256.0);\n" + " fragColor = texture(uTexPalette, vec2(u, 0.5)) * vColor;\n" + "}\n"; + +/* ---- State --------------------------------------------------------------- */ + +typedef struct { + GLuint prog, vao, vbo; + GLint uRes, uOffset, uDepth, uTexIndices, uTexPalette; +} rgl2d_t; + +typedef struct { + GLuint prog, vao, vbo; + GLint uMVP, uTexIndices, uTexPalette; + mat4 proj, view; +} rgl3d_t; + +static rgl2d_t gl2d; +static rgl3d_t gl3d; + +/* ---- Helpers ------------------------------------------------------------- */ + +static GLuint buildProgram(const char *vsrc, const char *fsrc) { + GLuint v = glCreateShader(GL_VERTEX_SHADER); + GLuint f = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(v, 1, &vsrc, NULL); glCompileShader(v); + glShaderSource(f, 1, &fsrc, NULL); glCompileShader(f); + GLuint p = glCreateProgram(); + glAttachShader(p, v); glAttachShader(p, f); + glLinkProgram(p); + glDeleteShader(v); glDeleteShader(f); + return p; +} + +/* ---- Init ---------------------------------------------------------------- */ + +errorret_t renderGLInit(void) { + /* 2D — 8 floats/vert: x y r g b a u v */ + gl2d.prog = buildProgram(VERT2D, FRAG); + gl2d.uRes = glGetUniformLocation(gl2d.prog, "uRes"); + gl2d.uOffset = glGetUniformLocation(gl2d.prog, "uOffset"); + gl2d.uDepth = glGetUniformLocation(gl2d.prog, "uDepth"); + gl2d.uTexIndices = glGetUniformLocation(gl2d.prog, "uTexIndices"); + gl2d.uTexPalette = glGetUniformLocation(gl2d.prog, "uTexPalette"); + glGenVertexArrays(1, &gl2d.vao); glGenBuffers(1, &gl2d.vbo); + glBindVertexArray(gl2d.vao); + glBindBuffer(GL_ARRAY_BUFFER, gl2d.vbo); + glBufferData(GL_ARRAY_BUFFER, 6*8*sizeof(GLfloat), NULL, GL_DYNAMIC_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (void*)0); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (void*)(2*sizeof(GLfloat))); + glEnableVertexAttribArray(1); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (void*)(6*sizeof(GLfloat))); + glEnableVertexAttribArray(2); + glBindVertexArray(0); + errorChain(errorGLCheck()); + + /* 3D — 9 floats/vert: x y z r g b a u v */ + gl3d.prog = buildProgram(VERT3D, FRAG); + gl3d.uMVP = glGetUniformLocation(gl3d.prog, "uMVP"); + gl3d.uTexIndices = glGetUniformLocation(gl3d.prog, "uTexIndices"); + gl3d.uTexPalette = glGetUniformLocation(gl3d.prog, "uTexPalette"); + glm_mat4_identity(gl3d.proj); + glm_mat4_identity(gl3d.view); + glGenVertexArrays(1, &gl3d.vao); glGenBuffers(1, &gl3d.vbo); + glBindVertexArray(gl3d.vao); + glBindBuffer(GL_ARRAY_BUFFER, gl3d.vbo); + glBufferData(GL_ARRAY_BUFFER, 6*9*sizeof(GLfloat), NULL, GL_DYNAMIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9*sizeof(GLfloat), (void*)0); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 9*sizeof(GLfloat), (void*)(3*sizeof(GLfloat))); + glEnableVertexAttribArray(1); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 9*sizeof(GLfloat), (void*)(7*sizeof(GLfloat))); + glEnableVertexAttribArray(2); + glBindVertexArray(0); + errorChain(errorGLCheck()); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + + renderGLTextureInit(); + errorChain(errorGLCheck()); + errorOk(); +} + +/* ---- 2D draw ------------------------------------------------------------- */ + +static void draw2DSprite(const ropsprite_t *s, float rW, float rH) { + float r = s->tint.r/255.0f, g = s->tint.g/255.0f; + float b = s->tint.b/255.0f, a = s->tint.a/255.0f; + float x0 = (float)s->x, y0 = (float)s->y; + float x1 = x0+(float)s->w, y1 = y0+(float)s->h; + float u0 = s->uvX/255.0f, v0 = s->uvY/255.0f; + float u1 = (s->uvX+s->uvW)/255.0f, v1 = (s->uvY+s->uvH)/255.0f; + /* int16 depth: 0=front, 32767=back mapped to [0,1] NDC Z */ + float depth = (float)s->header.depth / 32767.0f; + + GLfloat verts[6][8] = { + {x0,y1, r,g,b,a, u0,v1}, + {x0,y0, r,g,b,a, u0,v0}, + {x1,y0, r,g,b,a, u1,v0}, + {x0,y1, r,g,b,a, u0,v1}, + {x1,y0, r,g,b,a, u1,v0}, + {x1,y1, r,g,b,a, u1,v1}, + }; + + glUseProgram(gl2d.prog); + glUniform2f(gl2d.uRes, rW, rH); + glUniform2f(gl2d.uOffset, 0.0f, 0.0f); + glUniform1f(gl2d.uDepth, depth); + glUniform1i(gl2d.uTexIndices, 0); + glUniform1i(gl2d.uTexPalette, 1); + renderGLTextureBind(s->texture); + glBindVertexArray(gl2d.vao); + glBindBuffer(GL_ARRAY_BUFFER, gl2d.vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts); + glDrawArrays(GL_TRIANGLES, 0, 6); +} + +static void draw2DTilemapChunk(const roptilemapc_t *t, float rW, float rH) { + const glchunkentry_t *e = renderGLTilemapChunkGet(t->chunk); + if(!e) return; + float depth = (float)t->header.depth / 32767.0f; + glUseProgram(gl2d.prog); + glUniform2f(gl2d.uRes, rW, rH); + glUniform2f(gl2d.uOffset, (float)t->x, (float)t->y); + glUniform1f(gl2d.uDepth, depth); + glUniform1i(gl2d.uTexIndices, 0); + glUniform1i(gl2d.uTexPalette, 1); + renderGLTextureBind(e->tileset); + glBindVertexArray(e->vao); + glDrawArrays(GL_TRIANGLES, 0, (GLsizei)e->vertCount); +} + +/* ---- 3D draw ------------------------------------------------------------- */ + +static void draw3DQuad(const ropquad3d_t *q) { + float r = q->tint.r/255.0f, g = q->tint.g/255.0f; + float b = q->tint.b/255.0f, a = q->tint.a/255.0f; + float u0 = q->uvX/255.0f, v0 = q->uvY/255.0f; + float u1 = (q->uvX+q->uvW)/255.0f, v1 = (q->uvY+q->uvH)/255.0f; + + 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; + + float tlx = cx-rx+ux, tly = cy-ry+uy, tlz = cz-rz+uz; + float trx = cx+rx+ux, tr_y= 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; + + GLfloat verts[6][9] = { + {tlx,tly,tlz, r,g,b,a, u0,v0}, + {blx,bly,blz, r,g,b,a, u0,v1}, + {brx,bry,brz, r,g,b,a, u1,v1}, + {tlx,tly,tlz, r,g,b,a, u0,v0}, + {brx,bry,brz, r,g,b,a, u1,v1}, + {trx,tr_y,trz, r,g,b,a, u1,v0}, + }; + + mat4 mvp; + glm_mat4_mul(gl3d.proj, gl3d.view, mvp); + + glUseProgram(gl3d.prog); + glUniformMatrix4fv(gl3d.uMVP, 1, GL_FALSE, (GLfloat *)mvp); + glUniform1i(gl3d.uTexIndices, 0); + glUniform1i(gl3d.uTexPalette, 1); + renderGLTextureBind(q->texture); + glBindVertexArray(gl3d.vao); + glBindBuffer(GL_ARRAY_BUFFER, gl3d.vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts); + glDrawArrays(GL_TRIANGLES, 0, 6); +} + +/* ---- Flush --------------------------------------------------------------- */ + +errorret_t renderGLFlush(ropbuffer_t *buf, int winW, int winH) { + glViewport(0, 0, winW, winH); + errorChain(errorGLCheck()); + + 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; + glClearColor( + c->color.r/255.0f, c->color.g/255.0f, + c->color.b/255.0f, c->color.a/255.0f + ); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + errorChain(errorGLCheck()); + break; + } + case ROP_DRAW_SPRITE: { + draw2DSprite((const ropsprite_t *)hdr, (float)winW, (float)winH); + errorChain(errorGLCheck()); + break; + } + case ROP_SET_PROJECTION: { + const ropprojection_t *p = (const ropprojection_t *)hdr; + if(p->fovY > FIXED_ZERO) + glm_perspective( + fixedToFloat(p->fovY), fixedToFloat(p->aspect), + fixedToFloat(p->nearZ), fixedToFloat(p->farZ), + gl3d.proj + ); + else + glm_mat4_identity(gl3d.proj); + break; + } + case ROP_SET_VIEW: { + const ropview_t *v = (const ropview_t *)hdr; + vec3 eye = {(float)v->eyeX, (float)v->eyeY, (float)v->eyeZ}; + vec3 tgt = {(float)v->tgtX, (float)v->tgtY, (float)v->tgtZ}; + vec3 up = {0.0f, 1.0f, 0.0f}; + glm_lookat(eye, tgt, up, gl3d.view); + break; + } + case ROP_DRAW_QUAD_3D: { + draw3DQuad((const ropquad3d_t *)hdr); + errorChain(errorGLCheck()); + break; + } + case ROP_DRAW_TILEMAP_CHUNK: { + draw2DTilemapChunk((const roptilemapc_t *)hdr, (float)winW, (float)winH); + errorChain(errorGLCheck()); + break; + } + default: + break; + } + offset += ropOpSize(op); + } + + glUseProgram(0); + glBindVertexArray(0); + errorOk(); +} + +/* ---- Dispose ------------------------------------------------------------- */ + +void renderGLDispose(void) { + renderGLTilemapChunkTableDispose(); + renderGLTextureTableDispose(); + if(gl2d.vbo) { glDeleteBuffers(1, &gl2d.vbo); gl2d.vbo = 0; } + if(gl2d.vao) { glDeleteVertexArrays(1, &gl2d.vao); gl2d.vao = 0; } + if(gl2d.prog) { glDeleteProgram(gl2d.prog); gl2d.prog = 0; } + if(gl3d.vbo) { glDeleteBuffers(1, &gl3d.vbo); gl3d.vbo = 0; } + if(gl3d.vao) { glDeleteVertexArrays(1, &gl3d.vao); gl3d.vao = 0; } + if(gl3d.prog) { glDeleteProgram(gl3d.prog); gl3d.prog = 0; } +} diff --git a/src/duskgl/render/rendergl.h b/src/duskgl/display/render/rendergl.h similarity index 89% rename from src/duskgl/render/rendergl.h rename to src/duskgl/display/render/rendergl.h index 7cc449da..7a38f4c7 100644 --- a/src/duskgl/render/rendergl.h +++ b/src/duskgl/display/render/rendergl.h @@ -8,7 +8,7 @@ #pragma once #include "duskgl.h" #include "error/error.h" -#include "render/ropbuffer.h" +#include "display/render/ropbuffer.h" errorret_t renderGLInit(void); errorret_t renderGLFlush(ropbuffer_t *buf, int winW, int winH); diff --git a/src/duskgl/display/render/rendertexturegl.c b/src/duskgl/display/render/rendertexturegl.c new file mode 100644 index 00000000..93e75670 --- /dev/null +++ b/src/duskgl/display/render/rendertexturegl.c @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/render/rendertexturegl.h" +#include "display/color.h" +#include "assert/assert.h" +#include + +/* Each slot stores a CPU-side copy of the palette and index data. + * Dirty flags are set by the getters; renderGLTextureBind re-uploads + * to the GPU textures lazily before binding. */ +typedef struct { + GLuint idxTex; + GLuint palTex; + uint8_t *cpuIndices; /* heap-allocated w*h */ + color_t palette[256]; + uint16_t w, h; + uint8_t idxDirty; + uint8_t palDirty; +} gltexentry_t; + +static gltexentry_t glTexTable[RTEXTURE_MAX]; +static uint16_t glTexNext = 1; + +static void initIndexTex(GLuint id, uint16_t w, uint16_t h, const uint8_t *data) { + glBindTexture(GL_TEXTURE_2D, id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, data); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +static void initPaletteTex(GLuint id, const color_t *palette) { + glBindTexture(GL_TEXTURE_2D, id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, palette); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +void renderGLTextureInit(void) { + static const uint8_t whiteIdx = 0; + static color_t whitePal[256]; + whitePal[0] = COLOR_WHITE; + + gltexentry_t *e = &glTexTable[0]; + e->cpuIndices = (uint8_t *)malloc(1); + assertNotNull(e->cpuIndices, "GL: failed to allocate fallback index buffer"); + e->cpuIndices[0] = 0; + e->palette[0] = COLOR_WHITE; + e->w = e->h = 1; + e->idxDirty = e->palDirty = 0; + + glGenTextures(1, &e->idxTex); + glGenTextures(1, &e->palTex); + initIndexTex(e->idxTex, 1, 1, &whiteIdx); + initPaletteTex(e->palTex, whitePal); +} + +void renderGLTextureTableDispose(void) { + for(uint16_t i = 0; i < glTexNext; i++) { + gltexentry_t *e = &glTexTable[i]; + if(e->idxTex) { glDeleteTextures(1, &e->idxTex); e->idxTex = 0; } + if(e->palTex) { glDeleteTextures(1, &e->palTex); e->palTex = 0; } + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } + } + glTexNext = 1; +} + +rtexture_t renderGLTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +) { + assertTrue(glTexNext < RTEXTURE_MAX, "Texture table full"); + + rtexture_t handle = (rtexture_t)glTexNext++; + gltexentry_t *e = &glTexTable[handle]; + + uint32_t pixels = (uint32_t)w * h; + e->cpuIndices = (uint8_t *)malloc(pixels); + assertNotNull(e->cpuIndices, "GL: failed to allocate index buffer"); + memcpy(e->cpuIndices, indices, pixels); + memcpy(e->palette, palette, 256 * sizeof(color_t)); + e->w = w; e->h = h; + e->idxDirty = e->palDirty = 0; + + glGenTextures(1, &e->idxTex); + glGenTextures(1, &e->palTex); + initIndexTex(e->idxTex, w, h, indices); + initPaletteTex(e->palTex, palette); + return handle; +} + +void renderGLTextureDispose(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= RTEXTURE_MAX) return; + gltexentry_t *e = &glTexTable[tex]; + if(e->idxTex) { glDeleteTextures(1, &e->idxTex); e->idxTex = 0; } + if(e->palTex) { glDeleteTextures(1, &e->palTex); e->palTex = 0; } + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } +} + +color_t *renderGLTextureGetPalette(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= RTEXTURE_MAX) return NULL; + glTexTable[tex].palDirty = 1; + return glTexTable[tex].palette; +} + +uint8_t *renderGLTextureGetIndices(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= RTEXTURE_MAX) return NULL; + glTexTable[tex].idxDirty = 1; + return glTexTable[tex].cpuIndices; +} + +void renderGLTextureGetSize(rtexture_t tex, uint16_t *w, uint16_t *h) { + if(tex == RTEXTURE_NONE || tex >= RTEXTURE_MAX || !glTexTable[tex].idxTex) { + *w = 1; *h = 1; + return; + } + *w = glTexTable[tex].w; + *h = glTexTable[tex].h; +} + +/* Flush any dirty CPU data to GPU, then bind both texture units. */ +void renderGLTextureBind(rtexture_t tex) { + gltexentry_t *e = (tex < RTEXTURE_MAX && glTexTable[tex].idxTex) + ? &glTexTable[tex] + : &glTexTable[0]; + + if(e->palDirty) { + glBindTexture(GL_TEXTURE_2D, e->palTex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, 1, GL_RGBA, GL_UNSIGNED_BYTE, e->palette); + e->palDirty = 0; + } + if(e->idxDirty) { + glBindTexture(GL_TEXTURE_2D, e->idxTex); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, e->w, e->h, GL_RED, GL_UNSIGNED_BYTE, e->cpuIndices); + e->idxDirty = 0; + } + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, e->idxTex); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, e->palTex); +} diff --git a/src/duskgl/display/render/rendertexturegl.h b/src/duskgl/display/render/rendertexturegl.h new file mode 100644 index 00000000..11c20cad --- /dev/null +++ b/src/duskgl/display/render/rendertexturegl.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "duskgl.h" +#include "display/render/rtexture.h" +#include "display/color.h" + +#define RTEXTURE_MAX 256 + +void renderGLTextureInit(void); +void renderGLTextureTableDispose(void); +rtexture_t renderGLTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +); +void renderGLTextureDispose(rtexture_t tex); +void renderGLTextureBind(rtexture_t tex); +color_t *renderGLTextureGetPalette(rtexture_t tex); +uint8_t *renderGLTextureGetIndices(rtexture_t tex); +void renderGLTextureGetSize(rtexture_t tex, uint16_t *w, uint16_t *h); diff --git a/src/duskgl/display/render/rendertilemapcgl.c b/src/duskgl/display/render/rendertilemapcgl.c new file mode 100644 index 00000000..158428fe --- /dev/null +++ b/src/duskgl/display/render/rendertilemapcgl.c @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/render/rendertilemapcgl.h" +#include "display/render/rendertexturegl.h" +#include "assert/assert.h" +#include + +static glchunkentry_t glChunkTable[RTILEMAPCHUNK_MAX]; +static uint16_t glChunkNext = 1; /* 0 reserved for RTILEMAPCHUNK_INVALID */ + +void renderGLTilemapChunkTableDispose(void) { + for(uint16_t i = 1; i < glChunkNext; i++) { + glchunkentry_t *e = &glChunkTable[i]; + if(e->vbo) { glDeleteBuffers(1, &e->vbo); e->vbo = 0; } + if(e->vao) { glDeleteVertexArrays(1, &e->vao); e->vao = 0; } + } + glChunkNext = 1; +} + +rtilemapchunk_t renderGLTilemapChunkCreate( + uint16_t chunkW, uint16_t chunkH, + uint16_t tileW, uint16_t tileH, + rtexture_t tileset, + const uint8_t *tileIndices +) { + assertTrue(glChunkNext < RTILEMAPCHUNK_MAX, "Tilemap chunk table full"); + assertTrue(tileW > 0 && tileH > 0, "Tile dimensions must be > 0"); + + uint16_t texW, texH; + renderGLTextureGetSize(tileset, &texW, &texH); + assertTrue(texW >= tileW && texH >= tileH, "Tileset smaller than one tile"); + + uint16_t tilesPerRow = texW / tileW; + float fTexW = (float)texW; + float fTexH = (float)texH; + float fTileW = (float)tileW; + float fTileH = (float)tileH; + + rtilemapchunk_t handle = (rtilemapchunk_t)glChunkNext++; + glchunkentry_t *e = &glChunkTable[handle]; + e->tileset = tileset; + + uint32_t tileCount = (uint32_t)chunkW * chunkH; + e->vertCount = tileCount * 6; + + /* Temporary CPU buffer: 6 verts × 8 floats (x y r g b a u v) per tile */ + GLfloat *verts = (GLfloat *)malloc(e->vertCount * 8 * sizeof(GLfloat)); + assertNotNull(verts, "GL: failed to allocate tilemap chunk vertex buffer"); + + uint32_t v = 0; + for(uint32_t ci = 0; ci < tileCount; ci++) { + uint8_t idx = tileIndices[ci]; + uint16_t tileCol = idx % tilesPerRow; + uint16_t tileRow = idx / tilesPerRow; + + float px0 = (float)((ci % chunkW) * tileW); + float py0 = (float)((ci / chunkW) * tileH); + float px1 = px0 + fTileW; + float py1 = py0 + fTileH; + + float u0 = (float)(tileCol * tileW) / fTexW; + float v0 = (float)(tileRow * tileH) / fTexH; + float u1 = u0 + fTileW / fTexW; + float v1 = v0 + fTileH / fTexH; + +#define V(px,py,uu,vv) \ + verts[v*8+0]=(px); verts[v*8+1]=(py); \ + verts[v*8+2]=1.0f; verts[v*8+3]=1.0f; \ + verts[v*8+4]=1.0f; verts[v*8+5]=1.0f; \ + verts[v*8+6]=(uu); verts[v*8+7]=(vv); v++ + + V(px0, py1, u0, v1); + V(px0, py0, u0, v0); + V(px1, py0, u1, v0); + V(px0, py1, u0, v1); + V(px1, py0, u1, v0); + V(px1, py1, u1, v1); +#undef V + } + + glGenVertexArrays(1, &e->vao); + glGenBuffers(1, &e->vbo); + glBindVertexArray(e->vao); + glBindBuffer(GL_ARRAY_BUFFER, e->vbo); + glBufferData( + GL_ARRAY_BUFFER, + (GLsizeiptr)(e->vertCount * 8 * sizeof(GLfloat)), + verts, + GL_STATIC_DRAW + ); + /* layout: aPos(2) aColor(4) aUV(2) — matches the 2D shader */ + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (void*)0); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (void*)(2*sizeof(GLfloat))); + glEnableVertexAttribArray(1); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (void*)(6*sizeof(GLfloat))); + glEnableVertexAttribArray(2); + glBindVertexArray(0); + + free(verts); + return handle; +} + +void renderGLTilemapChunkDispose(rtilemapchunk_t chunk) { + if(chunk == RTILEMAPCHUNK_INVALID || chunk >= RTILEMAPCHUNK_MAX) return; + glchunkentry_t *e = &glChunkTable[chunk]; + if(e->vbo) { glDeleteBuffers(1, &e->vbo); e->vbo = 0; } + if(e->vao) { glDeleteVertexArrays(1, &e->vao); e->vao = 0; } +} + +const glchunkentry_t *renderGLTilemapChunkGet(rtilemapchunk_t chunk) { + if(chunk == RTILEMAPCHUNK_INVALID || chunk >= RTILEMAPCHUNK_MAX) return NULL; + if(!glChunkTable[chunk].vao) return NULL; + return &glChunkTable[chunk]; +} diff --git a/src/duskgl/display/render/rendertilemapcgl.h b/src/duskgl/display/render/rendertilemapcgl.h new file mode 100644 index 00000000..6e0375e7 --- /dev/null +++ b/src/duskgl/display/render/rendertilemapcgl.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 "duskgl.h" +#include "display/render/rtilemapchunk.h" +#include "display/render/rtexture.h" + +#define RTILEMAPCHUNK_MAX 256 + +/* Opaque GL state for one pre-built chunk. */ +typedef struct { + GLuint vao, vbo; + uint32_t vertCount; + rtexture_t tileset; +} glchunkentry_t; + +void renderGLTilemapChunkTableDispose(void); +rtilemapchunk_t renderGLTilemapChunkCreate( + uint16_t chunkW, uint16_t chunkH, + uint16_t tileW, uint16_t tileH, + rtexture_t tileset, + const uint8_t *tileIndices +); +void renderGLTilemapChunkDispose(rtilemapchunk_t chunk); +const glchunkentry_t *renderGLTilemapChunkGet(rtilemapchunk_t chunk); diff --git a/src/duskgl/render/rendergl.c b/src/duskgl/render/rendergl.c deleted file mode 100644 index d65dccf5..00000000 --- a/src/duskgl/render/rendergl.c +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "render/rendergl.h" -#include "error/errorgl.h" -#include "render/rop.h" - -static const char *VERT_SRC = - "#version 330 core\n" - "layout(location=0) in vec2 aPos;\n" - "layout(location=1) in vec4 aColor;\n" - "uniform vec2 uRes;\n" - "out vec4 vColor;\n" - "void main() {\n" - " vec2 clip = (aPos / uRes) * 2.0 - 1.0;\n" - " clip.y = -clip.y;\n" - " gl_Position = vec4(clip, 0.0, 1.0);\n" - " vColor = aColor;\n" - "}\n"; - -static const char *FRAG_SRC = - "#version 330 core\n" - "in vec4 vColor;\n" - "out vec4 fragColor;\n" - "void main() {\n" - " fragColor = vColor;\n" - "}\n"; - -typedef struct { - GLuint prog; - GLuint vao; - GLuint vbo; - GLint uRes; -} rendergl_t; - -static rendergl_t renderGL; - -static GLuint compileShader(GLenum type, const char *src) { - GLuint s = glCreateShader(type); - glShaderSource(s, 1, &src, NULL); - glCompileShader(s); - return s; -} - -errorret_t renderGLInit(void) { - GLuint vert = compileShader(GL_VERTEX_SHADER, VERT_SRC); - GLuint frag = compileShader(GL_FRAGMENT_SHADER, FRAG_SRC); - - renderGL.prog = glCreateProgram(); - glAttachShader(renderGL.prog, vert); - glAttachShader(renderGL.prog, frag); - glLinkProgram(renderGL.prog); - glDeleteShader(vert); - glDeleteShader(frag); - errorChain(errorGLCheck()); - - renderGL.uRes = glGetUniformLocation(renderGL.prog, "uRes"); - - glGenVertexArrays(1, &renderGL.vao); - glGenBuffers(1, &renderGL.vbo); - - glBindVertexArray(renderGL.vao); - glBindBuffer(GL_ARRAY_BUFFER, renderGL.vbo); - /* 6 verts * (2 pos + 4 color) floats — enough for one sprite */ - glBufferData(GL_ARRAY_BUFFER, 6 * 6 * sizeof(GLfloat), NULL, GL_DYNAMIC_DRAW); - - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (void*)(2*sizeof(GLfloat))); - glEnableVertexAttribArray(1); - - glBindVertexArray(0); - errorChain(errorGLCheck()); - errorOk(); -} - -static void drawSprite(const ropsprite_t *s, float rW, float rH) { - float r = s->tint.r / 255.0f; - float g = s->tint.g / 255.0f; - float b = s->tint.b / 255.0f; - float a = s->tint.a / 255.0f; - - float x0 = (float)s->x; - float y0 = (float)s->y; - float x1 = x0 + (float)s->w; - float y1 = y0 + (float)s->h; - - GLfloat verts[6][6] = { - { x0, y1, r,g,b,a }, - { x0, y0, r,g,b,a }, - { x1, y0, r,g,b,a }, - { x0, y1, r,g,b,a }, - { x1, y0, r,g,b,a }, - { x1, y1, r,g,b,a }, - }; - - glBindVertexArray(renderGL.vao); - glBindBuffer(GL_ARRAY_BUFFER, renderGL.vbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts); - glDrawArrays(GL_TRIANGLES, 0, 6); - (void)rW; (void)rH; -} - -errorret_t renderGLFlush(ropbuffer_t *buf, int winW, int winH) { - glViewport(0, 0, winW, winH); - errorChain(errorGLCheck()); - - glUseProgram(renderGL.prog); - glUniform2f(renderGL.uRes, (GLfloat)winW, (GLfloat)winH); - errorChain(errorGLCheck()); - - for(uint32_t i = 0; i < buf->count; i++) { - const ropheader_t *hdr = (const ropheader_t *)(buf->data + i * ROP_SIZE); - switch(hdr->op) { - case ROP_CLEAR: { - const ropclear_t *c = (const ropclear_t *)hdr; - glClearColor( - c->color.r / 255.0f, - c->color.g / 255.0f, - c->color.b / 255.0f, - c->color.a / 255.0f - ); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - errorChain(errorGLCheck()); - break; - } - - case ROP_DRAW_SPRITE: { - const ropsprite_t *s = (const ropsprite_t *)hdr; - drawSprite(s, (float)winW, (float)winH); - errorChain(errorGLCheck()); - break; - } - - default: - break; - } - } - - glUseProgram(0); - glBindVertexArray(0); - errorOk(); -} - -void renderGLDispose(void) { - if(renderGL.vbo) { glDeleteBuffers(1, &renderGL.vbo); renderGL.vbo = 0; } - if(renderGL.vao) { glDeleteVertexArrays(1, &renderGL.vao); renderGL.vao = 0; } - if(renderGL.prog) { glDeleteProgram(renderGL.prog); renderGL.prog = 0; } -} diff --git a/src/dusklinux/display/render/renderplatform.h b/src/dusklinux/display/render/renderplatform.h new file mode 100644 index 00000000..5d227c10 --- /dev/null +++ b/src/dusklinux/display/render/renderplatform.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 "display/render/rendertexturegl.h" +#include "display/render/rendertilemapcgl.h" + +#define renderPlatformTextureCreate renderGLTextureCreate +#define renderPlatformTextureDispose renderGLTextureDispose +#define renderPlatformTextureGetPalette renderGLTextureGetPalette +#define renderPlatformTextureGetIndices renderGLTextureGetIndices + +#define renderPlatformTilemapChunkCreate renderGLTilemapChunkCreate +#define renderPlatformTilemapChunkDispose renderGLTilemapChunkDispose diff --git a/src/duskpsp/CMakeLists.txt b/src/duskpsp/CMakeLists.txt index ca661aa5..67fa4ef3 100644 --- a/src/duskpsp/CMakeLists.txt +++ b/src/duskpsp/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME} # Subdirs add_subdirectory(asset) +add_subdirectory(display) add_subdirectory(input) add_subdirectory(log) add_subdirectory(network) diff --git a/src/duskpsp/display/CMakeLists.txt b/src/duskpsp/display/CMakeLists.txt new file mode 100644 index 00000000..456904e2 --- /dev/null +++ b/src/duskpsp/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_LIBRARY_TARGET_NAME} + PUBLIC + displaypsp.c +) + +add_subdirectory(render) diff --git a/src/duskpsp/display/displayplatform.h b/src/duskpsp/display/displayplatform.h index 57dbf17e..5d2a233c 100644 --- a/src/duskpsp/display/displayplatform.h +++ b/src/duskpsp/display/displayplatform.h @@ -6,12 +6,11 @@ */ #pragma once -#include "display/displaysdl2.h" +#include "display/displaypsp.h" -typedef displaysdl2_t displayplatform_t; +typedef displaypsp_t displayplatform_t; -#define displayPlatformInit displaySDL2Init -#define displayPlatformUpdate displaySDL2Update -#define displayPlatformSwap displaySDL2Swap -#define displayPlatformDispose displaySDL2Dispose -#define displayPlatformSetState displaySDL2SetState \ No newline at end of file +#define displayPlatformInit displayPSPInit +#define displayPlatformFlush displayPSPFlush +#define displayPlatformSwap displayPSPSwap +#define displayPlatformDispose displayPSPDispose \ No newline at end of file diff --git a/src/duskpsp/display/displaypsp.c b/src/duskpsp/display/displaypsp.c new file mode 100644 index 00000000..cf470b46 --- /dev/null +++ b/src/duskpsp/display/displaypsp.c @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/displaypsp.h" +#include "display/render/renderpsp.h" +#include "display/display.h" +#include "assert/assert.h" +#include +#include +#include +#include + +#define FRAME_SIZE (PSP_SCREEN_W * PSP_SCREEN_H * 4) +#define VRAM_ADDR(offset) ((void *)((uintptr_t)sceGeEdramGetAddr() + (offset))) + +static uint32_t __attribute__((aligned(64))) displayList[0x10000]; + +errorret_t displayPSPInit(void) { + 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); + + sceGuOffset(2048 - PSP_SCREEN_W / 2, 2048 - PSP_SCREEN_H / 2); + sceGuViewport(2048, 2048, PSP_SCREEN_W, PSP_SCREEN_H); + sceGuDepthRange(65535, 0); /* PSP uses reversed depth */ + sceGuScissor(0, 0, PSP_SCREEN_W, PSP_SCREEN_H); + sceGuEnable(GU_SCISSOR_TEST); + + sceGuEnable(GU_ALPHA_TEST); + sceGuAlphaFunc(GU_GREATER, 0, 0xFF); + + sceGuEnable(GU_DEPTH_TEST); + sceGuDepthFunc(GU_GEQUAL); + + sceGuFrontFace(GU_CW); + + sceGuEnable(GU_TEXTURE_2D); + sceGuEnable(GU_CLIP_PLANES); + + sceGuFinish(); + sceGuSync(0, 0); + + sceDisplaySetMode(0, PSP_SCREEN_W, PSP_SCREEN_H); + sceGuDisplay(GU_TRUE); + + errorChain(renderPSPInit()); + errorOk(); +} + +errorret_t displayPSPFlush(ropbuffer_t *buf) { + assertNotNull(buf, "PSP flush: null ropbuffer"); + errorChain(renderPSPFlush(buf)); + errorOk(); +} + +errorret_t displayPSPSwap(void) { + sceGuSwapBuffers(); + errorOk(); +} + +void displayPSPDispose(void) { + renderPSPDispose(); + sceGuDisplay(GU_FALSE); + sceGuTerm(); +} diff --git a/src/duskpsp/display/displaypsp.h b/src/duskpsp/display/displaypsp.h new file mode 100644 index 00000000..0ce870d5 --- /dev/null +++ b/src/duskpsp/display/displaypsp.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 PSP_SCREEN_W 480 +#define PSP_SCREEN_H 272 + +typedef struct { + int_t whichBuffer; +} displaypsp_t; + +errorret_t displayPSPInit(void); +errorret_t displayPSPFlush(ropbuffer_t *buf); +errorret_t displayPSPSwap(void); +void displayPSPDispose(void); diff --git a/src/duskgl/render/CMakeLists.txt b/src/duskpsp/display/render/CMakeLists.txt similarity index 91% rename from src/duskgl/render/CMakeLists.txt rename to src/duskpsp/display/render/CMakeLists.txt index 5261e791..2c6236e8 100644 --- a/src/duskgl/render/CMakeLists.txt +++ b/src/duskpsp/display/render/CMakeLists.txt @@ -5,5 +5,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - rendergl.c + renderpsp.c ) diff --git a/src/duskpsp/display/render/renderplatform.h b/src/duskpsp/display/render/renderplatform.h new file mode 100644 index 00000000..b8fa7194 --- /dev/null +++ b/src/duskpsp/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/renderpsp.h" + +#define renderPlatformTextureCreate renderPSPTextureCreate +#define renderPlatformTextureDispose renderPSPTextureDispose +#define renderPlatformTextureGetPalette renderPSPTextureGetPalette +#define renderPlatformTextureGetIndices renderPSPTextureGetIndices diff --git a/src/duskpsp/display/render/renderpsp.c b/src/duskpsp/display/render/renderpsp.c new file mode 100644 index 00000000..6e425d52 --- /dev/null +++ b/src/duskpsp/display/render/renderpsp.c @@ -0,0 +1,318 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/render/renderpsp.h" +#include "display/render/rop.h" +#include "display/color.h" +#include "assert/assert.h" +#include "util/memory.h" +#include +#include /* sceKernelDcacheWritebackRange */ +#include +#include +#include + +/* ---- Display list -------------------------------------------------------- */ + +#define DISPLAY_LIST_SIZE (256 * 1024) + +static uint32_t __attribute__((aligned(64))) displayList[DISPLAY_LIST_SIZE / 4]; + +/* ---- Texture table ------------------------------------------------------- */ + +#define PSP_RTEXTURE_MAX 256 + +/* 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. + * 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. */ +typedef struct { + uint8_t *cpuIndices; + uint8_t *gpuIndices; + color_t palette[256]; + uint16_t w, h, tbw; +} psptexentry_t; + +/* Shared 16-byte-aligned ABGR buffer used during every CLUT load. */ +static uint32_t __attribute__((aligned(16))) pspAbgrBuf[256]; + +static psptexentry_t pspTexTable[PSP_RTEXTURE_MAX]; +static uint16_t pspTexNext = 1; /* 0 = white fallback */ + +/* ---- Vertex types -------------------------------------------------------- */ + +/* 2D sprite: two corner vertices define the rect (GU_SPRITES uses 2 verts). */ +typedef struct { + uint16_t u, v; /* texel coords (integer, not normalised) */ + uint32_t color; /* ABGR */ + int16_t x, y, z; +} __attribute__((packed)) GuVert2D; + +/* 3D triangle vertex */ +typedef struct { + float u, v; + uint32_t color; /* ABGR */ + float x, y, z; +} __attribute__((packed)) GuVert3D; + +/* ---- Helpers ------------------------------------------------------------- */ + +static uint32_t toABGR(color_t c) { + return ((uint32_t)c.a << 24) | + ((uint32_t)c.b << 16) | + ((uint32_t)c.g << 8) | + ((uint32_t)c.r); +} + +/* Smallest power-of-two ≥ n and ≥ 8 (PSP GU minimum stride for T8). */ +static uint16_t texturePow2(uint16_t n) { + uint16_t p = 8; + while(p < n) p = (uint16_t)(p << 1); + return p; +} + +/* ---- Init ---------------------------------------------------------------- */ + +errorret_t renderPSPInit(void) { + /* White 1×1 fallback: index 0 → palette[0] = white */ + psptexentry_t *e = &pspTexTable[0]; + e->cpuIndices = (uint8_t *)memalign(16, 1); + assertNotNull(e->cpuIndices, "PSP: failed to allocate fallback cpu index buffer"); + e->cpuIndices[0] = 0; + + e->gpuIndices = (uint8_t *)memalign(16, 8); /* tbw=8 minimum */ + assertNotNull(e->gpuIndices, "PSP: failed to allocate fallback gpu index buffer"); + memoryZero(e->gpuIndices, 8); + + memoryZero(e->palette, 256 * sizeof(color_t)); + e->palette[0] = COLOR_WHITE; + e->w = 1; e->h = 1; e->tbw = 8; + errorOk(); +} + +/* ---- Texture ------------------------------------------------------------- */ + +rtexture_t renderPSPTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +) { + assertTrue(pspTexNext < PSP_RTEXTURE_MAX, "PSP texture table full"); + + uint16_t tbw = texturePow2(w); + rtexture_t handle = (rtexture_t)pspTexNext++; + psptexentry_t *e = &pspTexTable[handle]; + + uint32_t cpuBytes = (uint32_t)w * h; + e->cpuIndices = (uint8_t *)memalign(16, cpuBytes); + assertNotNull(e->cpuIndices, "PSP: failed to allocate cpu index buffer"); + memoryCopy(e->cpuIndices, indices, cpuBytes); + + uint32_t gpuBytes = (uint32_t)tbw * h; + e->gpuIndices = (uint8_t *)memalign(16, gpuBytes); + assertNotNull(e->gpuIndices, "PSP: failed to allocate gpu index buffer"); + memoryZero(e->gpuIndices, gpuBytes); + for(uint16_t row = 0; row < h; row++) + 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; + return handle; +} + +void renderPSPTextureDispose(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= PSP_RTEXTURE_MAX) return; + psptexentry_t *e = &pspTexTable[tex]; + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } + if(e->gpuIndices) { free(e->gpuIndices); e->gpuIndices = NULL; } +} + +color_t *renderPSPTextureGetPalette(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= PSP_RTEXTURE_MAX) return NULL; + return pspTexTable[tex].palette; +} + +uint8_t *renderPSPTextureGetIndices(rtexture_t tex) { + if(tex == RTEXTURE_NONE || tex >= PSP_RTEXTURE_MAX) return NULL; + return pspTexTable[tex].cpuIndices; +} + +static void bindTexture(rtexture_t tex) { + psptexentry_t *e = (tex < PSP_RTEXTURE_MAX && pspTexTable[tex].cpuIndices) + ? &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); + + /* 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)); + + sceGuTexMode(GU_PSM_T8, 0, 0, GU_FALSE); + sceGuClutMode(GU_PSM_8888, 0, 0xFF, 0); + sceGuClutLoad(32, pspAbgrBuf); /* 32 × 8 entries = 256 */ + sceGuTexImage(0, e->tbw, e->h, e->tbw, e->gpuIndices); + sceGuTexFunc(GU_TFX_MODULATE, GU_TCC_RGBA); + sceGuTexFilter(GU_NEAREST, GU_NEAREST); + sceGuTexScale(1.0f, 1.0f); + sceGuTexOffset(0.0f, 0.0f); +} + +/* ---- Draw helpers -------------------------------------------------------- */ + +static void draw2DSprite(const ropsprite_t *s) { + psptexentry_t *e = (s->texture < PSP_RTEXTURE_MAX && pspTexTable[s->texture].cpuIndices) + ? &pspTexTable[s->texture] + : &pspTexTable[0]; + uint32_t abgr = toABGR(s->tint); + float u0 = (s->uvX / 255.0f) * (float)e->w; + float v0 = (s->uvY / 255.0f) * (float)e->h; + float u1 = ((s->uvX + s->uvW) / 255.0f) * (float)e->w; + float v1 = ((s->uvY + s->uvH) / 255.0f) * (float)e->h; + + bindTexture(s->texture); + + 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].color = abgr; + verts[0].x = s->x; verts[0].y = s->y; verts[0].z = 0; + + verts[1].u = (uint16_t)u1; verts[1].v = (uint16_t)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; + + sceGuDrawArray( + GU_SPRITES, + GU_TEXTURE_16BIT | GU_COLOR_8888 | GU_VERTEX_16BIT | GU_TRANSFORM_2D, + 2, 0, verts + ); +} + +static void draw3DQuad(const ropquad3d_t *q) { + 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; + + 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; + + 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; + + bindTexture(q->texture); + + GuVert3D *verts = (GuVert3D *)sceGuGetMemory(6 * sizeof(GuVert3D)); + assertNotNull(verts, "PSP: failed to allocate 3D quad vertices"); + + verts[0] = (GuVert3D){u0,v0, abgr, tlx,tly,tlz}; + verts[1] = (GuVert3D){u0,v1, abgr, blx,bly,blz}; + verts[2] = (GuVert3D){u1,v1, abgr, brx,bry,brz}; + verts[3] = (GuVert3D){u0,v0, abgr, tlx,tly,tlz}; + verts[4] = (GuVert3D){u1,v1, abgr, brx,bry,brz}; + verts[5] = (GuVert3D){u1,v0, abgr, trx,try_,trz}; + + sceGuDrawArray( + GU_TRIANGLES, + GU_TEXTURE_32BITF | GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D, + 6, 0, verts + ); +} + +/* ---- Flush --------------------------------------------------------------- */ + +errorret_t renderPSPFlush(ropbuffer_t *buf) { + sceGuStart(GU_DIRECT, displayList); + + sceGuEnable(GU_TEXTURE_2D); + sceGuEnable(GU_DEPTH_TEST); + sceGuDepthFunc(GU_GEQUAL); /* PSP uses reversed depth */ + + 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; + uint32_t abgr = toABGR(c->color); + sceGuClearColor(abgr); + sceGuClearDepth(0); + sceGuClear(GU_COLOR_BUFFER_BIT | GU_DEPTH_BUFFER_BIT); + break; + } + case ROP_DRAW_SPRITE: + draw2DSprite((const ropsprite_t *)hdr); + break; + + case ROP_SET_PROJECTION: { + const ropprojection_t *p = (const ropprojection_t *)hdr; + sceGumMatrixMode(GU_PROJECTION); + sceGumLoadIdentity(); + if(p->fovY > FIXED_ZERO) { + float fovDeg = fixedToFloat(p->fovY) * (180.0f / 3.14159f); + sceGumPerspective( + fovDeg, fixedToFloat(p->aspect), + fixedToFloat(p->nearZ), fixedToFloat(p->farZ) + ); + } else { + float aspect = fixedToFloat(p->aspect); + sceGumOrtho(-aspect, aspect, -1.0f, 1.0f, + fixedToFloat(p->nearZ), fixedToFloat(p->farZ)); + } + break; + } + case ROP_SET_VIEW: { + 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}; + ScePspFVector3 up = {0.0f, 1.0f, 0.0f}; + sceGumMatrixMode(GU_VIEW); + sceGumLoadIdentity(); + sceGumLookAt(&eye, &target, &up); + sceGumMatrixMode(GU_MODEL); + sceGumLoadIdentity(); + break; + } + case ROP_DRAW_QUAD_3D: + draw3DQuad((const ropquad3d_t *)hdr); + break; + + default: + break; + } + offset += ropOpSize(op); + } + + sceGuFinish(); + sceGuSync(0, 0); + errorOk(); +} + +/* ---- Dispose ------------------------------------------------------------- */ + +void renderPSPDispose(void) { + for(uint16_t i = 0; i < pspTexNext; i++) { + psptexentry_t *e = &pspTexTable[i]; + if(e->cpuIndices) { free(e->cpuIndices); e->cpuIndices = NULL; } + if(e->gpuIndices) { free(e->gpuIndices); e->gpuIndices = NULL; } + } + pspTexNext = 1; +} diff --git a/src/duskpsp/display/render/renderpsp.h b/src/duskpsp/display/render/renderpsp.h new file mode 100644 index 00000000..9b8b7d6b --- /dev/null +++ b/src/duskpsp/display/render/renderpsp.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 renderPSPInit(void); +errorret_t renderPSPFlush(ropbuffer_t *buf); +void renderPSPDispose(void); + +rtexture_t renderPSPTextureCreate( + uint16_t w, uint16_t h, + const uint8_t *indices, const color_t *palette +); +void renderPSPTextureDispose(rtexture_t tex); +color_t *renderPSPTextureGetPalette(rtexture_t tex); +uint8_t *renderPSPTextureGetIndices(rtexture_t tex); diff --git a/src/dusksdl2/display/CMakeLists.txt b/src/dusksdl2/display/CMakeLists.txt index 8db4dd71..034c5039 100644 --- a/src/dusksdl2/display/CMakeLists.txt +++ b/src/dusksdl2/display/CMakeLists.txt @@ -3,10 +3,12 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - displaysdl2.c -) +# Sources — PSP uses its own native GU display; SDL2 display not compiled there +if(NOT DUSK_TARGET_SYSTEM STREQUAL "psp") + target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + displaysdl2.c + ) +endif() # Subdirs \ No newline at end of file diff --git a/src/dusksdl2/display/displaysdl2.c b/src/dusksdl2/display/displaysdl2.c index 31ae7766..d66edc33 100644 --- a/src/dusksdl2/display/displaysdl2.c +++ b/src/dusksdl2/display/displaysdl2.c @@ -7,7 +7,7 @@ #include "display/display.h" #include "engine/engine.h" -#include "render/rendergl.h" +#include "display/render/rendergl.h" #include "error/errorgl.h" errorret_t displaySDL2Init(void) { diff --git a/src/dusksdl2/display/displaysdl2.h b/src/dusksdl2/display/displaysdl2.h index ca5caf42..3b60aedb 100644 --- a/src/dusksdl2/display/displaysdl2.h +++ b/src/dusksdl2/display/displaysdl2.h @@ -8,7 +8,7 @@ #pragma once #include "error/error.h" #include "display/displaystate.h" -#include "render/ropbuffer.h" +#include "display/render/ropbuffer.h" typedef struct { SDL_Window *window; diff --git a/src/dusksdl2/dusksdl2.h b/src/dusksdl2/dusksdl2.h index 44a23c2c..8c18f526 100644 --- a/src/dusksdl2/dusksdl2.h +++ b/src/dusksdl2/dusksdl2.h @@ -7,4 +7,6 @@ #pragma once #include -#include "duskgl.h" \ No newline at end of file +#ifdef DUSK_OPENGL + #include "duskgl.h" +#endif \ No newline at end of file diff --git a/src/dusksdl2/input/inputsdl2.c b/src/dusksdl2/input/inputsdl2.c index e51b8ee6..d20b8a86 100644 --- a/src/dusksdl2/input/inputsdl2.c +++ b/src/dusksdl2/input/inputsdl2.c @@ -8,18 +8,48 @@ #include "input/input.h" #include "assert/assert.h" #include "display/display.h" +#include "engine/engine.h" void inputUpdateSDL2(void) { + #ifdef DUSK_INPUT_POINTER + INPUT.platform.scrollX = 0.0f; + INPUT.platform.scrollY = 0.0f; + #endif + + SDL_Event e; + while(SDL_PollEvent(&e)) { + switch(e.type) { + case SDL_QUIT: + ENGINE.running = false; + break; + + #ifdef DUSK_INPUT_POINTER + case SDL_MOUSEWHEEL: + INPUT.platform.scrollX = (float_t)e.wheel.x; + INPUT.platform.scrollY = (float_t)e.wheel.y; + break; + #endif + + #ifdef DUSK_INPUT_GAMEPAD + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + break; + #endif + + default: + break; + } + } + #ifdef DUSK_INPUT_GAMEPAD INPUT.platform.controller = NULL; - for(int32_t i = 0; i < SDL_NumJoysticks(); i++) { if(!SDL_IsGameController(i)) continue; INPUT.platform.controller = SDL_GameControllerOpen(i); if(INPUT.platform.controller) break; } #endif - + #ifdef DUSK_INPUT_KEYBOARD INPUT.platform.keyboardState = SDL_GetKeyboardState(NULL); #endif @@ -33,8 +63,6 @@ void inputUpdateSDL2(void) { INPUT.platform.mouseX = (float_t)pointerX / (float_t)windowWidth; INPUT.platform.mouseY = (float_t)pointerY / (float_t)windowHeight; - INPUT.platform.scrollX = 0.0f; - INPUT.platform.scrollY = 0.0f; #endif }