diff --git a/.claude/animation.md b/.claude/animation.md new file mode 100644 index 00000000..fdcfa481 --- /dev/null +++ b/.claude/animation.md @@ -0,0 +1,88 @@ +# Animation System + +Source: `src/dusk/animation/` + +## Overview + +The animation system provides time-based keyframe interpolation with +pluggable easing functions. It is intentionally minimal -- no skeleton, +no blending, no state machine. Animations produce a single `float_t` +value at a given time, which callers apply to whatever property they +are animating. + +## Keyframes (`keyframe.h`) + +```c +typedef struct { + float_t time; // time in seconds this keyframe is at + float_t value; // the value at this keyframe + easingtype_t easing; // easing applied between this frame and the next +} keyframe_t; +``` + +## Animation (`animation.h`) + +```c +typedef struct { + keyframe_t *keyframes; // caller-owned array + uint16_t keyframeCount; +} animation_t; + +void animationInit( + animation_t *anim, + keyframe_t *keyframes, + uint16_t keyframeCount +); + +float_t animationGetValue(animation_t *anim, float_t time); +// Returns the interpolated value at the given time. +// Before the first keyframe: returns the first keyframe's value. +// After the last keyframe: returns the last keyframe's value. +``` + +## Easing functions (`easing.h`) + +```c +typedef float_t (*easingfn_t)(float_t t); // t in [0, 1], out in [0, 1] + +extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT]; + +float_t easingApply(easingtype_t type, float_t t); +``` + +Available easing types: + +``` +EASING_LINEAR +EASING_IN_SINE EASING_OUT_SINE EASING_IN_OUT_SINE +EASING_IN_QUAD EASING_OUT_QUAD EASING_IN_OUT_QUAD +EASING_IN_CUBIC EASING_OUT_CUBIC EASING_IN_OUT_CUBIC +EASING_IN_QUART EASING_OUT_QUART EASING_IN_OUT_QUART +EASING_IN_BACK EASING_OUT_BACK EASING_IN_OUT_BACK +``` + +## Usage pattern + +```c +// Declare keyframes statically (no allocation): +static keyframe_t kfs[] = { + { .time = 0.0f, .value = 0.0f, .easing = EASING_OUT_CUBIC }, + { .time = 1.0f, .value = 1.0f, .easing = EASING_LINEAR }, +}; + +animation_t anim; +animationInit(&anim, kfs, 2); + +// In update loop: +float_t alpha = animationGetValue(&anim, TIME.time); +// Apply alpha to whatever is being animated. +``` + +## Design notes + +- Keyframe arrays are caller-owned and not copied. Use static or + long-lived arrays; do not allocate per-frame. +- The system has no notion of looping -- wrap `time` with `fmodf` if + you need a repeating animation. +- For multi-property animations, use multiple `animation_t` instances + sharing the same time source. diff --git a/.claude/assets.md b/.claude/assets.md new file mode 100644 index 00000000..7b2b247b --- /dev/null +++ b/.claude/assets.md @@ -0,0 +1,125 @@ +# Asset System + +Source: `src/dusk/asset/` + +## Overview + +All game assets are packed into a single ZIP archive named `dusk.dsk` +(`ASSET_FILE_NAME`). The asset system loads entries from this archive +asynchronously on a background thread, caches them, and provides +synchronous blocking access when an asset is required immediately. + +## Key limits + +| Constant | Value | Meaning | +|----------|-------|---------| +| `ASSET_LOADING_COUNT_MAX` | 4 | Concurrent in-flight loads | +| `ASSET_ENTRY_COUNT_MAX` | 128 | Cached entries | + +## Top-level API (`asset.h`) + +```c +errorret_t assetInit(); // Open dusk.dsk, start background thread +void assetUpdate(); // Dispatch completed-load callbacks (main thread) +errorret_t assetDispose(); // Wait for loads, close archive + +assetentry_t *assetGetEntry( + const char_t *path, + assetloadertype_t type, + assetloaderinput_t *input +); // Get (or create) a cache entry; does NOT start loading + +errorret_t assetRequireLoaded(assetentry_t *entry); +// Block the calling thread until this entry is fully loaded. +// Only safe to call from the main thread. + +void assetLock(assetentry_t *entry); +void assetUnlock(assetentry_t *entry); +// Reference counting. Lock before using loaded data; unlock when done. +// The entry will not be evicted while locked. +``` + +## Asset entry states + +Each cache entry goes through a state machine: + +``` +IDLE -> QUEUED -> READING (async) -> PROCESSING (sync, main thread) -> LOADED + -> ERROR +``` + +- **READING** runs on the background loader thread (file I/O). +- **PROCESSING** runs on the main thread (GPU uploads, parsing finalization). +- Once LOADED, data is available in `entry->data`. + +## Loader types + +Loader types are registered in the `ASSET_LOADER_CALLBACKS[]` table. +Each type implements three callbacks: `loadSync`, `loadAsync`, `dispose`. + +| `assetloadertype_t` | Data read | Description | +|---------------------|-----------|-------------| +| `ASSET_LOADER_TYPE_TEXTURE` | STB image | Loads image bytes async, creates GPU texture sync | +| `ASSET_LOADER_TYPE_TILESET` | `.dtf` binary | Custom tile format (magic, version, grid, UVs) | +| `ASSET_LOADER_TYPE_MESH` | `.stl` | STL mesh with configurable axis orientation | +| `ASSET_LOADER_TYPE_JSON` | yyjson | Up to 256 KB; parsed async | +| `ASSET_LOADER_TYPE_LOCALE` | Gettext `.po` | PO parser with plural-form expression evaluation | +| `ASSET_LOADER_TYPE_SCRIPT` | JS source | JerryScript module | + +## Adding a new loader type + +1. Add an enum value before `_COUNT` in `assetloadertype_t` + (`src/dusk/asset/loader/assetloader.h`). +2. Add fields to the input/loading/output unions in `assetloader.h`. +3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and + `assetXxxDispose` in `src/dusk/asset/loader/xxx/`. +4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in + `src/dusk/asset/loader/assetloader.c`. +5. If user-facing, create a JS module and a `.d.ts` file (see `CLAUDE.md`). + +## Asset batch (`assetbatch.h`) + +`assetbatch_t` groups multiple asset requests into a single logical +load. All entries in the batch start loading concurrently. The batch +fires a completion callback once every entry has reached LOADED (or +ERROR). + +```c +assetbatch_t batch; +assetBatchInit(&batch, entries, count, onComplete, user); +assetBatchStart(&batch); +// ... later, after assetUpdate() fires the callback ... +assetBatchDispose(&batch); +``` + +## Usage pattern + +```c +// 1. Get or create the cache entry (no I/O yet). +assetentry_t *tex = assetGetEntry( + "textures/hero.png", + ASSET_LOADER_TYPE_TEXTURE, + NULL +); +assetLock(tex); + +// 2. Option A -- non-blocking: check tex->state each frame. +// Option B -- blocking (main thread only): +errorChain(assetRequireLoaded(tex)); + +// 3. Use the loaded data. +texture_t *t = &tex->data.texture; + +// 4. Release when done. +assetUnlock(tex); +``` + +## Error macros (inside loader implementations) + +```c +assetLoaderErrorThrow("msg %d", val); // errorThrow equivalent +assetLoaderErrorChain(someCall()); // errorChain equivalent +``` + +Use these instead of the bare error macros inside loader callbacks so +that failures include the loader context in the stack trace. diff --git a/.claude/build.md b/.claude/build.md new file mode 100644 index 00000000..8510b39e --- /dev/null +++ b/.claude/build.md @@ -0,0 +1,85 @@ +# Build System + +Dusk uses CMake exclusively. Every source subdirectory owns its own +`CMakeLists.txt`; the root file only wires them together. + +## Golden rule + +**Never add source files to the root `CMakeLists.txt` directly.** + +Every `.c` file is registered in the `CMakeLists.txt` that lives in +the same directory (or a direct parent within the same module): + +```cmake +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + myfile.c +) +``` + +## Configuration variables + +| Variable | Purpose | +|------------------------|----------------------------------------------| +| `DUSK_TARGET_SYSTEM` | Selects the platform (see `.claude/platforms.md`) | +| `DUSK_BUILD_TESTS` | Enables the test suite (`ON` / `OFF`) | +| `CMAKE_TOOLCHAIN_FILE` | Cross-compiler toolchain for console targets | +| `CMAKE_BUILD_TYPE` | `Debug` / `Release` / `RelWithDebInfo` | + +## Typical configure + build + +```sh +# Linux debug build +cmake -B build -DDUSK_TARGET_SYSTEM=linux -DCMAKE_BUILD_TYPE=Debug +cmake --build build + +# Linux with tests +cmake -B build \ + -DDUSK_TARGET_SYSTEM=linux \ + -DDUSK_BUILD_TESTS=ON \ + -DCMAKE_BUILD_TYPE=Debug +cmake --build build +ctest --test-dir build +``` + +## Module layout convention + +Each logical module under `src/` gets its own directory: + +``` +src/dusk/ Platform-agnostic core +src/dusk/ Platform-specific impl (one dir per target) +``` + +Within a module, subdirectories mirror subsystem boundaries +(`asset/`, `entity/`, `script/`, etc.). Each subdirectory has its own +`CMakeLists.txt` that is `add_subdirectory()`-included by its parent. + +## Adding a new source file + +1. Create `src/.../myfile.c` (and `myfile.h` if needed). +2. Open the `CMakeLists.txt` in the same directory. +3. Add `myfile.c` to the `target_sources(...)` block. +4. Do **not** touch any parent or root `CMakeLists.txt`. + +## Platform-conditional sources + +Wrap platform-only files in a generator expression or `if()` block: + +```cmake +if(DUSK_TARGET_SYSTEM STREQUAL "psp") + target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + mypspfile.c + ) +endif() +``` + +## Tests + +- Test files live in `test/` mirroring the `src/dusk/` structure. +- Enable with `-DDUSK_BUILD_TESTS=ON`. +- Uses cmocka; include `dusktest.h` in every test file. +- Every test must assert `memoryGetAllocatedCount() == 0` at teardown + to catch allocator leaks. +- Test function signature: `static void test_something(void **state)` diff --git a/.claude/display-core.md b/.claude/display-core.md new file mode 100644 index 00000000..b97418e0 --- /dev/null +++ b/.claude/display-core.md @@ -0,0 +1,81 @@ +# Display -- Screen, Framebuffer, and Size Modes + +Source: `src/dusk/display/` + +See also: `.claude/display-texture.md`, `.claude/display-shader.md` + +--- + +## Display size modes + +Two compile-time configurations exist: + +### Fixed size (`DUSK_DISPLAY_WIDTH` + `DUSK_DISPLAY_HEIGHT`) + +The render resolution is constant. Set both defines at CMake configure +time. `SCREEN.width` and `SCREEN.height` are compile-time constants. + +### Dynamic size (`DUSK_DISPLAY_SIZE_DYNAMIC`) + +The window can be resized (desktop targets). Instead of fixed defines, +set `DUSK_DISPLAY_WIDTH_DEFAULT` and `DUSK_DISPLAY_HEIGHT_DEFAULT`. +The screen system renders to an internal framebuffer at a logical +resolution and scales/letterboxes to the actual window. + +Screen modes available only with `DUSK_DISPLAY_SIZE_DYNAMIC`: + +| Mode | Behaviour | +|------|-----------| +| `SCREEN_MODE_BACKBUFFER` | Render directly to the window backbuffer | +| `SCREEN_MODE_FIXED_SIZE` | Fixed pixel dimensions; letterboxed | +| `SCREEN_MODE_ASPECT_RATIO` | Maintain aspect ratio at all cost | +| `SCREEN_MODE_FIXED_HEIGHT` | Fixed height; width expands/contracts | +| `SCREEN_MODE_FIXED_WIDTH` | Fixed width; height expands/contracts | +| `SCREEN_MODE_FIXED_VIEWPORT_HEIGHT` | Fixed height at higher resolution | + +Configure via `SCREEN.mode` and the corresponding union field before +calling `screenInit()`. + +--- + +## Framebuffer (`framebuffer.h`) + +```c +extern framebuffer_t FRAMEBUFFER_BACKBUFFER; +extern const framebuffer_t *FRAMEBUFFER_BOUND; + +// Bind/unbind: +frameBufferBind(fb); +frameBufferUnbind(); + +// Clear (pass flag combination): +frameBufferClear(fb, FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH); + +// Dimensions of the currently bound framebuffer: +int32_t w = frameBufferGetWidth(); +int32_t h = frameBufferGetHeight(); +``` + +`FRAMEBUFFER_BACKBUFFER` is the window surface. Off-screen framebuffers +are used by the screen system when `DUSK_DISPLAY_SIZE_DYNAMIC` is on. + +--- + +## Screen (`screen.h`) + +```c +extern screen_t SCREEN; +// SCREEN.width, SCREEN.height -- logical render dimensions +// SCREEN.aspect -- width / height +// SCREEN.background -- clear colour + +errorret_t screenInit(); +errorret_t screenBind(); // call before rendering game content +errorret_t screenUnbind(); // call after game content, before UI +errorret_t screenRender(); // blit the internal framebuffer to the window +errorret_t screenDispose(); +``` + +`screenBind` / `screenUnbind` / `screenRender` are called by the scene +system automatically each frame. Game code normally does not call them +directly -- use the JS `render()` hook instead. diff --git a/.claude/display-shader.md b/.claude/display-shader.md new file mode 100644 index 00000000..5a0e4672 --- /dev/null +++ b/.claude/display-shader.md @@ -0,0 +1,73 @@ +# Display -- Shader, Material, and Display State + +Source: `src/dusk/display/` + +See also: `.claude/display-core.md`, `.claude/display-texture.md` + +--- + +## Shader (`shader.h` + `shaderlist.h`) + +Shaders are platform-abstracted. The current shader list is defined +in `shaderlist.h`. Currently only one shader is implemented: + +| Enum | Description | +|------|-------------| +| `SHADER_LIST_SHADER_UNLIT` | Unlit / flat colour + texture shader | + +```c +extern shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT]; +// SHADER_LIST_DEFS[n].shader is the platform shader object. + +// Bind a shader before drawing: +errorret_t shaderBind(shader_t *shader); + +// Upload a mat4 uniform by name: +errorret_t shaderSetMatrix(shader_t *shader, const char_t *name, mat4 m); +``` + +Adding a new shader means adding an entry to `shaderlist.h`, providing +platform-specific vertex/fragment sources, and implementing the +corresponding material type in `shadermaterial_t`. + +--- + +## Shader material (`shadermaterial.h`) + +`shadermaterial_t` is a union over per-shader material structs. +Currently contains only the unlit material: + +```c +typedef union shadermaterial_u { + shaderunlitmaterial_t unlit; +} shadermaterial_t; +``` + +`shaderunlitmaterial_t` typically holds a `texture_t *` and a +tint colour. Check `shaderunlitmaterial.h` for the exact fields. + +To use a shader material on a renderable entity: + +1. Set `renderable.type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL`. +2. Set `renderable.data.material.shaderType` to the desired + `shaderlistshadertype_t` value (e.g. `SHADER_LIST_SHADER_UNLIT`). +3. Fill in the corresponding union field: + `renderable.data.material.material.unlit`. +4. Set `renderable.data.material.state.flags` for rasterizer state. + +--- + +## Display state (`displaystate.h`) + +`displaystate_t` controls per-draw rasterizer state flags: + +```c +DISPLAY_STATE_FLAG_CULL // back-face culling +DISPLAY_STATE_FLAG_DEPTH_TEST // depth testing +DISPLAY_STATE_FLAG_BLEND // alpha blending +``` + +Set flags via `data.material.state.flags` on the renderable's material. +The default for an uninitialised state is all flags clear (no culling, +no depth test, no blending). Most opaque geometry should set at least +`CULL | DEPTH_TEST`. diff --git a/.claude/display-texture.md b/.claude/display-texture.md new file mode 100644 index 00000000..5a2886ac --- /dev/null +++ b/.claude/display-texture.md @@ -0,0 +1,86 @@ +# Display -- Texture, Tileset, and Font + +Source: `src/dusk/display/` + +See also: `.claude/display-core.md`, `.claude/display-shader.md` + +--- + +## Texture (`texture.h`) + +```c +extern texture_t TEXTURE_WHITE; // 4x4 opaque white; always available + +errorret_t textureInit( + texture_t *texture, + int32_t width, + int32_t height, + textureformat_t format, + texturedata_t data +); +errorret_t textureDispose(texture_t *texture); +``` + +`textureformat_t` and `texture_t` are platform aliases +(`textureformatplatform_t`, `textureplatform_t`). On OpenGL targets +the format maps to GL texture format constants. + +`texturedata_t` is a union: +- `.paletted.indices` + `.paletted.palette` -- for paletted formats +- `.rgbaColors` -- for RGBA formats + +### Texture rules + +- Dimensions must be powers of two on PSP and GameCube/Wii. Use + `mathNextPowTwo` from `util/math.h` if needed. +- Texture upload must happen on the main thread. In the asset loader, + this means `loadSync` (not `loadAsync`). +- `TEXTURE_WHITE` is always available without loading; use it as a + placeholder or for untextured geometry. + +--- + +## Tileset (`tileset.h`) + +A tileset subdivides a texture into a uniform grid of tiles. + +```c +typedef struct { + uint16_t tileWidth, tileHeight; + uint16_t tileCount; + uint16_t columns, rows; + vec2 uv; // UV size per tile (pre-computed from grid dimensions) +} tileset_t; + +// Get UV rect for a tile by index: +void tilesetTileGetUV( + const tileset_t *ts, uint16_t tileIndex, vec4 outUV +); + +// Get UV rect for a tile by grid position: +void tilesetPositionGetUV( + const tileset_t *ts, uint16_t column, uint16_t row, vec4 outUV +); +``` + +`outUV` is `{u, v, u2, v2}` in normalised [0, 1] texture space. + +Tilesets are loaded from `.dtf` binary files via +`ASSET_LOADER_TYPE_TILESET`. The DTF format stores tile width/height, +grid dimensions, and per-tile UV offsets (magic + version header). + +--- + +## Font (`font.h`) + +```c +typedef struct { + texture_t *texture; + tileset_t *tileset; +} font_t; +``` + +A font is a tileset-backed texture atlas where each tile is a character +glyph. No heap allocation -- both pointers are owned by the caller. +Character lookup is by glyph index into the tileset grid. Rendering is +handled by the spritebatch system using the tileset UV helpers. diff --git a/.claude/display.md b/.claude/display.md new file mode 100644 index 00000000..3b52263d --- /dev/null +++ b/.claude/display.md @@ -0,0 +1,19 @@ +# Display System + +Source: `src/dusk/display/` + +## Overview + +The display system is a platform-abstracted rendering layer. Each +subsystem (texture, shader, framebuffer, screen) is defined by a core +header that requires the platform layer to provide concrete types and +hook macros. The OpenGL implementation lives in `src/duskgl/`; the +Dolphin (GX) implementation in `src/duskdolphin/`. + +## Subsystem documentation + +| Subsystem | Reference | +|-----------|-----------| +| Screen size modes, framebuffer, screen | `.claude/display-core.md` | +| Texture, tileset, font | `.claude/display-texture.md` | +| Shader, shader material, display state | `.claude/display-shader.md` | diff --git a/.claude/ecs.md b/.claude/ecs.md new file mode 100644 index 00000000..97287420 --- /dev/null +++ b/.claude/ecs.md @@ -0,0 +1,179 @@ +# Entity Component System (ECS) + +Source: `src/dusk/entity/` + +## Core concepts + +- **Entity** (`entityid_t` = `uint8_t`) -- a numeric ID. No data of + its own; just an index into the entity manager pool. +- **Component** -- a plain data struct registered in `componentlist.h`. + Stores state; no behaviour. +- **System** -- functions that query all entities with a given component + type and act on them each tick. + +## Hard limits + +| Constant | Value | +|----------|-------| +| `ENTITY_COUNT_MAX` | 64 | +| `ENTITY_COMPONENT_COUNT_MAX` | 16 per entity | +| Total component slots | 1024 (64 x 16) | +| Update callbacks per entity | 5 | +| Dispose callbacks per entity | 5 | + +`ENTITY_ID_INVALID = 0xFF`, `COMPONENT_ID_INVALID = 0xFF`. + +## Global state + +```c +extern entitymanager_t ENTITY_MANAGER; +// .entities[64] -- entity structs +// .components[1024] -- all component data (entity * 16 + comp) +// .entitiesWithComponent -- O(1) lookup indexed by [type * 64 + entityId] +``` + +## Entity lifecycle + +```c +entityid_t id = entityManagerAdd(); // reserve first inactive slot +entityInit(id); // zero the entity, mark active + +componentid_t posId = entityAddComponent(id, COMPONENT_TYPE_POSITION); +componentid_t rendId = entityAddComponent(id, COMPONENT_TYPE_RENDERABLE); + +// Per-frame update (called by entityManagerUpdate): +entityUpdate(id); + +// Cleanup: +entityDispose(id); // dispose components, mark inactive +entityDisposeDeep(id); // dispose self + entire position hierarchy +``` + +## Component registration (X-macro) + +All component types are declared in a single table in +`src/dusk/entity/componentlist.h`: + +```c +X(NAME, type_t, fieldName, initFn, disposeFn, renderFn) +``` + +This generates: +- `COMPONENT_TYPE_NAME` enum value +- Union field `fieldName` in `componentdata_t` +- Entry in `COMPONENT_DEFINITIONS[]` with `init` / `dispose` / + `render` function pointers (any may be `NULL`) + +Current registered components: + +| Enum suffix | Struct | Notes | +|-------------|--------|-------| +| `POSITION` | `entityposition_t` | Transform + parent/child hierarchy | +| `CAMERA` | `entitycamera_t` | View matrix setup | +| `RENDERABLE` | `entityrenderable_t` | Sprite batch, shader material, or custom draw | +| `PHYSICS` | `entityphysics_t` | Physics body (see `.claude/physics.md`) | +| `TRIGGER` | `entitytrigger_t` | Collision trigger zone | + +## Accessing component data + +```c +void *componentGetData( + entityid_t entityId, + componentid_t componentId, + componenttype_t type +); +// Returns pointer into the preallocated components pool. +// Never NULL for a valid (id, type) pair. +``` + +Querying all entities with a given type: + +```c +entityid_t ids[ENTITY_COUNT_MAX]; +componentid_t comps[ENTITY_COUNT_MAX]; +entityid_t count = componentGetEntitiesWithComponent( + COMPONENT_TYPE_PHYSICS, ids, comps +); +``` + +## Adding a new component -- checklist + +1. Create `src/dusk/entity/component//entity.h/.c`. + - Struct: `entity_t` + - `entityInit(entityid_t, componentid_t)` (required) + - `entityDispose(entityid_t, componentid_t)` (if needed) +2. `#include` the new header in the header block of `componentlist.h`. +3. Add an `X(...)` row in `componentlist.h`. +4. If JS-facing, add a script module (see `CLAUDE.md`). + +## Position component (`entityposition_t`) + +The position component implements the transform hierarchy and uses lazy +evaluation with dirty flags to avoid redundant matrix rebuilds. + +**Dirty flags:** + +| Flag | Meaning | +|------|---------| +| `ENTITY_POSITION_FLAG_PRS_DIRTY` | Cached position/rotation/scale stale vs localTransform | +| `ENTITY_POSITION_FLAG_ROTATION_DIRTY` | Rotation columns of localTransform stale | +| `ENTITY_POSITION_FLAG_POSITION_DIRTY` | Position column of localTransform stale | +| `ENTITY_POSITION_FLAG_WORLD_DIRTY` | World matrix stale | + +**Hierarchy:** up to 8 children per entity. `entityPositionSetParent()` +reparents and maintains the child list. `entityDisposeDeep()` / +`entityPositionDisposeDeep()` recursively disposes the entire subtree. + +**Key functions:** + +```c +// Local space getters/setters (mark local dirty): +entityPositionGetLocalPosition / SetLocalPosition +entityPositionGetLocalRotation / SetLocalRotation +entityPositionGetLocalScale / SetLocalScale + +// World space getters/setters (ensure world updated): +entityPositionGetWorldPosition / SetWorldPosition +entityPositionGetWorldRotation / SetWorldRotation +entityPositionGetWorldScale / SetWorldScale + +entityPositionSetParent(entityId, parentEntityId, parentComponentId); +entityPositionLookAt(entityId, componentId, eye, target, up); +entityPositionRebuild(pos); // force immediate matrix rebuild +``` + +## Renderable component (`entityrenderable_t`) + +Three rendering modes selected via `entityrenderabletype_t`: + +| Mode | Description | +|------|-------------| +| `ENTITY_RENDERABLE_TYPE_CUSTOM` | User-supplied `draw` callback | +| `ENTITY_RENDERABLE_TYPE_SPRITEBATCH` | Up to 64 sprites + texture | +| `ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL` | Up to 8 meshes, shader, material, display state | + +Default on init: shader material (white, unlit, depth-tested cube). +`priority` (int8_t) controls render order; 0 = automatic. + +## Callback hooks on entities + +Entities support up to 5 registered update callbacks and 5 dispose +callbacks. These are used by systems that need per-entity ticks without +building a full query loop every frame: + +```c +entityUpdateAdd(entityId, updateFn, componentId, user); +entityUpdateRemove(entityId, updateFn); + +entityDisposeAdd(entityId, disposeFn, componentId, user); +entityDisposeRemove(entityId, disposeFn); +``` + +## Design rules + +- Components store **data only**. No logic in a component struct or + its init beyond setting default values. +- Keep components small and focused. +- Cross-component access is fine from a system, but a component must + never hold a pointer to another component -- use entity IDs. +- Systems must not assume component ordering. Use `componentGetEntitiesWithComponent`. diff --git a/.claude/engine.md b/.claude/engine.md new file mode 100644 index 00000000..6e013d2b --- /dev/null +++ b/.claude/engine.md @@ -0,0 +1,95 @@ +# Engine, System, and Log + +Sources: `src/dusk/engine/`, `src/dusk/system/`, `src/dusk/log/` + +--- + +## Engine (`engine.h`) + +The engine owns the top-level init / update / dispose loop. Every +platform's `main()` calls these three functions in order. + +```c +extern engine_t ENGINE; +// ENGINE.running -- false causes the main loop to exit +// ENGINE.argc / ENGINE.argv -- passed from main() +// ENGINE.version -- version string + +errorret_t engineInit(int32_t argc, const char_t **argv); +errorret_t engineUpdate(void); // called once per tick +errorret_t engineDispose(void); +``` + +`engineInit` initialises subsystems in order: system, log, assert, +display, time, asset, input, physics, script, etc. + +`engineUpdate` steps each subsystem: time, input, physics, script, ECS +entities, rendering, audio, network, asset completion callbacks. + +`engineDispose` shuts everything down in reverse order. + +**To exit gracefully:** set `ENGINE.running = false` -- the platform +main loop checks this each tick and calls `engineDispose` before +returning. + +--- + +## System (`system.h`) + +The system module is initialised very early (before most other +subsystems) and provides two things: + +### Platform identity + +```c +typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t; + +systemplatform_t systemGetPlatform(void); +``` + +Platform names come from `systemplatformlist.h` via an X-macro. This +lets game code query the runtime platform when compile-time guards are +not sufficient (e.g. serializing platform name to a log). + +### Dialog blocking + +Some platforms (PSP, Wii) show OS-level dialogs (Wi-Fi setup, save +management) that block the normal game loop. The system module exposes +the current dialog state so the engine main loop can adjust: + +```c +typedef enum { + SYSTEM_DIALOG_TYPE_NONE, + SYSTEM_DIALOG_TYPE_RENDER_BLOCKING, // skip render but still tick + SYSTEM_DIALOG_TYPE_TICK_BLOCKING, // skip both render and tick +} systemdialogtype_t; + +systemdialogtype_t systemGetActiveDialogType(void); +``` + +`engineUpdate` checks this before calling render / update code. +Most platforms always return `SYSTEM_DIALOG_TYPE_NONE`. + +--- + +## Log (`log.h`) + +Simple printf-style logging with two levels. Always use these instead +of `printf` / `fprintf`. + +```c +void logDebug(const char_t *message, ...); +// Writes to the debug output (stdout on desktop, platform console +// on handhelds). No-op in release builds on some platforms. + +void logError(const char_t *message, ...); +// Writes to the error output. On some platforms (PSP) this may +// pause execution to ensure the message is visible before continuing. +``` + +**Do not** use `logDebug` / `logError` for structured error handling -- +that is what `errorThrow` / `errorChain` are for. Log calls are for +human-readable diagnostics only. + +The error system calls `logError` internally when printing a caught +error via `errorPrint()`. diff --git a/.claude/errors.md b/.claude/errors.md new file mode 100644 index 00000000..8154e2c3 --- /dev/null +++ b/.claude/errors.md @@ -0,0 +1,133 @@ +# Error Handling System + +Source: `src/dusk/error/` + +## Philosophy + +Error handling is return-value based. Functions that can fail return +`errorret_t`. There are no exceptions, `errno`, `setjmp`, or global +error codes. Each thread has its own isolated error state. + +## Types + +```c +typedef uint8_t errorcode_t; + +typedef struct { + errorcode_t code; + char_t *message; // allocated; freed by errorCatch + char_t *lines; // call-stack trace; allocated; freed by errorCatch +} errorstate_t; + +typedef struct { + errorcode_t code; + errorstate_t *state; // NULL on success +} errorret_t; +``` + +**Constants:** `ERROR_OK = 0`, `ERROR_NOT_OK = 1`. + +Error state is thread-local: + +```c +extern THREAD_LOCAL errorstate_t ERROR_STATE; +``` + +Each thread has its own `ERROR_STATE` so concurrent errors never +interfere. + +## Macros + +### Throwing an error + +```c +errorThrow("message %d", value); +// Throws with ERROR_NOT_OK, captures __FILE__ / __func__ / __LINE__. + +errorThrowWithCode(code, "message %d", value); +// Same but with a specific error code. +``` + +Both macros **return from the current function** with an `errorret_t`. +Do not call them in void functions. + +### Propagating up the call stack + +```c +errorChain(someCall()); +``` + +If `someCall()` returned an error, appends the current location to the +stack trace and **returns** that error from the current function. +If `someCall()` returned success, execution continues normally. + +### Returning success + +```c +errorOk(); +``` + +Returns `errorret_t` with `code == ERROR_OK` and asserts the thread's +`ERROR_STATE` is clean (no leftover error). Must be the last statement +in a fallible function. + +### Inspecting a result + +```c +if(errorIsOk(ret)) { ... } +if(errorIsNotOk(ret)) { ... } +``` + +### Cleaning up + +```c +errorCatch(ret); +``` + +Frees `ret.state->message` and `ret.state->lines`, resets the thread's +`ERROR_STATE.code` to `ERROR_OK`. Safe to call on a success return +(no-op). **Always call `errorCatch` on errors you are handling** -- +otherwise the allocated message and stack-trace leak. + +### Logging + +```c +errorPrint(ret); // prints code + message + stack trace, returns ret +``` + +## Typical patterns + +### Fallible function + +```c +errorret_t myFunction(int_t x) { + if(x < 0) errorThrow("x must be non-negative, got %d", x); + errorChain(someOtherFallibleCall(x)); + errorOk(); +} +``` + +### Caller that handles errors + +```c +errorret_t ret = myFunction(-1); +if(errorIsNotOk(ret)) { + errorCatch(errorPrint(ret)); + // ... fallback logic ... +} +``` + +### Stack trace accumulation + +Each `errorChain()` call appends a line to `ret.state->lines` in the +format ` at file:line in function\n`. A deeply chained error produces +a full call path readable from `ret.state->lines`. + +## What NOT to do + +- Do not use raw `errno` for in-engine errors. +- Do not return an error code integer -- always return `errorret_t`. +- Do not ignore an error return without calling `errorCatch` on it if + you are not propagating it. +- Do not mix assertions with error handling. Assertions are for + programmer mistakes; `errorThrow` is for expected failure paths. diff --git a/.claude/events.md b/.claude/events.md new file mode 100644 index 00000000..79ad1007 --- /dev/null +++ b/.claude/events.md @@ -0,0 +1,102 @@ +# Event System + +Source: `src/dusk/event/` + +## Overview + +The event system is a simple publish-subscribe mechanism backed by +caller-owned static arrays. There is no heap allocation in the event +system itself -- the caller provides the backing storage. + +## API + +```c +typedef void (*eventcallback_t)(void *params, void *user); + +typedef struct { + eventcallback_t *callbacks; + void **users; + size_t size; + uint32_t count; +} event_t; +``` + +### Initialise + +```c +void eventInit( + event_t *event, + eventcallback_t *callbacks, + void **users, + size_t size +); +``` + +`callbacks` and `users` are caller-owned arrays of length `size`. Both +are zeroed by `eventInit`. `users` may be `NULL` if no subscriber needs +a user pointer. + +### Subscribe / unsubscribe + +```c +void eventSubscribe(event_t *event, eventcallback_t callback, void *user); +void eventUnsubscribe(event_t *event, eventcallback_t callback); +``` + +The same `(callback, user)` pair may only be subscribed once -- +`eventSubscribe` asserts on a duplicate. `eventUnsubscribe` is a no-op +if the pair is not found. Unsubscribing uses swap-with-last to keep the +array packed; ordering is not preserved. + +### Fire + +```c +void eventInvoke(const event_t *event, void *params); +``` + +Calls every subscriber in registration order, passing `params` and +each subscriber's `user` pointer. + +## Usage pattern + +Declare the backing arrays alongside the event struct, typically as +struct fields or static variables: + +```c +#define MY_EVENT_CAPACITY 4 + +typedef struct { + event_t onComplete; + eventcallback_t _completeCbs[MY_EVENT_CAPACITY]; + void *_completeUsers[MY_EVENT_CAPACITY]; +} mystate_t; + +// Init: +eventInit( + &state.onComplete, + state._completeCbs, + state._completeUsers, + MY_EVENT_CAPACITY +); + +// Publish: +eventInvoke(&state.onComplete, &someParams); + +// Subscribe from outside: +eventSubscribe(&state.onComplete, myHandler, myUserPtr); +``` + +## Constraints + +- Capacity is fixed at init time. Exceeding it is a runtime assertion. +- Subscriber order is not stable after an unsubscribe. +- `eventInvoke` is synchronous -- all callbacks run on the calling + thread before it returns. +- Do not subscribe or unsubscribe from inside a callback -- the array + may shift during iteration. + +## Where events are used + +- `inputactiondata_t`: `onPressed`, `onReleased` per action +- Any engine subsystem that exposes hooks (network connect/disconnect, + asset batch completion, etc.) diff --git a/.claude/input.md b/.claude/input.md new file mode 100644 index 00000000..b22abdf1 --- /dev/null +++ b/.claude/input.md @@ -0,0 +1,130 @@ +# Input System + +Source: `src/dusk/input/`, platform layers in `src/dusk/input/` + +## Architecture + +The input system has two layers: + +1. **Action layer** (`inputaction_t`) -- named gameplay inputs, e.g. + UP, DOWN, ACCEPT, CANCEL. This is what game code reads. +2. **Button layer** (`inputbutton_t`) -- physical hardware inputs, e.g. + keyboard key, gamepad button, analog axis, mouse axis. Multiple + buttons can bind to the same action (the highest value wins). + +The platform layer implements two hooks: +- `inputUpdatePlatform()` -- read hardware state once per frame +- `inputButtonGetValuePlatform()` -- return the analog value [0.0, 1.0] + for a given button + +## Global state + +```c +extern input_t INPUT; +// INPUT.actions[INPUT_ACTION_COUNT] -- all action states +// INPUT.platform -- platform-specific data +``` + +## Reading actions (game code) + +```c +// Analog value this frame (0.0 - 1.0) +float_t inputGetCurrentValue(inputaction_t action); + +// Analog value last frame +float_t inputGetLastValue(inputaction_t action); + +// Boolean helpers (built on current/last values) +bool_t inputIsDown(inputaction_t action); +bool_t inputWasDown(inputaction_t action); +bool_t inputPressed(inputaction_t action); // was up, now down +bool_t inputReleased(inputaction_t action); // was down, now up + +// 2D axis helpers +void inputAxis2D( + inputaction_t horiz, + inputaction_t vert, + vec2 out +); +float_t inputAngle2D(inputaction_t horiz, inputaction_t vert); +void inputAxis(inputaction_t action, float_t *out); + +// Deadzone filter (applied to raw axis values) +float_t inputDeadzone(float_t value, float_t deadzone); +``` + +## Binding buttons to actions + +```c +void inputBind(inputaction_t action, inputbutton_t button); +``` + +Each platform's init function calls `inputBind` to wire its hardware +buttons to the standard action IDs. Game code should never need to call +`inputBind` -- it is set up once during platform init. + +## Button types + +```c +INPUT_BUTTON_TYPE_KEYBOARD // SDL scancode (SDL2 targets only) +INPUT_BUTTON_TYPE_POINTER // Mouse axes: X, Y, Z, WHEEL_X, WHEEL_Y +INPUT_BUTTON_TYPE_TOUCH // Touch (defined, not fully implemented) +INPUT_BUTTON_TYPE_GAMEPAD // Digital gamepad buttons +INPUT_BUTTON_TYPE_GAMEPAD_AXIS // Analog axes (-1.0 to 1.0 internally) +``` + +## Events + +Each action has `onPressed` and `onReleased` event callbacks. Subscribe +via the event system (see `.claude/events.md`): + +```c +eventSubscribe(&INPUT.actions[ACTION_ACCEPT].onPressed, myCallback, NULL); +``` + +## Platform implementations + +### SDL2 (`src/dusksdl2/input/`) + +Handles Linux, Knulli, and PSP (PSP adds its own button mapping layer +on top of SDL2). + +- Keyboard: SDL scancode array from `SDL_GetKeyboardState()` +- Pointer: normalized mouse position (0.0-1.0), scroll axes +- Gamepad: first available `SDL_GameController`; axis values normalized + to [-1.0, 1.0] with deadzone (default 0.2f via `inputGetDeadzoneSDL2`) + +### Dolphin -- GameCube / Wii (`src/duskdolphin/input/`) + +Uses `libogc` PAD API. No keyboard or pointer input -- trying to use +those button types is a compile-time `#error`. + +- Gamepad: `PAD_ScanPads()` + `PAD_ButtonsHeld()` for pad 0 +- Axes: left stick X/Y, C-stick X/Y, L/R triggers (6 total) +- Deadzone: hardcoded 0.2f +- Default bindings set at init: D-pad/L-stick = directional actions, + A = ACCEPT, B = CANCEL, X = CONSOLE, Start = RAGEQUIT + +### PSP (`src/duskpsp/input/`) + +Layered on top of SDL2. `inputInitPSP()` remaps SDL2 controller button +constants to PSP button names, then calls `inputBind` to wire them: + +| PSP button | SDL2 constant | +|------------|---------------| +| Cross | `SDL_CONTROLLER_BUTTON_A` | +| Circle | `SDL_CONTROLLER_BUTTON_B` | +| Triangle | `SDL_CONTROLLER_BUTTON_Y` | +| Square | `SDL_CONTROLLER_BUTTON_X` | +| L / R | `SDL_CONTROLLER_BUTTON_LEFTSHOULDER` / `RIGHTSHOULDER` | +| L-Stick | `SDL_CONTROLLER_AXIS_LEFTX/Y` | + +## Platform capability notes + +| Feature | Linux/Knulli | PSP | GameCube/Wii | +|---------|-------------|-----|--------------| +| Keyboard | Yes (SDL2) | No | No | +| Pointer/Mouse | Yes (SDL2) | No | No | +| Gamepad | Yes (SDL2) | Yes (SDL2) | Yes (PAD) | +| Analog axes | Yes | L-Stick only | L-Stick, C-Stick, Triggers | +| Touch | Defined, not implemented | -- | -- | diff --git a/.claude/locale.md b/.claude/locale.md new file mode 100644 index 00000000..ab4469db --- /dev/null +++ b/.claude/locale.md @@ -0,0 +1,90 @@ +# Locale System + +Source: `src/dusk/locale/`, asset loader at +`src/dusk/asset/loader/locale/` + +## Overview + +The locale system loads Gettext PO files from the asset archive and +provides string lookup with plural-form support and printf-style +argument substitution. Locale files live in `locale/` inside `dusk.dsk`. + +## Global state + +```c +extern localemanager_t LOCALE; +// LOCALE.locale -- currently active localeinfo_t +// LOCALE.entry -- locked assetentry_t for the current PO file +``` + +## Initialise and switch locale + +```c +errorret_t localeManagerInit(); +// Defaults to LOCALE_EN_US (locale/en_US.po). + +errorret_t localeManagerSetLocale(const localeinfo_t *locale); +// Unlocks the old entry, loads and locks the new one. +// Blocks until the new PO file is fully parsed. + +void localeManagerDispose(); +``` + +## Getting a localised string + +```c +// Variadic (printf-style args): +localeManagerGetText(id, buffer, bufferSize, plural, ...); + +// With a pre-built args array: +localeManagerGetTextArgs(id, buffer, bufferSize, plural, args, argCount); +``` + +Both are macros that delegate to `assetLocaleGetStringWithVA` / +`assetLocaleGetStringWithArgs`. + +- `id` -- message ID string (the English key in the PO file) +- `plural` -- plural index (0 for singular, 1+ per PO plural rules) +- `buffer` -- destination `char_t` array +- `bufferSize` -- size of the destination buffer +- `...` -- format arguments matching `%s`, `%d`, `%f` placeholders + +## Locale descriptors (`localeinfo_t`) + +```c +typedef struct { + const char_t *name; // e.g. "en-US" + const char_t *file; // path inside dusk.dsk, e.g. "locale/en_US.po" +} localeinfo_t; +``` + +The built-in descriptor is: + +```c +static const localeinfo_t LOCALE_EN_US = { + .name = "en-US", + .file = "locale/en_US.po", +}; +``` + +Add new locales by declaring another `localeinfo_t` constant and +shipping the corresponding `.po` file in the asset archive. + +## PO file format notes + +The loader (`assetlocaleloader`) parses standard Gettext PO syntax: +- `msgid` / `msgstr` pairs +- `msgid_plural` / `msgstr[n]` for plural forms +- The `Plural-Forms:` header (e.g. `nplurals=2; plural=(n != 1);`) + is parsed and evaluated at lookup time + +Argument substitution uses `%s`, `%d`, `%f` placeholders (not +standard Gettext `%1` positional args). + +## Adding a new locale + +1. Create `locale/.po` with a valid `Plural-Forms:` + header and the translated `msgid`/`msgstr` entries. +2. Pack it into `dusk.dsk`. +3. Add a `localeinfo_t` constant in `localeinfo.h`. +4. Call `localeManagerSetLocale()` with the new descriptor to activate. diff --git a/.claude/network.md b/.claude/network.md new file mode 100644 index 00000000..858d8763 --- /dev/null +++ b/.claude/network.md @@ -0,0 +1,132 @@ +# Network System + +Source: `src/dusk/network/`, platform layers in +`src/dusk/network/` + +## Overview + +The network system provides a platform-agnostic API for detecting and +managing a network connection. Higher-level functionality (HTTP, sockets) +is not yet implemented in any platform. The system handles the +connection lifecycle -- connect, detect disconnect, disconnect -- and +reports the current IP address. + +## Implementation status by platform + +| Platform | Connection | IP info | HTTP/Requests | Notes | +|----------|-----------|---------|--------------|-------| +| **Linux** | Auto (OS) | IPv4 + IPv6 | Not implemented | `getifaddrs()` | +| **Knulli** | Auto (OS) | IPv4 + IPv6 | Not implemented | same as Linux | +| **PSP** | Manual dialog | IPv4 only | Not implemented | `sceUtilityNetconf`; SSL/HTTP modules commented out | +| **GameCube** | Manual DHCP | IPv4 only | Not implemented | `if_config()` stubbed; `net_init()` commented out | +| **Wii** | Manual DHCP | IPv4 only | Not implemented | blocking `if_config()` via System Menu Wi-Fi settings | + +## Connection states + +```c +typedef enum { + NETWORK_STATE_DISCONNECTED, + NETWORK_STATE_CONNECTING, + NETWORK_STATE_CONNECTED, + NETWORK_STATE_DISCONNECTING, +} networkstate_t; +``` + +## Global state + +```c +extern network_t NETWORK; +// NETWORK.state -- current connection state +// NETWORK.platform -- platform-specific data +// NETWORK.onDisconnect -- callback fired on unexpected disconnect +``` + +## Core API (`network.h`) + +```c +errorret_t networkInit(); +errorret_t networkUpdate(); // call each frame; detects dropped connections +errorret_t networkDispose(); + +bool_t networkIsConnected(); + +void networkRequestConnection( + void (*onConnected)(void *user), + void (*onFailed)(errorret_t error, void *user), + void (*onDisconnect)(errorret_t error, void *user), + void *user +); + +void networkRequestDisconnection( + void (*onComplete)(void *user), + void *user +); +``` + +On platforms that manage their own connection (Linux, macOS, Windows), +`networkRequestConnection` immediately calls `onConnected` if a network +interface is up, or `onFailed` if not. No `networkPlatformRequestConnection` +macro is needed. + +On platforms that require explicit connection (PSP, Wii), the platform +implements `networkPlatformRequestConnection` and +`networkPlatformRequestDisconnection`. + +## Network info + +```c +networkinfo_t networkGetInfo(); +// Only valid when NETWORK.state == NETWORK_STATE_CONNECTED. +``` + +```c +typedef struct { + networktype_t type; // NETWORK_TYPE_IPV4 or (ifdef) NETWORK_TYPE_IPV6 + union { + networkinfoipv4_t ipv4; // uint8_t ip[4] + networkinfoipv6_t ipv6; // uint8_t ip[16] (requires DUSK_NETWORK_IPV6) + }; +} networkinfo_t; +``` + +IPv6 support requires the `DUSK_NETWORK_IPV6` compile-time define. + +## Platform-specific notes + +### Linux / Knulli + +Fully functional for connection detection and IP querying. No explicit +connect/disconnect step needed -- the OS manages the interface. Uses +`getifaddrs()` to find the first non-loopback running interface. + +### PSP + +Connection is asynchronous and driven by a state machine in +`networkPSPUpdate()`. The PSP shows the built-in network configuration +dialog (`sceUtilityNetconfInitStart`) to let the user pick a Wi-Fi +access point. + +HTTP/SSL modules (`psphttp`, `pspssl`) are loaded in commented-out code +-- the infrastructure for HTTP exists but is disabled. + +### GameCube + +`net_init()` is commented out. Networking on GameCube is non-functional +in the current build. + +### Wii + +Uses `if_config()` (DHCP via libogc) to connect using the Wi-Fi settings +stored in Wii System Menu. This call **blocks** the main thread. The +connection only works when `DUSK_WII` is defined; the GameCube path +always fails. + +## Adding a new platform implementation + +1. Create `src/dusk/network/network.h/.c`. +2. Implement the five required functions: + `Init`, `Update`, `Dispose`, `IsConnected`, `GetInfo`. +3. Optionally implement `RequestConnection` and `RequestDisconnection` + if the platform requires an explicit connection step. +4. Create `networkplatform.h` mapping each `networkPlatform*` macro to + your functions, and defining `networkplatform_t`. diff --git a/.claude/optimization.md b/.claude/optimization.md new file mode 100644 index 00000000..7f1d711a --- /dev/null +++ b/.claude/optimization.md @@ -0,0 +1,83 @@ +# Optimization Guidelines + +Dusk must run well on severely resource-constrained hardware. The PSP +has 32 MB of RAM and a 333 MHz MIPS CPU. The GameCube has 24 MB of RAM +and a 485 MHz PowerPC CPU with no FPU for integer paths. Optimization +is not an afterthought -- it is a first-class design constraint. + +## General principles + +- **Measure before optimizing.** Don't guess where bottlenecks are. + Profile on the actual target hardware when possible. +- **Data-oriented design.** The ECS exists to enable cache-friendly + iteration over components. Keep hot data tightly packed (SoA over + AoS where it matters). +- **Minimize allocations.** Dynamic allocation at runtime is expensive + and causes fragmentation. Prefer fixed-size pools, arenas, and + pre-allocated arrays. +- **Avoid per-frame allocations.** Anything allocated and freed every + tick is a red flag. Use scratch buffers or static pools. +- **Avoid recursion** on constrained targets -- stack is small. + +## Memory + +| Platform | Total RAM | Notes | +|------------|-------------|-----------------------------------| +| GameCube | 24 MB | 16 MB main + 8 MB "Aram" (audio) | +| Wii | 88 MB | 24 MB MEM1 + 64 MB MEM2 | +| PSP | 32 MB | 4 MB reserved for OS | +| Vita | 512 MB | Much more headroom | +| Linux | Host RAM | Effectively unlimited | + +Treat the GameCube 16 MB main RAM as the worst-case constraint when +designing data structures and budgets. + +Always use `memoryAllocate` / `memoryFree` -- never `malloc` / `free`. +The engine allocator tracks usage and can enforce budgets per platform. + +## Math + +- Prefer integer math over floating-point on platforms without an FPU. +- Use fixed-point arithmetic (`int32_t` with a known scale) for physics + and animation on PSP/GameCube where FPU throughput is limited. +- SIMD / VFPU (PSP) and paired-singles (GameCube) are available but + require platform-guarded code paths under `#ifdef DUSK_PSP` etc. +- Avoid `double` entirely -- use `float_t` (32-bit) throughout. + +## Rendering + +- Batch draw calls aggressively. Every draw call has overhead on all + platforms; consoles are especially sensitive. +- Minimize state changes (texture binds, shader switches, etc.). +- Use display lists (GameCube/Wii) and vertex buffer objects (OpenGL) + to offload geometry to GPU memory. +- Keep texture sizes powers of two. Non-PoT textures are unsupported + or have penalties on PSP and GameCube. + +## Asset loading + +- Assets are loaded asynchronously via the asset loader system. Do not + block the game loop waiting for assets. +- Compress textures to the native format for each platform at build + time, not at runtime. +- Stream large assets from the filesystem rather than loading them all + at startup. + +## Platform-specific notes + +### PSP +- The Media Engine (ME) is a second CPU core -- use it for audio and + background decompression, not general logic. +- VFPU gives 4-wide SIMD floats; use it for matrix and vector math. +- Keep the uncached scratchpad (4 KB at 0x00010000) in mind for hot + temporary data. + +### GameCube / Wii +- The GX display list pipeline is the primary rendering path; avoid + immediate-mode GX calls in the hot path. +- Texture Compression (CMPR / S3TC equivalent) halves texture memory. +- Wii: prefer MEM1 for GPU-accessed data; MEM2 for CPU-only buffers. + +### PSP / Vita +- OpenGL ES has a subset of desktop OpenGL. Avoid extensions and + features that are not in the ES 1.1 / ES 2.0 core. diff --git a/.claude/physics.md b/.claude/physics.md new file mode 100644 index 00000000..6733b688 --- /dev/null +++ b/.claude/physics.md @@ -0,0 +1,127 @@ +# Physics System + +Source: `src/dusk/physics/`, entity component at +`src/dusk/entity/component/physics/entityphysics.h/.c` + +## Overview + +Dusk uses a lightweight, custom 3D physics simulation with no external +library dependency. It is integrated with the ECS: only entities that +have both a `COMPONENT_TYPE_PHYSICS` and a `COMPONENT_TYPE_POSITION` +component participate in the simulation. + +## Shapes + +```c +typedef enum { + PHYSICS_SHAPE_CUBE, // Axis-aligned bounding box (AABB) + PHYSICS_SHAPE_SPHERE, + PHYSICS_SHAPE_CAPSULE, // Y-axis aligned; radius + halfHeight + PHYSICS_SHAPE_PLANE, // Infinite plane; normal + distance +} physicshapetype_t; +``` + +All shape pairs are supported by the collision dispatch +(`physicsTestShapeVsShape`). See `physicstest.h` for the individual +test functions. + +## Body types + +```c +typedef enum { + PHYSICS_BODY_STATIC, // Never moves; immovable collision surface + PHYSICS_BODY_DYNAMIC, // Driven by gravity, velocity, collisions + PHYSICS_BODY_KINEMATIC, // Moved programmatically; collides but not + // driven by the simulation (e.g. player) +} physicsbodytype_t; +``` + +## World and gravity + +```c +extern physicsworld_t PHYSICS_WORLD; +// PHYSICS_WORLD.gravity -- default {0, -9.81, 0} +``` + +The simulation step is driven by `physicsManagerUpdate()`, which is +called each fixed-timestep game loop tick. It skips dynamic-timestep +sub-steps (`DUSK_TIME_DYNAMIC`). + +## Simulation phases (each step) + +1. **Integrate dynamics** -- apply gravity scaled by `gravityScale`, + advance velocity, update position. +2. **Dynamic vs static/kinematic** -- resolve penetration and cancel + the normal velocity component. +3. **Dynamic vs dynamic** -- split penetration 50/50; exchange + relative normal velocity. +4. **Rebuild transforms** -- call `entityPositionRebuild()` for all + affected dynamic bodies. + +`PHYSICS_GROUND_THRESHOLD = 0.707f` -- a collision normal with a Y +component above this value sets `onGround = true` on the dynamic body. + +## Entity component (`entityphysics_t`) + +```c +typedef struct { + physicsbodytype_t type; + physicsshape_t shape; + vec3 velocity; + float_t gravityScale; // default 1.0 + bool_t onGround; // set by the solver each step +} entityphysics_t; +``` + +Default on init: DYNAMIC body, 0.5m half-extents AABB cube, +`gravityScale = 1.0f`. + +### Component API + +```c +entityphysics_t *entityPhysicsGet(entityid_t, componentid_t); + +void entityPhysicsSetShape(entityid_t, componentid_t, physicsshape_t); +physicsshape_t entityPhysicsGetShape(entityid_t, componentid_t); + +void entityPhysicsSetVelocity(entityid_t, componentid_t, vec3); +void entityPhysicsGetVelocity(entityid_t, componentid_t, vec3 dest); +void entityPhysicsApplyImpulse(entityid_t, componentid_t, vec3); +// No-op on STATIC bodies. + +bool_t entityPhysicsIsOnGround(entityid_t, componentid_t); +void entityPhysicsSetBodyType(entityid_t, componentid_t, physicsbodytype_t); +physicsbodytype_t entityPhysicsGetBodyType(entityid_t, componentid_t); +``` + +## Collision detection primitives (`physicstest.h`) + +Each function returns `true` if overlapping and writes the push-out +normal (pointing from B toward A) and penetration depth. + +| Function | Shapes | +|----------|--------| +| `physicsTestAabbVsAabb` | CUBE vs CUBE | +| `physicsTestSphereVsSphere` | SPHERE vs SPHERE | +| `physicsTestSphereVsAabb` | SPHERE vs CUBE | +| `physicsTestSphereVsPlane` | SPHERE vs PLANE | +| `physicsTestAabbVsPlane` | CUBE vs PLANE | +| `physicsTestCapsuleVsSphere` | CAPSULE vs SPHERE | +| `physicsTestCapsuleVsAabb` | CAPSULE vs CUBE | +| `physicsTestCapsuleVsPlane` | CAPSULE vs PLANE | +| `physicsTestCapsuleVsCapsule` | CAPSULE vs CAPSULE | +| `physicsTestShapeVsShape` | Any pair via dispatch | + +Capsules are always Y-axis aligned. Planes are infinite (not half-spaces). + +## Limitations and known gaps + +- No rotation simulation -- bodies do not rotate from collisions. +- No friction or damping model yet. +- No sleeping / deactivation for resting bodies. +- No broad-phase culling: the solver is O(n^2) per phase. + This is acceptable up to the ECS entity limit (64 entities) but must + be revisited if the entity count grows. +- Capsule vs plane uses the bottom/top hemisphere centers as a + degenerate approximation -- accurate for large planes but + not for thin surfaces. diff --git a/.claude/platform-dolphin.md b/.claude/platform-dolphin.md new file mode 100644 index 00000000..d7de7eff --- /dev/null +++ b/.claude/platform-dolphin.md @@ -0,0 +1,288 @@ +# Platform -- Dolphin (GameCube and Wii) + +`DUSK_TARGET_SYSTEM`: `gamecube` / `wii` +Source layer: `src/duskdolphin/` +Renderer: libogc GX (native Nintendo hardware) + +--- + +## Overview + +GameCube and Wii are collectively called the **Dolphin** targets. They +share a single source layer (`src/duskdolphin/`) and a shared CMake base +(`cmake/targets/dolphin.cmake`). Individual targets add `DUSK_GAMECUBE` +or `DUSK_WII` on top. + +Both are **big-endian** PowerPC platforms. They do **not** use SDL2 or +OpenGL -- rendering and input go through `libogc` (the open-source +GameCube/Wii SDK) and the GX hardware API directly. + +--- + +## Hardware + +| Attribute | GameCube | Wii | +|-----------|---------|-----| +| CPU | IBM PowerPC 750CL (Gekko), 485 MHz | IBM Broadway (Wii CPU), 729 MHz | +| RAM | 24 MB (16 MB MEM1 + 8 MB ARAM) | 88 MB (24 MB MEM1 + 64 MB MEM2) | +| GPU | ATI Flipper (GX) | ATI Hollywood (GX) | +| Display | 640x480 (480p max) | 640x480 (480p/576i), 480p/1080i via component | +| Storage | Memory Card (slots A/B), SD Gecko | SD card, USB, NAND | +| Endian | Big-endian | Big-endian | + +Treat the **GameCube 16 MB MEM1** as the worst-case RAM budget for data +structures shared between both targets. + +--- + +## Compile-time macros + +| Macro | GameCube | Wii | Notes | +|-------|---------|-----|-------| +| `DUSK_DOLPHIN` | yes | yes | Set by `dolphin.cmake` | +| `DUSK_GAMECUBE` | yes | no | | +| `DUSK_WII` | no | yes | | +| `DUSK_INPUT_GAMEPAD` | yes | yes | | +| `DUSK_DISPLAY_WIDTH` | 640 | 640 | | +| `DUSK_DISPLAY_HEIGHT` | 480 | 480 | | +| `DUSK_THREAD_PTHREAD` | yes | yes | devkitPPC pthreads | +| `DUSK_PLATFORM_ENDIAN_BIG` | yes | yes | Not set by cmake -- apply manually | +| `DOL` | 1 | 1 | Build type token | +| `ISO` | 2 | 2 | Build type token | +| `DUSK_DOLPHIN_BUILD_TYPE` | `DOL` or `ISO` | `DOL` or `ISO` | | +| `DUSK_DOLPHIN_BUILD_ISO` | if ISO mode | if ISO mode | | + +No `DUSK_SDL2`, no `DUSK_OPENGL`, no `DUSK_INPUT_KEYBOARD`, +no `DUSK_INPUT_POINTER`, no `DUSK_TIME_DYNAMIC`. + +Attempting to use `DUSK_INPUT_KEYBOARD` or `DUSK_INPUT_POINTER` causes +a compile-time `#error` in `inputdolphin.h`. + +--- + +## Endianness + +**Both GameCube and Wii are big-endian.** This is the most critical +platform difference from all other targets. + +- All binary asset data (`.dtf` tilesets, STL meshes, DTF headers, etc.) + must be byte-swapped when read on Dolphin. +- Use `endianLittleToHost32` / `endianLittleToHost16` etc. from + `util/endian.h` when reading any multi-byte value from a file. +- Save files are stored in little-endian order; the save stream handles + this transparently via the `saveFile*` macros. +- Network data likewise needs endian conversion. + +See `.claude/util.md` (Endian section) for the full API. + +--- + +## Display + +- Fixed 640x480 resolution, driven by GX (the hardware rasteriser). +- Uses double-buffered framebuffers: + ```c + typedef struct { + void *frameBuffer[2]; // double-buffered + int_t whichFrameBuffer; + GXRModeObj *screenMode; + void *fifoBuffer; // GX command FIFO, 256 KB + } displaydolphin_t; + ``` +- The GX pipeline uses display lists for efficient draw call batching -- + avoid immediate-mode GX calls in the hot path. +- `CONF_GetAspectRatio()` returns `CONF_ASPECT_4_3` on GameCube (always) + and the user's setting on Wii. Use `systemGetAspectRatioDolphin()`. + +--- + +## Asset loading + +Two modes are selected at CMake configure time via +`DUSK_DOLPHIN_BUILD_TYPE`: + +### DOL mode (default -- `DUSK_DOLPHIN_BUILD_TYPE=DOL`) + +Assets are loaded from `dusk.dsk` on a FAT filesystem -- SD card on Wii +(via SD slot), or SD Gecko / SD adapter on GameCube. The loader searches +these paths in order: + +```c +"/", "/Dusk", "/dusk", "/DUSK", +"/apps", "/apps/Dusk", "/apps/dusk", "/apps/DUSK", +".", "./Dusk", "./dusk", ... +``` + +Uses `libfat` for filesystem access. + +### ISO mode (`DUSK_DOLPHIN_BUILD_TYPE=ISO`) + +`dusk.dsk` is read directly off the DVD disc via the libogc DVD driver +(`assetdolphindvd.c`). Reads are 32-byte aligned: + +```c +#define ASSET_DOLPHIN_DVD_ALIGN 32u +``` + +The DVD FST (file-system table) is parsed at init to locate the data +file. All reads go through `assetDolphinDVDRead(offset, size)` which +returns an aligned heap buffer that the caller must free. + +Post-build in ISO mode, `makedolphiniso.py` produces **three disc +images** (NTSC-J, NTSC-U, PAL). + +--- + +## Input + +Uses libogc `PAD` API. Only GameCube controllers are supported (port 0 +by default; up to 4 via `PAD_CHANMAX`). + +Available axes (6 total per controller): + +| Axis | Enum | +|------|------| +| Left stick X/Y | `INPUT_GAMEPAD_AXIS_LEFT_X/Y` | +| C-stick X/Y | `INPUT_GAMEPAD_AXIS_C_X/Y` | +| L trigger | `INPUT_GAMEPAD_AXIS_TRIGGER_LEFT` | +| R trigger | `INPUT_GAMEPAD_AXIS_TRIGGER_RIGHT` | + +Axis values are normalised by dividing the raw 8-bit value by 128.0. +Deadzone: 0.2 (hardcoded). + +Default bindings set at init: D-pad/left stick = directional actions, +A = ACCEPT, B = CANCEL, X = CONSOLE, Start = RAGEQUIT. + +Wii Remote / Nunchuk / Classic Controller / Pro Controller are not yet +implemented (noted as TODO in `inputdolphin.h`). + +--- + +## Save system + +Uses the libogc Memory Card API (`CARD_*`) to read/write save slots. + +```c +typedef struct { + card_file cardFile; + uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32))); + bool_t mounted; +} savedolphin_t; +``` + +- Default channel: `CARD_SLOTA` (Memory Card slot A). + Override via `SAVE_DOLPHIN_CHANNEL`. +- Sector size: 8192 bytes (`SAVE_DOLPHIN_SECTOR_SIZE`). +- Buffers must be 32-byte aligned (enforced by `__attribute__((aligned(32)))`). +- Game code: `DUSK` (4 chars, override via `SAVE_DOLPHIN_GAME_CODE`). +- The card must be mounted before any read/write. `saveInitDolphin()` + mounts slot A; failures are treated as "no save present". +- Save stream handles little-endian encoding transparently -- all data + stored little-endian on the card even though the CPU is big-endian. + +--- + +## Network + +### GameCube + +`net_init()` is commented out. Networking is **non-functional** on +GameCube in the current codebase. The BBA (Broadband Adapter) link +library is in a commented `# bba` in `gamecube.cmake`. + +### Wii + +Uses `if_config()` from libogc which reads Wi-Fi settings saved in the +Wii System Menu. The call **blocks** the main thread until DHCP +completes or fails. Wii network is available only when `DUSK_WII` is +defined; the GameCube path always fails immediately. + +IPv6 is not supported on either Dolphin target. + +--- + +## Time + +- No `DUSK_TIME_DYNAMIC`. All ticks are fixed 16 ms steps. +- Tick source: `__SYS_GetSystemTime()` returns PowerPC bus ticks. +- Real time: ticks converted to microseconds via `ticks_to_microsecs()`, + then offset from the GameCube epoch (2000-01-01 00:00:00) to the UNIX + epoch (1970-01-01 00:00:00) by adding **946 684 800 seconds**. +- Timezone: always returned as 0 -- no timezone data without network time. + +--- + +## System + +Language and aspect ratio queries: + +```c +// Language (used for locale selection): +systemGetLanguageDolphin(); +// -> SYS_GetLanguage() on GameCube +// -> CONF_GetLanguage() on Wii + +// Aspect ratio: +systemGetAspectRatioDolphin(); +// -> CONF_ASPECT_4_3 always on GameCube +// -> CONF_GetAspectRatio() on Wii (4:3 or 16:9) +``` + +--- + +## Build and toolchain + +Requires [devkitPro](https://devkitpro.org/) with `devkitPPC` and +`libogc` installed. + +```sh +# GameCube (SD card / DOL mode) +cmake -B build \ + -DDUSK_TARGET_SYSTEM=gamecube \ + -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/GameCube.cmake \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build + +# Wii (SD card / DOL mode) +cmake -B build \ + -DDUSK_TARGET_SYSTEM=wii \ + -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Wii.cmake \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build + +# Either target in ISO mode +cmake -B build \ + -DDUSK_TARGET_SYSTEM=gamecube \ + -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/GameCube.cmake \ + -DDUSK_DOLPHIN_BUILD_TYPE=ISO \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Post-build outputs (DOL mode): `Dusk.elf` + `Dusk.dol` (generated by +`elf2dol`). Copy `Dusk.dol` and `dusk.dsk` to the SD card. + +Post-build outputs (ISO mode): `Dusk.dol` + disc images in +`NTSC-J/`, `NTSC-U/`, `PAL/` subdirectories. + +Dependencies: libogc, devkitPPC, `fat` (DOL mode), cglm, zip, bz2, +zstd, z, lzma, m. + +--- + +## Gotchas + +- **Big-endian is the most common source of bugs** when porting code + from Linux. Always use `endian.h` utilities for file I/O and network. +- Memory is tight on GameCube -- 16 MB MEM1 must hold code, stack, heap, + framebuffers (2x 640x480x2 bytes), and the GX FIFO (256 KB). +- GX display lists are the correct rendering path; immediate-mode GX + calls carry heavy CPU overhead on the short FIFO pipeline. +- The GameCube has no FPU for integer paths. Avoid `double`; use + `float_t` throughout. +- `consoleInit` is shadowed to `consoleInitDolphin` to avoid conflicts + with the devkitPPC console API. +- On GameCube `CONF_GetAspectRatio()` is always 4:3; the macro is + defined to return `CONF_ASPECT_4_3` unconditionally. +- DVD reads must be 32-byte aligned and padded -- use + `ASSET_DOLPHIN_DVD_ALIGN_UP(n)` when computing read sizes in ISO mode. diff --git a/.claude/platform-linux.md b/.claude/platform-linux.md new file mode 100644 index 00000000..946b5402 --- /dev/null +++ b/.claude/platform-linux.md @@ -0,0 +1,162 @@ +# Platform -- Linux and Knulli + +`DUSK_TARGET_SYSTEM`: `linux` / `knulli` +Source layer: `src/dusklinux/` +Renderer: OpenGL (Linux) / OpenGL ES via EGL (Knulli) + +--- + +## Overview + +Linux is the primary development target. Knulli is a Linux-based handheld +OS (e.g. Anbernic devices); it shares the `src/dusklinux/` layer entirely +and differs only in the CMake target (OpenGL ES instead of desktop OpenGL, +EGL instead of GLX, and no backtrace support). + +Both targets use SDL2 for windowing and input. The window is resizable on +both (`DUSK_DISPLAY_SIZE_DYNAMIC`). + +--- + +## Compile-time macros + +| Macro | Linux | Knulli | +|-------|-------|--------| +| `DUSK_LINUX` | yes | yes | +| `DUSK_KNULLI` | no | yes | +| `DUSK_SDL2` | yes | yes | +| `DUSK_OPENGL` | yes | yes | +| `DUSK_OPENGL_ES` | no | yes | +| `DUSK_DISPLAY_SIZE_DYNAMIC` | yes | yes | +| `DUSK_INPUT_KEYBOARD` | yes | yes | +| `DUSK_INPUT_POINTER` | yes | yes | +| `DUSK_INPUT_GAMEPAD` | yes | yes | +| `DUSK_TIME_DYNAMIC` | yes | yes | +| `DUSK_NETWORK_IPV6` | yes | no | +| `DUSK_THREAD_PTHREAD` | yes | yes | +| `DUSK_CONSOLE_POSIX` | yes | no | + +--- + +## Display + +- Default logical resolution: **640x480** (`DUSK_DISPLAY_WIDTH_DEFAULT` / + `DUSK_DISPLAY_HEIGHT_DEFAULT`); game content renders at + `DUSK_DISPLAY_SCREEN_HEIGHT=240`. +- Dynamic resize: the window can be resized at any time; the engine + letterboxes/scales the logical framebuffer to fit. +- Screen mode is configurable via `SCREEN.mode` (see + `.claude/display-core.md`). +- Knulli uses OpenGL ES (GLES2) linked via EGL. Avoid any desktop + OpenGL extensions that are not in the ES2 core. + +--- + +## Asset loading + +`dusk.dsk` is located by searching a list of paths relative to the +current working directory: + +```c +static const char_t *ASSET_LINUX_SEARCH_PATHS[] = { + "%s", + "../%s", + "../../%s", + "data/%s", + "../data/%s", + NULL +}; +``` + +The first path where `dusk.dsk` is found wins. No packaging step is +required on Linux -- run from the build directory or the project root. + +--- + +## Input + +All three input types are supported: + +- **Keyboard** -- SDL scancode array via `SDL_GetKeyboardState()`. +- **Pointer** -- mouse position normalized to [0, 1], scroll axes. +- **Gamepad** -- first available `SDL_GameController`; axes normalized + to [-1, 1] with a 0.2 deadzone. + +See `.claude/input.md` for the full action/button API. + +--- + +## Save system + +Save files are plain files written to disk. + +- Path: `./saves/save_N.dat` (override `SAVE_LINUX_PATH` to change the + directory at CMake configure time). +- Format: `SAVE_LINUX_FILE_FORMAT = "%s/save_%u.dat"` where `%u` is the + slot index. +- No OS-level dialog blocking -- saves are synchronous filesystem calls. +- Endian: host byte order (little-endian on x86/ARM). + +--- + +## Network + +- Connection is detected automatically via `getifaddrs()`. No explicit + connect step is needed. +- `networkRequestConnection` immediately calls `onConnected` if any + non-loopback interface is up, `onFailed` otherwise. +- IPv4 and IPv6 supported (`DUSK_NETWORK_IPV6`). + +--- + +## Time + +- Tick source: `SDL_GetTicks64()`. +- Real time: `clock_gettime(CLOCK_REALTIME)`. +- Dynamic timestep enabled (`DUSK_TIME_DYNAMIC`). + +--- + +## Threading + +pthreads (`DUSK_THREAD_PTHREAD`). Thread-local storage via `__thread`. + +--- + +## Build and toolchain + +No cross-compiler needed -- use the host GCC/Clang. + +```sh +# Debug build +cmake -B build -DDUSK_TARGET_SYSTEM=linux -DCMAKE_BUILD_TYPE=Debug +cmake --build build + +# Knulli (cross-compile to aarch64) +cmake -B build \ + -DDUSK_TARGET_SYSTEM=knulli \ + -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/aarch64-linux-gnu.cmake \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Dependencies: `SDL2`, `OpenGL` (Linux) or `GLES2` + `EGL` (Knulli), +`pthread`, `m`. + +--- + +## Endianness + +Little-endian. Detected at CMake configure time via `TestBigEndian` and +set as `DUSK_PLATFORM_ENDIAN_LITTLE` or `DUSK_PLATFORM_ENDIAN_BIG`. + +--- + +## Gotchas + +- `DUSK_CONSOLE_POSIX` enables POSIX-specific assert backtracing (Linux + only; Knulli does not set it). +- Knulli does not set `DUSK_NETWORK_IPV6` -- IPv6 may not be available + on handheld devices. +- `DUSK_TIME_DYNAMIC` is set, so physics/networking skip dynamic sub-steps + by checking `if(TIME.dynamicUpdate) return;`. diff --git a/.claude/platform-macos.md b/.claude/platform-macos.md new file mode 100644 index 00000000..725fc6c5 --- /dev/null +++ b/.claude/platform-macos.md @@ -0,0 +1,46 @@ +# Platform -- macOS + +`DUSK_TARGET_SYSTEM`: `macos` +Source layer: `src/duskmacos/` (planned, does not exist yet) +Status: **Planned -- not yet implemented** + +--- + +## Overview + +macOS desktop is a planned target. No source layer, CMake target file, +or toolchain exists yet. The intended architecture mirrors Linux: SDL2 +for windowing/input, OpenGL (or Metal via MoltenVK/SDL2) for rendering. + +--- + +## Expected macros (when implemented) + +| Macro | Expected | +|-------|---------| +| `DUSK_MACOS` | yes | +| `DUSK_SDL2` | yes | +| `DUSK_OPENGL` | yes | +| `DUSK_DISPLAY_SIZE_DYNAMIC` | yes | +| `DUSK_INPUT_KEYBOARD` | yes | +| `DUSK_INPUT_POINTER` | yes | +| `DUSK_INPUT_GAMEPAD` | yes | +| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes | +| `DUSK_TIME_DYNAMIC` | yes | +| `DUSK_THREAD_PTHREAD` | yes | + +--- + +## Notes + +- Will be little-endian (Apple Silicon and Intel x86-64). +- Apple deprecated OpenGL on macOS in 10.14 (Mojave). The implementation + will need to either target the deprecated OpenGL path or use MoltenVK + (Vulkan-over-Metal) with an SDL2 OpenGL layer. This decision is + pending. +- Save files will likely live in `~/Library/Application Support/`. +- Expected to share `src/dusksdl2/` and `src/duskgl/` with Linux. +- Toolchain: native Clang via Xcode Command Line Tools, or a + cross-compile from Linux with osxcross. + +Update this document when the macOS target is implemented. diff --git a/.claude/platform-psp.md b/.claude/platform-psp.md new file mode 100644 index 00000000..eacdd2d6 --- /dev/null +++ b/.claude/platform-psp.md @@ -0,0 +1,200 @@ +# Platform -- Sony PSP + +`DUSK_TARGET_SYSTEM`: `psp` +Source layer: `src/duskpsp/` +Renderer: OpenGL ES (legacy, via PSPGL/SDL2) + +--- + +## Overview + +The PSP is a 32 MB MIPS-based handheld console running at up to 333 MHz. +It uses SDL2 (ported to PSP) for windowing and OpenGL in legacy/fixed- +function mode. The game binary and all assets are packaged together inside +a `.pbp` file -- the PSP's native executable format. + +--- + +## Hardware + +| Attribute | Value | +|-----------|-------| +| CPU | MIPS R4000 (Allegrex), up to 333 MHz | +| RAM | 32 MB (4 MB reserved for OS) | +| Display | 480x272, 16/32-bit colour | +| Storage | Memory Stick (UMD for retail; MS for homebrew) | +| Endian | Little-endian | + +--- + +## Compile-time macros + +| Macro | Set | +|-------|-----| +| `DUSK_PSP` | yes | +| `DUSK_SDL2` | yes | +| `DUSK_OPENGL` | yes | +| `DUSK_OPENGL_LEGACY` | yes | +| `DUSK_INPUT_GAMEPAD` | yes | +| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes | +| `DUSK_DISPLAY_WIDTH` | 480 | +| `DUSK_DISPLAY_HEIGHT` | 272 | +| `DUSK_THREAD_PTHREAD` | yes | + +No `DUSK_DISPLAY_SIZE_DYNAMIC` -- the resolution is fixed. +No `DUSK_INPUT_KEYBOARD`, no `DUSK_INPUT_POINTER`. + +--- + +## Display + +- Fixed 480x272 resolution. +- OpenGL legacy (fixed-function pipeline, `DUSK_OPENGL_LEGACY`). +- Texture dimensions **must** be powers of two (use `mathNextPowTwo`). +- VFPU (4-wide float SIMD) is available -- use it for matrix/vector hot + paths under `#ifdef DUSK_PSP`. + +--- + +## Asset loading + +Assets are packed into the **PSAR** section of the `.pbp` file by the +post-build `create_pbp_file()` CMake command. At runtime, +`assetInitPBP()` locates and opens the PSAR from the running executable's +path. + +The PBP format header: +```c +typedef struct { + char_t signature[4]; // "\0PBP" + uint32_t version; + uint32_t sfoOffset; + uint32_t icon0Offset; + uint32_t icon1Offset; + uint32_t pic0Offset; + uint32_t pic1Offset; + uint32_t snd0Offset; + uint32_t pspOffset; + uint32_t psarOffset; // dusk.dsk starts here +} assetpbpheader_t; +``` + +`assetpbp_t` holds the open file handle and parsed header. Asset paths +inside the PSAR are ZIP paths within `dusk.dsk`. + +--- + +## Input + +Layered on SDL2. `inputInitPSP()` maps PSP physical buttons to SDL2 +`SDL_CONTROLLER_BUTTON_*` constants: + +| PSP button | Action | +|------------|--------| +| Cross | Accept | +| Circle | Cancel | +| Triangle | - | +| Square | - | +| L / R | Shoulder buttons | +| D-pad | Directional | +| L-Stick | Analog axes | + +No keyboard or pointer input available. Attempting to use +`INPUT_BUTTON_TYPE_KEYBOARD` on PSP is undefined behaviour. + +The PSP system setting `PSP_UTILITY_ACCEPT_CROSS` / `ACCEPT_CIRCLE` +swaps the Cross and Circle button roles in OS dialogs -- read this via +`systemPSPGetCrossButtonSetting()` if you need to match the system +convention. + +--- + +## Save system + +- Path: `ms0:/PSP/SAVEDATA//save.dat` + (default title ID `DUSK00001`, configurable via `SAVE_PSP_TITLE_ID`). +- Uses `sceIo` for file I/O -- no extra dialog required for raw reads. +- PSP OS-level save/load dialogs (via `sceUtility`) are separate and + block the main loop when open (`systemGetActiveDialogType()` returns + `SYSTEM_DIALOG_TYPE_TICK_BLOCKING`). +- Do not call save functions directly from game code during a dialog. + +--- + +## Network + +Connection requires an explicit user Wi-Fi selection step via the PSP +system network dialog (`sceUtilityNetconfInitStart`). + +``` +networkRequestConnection(onConnected, onFailed, onDisconnect, user); +// -> shows PSP Wi-Fi selection dialog (blocking dialog type) +// -> calls onConnected or onFailed when the dialog closes +``` + +HTTP and SSL modules (`psphttp`, `pspssl`, `pspnet_resolver`) are +linked in `psp.cmake` but the HTTP implementation code is commented out. +The infrastructure exists for future use. + +--- + +## Time + +- Tick source: `SDL_GetTicks64()`. +- Real time: `sceRtcGetCurrentTick()` (returns microseconds). +- Dynamic timestep is **not** enabled (`DUSK_TIME_DYNAMIC` not set). + Every tick is a fixed 16 ms step. + +--- + +## System dialogs + +PSP shows OS-level dialogs for: +- Wi-Fi configuration (`networkRequestConnection`) +- Save management (if using `sceUtility` save dialogs) + +Check `systemGetActiveDialogTypePSP()` to know whether the main loop +should skip rendering or ticking. + +--- + +## Build and toolchain + +Requires the [PSPDEV toolchain](https://github.com/pspdev/pspdev). +Set `PSPDEV` in your environment before configuring. + +```sh +cmake -B build \ + -DDUSK_TARGET_SYSTEM=psp \ + -DCMAKE_TOOLCHAIN_FILE=${PSPDEV}/lib/cmake/psp.cmake \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Post-build output: `Dusk.pbp` (executable + assets combined). + +Dependencies: SDL2-PSP, OpenGL-PSP, pspgu, pspctrl, pspdisplay, +pspaudio, pspaudiolib, psputility, pspvfpu, pspvram, pspnet, +pspnet_inet, pspnet_apctl, psphttp, pspssl, pspdebug, psphprm, +mbedtls, mbedcrypto, lzma, zip, bz2, z. + +--- + +## Endianness + +Little-endian. `DUSK_PLATFORM_ENDIAN_LITTLE` is set at compile time. +No runtime endian check is needed. + +--- + +## Gotchas + +- The PSP has only 28 MB of usable RAM after the OS. Keep asset budgets + tight -- see `.claude/optimization.md`. +- VFPU instructions are not valid on threads other than the main thread + on some firmware versions. Use `assertIsMainThread` on any code that + calls VFPU intrinsics. +- OpenGL legacy mode means no vertex/fragment shaders; rendering uses + the fixed-function pipeline via `pspgl`. +- `DUSK_TIME_DYNAMIC` is absent -- physics always runs at exactly the + fixed step rate. diff --git a/.claude/platform-vita.md b/.claude/platform-vita.md new file mode 100644 index 00000000..f2955053 --- /dev/null +++ b/.claude/platform-vita.md @@ -0,0 +1,173 @@ +# Platform -- PlayStation Vita + +`DUSK_TARGET_SYSTEM`: `vita` +Source layer: `src/duskvita/` +Renderer: vitaGL (OpenGL-over-GXM compatibility layer) + +--- + +## Overview + +The PlayStation Vita is an ARM-based handheld with 512 MB of RAM and a +960x544 OLED/LCD display. It uses SDL2 (ported to Vita) for input +abstraction, but the graphics layer is vitaGL -- an OpenGL compatibility +shim that translates OpenGL calls to Sony's native GXM API. + +The distribution format is a `.vpk` (Vita Package) file containing the +signed executable and `dusk.dsk` bundled as `dusk.dsk` at the package +root, accessible at `app0:/dusk.dsk` at runtime. + +--- + +## Hardware + +| Attribute | Value | +|-----------|-------| +| CPU | ARM Cortex-A9 quad-core, ~444 MHz | +| RAM | 512 MB | +| Display | 960x544 | +| Storage | Vita game card / memory card / internal flash | +| Endian | Little-endian | + +--- + +## Compile-time macros + +| Macro | Set | +|-------|-----| +| `DUSK_VITA` | yes | +| `DUSK_SDL2` | yes | +| `DUSK_OPENGL` | yes | +| `DUSK_OPENGL_LEGACY` | yes | +| `DUSK_INPUT_GAMEPAD` | yes | +| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes | +| `DUSK_DISPLAY_WIDTH` | 960 | +| `DUSK_DISPLAY_HEIGHT` | 544 | + +No `DUSK_DISPLAY_SIZE_DYNAMIC`, no `DUSK_TIME_DYNAMIC`, +no `DUSK_INPUT_KEYBOARD`, no `DUSK_INPUT_POINTER`, +no `DUSK_NETWORK_IPV6`. + +--- + +## Display + +- Fixed 960x544 resolution. +- vitaGL translates OpenGL calls to GXM. Some OpenGL calls are stubbed + out in `duskplatform.h` where vitaGL does not support them: + + ```c + #define glDrawArrays(type, first, count) ((void)0) + #define glDepthFunc(func) ((void)0) + #define glBlendFunc(sfactor, dfactor) ((void)0) + #define glColorTableEXT(...) ((void)0) + ``` + + These stubs mean the Vita uses the fixed-function pipeline through + vitaGL. Do not rely on `glDrawArrays` or depth/blend state changes + being applied -- use the engine's `displaystate_t` flags instead + (see `.claude/display-shader.md`). +- `DUSK_OPENGL_LEGACY` is set. Avoid shader-based features that are + not in the fixed-function ES1 subset. +- Texture dimensions **must** be powers of two. + +--- + +## Asset loading + +`dusk.dsk` is bundled inside the `.vpk` and mounted at `app0:/` by the +Vita OS. The asset system opens it at the fixed path: + +```c +#define ASSET_VITA_DSK_PATH "app0:/" ASSET_FILE_NAME +``` + +No path search is needed -- the file is always at that location. + +--- + +## Input + +Uses SDL2 with Vita button mapping. Buttons map to SDL2 gamepad +constants: + +| Vita button | SDL2 constant | +|-------------|---------------| +| Triangle | `SDL_CONTROLLER_BUTTON_Y` | +| Cross | `SDL_CONTROLLER_BUTTON_A` | +| Circle | `SDL_CONTROLLER_BUTTON_B` | +| Square | `SDL_CONTROLLER_BUTTON_X` | +| Start | `SDL_CONTROLLER_BUTTON_START` | +| Select | `SDL_CONTROLLER_BUTTON_BACK` | +| D-pad | `SDL_CONTROLLER_BUTTON_DPAD_*` | +| L / R | `SDL_CONTROLLER_BUTTON_LEFTSHOULDER / RIGHTSHOULDER` | +| L-Stick | `SDL_CONTROLLER_AXIS_LEFTX/Y` | + +Vita also has L2, R2, L3, R3 and touch surfaces -- not currently wired +into the input system. + +--- + +## Save system + +Uses `SceIofilemgr` (Vita filesystem API) for file I/O. Save data lives +in the application's sandbox on the memory card. The stream API +(`savestream_t`) handles all serialization with automatic CRC32 and +little-endian encoding (see `.claude/save.md`). + +--- + +## Network + +No network implementation exists in `src/duskvita/`. Network +functionality is not currently available on Vita. + +--- + +## Time + +No `src/duskvita/time/` directory exists -- the Vita time implementation +falls back to the SDL2 time layer if available, or is not yet +implemented. + +--- + +## Build and toolchain + +Requires the [VITASDK](https://vitasdk.org/). Set `VITASDK` in your +environment before configuring. + +```sh +cmake -B build \ + -DDUSK_TARGET_SYSTEM=vita \ + -DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.cmake \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Post-build output: `Dusk.self` (signed executable) and `Dusk.vpk` +(installable package containing the SELF + `dusk.dsk`). + +Title ID: `DUSK00001` (configurable via `VITA_TITLEID`). + +Dependencies: SDL2, vitaGL, mathneon, vitashark, kubridge, SceGxm, +SceCtrl, SceAudio, SceTouch, SceRtc, SceAppUtil, zip, bz2, z, lzma. + +--- + +## Endianness + +Little-endian. `DUSK_PLATFORM_ENDIAN_LITTLE` is set at compile time. + +--- + +## Gotchas + +- vitaGL stubs several OpenGL calls. Always use the engine display state + API rather than calling `glBlendFunc` / `glDepthFunc` directly. +- `DUSK_TIME_DYNAMIC` is not set -- all ticks are fixed-step 16 ms. +- Threading: `pthread` is linked but `DUSK_THREAD_PTHREAD` is not + explicitly defined in `vita.cmake`. Verify threading behaviour before + relying on it. +- The Vita implementation is less complete than Linux and PSP -- network + and time platform layers are absent. Contributions welcome. diff --git a/.claude/platform-windows.md b/.claude/platform-windows.md new file mode 100644 index 00000000..ff086149 --- /dev/null +++ b/.claude/platform-windows.md @@ -0,0 +1,46 @@ +# Platform -- Windows + +`DUSK_TARGET_SYSTEM`: `windows` +Source layer: `src/duskwindows/` (planned, does not exist yet) +Status: **Planned -- not yet implemented** + +--- + +## Overview + +Windows desktop is a planned target. No source layer, CMake target file, +or toolchain exists yet. The intended architecture closely mirrors Linux: +SDL2 for windowing/input, desktop OpenGL for rendering, pthreads (via +MinGW or MSVC pthreads shim) for threading. + +--- + +## Expected macros (when implemented) + +| Macro | Expected | +|-------|---------| +| `DUSK_WINDOWS` | yes | +| `DUSK_SDL2` | yes | +| `DUSK_OPENGL` | yes | +| `DUSK_DISPLAY_SIZE_DYNAMIC` | yes | +| `DUSK_INPUT_KEYBOARD` | yes | +| `DUSK_INPUT_POINTER` | yes | +| `DUSK_INPUT_GAMEPAD` | yes | +| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes | +| `DUSK_TIME_DYNAMIC` | yes | +| `DUSK_THREAD_PTHREAD` | yes | + +--- + +## Notes + +- Will be little-endian (x86-64 Windows). +- Expected to share `src/dusksdl2/` and `src/duskgl/` with Linux and + Knulli; only a thin `src/duskwindows/` layer for OS-specific + functionality (save paths, system dialogs) should be needed. +- Save files will likely live in `%APPDATA%` or a sibling `saves/` + directory. +- No cross-compiler needed; MSVC or MinGW-w64 on Windows or a + cross-compile from Linux. + +Update this document when the Windows target is implemented. diff --git a/.claude/platforms.md b/.claude/platforms.md new file mode 100644 index 00000000..6ae4aa6a --- /dev/null +++ b/.claude/platforms.md @@ -0,0 +1,98 @@ +# Platform Support + +Dusk targets a wide range of platforms, from modern desktops to classic +handheld and home consoles. New platform targets will be added over time. + +## Platform index + +| Platform | `DUSK_TARGET_SYSTEM` | Status | Reference | +|----------|----------------------|--------|-----------| +| Linux | `linux` | Supported | `.claude/platform-linux.md` | +| Knulli | `knulli` | Supported | `.claude/platform-linux.md` | +| Windows | `windows` | Planned | `.claude/platform-windows.md` | +| macOS | `macos` | Planned | `.claude/platform-macos.md` | +| Sony PSP | `psp` | Supported | `.claude/platform-psp.md` | +| PlayStation Vita | `vita` | Supported | `.claude/platform-vita.md` | +| Nintendo GameCube | `gamecube` | Supported | `.claude/platform-dolphin.md` | +| Nintendo Wii | `wii` | Supported | `.claude/platform-dolphin.md` | + +GameCube and Wii share the `src/duskdolphin/` layer and are collectively +referred to as **Dolphin** targets throughout the codebase. + +--- + +## Layer structure + +``` +src/dusk/ Core -- platform-agnostic game logic and ECS +src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita) +src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita) +src/dusklinux/ Linux + Knulli platform impl +src/duskpsp/ PSP platform impl +src/duskvita/ Vita platform impl +src/duskdolphin/ GameCube + Wii platform impl +``` + +Dolphin is the only target that bypasses SDL2 and OpenGL entirely -- +it uses native libogc GX for rendering and PAD for input. + +--- + +## Capability macros + +Each target sets a combination of these macros. Do not assume a +capability is present without checking the appropriate macro. + +| Macro | Meaning | +|-------|---------| +| `DUSK_SDL2` | SDL2 is available | +| `DUSK_OPENGL` | OpenGL is available | +| `DUSK_OPENGL_ES` | OpenGL ES variant (Knulli) | +| `DUSK_OPENGL_LEGACY` | Fixed-function OpenGL (PSP, Vita) | +| `DUSK_INPUT_GAMEPAD` | Gamepad / controller input | +| `DUSK_INPUT_KEYBOARD` | Keyboard input (Linux, Knulli only) | +| `DUSK_INPUT_POINTER` | Mouse / pointer input (Linux, Knulli only) | +| `DUSK_DISPLAY_SIZE_DYNAMIC` | Window is resizable (Linux, Knulli) | +| `DUSK_TIME_DYNAMIC` | Dynamic timestep available (Linux, Knulli) | +| `DUSK_THREAD_PTHREAD` | pthreads available | +| `DUSK_NETWORK_IPV6` | IPv6 supported (Linux only) | +| `DUSK_PLATFORM_ENDIAN_BIG` | Big-endian byte order | +| `DUSK_PLATFORM_ENDIAN_LITTLE` | Little-endian byte order | +| `DUSK_DOLPHIN` | Any Dolphin target | +| `DUSK_DOLPHIN_BUILD_ISO` | Dolphin DVD-ISO asset mode | +| `DUSK_CONSOLE_POSIX` | POSIX assert backtrace (Linux only) | + +--- + +## Quick comparison + +| | Linux | Knulli | PSP | Vita | GameCube | Wii | +|-|-------|--------|-----|------|----------|-----| +| SDL2 | yes | yes | yes | yes | no | no | +| OpenGL | desktop | ES2 | legacy | vitaGL | no | no | +| Endian | little | little | little | little | **big** | **big** | +| Dynamic resize | yes | yes | no | no | no | no | +| Dynamic timestep | yes | yes | no | no | no | no | +| Keyboard | yes | yes | no | no | no | no | +| Pointer/mouse | yes | yes | no | no | no | no | +| Network | full | full | partial | no | no | partial | +| Save storage | file | file | MS/SAVEDATA | SceIo | Mem Card | SD/NAND | +| Asset source | dsk file | dsk file | inside .pbp | inside .vpk | SD or DVD | SD or DVD | + +--- + +## Abstraction pattern + +Platform-specific implementations are wired in via `#define` macros in +each platform's `displayplatform.h`, `inputplatform.h`, etc., which the +core calls through. Functions that a platform does not support are +simply left undefined -- the core guards calls with `#ifdef`. + +## Adding platform-specific code + +- Put new code under `src/dusk/` in the matching subsystem + folder. +- Gate any core call-site with `#ifdef DUSK_` or the + relevant capability macro. +- Keep `src/dusk/` free of platform `#ifdef`s -- delegate through + the platform header macros instead. diff --git a/.claude/save.md b/.claude/save.md new file mode 100644 index 00000000..98733e49 --- /dev/null +++ b/.claude/save.md @@ -0,0 +1,148 @@ +# Save System + +Source: `src/dusk/save/`, platform layers in `src/dusk/save/` + +## Overview + +The save system provides multi-slot persistent storage. Each slot +maps to one `savefile_t`. Platform implementations handle the actual +read/write (memory card on GameCube/Wii, EEPROM/flash on PSP, +filesystem on Linux). + +## Global state + +```c +extern save_t SAVE; +// SAVE.files[SAVE_FILE_COUNT_MAX] -- one per slot +// SAVE.platform -- platform-specific state +``` + +## API + +```c +errorret_t saveInit(void); +errorret_t saveDispose(void); + +errorret_t saveLoad(uint8_t slot); // read slot from storage -> SAVE.files[slot] +errorret_t saveWrite(uint8_t slot); // write SAVE.files[slot] -> storage +errorret_t saveDelete(uint8_t slot); // delete slot from storage + +bool_t saveExists(uint8_t slot); // true if a save file is present + +savefile_t *saveGet(uint8_t slot); // pointer to the in-memory slot data +``` + +Slot indices are 0-based, range `[0, SAVE_FILE_COUNT_MAX - 1]`. + +## Save file structure (`savefile.h`) + +`savefile_t` is a plain struct written verbatim to storage. Keep it +small and use fixed-width integer types (`uint8_t`, `int32_t`, etc.) +to ensure cross-platform binary compatibility. + +**Endianness:** storage is always written in little-endian byte order. +Use the `endian.h` utilities when reading fields on big-endian targets +(GameCube, Wii). See `.claude/util.md`. + +**Versioning:** include a version field at the start of `savefile_t`. +Check it on load and handle mismatches gracefully (reset to defaults +rather than crashing on corrupt data). + +## Save stream (`savestream.h`) + +`savestream_t` is a cursor-based reader/writer used to serialize +`savefile_t` to/from a raw byte buffer. Platform implementations +use it to abstract the I/O layer. + +```c +typedef struct { + bool_t found; + uint32_t checksum; + uint32_t expectedChecksum; + saveplatformstream_t platform; +} savestream_t; +``` + +### Typed read/write macros + +Use the `saveFile*` macros inside `saveFileLoad` and `saveFileWrite`. +All multi-byte values are stored in little-endian order; endian +conversion is handled automatically. + +```c +saveFileReadHeader(stream, headerBuf) +saveFileWriteHeader(stream, headerBuf) + +saveFileReadVersion(stream, &version) +saveFileWriteVersion(stream, &version) + +saveFileReadBool(stream, &boolField) +saveFileWriteBool(stream, &boolField) + +saveFileReadInt8(stream, &i8) saveFileWriteInt8(stream, &i8) +saveFileReadUInt8(stream, &u8) saveFileWriteUInt8(stream, &u8) +saveFileReadInt16(stream, &i16) saveFileWriteInt16(stream, &i16) +saveFileReadUInt16(stream, &u16) saveFileWriteUInt16(stream, &u16) +saveFileReadInt32(stream, &i32) saveFileWriteInt32(stream, &i32) +saveFileReadUInt32(stream, &u32) saveFileWriteUInt32(stream, &u32) +saveFileReadInt64(stream, &i64) saveFileWriteInt64(stream, &i64) +saveFileReadUInt64(stream, &u64) saveFileWriteUInt64(stream, &u64) +saveFileReadFloat(stream, &f) saveFileWriteFloat(stream, &f) + +saveFileReadString(stream, buf, maxLen) +saveFileWriteString(stream, str, maxLen) + +saveFileReadDate(stream, &epoch) +saveFileWriteDate(stream, &epoch) +``` + +Each macro expands to `errorChain(saveStreamRead/WriteXxxImpl(...))`. +A failing read/write propagates the error up from `saveFileLoad` / +`saveFileWrite`. + +### Typical saveFileLoad / saveFileWrite pattern + +```c +errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) { + char_t header[SAVE_FILE_HEADER_SIZE]; + saveFileReadHeader(stream, header); + saveFileReadVersion(stream, &file->version); + saveFileReadInt32(stream, &file->score); + // ... remaining fields ... + errorOk(); +} + +errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) { + char_t header[SAVE_FILE_HEADER_SIZE] = SAVE_FILE_HEADER; + saveFileWriteHeader(stream, header); + saveFileWriteVersion(stream, &file->version); + saveFileWriteInt32(stream, &file->score); + // ... remaining fields ... + errorOk(); +} +``` + +After `saveFileWrite` completes, the platform layer calls +`saveStreamFinalizeWriteImpl` which seeks back and writes the CRC32. +After `saveFileLoad`, the platform calls `saveStreamVerifyChecksumImpl` +to confirm the CRC matches. + +## Platform notes + +| Platform | Storage mechanism | +|----------|------------------| +| Linux | File in user home / working directory | +| Knulli | File on filesystem | +| PSP | EEPROM / memory stick via `sceIo` | +| GameCube | Memory Card via libogc `CARD_*` API | +| Wii | NAND filesystem via libogc or SD card | + +Platform-specific save implementations go in `src/dusk/save/` +and are wired in via `save/saveplatform.h` macros. + +## PSP note + +PSP save dialogs are OS-level UI shown via `sceUtility`. When a dialog +is open, `systemGetActiveDialogType()` returns a blocking type so the +engine pauses the main loop. Never call save functions directly from +game code without going through the engine's dialog guard. diff --git a/.claude/scene.md b/.claude/scene.md new file mode 100644 index 00000000..d8fc0589 --- /dev/null +++ b/.claude/scene.md @@ -0,0 +1,110 @@ +# Scene System + +Source: `src/dusk/scene/` + +## Overview + +The scene system is the top-level coordinator for a running game state. +It manages one active scene at a time. Scenes are JS scripts -- each +scene is a `.js` asset file that exports an object with lifecycle hooks. +The scene system loads, ticks, and tears down these scripts, while the +C side runs the ECS and render pipeline on each tick. + +## Scene lifecycle (C side) + +```c +extern scene_t SCENE; + +errorret_t sceneInit(void); // initialise the scene manager +errorret_t sceneUpdate(void); // process pending transition, tick active scene +errorret_t sceneRender(void); // render entities + render pipeline + UI +errorret_t sceneDispose(void); // dispose the active scene immediately +``` + +`sceneUpdate` each tick: +1. Checks for a pending scene transition and performs it (dispose old, + load and init new). +2. Calls the JS scene's `update()` hook. +3. Calls `entityManagerUpdate()` to fire all entity update callbacks. + +`sceneRender` each tick: +1. Binds the screen. +2. Calls `sceneRenderPipeline()` -- renders all entities with a + `COMPONENT_TYPE_RENDERABLE` in priority order. +3. Renders UI. +4. Calls the JS scene's `render()` hook (for any custom drawing). +5. Unbinds the screen. + +## Scene lifecycle (JS side) + +A scene file exports a plain object with these optional hooks: + +```js +var scene = {}; + +scene.init = async function() { + // Load assets, create entities, set up state. + // May be async -- await asset loads here. +}; + +scene.update = function() { + // Called each fixed-timestep tick. +}; + +scene.render = function() { + // Called each render tick, after ECS renderables. +}; + +scene.dispose = function() { + // Clean up entities and state. +}; + +module.exports = scene; +``` + +See `CLAUDE.md` -- "JavaScript (asset scripts)" for JS style rules. + +## Render pipeline (`scenerenderpipeline.h`) + +`sceneRenderPipeline(cameraEntityId)` gathers all active +`COMPONENT_TYPE_RENDERABLE` components, sorts them by effective +priority, and draws each one using its shader. + +**Priority rules:** +- `renderable.priority != 0` -- use that value directly. +- `renderable.priority == 0` -- auto-derive: opaque geometry sorts + before transparent geometry; sprite batches sort before shader + materials; etc. +- Lower priority number = drawn first (behind); higher = drawn last + (on top). + +The shader used for each renderable: +- `ENTITY_RENDERABLE_TYPE_SPRITEBATCH` and `CUSTOM` default to + `SHADER_LIST_SHADER_UNLIT`. +- `ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL` uses the shader indexed by + `renderable.data.material.shaderType` in `SHADER_LIST_DEFS`. + +## Transitioning between scenes + +To move to a new scene from JS, call the scene module's transition +function (exact API in the `scene` JS module). The C side defers the +actual switch to the start of the next `sceneUpdate` call so the +current tick completes cleanly before any dispose runs. + +## Relationship to the engine loop + +``` +engineUpdate() + timeUpdate() + inputUpdate() + physicsManagerUpdate() + scriptUpdate() <- runs JS microjobs + sceneUpdate() <- JS update + ECS entity updates + +engineUpdate() -> sceneRender() + screenBind() + sceneRenderPipeline() <- ECS renderables sorted by priority + uiRender() + sceneRender (JS hook) + screenUnbind / screenRender +``` diff --git a/.claude/script-promises.md b/.claude/script-promises.md new file mode 100644 index 00000000..86a7af32 --- /dev/null +++ b/.claude/script-promises.md @@ -0,0 +1,104 @@ +# Script -- Async Promises (`scriptpromisepend_t`) + +Source: `src/dusk/script/scriptpromisepend.h` + +See also: `.claude/script.md` + +--- + +## Overview + +When a C module needs to resolve a JS `Promise` from an asynchronous +C event (e.g. an asset finishing loading, a network response arriving), +use `scriptpromisepend_t`. The pattern avoids heap allocation by using +a fixed-size pending slot array declared in the module. + +--- + +## Declaring the pending array + +```c +#define MY_MODULE_PENDING_MAX 8 +static scriptpromisepend_t MY_PENDING[MY_MODULE_PENDING_MAX]; +static uint32_t MY_PENDING_COUNT = 0; +``` + +--- + +## Add a pending promise + +Called from the JS-facing function that returns the Promise: + +```c +jerry_value_t promise = jerry_create_promise(); +scriptPromisePendAdd( + MY_PENDING, &MY_PENDING_COUNT, MY_MODULE_PENDING_MAX, + key, // opaque void * used to match the resolve/reject later + promise +); +return jerry_acquire_value(promise); // return a copy to the caller +``` + +The `key` should be a stable pointer that uniquely identifies the +async operation -- e.g. an `assetentry_t *`, a network request handle, +or a pointer to a fixed-size slot in the module. + +--- + +## Resolve or reject + +Called when the C event fires, typically from `moduleUpdate` or an +event callback: + +```c +// On success: +scriptPromisePendResolve( + MY_PENDING, &MY_PENDING_COUNT, + key, jerry_undefined() // or a result value +); + +// On failure: +jerry_value_t err = jerry_create_error( + JERRY_ERROR_COMMON, (const jerry_char_t *)"reason" +); +scriptPromisePendReject(MY_PENDING, &MY_PENDING_COUNT, key, err); +jerry_release_value(err); +``` + +Both macros remove the slot from the pending array after settling. + +--- + +## Guard against double-submit + +```c +if(scriptPromisePendHas(MY_PENDING, MY_PENDING_COUNT, key)) { + // already waiting -- return the existing promise or an error +} +``` + +--- + +## Module teardown + +Free all pending promises before cleaning up events or other state: + +```c +scriptPromisePendFreeAll(MY_PENDING, &MY_PENDING_COUNT); +``` + +This rejects all still-pending promises and resets the count to 0. +Call it from the module's `Dispose` function, **before** any backing +data (asset entries, event subscriptions) is torn down. + +--- + +## Design notes + +- `MY_MODULE_PENDING_MAX` sets a hard cap on concurrent async ops. + Exceeding it is a runtime assertion -- size the array to the maximum + realistic concurrency for the module. +- The key is opaque (`void *`); the system does not dereference it. + A raw integer cast to `void *` is fine if no pointer is available. +- `scriptUpdate()` runs the JerryScript microjob queue each frame, + which is what processes `.then()` chains after a resolve/reject. diff --git a/.claude/script.md b/.claude/script.md new file mode 100644 index 00000000..a94b8fc0 --- /dev/null +++ b/.claude/script.md @@ -0,0 +1,167 @@ +# Script System (JerryScript) + +Source: `src/dusk/script/`, modules at `src/dusk/script/module/` + +## Overview + +The engine embeds **JerryScript** as its scripting runtime. Game scenes +and logic are authored in JavaScript (ES5 subset). The script system +initialises JerryScript, registers all built-in C modules as JS globals, +and runs the event loop each tick. + +The full rules for writing JS asset scripts are in `CLAUDE.md` under +"JavaScript (asset scripts)". This doc covers the C-side module system. + +## Script lifecycle + +```c +errorret_t scriptInit(); // start JerryScript, register all modules +errorret_t scriptUpdate(); // run pending microjobs (call once per frame) +errorret_t scriptDispose(); // shut down JerryScript + +errorret_t scriptExecString(const char_t *source); +// Evaluate a JS source string in global scope. + +errorret_t scriptExecFile(const char_t *path); +// Load + eval a script from the asset archive. Result cached by asset +// system -- repeated calls with the same path do not re-execute. +``` + +## Module registration + +All C modules are initialised in `src/dusk/script/module/modulelist.c`: + +```c +void moduleListInit(void); // called by scriptInit +void moduleListDispose(void); // called by scriptDispose +``` + +Each module's `Init` is called once. The module registers its +properties and methods on `scriptproto_t` objects (see below), which +become JS globals. + +## Writing a C module -- the `scriptproto_t` pattern + +A `scriptproto_t` represents a JS class prototype backed by a C struct. + +### 1. Declare in the header + +```c +// moduleMything.h +extern scriptproto_t MODULE_MYTHING_PROTO; + +// Init and dispose for the module itself: +void moduleMyThingInit(void); +void moduleMyThingDispose(void); +``` + +### 2. Implement + +```c +// moduleMything.c +scriptproto_t MODULE_MYTHING_PROTO; + +// JS-callable function using the convenience macro: +moduleBaseFunction(myThingDoSomething) { + moduleBaseRequireArgs(1); + moduleBaseRequireNumber(0); + float_t x = moduleBaseArgFloat(0); + // ... do work ... + return jerry_undefined(); +} + +void moduleMyThingInit(void) { + scriptProtoInit( + &MODULE_MYTHING_PROTO, + "MyThing", // JS global name; NULL to skip registration + sizeof(mything_t), + myThingCtor // constructor handler, or NULL + ); + + // Instance methods: + scriptProtoDefineFunc( + &MODULE_MYTHING_PROTO, "doSomething", myThingDoSomething + ); + + // Instance property (get/set): + scriptProtoDefineProp( + &MODULE_MYTHING_PROTO, "x", myThingGetX, myThingSetX + ); + + // Static method: + scriptProtoDefineStaticFunc( + &MODULE_MYTHING_PROTO, "create", myThingCreate + ); +} +``` + +### 3. Register + +In `modulelist.c`: `#include` the header and call `moduleMyThingInit()` +in `moduleListInit()` (and `Dispose` in `moduleListDispose()`). + +## `moduleBaseFunction` macro + +```c +moduleBaseFunction(myFn) { + // callInfo, args[], argc available + moduleBaseRequireArgs(2); + moduleBaseRequireNumber(0); + moduleBaseRequireString(1); + + float_t x = moduleBaseArgFloat(0); + int32_t n = moduleBaseArgInt(0); + bool_t b = moduleBaseArgBool(0); + float_t opt = moduleBaseOptFloat(2, 0.0f); // optional with default + + // Error propagation: + errorret_t ret = someCall(); + if(errorIsNotOk(ret)) return moduleBaseThrowError(ret); + + return jerry_undefined(); // or jerry_boolean(true) etc. +} +``` + +## Wrapping C values in JS objects + +```c +// Create a JS object wrapping a copy of a C value: +jerry_value_t obj = scriptProtoCreateValue(&MY_PROTO, &myValue); + +// Unwrap back to C pointer: +mything_t *ptr = scriptProtoGetValue(&MY_PROTO, jsObj); +// ptr is NULL if jsObj is not an instance of MY_PROTO. +``` + +## Utility helpers (`modulebase.h`) + +| Helper | Purpose | +|--------|---------| +| `moduleBaseThrow(msg)` | Return a JS TypeError | +| `moduleBaseThrowError(ret)` | Convert `errorret_t` -> JS error | +| `moduleBaseToString(val, buf, len)` | Jerry value -> C string | +| `moduleBaseGetProp(obj, name)` | Get object property by name | +| `moduleBaseWrapPointer(ptr)` | Wrap a raw pointer in a JS object | +| `moduleBaseUnwrapPointer(val)` | Unwrap a raw pointer | +| `moduleBaseSetValue(name, val)` | Set a global JS variable | +| `moduleBaseSetNumber(name, n)` | Set a global JS number | +| `moduleBaseSetInt(name, n)` | Set a global JS integer | +| `moduleBaseDefineMethod(obj, name, fn)` | Add method to any JS object | +| `moduleBaseDefineGlobalMethod(name, fn)` | Add method to global scope | + +## Async JS -- pending promises (`scriptpromisepend.h`) + +When a C module needs to resolve a JS `Promise` from an asynchronous +C event, use `scriptpromisepend_t`. Each module declares a fixed-size +pending slot array; the helpers add/resolve/reject by an opaque key. + +Full API and design notes: `.claude/script-promises.md` + +## Type declarations (`.d.ts`) + +Every module that is accessible from JS **must** have a corresponding +TypeScript declaration file in `types/`. The CLAUDE.md checklist +requires updating these whenever a `.c` module file changes. + +- Add `types//mymod.d.ts` +- Add `/// ` to `types/index.d.ts` diff --git a/.claude/tests.md b/.claude/tests.md new file mode 100644 index 00000000..b8380374 --- /dev/null +++ b/.claude/tests.md @@ -0,0 +1,111 @@ +# Tests and Assertions + +## Test infrastructure + +Tests live in `test/` and mirror the `src/dusk/` directory structure. +Enable with `-DDUSK_BUILD_TESTS=ON`. The test runner is **cmocka**. + +### Entry point + +Every test file includes `dusktest.h`, which pulls in `dusk.h` and +`assert/assert.h`. When `DUSK_TEST_ASSERT` is defined, `assert.h` +includes `cmocka.h` and redirects assertion failures through +`mock_assert()` instead of calling `abort()`. + +### Test function signature + +```c +static void test_something(void **state) { + // ... setup ... + // ... exercise ... + // ... assert ... + assert_int_equal(memoryGetAllocatedCount(), 0); // leak check +} +``` + +### Registering and running tests + +```c +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_errorThrow), + cmocka_unit_test(test_errorOk), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} +``` + +Use `cmocka_unit_test_setup_teardown()` when a test needs per-test +setup or teardown callbacks. + +### Assertion mix + +Tests use **two** sets of assertion macros: + +| Origin | When to use | +|--------|-------------| +| cmocka: `assert_int_equal()`, `assert_non_null()` etc. | Validate results inside test functions | +| Dusk: `assertTrue()`, `assertNotNull()` etc. | Exercise the code under test (these may fire and need catching) | + +To assert that a Dusk assertion fires, use cmocka's mock system: + +```c +expect_assert_failure(assertTrueImpl(__FILE__, __LINE__, false, "msg")); +``` + +### Memory leak discipline + +Every test function must end by asserting: + +```c +assert_int_equal(memoryGetAllocatedCount(), 0); +``` + +This ensures all allocations from the code under test were freed. + +--- + +## Assertion system + +Source: `src/dusk/assert/` + +### Runtime vs test mode + +| Mode | Trigger | Effect on failure | +|------|---------|-------------------| +| Runtime (default) | Release / non-test builds | Logs the message + backtrace, then calls `abort()` | +| Test (`DUSK_TEST_ASSERT`) | `-DDUSK_TEST_ASSERT` build flag | Routes through cmocka `mock_assert()` for controlled catching | +| Faked (`DUSK_ASSERTIONS_FAKED`) | Defined by platform or test | All macros become no-ops (`((void)0)`) | + +### Available macros + +| Macro | Description | +|-------|-------------| +| `assertTrue(x, msg)` | Fails if `x` is false | +| `assertFalse(x, msg)` | Fails if `x` is true | +| `assertNotNull(ptr, msg)` | Fails if `ptr` is NULL | +| `assertNull(ptr, msg)` | Fails if `ptr` is not NULL | +| `assertUnreachable(msg)` | Unconditional failure; marks unreachable code | +| `assertDeprecated(msg)` | Marks a code path as deprecated | +| `assertStringEqual(a, b, msg)` | Fails if strings differ | +| `assertStrLenMax(str, len, msg)` | Fails if `strlen(str) >= len` | +| `assertStrLenMin(str, len, msg)` | Fails if `strlen(str) < len` | +| `assertIsMainThread(msg)` | Fails if called from a non-main thread | +| `assertNotMainThread(msg)` | Fails if called from the main thread | +| `assertStructSize(type, size)` | Compile-time size check via `_Static_assert` | + +### Thread tracking + +`assertInit()` records the main thread ID (pthreads). The main-thread +assertions compare against this stored ID. Call `assertInit()` once at +startup before spawning any threads. + +### Usage guidelines + +- Prefer the specific macro over a bare `assertTrue` for clarity + (e.g. use `assertNotNull` instead of `assertTrue(ptr != NULL, ...)`). +- Use `assertUnreachable` in `default:` cases of exhaustive switches. +- Use `assertStructSize` to guard struct layouts that must match + a known binary format or a platform ABI. +- Do not use asserts for expected error paths -- use `errorThrow` + instead. Asserts are for programmer mistakes, not runtime errors. diff --git a/.claude/threading.md b/.claude/threading.md new file mode 100644 index 00000000..5c783b3c --- /dev/null +++ b/.claude/threading.md @@ -0,0 +1,100 @@ +# Threading System + +Source: `src/dusk/thread/` + +## Platform support + +Threading currently requires **pthreads** (`DUSK_THREAD_PTHREAD`). The +implementation lives in the core thread files and is guarded by that +compile-time flag -- there are no separate per-platform thread +directories. + +Thread-local storage uses the `THREAD_LOCAL` macro, which maps to +`__thread` when pthreads is available. This is used by the error system +to give each thread its own `ERROR_STATE`. + +## Thread lifecycle + +Threads follow a strict state machine: + +``` +STOPPED -> STARTING -> RUNNING -> STOP_REQUESTED -> STOPPED +``` + +- `threadStart()` -- blocking: starts the thread and waits until it + reaches RUNNING. +- `threadStop()` -- blocking: requests stop and waits until STOPPED. +- `threadStartRequest()` -- non-blocking equivalent of `threadStart`. +- `threadStopRequest()` -- non-blocking equivalent of `threadStop`. + +The thread callback polls `threadShouldStop()` to know when to exit. +Never kill a thread forcefully -- always let it stop cooperatively. + +## Thread API + +```c +void threadInit(thread_t *thread, errorret_t (*callback)(thread_t *t)); +// Initialise; callback is the thread entry point. + +errorret_t threadStart(thread_t *thread); +// Start and block until RUNNING. + +errorret_t threadStop(thread_t *thread); +// Request stop, block until STOPPED. + +void threadStartRequest(thread_t *thread); +void threadStopRequest(thread_t *thread); +// Non-blocking variants. + +bool_t threadShouldStop(const thread_t *thread); +// Call from inside the thread callback to know when to exit. +``` + +## Mutex API (`threadmutex.h`) + +Each `threadmutex_t` wraps a pthread mutex and a condition variable. + +```c +void threadMutexInit(threadmutex_t *mutex); +void threadMutexDispose(threadmutex_t *mutex); + +void threadMutexLock(threadmutex_t *mutex); +void threadMutexUnlock(threadmutex_t *mutex); +bool_t threadMutexTryLock(threadmutex_t *mutex); +// Returns true if the lock was acquired; false if already held. + +void threadMutexWaitLock(threadmutex_t *mutex); +// Block until signalled (like pthread_cond_wait). +// Must be called while holding the lock. + +void threadMutexSignal(threadmutex_t *mutex); +// Wake one waiter. +``` + +## Usage example + +```c +static errorret_t workerCallback(thread_t *t) { + while(!threadShouldStop(t)) { + // do work + } + errorOk(); +} + +thread_t worker; +threadInit(&worker, workerCallback); +errorChain(threadStart(&worker)); +// ... later ... +errorChain(threadStop(&worker)); +``` + +## Thread safety rules + +- The error system (`ERROR_STATE`) is thread-local -- each thread has + its own error state. Do not pass `errorret_t` across thread + boundaries without copying the message and lines strings first. +- Asset loading: the background thread calls `loadAsync`; the main + thread calls `loadSync`. Never call GPU or SDL functions from the + loader background thread. +- Use `assertIsMainThread()` / `assertNotMainThread()` to guard + functions that have thread affinity requirements. diff --git a/.claude/time.md b/.claude/time.md new file mode 100644 index 00000000..80184d5f --- /dev/null +++ b/.claude/time.md @@ -0,0 +1,115 @@ +# Time System + +Source: `src/dusk/time/`, platform layers in `src/dusk/time/` + +## Global state + +```c +extern dusktime_t TIME; +``` + +```c +typedef struct { + float_t delta; // Fixed step size in seconds (DUSK_TIME_STEP) + float_t time; // Accumulated game time in seconds + + // Only present when DUSK_TIME_DYNAMIC is defined: + float_t lastNonDynamic; + bool_t dynamicUpdate; // true on sub-step ticks + float_t dynamicDelta; // real elapsed seconds this frame + float_t dynamicTime; // accumulated real time +} dusktime_t; +``` + +## Fixed vs dynamic timestep + +### Fixed timestep (default) + +`DUSK_TIME_STEP` defaults to `16ms / 1000 = 0.016f` seconds (62.5 Hz). +Every call to `timeUpdate()` advances `TIME.time` by exactly +`DUSK_TIME_STEP` and sets `TIME.delta = DUSK_TIME_STEP`. This is the +safe, deterministic mode for physics and game logic. + +### Dynamic timestep (`DUSK_TIME_DYNAMIC`) + +When enabled, `timeUpdate()` calls the platform tick hook to measure +actual elapsed time. It fires a "non-dynamic" step (`dynamicUpdate = +false`, `delta = DUSK_TIME_STEP`) once per fixed interval, and +"dynamic" sub-steps (`dynamicUpdate = true`) in between. Systems that +must run on the fixed interval (physics, networking) skip the dynamic +sub-steps by checking: + +```c +if(TIME.dynamicUpdate) return; +``` + +## Platform hooks + +Each platform provides three macros in its `time/timeplatform.h`: + +| Macro | Purpose | +|-------|---------| +| `timeTickPlatform()` | Sample the hardware timer | +| `timeGetDeltaPlatform()` | Return seconds since last tick | +| `timeGetRealPlatform()` | Return epoch seconds since 1970 | +| `timeGetRealTimeZonePlatform()` | Return local timezone offset (seconds) | + +`timeTickPlatform` and `timeGetDeltaPlatform` are only required when +`DUSK_TIME_DYNAMIC` is defined. + +## Platform implementations + +| Platform | Tick source | Real time source | +|----------|------------|-----------------| +| Linux | `SDL_GetTicks64()` (via SDL2) | `clock_gettime(CLOCK_REALTIME)` | +| Knulli | `SDL_GetTicks64()` (via SDL2) | `clock_gettime(CLOCK_REALTIME)` | +| PSP | `SDL_GetTicks64()` (via SDL2) | `sceRtcGetCurrentTick()` (microseconds) | +| GameCube | none (fixed step only) | `ticks_to_microsecs(__SYS_GetSystemTime())` + 2000->1970 offset | +| Wii | none (fixed step only) | same as GameCube | + +GameCube / Wii note: the hardware timer returns ticks since +2000-01-01, so an offset of 946684800 seconds is added to convert to +UNIX epoch. The timezone offset is always returned as 0.0 on Dolphin +(timezone is not available without network time). + +## Epoch time (`timeepoch.h`) + +```c +typedef struct { + double_t time; // raw UTC seconds since 1970 + double_t timeZone; // timezone offset in seconds + double_t offsetTime; // time + timeZone +} dusktimeepoch_t; + +dusktimeepoch_t timeGetEpoch(void); +// Returns current time in local timezone. +``` + +### Epoch helpers + +```c +int32_t timeEpochGetYear(epoch); +int32_t timeEpochGetMonth(epoch); // 1-12 +int32_t timeEpochGetDayOfMonth(epoch); // 1-31 +int32_t timeEpochGetHours(epoch); // 0-23 +int32_t timeEpochGetMinutes(epoch); // 0-59 +int32_t timeEpochGetSeconds(epoch); // 0-59 +bool_t timeEpochIsLeapYear(year); + +size_t timeEpochFormat( + dusktimeepoch_t epoch, + const char_t *format, // %Y %m %d %H %M %S + char_t *buffer, + size_t bufferSize +); +``` + +## Adding a new platform time implementation + +1. Create `src/dusk/time/time.h/.c`. +2. Implement `timeGetReal()` and + `timeGetRealTimeZone()`. +3. If `DUSK_TIME_DYNAMIC`: also implement `timeTick()` and + `timeGetDelta()`. +4. Create `src/dusk/time/timeplatform.h` with the `#define` + macros pointing to your functions. diff --git a/.claude/util.md b/.claude/util.md new file mode 100644 index 00000000..f632e2a8 --- /dev/null +++ b/.claude/util.md @@ -0,0 +1,191 @@ +# Utility Library + +Source: `src/dusk/util/` + +All C code in the project must use these utilities instead of their +standard library equivalents. Do not use `malloc`, `free`, `strcmp`, +`strcpy`, `memcpy`, `memset`, etc. directly. + +--- + +## Memory (`memory.h`) + +```c +void *memoryAllocate(size_t size); +void *memoryAlign(size_t alignment, size_t size); // aligned alloc +void memoryFree(void *ptr); +void memoryCopy(void *dest, const void *src, size_t size); +void memoryZero(void *dest, size_t size); +errorret_t memoryCompare(const void *a, const void *b, size_t size); + +size_t memoryGetAllocatedCount(void); +// Returns the number of live allocations. Must be 0 at test teardown. + +void memoryTrack(void *ptr); +// Register a pointer that was malloc'd outside the engine (e.g. by a +// third-party library) so it counts toward the allocation tracker. +``` + +`MEMORY_POINTERS_IN_USE` is a file-scope static tracking the live count. +It is incremented by `memoryAllocate` / `memoryTrack` and decremented by +`memoryFree`. Tests assert this is 0 at teardown to catch leaks. + +--- + +## String (`string.h`) + +Use these instead of `` / `` functions: + +```c +void stringCopy(char_t *dest, const char_t *src, size_t destSize); +int stringCompare(const char_t *a, const char_t *b); +bool_t stringEquals(const char_t *a, const char_t *b); +int stringCompareInsensitive(const char_t *a, const char_t *b); +size_t stringLength(const char_t *str); +void stringTrim(char_t *str); +bool_t stringIsWhitespace(char_t c); +bool_t stringStartsWith(const char_t *str, const char_t *prefix); +bool_t stringEndsWith(const char_t *str, const char_t *suffix); +bool_t stringContains(const char_t *haystack, const char_t *needle); +char_t *stringFind(const char_t *haystack, const char_t *needle); +void stringFormat(char_t *dest, size_t destSize, const char_t *fmt, ...); +int32_t stringToInt(const char_t *str); +float_t stringToFloat(const char_t *str); +void stringFromInt(char_t *dest, size_t destSize, int32_t value); +void stringFromFloat(char_t *dest, size_t destSize, float_t value); +``` + +`destSize` in `stringCopy` / `stringFormat` is the buffer capacity +**excluding** the null terminator. + +--- + +## Math (`math.h`) + +```c +#define MATH_PI M_PI + +#define mathMax(a, b) +#define mathMin(a, b) +#define mathClamp(x, lower, upper) +#define mathAbs(amt) + +uint32_t mathNextPowTwo(uint32_t value); +float_t mathModFloat(float_t x, float_t y); // always non-negative +float_t mathLerp(float_t a, float_t b, float_t t); +// plus additional trig / remap helpers +``` + +The project uses **cglm** for vector and matrix math (`vec3`, `mat4`, +`glm_vec3_*`, `glm_mat4_*`, etc.). `math.h` provides scalar helpers +that complement cglm. + +--- + +## Endian (`endian.h`) + +GameCube and Wii are big-endian. Any binary data format (asset files, +network packets) must use the endian utilities when reading multi-byte +values. + +```c +bool_t isHostLittleEndian(void); +uint16_t endianLittleToHost16(uint16_t value); +uint32_t endianLittleToHost32(uint32_t value); +uint64_t endianLittleToHost64(uint64_t value); +float_t endianLittleToHostFloat(float_t value); +``` + +If neither `DUSK_PLATFORM_ENDIAN_LITTLE` nor `DUSK_PLATFORM_ENDIAN_BIG` +is defined, the implementation falls back to a runtime check +(`ENDIAN_MAGIC` probe). Prefer setting the compile-time macro for new +platform targets. + +--- + +## Reference counting (`ref.h`) + +`ref_t` is a generic reference-counted handle with optional lock / +unlock / all-unlocked callbacks. + +```c +void refInit( + ref_t *ref, + void *data, + refcallback_t onLock, + refcallback_t onUnlock, + refcallback_t onAllUnlocked // called when count -> 0; do cleanup here +); + +void refLock(ref_t *ref); // increment count +bool_t refUnlock(ref_t *ref); // decrement; returns true if count == 0 +``` + +The asset entry system uses `ref_t` internally to track how many +subsystems have locked a loaded asset. + +--- + +## Array (`array.h`) + +```c +void arrayReverse(void *array, size_t count, size_t elementSize); +``` + +Generic in-place reverse using the element stride. + +--- + +## Sort (`sort.h`) + +Use these instead of `qsort` for portability across all platforms. + +```c +typedef int_t (*sortcompare_t)(const void *, const void *); + +void sortBubble( + void *array, + const size_t count, + const size_t size, + const sortcompare_t compare +); + +void sortQuick( + void *array, + const size_t count, + const size_t size, + const sortcompare_t compare +); + +#define sort sortQuick // preferred; use this in new code +``` + +Typed convenience helpers for `uint8_t` arrays: + +```c +int sortArrayU8Compare(const void *a, const void *b); +void sortArrayU8(uint8_t *array, const size_t count); +``` + +--- + +## Crypt (`crypt.h`) + +CRC32 checksum for save file integrity. Not cryptographically secure -- +do not use for security purposes. + +```c +// One-shot checksum: +uint32_t cryptCRC32(const void *data, const size_t size); + +// Streaming (incremental) CRC32: +uint32_t cryptCRC32Begin(void); +void cryptCRC32Update( + uint32_t *crc, const void *data, const size_t size +); +uint32_t cryptCRC32End(const uint32_t crc); +``` + +The streaming API allows computing a checksum across multiple buffers +or while interleaving other reads -- the save system uses this to +verify the whole save slot in a single pass. diff --git a/CLAUDE.md b/CLAUDE.md index ed7bea5e..57bb5f6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,73 @@ -# Dusk — Claude Code rules +# Dusk -- Claude Code rules + +## About Dusk + +Dusk is a pure C game and game engine. There is no C++ anywhere in the +codebase. The engine is built around a data-oriented Entity Component +System (ECS) and is designed for heavy optimization across a wide range +of hardware targets, including platforms with very limited RAM and CPU. + +**Current and planned platforms:** Linux, Windows, macOS, Sony PSP, +PlayStation Vita, Nintendo GameCube, Nintendo Wii. Additional platforms +will be added over time. GameCube and Wii are collectively referred to +as the **Dolphin** targets throughout the codebase and docs. + +**Build system:** CMake exclusively. Every source subdirectory owns its +own `CMakeLists.txt`. + +**Architecture:** Entity Component System (ECS) as the primary pattern. +All game objects are entities; behaviour and state are attached via +components. No inheritance hierarchies -- favour composition. + +**Optimization:** Performance is a first-class constraint, not an +afterthought. The engine must run well on hardware as constrained as +the GameCube (16 MB main RAM, 485 MHz PowerPC) and PSP (32 MB RAM, +333 MHz MIPS). Every design and implementation decision should consider +the most constrained target. + +**Coding style:** All C code must strictly follow the project style +rules documented in the [Coding style](#coding-style) section below. +Deviations are not acceptable. + +### Further reading + +Detailed documentation on specific topics lives in `.claude/`: + +| Topic | File | +|-------|------| +| Platform overview, capability macros, quick comparison | `.claude/platforms.md` | +| Platform -- Linux and Knulli | `.claude/platform-linux.md` | +| Platform -- Sony PSP | `.claude/platform-psp.md` | +| Platform -- PlayStation Vita | `.claude/platform-vita.md` | +| Platform -- GameCube and Wii (Dolphin) | `.claude/platform-dolphin.md` | +| Platform -- Windows (planned) | `.claude/platform-windows.md` | +| Platform -- macOS (planned) | `.claude/platform-macos.md` | +| ECS architecture and conventions | `.claude/ecs.md` | +| CMake build system and toolchain setup | `.claude/build.md` | +| Optimization guidelines and platform budgets | `.claude/optimization.md` | +| Test infrastructure and assertion macros | `.claude/tests.md` | +| Error handling system (`errorret_t`, macros) | `.claude/errors.md` | +| Asset system (loading, caching, loaders) | `.claude/assets.md` | +| Threading (`thread_t`, mutex, thread-local) | `.claude/threading.md` | +| Input system (actions, buttons, platforms) | `.claude/input.md` | +| Physics simulation and collision shapes | `.claude/physics.md` | +| Event system (pub/sub) | `.claude/events.md` | +| Locale / localisation system | `.claude/locale.md` | +| Time system (fixed/dynamic, epoch) | `.claude/time.md` | +| Network system (per-platform status) | `.claude/network.md` | +| Utility library (memory, string, math, endian, ref) | `.claude/util.md` | +| Script system (JerryScript, module proto API) | `.claude/script.md` | +| Script async promises (`scriptpromisepend_t`) | `.claude/script-promises.md` | +| Engine main loop, system platform API, log | `.claude/engine.md` | +| Save system (multi-slot, platform storage) | `.claude/save.md` | +| Animation (keyframes, easing functions) | `.claude/animation.md` | +| Display (index) | `.claude/display.md` | +| Display -- screen, framebuffer, size modes | `.claude/display-core.md` | +| Display -- texture, tileset, font | `.claude/display-texture.md` | +| Display -- shader, material, display state | `.claude/display-shader.md` | +| Scene system (lifecycle, render pipeline, JS hooks) | `.claude/scene.md` | + +--- ## File headers Every C, H, and JS file starts with: @@ -101,6 +170,10 @@ assertIsMainThread("msg"); --- ## Build system + +See `.claude/build.md` for extended CMake conventions, platform +toolchain setup, and adding platform-conditional sources. + Each subdirectory has its own `CMakeLists.txt` that adds sources with: ```cmake @@ -116,6 +189,10 @@ Never add source files to the root `CMakeLists.txt` directly. ## Platform support +See `.claude/platforms.md` for the full platform table (including +planned Windows / macOS targets), capability macros, toolchain setup, +and endianness notes. + ### Targets Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform: @@ -175,19 +252,11 @@ simply left undefined — the core guards calls with `#ifdef`. --- -## Adding a new asset loader type -1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in - `src/dusk/asset/loader/assetloader.h`. -2. Add fields to the input/loading/output unions in `assetloader.h`. -3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and - `assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory. -4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in - `src/dusk/asset/loader/assetloader.c`. -5. If user-facing, create a JS module (see below) and a `.d.ts` file. - ---- - ## Adding a new entity component + +See `.claude/ecs.md` for ECS design rules, component categories, and +entity lifecycle details. + 1. Create `src/dusk/entity/component//entityMyComp.h/.c` with struct `entityMyComp_t`, `entityMyCompInit()`, and optionally `entityMyCompDispose()`. @@ -201,6 +270,18 @@ simply left undefined — the core guards calls with `#ifdef`. --- +## Adding a new asset loader type +1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in + `src/dusk/asset/loader/assetloader.h`. +2. Add fields to the input/loading/output unions in `assetloader.h`. +3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and + `assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory. +4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in + `src/dusk/asset/loader/assetloader.c`. +5. If user-facing, create a JS module (see below) and a `.d.ts` file. + +--- + ## Adding a new script (JS) module 1. Create `src/dusk/script/module//moduleMyMod.h/.c`. - Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.