88 Commits

Author SHA1 Message Date
YourWishes 43d0593872 Final docs 2026-06-16 13:07:21 -05:00
YourWishes c0a2ae234f Add claude docs 2026-06-16 12:29:36 -05:00
YourWishes ed5c60ac30 Add claude docs 2026-06-16 10:15:59 -05:00
YourWishes 8131bcd4d4 Build on github tag 2026-06-09 15:43:31 -05:00
YourWishes 4ba11e3363 Test
Build Dusk / run-tests (push) Failing after 15m50s
2026-06-08 18:01:19 -05:00
YourWishes acf2be3f66 Try set
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 17:04:07 -05:00
YourWishes f5df0195e2 Docker compose
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 17:02:58 -05:00
YourWishes d26995b48d pwd
Build Dusk / run-tests (push) Failing after 6s
2026-06-08 16:59:39 -05:00
YourWishes 617f8120ae Docker exec
Build Dusk / run-tests (push) Failing after 14s
2026-06-08 16:48:17 -05:00
YourWishes 06c517c9aa chmod
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 16:47:45 -05:00
YourWishes 593ed6408c test2
Build Dusk / run-tests (push) Successful in 7s
2026-06-08 16:47:03 -05:00
YourWishes fb7d3ed122 LS stuff
Build Dusk / run-tests (push) Successful in 7s
2026-06-08 16:45:59 -05:00
YourWishes 19b88ec858 Test linux?
Build Dusk / run-tests (push) Failing after 6s
2026-06-08 16:08:24 -05:00
YourWishes 160e65be7f Run on ubuntu-latest
Build Dusk / run-tests (push) Failing after 16s
2026-06-08 15:34:31 -05:00
YourWishes 079b0d2cf6 Update test script back to what it was
Build Dusk / run-tests (push) Has been cancelled
2026-06-08 15:18:40 -05:00
YourWishes 78f1310f41 Update test script
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 15:14:53 -05:00
YourWishes eb1974c113 Update to docker-host
Build Dusk / run-tests (push) Failing after 6m15s
2026-06-08 14:55:31 -05:00
YourWishes 551409a023 Force run again
Build Dusk / run-tests (push) Failing after 9m15s
2026-06-08 13:58:11 -05:00
YourWishes a11e14daac Try without setup docker
Build Dusk / run-tests (push) Failing after 8m28s
2026-06-08 13:15:15 -05:00
YourWishes 7441e15e76 Run on ubuntu-latest
Build Dusk / run-tests (push) Failing after 3m15s
2026-06-08 13:09:53 -05:00
YourWishes 46506228a6 One thing at a time
Build Dusk / run-tests (push) Has been cancelled
2026-06-08 12:46:38 -05:00
YourWishes 17c49c74cf Try tests updated
Build Dusk / build-linux (push) Failing after 29s
Build Dusk / build-psp (push) Failing after 1m13s
Build Dusk / run-tests (push) Has been cancelled
Build Dusk / build-gamecube (push) Has been cancelled
Build Dusk / build-gamecube-iso (push) Has been cancelled
Build Dusk / build-knulli (push) Has been cancelled
Build Dusk / build-wii (push) Has been cancelled
Build Dusk / build-wii-iso (push) Has been cancelled
2026-06-08 12:44:22 -05:00
YourWishes 3f8024d4db Workflows
Build Dusk / run-tests (push) Failing after 58s
Build Dusk / build-linux (push) Failing after 1m11s
Build Dusk / build-psp (push) Failing after 46s
Build Dusk / build-knulli (push) Failing after 53s
Build Dusk / build-gamecube (push) Failing after 47s
Build Dusk / build-gamecube-iso (push) Failing after 49s
Build Dusk / build-wii (push) Failing after 49s
Build Dusk / build-wii-iso (push) Failing after 1m14s
2026-06-08 12:25:48 -05:00
YourWishes 8675e44d28 GitTea actions 2026-06-08 12:24:09 -05:00
YourWishes 1301d9a718 idk why I btoher with github actions 2026-06-08 12:23:18 -05:00
YourWishes da3db50ca8 Want to test this in PSP 2026-06-08 11:32:59 -05:00
YourWishes 2ca6780305 Just trying to fix things now 2026-06-08 09:39:09 -05:00
YourWishes be68fe5a35 Emdashless 2026-06-07 21:27:59 -05:00
YourWishes dc41c0e302 Cleanup 2026-06-07 21:16:46 -05:00
YourWishes 51388c90d5 we ball I guess 2026-06-07 19:51:54 -05:00
YourWishes f8c9d33df2 Fix some script bugs 2026-06-07 18:47:34 -05:00
YourWishes ed0420fdce Cleanup of script modules. 2026-06-07 12:59:17 -05:00
YourWishes 47a6f396fa Fix typedefs 2026-06-06 19:06:52 -05:00
YourWishes 9edb2aa0c1 Example scene working 2026-06-06 18:46:08 -05:00
YourWishes 003b647d83 Fix tests 2026-06-06 17:36:13 -05:00
YourWishes 2849ff8844 Fix linux? 2026-06-06 17:14:42 -05:00
YourWishes 9f3089742a Fixed ISO builds. 2026-06-06 17:07:30 -05:00
YourWishes b286a9bbcd Fixed wii build 2026-06-06 16:53:06 -05:00
YourWishes 6204e745ba Fixed gamecube building. 2026-06-06 16:47:07 -05:00
YourWishes bbe0e48d23 Builds again? 2026-06-06 16:42:12 -05:00
YourWishes 79054080c0 Cleanup a bit. 2026-06-06 16:39:27 -05:00
YourWishes 81024c4c09 Require async 2026-06-06 10:55:10 -05:00
YourWishes 9068d96130 Add timeouts 2026-06-06 10:38:10 -05:00
YourWishes 6f47543720 Update jerryscript each frame. 2026-06-06 10:30:22 -05:00
YourWishes 5a08384ae1 start async 2026-06-05 19:42:24 -05:00
YourWishes 45d8fda0e4 require() working as I like 2026-06-05 18:49:10 -05:00
YourWishes a9e664492f First round of asset refactoring 2026-06-05 13:18:08 -05:00
YourWishes 3c8b6cb2cc Scene script code 2026-06-04 13:36:35 -05:00
YourWishes 2b3abbe13b ABout to try scene and script merger 2026-06-02 16:46:39 -05:00
YourWishes 241a52b94a nuke unused overworld code 2026-06-02 13:23:11 -05:00
YourWishes 82c300b077 Add some script modules 2026-06-02 12:55:32 -05:00
YourWishes 0f8b629e20 Add logging to Wii 2026-06-02 11:01:54 -05:00
YourWishes 36f6ac65f2 Builds and works on Gamecube 2026-06-02 09:53:56 -05:00
YourWishes a25871a849 Test sprite from script 2026-06-02 09:32:07 -05:00
YourWishes 57766a9104 Merge branch 'main' into scriptentity 2026-06-02 07:36:44 -05:00
YourWishes 3770ae1645 Fix tests? 2026-06-02 07:35:28 -05:00
YourWishes d73edb403f Example Camera 2026-06-01 23:04:55 -05:00
YourWishes b14196ff0d Basic entity script 2026-06-01 22:56:37 -05:00
YourWishes 88903fee94 No need for asset batching on text.c 2026-06-01 22:36:02 -05:00
YourWishes 1e8311fc04 Add asset batch 2026-06-01 22:34:44 -05:00
YourWishes 2b78370cb8 Add asset reaping 2026-06-01 22:20:57 -05:00
YourWishes 8f78bba9e9 Restoring JerryScript a bit cleaner 2026-06-01 21:52:36 -05:00
YourWishes 41a4be678e Added a tiny sleep on assets to stop pegging the CPU 2026-06-01 15:48:10 -05:00
YourWishes 8b2b4b7c3d Fixed JSON loader, added some tests 2026-06-01 15:31:22 -05:00
YourWishes 1f3a29f89d Asyncify other loaders 2026-06-01 15:10:58 -05:00
YourWishes c4c93097cd Add async texture loading 2026-06-01 14:53:18 -05:00
YourWishes eedb7769e6 Add some extra tests 2026-06-01 13:48:29 -05:00
YourWishes 98db62a4bc Add some more tests, prepping for asset testing 2026-06-01 13:37:14 -05:00
YourWishes df48c8e500 Consistency and fixing thread unit tests 2026-06-01 11:33:27 -05:00
YourWishes db9cc0f4c6 Add thread tests 2026-06-01 10:59:56 -05:00
YourWishes a79ee429b4 Prepping for async 2026-06-01 10:57:40 -05:00
YourWishes 6acfca6d48 Consistent 2026-05-30 20:30:13 -05:00
YourWishes 1cd6f4cb72 First refactor of new asset system 2026-05-30 08:21:58 -05:00
YourWishes 3271e8c7d6 FInished porting last asset loader types 2026-05-30 07:59:06 -05:00
YourWishes 0bcde064af Asset refactor, phase one. 2026-05-29 14:27:40 -05:00
YourWishes 957980b3c5 Updating event handler 2026-05-28 14:22:13 -05:00
YourWishes 03eb328d81 Allow dynamic trace on any platform that can support it. 2026-05-28 11:21:36 -05:00
YourWishes e1716a741f Trigger test 2026-05-26 22:18:41 -05:00
YourWishes e24707c847 Scene loading example 2026-05-26 21:42:37 -05:00
YourWishes 7c4b8c307f Fix flocking bug 2026-05-26 20:24:34 -05:00
YourWishes 109318aeaf Remove useless void checks 2026-05-26 19:24:17 -05:00
YourWishes 1f2657cea0 Spritebatch cleanup 2026-05-26 19:07:07 -05:00
YourWishes 382c435bac Entity refactoring 2026-05-22 23:01:45 -05:00
YourWishes 130fe4ca5d Delete JS assets 2026-05-22 00:00:23 -05:00
YourWishes 31ba3fe127 add build to corner of screen 2026-05-21 23:59:26 -05:00
YourWishes f68b31158f Asset refactor 2026-05-21 23:42:56 -05:00
YourWishes 653ca9a72d PSP rendering fix 2026-05-21 22:07:56 -05:00
YourWishes ba7857f4df Fix rendering 2026-05-21 18:24:18 -05:00
398 changed files with 22515 additions and 2461 deletions
+88
View File
@@ -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.
+125
View File
@@ -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.
+116
View File
@@ -0,0 +1,116 @@
# 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>/ 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()
```
## Embedding JS files (`dusk_embed_js`)
Source: `cmake/modules/duskjs2c.cmake`
The `dusk_embed_js()` CMake function embeds a `.js` source file as a
C string constant in a generated header. It is used to ship script
module code alongside the engine binary without a separate file load.
```cmake
dusk_embed_js(
TARGET ${DUSK_LIBRARY_TARGET_NAME}
JS_FILE path/to/mymodule.js
# NAME is optional; defaults to uppercase stem + "_JS"
# e.g. "mymodule.js" -> "MYMODULE_JS"
NAME MY_CUSTOM_NAME
)
```
The generated header is placed in
`${DUSK_GENERATED_HEADERS_DIR}/<stem>_js.h` and defines:
```c
static const char MY_CUSTOM_NAME[] = "... js source ...";
static const size_t MY_CUSTOM_NAME_SIZE = sizeof(MY_CUSTOM_NAME) - 1;
```
Under the hood it calls `python -m tools.js2c` from the repo root.
The header is generated at build time; include it in the `.c` file
that registers the JS module, then pass `NAME` and `NAME_SIZE` to
`jerry_eval()` (or the equivalent module load helper).
## 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)`
+92
View File
@@ -0,0 +1,92 @@
# Console
Source: `src/dusk/console/`
---
## Overview
The console is a lightweight in-engine debug overlay. It maintains a
fixed-size ring buffer of text lines and can render them to the screen
as an overlay. On Linux (where `DUSK_CONSOLE_POSIX` is defined), it
also reads commands from stdin on a background thread, allowing
interactive input during development without pausing the game loop.
---
## Limits
| Constant | Value |
|----------|-------|
| `CONSOLE_LINE_MAX` | 512 chars per line |
| `CONSOLE_HISTORY_MAX` | 16 lines in the ring buffer |
| `CONSOLE_EXEC_BUFFER_MAX` | 32 pending execution slots |
---
## Global state
```c
typedef struct {
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; // ring buffer
bool_t visible;
#ifdef DUSK_CONSOLE_POSIX
threadmutex_t printMutex; // guards ring buffer on POSIX targets
#endif
} console_t;
extern console_t CONSOLE;
```
---
## API
```c
void consoleInit(void);
// printf-style print into the ring buffer.
// Thread-safe on POSIX (uses printMutex).
void consolePrint(const char_t *message, ...);
// Process any queued script input lines. Must be called from the
// main thread once per frame.
void consoleUpdate(void);
// Draw the ring buffer as an overlay in UI space.
errorret_t consoleDraw(void);
void consoleDispose(void);
```
---
## POSIX stdin mode (`DUSK_CONSOLE_POSIX`)
On Linux only (`DUSK_CONSOLE_POSIX`), the console launches a background
thread that polls stdin using `poll()` at 75 ms intervals
(`CONSOLE_POSIX_POLL_RATE`). Lines typed at the terminal are queued
and dispatched on the main thread by `consoleUpdate()`.
This allows typing commands or JS expressions into the running game
without blocking the render loop. The ring buffer is protected by
`CONSOLE.printMutex` so `consolePrint` is safe to call from either
thread.
POSIX mode is not available on PSP, Vita, or Dolphin targets.
---
## Notes
- `CONSOLE.visible` controls whether `consoleDraw` renders anything.
Toggle it from a debug keybind or always set it to `true` in
development builds.
- `consolePrint` wraps lines at `CONSOLE_LINE_MAX` -- long messages are
truncated. Use multiple calls for long output.
- The console is distinct from the error system (`errorThrow` /
`errorPrint`). Use `consolePrint` for diagnostic output; use
`errorThrow` for recoverable failures.
- Log calls (`logDebug`, `logError`) go to the platform's debug output
(stdout/stderr), not to the console ring buffer.
+88
View File
@@ -0,0 +1,88 @@
# Display -- Color
Source: `build/generated/display/color.h` (generated at build time)
---
## Overview
`color.h` is a generated header. It is not hand-edited -- the source
lives in the CMake generator that produces it from platform configuration.
Include it via `"display/color.h"`.
---
## Types
```c
typedef float_t colorchannelf_t; // float channel [0.0, 1.0]
typedef uint8_t colorchannel8_t; // 8-bit channel [0, 255]
typedef struct { colorchannelf_t r, g, b; } color3f_t;
typedef struct { colorchannelf_t r, g, b, a; } color4f_t;
typedef struct { colorchannel8_t r, g, b; } color3b_t;
typedef struct { colorchannel8_t r, g, b, a; } color4b_t;
typedef color4b_t color_t; // default: RGBA uint8
```
`color_t` is always `color4b_t` -- four `uint8_t` channels.
Most engine APIs (text rendering, UI, mesh vertices) take `color_t`.
---
## Constructors
```c
color3f(r, g, b) // float RGB
color4f(r, g, b, a) // float RGBA
color3b(r, g, b) // uint8 RGB
color4b(r, g, b, a) // uint8 RGBA
color(r, g, b, a) // alias for color4b
colorHex(0xRRGGBBAA) // unpack 32-bit hex into color4b
```
---
## Predefined constants
Each named colour comes in four variants: `_4B` (uint8 RGBA, default),
`_3B` (uint8 RGB), `_4F` (float RGBA), `_3F` (float RGB). The bare
name (e.g. `COLOR_BLACK`) always resolves to the `_4B` variant.
| Constant | RGBA (uint8) |
|----------|-------------|
| `COLOR_BLACK` | 0, 0, 0, 255 |
| `COLOR_WHITE` | 255, 255, 255, 255 |
| `COLOR_RED` | 255, 0, 0, 255 |
| `COLOR_GREEN` | 0, 255, 0, 255 |
| `COLOR_BLUE` | 0, 0, 255, 255 |
| `COLOR_YELLOW` | 255, 255, 0, 255 |
| `COLOR_CYAN` | 0, 255, 255, 255 |
| `COLOR_MAGENTA` | 255, 0, 255, 255 |
| `COLOR_ORANGE` | 255, 165, 0, 255 |
| `COLOR_PURPLE` | 127, 0, 127, 255 |
| `COLOR_GRAY` | 127, 127, 127, 255 |
| `COLOR_LIGHT_GRAY` | 191, 191, 191, 255 |
| `COLOR_DARK_GRAY` | 63, 63, 63, 255 |
| `COLOR_BROWN` | 153, 102, 51, 255 |
| `COLOR_PINK` | 255, 191, 204, 255 |
| `COLOR_LIME` | 191, 255, 0, 255 |
| `COLOR_NAVY` | 0, 0, 127, 255 |
| `COLOR_TEAL` | 0, 127, 127, 255 |
| `COLOR_CORNFLOWER_BLUE` | 99, 147, 237, 255 |
| `COLOR_TRANSPARENT` | 0, 0, 0, 0 |
| `COLOR_TRANSPARENT_WHITE` | 255, 255, 255, 0 |
| `COLOR_TRANSPARENT_BLACK` | 0, 0, 0, 0 |
---
## Notes
- `color_t` uses premultiplied-friendly `uint8_t` channels for
compatibility with both OpenGL texture uploads and GX on Dolphin.
- For shader uniforms that expect float colours, convert manually:
`(float_t)col.r / 255.0f` etc.
- The `COLOR_SCRIPT` macro is a C string containing the JS Color class
static methods. It is concatenated into the embedded JS runtime
during module init (see `.claude/script.md`).
+81
View File
@@ -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.
+240
View File
@@ -0,0 +1,240 @@
# Display -- Mesh
Source: `src/dusk/display/mesh/`
See also: `.claude/display-spritebatch.md`, `.claude/display-shader.md`
---
## Overview
The mesh system wraps platform-specific GPU geometry buffers behind a
common `mesh_t` type. Geometry is described as an array of
`meshvertex_t` values and a primitive type. The platform layer
(`meshplatform.h`) provides the concrete buffer and draw implementation
(VBO on OpenGL, display list or immediate-mode on Dolphin GX).
---
## Vertex format (`meshvertex.h`)
```c
typedef struct {
#if MESH_ENABLE_COLOR
color_t color; // optional per-vertex colour (disabled by default)
#endif
float_t uv[2]; // texture coordinates (U, V)
float_t pos[3]; // position (X, Y, Z)
} meshvertex_t;
```
`MESH_ENABLE_COLOR` is a compile-time flag (default 0). Enable it with
`-DMESH_ENABLE_COLOR=1` at CMake configure time if per-vertex colouring
is needed; be aware this changes the struct size and breaks binary
compatibility with pre-built mesh data.
---
## Core API (`mesh.h`)
```c
// Platform alias -- do not use meshplatform_t directly.
typedef meshplatform_t mesh_t;
typedef meshprimitivetypeplatform_t meshprimitivetype_t;
// Upload vertices to the GPU. Must be called from the main thread.
errorret_t meshInit(
mesh_t *mesh,
const meshprimitivetype_t primitiveType,
const int32_t vertexCount,
const meshvertex_t *vertices
);
// Flush a range of updated vertices to the GPU (modern targets only).
// vertexCount == -1 flushes all vertices.
errorret_t meshFlush(
mesh_t *mesh,
const int32_t vertexOffset,
const int32_t vertexCount
);
// Draw the mesh. vertexCount == -1 draws all vertices.
errorret_t meshDraw(
const mesh_t *mesh,
const int32_t vertexOffset,
const int32_t vertexCount
);
// Compute the axis-aligned bounding box.
void meshGetBounds(const mesh_t *mesh, vec3 outMin, vec3 outMax);
int32_t meshGetVertexCount(const mesh_t *mesh);
errorret_t meshDispose(mesh_t *mesh);
```
On constrained targets (GameCube/Wii) `meshFlush` is a no-op -- the
hardware reads vertices from main memory directly. Always call it on
desktop/mobile targets after modifying vertex data.
---
## Primitive generators
Each generator provides:
- A `*Buffer(vertices, ...)` function that writes into a caller-supplied
`meshvertex_t` array (no allocation).
- A global pre-built `*_MESH_SIMPLE` singleton + `*_MESH_SIMPLE_VERTICES`
array initialised at engine startup (for common one-off uses).
All generators use CCW winding and `MESH_PRIMITIVE_TYPE_TRIANGLES`.
### Quad (`quad.h`)
```c
#define QUAD_VERTEX_COUNT 6 // two triangles
// 2D quad in XY plane:
void quadBuffer(
meshvertex_t *vertices,
const float_t minX, const float_t minY,
const float_t maxX, const float_t maxY,
const float_t u0, const float_t v0,
const float_t u1, const float_t v1
);
// 3D quad using full vec3 min/max:
void quadBuffer3D(
meshvertex_t *vertices,
const vec3 min, const vec3 max,
const vec2 uvMin, const vec2 uvMax
);
extern mesh_t QUAD_MESH_SIMPLE;
```
The SpriteBatch is built on `quadBuffer3D` internally.
### Cube (`cube.h`)
```c
#define CUBE_VERTEX_COUNT 36 // 6 faces x 6 vertices
// Axis-aligned box from min to max:
void cubeBuffer(
meshvertex_t *vertices,
const vec3 min, const vec3 max
);
extern mesh_t CUBE_MESH_SIMPLE; // unit cube (0,0,0) to (1,1,1)
```
### Plane (`plane.h`)
```c
#define PLANE_VERTEX_COUNT 6
typedef enum {
PLANE_AXIS_XY, // flat in XY, normal along +Z (billboard / wall face)
PLANE_AXIS_XZ, // flat in XZ, normal along +Y (ground / floor)
PLANE_AXIS_YZ, // flat in YZ, normal along +X (side wall)
} planeaxis_t;
void planeBuffer(
meshvertex_t *vertices,
const planeaxis_t axis,
const vec3 min, const vec3 max,
const vec2 uvMin, const vec2 uvMax
);
extern mesh_t PLANE_MESH_SIMPLE; // unit XZ plane (0,0,0) to (1,0,1)
```
### Sphere (`sphere.h`)
```c
#define SPHERE_STACKS 8
#define SPHERE_SECTORS 16
#define SPHERE_VERTEX_COUNT (SPHERE_STACKS * SPHERE_SECTORS * 6)
void sphereBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const int32_t stacks,
const int32_t sectors
);
extern mesh_t SPHERE_MESH_SIMPLE; // unit sphere centered at (0,0,0), r=0.5
```
### Capsule (`capsule.h`)
```c
#define CAPSULE_CAP_RINGS 4
#define CAPSULE_SECTORS 16
// Total vertex count = (2 * capRings + 1) * sectors * 6
void capsuleBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const float_t halfHeight, // half-height of the cylindrical section only
const int32_t capRings,
const int32_t sectors
);
extern mesh_t CAPSULE_MESH_SIMPLE; // r=0.5, halfHeight=0.5 (total h=2.0)
```
The long axis is always Y. This mirrors the physics capsule body (see
`.claude/physics.md`).
### Triangular prism (`triprism.h`)
```c
#define TRIPRISM_VERTEX_COUNT 24
// Cross-section triangle defined by three 2D points in XY;
// extruded along Z from minZ to maxZ.
void triPrismBuffer(
meshvertex_t *vertices,
const float_t x0, const float_t y0,
const float_t x1, const float_t y1,
const float_t x2, const float_t y2,
const float_t minZ, const float_t maxZ
);
extern mesh_t TRIPRISM_MESH_SIMPLE;
// Unit prism: triangle (0,0),(1,0),(0.5,1) extruded z=0 to z=1.
```
---
## Custom dynamic mesh
If you need to update geometry each frame (e.g. a procedural mesh):
```c
static meshvertex_t myVerts[MY_VERT_COUNT];
static mesh_t myMesh;
// On init:
// Fill myVerts, then:
errorChain(meshInit(&myMesh, MESH_PRIMITIVE_TYPE_TRIANGLES,
MY_VERT_COUNT, myVerts));
// Each frame (after modifying myVerts):
errorChain(meshFlush(&myMesh, 0, -1));
errorChain(meshDraw(&myMesh, 0, -1));
```
---
## Notes
- `meshInit` must be called on the **main thread** (GPU upload).
- `meshFlush` is required on OpenGL targets when vertices change
after init. It is a no-op on Dolphin.
- All `_MESH_SIMPLE` globals are initialised during engine startup --
do not call `meshInit` on them manually.
+86
View File
@@ -0,0 +1,86 @@
# 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` fields:
```c
typedef struct {
color_t color; // tint colour (multiplied with the texture sample)
texture_t *texture; // NULL uses TEXTURE_WHITE (solid colour draw)
} shaderunlitmaterial_t;
```
The shader exposes uniforms `u_Proj`, `u_View`, `u_Model` (mat4),
`u_Texture` (sampler), and `u_Color` (vec4). They are uploaded via
`shaderUnlitSetMaterial(shader, material)`.
A global singleton `SHADER_UNLIT` is the live shader object;
`SHADER_UNLIT_DEFINITION` is its platform definition descriptor.
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`.
+133
View File
@@ -0,0 +1,133 @@
# Display -- SpriteBatch
Source: `src/dusk/display/spritebatch/`
See also: `.claude/display-mesh.md`, `.claude/display-texture.md`
---
## Overview
The SpriteBatch is the primary 2D rendering primitive. It accumulates
axis-aligned quads (sprites) into a shared vertex buffer and draws them
in batches. All 2D rendering in the engine -- UI frames, text, tilemaps,
HUD -- goes through the global `SPRITEBATCH`.
The batch flushes automatically when the per-flush limit is reached, or
explicitly via `spriteBatchFlush()`.
---
## Limits
| Constant | Value | Meaning |
|----------|-------|---------|
| `SPRITEBATCH_SPRITES_MAX` | 512 | Total sprites in the vertex buffer |
| `SPRITEBATCH_FLUSH_COUNT` | 16 | Number of auto-flush segments |
| `SPRITEBATCH_SPRITES_MAX_PER_FLUSH` | 32 | Sprites per auto-flush segment |
| `SPRITEBATCH_VERTEX_COUNT` | 3072 | Total vertices (512 * QUAD_VERTEX_COUNT) |
---
## Sprite structure
```c
typedef struct {
vec3 min; // minimum XYZ corner of the quad in world/screen space
vec3 max; // maximum XYZ corner of the quad in world/screen space
vec2 uvMin; // minimum UV (top-left in [0,1] texture space)
vec2 uvMax; // maximum UV (bottom-right in [0,1] texture space)
} spritebatchsprite_t;
```
Z in `min` and `max` controls draw depth (further from camera = higher Z
in a typical orthographic setup). For flat 2D, set `min.z = max.z = 0`.
---
## SpriteBatch struct
```c
typedef struct {
mesh_t mesh;
int32_t spriteCount;
int32_t spriteFlush;
shader_t *shader;
shadermaterial_t material;
} spritebatch_t;
extern spritebatch_t SPRITEBATCH;
extern meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT];
```
`SPRITEBATCH_VERTICES` is a separate global (not embedded in the struct)
for platform alignment requirements.
---
## API
```c
errorret_t spriteBatchInit();
errorret_t spriteBatchDispose();
// Clear the buffer and reset state. Call before starting a new batch.
void spriteBatchClear();
// Append sprites to the buffer. Flushes automatically when the per-flush
// segment fills. shader + material are used on the next flush.
errorret_t spriteBatchBuffer(
const spritebatchsprite_t *sprites,
const uint32_t count,
shader_t *shader,
const shadermaterial_t material
);
// Upload and draw all buffered sprites. Binds shader and applies
// material if set. No-op if the buffer is empty.
errorret_t spriteBatchFlush();
```
---
## Typical usage
```c
// Beginning of a 2D render pass:
spriteBatchClear();
// Build sprites (e.g. via tilesetTileGetUV, then fill spritebatchsprite_t):
spritebatchsprite_t s;
glm_vec3_copy((vec3){ x, y, 0 }, s.min);
glm_vec3_copy((vec3){ x + w, y + h, 0 }, s.max);
glm_vec2_copy(uvMin, s.uvMin);
glm_vec2_copy(uvMax, s.uvMax);
shadermaterial_t mat = { .unlit = { .texture = myTexture } };
spriteBatchBuffer(&s, 1, myShader, mat);
// End of pass -- flush remaining sprites:
spriteBatchFlush();
```
---
## Relationship to other systems
- **Text rendering** (`textDraw`) internally calls `spriteBatchBuffer`
for each glyph and requires a final `spriteBatchFlush()` after drawing.
- **UI frames** (`uiFrameDraw`) push 9 quads to the batch without
flushing -- the caller or `uitextboxDraw` is responsible for the flush.
- **ECS renderables** of type `ENTITY_RENDERABLE_TYPE_SPRITEBATCH` are
drawn via the spritebatch in the scene render pipeline.
---
## Notes
- `spriteBatchBuffer` changes the batch's `shader` and `material` fields.
If you mix different shaders or textures in one batch, add an explicit
`spriteBatchFlush()` call between groups to avoid draws with the wrong
material.
- The vertex buffer is a static global -- `SPRITEBATCH_VERTICES` must
not be written from multiple threads.
+115
View File
@@ -0,0 +1,115 @@
# Display -- Text Rendering
Source: `src/dusk/display/text/`
See also: `.claude/display-spritebatch.md`, `.claude/display-texture.md`
---
## Overview
Text rendering is layered on top of the SpriteBatch. Each character
maps to a glyph tile in a bitmap font atlas; `textDraw` builds the
corresponding `spritebatchsprite_t` values and pushes them to the
global `SPRITEBATCH`. The caller is responsible for flushing the batch.
---
## Font type (`font.h`)
```c
typedef struct {
texture_t *texture; // glyph atlas texture
tileset_t *tileset; // grid describing glyph size + UV layout
} font_t;
```
Both pointers are caller-owned. The text system does not allocate or
free them.
```c
extern font_t FONT_DEFAULT;
```
`FONT_DEFAULT` is the engine's built-in bitmap font. It is initialised
during `textInit()` and available for the engine lifetime.
---
## Character range
```c
#define TEXT_CHAR_START '!' // ASCII 33
```
The glyph atlas begins at `'!'` (ASCII 33). Characters below this value
-- space, control characters -- are handled specially:
- `' '` (space) advances the cursor by one tile width without drawing.
- Characters below `TEXT_CHAR_START` other than space are skipped.
---
## API (`text.h`)
```c
// Initialises the text system and FONT_DEFAULT.
errorret_t textInit(void);
// Disposes of the text system.
errorret_t textDispose(void);
// Draw a null-terminated string at (x, y) in screen/world space.
// Pushes sprites to SPRITEBATCH. Caller must call spriteBatchFlush()
// after all text has been drawn.
errorret_t textDraw(
const float_t x,
const float_t y,
const char_t *text,
const color_t color,
font_t *font
);
// Measure the bounding box of a string without drawing it.
void textMeasure(
const char_t *text,
const font_t *font,
int32_t *outWidth,
int32_t *outHeight
);
// Low-level: build a single glyph sprite at position pos.
// Returns a spritebatchsprite_t ready for spriteBatchBuffer.
spritebatchsprite_t textGetSprite(
const vec2 pos,
const char_t c,
const font_t *font
);
```
---
## Typical usage
```c
// Inside a render callback:
errorChain(textDraw(10.0f, 10.0f, "Hello", COLOR_WHITE, &FONT_DEFAULT));
errorChain(spriteBatchFlush());
```
If you are also drawing UI frames or other sprites in the same pass,
batch all the `textDraw` and `spriteBatchBuffer` calls first, then call
`spriteBatchFlush()` once at the end.
---
## Notes
- Text coordinates are in the same space as the scene render (screen
space for UI, or world space if placed in the scene).
- `textMeasure` returns pixel dimensions based on the font's
`tileset.tileWidth` and `tileset.tileHeight`. Use it to centre or
right-align text before drawing.
- For UI-attached text (dialogue, labels), prefer the `uitextbox_t`
system which handles word-wrap and paging automatically
(see `.claude/ui.md`).
+86
View File
@@ -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.
+23
View File
@@ -0,0 +1,23 @@
# 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` |
| Mesh, vertex format, primitive generators | `.claude/display-mesh.md` |
| SpriteBatch (2D quad renderer) | `.claude/display-spritebatch.md` |
| Text rendering, font, FONT_DEFAULT | `.claude/display-text.md` |
| Color types, macros, named constants | `.claude/display-color.md` |
+179
View File
@@ -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/<category>/entity<Name>.h/.c`.
- Struct: `entity<Name>_t`
- `entity<Name>Init(entityid_t, componentid_t)` (required)
- `entity<Name>Dispose(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`.
+95
View File
@@ -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()`.
+133
View File
@@ -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.
+109
View File
@@ -0,0 +1,109 @@
# 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
| Subsystem | Event | Fires when |
|-----------|-------|-----------|
| `inputactiondata_t` | `onPressed`, `onReleased` | Action button state changes |
| `uitextbox_t` | `onPageComplete` | Typewriter scroll reveals the full page |
| `uitextbox_t` | `onLastPage` | Last page is fully scrolled |
| `uifullbox_t` | `onTransitionEnd` | Colour transition animation completes |
| `uiloading_t` | `onShow` / `onHide` | Loading indicator fade completes |
| Asset system | `assetbatch_t` callback | All entries in a batch reach LOADED/ERROR |
See `.claude/ui.md` for the UI event details.
+187
View File
@@ -0,0 +1,187 @@
# Input System
Source: `src/dusk/input/`, platform layers in `src/dusk<platform>/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
## Defined actions
Actions are defined in `src/dusk/input/input.csv` and code-generated
into the `inputaction_t` enum. Current values:
| Constant | Meaning |
|----------|---------|
| `INPUT_ACTION_NULL` | Invalid / sentinel (0) |
| `INPUT_ACTION_UP` | Up direction |
| `INPUT_ACTION_DOWN` | Down direction |
| `INPUT_ACTION_LEFT` | Left direction |
| `INPUT_ACTION_RIGHT` | Right direction |
| `INPUT_ACTION_ACCEPT` | Confirm / primary action |
| `INPUT_ACTION_CANCEL` | Back / secondary action |
| `INPUT_ACTION_RAGEQUIT` | Quit the application |
| `INPUT_ACTION_CONSOLE` | Toggle debug console |
| `INPUT_ACTION_POINTERX` | Mouse / pointer X axis |
| `INPUT_ACTION_POINTERY` | Mouse / pointer Y axis |
| `INPUT_ACTION_COUNT` | Total count (not a valid action) |
## 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
// Single axis from a neg + pos pair of actions (returns [-1, 1]):
float_t inputAxis(inputaction_t neg, inputaction_t pos);
// 2D axis from four actions (negX/posX/negY/posY):
void inputAxis2D(
inputaction_t negX, inputaction_t posX,
inputaction_t negY, inputaction_t posY,
vec2 result
);
// Same four-action axis, normalized to a unit vector via atan2:
void inputAngle2D(
inputaction_t negX, inputaction_t posX,
inputaction_t negY, inputaction_t posY,
vec2 result
);
// 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` |
### Vita (`src/duskvita/input/`)
Layered on top of SDL2 (via vitaSDL2). Behaviour is similar to PSP --
no keyboard, no pointer, gamepad only.
## JS module (`Input`)
The input system is exposed to JS as the global `Input` object with
static methods. Action constants are pre-defined as numeric properties
on the `Input` object (e.g. `Input.ACCEPT`, `Input.UP`):
```js
// Check if the accept button is held this frame:
if(Input.isDown(Input.ACCEPT)) { ... }
// Was the cancel button just pressed?
if(Input.pressed(Input.CANCEL)) { ... }
// Analog value for the right trigger:
var val = Input.getValue(Input.RIGHT);
// Single axis (-1 to 1) from a neg/pos pair:
var h = Input.axis(Input.LEFT, Input.RIGHT);
```
All `Input.*` action constants match the `INPUT_ACTION_*` enum values
from the C layer (UP, DOWN, LEFT, RIGHT, ACCEPT, CANCEL, RAGEQUIT,
CONSOLE, POINTERX, POINTERY).
## Platform capability notes
| Feature | Linux/Knulli | PSP | Vita | GameCube/Wii |
|---------|-------------|-----|------|--------------|
| Keyboard | Yes (SDL2) | No | No | No |
| Pointer/Mouse | Yes (SDL2) | No | No | No |
| Gamepad | Yes (SDL2) | Yes (SDL2) | Yes (SDL2) | Yes (PAD) |
| Analog axes | Yes | L-Stick only | L-Stick, R-Stick | L-Stick, C-Stick, Triggers |
| Touch | Defined, not implemented | -- | -- | -- |
+90
View File
@@ -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/<lang_COUNTRY>.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.
+132
View File
@@ -0,0 +1,132 @@
# Network System
Source: `src/dusk/network/`, platform layers in
`src/dusk<platform>/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<platform>/network/network<platform>.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`.
+83
View File
@@ -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.
+127
View File
@@ -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.
+288
View File
@@ -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.
+162
View File
@@ -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;`.
+46
View File
@@ -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.
+200
View File
@@ -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/<TITLE_ID><slot>/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.
+173
View File
@@ -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.
+46
View File
@@ -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.
+98
View File
@@ -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<platform>/` in the matching subsystem
folder.
- Gate any core call-site with `#ifdef DUSK_<PLATFORM>` or the
relevant capability macro.
- Keep `src/dusk/` free of platform `#ifdef`s -- delegate through
the platform header macros instead.
+148
View File
@@ -0,0 +1,148 @@
# Save System
Source: `src/dusk/save/`, platform layers in `src/dusk<platform>/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<platform>/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.
+138
View File
@@ -0,0 +1,138 @@
# 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
Scene transitions are handled entirely in JS via the `Scene` global.
The `Scene` object is a singleton with:
```js
// Switch to a new scene. Calls dispose() on the current scene, then
// init() on the new one. Both happen synchronously this tick.
Scene.set(newSceneObject);
// The current scene object (may be null):
Scene.current
```
Typical scene-switch pattern:
```js
// Inside a scene's update or event handler:
const nextScene = require("scenes/gameplay.js");
Scene.set(nextScene);
```
`Scene.set` is synchronous -- it calls `dispose` on the old scene and
`init` on the new scene before returning. If `init` needs async work
(loading assets), use an async function and `await` inside `init`:
```js
nextScene.init = async function() {
await batch.load(); // wait for assets before proceeding
};
```
The C side does not defer the transition; the switch happens inside
the current `sceneUpdate` call.
## 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
```
+104
View File
@@ -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.
+167
View File
@@ -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/<category>/mymod.d.ts`
- Add `/// <reference path="..." />` to `types/index.d.ts`
+111
View File
@@ -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.
+100
View File
@@ -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.
+115
View File
@@ -0,0 +1,115 @@
# Time System
Source: `src/dusk/time/`, platform layers in `src/dusk<platform>/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<platform>/time/time<platform>.h/.c`.
2. Implement `timeGetReal<Platform>()` and
`timeGetRealTimeZone<Platform>()`.
3. If `DUSK_TIME_DYNAMIC`: also implement `timeTick<Platform>()` and
`timeGetDelta<Platform>()`.
4. Create `src/dusk<platform>/time/timeplatform.h` with the `#define`
macros pointing to your functions.
+226
View File
@@ -0,0 +1,226 @@
# UI System
Source: `src/dusk/ui/`
See also: `.claude/display-spritebatch.md`, `.claude/display-text.md`,
`.claude/display-color.md`
---
## Overview
The UI system renders overlaid interface elements on top of the scene
each frame. It is called by the engine after the scene render pipeline
and before the screen unbind -- game code does not drive it directly.
All UI elements render through the global SpriteBatch.
---
## Lifecycle
```c
extern ui_t UI;
errorret_t uiInit(void);
void uiUpdate(void);
errorret_t uiRender(void);
void uiDispose(void);
```
`uiUpdate` is called each game tick; `uiRender` is called each render
tick. The engine calls both automatically -- do not call them from
game scripts.
---
## Element registration (`uielement.h`)
```c
typedef struct {
uielementtype_t type; // UI_ELEMENT_TYPE_NULL or UI_ELEMENT_TYPE_NATIVE
errorret_t (*draw)(); // draw callback
} uielement_t;
extern uielement_t UI_ELEMENTS[]; // registered element array
```
`uiRender` iterates `UI_ELEMENTS` and calls each element's `draw`
callback. Elements register themselves during `uiInit`.
---
## Elements
### uiframe_t -- 9-slice border (`uiframe.h`)
A resizable bordered panel rendered using 9-slice (9-patch) technique
from a 3x3 tileset.
```c
typedef struct {
tileset_t tileset;
texture_t *texture;
} uiframe_t;
errorret_t uiFrameInit(uiframe_t *frame);
errorret_t uiFrameDraw(
const uiframe_t *frame,
const float_t x,
const float_t y,
const float_t width,
const float_t height
);
void uiFrameDispose(uiframe_t *frame); // does not dispose texture
```
`uiFrameDraw` pushes 9 quads (corners + edges + centre) to the
SpriteBatch without flushing. The caller must call `spriteBatchFlush()`
after all sprites for a draw pass are buffered.
---
### uitextbox_t -- typewriter dialogue box (`uitextbox.h`)
A global single-instance dialogue box that displays text one character
at a time with word-wrap and multi-page support.
```c
extern uitextbox_t UI_TEXTBOX;
errorret_t uiTextboxInit(void);
void uiTextboxDispose(void);
```
**Setting text:**
```c
uiTextboxSetText("Hello, world!");
// Automatically word-wraps and paginates.
// Resets currentPage and scroll to 0.
```
**Tick update (called from script `update` or uiUpdate):**
```c
uiTextboxUpdate();
// Advances typewriter by UI_TEXTBOX_SCROLL_CHARS_PER_TICK (1) each
// fixed tick. Skipped on dynamic sub-ticks.
```
**Page control:**
```c
bool_t uiTextboxPageIsComplete(void); // scroll revealed full page
bool_t uiTextboxHasNextPage(void); // more pages remain
void uiTextboxNextPage(void); // advance; no-op on last page
int32_t uiTextboxGetPageCharCount(void);
```
**Events:**
```c
// Fires when the current page is fully scrolled into view:
UI_TEXTBOX.onPageComplete
// Fires when the last page has been fully scrolled:
UI_TEXTBOX.onLastPage
```
Subscribe via the event system (see `.claude/events.md`).
**Limits:**
| Constant | Value |
|----------|-------|
| `UI_TEXTBOX_TEXT_MAX` | 1024 chars |
| `UI_TEXTBOX_LINES_MAX` | 64 lines |
| `UI_TEXTBOX_LINES_PER_PAGE_MAX` | 3 lines per page |
| `UI_TEXTBOX_SCROLL_CHARS_PER_TICK` | 1 char per tick |
---
### uifullbox_t -- full-screen colour overlay (`uifullbox.h`)
An animated solid-colour overlay that covers the entire screen. Used
for fade-to-black, scene transitions, and flash effects.
Two global instances are provided:
```c
extern uifullbox_t UI_FULLBOX_UNDER; // draws below scene content
extern uifullbox_t UI_FULLBOX_OVER; // draws above all content
```
```c
void uiFullboxInit(uifullbox_t *fullbox);
// Start a colour-to-colour transition:
void uiFullboxTransition(
uifullbox_t *fullbox,
color_t from,
color_t to,
float_t duration,
easingtype_t easing
);
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta);
errorret_t uiFullboxDraw(uifullbox_t *fullbox);
// Draw is skipped entirely when alpha == 0.
```
`fullbox.onTransitionEnd` event fires once when the transition
completes. Subscribe to it to chain scene transitions:
```c
// Typical fade-out before a scene change:
uiFullboxTransition(
&UI_FULLBOX_OVER,
COLOR_TRANSPARENT, COLOR_BLACK,
0.5f, EASING_OUT_QUAD
);
eventSubscribe(&UI_FULLBOX_OVER.onTransitionEnd, onFadeComplete, NULL);
```
---
### uiloading_t -- loading indicator (`uiloading.h`)
An animated loading indicator with fade-in / fade-out transitions.
Shown while asset batches are loading.
```c
extern uiloading_t UI_LOADING;
void uiLoadingInit(void);
void uiLoadingUpdate(float_t delta);
errorret_t uiLoadingDraw(void);
// Fade in; callback fires when fully visible:
void uiLoadingShow(eventcallback_t callback, void *user);
// Fade out; callback fires when fully hidden:
void uiLoadingHide(eventcallback_t callback, void *user);
```
`UI_LOADING_FADE_DURATION` is 0.5 seconds. `UI_LOADING_MARGIN` is 8px.
`uiLoadingDraw` is a no-op when the indicator is fully transparent.
---
### uifps_t -- FPS counter (`uifps.h`)
Debug FPS display. Measures real elapsed time between ticks and draws
the average frame rate as text in the corner of the screen.
```c
extern uifps_t UIFPS;
errorret_t uiFPSDraw(); // also performs the measurement update
```
Intended for debug builds only. Draw it explicitly from a JS `render`
hook or from a UI element -- it is not registered in `UI_ELEMENTS` by
default.
+191
View File
@@ -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 `<string.h>` / `<ctype.h>` 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.
+3 -6
View File
@@ -1,11 +1,8 @@
name: Build Dusk
on:
push:
branches:
- main
pull_request:
branches:
- main
tags:
- '*'
jobs:
run-tests:
runs-on: ubuntu-latest
@@ -144,7 +141,7 @@ jobs:
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
- name: Upload Wii binary
+1
View File
@@ -106,3 +106,4 @@ yarn.lock
/build*
/assets/test
/tools_old
/assets/test.png
+519
View File
@@ -0,0 +1,519 @@
# 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` |
| Display -- mesh, vertex format, primitives | `.claude/display-mesh.md` |
| Display -- SpriteBatch (2D quad renderer) | `.claude/display-spritebatch.md` |
| Display -- text rendering, FONT_DEFAULT | `.claude/display-text.md` |
| Display -- color types, macros, constants | `.claude/display-color.md` |
| UI system (frame, textbox, fullbox, loading, FPS) | `.claude/ui.md` |
| Console (debug overlay, POSIX stdin mode) | `.claude/console.md` |
| Scene system (lifecycle, render pipeline, JS hooks) | `.claude/scene.md` |
---
## File headers
Every C, H, and JS file starts with:
```c
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
```
JS files use `//` comment style instead.
---
## C conventions
### Types
Always use the project-defined aliases instead of bare C primitives:
| Use | Not |
|-----------|--------------|
| `bool_t` | `bool` |
| `int_t` | `int` |
| `float_t` | `float` |
| `char_t` | `char` |
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
### Naming
- **Functions** — snake_case, prefixed with their module:
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
- **Macros / constants** — UPPER_SNAKE_CASE:
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
- **Files** — snake_case matching the primary type: `entityposition.c`,
`moduleassetbatch.c`
### Header files (`.h`)
- Use `#pragma once` — no include guards.
- Declare every public function, `#define`, and `extern` global.
- Write a JSDoc block (`/** … */`) above every declaration explaining
purpose, `@param`s, and `@returns`.
- Only include headers that the `.h` file itself strictly requires for
the types it exposes. Move everything else to the `.c` file.
Do not use forward declarations as a workaround — use the real
include in the `.c` file instead.
### Implementation files (`.c`)
- Contain function bodies only; no declarations.
- Pull in whatever additional includes the implementation needs.
- Do not use `static` or `inline` on **functions**. Every function,
including internal helpers, must be declared in the matching `.h` and
defined in the `.c` file. Internal helpers belong near the bottom of
the `.c` file, not at the top with a `static` qualifier.
`static` and `inline` on functions are only appropriate when the
function body is written directly inside a `.h` file.
`static` on **variables** (file-scope state) is fine and expected.
### Formatting
- Hard-wrap all lines at **80 characters**.
### Error handling
Return `errorret_t` from fallible functions. Use these macros:
```c
errorOk(); // return success
errorThrow("msg %d", val); // return failure with message
errorChain(someCall()); // propagate failure, continue on success
errorIsOk(ret) / errorIsNotOk(ret) // test a result
errorCatch(ret); // handle + free an error
```
Never return raw error codes or use `errno` for in-engine errors.
### Memory
Use the project allocator — never raw `malloc`/`free`:
```c
memoryAllocate(size) // allocate
memoryFree(ptr) // free
memoryZero(dest, size) // zero a block
memoryCopy(dest, src, size) // copy
```
### Asserts
Prefer specific assert macros over bare `assert()`:
```c
assertNotNull(ptr, "msg");
assertTrue(cond, "msg");
assertFalse(cond, "msg");
assertUnreachable("msg");
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
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
myfile.c
)
```
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:
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|----------------------|-------------------|------------------|
| `linux` | `DUSK_LINUX` | Linux desktop |
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
| `psp` | `DUSK_PSP` | Sony PSP |
| `vita` | `DUSK_VITA` | PlayStation Vita |
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
| `wii` | `DUSK_WII` | Nintendo Wii |
### Layer structure
```
src/dusk/ core, platform-agnostic game logic
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 (no SDL2/OpenGL)
```
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
it uses native GameCube/Wii rendering and input APIs.
### Platform guards
Use the compile-time macros for platform-specific code:
```c
#ifdef DUSK_PSP
// PSP-only path
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
// GameCube / Wii path
#else
// Generic / Linux fallback
#endif
```
Additional capability macros set per-target:
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
### 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 it under `src/dusk<platform>/` in the matching subsystem folder.
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
or capability macro.
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
the platform header macros instead.
---
## 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/<category>/entityMyComp.h/.c` with
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
`entityMyCompDispose()`.
2. Add the include to `src/dusk/entity/componentlist.h` header block.
3. Add a row to `src/dusk/entity/componentlist.h`:
```c
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
```
This auto-generates the enum, union field, and definition entry.
4. If JS-facing, create the script module and `.d.ts` (see below).
---
## 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/<category>/moduleMyMod.h/.c`.
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
- Use `moduleBaseFunction(name)` to define JS-callable functions.
- Register props/funcs in `moduleMyModInit()` with
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
`scriptProtoDefineStaticFunc`.
2. `#include` the header in
`src/dusk/script/module/modulelist.c` and call
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
`moduleListDispose()`).
3. For component modules also register in
`src/dusk/script/module/entity/component/modulecomponentlist.c`
so `entity.add()` returns the typed wrapper.
4. Create `types/<category>/mymod.d.ts` and add a
`/// <reference path="..." />` line to `types/index.d.ts`.
---
## Script module type declarations
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
check whether the corresponding `types/**/*.d.ts` needs updating and
apply any changes before finishing the task.
---
## JavaScript (asset scripts)
- Use `var` for module-level state; `const` for values that never
change.
- Always use semicolons.
- Scene objects are plain objects (`var scene = {}`) with assigned
methods.
- Export via `module.exports = scene`.
- Async scene init should use `async function` and `await`.
---
## Coding style
### ASCII only
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000U+007F).
Non-ASCII characters are banned even in comments and string literals.
Use ASCII-only substitutes instead:
- `--` or `-` instead of `` (em dash)
- `->` instead of `` (arrow)
- `x` or `*` instead of `×` (multiplication)
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
### Indentation
2 spaces. No tabs.
### Keyword and operator spacing
No space between a keyword or function name and its opening parenthesis:
```c
if(!ptr) return;
for(uint8_t i = 0; i < count; i++) {
while(entry->state != DONE) {
switch(type) {
sizeof(assetbatch_t)
memoryZero(ptr, size)
```
Spaces around all binary operators and after every comma:
```c
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
(size_t)end - (size_t)start
foo(a, b, c)
```
### Braces
Opening brace on the **same line** as the statement (K&R style) for all
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
```c
void assetEntryLock(assetentry_t *entry) {
...
}
if(dirty) {
...
} else {
...
}
```
### Guard returns
Short guards go on one line with no braces:
```c
if(!ptr) return;
if(!b || !b->batch) return jerry_undefined();
if(!(flags & DIRTY)) return;
```
### Blank lines
- One blank line between functions; no blank line at the start or end of
a function body.
- One blank line between logical blocks inside a function body.
- No trailing blank lines at the end of a file.
### Pointer placement
`*` is attached to the variable name, not the type:
```c
assetentry_t *entry
const char_t *name
void *ptr
uint8_t *d = (uint8_t *)dest;
```
### Casts
Space between cast and operand:
```c
(assetbatch_t *)user
(uint8_t *)dest
(textureformat_t)v
```
### Return
No parentheses around the return value:
```c
return ptr;
return MEMORY_POINTERS_IN_USE;
```
### switch / case
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
```c
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
default:
break;
}
```
### Multi-line function signatures
When parameters don't fit on one line, put each on its own line indented
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
its own line at column 0:
```c
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
errorret_t memoryCompare(
const void *a,
const void *b,
const size_t size
);
```
### Structs and enums
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
brace and name on the same line:
```c
typedef struct {
errorcode_t code;
char_t *message;
} errorstate_t;
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
```
### Designated initialisers
Spaces inside braces; `.field = value`:
```c
jsassetentry_t e = { .entry = entry };
assetbatchloadedpend_t init = { .batch = batch };
```
### Ternary operator
Spaces around `?` and `:`:
```c
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
```
### const placement
`const` before the type, `*` attached to the variable:
```c
const char_t *name
const void *src
const size_t size
```
### Comments in `.c` files
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
functions follow one another with a single blank line between them.
- Multi-line explanatory comments inside function bodies use `//` lines:
```c
// Script modules are freed; orphaned JS wrapper objects now get GC'd
// so their finalizers fire before assetDispose() checks ref counts.
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
```
- Do not use `/* */` for inline or inline-block comments inside `.c`
function bodies.
### Comments in `.h` files
Every public declaration gets a Javadoc block (`/** … */`) with
`@param` and `@returns` where relevant. Keep it on the lines immediately
above the declaration with no blank line in between.
---
## Tests
- Tests live in `test/` mirroring `src/dusk/` structure.
- Use cmocka; include `dusktest.h`.
- Test functions: `static void test_something(void **state)`.
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
leaks.
- Build with `-DDUSK_BUILD_TESTS=ON`.
+5
View File
@@ -76,6 +76,10 @@ else()
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}" CACHE INTERNAL ${DUSK_CACHE_TARGET})
endif()
if(NOT DEFINED DUSK_VERSION)
string(TIMESTAMP DUSK_VERSION "debug-%y%m%d%H%M%S")
endif()
# Definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
@@ -84,6 +88,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
DUSK_GAME_AUTHOR="${DUSK_GAME_AUTHOR}"
DUSK_GAME_SHORT_DESCRIPTION="${DUSK_GAME_SHORT_DESCRIPTION}"
DUSK_GAME_LONG_DESCRIPTION="${DUSK_GAME_LONG_DESCRIPTION}"
DUSK_VERSION="${DUSK_VERSION}"
)
# Toolchains
-30
View File
@@ -1,30 +0,0 @@
function MoveCubeCutscene(params) {
Cutscene.call(this);
this.cube = params.cube;
var SPEED = 3.0;
var DURATION = 2.0;
this.anim = new Animation([
[
{ time: 0.0, value: 0, easing: Easing.inOutQuad },
{ time: DURATION, value: -(SPEED * DURATION), easing: Easing.inOutQuad },
{ time: DURATION * 2, value: 0 }
]
]);
this.anim.onComplete = function() {
Cutscene.finish();
};
}
MoveCubeCutscene.prototype = Object.create(Cutscene.prototype);
MoveCubeCutscene.prototype.constructor = MoveCubeCutscene;
MoveCubeCutscene.prototype.update = function() {
this.anim.update(TIME.delta);
this.cube.position.position.x = this.anim.properties[0].value;
};
module = MoveCubeCutscene;
-24
View File
@@ -1,24 +0,0 @@
var OverworldEntity = include('entities/OverworldEntity.js');
function CubeEntity() {
OverworldEntity.call(this);
this.add(RENDERABLE);
this.cubeMesh = Mesh.createCube();
this.renderable.mesh = this.cubeMesh;
}
CubeEntity.prototype = Object.create(OverworldEntity.prototype);
CubeEntity.prototype.constructor = CubeEntity;
CubeEntity.prototype.update = function() {
OverworldEntity.prototype.update.call(this);
var speed = 5.0;
var move = Input.axis2D(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT, INPUT_ACTION_UP, INPUT_ACTION_DOWN);
this.position.position.x += move.x * speed * TIME.delta;
this.position.position.z += move.y * speed * TIME.delta;
this.renderable.color = Color.rainbow();
};
module = CubeEntity;
-21
View File
@@ -1,21 +0,0 @@
function OverworldEntity() {
Entity.call(this);
this.add(POSITION);
}
OverworldEntity.prototype = Object.create(Entity.prototype);
OverworldEntity.prototype.constructor = OverworldEntity;
OverworldEntity.prototype.update = function() {
// var speed = 3.0;
// var move = Input.axis2D(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT, INPUT_ACTION_UP, INPUT_ACTION_DOWN);
// this.position.position.x += move.x * speed * TIME.delta;
// this.position.position.z += move.y * speed * TIME.delta;
}
OverworldEntity.prototype.dispose = function() {
Entity.prototype.dispose.call(this);
}
module = OverworldEntity;
+17 -89
View File
@@ -1,93 +1,21 @@
Console.visible = true;
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
// Default input bindings.
if (typeof PSP !== 'undefined') {
Input.bind("up", INPUT_ACTION_UP);
Input.bind("down", INPUT_ACTION_DOWN);
Input.bind("left", INPUT_ACTION_LEFT);
Input.bind("right", INPUT_ACTION_RIGHT);
Input.bind("accept", INPUT_ACTION_ACCEPT);
Input.bind("cancel", INPUT_ACTION_CANCEL);
Input.bind("select", INPUT_ACTION_RAGEQUIT);
Input.bind("lstick_up", INPUT_ACTION_UP);
Input.bind("lstick_down", INPUT_ACTION_DOWN);
Input.bind("lstick_left", INPUT_ACTION_LEFT);
Input.bind("lstick_right", INPUT_ACTION_RIGHT);
Input.bind("triangle", INPUT_ACTION_CONSOLE);
const platformNames = {
[System.PLATFORM_LINUX]: 'Linux',
[System.PLATFORM_KNULLI]: 'Knulli',
[System.PLATFORM_PSP]: 'PSP',
[System.PLATFORM_GAMECUBE]: 'GameCube',
[System.PLATFORM_WII]: 'Wii',
};
} else if (typeof DOLPHIN !== 'undefined') {
Input.bind("up", INPUT_ACTION_UP);
Input.bind("down", INPUT_ACTION_DOWN);
Input.bind("left", INPUT_ACTION_LEFT);
Input.bind("right", INPUT_ACTION_RIGHT);
Input.bind("b", INPUT_ACTION_CANCEL);
Input.bind("a", INPUT_ACTION_ACCEPT);
Input.bind("z", INPUT_ACTION_CONSOLE);
Input.bind("lstick_up", INPUT_ACTION_UP);
Input.bind("lstick_down", INPUT_ACTION_DOWN);
Input.bind("lstick_left", INPUT_ACTION_LEFT);
Input.bind("lstick_right", INPUT_ACTION_RIGHT);
Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown'));
} else if (typeof LINUX !== 'undefined') {
if (typeof INPUT_KEYBOARD !== 'undefined') {
Input.bind("w", INPUT_ACTION_UP);
Input.bind("s", INPUT_ACTION_DOWN);
Input.bind("a", INPUT_ACTION_LEFT);
Input.bind("d", INPUT_ACTION_RIGHT);
UIFullboxOver.setColor(Color.BLACK);
Input.bind("left", INPUT_ACTION_LEFT);
Input.bind("right", INPUT_ACTION_RIGHT);
Input.bind("up", INPUT_ACTION_UP);
Input.bind("down", INPUT_ACTION_DOWN);
Input.bind("enter", INPUT_ACTION_ACCEPT);
Input.bind("e", INPUT_ACTION_ACCEPT);
Input.bind("q", INPUT_ACTION_CANCEL);
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
Input.bind("`", INPUT_ACTION_CONSOLE);
}
if (typeof INPUT_GAMEPAD !== 'undefined') {
Input.bind("gamepad_up", INPUT_ACTION_UP);
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
Input.bind("gamepad_b", INPUT_ACTION_CANCEL);
Input.bind("gamepad_back", INPUT_ACTION_RAGEQUIT);
Input.bind("gamepad_lstick_up", INPUT_ACTION_UP);
Input.bind("gamepad_lstick_down", INPUT_ACTION_DOWN);
Input.bind("gamepad_lstick_left", INPUT_ACTION_LEFT);
Input.bind("gamepad_lstick_right", INPUT_ACTION_RIGHT);
}
if (typeof INPUT_POINTER !== 'undefined') {
Input.bind("mouse_x", INPUT_ACTION_POINTERX);
Input.bind("mouse_y", INPUT_ACTION_POINTERY);
}
} else {
print("Unknown platform, no default input bindings set.");
}
Scene.set('scenes/cube.js');
// Console.print("Testing save stuff;");
// Console.print("Save Count: " + Save.count);
// Console.print("Save 0 exists? " + Save.exists(0));
// try {
// Save.load(0);
// Console.print("Successfully loaded save 0.");
// } catch (e) {
// Console.print("Error loading save 0: " + e);
// Save.delete(0);
// }
// Console.print("Save 0 exists? " + Save.exists(0));
// Console.print("Writing...");
// Save.write(0);
// Console.print("Save 0 exists? " + Save.exists(0));
// Console.print("Save 0 data: " + Save.load(0));
requireAsync('testscene.js').then(Scene.set).catch(err => {
Console.print('Error loading scene: ' + err);
Engine.exit();
});
-17
View File
@@ -1,17 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function TestChunkN100(x, y, z) {
MapChunk.call(this, x, y, z);
this.cube = new CubeEntity();
this.cube.position.position = new Vec3(-16, 0, 0);
}
TestChunkN100.prototype = Object.create(MapChunk.prototype);
TestChunkN100.prototype.constructor = TestChunkN100;
TestChunkN100.prototype.dispose = function() {
this.cube.dispose();
};
module = TestChunkN100;
-17
View File
@@ -1,17 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function TestChunk000(x, y, z) {
MapChunk.call(this, x, y, z);
this.cube = new CubeEntity();
this.cube.position.position = new Vec3(0, 0, 0);
}
TestChunk000.prototype = Object.create(MapChunk.prototype);
TestChunk000.prototype.constructor = TestChunk000;
TestChunk000.prototype.dispose = function() {
this.cube.dispose();
};
module = TestChunk000;
-17
View File
@@ -1,17 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function TestChunk001(x, y, z) {
MapChunk.call(this, x, y, z);
this.cube = new CubeEntity();
this.cube.position.position = new Vec3(0, 0, 16);
}
TestChunk001.prototype = Object.create(MapChunk.prototype);
TestChunk001.prototype.constructor = TestChunk001;
TestChunk001.prototype.dispose = function() {
this.cube.dispose();
};
module = TestChunk001;
-17
View File
@@ -1,17 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function TestChunk010(x, y, z) {
MapChunk.call(this, x, y, z);
this.cube = new CubeEntity();
this.cube.position.position = new Vec3(0, 16, 0);
}
TestChunk010.prototype = Object.create(MapChunk.prototype);
TestChunk010.prototype.constructor = TestChunk010;
TestChunk010.prototype.dispose = function() {
this.cube.dispose();
};
module = TestChunk010;
-17
View File
@@ -1,17 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function TestChunk100(x, y, z) {
MapChunk.call(this, x, y, z);
this.cube = new CubeEntity();
this.cube.position.position = new Vec3(16, 0, 0);
}
TestChunk100.prototype = Object.create(MapChunk.prototype);
TestChunk100.prototype.constructor = TestChunk100;
TestChunk100.prototype.dispose = function() {
this.cube.dispose();
};
module = TestChunk100;
-17
View File
@@ -1,17 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function TestChunk200(x, y, z) {
MapChunk.call(this, x, y, z);
this.cube = new CubeEntity();
this.cube.position.position = new Vec3(32, 0, 0);
}
TestChunk200.prototype = Object.create(MapChunk.prototype);
TestChunk200.prototype.constructor = TestChunk200;
TestChunk200.prototype.dispose = function() {
this.cube.dispose();
};
module = TestChunk200;
-17
View File
@@ -1,17 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function TestChunk300(x, y, z) {
MapChunk.call(this, x, y, z);
this.cube = new CubeEntity();
this.cube.position.position = new Vec3(48, 0, 0);
}
TestChunk300.prototype = Object.create(MapChunk.prototype);
TestChunk300.prototype.constructor = TestChunk300;
TestChunk300.prototype.dispose = function() {
this.cube.dispose();
};
module = TestChunk300;
-17
View File
@@ -1,17 +0,0 @@
function TestMap() {
Map.call(this);
Map.setChunkSize(16, 16, 16);
Map.setPosition(0, 0, 0);
}
TestMap.prototype = Object.create(Map.prototype);
TestMap.prototype.constructor = TestMap;
TestMap.prototype.update = function() {
};
TestMap.prototype.dispose = function() {
};
module = TestMap;
+63
View File
@@ -0,0 +1,63 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const PLAYER_SPEED = 5.0;
// 1 world unit = 16 pixels.
const PIXEL_SCALE = 1.0 / 16.0;
// Player sprite is 32x32 px (test.png dimensions).
const PLAYER_W = 32 * PIXEL_SCALE;
const PLAYER_H = 32 * PIXEL_SCALE;
var player = {};
player.getAssets = () => {
return [
{ path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA }
];
}
player.init = function(scene) {
var texture = scene.assets.getAssetByPath('test.png');
Console.print('Player init: got texture ' + texture);
_entity = Entity.create();
_position = _entity.add(Component.POSITION);
_physics = _entity.add(Component.PHYSICS);
_physics.bodyType = Physics.DYNAMIC;
_physics.shape = Physics.SHAPE_CUBE;
_physics.gravityScale = 1.0;
var r = _entity.add(Component.RENDERABLE);
r.texture = texture.texture;
r.type = Renderable.SPRITEBATCH;
r.color = new Color(220, 80, 80);
// Upright quad centered on X, bottom-aligned on Y.
r.sprites = [[-PLAYER_W/2, 0, 0, PLAYER_W/2, PLAYER_H, 0, 0, 1, 1, 0]];
_position.localPosition = new Vec3(0, PLAYER_H, 0);
};
player.getPosition = function() {
return _position;
};
player.update = function() {
if(!_physics) return;
var vx = Input.axis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT) * PLAYER_SPEED;
var vz = Input.axis(INPUT_ACTION_DOWN, INPUT_ACTION_UP) * PLAYER_SPEED;
// Preserve vertical velocity so gravity and landing work correctly.
var vy = _physics.velocity.y;
_physics.velocity = new Vec3(vx, vy, vz);
};
player.dispose = function() {
Entity.dispose(_entity);
_entity = null;
_position = null;
_physics = null;
};
module.exports = player;
-33
View File
@@ -1,33 +0,0 @@
var CubeEntity = include('entities/CubeEntity.js');
function CubeScene() {
Map.load('test');
this.cam = new Entity();
this.cam.add(POSITION);
this.cam.add(CAMERA);
this.player = new CubeEntity();
this.player.position.position = new Vec3(0, 0, 0);
}
CubeScene.prototype = Object.create(Scene.prototype);
CubeScene.prototype.constructor = CubeScene;
CubeScene.prototype.update = function() {
this.player.update();
var pos = this.player.position.position;
this.cam.position.position = new Vec3(pos.x, pos.y + 8, pos.z + 8);
this.cam.position.lookAt(pos);
Map.setPosition(Math.floor(pos.x), Math.floor(pos.y), Math.floor(pos.z));
};
CubeScene.prototype.dispose = function() {
this.cam.dispose();
this.player.dispose();
Map.dispose();
};
module = CubeScene;
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
var scene = {};
// Pokemon DS-style camera: ~34 degrees elevation (atan(6/9)).
// CAM_HEIGHT / CAM_DIST ratio controls the tilt - keep it under 0.7 for
// the characteristically shallow DS angle.
const CAM_HEIGHT = 6;
const CAM_DIST = 9;
scene.init = async function() {
// Camera
scene.cam = Entity.create();
var camPos = scene.cam.add(Component.POSITION);
var cam = scene.cam.add(Component.CAMERA);
camPos.localPosition = new Vec3(3, 3, 3);
camPos.lookAt(new Vec3(0, 0, 0));
// Floor - large flat slab, no texture needed.
scene.floor = Entity.create();
var floorPos = scene.floor.add(Component.POSITION);
var floorR = scene.floor.add(Component.RENDERABLE);
floorR.type = Renderable.SHADER_MATERIAL;
floorR.color = Color.BLUE;
// floorPos.localScale = new Vec3(16, 0.2, 16);
// floorPos.localPosition = new Vec3(0, -0.1, 0);
await UIFullboxOver.transition(Color.BLACK, Color.TRANSPARENT, 1.0);
};
scene.update = function() {
};
scene.dispose = function() {
Entity.dispose(scene.floor);
Entity.dispose(scene.cam);
};
module.exports = scene;
+96
View File
@@ -0,0 +1,96 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Turn things off we don't need
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
set(JERRY_EXT ON CACHE BOOL "" FORCE)
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
# Fetch Jerry
include(FetchContent)
FetchContent_Declare(
jerryscript
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
GIT_TAG float32-fix
)
FetchContent_MakeAvailable(jerryscript)
# Mark found
set(jerryscript_FOUND ON)
# Define targets
if(TARGET jerryscript-core)
set(JERRY_CORE_TARGET jerryscript-core)
elseif(TARGET jerry-core)
set(JERRY_CORE_TARGET jerry-core)
endif()
if(TARGET jerryscript-ext)
set(JERRY_EXT_TARGET jerryscript-ext)
elseif(TARGET jerry-ext)
set(JERRY_EXT_TARGET jerry-ext)
endif()
if(TARGET jerryscript-port-default)
set(JERRY_PORT_TARGET jerryscript-port-default)
elseif(TARGET jerry-port-default)
set(JERRY_PORT_TARGET jerry-port-default)
elseif(TARGET jerryscript-port)
set(JERRY_PORT_TARGET jerryscript-port)
elseif(TARGET jerry-port)
set(JERRY_PORT_TARGET jerry-port)
endif()
if(NOT JERRY_CORE_TARGET)
message(FATAL_ERROR "JerryScript core target not found")
endif()
if(NOT JERRY_EXT_TARGET)
message(FATAL_ERROR "JerryScript ext target not found")
endif()
if(NOT JERRY_PORT_TARGET)
message(FATAL_ERROR "JerryScript port target not found")
endif()
foreach(tgt IN ITEMS
${JERRY_CORE_TARGET}
${JERRY_EXT_TARGET}
${JERRY_PORT_TARGET}
)
if(TARGET ${tgt})
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
JERRY_NUMBER_TYPE_FLOAT64=0
JERRY_BUILTIN_DATE=0
)
endif()
endforeach()
# Export include dirs through the targets
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-core/include
)
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-ext/include
)
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-port/default/include
)
# Suppress JerryScript-only warning
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
-Wno-error
)
endif()
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
+43
View File
@@ -0,0 +1,43 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# dusk_embed_js(TARGET JS_FILE [NAME identifier])
#
# Converts a JS file into a C string header in DUSK_GENERATED_HEADERS_DIR.
# The generated header defines:
# static const char <NAME>[] = "...";
# static const size_t <NAME>_SIZE = sizeof(<NAME>) - 1;
#
# NAME defaults to the uppercase stem + "_JS" (e.g. scene.js -> SCENE_JS).
function(dusk_embed_js TARGET JS_FILE)
cmake_parse_arguments(ARG "" "NAME" "" ${ARGN})
get_filename_component(JS_ABS "${JS_FILE}" ABSOLUTE)
get_filename_component(JS_STEM "${JS_FILE}" NAME_WE)
set(OUTPUT_HEADER "${DUSK_GENERATED_HEADERS_DIR}/${JS_STEM}_js.h")
set(NAME_ARG "")
if(ARG_NAME)
set(NAME_ARG "--name" "${ARG_NAME}")
endif()
add_custom_command(
OUTPUT "${OUTPUT_HEADER}"
COMMAND ${Python3_EXECUTABLE} -m tools.js2c
--input "${JS_ABS}"
--output "${OUTPUT_HEADER}"
${NAME_ARG}
WORKING_DIRECTORY "${DUSK_ROOT_DIR}"
DEPENDS "${JS_ABS}"
COMMENT "js2c: ${JS_STEM}.js -> ${JS_STEM}_js.h"
VERBATIM
)
file(RELATIVE_PATH JS_REL "${DUSK_ROOT_DIR}" "${JS_ABS}")
string(MAKE_C_IDENTIFIER "dusk_js2c_${JS_REL}" JS_TARGET)
add_custom_target(${JS_TARGET} DEPENDS "${OUTPUT_HEADER}")
add_dependencies(${TARGET} ${JS_TARGET})
endfunction()
+8 -5
View File
@@ -1,15 +1,18 @@
# Build type: FAT (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "FAT" CACHE STRING "Dolphin asset source: FAT (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "FAT" "ISO")
# Build type: DOL (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "DOL" CACHE STRING "Dolphin asset source: DOL (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "DOL" "ISO")
# Target definitions
# Numeric tokens so #if DUSK_DOLPHIN_BUILD_TYPE == DOL works in C.
# DUSK_DOLPHIN_BUILD_TYPE is passed without quotes so it expands to the identifier.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_DOLPHIN
DUSK_INPUT_GAMEPAD
DUSK_DISPLAY_WIDTH=640
DUSK_DISPLAY_HEIGHT=480
DUSK_THREAD_PTHREAD
DUSK_DOLPHIN_BUILD_TYPE="${DUSK_DOLPHIN_BUILD_TYPE}"
DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
)
# Custom compiler flags
+1
View File
@@ -34,6 +34,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_OPENGL
DUSK_OPENGL_ES
DUSK_LINUX
DUSK_KNULLI
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_HEIGHT_DEFAULT=480
+2
View File
@@ -26,6 +26,8 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# CURL::libcurl
)
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
+1
View File
@@ -7,6 +7,7 @@ fi
mkdir -p build-gamecube
cmake -S. -Bbuild-gamecube \
-DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \
-DDKP_OGC_PLATFORM_LIBRARY=libogc2
cd build-gamecube
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/build-linux.sh"
docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-psp -f docker/psp/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-psp /bin/bash -c "./scripts/build-psp.sh"
docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-vita -f docker/vita/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-vita /bin/bash -c "./scripts/build-vita.sh"
docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
+5 -1
View File
@@ -5,6 +5,10 @@ if [ -z "$DEVKITPRO" ]; then
fi
mkdir -p build-wii
cmake -S. -Bbuild-wii -DDUSK_TARGET_SYSTEM=wii -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cmake -S. -Bbuild-wii \
-DDUSK_TARGET_SYSTEM=wii \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL
cd build-wii
make -j$(nproc) VERBOSE=1
mv Dusk.dol boot.dol
+5 -1
View File
@@ -1,3 +1,7 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/test-linux.sh"
docker run \
--rm \
-v "${GITHUB_WORKSPACE}:/workdir" \
dusk-linux \
/bin/bash -c "./scripts/test-linux.sh"
+2
View File
@@ -1,4 +1,6 @@
#!/bin/bash
set -e
rm -rf build-tests
cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-tests -- -j$(nproc)
ctest --output-on-failure --test-dir build-tests
+17
View File
@@ -32,6 +32,22 @@ if(NOT yyjson_FOUND)
endif()
endif()
if(NOT jerryscript_FOUND)
find_package(jerryscript REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
jerryscript::core
jerryscript::ext
jerryscript::port
)
endif()
if(DUSK_BACKTRACE)
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_BACKTRACE
)
endif()
# Includes
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
@@ -62,6 +78,7 @@ add_subdirectory(input)
add_subdirectory(locale)
add_subdirectory(physics)
add_subdirectory(scene)
add_subdirectory(script)
add_subdirectory(system)
add_subdirectory(time)
add_subdirectory(ui)
-1
View File
@@ -7,5 +7,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
animation.c
animationproperty.c
)
+33 -74
View File
@@ -8,86 +8,45 @@
#include "util/memory.h"
#include "util/math.h"
void animationInit(animation_t *anim) {
memoryZero(anim, sizeof(animation_t));
eventInit(&anim->onStart);
eventInit(&anim->onComplete);
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
anim->keyframes = keyframes;
anim->keyframeCount = keyframeCount;
}
animationproperty_t *animationAddProperty(animation_t *anim, float_t *target) {
assertTrue(
anim->propertyCount < ANIMATION_PROPERTY_COUNT_MAX,
"Property count exceeds ANIMATION_PROPERTY_COUNT_MAX"
);
animationproperty_t *prop = &anim->properties[anim->propertyCount++];
prop->keyframeCount = 0;
prop->target = target;
return prop;
}
float_t animationGetValue(animation_t *anim, const float_t time) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
void animationUpdate(animation_t *anim, float_t delta) {
assertNotNull(anim, "Animation cannot be null");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
if(
(anim->flags & ANIMATION_FLAG_FINISHED) &&
!(anim->flags & ANIMATION_FLAG_LOOP_ENABLED)
) return;
bool_t wasStarted = (anim->flags & ANIMATION_FLAG_STARTED) != 0;
anim->flags |= ANIMATION_FLAG_STARTED;
anim->time += delta;
float_t duration = animationGetDuration(anim);
bool_t justFinished = false;
if(anim->time >= duration) {
if(anim->flags & ANIMATION_FLAG_LOOP_ENABLED) {
anim->time = mathModFloat(anim->time, duration);
} else {
anim->time = duration;
if(!(anim->flags & ANIMATION_FLAG_FINISHED)) {
anim->flags |= ANIMATION_FLAG_FINISHED;
justFinished = true;
}
do {
if(current->time > time) {
end = current;
break;
}
}
start = current;
current++;
for(uint8_t i = 0; i < anim->propertyCount; i++) {
animationproperty_t *prop = &anim->properties[i];
if(prop->target != NULL) {
*prop->target = animationPropertyGetValue(prop, anim->time);
if(current > last) {
end = start;
break;
}
}
} while(true);
if(!wasStarted) eventInvoke(&anim->onStart, anim);
if(justFinished) eventInvoke(&anim->onComplete, anim);
}
void animationReset(animation_t *anim) {
anim->time = 0.0f;
anim->flags &= ~(ANIMATION_FLAG_FINISHED | ANIMATION_FLAG_STARTED);
}
bool_t animationIsFinished(const animation_t *anim) {
return (anim->flags & ANIMATION_FLAG_FINISHED) != 0;
}
bool_t animationIsStarted(const animation_t *anim) {
return (anim->flags & ANIMATION_FLAG_STARTED) != 0;
}
bool_t animationIsLooping(const animation_t *anim) {
return (anim->flags & ANIMATION_FLAG_LOOP_ENABLED) != 0;
}
float_t animationGetDuration(const animation_t *anim) {
float_t duration = 0.0f;
for(uint8_t i = 0; i < anim->propertyCount; i++) {
animationproperty_t *prop = &anim->properties[i];
if(prop->keyframeCount == 0) continue;
float_t lastKeyframeTime =
prop->keyframes[prop->keyframeCount - 1].time;
if(lastKeyframeTime > duration) duration = lastKeyframeTime;
}
return duration;
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
+15 -71
View File
@@ -4,87 +4,31 @@
// https://opensource.org/licenses/MIT
#pragma once
#include "animationproperty.h"
#include "event/event.h"
#define ANIMATION_PROPERTY_COUNT_MAX 8
#define ANIMATION_FLAG_FINISHED (1 << 0)
#define ANIMATION_FLAG_STARTED (1 << 1)
#define ANIMATION_FLAG_LOOP_ENABLED (1 << 2)
#include "keyframe.h"
typedef struct {
animationproperty_t properties[ANIMATION_PROPERTY_COUNT_MAX];
uint8_t propertyCount;
float_t time;
uint8_t flags;
event_t onStart;
event_t onComplete;
keyframe_t *keyframes;
uint16_t keyframeCount;
} animation_t;
/**
* Initializes an animation.
*
* @param anim The animation to initialize.
* @param keyframes The keyframes to use for the animation.
* @param keyframeCount The number of keyframes in the animation.
*/
void animationInit(animation_t *anim);
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
);
/**
* Adds a new animated property. The caller owns target and must keep it valid
* for the lifetime of the animation.
* Gets the value of the animation at a given time.
*
* @param anim The animation to add the property to.
* @param target Pointer to the float to write interpolated values into.
* @return A pointer to the new property.
* @param anim The animation to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
*/
animationproperty_t *animationAddProperty(animation_t *anim, float_t *target);
/**
* Advances the animation by delta seconds and writes interpolated values to
* each property's target pointer.
*
* @param anim The animation to update.
* @param delta The time to advance the animation by, in seconds.
*/
void animationUpdate(animation_t *anim, float_t delta);
/**
* Resets the animation to the beginning, clearing Started and Finished flags.
*
* @param anim The animation to reset.
*/
void animationReset(animation_t *anim);
/**
* Returns true if the animation has finished (i.e. reached the end of its
* duration and is not looping).
*
* @param anim The animation to check.
* @return true if the animation has finished, false otherwise.
*/
bool_t animationIsFinished(const animation_t *anim);
/**
* Returns true if the animation has been started (i.e. animationUpdate has
* been called at least once).
*
* @param anim The animation to check.
* @return true if the animation has been started, false otherwise.
*/
bool_t animationIsStarted(const animation_t *anim);
/**
* Returns true if the animation is set to loop.
*
* @param anim The animation to check.
* @return true if the animation is set to loop, false otherwise.
*/
bool_t animationIsLooping(const animation_t *anim);
/**
* Gets the total duration of the animation (based on the keyframes).
*
* @param anim The animation to get the duration of.
* @return The total duration of the animation, in seconds.
*/
float_t animationGetDuration(const animation_t *anim);
float_t animationGetValue(animation_t *anim, const float_t time);
-55
View File
@@ -1,55 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "animationproperty.h"
#include "assert/assert.h"
void animationPropertyAddKeyframe(
animationproperty_t *prop,
const float_t time,
const float_t value,
const easingtype_t easing
) {
assertNotNull(prop, "Property cannot be null");
assertTrue(
prop->keyframeCount < ANIMATION_PROPERTY_KEYFRAME_COUNT_MAX,
"Too many keyframes added to property"
);
assertTrue(time >= 0.0f, "Keyframe time cannot be negative");
keyframe_t *frame = &prop->keyframes[prop->keyframeCount++];
frame->time = time;
frame->value = value;
frame->easing = easing;
}
float_t animationPropertyGetValue(
const animationproperty_t *prop,
const float_t time
) {
assertNotNull(prop, "Property cannot be null");
assertTrue(time >= 0.0f, "Time cannot be negative");
if(prop->keyframeCount == 0) return 0.0f;
uint8_t last = prop->keyframeCount - 1;
if(prop->keyframeCount == 1) return prop->keyframes[0].value;
if(time <= prop->keyframes[0].time) return prop->keyframes[0].value;
if(time >= prop->keyframes[last].time) return prop->keyframes[last].value;
for(uint8_t i = 0; i < last; i++) {
const keyframe_t *a = &prop->keyframes[i];
const keyframe_t *b = &prop->keyframes[i + 1];
if(time < a->time || time >= b->time) continue;
float_t t = (time - a->time) / (b->time - a->time);
t = easingApply(a->easing, t);
return a->value + (b->value - a->value) * t;
}
return prop->keyframes[last].value;
}
-45
View File
@@ -1,45 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "keyframe.h"
#define ANIMATION_PROPERTY_KEYFRAME_COUNT_MAX 16
typedef struct {
keyframe_t keyframes[ANIMATION_PROPERTY_KEYFRAME_COUNT_MAX];
uint8_t keyframeCount;
float_t *target;
} animationproperty_t;
/**
* Appends a keyframe to a property. Keyframes must be added in ascending time
* order. Updates the animation's duration.
*
* @param property The property to add the keyframe to.
* @param time The time of the keyframe, in seconds.
* @param value The value of the keyframe.
* @param easing The easing type to use when interpolating to the next keyframe.
*/
void animationPropertyAddKeyframe(
animationproperty_t *property,
const float_t time,
const float_t value,
const easingtype_t easing
);
/**
* Gets the property's value at a given time.
*
* @param prop The property to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the property at the given time.
*/
float_t animationPropertyGetValue(
const animationproperty_t *prop,
const float_t time
);
+4 -4
View File
@@ -5,7 +5,7 @@
#include "easing.h"
#include "assert/assert.h"
#include <math.h>
#include "util/math.h"
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
easingLinear,
@@ -36,15 +36,15 @@ float_t easingLinear(const float_t t) {
}
float_t easingInSine(const float_t t) {
return 1.0f - cosf(t * EASING_PI * 0.5f);
return 1.0f - cosf(t * MATH_PI * 0.5f);
}
float_t easingOutSine(const float_t t) {
return sinf(t * EASING_PI * 0.5f);
return sinf(t * MATH_PI * 0.5f);
}
float_t easingInOutSine(const float_t t) {
return -(cosf(EASING_PI * t) - 1.0f) * 0.5f;
return -(cosf(MATH_PI * t) - 1.0f) * 0.5f;
}
float_t easingInQuad(const float_t t) {
+60 -2
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -8,8 +8,19 @@
#include "assert.h"
#include "log/log.h"
#include "util/string.h"
#include "util/memory.h"
#ifdef DUSK_THREAD_PTHREAD
pthread_t ASSERT_MAIN_THREAD_ID = 0;
#endif
#ifndef DUSK_ASSERTIONS_FAKED
void assertInit(void) {
#ifdef DUSK_THREAD_PTHREAD
ASSERT_MAIN_THREAD_ID = pthread_self();
#endif
}
#ifdef DUSK_TEST_ASSERT
void assertTrueImpl(
const char *file,
@@ -25,6 +36,25 @@
);
}
#else
#ifdef DUSK_BACKTRACE
#include <execinfo.h>
#include <stdlib.h>
static void assertLogBacktrace(void) {
void *frames[64];
int count = backtrace(frames, 64);
char **symbols = backtrace_symbols(frames, count);
memoryTrack(symbols);
logError("Stack trace:\n");
if(symbols) {
for(int i = 0; i < count; i++) {
logError(" %s\n", symbols[i]);
}
memoryFree(symbols);
}
}
#endif
void assertTrueImpl(
const char *file,
const int32_t line,
@@ -33,11 +63,14 @@
) {
if(x != true) {
logError(
"Assertion Failed in %s:%i\n\n%s\n",
"Assertion Failed in %s:%i\n\n%s\n\n",
file,
line,
message
);
#ifdef DUSK_BACKTRACE
assertLogBacktrace();
#endif
abort();
}
}
@@ -109,4 +142,29 @@
) {
assertTrueImpl(file, line, stringCompare(a, b) == 0, message);
}
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() == ASSERT_MAIN_THREAD_ID, message
);
#endif
}
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() != ASSERT_MAIN_THREAD_ID, message
);
#endif
}
#endif
+57 -2
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -16,7 +16,18 @@
#endif
#endif
#ifdef DUSK_THREAD_PTHREAD
#include "thread/thread.h"
extern pthread_t ASSERT_MAIN_THREAD_ID;
#endif
#ifndef DUSK_ASSERTIONS_FAKED
/**
* Initializes the assert system. Must be the very first call in engine
* startup.
*/
void assertInit(void);
/**
* Assert a given value to be true.
*
@@ -121,6 +132,28 @@
const char *message
);
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/**
* Asserts a given value to be true.
*
@@ -205,8 +238,28 @@
#define assertStringEqual(a, b, message) \
assertStringEqualImpl(__FILE__, __LINE__, a, b, message)
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertIsMainThread(message) \
assertIsMainThreadImpl(__FILE__, __LINE__, message)
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertNotMainThread(message) \
assertNotMainThreadImpl(__FILE__, __LINE__, message)
#else
// If assertions are faked, we define the macros to do nothing.
#define assertInit() ((void)0)
#define assertMainThreadInit() ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#define assertTrue(x, message) ((void)0)
#define assertFalse(x, message) ((void)0)
#define assertUnreachable(message) ((void)0)
@@ -215,11 +268,13 @@
#define assertDeprecated(message) ((void)0)
#define assertStrLenMax(str, len, message) ((void)0)
#define assertStrLenMin(str, len, message) ((void)0)
#define assertStringEqual(a, b, message) ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#endif
// Static Assertions
#define assertStructSize(struct, size) \
_Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size)
+1
View File
@@ -7,6 +7,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
asset.c
assetbatch.c
assetfile.c
)
+401 -13
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -11,15 +11,23 @@
#include "assert/assert.h"
#include "engine/engine.h"
#include "util/string.h"
#include "console/console.h"
#include <unistd.h>
asset_t ASSET;
errorret_t assetInit(void) {
memoryZero(&ASSET, sizeof(asset_t));
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
// assetInitPlatform must either define ASSET.zip or throw an error.
errorChain(assetInitPlatform());
assertNotNull(ASSET.zip, "Asset zip null without error.");
threadInit(&ASSET.loadThread, assetUpdateAsync);
threadStart(&ASSET.loadThread);
errorOk();
}
@@ -32,24 +40,404 @@ bool_t assetFileExists(const char_t *filename) {
return true;
}
errorret_t assetLoad(
const char_t *filename,
assetfileloader_t loader,
void *params,
void *output
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
assertNotNull(output, "Output pointer cannot be NULL.");
assertNotNull(loader, "Asset file loader cannot be NULL.");
// Is there an existing asset?
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(stringEquals(entry->name, name)) {
assertTrue(entry->type == type, "Asset entry type mismatch.");
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assetfile_t file;
errorChain(assetFileInit(&file, filename, params, output));
errorChain(loader(&file));
errorChain(assetFileDispose(&file));
// We did not find one existing, Find first available slot.
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("No available asset entry slots.");
return NULL;
}
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
) {
assertNotNull(outEntries, "Output entries cannot be NULL.");
uint32_t count = 0;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == type) {
outEntries[count++] = entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
return count;
}
errorret_t assetRequireLoaded(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk();
}
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk();
}
errorret_t assetRequireDisposed(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->type == ASSET_LOADER_TYPE_NULL) {
errorOk();
}
if(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
entry->state == ASSET_ENTRY_STATE_ERROR
) {
errorOk();
}
assertTrue(
entry->refs.count == 0,
"Cannot require disposal of an entry with active references."
);
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->type != ASSET_LOADER_TYPE_NULL) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk();
}
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assetentry_t *entry = assetGetEntry(name, type, input);
assetEntryLock(entry);
return entry;
}
void assetUnlock(const char_t *name) {
assertNotNull(name, "Name cannot be NULL.");
assetentry_t *entry = ASSET.entries;
do {
if(
entry->type != ASSET_LOADER_TYPE_NULL &&
stringEquals(entry->name, name)
) {
assetEntryUnlock(entry);
return;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("Asset entry not found for unlock.");
}
void assetUnlockEntry(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assetEntryUnlock(entry);
}
errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread.");
// Determine how many available loading slots we have.
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
uint8_t availableLoadingCount = 0;
assetloading_t *loading = ASSET.loading;
assetentry_t *entry;
do {
// We only care about NULL entry references. Nothing async touches this so
// it's fine to use raw here.
if(loading->entry != NULL) {
loading++;
continue;
}
availableLoading[availableLoadingCount++] = loading;
loading++;
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Now we can check for pending asset entries, we can't do anything if there
// is no available slots though.
if(availableLoadingCount > 0) {
entry = ASSET.entries;
do {
// Is this asset "ready to start loading" ?
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
// We only care about assets not started.
if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) {
entry++;
continue;
}
// Pop a loading slot for this asset entry.
loading = availableLoading[--availableLoadingCount];
// Start loading this asset.
assetEntryStartLoading(entry, loading);
entry++;
// Did we run out of loading slots?
if(availableLoadingCount == 0) {
break;
}
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
}
// Now walk over all the loading slots and see what needs to be done.
loading = ASSET.loading;
do {
// Is the loading slot in use? Entry can only be modified synchronously.
if(loading->entry == NULL) {
loading++;
continue;
}
// Lock the loading slot. This will prevent any async modifications.
threadMutexLock(&loading->mutex);
// Check the state of the entry.
switch(loading->entry->state) {
// This thing is pending synchronous loading.
case ASSET_ENTRY_STATE_PENDING_SYNC:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
// Unlock before calling loadSync. The sync loader may re-enter
// assetUpdate (e.g. a script loading another asset), and the async
// thread never touches LOADING_SYNC entries, so this is safe.
threadMutexUnlock(&loading->mutex);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading)
);
// After a sync load, these are the only valid states.
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_LOADED ||
loading->entry->state == ASSET_ENTRY_STATE_ERROR ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_SYNC ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_ASYNC,
"Loader did not set entry state to loaded or error on finished load."
);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
eventInvoke(&loading->entry->onLoaded, loading->entry);
}
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_SYNC:
// A re-entrant assetUpdate call (e.g. from a script loading another
// asset) will see this entry mid-sync-load. Skip it.
threadMutexUnlock(&loading->mutex);
loading++;
continue;
// Done loading, we can just free it up.
case ASSET_ENTRY_STATE_LOADED:
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_ERROR: {
assetentry_t *errEntry = loading->entry;
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
eventInvoke(&errEntry->onError, errEntry);
errorThrow("Failed to load asset asynchronously.");
break;
}
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap unused entries.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->refs.count > 0) {
entry++;
continue;
}
consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorOk();
}
void assetUpdateAsync(thread_t *thread) {
assertNotMainThread("assetUpdateAsync must not run on the main thread.");
while(!threadShouldStop(thread)) {
// Walk over each asset
assetloading_t *loading;
loading = ASSET.loading;
do {
threadMutexLock(&loading->mutex);
if(loading->entry == NULL) {
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
switch(loading->entry->state) {
case ASSET_ENTRY_STATE_PENDING_ASYNC:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC;
assertNotNull(
ASSET_LOADER_CALLBACKS[loading->type].loadAsync,
"Loader does not support async loading."
);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading)
);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
}
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_ASYNC:
assertUnreachable(
"Entry is in a pending async state still?"
);
break;
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
if(threadShouldStop(thread)) break;
usleep(1000);
}
}
errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Free any script read-buffers left behind by an in-flight async load
// that was interrupted before the sync eval phase ran.
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
assetloading_t *loading = &ASSET.loading[i];
if(
loading->entry != NULL &&
loading->type == ASSET_LOADER_TYPE_SCRIPT &&
loading->loading.script.buffer != NULL
) {
memoryFree(loading->loading.script.buffer);
loading->loading.script.buffer = NULL;
}
threadMutexDispose(&loading->mutex);
}
// Dispose every non-null entry so type-specific dispose callbacks
// (e.g. assetScriptDispose freeing jerry values) run before the
// scripting engine is torn down.
assetentry_t *entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
errorChain(assetEntryDispose(entry));
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// Cleanup zip file.
if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) {
errorThrow("Failed to close asset zip archive.");
+101 -12
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -9,6 +9,9 @@
#include "error/error.h"
#include "asset/assetplatform.h"
#include "assetfile.h"
#include "thread/thread.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloading.h"
#ifndef assetInitPlatform
#error "Platform must define assetInitPlatform function."
@@ -20,15 +23,27 @@
#define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 4
#define ASSET_ENTRY_COUNT_MAX 128
typedef struct asset_s {
zip_t *zip;
assetplatform_t platform;
// Background loading thread.
thread_t loadThread;
// Assets that are mid loading.
assetloading_t loading[ASSET_LOADING_COUNT_MAX];
assetentry_t entries[ASSET_ENTRY_COUNT_MAX];
} asset_t;
extern asset_t ASSET;
/**
* Initializes the asset system.
*
* @return An error code if the asset system could not be initialized properly.
*/
errorret_t assetInit(void);
@@ -41,21 +56,95 @@ errorret_t assetInit(void);
bool_t assetFileExists(const char_t *filename);
/**
* Loads an asset by its filename,
* Gets, or creates, a new asset entry. Internal - prefer assetLock.
*
* @param filename The filename of the asset to retrieve.
* @param loader Loader to use for loading the asset file.
* @param params Parameters to pass to the loader.
* @param output The output pointer to store the loaded asset data.
* @return An error code if the asset could not be loaded.
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
*/
errorret_t assetLoad(
const char_t *filename,
assetfileloader_t loader,
void *params,
void *output
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Gets all asset entries of a given type.
*
* @param outEntries Output array to write the entries to.
* @param type Type of the asset entries to get.
* @return The number of entries written to outEntries.
*/
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
);
/**
* Gets, creates, and locks an asset entry. The asset will begin loading on
* the next assetUpdate. Call assetUnlock when done to allow the entry to be
* reclaimed.
*
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
* @return The locked asset entry.
*/
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Releases a lock on an asset entry by name. When all locks are released the
* entry will be reclaimed at the start of the next assetUpdate.
*
* @param name Filename of the asset to unlock.
*/
void assetUnlock(const char_t *name);
/**
* Releases a lock on an asset entry by pointer. When all locks are released
* the entry will be reclaimed at the start of the next assetUpdate.
*
* @param entry The asset entry to unlock.
*/
void assetUnlockEntry(assetentry_t *entry);
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
*
* @param entry The asset entry to require.
* @return An error code if the asset entry could not be loaded properly.
*/
errorret_t assetRequireLoaded(assetentry_t *entry);
/**
* Requires an asset entry to be disposed. This will block until the asset entry
* is fully disposed.
*
* @param entry The asset entry to require disposal of.
* @return An error code if the asset entry could not be disposed properly.
*/
errorret_t assetRequireDisposed(assetentry_t *entry);
/**
* Updates the asset system.
*
* @return An error code if the asset system could not be updated properly.
*/
errorret_t assetUpdate(void);
/**
* Starts the background asset loading thread. The thread runs assetUpdate
* in a loop with a short sleep until stopped.
*
* @param thread The thread runner.
*/
void assetUpdateAsync(thread_t *thread);
/**
* Disposes/cleans up the asset system.
*
+165
View File
@@ -0,0 +1,165 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetbatch.h"
#include "asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <unistd.h>
void assetBatchInit(
assetbatch_t *batch,
const uint16_t count,
const assetbatchdesc_t *descs
) {
assertNotNull(batch, "Batch cannot be NULL.");
assertNotNull(descs, "Descs cannot be NULL.");
assertTrue(count > 0, "Count must be greater than 0.");
assertTrue(
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
);
memoryZero(batch, sizeof(assetbatch_t));
batch->count = count;
eventInit(
&batch->onLoaded,
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryLoaded,
batch->onEntryLoadedCallbacks,
batch->onEntryLoadedUsers,
ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onError,
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryError,
batch->onEntryErrorCallbacks,
batch->onEntryErrorUsers,
ASSET_BATCH_EVENT_MAX
);
for(uint16_t i = 0; i < count; i++) {
batch->inputs[i] = descs[i].input;
batch->entries[i] = assetLock(
descs[i].path, descs[i].type, &batch->inputs[i]
);
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
// Already loaded (cached) - count it now, no subscription needed.
batch->loadedCount++;
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
batch->errorCount++;
} else {
eventSubscribe(
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
);
eventSubscribe(
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
);
}
}
}
void assetBatchLock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryLock(batch->entries[i]);
}
}
void assetBatchUnlock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryUnlock(batch->entries[i]);
}
}
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
}
return true;
}
bool_t assetBatchHasError(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
}
return false;
}
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
bool_t allDone;
do {
allDone = true;
for(uint16_t i = 0; i < batch->count; i++) {
const assetentrystate_t state = batch->entries[i]->state;
if(state == ASSET_ENTRY_STATE_ERROR) {
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
}
if(state != ASSET_ENTRY_STATE_LOADED) {
allDone = false;
}
}
if(!allDone) {
usleep(1000);
errorChain(assetUpdate());
}
} while(!allDone);
errorOk();
}
void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]) {
// Unsubscribe while we still hold a lock so the entry is live.
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
assetUnlockEntry(batch->entries[i]);
}
}
memoryZero(batch, sizeof(assetbatch_t));
}
void assetBatchEntryOnLoadedCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->loadedCount++;
eventInvoke(&batch->onEntryLoaded, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
if(batch->errorCount == 0) {
eventInvoke(&batch->onLoaded, batch);
} else {
eventInvoke(&batch->onError, batch);
}
}
}
void assetBatchEntryOnErrorCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->errorCount++;
eventInvoke(&batch->onEntryError, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
eventInvoke(&batch->onError, batch);
}
}
+124
View File
@@ -0,0 +1,124 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "event/event.h"
#define ASSET_BATCH_COUNT_MAX 64
#define ASSET_BATCH_EVENT_MAX 4
typedef struct {
const char_t *path;
assetloadertype_t type;
assetloaderinput_t input;
} assetbatchdesc_t;
typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count;
uint16_t loadedCount;
uint16_t errorCount;
/** Fires once when every entry loaded. params = assetbatch_t * */
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry loads. params = assetentry_t * */
event_t onEntryLoaded;
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry errors. params = assetentry_t * */
event_t onEntryError;
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
} assetbatch_t;
/**
* Initialises the batch from an array of descriptors. Each entry is locked
* and queued for loading immediately.
*
* @param batch Batch to initialise.
* @param descs Array of entry descriptors (need not outlive this call).
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
*/
void assetBatchInit(
assetbatch_t *batch,
uint16_t count,
const assetbatchdesc_t *descs
);
/**
* Acquires one additional lock on every entry in the batch.
*
* @param batch Batch to lock.
*/
void assetBatchLock(assetbatch_t *batch);
/**
* Releases one lock from every entry in the batch. When an entry's lock
* count reaches zero it will be reaped on the next assetUpdate.
*
* @param batch Batch to unlock.
*/
void assetBatchUnlock(assetbatch_t *batch);
/**
* Returns true if every entry in the batch has finished loading.
*
* @param batch Batch to query.
*/
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
/**
* Returns true if any entry in the batch is in an error state.
*
* @param batch Batch to query.
*/
bool_t assetBatchHasError(const assetbatch_t *batch);
/**
* Blocks until every entry is loaded. Returns an error if any entry fails.
*
* @param batch Batch to wait on.
*/
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
/**
* Releases the batch's lock on every entry and clears the batch. After this
* call the batch struct may be reused with assetBatchInit.
*
* @param batch Batch to dispose.
*/
void assetBatchDispose(assetbatch_t *batch);
/**
* Event trampoline invoked when a batch entry finishes loading.
* Increments the loaded counter and fires batch-level events.
*
* @param params The loaded assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnLoadedCb(void *params, void *user);
/**
* Event trampoline invoked when a batch entry fails to load.
* Increments the error counter and fires batch-level events.
*
* @param params The errored assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnErrorCb(void *params, void *user);
+45
View File
@@ -117,6 +117,51 @@ errorret_t assetFileDispose(assetfile_t *file) {
errorOk();
}
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(outBuffer, "outBuffer cannot be NULL.");
assertNotNull(outSize, "outSize cannot be NULL.");
assertTrue(
file->size > 0,
"Asset file has no size; call assetFileInit first."
);
// File should be closed currently.
assertNull(file->zipFile, "Asset file must be closed before reading entire.");
// Open file
errorret_t ret = assetFileOpen(file);
if(errorIsNotOk(ret)) {
errorChain(ret);
}
// Set output.
size_t size = (size_t)file->size;
uint8_t *buffer = (uint8_t *)memoryAllocate(size);
// Read entire file.
ret = assetFileRead(file, buffer, size);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
// Close the file.
ret = assetFileClose(file);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
*outBuffer = buffer;
*outSize = size;
errorOk();
}
// Line Reader;
void assetFileLineReaderInit(
assetfilelinereader_t *reader,
+16
View File
@@ -15,6 +15,7 @@ typedef struct assetfile_s assetfile_t;
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
// Describes a file not yet loaded.
typedef struct assetfile_s {
char_t filename[ASSET_FILE_NAME_MAX];
void *params;
@@ -91,6 +92,21 @@ errorret_t assetFileClose(assetfile_t *file);
*/
errorret_t assetFileDispose(assetfile_t *file);
/**
* Reads the entire contents of the asset file into a newly allocated buffer.
* The caller is responsible for freeing the buffer with memoryFree.
*
* @param file The asset file to read. Must be initialized but not open.
* @param outBuffer Receives a pointer to the allocated buffer.
* @param outSize Receives the number of bytes written to the buffer.
* @return An error code if the file could not be read.
*/
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
);
typedef struct {
assetfile_t *file;
uint8_t *readBuffer;
+7
View File
@@ -4,8 +4,15 @@
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetentry.c
assetloader.c
)
# Subdirs
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(script)
+104
View File
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetentry.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assertNotNull(entry, "Entry cannot be NULL");
assertStrLenMin(name, 1, "Name cannot be empty");
assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long");
assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
if(input) {
entry->inputData = *input;
entry->input = &entry->inputData;
} else {
memoryZero(&entry->inputData, sizeof(assetloaderinput_t));
entry->input = NULL;
}
refInit(&entry->refs, entry, NULL, NULL, NULL);
eventInit(
&entry->onLoaded,
entry->onLoadedCallbacks, entry->onLoadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onUnloaded,
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onError,
entry->onErrorCallbacks, entry->onErrorUsers,
ASSET_ENTRY_EVENT_MAX
);
}
void assetEntryLock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refLock(&entry->refs);
}
void assetEntryUnlock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refUnlock(&entry->refs);
}
void assetEntryStartLoading(
assetentry_t *entry,
assetloading_t *loading
) {
assertNotNull(entry, "Entry cannot be NULL");
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED,
"Can only start loading from NOT_STARTED state."
);
assertIsMainThread("Must be called from the main thread.");
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
memoryZero(&loading->loading, sizeof(assetloaderloading_t));
loading->type = entry->type;
loading->entry = entry;
// At this point the asset manager will manage this thing's loading
}
errorret_t assetEntryDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
assertTrue(
entry->refs.count == 0,
"Asset entry still refed at dispose time."
);
eventInvoke(&entry->onUnloaded, entry);
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t));
errorOk();
}
+112
View File
@@ -0,0 +1,112 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetloading.h"
#include "event/event.h"
#include "util/ref.h"
typedef enum {
ASSET_ENTRY_STATE_NOT_STARTED,
ASSET_ENTRY_STATE_PENDING_ASYNC,
ASSET_ENTRY_STATE_LOADING_ASYNC,
ASSET_ENTRY_STATE_PENDING_SYNC,
ASSET_ENTRY_STATE_LOADING_SYNC,
ASSET_ENTRY_STATE_LOADED,
ASSET_ENTRY_STATE_ERROR
} assetentrystate_t;
/** Maximum number of subscribers for each per-entry event. */
#define ASSET_ENTRY_EVENT_MAX 2
typedef struct assetentry_s assetentry_t;
struct assetentry_s {
char_t name[ASSET_FILE_NAME_MAX];
assetloadertype_t type;
assetloaderoutput_t data;
assetentrystate_t state;
ref_t refs;
assetloaderinput_t *input;
assetloaderinput_t inputData;
/**
* Fired once when loading completes successfully (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
* The asset data is still accessible when the callback runs.
* Always invoked on the main thread.
*/
event_t onUnloaded;
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when loading fails (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
};
/**
* Initializes an asset entry with the given name and type. This does not load
* the asset.
*
* @param entry The asset entry to initialize.
* @param name The name of the asset, used as a key for loading and caching.
* @param type The type of asset this entry represents.
* @param input Data that will be passed to the loader about how it should load.
*/
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Locks an asset entry, preventing it from being freed until it is unlocked.
*
* @param entry The asset entry to lock.
*/
void assetEntryLock(assetentry_t *entry);
/**
* Unlocks an asset entry, allowing it to be freed if there are no more locks.
*
* @param entry The asset entry to unlock.
*/
void assetEntryUnlock(assetentry_t *entry);
/**
* Starts loading the given asset entry using an assetloading slot. This will
* be called by the asset manager when it deems it's a good time to begin the
* loading of this asset entry.
*
* Currently we return the error but in future this will not be returned.
*
* @param entry The asset entry to start loading.
* @param loading The assetloading slot to use for loading this asset entry.
* @return Any error that occurs during loading.
*/
void assetEntryStartLoading(assetentry_t *entry, assetloading_t *loading);
/**
* Disposes an asset entry, freeing any resources it holds.
* Fires the onUnloaded event before releasing asset data.
*
* @param entry The asset entry to dispose.
* @return Any error that occurs during disposal.
*/
errorret_t assetEntryDispose(assetentry_t *entry);
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetloader.h"
assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
[ASSET_LOADER_TYPE_NULL] = { 0 },
[ASSET_LOADER_TYPE_MESH] = {
.loadSync = assetMeshLoaderSync,
.loadAsync = assetMeshLoaderAsync,
.dispose = assetMeshDispose
},
[ASSET_LOADER_TYPE_TEXTURE] = {
.loadSync = assetTextureLoaderSync,
.loadAsync = assetTextureLoaderAsync,
.dispose = assetTextureDispose
},
[ASSET_LOADER_TYPE_TILESET] = {
.loadSync = assetTilesetLoaderSync,
.loadAsync = assetTilesetLoaderAsync,
.dispose = assetTilesetDispose
},
[ASSET_LOADER_TYPE_LOCALE] = {
.loadSync = assetLocaleLoaderSync,
.loadAsync = assetLocaleLoaderAsync,
.dispose = assetLocaleDispose
},
[ASSET_LOADER_TYPE_JSON] = {
.loadSync = assetJsonLoaderSync,
.loadAsync = assetJsonLoaderAsync,
.dispose = assetJsonDispose
},
[ASSET_LOADER_TYPE_SCRIPT] = {
.loadSync = assetScriptLoaderSync,
.loadAsync = assetScriptLoaderAsync,
.dispose = assetScriptDispose
},
};
+96
View File
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/display/assetmeshloader.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/script/assetscriptloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_MESH,
ASSET_LOADER_TYPE_TEXTURE,
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_SCRIPT,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
typedef union {
assetmeshloaderinput_t mesh;
assettextureloaderinput_t texture;
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
assetscriptloaderinput_t script;
} assetloaderinput_t;
typedef union {
assetmeshloaderloading_t mesh;
assettextureloaderloading_t texture;
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
assetscriptloaderloading_t script;
} assetloaderloading_t;
typedef union {
assetmeshoutput_t mesh;
assettextureoutput_t texture;
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
assetscriptoutput_t script;
} assetloaderoutput_t;
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef errorret_t (assetloadersynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderasynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderdisposecallback_t)(assetentry_t *entry);
typedef struct {
assetloadersynccallback_t *loadSync;
assetloaderasynccallback_t *loadAsync;
assetloaderdisposecallback_t *dispose;
} assetloadercallbacks_t;
extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT];
/**
* Shorthand method to both chain an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ret The error return value to check and chain if it's an error.
*/
#define assetLoaderErrorChain(loading, _expr) {\
errorret_t _alec = (_expr); \
if(errorIsNotOk(_alec)) { \
(loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorChain(_alec); \
} \
}
/**
* Shorthand method to both throw an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ... Format string and arguments for the error message.
*/
#define assetLoaderErrorThrow(loading, ...) {\
loading->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorThrow(__VA_ARGS__); \
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "assetloader.h"
#include "asset/assetfile.h"
#include "thread/threadmutex.h"
typedef struct assetentry_s assetentry_t;
typedef struct assetloading_s {
threadmutex_t mutex;
assetloadertype_t type;
assetentry_t *entry;
assetloaderloading_t loading;
} assetloading_t;
typedef errorret_t (assetloadingcallback_t)(assetloading_t *loading);
typedef struct {
assetloadingcallback_t *loadSync;
} assetloadingcallbacks_t;
+114 -116
View File
@@ -5,72 +5,77 @@
* https://opensource.org/licenses/MIT
*/
#include "asset/loader/display/assetmeshloader.h"
#include "asset/asset.h"
#include "assetmeshloader.h"
#include "assert/assert.h"
#include "util/endian.h"
#include "util/memory.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetMeshLoader(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be null");
errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assetmeshoutput_t *output = (assetmeshoutput_t *)file->output;
assertNotNull(output, "Output cannot be null");
assertNotNull(output->outMesh, "Output mesh cannot be null");
assertNotNull(output->outVertices, "Output vertices cannot be null");
if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) {
errorOk();
}
// STL file loading
errorChain(assetFileOpen(file));
assetmeshoutput_t *out = &loading->entry->data.mesh;
assetfile_t *file = &loading->loading.mesh.file;
assetmeshinputaxis_t axis = loading->entry->inputData.mesh;
// Skip the 80 byte header
errorChain(assetFileRead(file, NULL, 80));
if(file->lastRead != 80) errorThrow("Failed to skip STL header");
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
assetLoaderErrorChain(loading, assetFileOpen(file));
// Skip the 80-byte STL header.
assetLoaderErrorChain(loading, assetFileRead(file, NULL, 80));
if(file->lastRead != 80) {
assetLoaderErrorThrow(loading, "Failed to skip STL header.");
}
uint32_t triangleCount;
errorChain(assetFileRead(file, &triangleCount, sizeof(uint32_t)));
if(file->lastRead != sizeof(uint32_t)) errorThrow("Failed read tri count");
// normalize
assetLoaderErrorChain(loading,
assetFileRead(file, &triangleCount, sizeof(uint32_t))
);
if(file->lastRead != sizeof(uint32_t)) {
assetLoaderErrorThrow(loading, "Failed to read tri count");
}
triangleCount = endianLittleToHost32(triangleCount);
// Allocate mesh and vertex data
errorret_t ret;
meshvertex_t *verts = memoryAllocate(
sizeof(meshvertex_t) * triangleCount * 3
);
*output->outVertices = verts;
out->vertices = memoryAllocate(sizeof(meshvertex_t) * triangleCount * 3);
meshvertex_t *verts = out->vertices;
// Read triangle data
errorret_t ret;
for(uint32_t i = 0; i < triangleCount; i++) {
assetmeshstltriangle_t triData;
ret = assetFileRead(file, &triData, sizeof(triData));
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
memoryFree(verts);
errorChain(ret);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
if(file->lastRead != sizeof(triData)) {
memoryFree(verts);
errorThrow("Failed to read triangle data");
out->vertices = NULL;
assetLoaderErrorThrow(loading, "Failed to read triangle data");
}
// Skip normals, we don't use them
// Fix endianess of of data
for(uint8_t j = 0; j < 3; j++) {
#if MESH_ENABLE_COLOR
verts[i * 3 + j].color.r = (uint8_t)(endianLittleToHostFloat(
triData.normal[0]
) * 255.0f);
verts[i * 3 + j].color.g = (uint8_t)(endianLittleToHostFloat(
triData.normal[1]
) * 255.0f);
verts[i * 3 + j].color.b = (uint8_t)(endianLittleToHostFloat(
triData.normal[2]
) * 255.0f);
verts[i * 3 + j].color.r = (
(uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f)
);
verts[i * 3 + j].color.g = (
(uint8_t)(endianLittleToHostFloat(triData.normal[1]) * 255.0f)
);
verts[i * 3 + j].color.b = (
(uint8_t)(endianLittleToHostFloat(triData.normal[2]) * 255.0f)
);
verts[i * 3 + j].color.a = 0xFF;
#endif
verts[i * 3 + j].uv[0] = 0.0f; // No UV data in STL, just set to 0
verts[i * 3 + j].uv[0] = 0.0f;
verts[i * 3 + j].uv[1] = 0.0f;
for(uint8_t k = 0; k < 3; k++) {
@@ -79,104 +84,97 @@ errorret_t assetMeshLoader(assetfile_t *file) {
);
}
switch(output->inputAxis) {
case MESH_INPUT_AXIS_Z_UP:
// Convert Z-Up to Y-Up
{
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
}
switch(axis) {
case MESH_INPUT_AXIS_Z_UP: {
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
break;
case MESH_INPUT_AXIS_X_UP:
// Convert X-Up to Y-Up
{
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = temp;
}
}
case MESH_INPUT_AXIS_X_UP: {
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = temp;
break;
}
case MESH_INPUT_AXIS_Y_DOWN:
// Invert Y axis
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[1];
break;
case MESH_INPUT_AXIS_Z_DOWN:
// Convert Z-Up to Y-Up and invert Y axis
{
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
}
case MESH_INPUT_AXIS_Z_DOWN: {
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
break;
case MESH_INPUT_AXIS_X_DOWN:
// Convert X-Up to Y-Up and invert Y axis
{
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -temp;
}
}
case MESH_INPUT_AXIS_X_DOWN: {
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -temp;
break;
}
case MESH_INPUT_AXIS_Y_UP:
default:
// No covnersion possible / Needed
break;
}
}
}
// Finally, init mesh
ret = meshInit(
output->outMesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
triangleCount * 3,
verts
);
if(ret.code != ERROR_OK) {
memoryFree(verts);
errorChain(ret);
}
ret = assetFileClose(file);
if(ret.code != ERROR_OK) {
errorCatch(errorPrint(meshDispose(output->outMesh)));
if(errorIsNotOk(ret)) {
memoryFree(verts);
errorChain(ret);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
assetFileDispose(file);
loading->loading.mesh.triangleCount = triangleCount;
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_CREATE_MESH;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetMeshLoadToOutput(
const char_t *path,
assetmeshoutput_t *output
) {
assertNotNull(path, "Path cannot be null");
assertNotNull(output, "Output cannot be null");
assertNotNull(output->outMesh, "Output mesh cannot be null");
assertNotNull(output->outVertices, "Output vertices cannot be null");
errorret_t assetMeshLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
return assetLoad(path, &assetMeshLoader, NULL, output);
switch(loading->loading.mesh.state) {
case ASSET_MESH_LOADING_STATE_INITIAL:
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_MESH_LOADING_STATE_CREATE_MESH:
break;
default:
errorOk();
}
assetmeshoutput_t *out = &loading->entry->data.mesh;
assertNotNull(out->vertices, "Mesh vertices should have been loaded by now.");
errorret_t ret = meshInit(
&out->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
loading->loading.mesh.triangleCount * 3,
out->vertices
);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
memoryFree(out->vertices);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetMeshLoad(
const char_t *path,
mesh_t *outMesh,
meshvertex_t **outVertices,
const assetmeshinputaxis_t inputAxis
) {
assertNotNull(path, "Path cannot be null");
assertNotNull(outMesh, "Output mesh cannot be null");
assertNotNull(outVertices, "Output vertices cannot be null");
assetmeshoutput_t output = {
outMesh,
outVertices,
inputAxis
};
return assetMeshLoadToOutput(path, &output);
errorret_t assetMeshDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
errorChain(meshDispose(&entry->data.mesh.mesh));
memoryFree(entry->data.mesh.vertices);
errorOk();
}
+25 -28
View File
@@ -6,12 +6,15 @@
*/
#pragma once
#include "asset/asset.h"
#include "asset/assetfile.h"
#include "display/mesh/mesh.h"
#include "assert/assert.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef enum {
MESH_INPUT_AXIS_Y_UP,// Default
MESH_INPUT_AXIS_Y_UP,
MESH_INPUT_AXIS_Z_UP,
MESH_INPUT_AXIS_X_UP,
@@ -20,10 +23,24 @@ typedef enum {
MESH_INPUT_AXIS_X_DOWN,
} assetmeshinputaxis_t;
typedef assetmeshinputaxis_t assetmeshloaderinput_t;
typedef enum {
ASSET_MESH_LOADING_STATE_INITIAL,
ASSET_MESH_LOADING_STATE_READ_FILE,
ASSET_MESH_LOADING_STATE_CREATE_MESH,
ASSET_MESH_LOADING_STATE_DONE
} assetmeshloadingstate_t;
typedef struct {
mesh_t *outMesh;
meshvertex_t **outVertices;
assetmeshinputaxis_t inputAxis;
assetfile_t file;
assetmeshloadingstate_t state;
uint32_t triangleCount;
} assetmeshloaderloading_t;
typedef struct {
mesh_t mesh;
meshvertex_t *vertices;
} assetmeshoutput_t;
#pragma pack(push, 1)
@@ -36,26 +53,6 @@ typedef struct {
assertStructSize(assetmeshstltriangle_t, 50);
/**
* Loader for mesh assets.
*
* @param file Asset file to load the mesh from.
* @return Any error that occurs during loading.
*/
errorret_t assetMeshLoader(assetfile_t *file);
/**
* Handler for mesh assets.
*
* @param file Asset file to load the mesh from.
* @param outMesh Output mesh to load the data into.
* @param outVertices Output pointer to the vertex data, used for cleanup.
* @param inputAxis The axis orientation of the input mesh data.
* @return Any error that occurs during loading.
*/
errorret_t assetMeshLoad(
const char_t *path,
mesh_t *outMesh,
meshvertex_t **outVertices,
const assetmeshinputaxis_t inputAxis
);
errorret_t assetMeshLoaderAsync(assetloading_t *loading);
errorret_t assetMeshLoaderSync(assetloading_t *loading);
errorret_t assetMeshDispose(assetentry_t *entry);
@@ -11,6 +11,8 @@
#include "stb_image.h"
#include "log/log.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
stbi_io_callbacks ASSET_TEXTURE_STB_CALLBACKS = {
.read = assetTextureReader,
@@ -25,7 +27,7 @@ int assetTextureReader(void *user, char *data, int size) {
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, data, (size_t)size);
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return -1;
}
@@ -38,7 +40,7 @@ void assetTextureSkipper(void *user, int n) {
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, NULL, (size_t)n);
if(ret.code != ERROR_OK) {
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
}
}
@@ -50,66 +52,120 @@ int assetTextureEOF(void *user) {
return file->position >= file->size;
}
errorret_t assetTextureLoader(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->params, "Asset file parameters cannot be NULL.");
assertNotNull(file->output, "Asset file output cannot be NULL.");
errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
assettextureloaderparams_t *p = (assettextureloaderparams_t*)file->params;
assertNotNull(p, "Asset texture loader parameters cannot be NULL.");
// Only care about loading pixels.
if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){
errorOk();
}
// Init the file
assertNull(
loading->loading.texture.data, "Pixels already defined?"
);
assetfile_t *file = &loading->loading.texture.file;
assetLoaderErrorChain(loading, assetFileInit(
file,
loading->entry->name,
NULL,
&loading->entry->data.texture
));
assetLoaderErrorChain(loading, assetFileOpen(file));
// Determine channels
int channelsDesired;
switch(p->format) {
switch(loading->entry->inputData.texture) {
case TEXTURE_FORMAT_RGBA:
channelsDesired = 4;
break;
default:
errorThrow("Bad texture format.");
assetLoaderErrorThrow(loading, "Bad texture format.");
}
int width, height, channels;
errorChain(assetFileOpen(file));
uint8_t *data = stbi_load_from_callbacks(
// Load image pixels.
loading->loading.texture.data = stbi_load_from_callbacks(
&ASSET_TEXTURE_STB_CALLBACKS,
file,
&width,
&height,
&channels,
&loading->loading.texture.width,
&loading->loading.texture.height,
&loading->loading.texture.channels,
channelsDesired
);
errorChain(assetFileClose(file));
if(data == NULL) {
// Close out the file.
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
// Ensure we loaded correctly.
if(loading->loading.texture.data == NULL) {
const char_t *errorStr = stbi_failure_reason();
errorThrow("Failed to load texture from file %s.", errorStr);
assetLoaderErrorThrow(
loading, "Failed to load texture from file %s.", errorStr
);
}
// Fixes a specific bug probably with Dolphin but for now just assuming endian
if(!isHostLittleEndian()) {
stbi__vertical_flip(data, width, height, channelsDesired);
stbi__vertical_flip(
loading->loading.texture.data,
loading->loading.texture.width,
loading->loading.texture.height,
loading->loading.texture.channels
);
}
errorChain(textureInit(
(texture_t*)file->output,
(int32_t)width, (int32_t)height,
p->format,
(texturedata_t){
.rgbaColors = (color_t*)data
}
));
stbi_image_free(data);
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetTextureLoad(
const char_t *path,
texture_t *out,
const textureformat_t format
) {
assettextureloaderparams_t params = {
.format = format
};
return assetLoad(path, assetTextureLoader, &params, out);
errorret_t assetTextureLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.texture.state) {
case ASSET_TEXTURE_LOADING_STATE_INITIAL:
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE:
break;
default:
errorOk();
}
// Create the texture.
assertNotNull(
loading->loading.texture.data, "Pixels should have been loaded by now."
);
assetLoaderErrorChain(loading, textureInit(
(texture_t*)&loading->entry->data.texture,
loading->loading.texture.width,
loading->loading.texture.height,
loading->entry->inputData.texture,
(texturedata_t){
.rgbaColors = (color_t*)loading->loading.texture.data
}
));
// Free the pixels.
stbi_image_free(loading->loading.texture.data);
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetTextureDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
return textureDispose(&entry->data.texture);
}
@@ -6,12 +6,29 @@
*/
#pragma once
#include "asset/asset.h"
#include "asset/assetfile.h"
#include "display/texture/texture.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef textureformat_t assettextureloaderinput_t;
typedef enum {
ASSET_TEXTURE_LOADING_STATE_INITIAL,
ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS,
ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE,
ASSET_TEXTURE_LOADING_STATE_DONE
} assettextureloadingstate_t;
typedef struct {
textureformat_t format;
} assettextureloaderparams_t;
assetfile_t file;
assettextureloadingstate_t state;
int channels, width, height;
uint8_t *data;
} assettextureloaderloading_t;
typedef texture_t assettextureoutput_t;
/**
* STB image read callback for asset files.
@@ -23,28 +40,42 @@ typedef struct {
*/
int assetTextureReader(void *user, char *data, int size);
/**
* STB image skip callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param n Number of bytes to skip in the file.
*/
void assetTextureSkipper(void *user, int n);
/**
* STB image EOF callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @return Non-zero if end of file has been reached, zero otherwise.
*/
int assetTextureEOF(void *user);
/**
* Handler for texture assets.
* Synchronous loader for texture assets.
*
* @param file Asset file to load the texture from.
* @return Any error that occurs during loading.
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoader(assetfile_t *file);
errorret_t assetTextureLoaderAsync(assetloading_t *loading);
/**
* Loads a texture from the specified path.
* Synchronous loader for texture assets.
*
* @param path Path to the texture asset.
* @param out Output texture to load into.
* @param format Format of the texture to load.
* @return Any error that occurs during loading.
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoad(
const char_t *path,
texture_t *out,
const textureformat_t format
);
errorret_t assetTextureLoaderSync(assetloading_t *loading);
/**
* Disposer for texture assets.
*
* @param entry Asset entry containing the texture to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTextureDispose(assetentry_t *entry);
@@ -9,72 +9,116 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetTilesetLoader(assetfile_t *file) {
assertNotNull(file, "Asset file pointer for tileset loader is null.");
errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
tileset_t *out = (tileset_t *)file->output;
assertNotNull(out, "Output pointer for tileset loader is null.");
uint8_t *entire = memoryAllocate(file->size);
errorChain(assetFileOpen(file));
errorChain(assetFileRead(file, entire, file->size));
errorChain(assetFileClose(file));
assertTrue(file->lastRead == file->size, "Failed to read entire file.");
if(
entire[0] != 'D' ||
entire[1] != 'T' ||
entire[2] != 'F'
) {
errorThrow("Invalid tileset header");
if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) {
errorOk();
}
if(entire[3] != 0x00) {
errorThrow("Unsupported tileset version");
}
assertNull(loading->loading.tileset.data, "Data already defined?");
// Fix endianness
assetfile_t *file = &loading->loading.tileset.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
out->tileWidth = endianLittleToHost16(*(uint16_t *)(entire + 4));
out->tileHeight = endianLittleToHost16(*(uint16_t *)(entire + 6));
out->columns = endianLittleToHost16(*(uint16_t *)(entire + 8));
out->rows = endianLittleToHost16(*(uint16_t *)(entire + 10));
// out->right = endianLittleToHost16(*(uint16_t *)(entire + 12));
// out->bottom = endianLittleToHost16(*(uint16_t *)(entire + 14));
if(out->tileWidth == 0) {
errorThrow("Tile width cannot be 0");
}
if(out->tileHeight == 0) {
errorThrow("Tile height cannot be 0");
}
if(out->columns == 0) {
errorThrow("Column count cannot be 0");
}
if(out->rows == 0) {
errorThrow("Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(entire + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(entire + 20));
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
errorThrow("Invalid v0 value in tileset");
}
// Setup tileset
out->tileCount = out->columns * out->rows;
memoryFree(entire);
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
assertTrue(
file->lastRead == file->size,
"Failed to read entire tileset file."
);
loading->loading.tileset.data = data;
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetTilesetLoad(
const char_t *path,
tileset_t *out
) {
return assetLoad(path, assetTilesetLoader, NULL, out);
errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.tileset.state) {
case ASSET_TILESET_LOADING_STATE_INITIAL:
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TILESET_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *data = loading->loading.tileset.data;
assertNotNull(data, "Tileset data should have been loaded by now.");
tileset_t *out = &loading->entry->data.tileset;
if(data[0] != 'D' || data[1] != 'T' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid tileset header");
}
if(data[3] != 0x00) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Unsupported tileset version");
}
out->tileWidth = endianLittleToHost16(*(uint16_t *)(data + 4));
out->tileHeight = endianLittleToHost16(*(uint16_t *)(data + 6));
out->columns = endianLittleToHost16(*(uint16_t *)(data + 8));
out->rows = endianLittleToHost16(*(uint16_t *)(data + 10));
if(out->tileWidth == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile width cannot be 0");
}
if(out->tileHeight == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile height cannot be 0");
}
if(out->columns == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Column count cannot be 0");
}
if(out->rows == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20));
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
}
out->tileCount = out->columns * out->rows;
memoryFree(data);
loading->loading.tileset.data = NULL;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetTilesetDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
errorOk();
}

Some files were not shown because too many files have changed in this diff Show More